Spring Boot - 利用MDC(Mapped Diagnostic Context)实现轻量级同步/异步日志追踪

news2024/10/5 18:30:09

文章目录

  • Pre
  • 什么是MDC(Mapped Diagnostic Context)
  • Slf4j 和 MDC
  • 基础工程
    • 工程结构
    • POM
    • logback-spring.xml
    • application.yml
    • 同步方式
      • 方式一: 拦截器
        • 自定义日志拦截器
        • 添加拦截器
      • 方式二: 自定义注解 + AOP
        • 自定义注解 TraceLog
        • 切面
      • 测试
    • 异步支持
      • 线程池配置类
      • 自定义线程池任务执行器
      • 线程MDC工具类
      • 模拟业务类@Async
      • 测试
  • 小结

在这里插入图片描述

Pre

每日一博 - ThreadLocal VS InheritableThreadLocal VS TransmittableThreadLocal


什么是MDC(Mapped Diagnostic Context)

MDC(Mapped Diagnostic Context)是一个在日志框架中常用的概念,主要用于在多线程环境中关联和传递一些上下文信息,以便在日志输出中包含这些信息,从而实现更好的日志记录和调试。

在Java中,常见的日志框架如Log4j、Logback和Log4j2都提供了对MDC的支持。

MDC的主要特点包括:

  1. 线程绑定的上下文信息: MDC允许在多线程环境中将上下文信息与线程相关联。可以在应用程序的不同部分设置一些上下文信息,并确保在同一线程中的后续日志记录中能够访问到这些信息。

  2. 适用于跟踪请求或会话: MDC特别适用于跟踪请求或会话相关的信息,如请求ID、会话ID等。通过在请求开始时设置这些信息,并在请求结束时清理它们,可以确保在整个请求处理过程中,日志都包含了相同的上下文信息,方便排查问题。

  3. 日志格式化支持: MDC的值可以通过特殊的占位符在日志输出格式中引用。这样,在日志输出时,可以直接将MDC中的值包含在日志中,从而让日志更具可读性和可跟踪性。

  4. 避免参数传递的复杂性: 使用MDC可以避免在方法调用链中手动传递上下文信息的复杂性。相反,可以在适当的地方将信息设置到MDC中,在日志输出时框架会自动将这些信息包含在日志中。

简而言之,MDC是一个非常有用的工具,可以帮助开发人员在日志中记录和跟踪关键的上下文信息,提高了调试和排查问题的效率。


Slf4j 和 MDC

在这里插入图片描述

**SLF4J(Simple Logging Facade for Java)**是一个日志门面框架,它提供了一种简单的方式来访问各种日志系统,例如Log4j、Logback、java.util.logging等。SLF4J本身并不是一个日志实现,而是提供了统一的接口,开发人员可以通过它来编写日志代码,而不用关心底层日志系统的具体实现。

**MDC(Mapped Diagnostic Context)**是SLF4J的一个功能,用于在日志输出中关联和传递上下文信息。MDC允许开发人员在代码中设置一些上下文信息,例如请求ID、用户ID等,然后在日志输出时将这些信息包含在日志中,以便于跟踪和调试。

SLF4J和MDC之间的关系可以总结如下:

  1. SLF4J提供了MDC的接口: SLF4J允许开发人员通过其API来使用MDC功能。它提供了一些方法,例如MDC.put(key, value)用于设置上下文信息,MDC.get(key)用于获取上下文信息等。

  2. MDC依赖于底层的日志实现: 虽然MDC是SLF4J提供的功能,但其实现是依赖于底层的日志实现的。不同的日志实现,如Logback、Log4j等,都有自己的MDC实现。因此,开发人员需要确保在使用MDC时,底层的日志实现已经正确配置。

  3. MDC提供了与SLF4J日志框架的集成: MDC的设计目的之一是与SLF4J的日志框架集成得很好。这意味着开发人员可以在使用SLF4J编写的日志代码中,轻松地使用MDC功能,从而在日志中记录和跟踪上下文信息。

