Java日志源码详解,SpringBoot日志 slf4j、logback、log4j

news2024/12/23 13:11:13
  • 日志视频讲解—上
  • 日志视频讲解—下
  • 学习文档集合

一、前提


在Java中说起日志,定听过这样几个名词:slf4j、logback、log4j,在正式开始之前,先了解几个简单的概念

  1. slf4j、logback、log4j 的作者都是一个人
  2. slf4j 的全名是 Simple Logging Facade for Java,它只是一个门面,可以简单理解是一个接口,具体实现由logback和log4j去实现
  3. logback、log4j 都是出自一个人,而logback是后面出来的,那不言而喻,一个人做了两个东西,肯定是对一个东西不是很满意,实际上logback也比log4j更有优势
  4. SpringBoot项目默认的日志就是用 logback,从侧面体现了它确有优势

二、原生Java使用日志


1、证明 slf4j 是一个门面

在一个基本的Java项目(只引入JDK)要想打印日志,只需要引入 slf4j 和 它的实现类基本包就可以了。
在这里插入图片描述

<?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 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>

    <groupId>cn.xdx</groupId>
    <artifactId>logLook</artifactId>
    <version>1.0-SNAPSHOT</version>

    <properties>
        <maven.compiler.source>8</maven.compiler.source>
        <maven.compiler.target>8</maven.compiler.target>
    </properties>

    <dependencies>
        
        <!-- 门面 slf4j -->
        <dependency>
            <groupId>org.slf4j</groupId>
            <artifactId>slf4j-api</artifactId>
            <version>1.7.30</version>
        </dependency>
        
        <!-- 使用 logback-->
        <dependency>
            <groupId>ch.qos.logback</groupId>
            <artifactId>logback-classic</artifactId>
            <version>1.2.3</version>
        </dependency>

        <!-- 使用 log4j-->
        <dependency>
            <groupId>org.apache.logging.log4j</groupId>
            <artifactId>log4j-slf4j-impl</artifactId>
            <version>2.13.3</version>
            <scope>compile</scope>
        </dependency>
        <dependency>
            <groupId>org.apache.logging.log4j</groupId>
            <artifactId>log4j-core</artifactId>
            <version>2.13.3</version>
            <scope>compile</scope>
        </dependency>
        
    </dependencies>
</project>

1-1、空实现-证明

去掉pom文件中的 log4j和logback的依赖,运行代码异常如下:找不到实现类
Failed to load class “org.slf4j.impl.StaticLoggerBinder” 注意这个包名后面会用到

在这里插入图片描述


1-2、独自logback、独自 log4j、一起使用

单独使用logback——只需要注释log4j的依赖。正常运行


单独使用 log4j——只需要注释logback的依赖。正常运行

在这里插入图片描述


一起使用——会提示找到多个实现类,最终按照pom文件的引用依赖顺序选择一个(可以试试把log4j依赖放前面)

在这里插入图片描述


2、Logger工厂创建 (ILoggerFactory)重要


Logger logger = LoggerFactory.getLogger(JavaLog.class);

public static Logger getLogger(String name) {
    ILoggerFactory iLoggerFactory = getILoggerFactory();
    return iLoggerFactory.getLogger(name);
}
  1. 创建Logger之前,先创建了一个工厂,再基于这个工厂来创建具体的Logger。(ILoggerFactory)
  2. 工厂就是来创建对象的,刚刚我们看到logback和log4j不同的实现,其本质就是不同的工厂创建的不同的对象。

2-1、工厂创建的开始
  1. INITIALIZATION_STATE 是当前工厂的状态,根据不同的状态执行不同的代码,默认是 UNINITIALIZED 未初始化。
  2. StaticLoggerBinder.getSingleton().getLoggerFactory(); 初始化好就会创建工厂类,这行就是返回创建好的工厂。
public static ILoggerFactory getILoggerFactory() {
    // 默认未初始化状态
    if (INITIALIZATION_STATE == UNINITIALIZED) {
        synchronized (LoggerFactory.class) {
            if (INITIALIZATION_STATE == UNINITIALIZED) {
                // 正在初始化
                INITIALIZATION_STATE = ONGOING_INITIALIZATION;
                // 初始化代码
                performInitialization();
            }
        }
    }
    switch (INITIALIZATION_STATE) {
    case SUCCESSFUL_INITIALIZATION:  // 初始化成功所执行的代码
        return StaticLoggerBinder.getSingleton().getLoggerFactory();
    case NOP_FALLBACK_INITIALIZATION:
        return NOP_FALLBACK_FACTORY;
    case FAILED_INITIALIZATION:
        throw new IllegalStateException(UNSUCCESSFUL_INIT_MSG);
    case ONGOING_INITIALIZATION:
        // support re-entrant behavior.
        // See also http://jira.qos.ch/browse/SLF4J-97
        return SUBST_FACTORY;
    }
    throw new IllegalStateException("Unreachable code");
}


2-2、绑定真正的工厂类

private final static void performInitialization() {
    bind();
    if (INITIALIZATION_STATE == SUCCESSFUL_INITIALIZATION) {
        versionSanityCheck();
    }
}


private final static void bind() {
    try {
        Set<URL> staticLoggerBinderPathSet = null;
        // 判断是否是安卓,Java不是安卓,所以会执行
        // 这个if中的代码没有实际的作用【作用就是如果你有多个工厂实现,就打印日志提醒你】
        if (!isAndroid()) {
            // 找到当前可以被绑定的工厂
            staticLoggerBinderPathSet = findPossibleStaticLoggerBinderPathSet();
            // 如果找到多个就打印日志,上面看到的多个绑定日志就是这里打印的
            reportMultipleBindingAmbiguity(staticLoggerBinderPathSet);
        }
        // 初始化LoggerFactory,这里就是按照顺序读取一个工厂bean
        StaticLoggerBinder.getSingleton();
        
        // 设置初始化状态为成功
        INITIALIZATION_STATE = SUCCESSFUL_INITIALIZATION;
        reportActualBinding(staticLoggerBinderPathSet);
    } catch (NoClassDefFoundError ncde) {
        // 异常处理
    } catch (java.lang.NoSuchMethodError nsme) {
         // 异常处理
    } catch (Exception e) {
         // 异常处理
    } finally {
        postBindCleanUp();
    }
}

2-3、slf4j 如何做到不同实现的随机切换

其实很简单,它里面写死了真正工厂类的权限定名:org.slf4j.impl.StaticLoggerBinder,也就是谁要使用slf4j这个门面,谁就要写一个这个类,用这个类去生成工厂。