SLF4J和MDC是紧密相关的,MDC是SLF4J的一个功能,用于在日志输出中传递上下文信息,而SLF4J提供了使用MDC功能的接口。这使得在使用SLF4J编写的日志代码中,可以方便地利用MDC来增强日志的可读性和可追踪性。


基础工程

工程结构

在这里插入图片描述


POM

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
	xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
	<modelVersion>4.0.0</modelVersion>
	<parent>
		<artifactId>boot2</artifactId>
		<groupId>com.artisan</groupId>
		<version>0.0.1-SNAPSHOT</version>
	</parent>

	<groupId>com.artisan</groupId>
	<artifactId>boot-trace</artifactId>
	<version>0.0.1-SNAPSHOT</version>
	<name>boot-trace</name>
	<description>boot-trace</description>

	<properties>
		<java.version>8</java.version>
	</properties>
	<dependencies>

		<dependency>
			<groupId>org.springframework.boot</groupId>
			<artifactId>spring-boot-starter</artifactId>
		</dependency>


		<dependency>
			<groupId>org.springframework.boot</groupId>
			<artifactId>spring-boot-starter-aop</artifactId>
		</dependency>

		<dependency>
			<groupId>org.springframework.boot</groupId>
			<artifactId>spring-boot-starter-web</artifactId>
		</dependency>
		<dependency>
			<groupId>org.springframework.boot</groupId>
			<artifactId>spring-boot-starter-test</artifactId>
			<scope>test</scope>
		</dependency>
		<dependency>
			<groupId>org.springframework.boot</groupId>
			<artifactId>spring-boot-starter-logging</artifactId>
		</dependency>
		<!--lombok配置-->
		<dependency>
			<groupId>org.projectlombok</groupId>
			<artifactId>lombok</artifactId>
		</dependency>

		<!-- ULID-->
		<dependency>
			<groupId>com.github.f4b6a3</groupId>
			<artifactId>ulid-creator</artifactId>
			<version>5.1.0</version>
		</dependency>
		<dependency>
			<groupId>de.huxhorn.sulky</groupId>
			<artifactId>de.huxhorn.sulky.ulid</artifactId>
			<version>8.3.0</version>
		</dependency>


	</dependencies>

	<build>
		<plugins>
			<plugin>
				<groupId>org.springframework.boot</groupId>
				<artifactId>spring-boot-maven-plugin</artifactId>
			</plugin>
		</plugins>
	</build>

</project>



logback-spring.xml

使用SLF4J门面是一个很好的实践,特别是在与logback等日志实现框架集成时。SLF4J提供了一个统一的接口,使得应用代码与具体的日志实现解耦,从而可以轻松地切换和替换底层的日志实现,而无需修改应用代码。

通过使用SLF4J门面,可以在应用程序中使用SLF4J的API编写日志代码,例如Logger接口中的方法,而不用关心底层的日志实现是logback、Log4j还是其他日志框架。这使得代码更具灵活性和可维护性,可以根据需要随时替换底层的日志实现,而不会影响应用程序的其他部分。

对于大多数Java项目来说,使用SLF4J门面和logback作为底层的日志实现是一个非常常见的选择。这样做不仅提供了对日志功能的灵活控制,还能够保持与标准的Java日志习惯的兼容性。同时,SLF4J和logback之间的集成也非常完善,可以充分发挥它们之间的协作优势。

这里我们使用logback ,其他日志组件均可无缝替换。

<?xml version="1.0" encoding="UTF-8"?>
<configuration debug="false">
    <!--日志存储路径-->
    <property name="log" value="D:/log" />
    <!-- 控制台输出 -->
    <appender name="console" class="ch.qos.logback.core.ConsoleAppender">
        <encoder class="ch.qos.logback.classic.encoder.PatternLayoutEncoder">
            <!--输出格式化-->
            <pattern>[%X{TRACE_ID}]  %d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger{50}[%10method,%line] - %msg%n</pattern>
        </encoder>
    </appender>
    <!-- 按天生成日志文件 -->
    <appender name="file" class="ch.qos.logback.core.rolling.RollingFileAppender">
        <rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">
            <!--日志文件名-->
            <FileNamePattern>${log}/%d{yyyy-MM-dd}.log</FileNamePattern>
            <!--保留天数-->
            <MaxHistory>30</MaxHistory>
        </rollingPolicy>
        <encoder class="ch.qos.logback.classic.encoder.PatternLayoutEncoder">
            <pattern>[%X{TRACE_ID}]  %d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger{50} - %msg%n</pattern>
        </encoder>
        <!--日志文件最大的大小-->
        <triggeringPolicy class="ch.qos.logback.core.rolling.SizeBasedTriggeringPolicy">
            <MaxFileSize>10MB</MaxFileSize>
        </triggeringPolicy>
    </appender>

    <!-- 日志输出级别 -->
    <root level="INFO">
        <appender-ref ref="console" />
        <appender-ref ref="file" />
    </root>
</configuration>

application.yml

server:
  port: 9999
logging:
  config: classpath:logback-spring.xml

同步方式

方式一: 拦截器

自定义日志拦截器
package com.artisan.boottrace.interceptor;

import com.artisan.boottrace.utils.TraceIdGenerator;
import org.slf4j.MDC;
import org.springframework.lang.Nullable;
import org.springframework.util.StringUtils;
import org.springframework.web.servlet.HandlerInterceptor;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

/**
 * @author 小工匠
 * @version 1.0
 * @mark: show me the code , change the world
 * <p>
 * MDC(Mapped Diagnostic Context)诊断上下文映射,是@Slf4j提供的一个支持动态打印日志信息的工具
 */
public class TraceLogInterceptor implements HandlerInterceptor {

    // 追踪ID在MDC中的键名
    private static final String TRACE_ID = "TRACE_ID";

    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) {
        // 生成追踪ID,如果客户端传入了追踪ID,则使用客户端传入的追踪ID,否则使用默认的ULID生成
        String tid = TraceIdGenerator.ulid();
        if (StringUtils.hasLength(request.getHeader(TRACE_ID))) {
            tid = request.getHeader(TRACE_ID);
        }
        // 将追踪ID放入MDC中
        MDC.put(TRACE_ID, tid);
        return true;
    }

    @Override
    public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler,
                                @Nullable Exception ex) {
        // 请求处理完成后,从MDC中移除追踪ID
        MDC.remove(TRACE_ID);
    }
}
    

在请求处理前后设置和清理MDC中的追踪ID的请求追踪日志拦截器。

  • preHandle方法中,它从请求头中获取追踪ID,如果不存在则使用默认的ULID生成器生成一个新的追踪ID,并将其放入MDC中。
  • afterCompletion方法中,它简单地移除MDC中的追踪ID,以确保不影响后续请求的日志记录。

添加拦截器
package com.artisan.boottrace.interceptor;

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;

/**
 * @author 小工匠
 * @version 1.0
 * @mark: show me the code , change the world
 */
@Configuration
public class WebConfigurerAdapter implements WebMvcConfigurer {

    /**
     * 注册TraceLogInterceptor拦截器
     */
    @Bean
    public TraceLogInterceptor logInterceptor() {
        return new TraceLogInterceptor();
    }

    /**
     * 添加拦截器到拦截器链
     */
    @Override
    public void addInterceptors(InterceptorRegistry registry) {
        registry.addInterceptor(logInterceptor());
        // 可以具体指定哪些需要拦截,哪些不拦截,其实也可以使用自定义注解更灵活完成
        // .addPathPatterns("/**")
        // .excludePathPatterns("/xxx.yyy");
    }
}
    

这个配置类是WebMvcConfigurer接口的实现类,用于配置拦截器。它注册了TraceLogInterceptor拦截器,并将其添加到拦截器链中。