在这里插入图片描述


##### 2-3-1、找到多个实现类,怎么做提示?

上面验证,当引入log4j和logback的时候,会提示有多个实现类,它是怎么做的呢?
就是用了一个写死的完整的类权限定名,如果加载到多个,就返回多个,就打印日志。

private static String STATIC_LOGGER_BINDER_PATH = "org/slf4j/impl/StaticLoggerBinder.class";

static Set<URL> findPossibleStaticLoggerBinderPathSet() {
    // use Set instead of list in order to deal with bug #138
    // LinkedHashSet appropriate here because it preserves insertion order
    // during iteration
    Set<URL> staticLoggerBinderPathSet = new LinkedHashSet<URL>();
    try {
        ClassLoader loggerFactoryClassLoader = LoggerFactory.class.getClassLoader();
        Enumeration<URL> paths;
        if (loggerFactoryClassLoader == null) {
            paths = ClassLoader.getSystemResources(STATIC_LOGGER_BINDER_PATH);
        } else {
            paths = loggerFactoryClassLoader.getResources(STATIC_LOGGER_BINDER_PATH);
        }
        while (paths.hasMoreElements()) {
            URL path = paths.nextElement();
            staticLoggerBinderPathSet.add(path);
        }
    } catch (IOException ioe) {
        Util.report("Error getting resources from path", ioe);
    }
    return staticLoggerBinderPathSet;
}


private static void reportMultipleBindingAmbiguity(Set<URL> binderPathSet) {
    if (isAmbiguousStaticLoggerBinderPathSet(binderPathSet)) {
        Util.report("Class path contains multiple SLF4J bindings.");
        for (URL path : binderPathSet) {
            Util.report("Found binding in [" + path + "]");
        }
        Util.report("See " + MULTIPLE_BINDINGS_URL + " for an explanation.");
    }
}

private static boolean isAmbiguousStaticLoggerBinderPathSet(Set<URL> binderPathSet) {
    return binderPathSet.size() > 1;
}

2-3-2、如果引入多个实现类,最终用哪个呢?

答案是谁先引入就用谁,在绑定的时候,会有下面的代码,这代码就是去初始化工厂类。

现在已经很明了它是怎么选择实现类了,而log4j和logback都是同一个作者,作者说logback比log4j好,那我们后续对日志的解读当然都是基于logback来了。

StaticLoggerBinder.getSingleton();

public static StaticLoggerBinder getSingleton() {
    return SINGLETON;
}

private static StaticLoggerBinder SINGLETON = new StaticLoggerBinder();

private StaticLoggerBinder() {
    defaultLoggerContext.setName(CoreConstants.DEFAULT_CONTEXT_NAME);
}

private LoggerContext defaultLoggerContext = new LoggerContext();

2-4、工厂类的初始化

  1. 创建了工厂类class LoggerContext implements ILoggerFactory
  2. 创建了根Logger,并为其设置了默认的配置(这个根Logger是干嘛的后面再说)
    • 默认的level:DEBUG
    • 默认的layout:%d{HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n
    • 默认的appende:ConsoleAppender
StaticLoggerBinder.getSingleton();

public static StaticLoggerBinder getSingleton() {
    return SINGLETON;
}

private static StaticLoggerBinder SINGLETON = new StaticLoggerBinder();

static {
    SINGLETON.init();
}

StaticLoggerBinder 里面有个静态代码块,里面执行了 init 方法。

// 使用logback的工厂是 LoggerContext
private LoggerContext defaultLoggerContext = new LoggerContext();

void init() {
    try {
        try {
            // 创建默认的工厂,并进行初始化
            new ContextInitializer(defaultLoggerContext).autoConfig();
        } catch (JoranException je) {
            Util.report("Failed to auto configure default logger context", je);
        }
        // logback-292
        if (!StatusUtil.contextHasStatusListener(defaultLoggerContext)) {
            StatusPrinter.printInCaseOfErrorsOrWarnings(defaultLoggerContext);
        }
        // 把当前的工厂放入上下午绑定器里面
        contextSelectorBinder.init(defaultLoggerContext, KEY);
        initialized = true;
    } catch (Exception t) { // see LOGBACK-1159
        Util.report("Failed to instantiate [" + LoggerContext.class.getName() + "]", t);
    }
}

创建根Logger,设置默认的等级为 DEBUG

public LoggerContext() {
    super();
    this.loggerCache = new ConcurrentHashMap<String, Logger>();
    this.loggerContextRemoteView = new LoggerContextVO(this);
    this.root = new Logger(Logger.ROOT_LOGGER_NAME, null, this);
    this.root.setLevel(Level.DEBUG);
    loggerCache.put(Logger.ROOT_LOGGER_NAME, root);
    initEvaluatorMap();
    size = 1;
    this.frameworkPackages = new ArrayList<String>();
}

设置根Logger的 Layout和Appender

public void autoConfig() throws JoranException {
    StatusListenerConfigHelper.installIfAsked(loggerContext);
    URL url = findURLOfDefaultConfigurationFile(true);
    if (url != null) {
        configureByResource(url);
    } else {
        Configurator c = EnvUtil.loadFromServiceLoader(Configurator.class);
        if (c != null) {
            try {
                c.setContext(loggerContext);
                c.configure(loggerContext);
            } catch (Exception e) {
                throw new LogbackException(String.format("Failed to initialize Configurator: %s using ServiceLoader", c != null ? c.getClass()
                                .getCanonicalName() : "null"), e);
            }
        } else {
            BasicConfigurator basicConfigurator = new BasicConfigurator();
            basicConfigurator.setContext(loggerContext);
            basicConfigurator.configure(loggerContext);
        }
    }
}


public void configure(LoggerContext lc) {
    addInfo("Setting up default configuration.");
    
    ConsoleAppender<ILoggingEvent> ca = new ConsoleAppender<ILoggingEvent>();
    ca.setContext(lc);
    ca.setName("console");
    LayoutWrappingEncoder<ILoggingEvent> encoder = new LayoutWrappingEncoder<ILoggingEvent>();
    encoder.setContext(lc);
    

    // same as 
    // PatternLayout layout = new PatternLayout();
    // layout.setPattern("%d{HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n");
    TTLLLayout layout = new TTLLLayout();
    layout.setContext(lc);
    layout.start();
    encoder.setLayout(layout);
    
    ca.setEncoder(encoder);
    ca.start();
    
    Logger rootLogger = lc.getLogger(Logger.ROOT_LOGGER_NAME);
    rootLogger.addAppender(ca);
}

// 这里可以先看看,只有 ROOT的Logger才有这个 Appender,数据是存在 aai 中
public synchronized void addAppender(Appender<ILoggingEvent> newAppender) {
    if (aai == null) {
        aai = new AppenderAttachableImpl<ILoggingEvent>();
    }
    aai.addAppender(newAppender);
}

3、Logger 创建


上面已经得出来最终的工厂类是 LoggerContext,现在只需要看它里面的 getLogger方法是如何创建Logger的即可。

Logger的创建并不像我们想象的那样每个类创建一个,它是有子父级概念的,和我们的包路径一样的结构,比如类权限定名为 cn.xdx.JavaLog,则会创建4个Logger,分别是 ROOT、cn、cn.xdx、cn.xdx.JavaLog,并且它是一个树结构,ROOT节点的字节点包含了下面的三个。

@Override
public final Logger getLogger(final String name) {

    if (name == null) {
        throw new IllegalArgumentException("name argument cannot be null");
    }
    // 判断是不是获取 根 Logger
    if (Logger.ROOT_LOGGER_NAME.equalsIgnoreCase(name)) {
        return root;
    }

    int i = 0;
    Logger logger = root;

    // 判断当前Logger 是否已经创建过了,如果存在就返回
    Logger childLogger = (Logger) loggerCache.get(name);
    // if we have the child, then let us return it without wasting time
    if (childLogger != null) {
        return childLogger;
    }

    String childName;
    while (true) {
        // 用【.】去切割权限定名,h就是返回有几层
        int h = LoggerNameUtil.getSeparatorIndexOf(name, i);
        // 获取当前层级
        if (h == -1) {
            childName = name;
        } else {
            childName = name.substring(0, h);
        }
        i = h + 1;
        // 从根节点开始便利,直到找到了当前的Logger
        synchronized (logger) {
            childLogger = logger.getChildByName(childName);
            if (childLogger == null) {
                // 创建 Logger
                childLogger = logger.createChildByName(childName);
                // 放入缓存
                loggerCache.put(childName, childLogger);
                incSize();
            }
        }
        logger = childLogger;
        if (h == -1) {
            return childLogger;
        }
    }
}

新Logger的创建

Logger createChildByName(final String childName) {
    int i_index = LoggerNameUtil.getSeparatorIndexOf(childName, this.name.length() + 1);
    if (i_index != -1) {
        throw new IllegalArgumentException("For logger [" + this.name + "] child name [" + childName
                        + " passed as parameter, may not include '.' after index" + (this.name.length() + 1));
    }

    if (childrenList == null) {
        childrenList = new CopyOnWriteArrayList<Logger>();
    }
    Logger childLogger;
    childLogger = new Logger(childName, this, this.loggerContext);
    childrenList.add(childLogger);
    childLogger.effectiveLevelInt = this.effectiveLevelInt;
    return childLogger;
}

Logger(String name, Logger parent, LoggerContext loggerContext) {
    this.name = name;
    this.parent = parent;
    this.loggerContext = loggerContext;
}

4、Logger 输出到控制台的过程

public static void main(String[] args) {
    Logger logger = LoggerFactory.getLogger(JavaLog.class);
    logger.error("xxxxx");
}

上面已经得到了Logger的实现类 ch.qos.logback.classic.Logger,现在就来看看这个 logger.error 是如何输出到控制的。


4-1、方法的重载

每种级别的方法参数都有很多,所以都会做参数不同的方法重载。

public void error(String msg) {
    filterAndLog_0_Or3Plus(FQCN, null, Level.ERROR, msg, null, null);
}

private void filterAndLog_0_Or3Plus(final String localFQCN, final Marker marker, final Level level, final String msg, final Object[] params,
                final Throwable t) {
    // 循环去运行过滤器
    final FilterReply decision = loggerContext.getTurboFilterChainDecision_0_3OrMore(marker, this, level, msg, params, t);
    // 如果状态是 中性,就判断当前是否要判断过滤级别
    if (decision == FilterReply.NEUTRAL) {
        if (effectiveLevelInt > level.levelInt) {
            return;
        }
    } else if (decision == FilterReply.DENY) {
        return;
    }
    buildLoggingEventAndAppend(localFQCN, marker, level, msg, params, t);
}

4-2、过滤掉不想输出的日志

有两种情况,即便是调用了日志打印方法也不会输出日志

  1. 被过滤器过滤了
  2. 日志的级别过低(比如我们设置的是error级别,但是你打印 info级别的就行)
final FilterReply getTurboFilterChainDecision_0_3OrMore(final Marker marker, final Logger logger, final Level level, final String format,
                final Object[] params, final Throwable t) {
    // 如果没有过滤器就返回 中性状态
    if (turboFilterList.size() == 0) {
        return FilterReply.NEUTRAL;
    }
    // 循环去运行所有过滤器
    return turboFilterList.getTurboFilterChainDecision(marker, logger, level, format, params, t);
}

自定义过滤器

public class MyLogBackFilter extends TurboFilter {
    @Override
    public FilterReply decide(Marker marker, Logger logger, Level level, String format, Object[] params, Throwable t) {

        System.out.println("kkkkkk");
        return FilterReply.ACCEPT;
    }
}


public static void main(String[] args) {
    Logger logger = LoggerFactory.getLogger(JavaLog.class);
    LoggerContext loggerContext =  (LoggerContext)LoggerFactory.getILoggerFactory();
    loggerContext.addTurboFilter(new MyLogBackFilter());

    logger.error("xxxxx");
}

4-3、构建日志事件

其实就是组装这次日志打印的信息

private void buildLoggingEventAndAppend(final String localFQCN, final Marker marker, final Level level, final String msg, final Object[] params,
                final Throwable t) {
    LoggingEvent le = new LoggingEvent(localFQCN, this, level, msg, t, params);
    le.setMarker(marker);
    callAppenders(le);
}

public LoggingEvent(String fqcn, Logger logger, Level level, String message, Throwable throwable, Object[] argArray) {
    this.fqnOfLoggerClass = fqcn;
    this.loggerName = logger.getName();
    this.loggerContext = logger.getLoggerContext();
    this.loggerContextVO = loggerContext.getLoggerContextRemoteView();
    this.level = level;

    this.message = message;
    this.argumentArray = argArray;

    if (throwable == null) {
        throwable = extractThrowableAnRearrangeArguments(argArray);
    }

    if (throwable != null) {
        this.throwableProxy = new ThrowableProxy(throwable);
        LoggerContext lc = logger.getLoggerContext();
        if (lc.isPackagingDataEnabled()) {
            this.throwableProxy.calculatePackagingData();
        }
    }

    timeStamp = System.currentTimeMillis();
}

4-4、日志输出
  1. 这里面的调用链很长,但逻辑都很简单,就直接把代码给出吧
  2. doAppend 方法之后就是一些正常的操作,就不再截图了,可以自行去看(ROOT 里面默认的是ConsoleAppender)
public void callAppenders(ILoggingEvent event) {
    int writes = 0;
    // 循环便利每个层级的Logger,如果当前Logger 有Appender 就输出
    // 上面创建 Logger的时候,其实就知道了只有ROOT_Logger 才有Appender,所里这里也是等到ROOT才会输出
    for (Logger l = this; l != null; l = l.parent) {
        writes += l.appendLoopOnAppenders(event);
        
        // 这个参数默认都是 true 不用管
        if (!l.additive) {
            break;
        }
    }
    // No appenders in hierarchy
    if (writes == 0) {
        loggerContext.noAppenderDefinedWarning(this);
    }
}


private int appendLoopOnAppenders(ILoggingEvent event) {
    if (aai != null) {
        return aai.appendLoopOnAppenders(event);
    } else {
        return 0;
    }
}

// ROOT 里面默认的是ConsoleAppender
public int appendLoopOnAppenders(E e) {
    int size = 0;
    Appender<E>[] appenderArray = (Appender[])this.appenderList.asTypedArray();
    int len = appenderArray.length;

    for(int i = 0; i < len; ++i) {
        appenderArray[i].doAppend(e);
        ++size;
    }

    return size;
}

4-4-1、日志格式化

上面看到最终输出日志的是ROOT_Logger,而ROOT中的 layout是 TTLLLayout,日志格式化的时候会调用doLayout 方法

@Override
public String doLayout(ILoggingEvent event) {
    if (!isStarted()) {
        return CoreConstants.EMPTY_STRING;
    }
    StringBuilder sb = new StringBuilder();

    long timestamp = event.getTimeStamp();

    sb.append(cachingDateFormatter.format(timestamp));
    sb.append(" [");
    sb.append(event.getThreadName());
    sb.append("] ");
    sb.append(event.getLevel().toString());
    sb.append(" ");
    sb.append(event.getLoggerName());
    sb.append(" - ");
    sb.append(event.getFormattedMessage());
    sb.append(CoreConstants.LINE_SEPARATOR);
    IThrowableProxy tp = event.getThrowableProxy();
    if (tp != null) {
        String stackTrace = tpc.convert(event);
        sb.append(stackTrace);
    }
    return sb.toString();
}

在这里插入图片描述



三、SpringBoot中的日志

使用SpringBoot就要引入相关的pom文件,这里需要把pom文件替换成下面的

<groupId>cn.xdx</groupId>
<artifactId>logLook</artifactId>
<version>1.0-SNAPSHOT</version>

<properties>
    <maven.compiler.source>8</maven.compiler.source>
    <maven.compiler.target>8</maven.compiler.target>
</properties>

<parent>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-parent</artifactId>
    <version>2.4.5</version>
    <relativePath/>
</parent>

<dependencies>
    <!--Use undertow, 设置服务器,和日志没关系哈-->
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-undertow</artifactId>
    </dependency>
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-web</artifactId>
        <exclusions>
            <exclusion>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-starter-tomcat</artifactId>
            </exclusion>
        </exclusions>
    </dependency>
</dependencies>

启动服务,使用日志功能,发现在不做任何配置的时候,它默认使用的是 logback打印

在这里插入图片描述


1、如何加载、选择日志工厂

1-1、前置:SpringBoot 自动加载流程

在SpringBoot项目启动的时候会自动做很多的操作,这里需要了解两点

  1. SpringFactoriesLoader 这个类会去加载所有 META-INF/spring.factories 文件,loadFactoryNames这个方法就是通过name找到所有spring.factories文件中对应的类。
  2. ApplicationListener 是一个接口,SpringBoot项目在启动的每个阶段都会投递事件到 onApplicationEvent 方法中。

在SpringBoot的 spring.factories 下有三个工厂构造器,启动的时候会把它们三个都加载进去(按照顺序加载第一个就是 logback)

在这里插入图片描述


在SpringBoot的中有一个 LoggingApplicationListener,它的继承关系如下,所以它本质上是一个ApplicationListener,并且重写了onApplicationEvent方法。

在这里插入图片描述


1-2、logback和log4j切换、默认的为何是logback

在原始日志中,知道LoggerFactory的实现类是取决于引用了什么包。而在SpringBoot项目中它默认就引入了 logback的包,所以它默认是用 logback。

如果在SpringBoot中想切换成 log4j,原理也是一样,去除logback的包,引入log4j

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-web</artifactId>
    <exclusions>
        <exclusion>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-tomcat</artifactId>
        </exclusion>
        <exclusion>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-logging</artifactId>
        </exclusion>
    </exclusions>
</dependency>


<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-log4j</artifactId>
    <version>1.3.8.RELEASE</version>
</dependency>

1-3、准备:初始化LoggerFactory

其实在SpringBoot中 LoggerFactory的构建也是基于上面原生的方式,只不过在原生方式创建了 LoggerFactory之后,SpringBoot再基于自己的配置,去修改、填充LoggerFactory中配置。

  1. 基于原生方式构建出 LoggerFactory
  2. SpringBoot基于启动阶段来做初始化(LoggingApplicationListener)
public void onApplicationEvent(ApplicationEvent event) {
    // 启动时候的事件 —— 进行日志前置初始化
    if (event instanceof ApplicationStartingEvent) {
        this.onApplicationStartingEvent((ApplicationStartingEvent)event);
    } 
    // 环境变量准备好的事件 —— 进行日志初始化
    else if (event instanceof ApplicationEnvironmentPreparedEvent) {
        this.onApplicationEnvironmentPreparedEvent((ApplicationEnvironmentPreparedEvent)event);
    }
    //  全部启动好之后的事件 —— 暂不关注
    else if (event instanceof ApplicationPreparedEvent) {
        this.onApplicationPreparedEvent((ApplicationPreparedEvent)event);
    } else if (event instanceof ContextClosedEvent && ((ContextClosedEvent)event).getApplicationContext().getParent() == null) {
        this.onContextClosedEvent();
    } else if (event instanceof ApplicationFailedEvent) {
        this.onApplicationFailedEvent();
    }
}

1-4、前置:初始化LoggerFactory

  1. 找到 loggingSystem,这个就是SpringBoot对各种日志实现的包装,对应的包装就是对应的实现类
  2. 初始化前置,这里其实是一个空实现,如果有必要可以实现这个接口做一些操作
private void onApplicationStartingEvent(ApplicationStartingEvent event) {
    this.loggingSystem = LoggingSystem.get(event.getSpringApplication().getClassLoader());
    this.loggingSystem.beforeInitialize();
}

在这里插入图片描述

private static final LoggingSystemFactory SYSTEM_FACTORY = LoggingSystemFactory.fromSpringFactories();

public static LoggingSystem get(ClassLoader classLoader) {
    String loggingSystemClassName = System.getProperty(SYSTEM_PROPERTY);
    // 这里默认为 null
    if (StringUtils.hasLength(loggingSystemClassName)) {
        return (LoggingSystem)("none".equals(loggingSystemClassName) ? new LoggingSystem.NoOpLoggingSystem() : get(classLoader, loggingSystemClassName));
    } else {
        // 走这个
        LoggingSystem loggingSystem = SYSTEM_FACTORY.getLoggingSystem(classLoader);
        Assert.state(loggingSystem != null, "No suitable logging system located");
        return loggingSystem;
    }
}
public interface LoggingSystemFactory {
    LoggingSystem getLoggingSystem(ClassLoader classLoader);

    static LoggingSystemFactory fromSpringFactories() {
        // 创建一个 DelegatingLoggingSystemFactory
        return new DelegatingLoggingSystemFactory((classLoader) -> {
            // 这里就是前面说的,通过name去获取 spring.factories 对应的数据,有兴趣自己去看看
            // 这里获取的数据会按照 @Order 排序,但是这三个实现类的 @Order是一样的,所以是默认顺序
            return SpringFactoriesLoader.loadFactories(LoggingSystemFactory.class, classLoader);
        });
    }
}

循环去遍历每一个factory,找到了就返回,其本质也是看当前项目下有没有具体的类

public LoggingSystem getLoggingSystem(ClassLoader classLoader) {
    List<LoggingSystemFactory> delegates = this.delegates != null ? (List)this.delegates.apply(classLoader) : null;
    if (delegates != null) {
        Iterator var3 = delegates.iterator();

        while(var3.hasNext()) {
            LoggingSystemFactory delegate = (LoggingSystemFactory)var3.next();
            LoggingSystem loggingSystem = delegate.getLoggingSystem(classLoader);
            if (loggingSystem != null) {
                return loggingSystem;
            }
        }
    }

    return null;
}

在这里插入图片描述


logback和log4j的实现,就是判断isPresent中的这个权限定名有没有。

@Order(2147483647)
public static class Factory implements LoggingSystemFactory {
    private static final boolean PRESENT = ClassUtils.isPresent("ch.qos.logback.core.Appender", LogbackLoggingSystem.Factory.class.getClassLoader());

    public Factory() {
    }

    public LoggingSystem getLoggingSystem(ClassLoader classLoader) {
        return PRESENT ? new LogbackLoggingSystem(classLoader) : null;
    }
}

@Order(2147483647)
public static class Factory implements LoggingSystemFactory {
    private static final boolean PRESENT = ClassUtils.isPresent("org.apache.logging.log4j.core.impl.Log4jContextFactory", Log4J2LoggingSystem.Factory.class.getClassLoader());

    public Factory() {
    }

    public LoggingSystem getLoggingSystem(ClassLoader classLoader) {
        return PRESENT ? new Log4J2LoggingSystem(classLoader) : null;
    }
}

1-5、开始:初始化LoggerFactory

前面已经创建好了LoggerFactory > LoggerContext,这里的初始化是对LoggerContext里面的一些数据进行赋值,主要是读取配置文件 自定义的 logback.xml 和 application

private void onApplicationEnvironmentPreparedEvent(ApplicationEnvironmentPreparedEvent event) {
   // 在前置里面已经生成了 loggingSystem,准确来说是它的实现类  LogbackLoggingSystem
   if (this.loggingSystem == null) {
      this.loggingSystem = LoggingSystem.get(event.getSpringApplication().getClassLoader());
   }
   // 初始化就是读取配置文件中的信息,来重新填充LoggerFactory——它的实现类 LoggerContext
   initialize(event.getEnvironment(), event.getSpringApplication().getClassLoader());
}

如果你配置过logback.xml,那你肯定在application里面配置过它的位置logging.config: classpath:logback.xml

protected void initialize(ConfigurableEnvironment environment, ClassLoader classLoader) {
   // ... 
   initializeSystem(environment, this.loggingSystem, this.logFile);
   // ...
}


public static final String CONFIG_PROPERTY = "logging.config";

private void initializeSystem(ConfigurableEnvironment environment, LoggingSystem system, LogFile logFile) {
   String logConfig = StringUtils.trimWhitespace(environment.getProperty(CONFIG_PROPERTY));
   try {
      LoggingInitializationContext initializationContext = new LoggingInitializationContext(environment);
      if (ignoreLogConfig(logConfig)) {
         system.initialize(initializationContext, null, logFile);
      }
      else {
         system.initialize(initializationContext, logConfig, logFile);
      }
   }
   catch (Exception ex) {
      // ...
   }
}

这里用的是 logback,所以最终是:org.springframework.boot.logging.logback.LogbackLoggingSystem#initialize

@Override
public void initialize(LoggingInitializationContext initializationContext, String configLocation, LogFile logFile) {
   LoggerContext loggerContext = getLoggerContext();
   if (isAlreadyInitialized(loggerContext)) {
      return;
   }
   super.initialize(initializationContext, configLocation, logFile);
   loggerContext.getTurboFilterList().remove(FILTER);
   markAsInitialized(loggerContext);
   if (StringUtils.hasText(System.getProperty(CONFIGURATION_FILE_PROPERTY))) {
      getLogger(LogbackLoggingSystem.class.getName()).warn("Ignoring '" + CONFIGURATION_FILE_PROPERTY
            + "' system property. Please use 'logging.config' instead.");
   }
}

2、如何解析配置信息、自定义配置信息


在读取配置文件的时候无非就几种情况,既然有多种情况,那肯定是有一个优先级的——即下面的排序

  1. 指定自己的配置文件 (logging.config: classpath:logback.xml)
  2. 默认读取的配置文件,本质上和【1】一样,只是文件的位置不同
    • logback自己的默认配置文件 (“logback-test.groovy”, “logback-test.xml”, “logback.groovy”, “logback.xml”)
    • 在Spring中 logback默认的配置文件(“logback-test-spring.groovy”, “logback-test-spring.xml”, “logback.groovy”, “logback-spring.xml”)
  1. 使用 application 配置
  2. 没有任何配置文件,默认策略

这也就是为什么说logback的在SpringBoot中的默认配置文件是 logback-spring.xml, 因为其它几个后缀基本上不会有。


从代码角度来看的logback日志配置也可以看成是:

  1. 读取 xml 配置
  2. 读取 application 配置

org.springframework.boot.logging.AbstractLoggingSystem#initialize

public void initialize(LoggingInitializationContext initializationContext, String configLocation, LogFile logFile) {
   // 读取 logging.config 配置的文件 也就是 logback.xml
   if (StringUtils.hasLength(configLocation)) {
      initializeWithSpecificConfig(initializationContext, configLocation, logFile);
      return;
   }
   // 没有单独的配置文件,读取 默认/application 的配置
   initializeWithConventions(initializationContext, logFile);
}
private void initializeWithConventions(LoggingInitializationContext initializationContext, LogFile logFile) {
   // 获取logback的默认配置  "logback-test.groovy", "logback-test.xml", "logback.groovy", "logback.xml"
   String config = getSelfInitializationConfig();
   if (config != null && logFile == null) {
      // self initialization has occurred, reinitialize in case of property changes
      reinitialize(initializationContext);
      return;
   }
   if (config == null) {
      // 获取Srping的默认配置  logback-test-spring.groovy", "logback-test-spring.xml", "logback.groovy", "logback-spring.xml
      config = getSpringInitializationConfig();
   }
   if (config != null) {
      // 加载配置
      loadConfiguration(initializationContext, config, logFile);
      return;
   }
   // 默认的配置 application
   loadDefaults(initializationContext, logFile);
}

2-1、application 读取

先来看没有自定义配置的情况,也就是没有配置 logging.config,也没有读取到logback默认配置和spring中logback的默认配置


org.springframework.boot.logging.logback.LogbackLoggingSystem#loadDefaults

@Override
protected void loadDefaults(LoggingInitializationContext initializationContext, LogFile logFile) {
   LoggerContext context = getLoggerContext();
   stopAndReset(context);
   boolean debug = Boolean.getBoolean("logback.debug");
   if (debug) {
      StatusListenerConfigHelper.addOnConsoleListenerInstance(context, new OnConsoleStatusListener());
   }
   Environment environment = initializationContext.getEnvironment();
   // 读取 application 中的配置存入 LoggerContext 中
   new LogbackLoggingSystemProperties(environment, context::putProperty).apply(logFile);
   LogbackConfigurator configurator = debug ? new DebugLogbackConfigurator(context)
         : new LogbackConfigurator(context);
   // 基于读取到的配置进行配置
   new DefaultLogbackConfiguration(initializationContext, logFile).apply(configurator);
   context.setPackagingDataEnabled(true);
}

2-1-1、读取 application 配置

这里的代码很清晰,具体如何去解析字段(比如 logging.pattern.console),可以自行去看。

public final void apply(LogFile logFile) {
   PropertyResolver resolver = getPropertyResolver();
   apply(logFile, resolver);
}

protected void apply(LogFile logFile, PropertyResolver resolver) {
   setSystemProperty(resolver, EXCEPTION_CONVERSION_WORD, "logging.exception-conversion-word");
   setSystemProperty(PID_KEY, new ApplicationPid().toString());
   setSystemProperty(resolver, CONSOLE_LOG_PATTERN, "logging.pattern.console");
   setSystemProperty(resolver, CONSOLE_LOG_CHARSET, "logging.charset.console", getDefaultCharset().name());
   setSystemProperty(resolver, LOG_DATEFORMAT_PATTERN, "logging.pattern.dateformat");
   setSystemProperty(resolver, FILE_LOG_PATTERN, "logging.pattern.file");
   setSystemProperty(resolver, FILE_LOG_CHARSET, "logging.charset.file", getDefaultCharset().name());
   setSystemProperty(resolver, LOG_LEVEL_PATTERN, "logging.pattern.level");
   applyDeprecated(resolver);
   if (logFile != null) {
      logFile.applyToSystemProperties();
   }
}

2-1-2、基于配置文件初始化

在开始之前需要回顾一下logback里面基本内容:

  1. Appender 日志如何输出,里面包含了输出的格式
  2. ROOT_log 日志是以包名来建立的层级,根目录是 ROOT
  3. 所以大部分的配置其实就是围绕上面两个对象

配置的主流程

  1. 把从application 中读取的内容变成配置
  2. 基于配置创建 Appender
  3. 把 Appender 放到 ROOT上
void apply(LogbackConfigurator config) {
   synchronized (config.getConfigurationLock()) {
      defaults(config);
      Appender<ILoggingEvent> consoleAppender = consoleAppender(config);
      if (this.logFile != null) {
         Appender<ILoggingEvent> fileAppender = fileAppender(config, this.logFile.toString());
         config.root(Level.INFO, consoleAppender, fileAppender);
      }
      else {
         config.root(Level.INFO, consoleAppender);
      }
   }
}

配置文件放入 LoggerContext

private void defaults(LogbackConfigurator config) {
   config.conversionRule("clr", ColorConverter.class);
   config.conversionRule("wex", WhitespaceThrowableProxyConverter.class);
   config.conversionRule("wEx", ExtendedWhitespaceThrowableProxyConverter.class);
   config.getContext().putProperty("CONSOLE_LOG_PATTERN", resolve(config, "${CONSOLE_LOG_PATTERN:-"
         + "%clr(%d{${LOG_DATEFORMAT_PATTERN:-yyyy-MM-dd HH:mm:ss.SSS}}){faint} %clr(${LOG_LEVEL_PATTERN:-%5p}) "
         + "%clr(${PID:- }){magenta} %clr(---){faint} %clr([%15.15t]){faint} %clr(%-40.40logger{39}){cyan} "
         + "%clr(:){faint} %m%n${LOG_EXCEPTION_CONVERSION_WORD:-%wEx}}"));
   config.getContext().putProperty("CONSOLE_LOG_CHARSET", resolve(config, "${CONSOLE_LOG_CHARSET:-default}"));
   config.getContext().putProperty("FILE_LOG_PATTERN", resolve(config, "${FILE_LOG_PATTERN:-"
         + "%d{${LOG_DATEFORMAT_PATTERN:-yyyy-MM-dd HH:mm:ss.SSS}} ${LOG_LEVEL_PATTERN:-%5p} ${PID:- } --- [%t] "
         + "%-40.40logger{39} : %m%n${LOG_EXCEPTION_CONVERSION_WORD:-%wEx}}"));
   config.getContext().putProperty("FILE_LOG_CHARSET", resolve(config, "${FILE_LOG_CHARSET:-default}"));
   config.logger("org.apache.catalina.startup.DigesterFactory", Level.ERROR);
   config.logger("org.apache.catalina.util.LifecycleBase", Level.ERROR);
   config.logger("org.apache.coyote.http11.Http11NioProtocol", Level.WARN);
   config.logger("org.apache.sshd.common.util.SecurityUtils", Level.WARN);
   config.logger("org.apache.tomcat.util.net.NioSelectorPool", Level.WARN);
   config.logger("org.eclipse.jetty.util.component.AbstractLifeCycle", Level.ERROR);
   config.logger("org.hibernate.validator.internal.util.Version", Level.WARN);
   config.logger("org.springframework.boot.actuate.endpoint.jmx", Level.WARN);
}

Appender 创建

private Appender<ILoggingEvent> consoleAppender(LogbackConfigurator config) {
   ConsoleAppender<ILoggingEvent> appender = new ConsoleAppender<>();
   PatternLayoutEncoder encoder = new PatternLayoutEncoder();
   encoder.setPattern(resolve(config, "${CONSOLE_LOG_PATTERN}"));
   encoder.setCharset(resolveCharset(config, "${CONSOLE_LOG_CHARSET}"));
   config.start(encoder);
   appender.setEncoder(encoder);
   config.appender("CONSOLE", appender);
   return appender;
}

绑定到ROOT

final void root(Level level, Appender<ILoggingEvent>... appenders) {
   Logger logger = this.context.getLogger(org.slf4j.Logger.ROOT_LOGGER_NAME);
   if (level != null) {
      logger.setLevel(level);
   }
   for (Appender<ILoggingEvent> appender : appenders) {
      logger.addAppender(appender);
   }
}

2-2、xml 读取

读取文件的规则可以是很多,而且很复杂,我暂时不想去了解这么复杂的逻辑,所以这里只说明入口,不做深入。

不管是怎么得到的xml,最终解析入口都是

protected abstract void loadConfiguration(LoggingInitializationContext initializationContext, String location,
      LogFile logFile);

如果配置了 logging.config 那就直接读取到了xml,来看看没有配置的时候怎么读取到的默认文件

private void initializeWithConventions(LoggingInitializationContext initializationContext, LogFile logFile) {
   String config = getSelfInitializationConfig();
   if (config != null && logFile == null) {
      // self initialization has occurred, reinitialize in case of property changes
      reinitialize(initializationContext);
      return;
   }
   if (config == null) {
      config = getSpringInitializationConfig();
   }
   if (config != null) {
      loadConfiguration(initializationContext, config, logFile);
      return;
   }
   loadDefaults(initializationContext, logFile);
}


// logback默认配置的原因
protected String getSelfInitializationConfig() {
   return findConfig(getStandardConfigLocations());
}
@Override
protected String[] getStandardConfigLocations() {
   return new String[] { "logback-test.groovy", "logback-test.xml", "logback.groovy", "logback.xml" };
}

// spring默认配置,其实它就是获取logback的默认配置,然后加上一个【-spring】后缀
protected String getSpringInitializationConfig() {
   return findConfig(getSpringConfigLocations());
}
protected String[] getSpringConfigLocations() {
   String[] locations = getStandardConfigLocations();
   for (int i = 0; i < locations.length; i++) {
      String extension = StringUtils.getFilenameExtension(locations[i]);
      locations[i] = locations[i].substring(0, locations[i].length() - extension.length() - 1) + "-spring."
            + extension;
   }
   return locations;
}

3、日志的输出

在原生的日志输出里面已经讲解了整个流程,输出过程都是一样的。


K、扩展


1、log读取MDC和application的数据


在日志输出的时候可能会要设置一些自己的参数

  1. 比如全局 traceId,这种情况可以把它设置到MDC里面去,本质上是放入 ThreadLocal中
  2. 读取application中的数据比如 spring.application.name
spring:
  application:
    name: xdx97

logging:
  pattern:
    console: "[${spring.application.name}] [%X{userId}] [%thread] %-5level %logger{36} - %msg%n"

在这里插入图片描述


2、lomback 的 @Slf4j 注解


在项目中大多数时候并不是直接自己注入log类,而是使用 @Slf4j,其本质上是一样的。

但为什么在没有编译之前就可以使用log这个参数,大概率是idea做了什么操作吧,这个等后续有空再研究。

在这里插入图片描述

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

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

相关文章

ByteTrack 论文学习

1. 解决了什么问题&#xff1f; 多目标跟踪是在给定的视频片段中&#xff0c;预测出目标的边框和 ID 信息。现有方法需要在 true positives 和 false positives 之间做取舍&#xff0c;将高于一定阈值的检测框关联起来&#xff0c;获取其 ID。而那些低得分的目标&#xff08;如…

全新的Windows12上线抢先体验

AIGC专栏/AI绘画教程/java面试题领取 win12太离谱了&#xff0c;win11还没用几天&#xff0c;win12就已经出来了&#xff0c;如此流畅的页面&#xff0c;很具有和苹果一拼的效果&#xff0c;流畅度也是一流。文末有领取方式。 WIN12系统在色差表现方面也超越了苹果。它采用了前…

华为数通方向HCIP-DataCom H12-831题库(单选题:141-160)

第141题 R3与R1的IS-IS邻居没有建立,根据本图的信息,可能的原因是? A、R3与R1的IS-Level不匹配 B、R3与R1的互连接口circuit-type不匹配 C、R3与R1的IIH认证失 D、R3与R1的System ID重复 答案: B 解析: 从上图的Bad Circuit Type:16 可知道R3与R1的互连接口circuit-type…

【EI会议征稿】第三届计算机图形学、人工智能与数据处理国际学术会议 (ICCAID 2023)

第三届计算机图形学、人工智能与数据处理国际学术会议 2023 3rd International Conference on Computer Graphics, Artificial Intelligence and Data Processing (ICCAID 2023) 第三届计算机图形学、人工智能与数据处理国际学术会议&#xff08;ICCAID 2023&#xff09;将于…

基于数据驱动的成本洞察,趣丸科技的FinOps进阶之路~

今年以来&#xff0c;我们注意到越来越多的单位开始积极实践FinOps&#xff0c;而随着FinOps的发展&#xff0c;大家对于其落地过程的关注也更加具体和深入&#xff0c;涉及了账单波动、FinOps的边际效应、成本模型、依赖工具等多个关键问题。 本月「UGeek大咖说」线上直播活动…

机器学习之泛化与过拟合的概念

文章目录 泛化&#xff08;Generalization&#xff09;&#xff1a;过拟合&#xff08;Overfitting&#xff09;&#xff1a;例子 泛化&#xff08;Generalization&#xff09;&#xff1a; 泛化是指机器学习模型在未见过的新数据上表现良好的能力。换句话说&#xff0c;一个好…

【软件测试】Junit5

Selenium自动化测试框架Junit单元测试框架拿着一个技术写自动化测试用例 (Selenium3)拿着一个技术管理已经编写好的测试用例 (Junit5) Junit相关技术 Junit是针对java的一个单元测试框架。 注解 Test 表示当前的这个方法是一个测试用例 添加依赖&#xff1a; 不需要main方…

解决react使用redux toolkits时出现的数组对象长度始终为0的怪异问题

有个react项目在添加购物车后&#xff0c;立马白屏&#xff0c;看一下console报错properties of undefined(reading length) 那意思是说数组没有长度&#xff0c;然后定位Header.tsx的182行&#xff0c;果然是数组长度报错 回到具体代码中&#xff1a;发现shoppingCartItems实…

大模型存在“反转诅咒”现象,无法处理反向问题;Langchain课程资源

&#x1f989; AI新闻 &#x1f680; 大模型存在“反转诅咒”现象&#xff0c;无法处理反向问题 摘要&#xff1a;最新研究发现&#xff0c;大语言模型存在“反转诅咒”现象&#xff0c;即明知道“A 是 B”&#xff0c;却答不出“B 是 A”。研究人员进行了两项实验&#xff0…

【RocketMQ专题】快速实战及集群架构原理详解

目录 课程内容一、MQ简介基本介绍*作用&#xff08;解决什么问题&#xff09; 二、RocketMQ产品特点2.1 RocketMQ介绍2.2 RocketMQ特点2.3 RocketMQ的运行架构2.4 消息模型 三、RocketMQ快速实战3.1 快速搭建RocketMQ服务3.2 快速实现消息收发3.3 搭建Maven客户端项目3.4 搭建R…

Linear Feedback Shift Register

线性反馈移位寄存器&#xff08;Linear Feedback Shift Register&#xff0c;简称LFSR&#xff09;是一种数字电路设计和密码学中常用的寄存器类型。它是一种简单而高效的方式&#xff0c;用于生成伪随机的二进制序列&#xff0c;并在数据混淆、错误检测和加密等领域中有应用。…

怎么用蜂邮EDM和Outlook批量发送邮件带附件

蜂邮EDM和Outlook批量发送邮件带附件的流程&#xff1f;有哪些邮件批量发送邮件附件的方法&#xff1f; 在现代社会中&#xff0c;电子邮件是一种广泛应用的沟通工具&#xff0c;而批量发送邮件带附件则是许多商业和个人用户的常见需求。本文将介绍如何使用蜂邮EDM和Outlook这…

时序预测 | MATLAB实现POA-CNN-BiLSTM鹈鹕算法优化卷积双向长短期记忆神经网络时间序列预测

时序预测 | MATLAB实现POA-CNN-BiLSTM鹈鹕算法优化卷积双向长短期记忆神经网络时间序列预测 目录 时序预测 | MATLAB实现POA-CNN-BiLSTM鹈鹕算法优化卷积双向长短期记忆神经网络时间序列预测预测效果基本介绍程序设计参考资料 预测效果 基本介绍 MATLAB实现POA-CNN-BiLSTM鹈鹕算…

吉力宝:智能科技鞋品牌步力宝引领传统产业创新思维

在现代经济环境下&#xff0c;市场经济下产品的竞争非常的激烈&#xff0c;如果没有营销&#xff0c;产品很可能不被大众认可&#xff0c;酒香也怕巷子深&#xff0c;许多传统产业不得不面临前所未有的挑战。而为了冲出这个“巷子”&#xff0c;许多企业需要采用创新思维&#…

单片机外设-串口(UART)详情

目录 学习UART要先认识一些基础知识 一&#xff1a;什么是串行、并行通信&#xff1f; &#xff08;1&#xff09;串行通信 串行通信概念&#xff1a; 串行通信的特点&#xff1a; &#xff08;2&#xff09;并行通信 并行通信概念&#xff1a; 并行通信特点&#xff1…

FairMOT 论文学习

1. 解决了什么问题&#xff1f; 现有的多目标跟踪方案将目标检测和 reID 任务放在一个网络里面优化学习&#xff0c;计算效率高。目标检测首先在每一帧中检测出兴趣目标&#xff0c;要么将其与现有的轨迹关联起来&#xff0c;要么创建一个新的轨迹。这两个任务会相互竞争&…

ElasticSearch - 基于 JavaRestClient 操作索引库和文档

目录 一、RestClient操作索引库 1.1、RestClient是什么&#xff1f; 1.2、JavaRestClient 实现创建、删除索引库 1.2.1、前言 1.2.1、初始化 JavaRestClient 1.2.2、创建索引库 1.2.3、判断索引库是否存在 1.2.4、删除索引库 1.3、JavaRestClient 实现文档的 CRUD 1.3…

简单理解三极管导通条件(从电压角度考虑)

1、本文仅描述三极管如何使用&#xff0c;不对三极管的原理做讲解。 2、本文内容如有错误&#xff0c;欢迎交流指正。 3、本文仅作为本人学习笔记&#xff0c;部分内容来源于网络、书籍&#xff0c;如涉及侵权&#xff0c;请联系删除。 三极管的分类&#xff1a;NPN型、PNP型。…

2023中国国际缝制设备展,正运动助力智能缝纫设备“更快更准”更智能!

■展会名称&#xff1a; 2023中国国际缝制设备展览会 ■展会日期 2023年9月25日-28日 ■展馆地点 上海新国际博览中心E6馆 ■展位号 E6-N09 正运动技术&#xff0c;作为国内领先的运动控制企业&#xff0c;将于9月25日参展2023中国国际缝制设备展。展会将在上海新国际博…

【校招VIP】专业课考点之进程同步

考点介绍&#xff1a; 进程同步是指在多个进程之间进行协调&#xff0c;以确保它们在访问共享资源时能够正确、有序地执行。其中最常见的同步机制是互斥锁和信号量 专业课考点之进程同步-相关题目及解析内容可点击文章末尾链接查看&#xff01; 一、考点试题 1.从执行状态挂…