可以通过addInterceptors方法来指定哪些请求需要被拦截,哪些请求不需要被拦截。通过这种方式,可以灵活地控制拦截器的应用范围,以满足不同的业务需求.


方式二: 自定义注解 + AOP

自定义注解 TraceLog
package com.artisan.boottrace.annotations;

import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

/**
 * @author artisan
 */
@Target({ElementType.METHOD, ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
public @interface TraceLog {
}
切面
package com.artisan.boottrace.aspect;

import com.artisan.boottrace.utils.TraceIdGenerator;
import org.aspectj.lang.annotation.After;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Before;
import org.slf4j.MDC;
import org.springframework.stereotype.Component;

/**
 * @author 小工匠
 * @version 1.0
 * @mark: show me the code , change the world
 */

@Aspect
@Component
public class TraceLogAspect {

    private static final String TRACE_ID = "TRACE_ID";

    @Before("@annotation(com.artisan.boottrace.annotations.TraceLog)")
    public void beforeMethodExecution() {
        String tid = TraceIdGenerator.ulid();
        MDC.put(TRACE_ID, tid);
    }

    @After("@annotation(com.artisan.boottrace.annotations.TraceLog)")
    public void afterMethodExecution() {
        MDC.remove(TRACE_ID);
    }
}


测试

拦截器方式

package com.artisan.boottrace.controller;

import com.artisan.boottrace.service.IArtisanService;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;

/**
 * @author 小工匠
 * @version 1.0
 * @mark: show me the code , change the world
 */

@Slf4j
@RestController
public class ArtisanTestController {

    @Autowired
    private IArtisanService artisanService;

    @GetMapping("/testTrace")
    public String testTrace(@RequestParam("name") String name) throws InterruptedException {
        log.info("testTrace  name={}", name);
        doSomething1();
        // 异步任务
        // artisanService.addArtisan();
        log.info("testTrace Call Over name={}", name);
        return "Hello," + name;
    }

    private void doSomething1() {
        log.info("doSomething1  info  log");
        doSomething2();
        log.error("doSomething1 error log");
    }

    private void doSomething2() {
        log.info("doSomething2  info log");

    }
}
    
    

启动应用,访问:http://127.0.0.1:9999/testTrace?name=artisan

在这里插入图片描述


验证 AOP的方式

在这里插入图片描述

	@TraceLog
    @GetMapping("/testTrace2")
    public String testTraceByAnno(@RequestParam("name") String name) throws InterruptedException {
        log.info("======================");
        log.info("testTraceByAnno  name={}", name);
        doSomething1(); 
        log.info("testTraceByAnno Call Over name={}", name);
        return "Hello," + name;
    }

在这里插入图片描述


异步支持

日常开发中,异步处理必不可少,我们来看看如何实现一个轻量的异步日志追踪吧 。

思路: 将父线程的trackId传递下去给子线程

线程池配置类

package com.artisan.boottrace.config;

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.scheduling.annotation.EnableAsync;

import java.util.concurrent.Executor;

/**
 * @author 小工匠
 * @version 1.0
 * @mark: show me the code , change the world
 * <p>
 * 线程池配置类,用于配置异步任务执行器
 */
@Configuration
@EnableAsync
public class ThreadPoolConfig {
    /**
     * 声明一个线程池
     *
     * @return 执行器
     */
    @Bean("ArtisanExecutor")
    public Executor asyncExecutor() {
        final int cpuSize = Runtime.getRuntime().availableProcessors();
        ArtisanThreadPoolTaskExecutor executor = new ArtisanThreadPoolTaskExecutor();
        executor.setCorePoolSize(cpuSize);
        executor.setMaxPoolSize(2 * cpuSize);
        executor.setQueueCapacity(500);
        executor.setKeepAliveSeconds(60);
        executor.setThreadNamePrefix("asyncArtisan-");
        executor.initialize();
        return executor;
    }
}

用于配置异步任务执行器,声明了一个名为"ArtisanExecutor"的Bean,返回一个自定义的ArtisanThreadPoolTaskExecutor执行器实例。在这个执行器中,配置了线程池的各种参数,如核心线程数、最大线程数、队列容量等。这样就创建了一个具有自定义配置的线程池执行器,用于执行异步任务。


自定义线程池任务执行器

package com.artisan.boottrace.config;

import com.artisan.boottrace.utils.ThreadMdcUtil;
import org.slf4j.MDC;
import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor;

import java.util.concurrent.Callable;
import java.util.concurrent.Future;

/**
 * @author 小工匠
 * @version 1.0
 * @mark: show me the code , change the world
 * <p>
 * 自定义线程池任务执行器,用于在任务执行时传递父线程的MDC上下文信息到子线程中。
 */
public class ArtisanThreadPoolTaskExecutor extends ThreadPoolTaskExecutor {
    public ArtisanThreadPoolTaskExecutor() {
        super();
    }

    /**
     * 执行任务,传递父线程的MDC上下文信息到子线程中
     */
    @Override
    public void execute(Runnable task) {
        super.execute(ThreadMdcUtil.wrap(task, MDC.getCopyOfContextMap()));
    }

    /**
     * 提交带有返回值的任务,传递父线程的MDC上下文信息到子线程中
     */
    @Override
    public <T> Future<T> submit(Callable<T> task) {
        return super.submit(ThreadMdcUtil.wrap(task, MDC.getCopyOfContextMap()));
    }

    /**
     * 提交无返回值的任务,传递父线程的MDC上下文信息到子线程中
     */
    @Override
    public Future<?> submit(Runnable task) {
        return super.submit(ThreadMdcUtil.wrap(task, MDC.getCopyOfContextMap()));
    }
}

继承自SpringThreadPoolTaskExecutor,在执行任务时使用了ThreadMdcUtil工具类来传递父线程的MDC上下文信息到子线程中。通过这种方式,可以确保异步任务在执行过程中能够访问到父线程的MDC上下文信息,从而实现了日志的跟踪。


线程MDC工具类

package com.artisan.boottrace.utils;

import org.slf4j.MDC;

import java.util.Map;
import java.util.concurrent.Callable;

/**
 * @author 小工匠
 * @version 1.0
 * @mark: show me the code , change the world
 * <p>
 * 线程MDC工具类,用于在多线程环境中传递MDC上下文信息
 */
public class ThreadMdcUtil {

    private static final String TRACE_ID = "TRACE_ID";

    /**
     * 生成唯一的追踪ID
     */
    public static String generateTraceId() {
        return TraceIdGenerator.ulid();
    }

    /**
     * 如果当前MDC中不存在追踪ID,则设置追踪ID
     */
    public static void setTraceIdIfAbsent() {
        if (MDC.get(TRACE_ID) == null) {
            MDC.put(TRACE_ID, generateTraceId());
        }
    }

    /**
     * 用于在父线程向线程池中提交任务时,将父线程的MDC上下文信息复制给子线程
     *
     * @param callable 要执行的任务
     * @param context  父线程的MDC上下文信息
     * @param <T>      任务返回类型
     * @return 复制了父线程MDC上下文信息的任务
     */
    public static <T> Callable<T> wrap(final Callable<T> callable, final Map<String, String> context) {
        return () -> {
            if (context == null) {
                MDC.clear();
            } else {
                MDC.setContextMap(context);
            }
            setTraceIdIfAbsent();
            try {
                return callable.call();
            } finally {
                MDC.clear();
            }
        };
    }

    /**
     * 用于在父线程向线程池中提交任务时,将父线程的MDC上下文信息复制给子线程
     *
     * @param runnable 要执行的任务
     * @param context  父线程的MDC上下文信息
     * @return 复制了父线程MDC上下文信息的任务
     */
    public static Runnable wrap(final Runnable runnable, final Map<String, String> context) {
        return () -> {
            if (context == null) {
                MDC.clear();
            } else {
                MDC.setContextMap(context);
            }
            setTraceIdIfAbsent();
            try {
                runnable.run();
            } finally {
                MDC.clear();
            }
        };
    }
}

在多线程环境中传递MDC上下文信息。提供了两个静态方法wrap,用于在父线程向线程池中提交任务时,将父线程的MDC上下文信息复制给子线程。这样可以确保在异步任务中也能够访问到父线程设置的MDC上下文信息,实现了日志的跟踪。


模拟业务类@Async

package com.artisan.boottrace.service;

import lombok.extern.slf4j.Slf4j;
import org.springframework.scheduling.annotation.Async;
import org.springframework.stereotype.Service;

import java.util.concurrent.TimeUnit;

/**
 * @author 小工匠
 * @version 1.0
 * @mark: show me the code , change the world
 */
@Slf4j
@Service
public class ArtisanService implements IArtisanService {

    @Async("ArtisanExecutor")
    @Override
    public void addArtisan() throws InterruptedException {
        TimeUnit.SECONDS.sleep(1);
        log.info("线程名称: {} ,执行方式:Async ---->  addArtisan ", Thread.currentThread().getName());
    }


}
    

测试

package com.artisan.boottrace.controller;

import com.artisan.boottrace.annotations.TraceLog;
import com.artisan.boottrace.service.IArtisanService;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;

/**
 * @author 小工匠
 * @version 1.0
 * @mark: show me the code , change the world
 */

@Slf4j
@RestController
public class ArtisanTestController {

    @Autowired
    private IArtisanService artisanService;

    @GetMapping("/testTrace")
    public String testTrace(@RequestParam("name") String name) throws InterruptedException {
        log.info("testTrace  name={}", name);
        doSomething1();
        // 异步任务
         artisanService.addArtisan();
        log.info("testTrace Call Over name={}", name);
        return "Hello," + name;
    }

    private void doSomething1() {
        log.info("doSomething1  info  log");
        doSomething2();
        log.error("doSomething1 error log");
    }

    private void doSomething2() {
        log.info("doSomething2  info log");
    }


    @TraceLog
    @GetMapping("/testTrace2")
    public String testTraceByAnno(@RequestParam("name") String name) throws InterruptedException {
        log.info("======================");
        log.info("testTraceByAnno  name={}", name);
        doSomething1();
        // 异步任务
        artisanService.addArtisan();
        log.info("testTraceByAnno Call Over name={}", name);
        return "Hello," + name;
    }


}
    

http://127.0.0.1:9999/testTrace?name=artisan
http://127.0.0.1:9999/testTrace2?name=artisan222

在这里插入图片描述


小结

通过合理地利用MDC、拦截器、自定义线程池和工具类等技术手段,可以很好地实现 Spring Boot 应用的日志链路追踪,从而更方便地定位和排查问题,提升应用的可维护性和可靠性。

在这里插入图片描述

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/1601684.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

Java实现优先级队列(堆)

前言 在学习完二叉树的相关知识后&#xff0c;我们对数据结构有了更多的认识&#xff0c;本文将介绍到优先级队列(堆&#xff09; 1.优先级队列 1.1概念 前面介绍过队列&#xff0c;队列是一种先进先出(FIFO)的数据结构&#xff0c;但有些情况下&#xff0c;操作的数据可能…

【机器学习】探究Q-Learning通过学习最优策略来解决AI序列决策问题

꒰˃͈꒵˂͈꒱ write in front ꒰˃͈꒵˂͈꒱ ʕ̯•͡˔•̯᷅ʔ大家好&#xff0c;我是xiaoxie.希望你看完之后,有不足之处请多多谅解&#xff0c;让我们一起共同进步૮₍❀ᴗ͈ . ᴗ͈ აxiaoxieʕ̯•͡˔•̯᷅ʔ—CSDN博客 本文由xiaoxieʕ̯•͡˔•̯᷅ʔ 原创 CSDN …

学会 Python 后可以做什么副业?

近年来&#xff0c;Python凭借其简洁易入门的特点受到越来越多人群的青睐。 当然这不仅仅是针对程序员来说&#xff0c;对于一些学生、职场人士也是如此。 Python为什么会大受欢迎呢&#xff1f;因为Python还被大家称为“胶水语言&#xff0c;它适用于网站、桌面应用开发、[自…

蓝牙耳机哪个牌子好用?力荐五款实力超群机型,快收藏!

​随着蓝牙耳机的普及&#xff0c;越来越多的年轻人开始追求这种无线的便利。市场上品牌众多&#xff0c;款式多样&#xff0c;选择起来确实让人眼花缭乱。我整理了一份蓝牙耳机品牌排行榜前十名&#xff0c;希望能为你提供一些参考&#xff0c;帮助你找到心仪的耳机。 一、如何…

C语言堆区内存管理

一、C语言编译的内存分配 二、堆区空间的分配 1、malloc函数 功能&#xff1a;从堆区分配内存 #include <stdlib.h> void *malloc(unsigned int size)//size 分配内存的字节数2、free函数 功能&#xff1a;释放内存 #include <stdlib.h> void free(void *ptr)…

L2-045 堆宝塔 - java

L2-045 堆宝塔 Java (javac) 时间限制 500 ms 内存限制 256 MB 其他编译器 时间限制 400 ms 内存限制 64 MB 栈限制 8192 KB 题目描述&#xff1a; 堆宝塔游戏是让小朋友根据抓到的彩虹圈的直径大小&#xff0c;按照从大到小的顺序堆起宝塔。但彩虹圈不一定是按照直径的大小顺…

Java NIO,高效操作I/O流的必备技能

Java IO在工作中其实不常用到&#xff0c;更别提NIO了。但NIO却是高效操作I/O流的必备技能&#xff0c;如顶级开源项目Kafka、Netty、RocketMQ等都采用了NIO技术&#xff0c;NIO也是大多数面试官必考的体系知识。虽然骨头有点难啃&#xff0c;但还是要慢慢消耗知识、学以致用哈…

Spring核心容器总结

2.2 核心容器总结 2.2.1 容器相关 BeanFactory是IoC容器的顶层接口&#xff0c;初始化BeanFactory对象时&#xff0c;加载的bean延迟加载 ApplicationContext接口是Spring容器的核心接口&#xff0c;初始化时bean立即加载 ApplicationContext接口提供基础的bean操作相关方法…

CTFHUB-技能树-Web前置技能-文件上传(无验证,JS前端验证,前端验证)

CTFHUB-技能树-Web前置技能-文件上传&#xff08;无验证&#xff0c;JS前端验证&#xff0c;前端验证—.htaccess&#xff09; 文章目录 CTFHUB-技能树-Web前置技能-文件上传&#xff08;无验证&#xff0c;JS前端验证&#xff0c;前端验证—.htaccess&#xff09;文件上传无验…

基于 LSTM 模型的古诗词自动生成算法实现及系统实现

近年来&#xff0c;研究者在利用循环神经网络&#xff08;Recurrent Neural Network&#xff0c;RNN&#xff09;进行古诗自动生成方面取得了显著的效果。但 RNN 存在梯度问题&#xff0c;导致处理时间跨度较长的序列时 RNN 并不具备长期记忆存储功能。随后&#xff0c;出现的基…

(十一)C++自制植物大战僵尸游戏客户端更新实现

植物大战僵尸游戏开发教程专栏地址http://t.csdnimg.cn/cFP3z 更新检查 游戏启动后会下载服务器中的版本号然后与本地版本号进行对比&#xff0c;如果本地版本号小于服务器版本号就会弹出更新提示。让用户选择是否更新客户端。 在弹出的更新对话框中有显示最新版本更新的内容…

全球化背景下的海外社媒营销战略:趋势洞察与策略调整

随着全球化的不断深入&#xff0c;企业在海外市场的竞争愈发激烈。在这一背景下&#xff0c;海外社交媒体平台成为了企业品牌推广和营销的重要渠道。本文Nox聚星将和大家探讨全球化背景下&#xff0c;企业如何利用海外社交媒体平台进行品牌推广和营销&#xff0c;并分析企业如何…

Git分布式版本控制系统——在IDEA中使用Git(一)

一、在IDEA中配置Git 本质上还是使用的本地安装的Git软件&#xff0c;所以需要在IDEA中配置Git 打开IDEA的设置页面&#xff0c;按照下图操作 二、在IDEA中使用Git获取仓库 1、本地初始化仓库 2、从远程仓库克隆 方法一&#xff1a; 方法二&#xff1a; 三、.gitignore文件…

#陶晶驰串口屏使用

1.陶晶驰串口屏输入要连接的wifi信息实现 &#xff08;1&#xff09;选择文本控件 &#xff08;2&#xff09;给文本控件配置输入键盘&#xff0c;id代表用户名&#xff0c;password代表wifi密码&#xff08;注意wifi的频段需要为2.4GHz&#xff09; &#xff08;3&#xff0…

k8s之etcd

1.特点&#xff1a; etcd 是云原生架构中重要的基础组件。有如下特点&#xff1a; 简单&#xff1a;安装配置简单&#xff0c;而且提供了 HTTP API 进行交互&#xff0c;使用也很简单键值对存储&#xff1a;将数据存储在分层组织的目录中&#xff0c;如同在标准文件系统中监…

RAG (Retrieval Augmented Generation) 结合 LlamaIndex、Elasticsearch 和 Mistral

作者&#xff1a;Srikanth Manvi 在这篇文章中&#xff0c;我们将讨论如何使用 RAG 技术&#xff08;检索增强生成&#xff09;和 Elasticsearch 作为向量数据库来实现问答体验。我们将使用 LlamaIndex 和本地运行的 Mistral LLM。 在开始之前&#xff0c;我们将先了解一些术…

性能工具之emqtt-bench BenchMark 测试示例

文章目录 一、前言二、典型压测场景三、机器准备四、典型压测场景1、并发连接2、消息吞吐量测试2.1 1 对 1&#xff08;示例&#xff09;2.2 多对1&#xff08;示例&#xff09;2.3 1对多&#xff08;示例&#xff09; 五、遇到的问题client(): EXIT for {shutdown,eaddrnotava…

OpenStack镜像管理与制作

一、OpenStack镜像服务 1、什么是镜像 镜像通常是指一系列文件或一个磁盘驱动器的精确副本。虚拟机所使用的虚拟磁盘&#xff0c;实际上是一种特殊格式的镜像文件。云环境下尤其需要镜像。镜像就是一个模板&#xff0c;类似于VMware的虚拟机模板&#xff0c;其预先安装基本的…

五步教你正确申请免费SSL证书

在当今数字化时代&#xff0c;保护网站数据安全和提升用户信任至关重要&#xff0c;而实现这一目标的有效途径之一便是为网站部署SSL&#xff08;Secure Sockets Layer&#xff09;证书。SSL证书能够加密网站与用户之间的通信&#xff0c;确保敏感信息不被第三方窃取。幸运的是…

1.8.5 卷积神经网络近年来在结构设计上的主要发展和变迁——Inception-v4 和 Inception-ResNet

1.8.5 卷积神经网络近年来在结构设计上的主要发展和变迁——Inception-v4 和 Inception-ResNet 前情回顾&#xff1a; 1.8.1 卷积神经网络近年来在结构设计上的主要发展和变迁——AlexNet 1.8.2 卷积神经网络近年来在结构设计上的主要发展和变迁——VGGNet 1.8.3 卷积神经网络近…