log4j源码浅析

从0到1带你深入理解log4j2漏洞 0x01前言 最近IT圈被爆出的log4j2漏洞闹的沸沸扬扬,log4j2作为一个优秀的java程序日志监控组件,被应用在了各种各样的衍生框架中,同时也是作为目前java全生态中的基础组件之一,这类组件一旦崩塌将造成不可估量的影响。 从Apache Log4j2 漏洞影响面查询的统计来看,影响多达60644个开源软件,涉及相关版本软件包更是达到了321094个。而本次漏洞的触发方式简单,利用成本极低,可以说是一场java生态的‘浩劫’。本文将从零到一带你深入了解log4j2漏洞。知其所以然,方可深刻理解、有 阅读详情

一. 前言

1. 相关文档

2. 说明

  • log4j可以使用log4j.xml 和 log4j.properties​​​ 两种配置文件, 以下源码解析皆基于log4j.properties​​​ 来展开
  • 本文只讲核心代码, 不是主线的代码不展示出来

3. 代码准备

二. 常用类图

1. Logger的类图

在这里插入图片描述
说明:

  • 在log4j.properties中, 可以配置根logger 和 自定义logger. 根logger的实现类为RootLogger, 自定义logger的实现类为Logger
  • Logger的属性都在父类Category中, 主要为 logger的名字, 输出日志等级, 父节点logger, AppenderAttachableImpl(本logger绑定的appender列表, appdender为日志输出目的地)
  • logger的绑定关系: 类似链表结构, 自定义logger的最终父节点为rootLogger
  • logger的执行顺序: 先遍历执行该logger的所有appender(appdender的日志等级>=当前输出日志等级才执行), 若当前logger的additive=false, 结束. 再执行其父类logger, 直至根logger, 在这之间遇到logger的additive=false就会提前结束

2. LogManager类图

在这里插入图片描述
说明:

  • 日志框架初始化在LogManager中的static代码块中
  • LogManager有一个重要属性RepositorySelector, 意为选择器, 作用是产生一个LoggerRepository
  • LoggerRepository的业务实现类为Hierarchy, Hierarchy里面有一个重要属性Logger root, 就是根logger

3. Appender类图

在这里插入图片描述

说明:

  • 常用Appender有 ConsoleAppender, RollingFileAppender, DailyRollingFileAppender, 如图红色框选部分
  • Appender译为输出目的地, 它存储了日志要输出的等级, 格式, 位置等信息, 作为一个日志输出器

4. Layout类图

在这里插入图片描述
说明: 日志的输出格式. 实现类有
TTCCLayout (输出 耗时 + 线程名 + 日志级别 + logger名 + ndc + 日志内容)
EnhancedPatternLayout (PatternLayout 的增强版本, 不展开讲, 感兴趣的自行看下源码)
HTMLLayout (html的格式, 输出 耗时 + 线程名 + 日志内容 +ogger名 + 日志内容等 )
PatternLayout (通过设置模式, 个性化指定要输出的内容)
SimpleLayout (简易模式, 只输出 日志级别 + 日志内容)
XMLLayout (xml的格式, 输出 logger名 + 时间 +日志级别 + 线程名 + ndc + 日志内容等 )

5. WriterAppender结构图

在这里插入图片描述

三. 获取Logger对象(初始化)

初始化流程

在这里插入图片描述

说明:

  1. 通过Logger.getLogger(Class clazz) 或 Logger.getLogger(String name)进入
  2. 加载LogManager进jvm, 执行静态代码块执行初始化, 创建出RepositorySelector实例及LoggerRepository实例(Hierarchy)
  3. 调用OptionConverter.selectAndConfigure(URL url, String clazz, LoggerRepository hierarchy)方法执行配置. (传递Hierarchy)
  4. 在OptionConverter中根据存在的配置文件类型创建Configurator实例(这里假设配置文件为log4j.properties), 执行Configurator的doConfigure(URL url, LoggerRepository repository) (传递Hierarchy)
  5. 在PropertyConfigurator完成所有组件配置.
  6. configureRootCategory(Properties props, LoggerRepository hierarchy) 配置根logger及其appdender等属性
  7. parseCatsAndRenderers(Properties props, LoggerRepository hierarchy) 配置非根的logger及其appdender等属性, 渲染器等
  8. LogManager完成静态代码块后, 通过Hierarchy.getLogger(String name)方法获取出logger对象

入口:

	入口: Logger.getLogger(Class clazz) 或 Logger.getLogger(String name)
public class LogTest {
    public static void main(String[] args) {
    	// 获取logger对象
    	//Logger logger = Logger.getLogger(LogTest.class.getName());
        Logger logger = Logger.getLogger(LogTest.class);
        System.out.println(logger);
    }
}

进入Logger的Logger getLogger(Class clazz)方法内:

public class Logger extends Category {
  static public Logger getLogger(Class clazz) {
    // 通过LogManager获取logger对象, 此时jvm加载LogManager
    return LogManager.getLogger(clazz.getName());
  }
}

jvm加载LogManager

执行LogManager的static代码块

public class Logger extends Category {
    static {
        // 核心逻辑: 初始化repositorySelector, 并设置选择器的LoggerRepository为Hierarchy, 创建根logger
        Hierarchy h = new Hierarchy(new RootLogger((Level) Level.DEBUG));
        repositorySelector = new DefaultRepositorySelector(h);

        // 重写(过时)
        String override = OptionConverter.getSystemProperty(DEFAULT_INIT_OVERRIDE_KEY, null);
        if (override == null || "false".equalsIgnoreCase(override)) {
            String configurationOptionStr = OptionConverter.getSystemProperty(DEFAULT_CONFIGURATION_KEY, null);
            URL url = null;

            // 核心逻辑: 先查找log4j.xml, 没有再查找log4j.properties文件
            if (configurationOptionStr == null) {
                url = Loader.getResource(DEFAULT_XML_CONFIGURATION_FILE);
                if (url == null) {
                    url = Loader.getResource(DEFAULT_CONFIGURATION_FILE);
                }
            } 

            if (url != null) {
                try {
                    // 核心逻辑: 读取配置, 完成logger初始化
                    OptionConverter.selectAndConfigure(url, configuratorClassName,
                            LogManager1.getLoggerRepository());
                } catch (NoClassDefFoundError e) {}
            } 
        }
    }
}

说明:

  1. 初始化repositorySelector, 并设置选择器的LoggerRepository为Hierarchy, 创建根logger
  2. 先查找log4j.xml, 没有再查找log4j.properties文件
  3. 读取配置, 完成logger初始化 OptionConverter.selectAndConfigure(…);

进入OptionConverter的selectAndConfigure(…)方法中

public class OptionConverter {
    static public void selectAndConfigure(URL url, String clazz, LoggerRepository hierarchy) {
        Configurator configurator = null;
        String filename = url.getFile();

        // 若配置文件为log4j.xml, 则创建DOMConfigurator来解析
        if (clazz == null && filename != null && filename.endsWith(".xml")) {
            clazz = "org.apache.log4j.xml.DOMConfigurator";
        }

        if (clazz != null) {
            LogLog.debug("Preferred configurator class: " + clazz);
            configurator = (Configurator) instantiateByClassName(clazz, Configurator.class, null);
            if (configurator == null) {
                LogLog.error("Could not instantiate configurator [" + clazz + "].");
                return;
            }
        } else {
            // 否则, 则创建PropertyConfigurator来解析
            configurator = new PropertyConfigurator();
        }

        // 进行配置
        configurator.doConfigure(url, hierarchy);
    }
}

说明:
1.配置文件为log4j.xml, 则使用 DOMConfigurator来解析
2.配置文件为log4j.properties, 则使用PropertyConfigurator来解析

进入PropertyConfigurator的doConfigure(URL configURL, LoggerRepository hierarchy)方法

public class PropertyConfigurator implements Configurator {
	// 几个属性解释
	// registry注册器, 用于初始化过程中缓存 已完成初始化的appender, 初始化完成后会重置
    protected Hashtable registry = new Hashtable(11);  
    // logger仓库, 存储hierarchy对象
    private LoggerRepository repository;
    // logger工厂, 使用来创建自定义的logger对象
    protected LoggerFactory loggerFactory = new DefaultCategoryFactory();

    public void doConfigure(java.net.URL configURL, LoggerRepository hierarchy) {
        // 读取配置文件内容, 写入props中
        Properties props = new Properties();
        InputStream istream = null;
        URLConnection uConn = null;
        try {
            uConn = configURL.openConnection();
            uConn.setUseCaches(false);
            istream = uConn.getInputStream();
            props.load(istream);
        } catch (Exception e) {} finally {}
        
        // 继续进行配置
        doConfigure(props, hierarchy);
    }
}

进入PropertyConfigurator的doConfigure(Properties properties, LoggerRepository hierarchy)方法

    public void doConfigure(Properties properties, LoggerRepository hierarchy) {
        // 缓存hierarchy, 方便后续使用
        repository = hierarchy;

        // 判断配置中log4j.debug是否开启, 开启则后续会打印log4j项目内的debug级别的日志
        String value = properties.getProperty(LogLog.DEBUG_KEY);
        if (value == null) {
            value = properties.getProperty("log4j.configDebug");
        }
        if (value != null) {
            LogLog.setInternalDebugging(OptionConverter.toBoolean(value, true));
        }

        // 判断配置中log4j.reset是否开启, 是则重置hierarchy
        String reset = properties.getProperty(RESET_KEY);
        if (reset != null && OptionConverter.toBoolean(reset, false)) {
            hierarchy.resetConfiguration();
        }

        // 获取配置中的log4j.threshold, 即全局的日志级别, 设置后低于该级别的日志都不能输出, 不设置则默认为ALL
        String thresholdStr = OptionConverter.findAndSubst(THRESHOLD_PREFIX, properties);
        if (thresholdStr != null) {
            hierarchy.setThreshold(OptionConverter.toLevel(thresholdStr, (Level) Level.ALL));
        }

        // 核心代码: 配置根logger及其appender
        configureRootCategory(properties, hierarchy);
        // 配置LoggerFactory
        configureLoggerFactory(properties);
        // 核心代码: 配置自定义的logger及其appender, 渲染器等
        parseCatsAndRenderers(properties, hierarchy);

        LogLog.debug("Finished configuring.");
        // 去除registry缓存(该缓存作用: 缓存初始化过程中的appender), 以便不影响垃圾回收
        registry.clear();
    }

说明:
1. 对一些全局的属性进行配置, 如是否开启debug(log4j.debug), 是否重置(log4j.reset), 日志级别(log4j.threshold)
2. 执行核心代码: 配置根logger及其appender
3. 执行核心代码: 配置自定义的logger及其appender, 以及一些异常处理器等

进入PropertyConfigurator的configureRootCategory(…)方法中

    void configureRootCategory(Properties props, LoggerRepository hierarchy) {
        // 获取log4j.rootLogger的配置信息. 
        String effectiveFrefix = ROOT_LOGGER_PREFIX;
        // OptionConverter.findAndSubst(..)方法是获取指定key前缀的配置, 赋值到props中, 后续经常使用
        String value = OptionConverter.findAndSubst(ROOT_LOGGER_PREFIX, props);

        if (value == null) {
            // 没配置log4j.rootLogger, 则获取log4j.rootCategory
            value = OptionConverter.findAndSubst(ROOT_CATEGORY_PREFIX, props);
            effectiveFrefix = ROOT_CATEGORY_PREFIX;
        }

        if (value == null){
            LogLog.debug("Could not find root logger information. Is this OK?");
        } else {
            // 获取出hierarchy中的根logger
            Logger root = hierarchy.getRootLogger();
            synchronized (root) {
                // 核心代码: 解析根logger 及绑定它的appender (后续自定义的loggger也是走这个逻辑)
                parseCategory(props, root, effectiveFrefix, INTERNAL_ROOT_NAME, value);
            }
        }
    }

说明:
1. 获取log4j.rootLogger的配置信息, 进入parseCategory(…)方法中, 解析根logger 及绑定它的appender
2. 后续自定义的loggger也是走PropertyConfigurator的parseCategory(…)方法

进入PropertyConfigurator的parseCategory(…)方法

    void parseCategory(Properties props, Logger logger, String optionKey,
                       String loggerName, String value) {

        // 将logger的配置信息按照逗号分隔, 示例: log4j.rootLogger=info,console,rollingFile, 分隔后得到info 和 console及rollingFile
        StringTokenizer st = new StringTokenizer(value, ",");

        if (!(value.startsWith(",") || value.equals(""))) {
            if (!st.hasMoreTokens()){
                return;
            }

            // 获取当前logger的日志级别level. 第一个参数
            String levelStr = st.nextToken();

            // 非法的level值判断
            if (INHERITED.equalsIgnoreCase(levelStr) || NULL.equalsIgnoreCase(levelStr)) {
                if (loggerName.equals(INTERNAL_ROOT_NAME)) {
                    LogLog.warn("The root logger cannot be set to null.");
                } else {
                    logger.setLevel(null);
                }
            } else {
                // 设置logger的level值, 不填则设置为DEBUG
                // (不填level这时第一个appenderName就被无效掉了, 示例log4j.rootLogger=console,rollingFile, 其中console就被丢弃了)
                logger.setLevel(OptionConverter.toLevel(levelStr, (Level) Level.DEBUG));
            }
            LogLog.debug("Category " + loggerName + " set to " + logger.getLevel());
        }

        // 移除所有的appender
        logger.removeAllAppenders();

        Appender appender;
        String appenderName;
        // 遍历log4j.rootLogger中的appenderName
        while (st.hasMoreTokens()) {
            appenderName = st.nextToken().trim();
            if (appenderName == null || appenderName.equals(",")){
                continue;
            }
            // 核心代码: 根据appenderName解析出appender
            appender = parseAppender(props, appenderName);
            if (appender != null) {
                // 将appender加入到 logger的AppenderAttachableImpl的appenderList中
                logger.addAppender(appender);
            }
        }
    }

说明:
1. 获取logger的配置, 设置日志级别level. (注: level值省略会导致第一个appdend丢失)
2. 执行核心代码: 根据appenderName解析出appender
3. 将appender加入到 logger的AppenderAttachableImpl的appenderList中

下面简单看一下logger.addAppender(appender);方法

// Category类的方法
  AppenderAttachableImpl aai;	
  synchronized public void addAppender(Appender newAppender) {
    if(aai == null) {
      aai = new AppenderAttachableImpl();
    }
    aai.addAppender(newAppender);
    repository.fireAddAppenderEvent(this, newAppender);
  }

// AppenderAttachableImpl 类的addAppender
public class AppenderAttachableImpl implements AppenderAttachable {
  protected Vector  appenderList;
  public void addAppender(Appender newAppender) {
    if(newAppender == null){
    	return;
    }
    if(appenderList == null) {
      appenderList = new Vector(1);
    }
    if(!appenderList.contains(newAppender))
      appenderList.addElement(newAppender);
  }
 }

进入PropertyConfigurator的parseAppender(…)方法中

    Appender parseAppender(Properties props, String appenderName) {
        // 根据appenderName获取registry缓存中的appender, 若存在则直接放回
        Appender appender = registryGet(appenderName);
        if ((appender != null)) {
            LogLog.debug("Appender \"" + appenderName + "\" was already parsed.");
            return appender;
        }

        // 注. 这里就写死了配置文件中layout只能是小写的.
        String prefix = "log4j.appender." + appenderName;
        String layoutPrefix = prefix + ".layout";

        // 通过配置文件中类的全限定名实例化appender对象. OptionConverter.instantiateByKey(....)该方法会经常使用到
        appender = (Appender) OptionConverter.instantiateByKey(props, prefix, Appender.class, null);
        if (appender == null) {
            LogLog.error("Could not instantiate appender named \"" + appenderName + "\".");
            return null;
        }
        // 设置appender的name
        appender.setName(appenderName);

        if (appender instanceof OptionHandler) {
            if (appender.requiresLayout()) {
                // 通过配置文件中类的全限定名实例化layout对象
                Layout layout = (Layout) OptionConverter.instantiateByKey(props, layoutPrefix, Layout.class, null);
                if (layout != null) {
                    // 设置appender的layout
                    appender.setLayout(layout);
                    LogLog.debug("Parsing layout options for \"" + appenderName + "\".");
                    // 反射设置layout属性(例如PatternLayout的conversionPattern属性), 初始化解析器(代码位置: org.apache.log4j.helpers.PatternParser.parse). 详情见[补充: PatternLayout的setConversionPattern及模式初始化]
                    PropertySetter.setProperties(layout, props, layoutPrefix + ".");
                    LogLog.debug("End of parsing for \"" + appenderName + "\".");
                }
            }

            // 初始化异常处理器
            final String errorHandlerPrefix = prefix + ".errorhandler";
            String errorHandlerClass = OptionConverter.findAndSubst(errorHandlerPrefix, props);
            if (errorHandlerClass != null) {
                ErrorHandler eh = (ErrorHandler) OptionConverter.instantiateByKey(props,
                        errorHandlerPrefix,
                        ErrorHandler.class,
                        null);
                if (eh != null) {
                    // 设置appender的errorHandler
                    appender.setErrorHandler(eh);
                    LogLog.debug("Parsing errorhandler options for \"" + appenderName + "\".");
                    parseErrorHandler(eh, errorHandlerPrefix, props, repository);
                    final Properties edited = new Properties();
                    final String[] keys = new String[]{
                            errorHandlerPrefix + "." + ROOT_REF,
                            errorHandlerPrefix + "." + LOGGER_REF,
                            errorHandlerPrefix + "." + APPENDER_REF_TAG
                    };
                    for (Iterator iter = props.entrySet().iterator(); iter.hasNext(); ) {
                        Map.Entry entry = (Map.Entry) iter.next();
                        int i = 0;
                        for (; i < keys.length; i++) {
                            if (keys[i].equals(entry.getKey())) {
                                break;
                            }
                        }
                        if (i == keys.length) {
                            edited.put(entry.getKey(), entry.getValue());
                        }
                    }
                    PropertySetter.setProperties(eh, edited, errorHandlerPrefix + ".");
                    LogLog.debug("End of errorhandler parsing for \"" + appenderName + "\".");
                }

            }
            // 反射设置appender的其他属性(如encoding、threshold、maxFileSize、datePattern). PropertySetter.setProperties(...)该方法会经常使用到
            PropertySetter.setProperties(appender, props, prefix + ".");
            LogLog.debug("Parsed \"" + appenderName + "\" options.");
        }
        // 解析appender的Filter. 使用较少, 这里不展开讲
        parseAppenderFilters(props, appenderName, appender);
        // 加入registry缓存中
        registryPut(appender);
        return appender;
    }

说明:
1.根据appenderName获取registry缓存中的appender, 若存在则直接放回(appender可能被其他logger就完成初始化了, )
2.初始化日志输出器appender对象
3.初始化布局layout对象, 并设置到appender中(配置文件的key中layout必须为小写)
4.初始化异常处理器errorHandler, 并设置到appender中
5.初始化过滤器filter, 并设置到appender中

补充: PatternLayout的setConversionPattern及模式初始化

public class PatternLayout extends Layout {
	// 输出日志内容的buffer
	private StringBuffer sbuf = new StringBuffer(BUF_SIZE);
	// 设置的模式, 默认值%m%n
    private String pattern;
	// 模式转换器, 链式结构
    private PatternConverter head;

   	// 实例化时
   	public PatternLayout() {
     	this("%m%n");
   	}

   	public PatternLayout(String pattern) {
     	this.pattern = pattern;
     	// 核心代码: parse()方法
     	head = createPatternParser((pattern == null) ? DEFAULT_CONVERSION_PATTERN : pattern).parse();
   	}
    
	// 如果有layout.conversionPattern配置, 初始化时, 反射调用该方法, 完成模式转换器链初始化
    public void setConversionPattern(String conversionPattern) {
        pattern = conversionPattern;
        // 核心代码: parse()方法
        head = createPatternParser(conversionPattern).parse();
    }

	// 创建PatternParser实例, 该类左右是用来初始化 模式转换器链PatternConverter head
	protected PatternParser createPatternParser(String pattern) {
        return new PatternParser(pattern);
    }
}

public class PatternParser {
	// 临时模式转换器头结点
  	PatternConverter head;
  	// 临时模式转换器尾结点
  	PatternConverter tail;
  	// 设置的模式值
  	protected String pattern;
  	// 模式解析. 使用状态机模式逐字符解析pattern属性. 这里代码非常多, 不展开讲, 直接说逻辑
    public PatternConverter parse() {
    	// 状态机模式, 需要进一步了解的请直接看源码, 下面贴出状态机的状态流转图
        char c;
        i = 0;
        while (i < patternLength) {
            c = pattern.charAt(i++);
            switch (state) {
                case LITERAL_STATE: ... break;
                case CONVERTER_STATE: ... break;
                case MIN_STATE: ... break;
                case DOT_STATE: ... break;
                case MAX_STATE: ... break;
            }
        }
		...
        return head;
    }
	
	protected void finalizeConverter(char c) {
		// 解析%后的字母, 如c, C, d, F, l, L等等
        PatternConverter pc = null;
        switch (c) {
            case 'c':
            	// 创建出PatternConverter的实现类CategoryPatternConverter
            	// extractPrecisionOption()方法用户获取pattern中{}内的值
                pc = new CategoryPatternConverter(formattingInfo, extractPrecisionOption());
                currentLiteral.setLength(0);
                break;
            //........
            case 'X':
                String xOpt = extractOption();
                // 创建出PatternConverter的实现类MDCPatternConverter. mdc可以用来做日志链路追踪
                // 链路追踪拓展: https://blog.youkuaiyun.com/Erica_java/article/details/128616137
                pc = new MDCPatternConverter(formattingInfo, xOpt);
                currentLiteral.setLength(0);
                break;
            default: ....;
        }
		// 将PatternConverter按照链式结果连接起来. 这里使用head和tail节点维护节点, 最终返回head到PatternLayout中
        addConverter(pc);
	}
}

状态机状态流转图
在这里插入图片描述

进入PropertyConfigurator的configureLoggerFactory(Properties props)方法中

    protected void configureLoggerFactory(Properties props) {
        // 获取配置文件loggerFactory全限定名, 创建loggerFactory实例
        String factoryClassName = OptionConverter.findAndSubst("log4j.loggerFactory", props);
        if (factoryClassName != null) {
            LogLog.debug("Setting category factory to [" + factoryClassName + "].");
            loggerFactory = (LoggerFactory) 
                    OptionConverter.instantiateByClassName(factoryClassName, LoggerFactory.class, loggerFactory);
            // 反射设置loggerFactory的属性
            PropertySetter.setProperties(loggerFactory, props, "log4j.factory" + ".");
        }
    }

说明:
1.获取配置文件loggerFactory全限定名, 创建loggerFactory实例
2.反射设置loggerFactory的属性
注: 不配置时, loggerFactory使用默认值, 为DefaultCategoryFactory实例对象. loggerFactory用于创建自定义的logger对象

进入PropertyConfigurator的parseCatsAndRenderers(…)方法中

    protected void parseCatsAndRenderers(Properties props, LoggerRepository hierarchy) {
        // 遍历Properties中所有的key
        Enumeration enumeration = props.propertyNames();
        while (enumeration.hasMoreElements()) {
            String key = (String) enumeration.nextElement();
            if (key.startsWith(CATEGORY_PREFIX) || key.startsWith(LOGGER_PREFIX)) {
                // 解析所有log4j.category.开头和log4j.logger.开头的key
                String loggerName = null;
                if (key.startsWith(CATEGORY_PREFIX)) {
                    loggerName = key.substring(CATEGORY_PREFIX.length());
                } else if (key.startsWith(LOGGER_PREFIX)) {
                    loggerName = key.substring(LOGGER_PREFIX.length());
                }
                String value = OptionConverter.findAndSubst(key, props);
                // 核心代码: 创建出自定义logger对象, 绑定其与其他logger的关系
                Logger logger = hierarchy.getLogger(loggerName, loggerFactory);
                synchronized (logger) {
                    // 核心代码: 解析非根logger 及绑定它的appender(这里和根logger解析是同一个方法)
                    parseCategory(props, logger, key, loggerName, value);
                    // 解析additivity属性, 设置到logger中
                    parseAdditivityForLogger(props, logger, loggerName);
                }
            } else if (key.startsWith(RENDERER_PREFIX)) {
                // 解析所有log4j.renderer.开头的key, 初始化和绑定渲染器. 较少使用, 不展开讲
                String renderedClass = key.substring(RENDERER_PREFIX.length());
                String renderingClass = OptionConverter.findAndSubst(key, props);
                if (hierarchy instanceof RendererSupport) {
                    RendererMap.addRenderer((RendererSupport) hierarchy, renderedClass,
                            renderingClass);
                }
            } else if (key.equals(THROWABLE_RENDERER_PREFIX)) {
                // 解析所有log4j.throwableRenderer开头的异常渲染器, 初始化和绑定异常渲染器. 较少使用, 不展开讲
                if (hierarchy instanceof ThrowableRendererSupport) {
                    ThrowableRenderer tr = (ThrowableRenderer)
                            OptionConverter.instantiateByKey(props, THROWABLE_RENDERER_PREFIX, ThrowableRenderer.class, null);
                    if (tr == null) {
                        LogLog.error("Could not instantiate throwableRenderer.");
                    } else {
                        PropertySetter setter = new PropertySetter(tr);
                        setter.setProperties(props, THROWABLE_RENDERER_PREFIX + ".");
                        ((ThrowableRendererSupport) hierarchy).setThrowableRenderer(tr);
                    }
                }
            }
        }
    }

进入Hierarchy的getLogger(String name, LoggerFactory factory)方法中

    public Logger getLogger(String name, LoggerFactory factory) {
        CategoryKey key = new CategoryKey(name);
        Logger logger;
        synchronized (ht) {
            Object o = ht.get(key);
            if (o == null) {
                // 为空. 创建logger实例. 默认实现 Logger logger = new Logger(name)
                logger = factory.makeNewLoggerInstance(name);
                logger.setHierarchy(this);
                // 写入Hierarchy的ht(Hashtable)缓存中
                ht.put(key, logger);
                // 绑定当前logger和父logger
                updateParents(logger);
                return logger;
            } else if (o instanceof Logger) {
                // Logger类型. 直接返回
                return (Logger) o;
            } else if (o instanceof ProvisionNode) {
                // 临时节点类型. 先创建出该节点
                logger = factory.makeNewLoggerInstance(name);
                logger.setHierarchy(this);
                // 写入Hierarchy的ht(Hashtable)缓存中
                ht.put(key, logger);
                // 找到创建logger之前临时存储的子节点, 重新绑定(子logger->旧parent  =>  子logger->当前logger->旧parent)
                updateChildren((ProvisionNode) o, logger);
                // 绑定当前logger和父logger
                updateParents(logger);
                return logger;
            } else {
                return null;
            }
        }
    }

说明:
1.Hierarchy有个属性Hashtable ht, 用于缓存初始化时已创建的appender和临时节点ProvisionNode
临时节点ProvisionNode介绍: ht存储临时节点示例 com.clj -> [logger(com.clj.abc), logger(com.clj.def)]. 其中com.clj为待logger的name, 列表中元素为已实例化的logger(具体使用见Hierarchy.updateChildren(…)和Hierarchy.updateParents(.)方法)
2.若ht中未找到值, 则创建logger, 加入ht缓存, 绑定当前logger和父logger
3.若ht中找到值且为Logger类型, 直接返回
4.若ht中找到值且为ProvisionNode类型, 则创建logger, 加入ht缓存, 绑定当前logger和子logger, 绑定当前logger和父logger

进入Hierarchy的updateParents(Logger cat)方法中

    final private void updateParents(Logger cat) {
        // 示例logger的name = com.clj.abc, 假设初始化到当前只解析了 根logger和logger(com.clj.abc)
        String name = cat.name;
        int length = name.length();
        // 标识是否找到父类了
        boolean parentFound = false;

        // 这里遍历出com.clj 和 com
        for (int i = name.lastIndexOf('.', length - 1); i >= 0;
             i = name.lastIndexOf('.', i - 1)) {
            // 第一次遍历得到com.clj. 第二次遍历得到com
            String substr = name.substring(0, i);
            CategoryKey key = new CategoryKey(substr);
            Object o = ht.get(key);
            // Create a provision node for a future parent.
            if (o == null) {
                // ht中没缓存到, 则创建出临时节点, 临时节点存储的是当前的logger(com.clj.abc)
                ProvisionNode pn = new ProvisionNode(cat);
                // ht缓存临时节点. 第一次遍历(缓存了com.clj -> [logger(com.clj.abc)]), 第二次遍历(缓存了com -> [logger(com.clj.abc)])
                ht.put(key, pn);
            } else if (o instanceof Category) {
                // 若ht中得到是Category类型, 直接绑定当前logger的父节点, 修改parentFound标识
                parentFound = true;
                cat.parent = (Category) o;
                break;
            } else if (o instanceof ProvisionNode) {
                // 若ht中得到是ProvisionNode类型, 则将当前cat加入列表.
                // 这里假设之前o的列表为 [logger(com.clj.def)], 添加后列表为 [logger(com.clj.def), logger(com.clj.abc)],
                //  对应ht中的缓存键值对为com.clj -> [logger(com.clj.def), logger(com.clj.abc)]
                ((ProvisionNode) o).addElement(cat);
            } else {
                Exception e = new IllegalStateException("unexpected object type " + o.getClass() + " in ht.");
                e.printStackTrace();
            }
        }
        // 暂没找到父类, 那直接设置根logger为父类
        if (!parentFound) {
            cat.parent = root;
        }
    }

进入Hierarchy的updateChildren(ProvisionNode pn, Logger logger)方法中

    final private void updateChildren(ProvisionNode pn, Logger logger) {
        // 遍历临时节点中所有的子节点
        final int last = pn.size();
        for (int i = 0; i < last; i++) {
            Logger l = (Logger) pn.elementAt(i);
            // 若子节点的 原父logger 不是当前logger, 则设置子节点的 父节点为 当前logger, 设置当前logger的 父节点为 原父logger
            // 示例: 当前logger(com.clj), 子节点logger(com.clj.abc), 子节点的父logger为 root, 即 logger(com.clj.abc) -> logger(root)
            //       处理后为: logger(com.clj.abc) -> logger(com.clj) -> logger(root)
            if (!l.parent.name.startsWith(logger.name)) {
                logger.parent = l.parent;
                l.parent = logger;
            }
        }
    }

进入PropertyConfigurator的parseAdditivityForLogger(…)方法中

    void parseAdditivityForLogger(Properties props, Logger cat, String loggerName) {
        // 获取additivity属性(key为log4j.additivity.[loggerName])
        String value = OptionConverter.findAndSubst(ADDITIVITY_PREFIX + loggerName, props);
        LogLog.debug("Handling " + ADDITIVITY_PREFIX + loggerName + "=[" + value + "]");
        // 设置到logger的additive字段中(作用: 表示打印本logger日志后, 是否将日志添加到父级logger日志中处理, 默认值为true)
        if ((value != null) && (!value.equals(""))) {
            boolean additivity = OptionConverter.toBoolean(value, true);
            LogLog.debug("Setting additivity for \"" + loggerName + "\" to " + additivity);
            cat.setAdditivity(additivity);
        }
    }

说明:
1.获取additivity属性(key为log4j.additivity.[loggerName])
2.设置到logger的additive字段中
3.logger的additive字段作用: 打印本logger日志后, 是否将日志添加到父级logger日志中处理, 默认值为true

四. 执行日志打印

流程

在这里插入图片描述

说明:

  1. 从main方法出发, 这里示例是调用info()方法. (故后续都是判断info级别)
  2. 判断是否满足全局的level级别>=info, 满足则 再判断本logger的level级别是否>=info(本logger没有值则找其父类直至有值)
  3. 全局level或本体系的level有其一不满足>=info, 提前结束流程
  4. 执行本logger再递归父类logger, 执行其aai.appendLoopOnAppenders()方法 (注: 当前logger执行完会判断additive属性, 为false则不再递归父类, 该点在流程图第20步体现)
  5. 遍历当前logger的日志输出器appender, 执行其doAppend()方法
  6. 进入appender的公共父类AppenderSkeleton的doAppend()方法, 先判断当前appender的日志级别level是否>=info
  7. 当前appender日志级别level<info, 结束该appender的执行, 遍历下一个appender
  8. 递归执行本appender的过滤器执行链
  9. Filter中进行过滤判定
  10. 不满足过滤条件则结束该appender的执行, 遍历下一个appender
  11. 执行当前appender的append()方法. (这里假设实现类为RollingFileAppender)
  12. 进入appender的公共父类WriterAppender的append()方法, 执行子类RollingFileAppender的subAppend()方法
  13. 进入RollingFileAppender的subAppend()方法, 立即调用父类WriterAppender的subAppend()方法, 执行后再执行本类的滚动文件逻辑(该点在流程图第19步体现)
  14. 在WriterAppender的subAppend()方法中调用layout执行内容格式化
  15. layout完成输出内容格式化, 返回内容信息
  16. 调用QuietWriter将内容写入缓存
  17. 在WriterAppender中, 判断属性是否为立即输出, 是则调用QuietWriter写出内容
  18. QuietWriter将内容写出到文件中(若appender为ConsoleAppender则是输出到控制台)
  19. 回到RollingFileAppender的subAppend()方法中, 判断是否满足滚动文件逻辑, 是则执行rollOver()
  20. 遍历完当前logger后, 判断当前logger的additive属性, 为false则不再递归父类, 提前结束流程

入口

public class LogTest {
    public static void main(String[] args) {
    	Logger logger = Logger.getLogger(LogTest.class);
    	// 入口. 这里示例为输出info级别的日志
        logger.info("这是info级别的日志");
    }
}

进入Category的info(Object message)方法

    public void info(Object message) {
        // 判断全局日志级别是否 >= info, 不满足则退出.
        // (repository为初始化时 LogManager的repositorySelector属性的LoggerRepository属性 -> Hierarchy)
        if (repository.isDisabled(Level.INFO_INT)) {
            return;
        }
        // 获取本logger继承体系中有效的level, 判断是否满足日志级别是否 >= info, 不满足则退出.
        if (Level.INFO.isGreaterOrEqual(this.getEffectiveLevel())) {
        	// 核心代码: 输出日志
            forcedLog(FQCN, Level.INFO, message, null);
        }
    }

说明:

  1. 判断全局日志级别是否 >= info, 不满足则退出 (全局日志级别配置key为log4j.threshold, 存储在Hierarchy中)
  2. 获取本logger继承体系中有效的level, 判断是否满足日志级别是否 >= info, 不满足则退出.

进入Category的getEffectiveLevel()方法

    public Level getEffectiveLevel() {
        // 递归当前logger及其父类, 直到找到有效的level, 返回
        for (Category c = this; c != null; c = c.parent) {
            if (c.level != null) {
                return c.level;
            }
        }
        return null;
    }

进入Category的forcedLog(…)方法

    protected void forcedLog(String fqcn, Priority level, Object message, Throwable t) {
        // 将要输出的基础日志内容,level,logger等封装到LoggingEvent中
        callAppenders(new LoggingEvent(fqcn, this, level, message, t));
    }

进入Category的callAppenders(LoggingEvent event)方法

    public void callAppenders(LoggingEvent event) {
        int writes = 0;

        // 递归本logger及其继承体系的logger
        for (Category c = this; c != null; c = c.parent) {
            synchronized (c) {
                // 核心代码: 获取logger属性AppenderAttachableImpl中的appenderList, 遍历执行appender
                if (c.aai != null) {
                    writes += c.aai.appendLoopOnAppenders(event);
                }
                // 若当前logger的additive为false, 则不再执行其父类logger, 结束日志输出
                if (!c.additive) {
                    break;
                }
            }
        }

        // 本logger及其继承体系中, 一个appender都没有找到, 输出warn的警告日志(项目启动后只会执行一次)
        if (writes == 0) {
            repository.emitNoAppenderWarning(this);
        }
    }

说明:

  1. 递归本logger及其继承体系的logger, 直至当前logger的additive属性为false
  2. 获取logger属性AppenderAttachableImpl中的appenderList, 遍历执行appender
  3. 若递归后一个appender都没有, 则输出warn的警告日志(项目启动后只能执行一次)

进入AppenderAttachableImpl的appendLoopOnAppenders(LoggingEvent event)方法中

    public int appendLoopOnAppenders(LoggingEvent event) {
        // 记录当前appender个数返回
        int size = 0;
        Appender appender;

        if (appenderList != null) {
            size = appenderList.size();
            // 核心代码: 遍历执行每个appender的doAppend()方法
            for (int i = 0; i < size; i++) {
                appender = (Appender) appenderList.elementAt(i);
                appender.doAppend(event);
            }
        }
        return size;
    }

进入AppenderSkeleton的doAppend(LoggingEvent event)方法中

注: 在这之后的示例使用Appender的实现类RollingFileAppender
注: 在这之后的示例使用Appender的实现类RollingFileAppender
注: 在这之后的示例使用Appender的实现类RollingFileAppender

    public synchronized void doAppend(LoggingEvent event) {
        if (closed) {
            LogLog.error("Attempted to append to closed appender named [" + name + "].");
            return;
        }

        // 判断当前appender的日志级别是否 >= 要输出的日志级别
        if (!isAsSevereAsThreshold(event.getLevel())) {
            return;
        }

        // 遍历过滤器链, 判断是否可以往下执行
        Filter f = this.headFilter;
        FILTER_LOOP:
        while (f != null) {
            switch (f.decide(event)) {
                case Filter.DENY:
                    return;
                case Filter.ACCEPT:
                    break FILTER_LOOP;
                case Filter.NEUTRAL:
                    f = f.getNext();
            }
        }

        // 执行子类append(LoggingEvent event)方法 (注: 在这之后的示例使用Appender的实现类RollingFileAppender)
        this.append(event);
    }

说明:

  1. 判断当前appender的日志级别是否 >= 要输出的日志级别, 不大于则退出
  2. 遍历过滤器链, 判断是否被拦截, 拦截则结束
  3. 执行子类append(LoggingEvent event)方法 (注: 在这之后的示例使用Appender的实现类RollingFileAppender)

进入WriterAppender的append(LoggingEvent event)方法中

注: 示例使用Appender的实现类RollingFileAppender

    public void append(LoggingEvent event) {
        // 判断QuietWriter和Layout是否为空, 为空则结束
        if (!checkEntryConditions()) {
            return;
        }
        // 核心代码: 进入RollingFileAppender的subAppend(LoggingEvent event)方法中
        subAppend(event);
    }

进入RollingFileAppender的subAppend(LoggingEvent event)方法中

    protected void subAppend(LoggingEvent event) {
    	// 核心代码: 执行父类WriterAppender的subAppend(LoggingEvent event)方法
    	super.subAppend(event);
	    if(fileName != null && qw != null) {
	    	// 超过文件最大size, 滚动文件
        	long size = ((CountingQuietWriter) qw).getCount();
        	if (size >= maxFileSize && size >= nextRollover) {
        		// 次核心代码: 滚动文件, 这里不展开讲. 
        		// 拓展: 若使用的appender为DailyRollingFileAppender, 需自动删除过期文件, 请查看[\[log4j配置详解及源码\]](https://blog.youkuaiyun.com/Java_dragon95/article/details/129953654)-[自定义DailyRollingFileAppender实现类, 自动删除过期日志]
            	rollOver();
        	}
    	}
    }

进入WriterAppender的subAppend(LoggingEvent event)方法中

	protected QuietWriter qw;
    protected void subAppend(LoggingEvent event) {
        // 核心代码: 格式化输出内容, 将格式化后的内容写入QuietWriter (write时出现异常会使用异常处理器errorHandler)
        // 假设Layout的实现类为PatternLayout
        this.qw.write(this.layout.format(event));

        // 处理的异常对象不包含在layout处理范围内, 则使用渲染器ThrowableRenderer渲染要输出的异常信息
        if (layout.ignoresThrowable()) {
            String[] s = event.getThrowableStrRep();
            if (s != null) {
                int len = s.length;
                for (int i = 0; i < len; i++) {
                    this.qw.write(s[i]);
                    this.qw.write(Layout.LINE_SEP);
                }
            }
        }
        // immediateFlush=true, 则立即写出到文件 (bufferedIO=true时, immediateFlush会被置为false)
        if (shouldFlush(event)) {
            this.qw.flush();
        }
    }

说明:

  1. 使用Layout格式化输出内容
  2. 使用QuietWriter写入格式化后的日志内容, write时出现异常会使用异常处理器errorHandler
  3. 处理的异常对象不包含在layout处理范围内, 则使用渲染器ThrowableRenderer渲染要输出的异常信息
  4. immediateFlush=true, 则立即写出到文件 (bufferedIO=true时, immediateFlush会被置为false, 这是要达到buffer大小才会写出)

进入PatternLayout的format(LoggingEvent event)方法

注: 上面已假设Layout的实现类为PatternLayout

    public String format(LoggingEvent event) {
        // 重置buffer容量 (因为有些大日志输出时会将buffer的容量变得很大)
        if (sbuf.capacity() > MAX_CAPACITY) {
            sbuf = new StringBuffer(BUF_SIZE);
        } else {
            sbuf.setLength(0);
        }

        // 遍历模式转化器链, 获取一个个模式转换器, 格式化拼接好日志内容
        PatternConverter c = head;
        while (c != null) {
        	// 按照具体的PatternConverter实现类, 格式化当前转化器负责的内容, 拼接到sbuf中. 
            c.format(sbuf, event);
            c = c.next;
        }
        // 返回最终的日志输出内容
        return sbuf.toString();
    }

进入父类PatternConverter的format(StringBuffer sbuf, LoggingEvent e)方法

注: 这里不列举所有子类重写的format方法

public abstract class PatternConverter {
    // 下一个模式转换器
    public PatternConverter next;
    // 小数点左侧的值
    int min = -1;
    // 小数点右侧的值
    int max = 0x7FFFFFFF;
    // 左对齐标志
    boolean leftAlign = false;
    /**
    * 转换出部分内容
    */
    abstract protected String convert(LoggingEvent event);

	public void format(StringBuffer sbuf, LoggingEvent e) {
		// 执行子类的convert方法
        String s = convert(e);
        if (s == null) {
            // 直接拼接min个空格
            if (0 < min) {
                spacePad(sbuf, min);
            }
            return;
        }

        int len = s.length();
        // 实际内容大于 阈值, 保留指定阈值长度右侧内容
        if (len > max) {
            sbuf.append(s.substring(len - max));
        } else if (len < min) {
            // 左对齐, 先拼接内容再拼接多余空格
            if (leftAlign) {
                sbuf.append(s);
                spacePad(sbuf, min - len);
            }
            // 右对齐, 先拼接多余空格再拼接内容
            else {
                spacePad(sbuf, min - len);
                sbuf.append(s);
            }
        } else {
            // 没有额外设置, 则直接拼接内容
            sbuf.append(s);
        }
    }

    static String[] SPACES = {" ", "  ", "    ", "        ", "                ", "                                "};
    /**
     * 有几个空格就拼接几个空格
     */
    public void spacePad(StringBuffer sbuf, int length) {
        while (length >= 32) {
            sbuf.append(SPACES[5]);
            length -= 32;
        }
        for (int i = 4; i >= 0; i--) {
            if ((length & (1 << i)) != 0) {
                sbuf.append(SPACES[i]);
            }
        }
    }
}

进入QuietWriter的write(String string)方法中

    public void write(String string) {
    	if (string != null) {
    		try {
    			// 写入日志
      			out.write(string);
    		} catch(Exception e) {
    			// 使用异常处理器处理异常. 该异常处理器就是appender初始化时appender内的异常处理器
      			errorHandler.error("Failed to write ["+string+"].", e, ErrorCode.WRITE_FAILURE);
	    	}
    	}
    }
Log4j2完全讲解 附项目集成源码下载 Log4j2完全讲解     本文内容大多翻译自log4j官网。     Apache的开源项目log4j(log for java)是一个功能强大的日志组件,提供方便的日志记录。在apache网站:http://www.apache.org/dyn/closer.cgi/logging/log4j 可以免费下载到Log4j最新版本的软件包。     但是,后来Apache软件基 阅读详情

相关推荐

Log4j源码解析--框架流程+核心解析

OK,现在我们来研究Log4j的源码: 这篇博客有参照上善若水的博客,原文出处:http://www.blogjava.net/DLevin/archive/2012/06/28/381667.html。感谢作者的无私分享。 Log4J将写日志功能抽象成七个核心类或者接口:Logger、LoggerRepository、Level、LoggingEvent、Appender、Layou

LinkinPark的博客 1万+

Log4j源码分析

Log4j作为java领域最早的日志工具,其出现填补了行业空白,也为JUL的出现提供了参考,JUL的设计大多参考了Log4j的思路。Logger作为日志记录器,其定义了日志级别,绑定了多个Appender,所有的Logger都交给LoggerRepository管理,在内存中所有的Logger构建成以RootLogger为根节点的单根树形结构,通过RootLogger能方便的找到所有的Logger。

yangzsirr的博客 1105

关于Log4J记录日志格式加载以及匹配源码解读

根据代码匹配的是BasicPatternConverter,在PatternLayout format的过程中调用的convert方法实际执行的就是匹配的Converter中的convert方法,可以看出%m 对应的是2004 所以只会打印获取到的Message。之前看了Log4J是如何匹配记录的log等级的的源码,log4J的配置项有很多,要一一的去进行查看log4J是如何应用这些配置项的,今天这篇记录下log4J是如何匹配记录的格式,先上配置,这里展示的是我自己重写的。

atp1314的博客 1364

Log4j原理&源码解析

一文搞懂Log4j原理

王文萱的博客 6025

Spring Boot集成Log4j详解:从入门到精通

追踪程序执行流程诊断和调试问题监控系统运行状态记录用户操作行为// 简单的日志记录示例logger.info("开始创建订单,订单ID:" + order.getId());try {// 业务逻辑logger.debug("订单处理中...");logger.error("创建订单失败", e);logger.info("订单创建完成");特性Log4j2Logback性能高中低配置方式XML/GroovyProperties异步日志支持支持。

本博客聚焦 YOLOv11 全流程落地,涵盖架构优化、数据集处理、训练技巧与多场景部署,兼及国产数据库、Java 开发与 AI 模型应用,内容兼顾理论与工程实践,为开发者提供系统干货,助力高效提升技术能力。 2113

深入探究Apache Log4j API及源码

Logger组件是Log4j框架中负责记录日志的核心部分。它是开发者与日志系统交互的接口,用于输出不同级别(如DEBUG、INFO、WARN、ERROR等)的日志消息。Log4j支持多层次的日志记录,Logger可以被组织成树状的层级结构。通过合理配置Logger,开发者可以控制日志输出的粒度和范围,这对于调试程序和事后分析问题具有重要意义。Logger组件通过继承关系实现父子结构,若未明确指定Logger的父级,则默认继承自根Logger(root Logger)。

weixin_34163588的博客 1113

Log4j源码解析

Log4j源码解析

jpkopkop的博客 1336

A2B音频总线在智能座舱中的应用

车载A2B链路音频是智能座舱的核心功能,涵盖车载音响、语音识别、e-Call、消噪及回声消除等应用,随着汽车智能网联化的发展,对音频的开发要求也越来越高,需要加入更先进、功能丰富的信息娱乐功能以满足消费者的用户体验。但传统的模拟并行音频信号传输方式,难以在功能增加与整车轻量化(线缆的重量及成本减少)之间取得均衡。这时候就需要A2B音频总线了。A2B音频总线用于连接车辆中的音频设备,如麦克风(MIC)、功放(AMP)、多媒体主机(Head Unit)和T-BOX等。通过一条低成本的非屏蔽双绞线UTP,进行跨距

怿星科技的博客 3933

关于log4j记录日志源码解读

而RootLogger中的消息等级在初始化LoggManager的过程将配置文件中的Root下的Level设定的值给设置到RootLogger中,当设定的为Level等级和实际要输出的Level等级不一致的时候,就不会进行记录,实际测试也是这样。这里我们就要回到Log4j加载参数的过程,上次分析log4j加载参数的过程只是看了加载配置文件的这部分,忽略了repository的内容,所以还是回去看log4j加载参数的过程。我们使用log4j日志记录log的时候,开始会生成一个Logger类的对象如下。

atp1314的博客 1590

基于EKF的UWB-IMU融合定位系统在农业机器人中的抗干扰优化与Matlab仿真

本文探讨了基于扩展卡尔曼滤波(EKF)的UWB-IMU传感器融合定位系统在农业机器人中的应用。通过Matlab仿真验证,该系统有效解决了农业环境中UWB信号遮挡和IMU累积误差问题,实现了厘米级高精度定位,显著提升了机器人在复杂场景下的抗干扰能力和导航可靠性。

numpy6sculptor的博客 622

视觉SLAM的前段后端最详细的梳理(硕士入门知识框架更新)

文章目录视觉SLAM的组成结构视觉里程计(VO)-特征点法 -构建稀疏点云地图-直接法 -构建稠密或半稠密的地图 --需要使用 RGB-D 传感器视觉里程计--总结:SLAM 后端研究滤波器法非线性优化后端方式的差异(有五种优化方式):单目视觉里程计的初始化:运动估计:半直接法运动估计的优化方案:关于解决累计误差的一些方法:关键帧的建图及后端优化:特征的深度估计:图优化的后端研究:视觉与IMU融合的分类:视觉与IMU的融合的优势:存在的问题:优秀的开源: 视觉SLAM的组成结构 视觉里程计(VO) -特

DC 6811

Log4j源码浅析

一.基本构成   Log4J最核心的也就5个类:Logger用于对日志记录行为的抽象,提供记录不同级别日志的接口;Level对日志级别的抽象;Appender是对记录日志形式的抽象;Layout是对日志行格式的抽象;而LoggingEvent是对一次日志记录过程中所能取到信息的抽象。另外LoggerRepository是Logger实例的容器。实现Log4J主要功能相关的类图:  二.流程

烈冬冰夏的博客 440

UG NX与PLC-1500的在环虚拟测试

TIA、PLCSIM、NetToPLCsim、KEPServerEX以及UG NX的在环虚拟测试

lvtlvtlvtlvt的博客 4216

log4j打印日志源码浅析

并不存在一些行为和习惯会比其他的行为和习惯更有意义,很多观点是大部分人一致认为那是有意义的。人要活出自己,对未来充满希望,积极面对人生。不要嫌弃与我行为,认知,和感受不同的人,我会保持自己的生活方式,即使自己不能接受其他人相反的方式,但并不妨碍我去理解并尊重与我不同的人,而且真心为他们的快乐而感到开心和祝福 目的 搞懂下面这两行代码到底是怎么打印出日志的 private static final Logger logger=Logger.getLogger(Log4jTest.class); logger

茄子的博客 336

AFSim编译一:windows

AFSim在windows上的编译

Meepo的博客 4734

LangGraph 中 Human-in-the-loop

摘要:本文探讨了在自主AI Agent系统中实现"人工介入关键节点"(HIL)的技术方案,主要基于LangGraph框架构建。针对Agent可能执行高风险操作(如删除数据库)的问题,提出通过断点机制在关键节点暂停流程,实现人工审批后再继续执行。具体实现包括:(1)使用checkpointer组件持久化状态;(2)通过interrupt_before/after参数设置断点;(3)支持三种交互模式:批准、编辑和输入。文章还提供了代码示例,展示如何在不同场景(如工具调用)中动态触发人工审核机

qq_52053775的博客 153

Java日志学习三:Apache Log4j源码浅析

一.Apache Log4j http://logging.apache.org/log4j/2.x/本文只讲初始化Logger的过程。 二.Apache Log4j里的几个核心类 Logger:日志类。getLogger()交给LogManager去实现。Level:八个级别。 #OFF,FATAL,ERROR,WARN,INFO,DEBUG,TRACE,ALL。LogMana

wwlwxgwenwen的博客 502

Qt 【window udp 高频采集丢包问题】

使用Asio + 无锁队列或者环形缓存区方案可以解决 qt udp 在window平台高速采集 (128k)丢包问题。

weixin_45397344的博客 177

log4j源码解析

(一)几个基本概念 Logger - 日志写出器,供程序员输出日志信息 Appender - 日志目的地,把格式化好的日志信息输出到指定的地方去 ConsoleAppender - 目的地为控制台的Appender FileAppender - 目的地为文件的Appender RollingFileAppender - 目的地为大小受限的文件的Appender Layout

jidubaozou的专栏 5980

CloudCompare二次开发(1)——获取指定高程的所有点

使用CloudCompare插件实现获取点云数据中指定高程的所有点。

点云侠的博客 4732

lwip-2.0.3

lwip是瑞典计算机科学院(SICS)的Adam Dunkels 开发的一个小型开源的TCP/IP协议栈。实现的重点是在保持TCP协议主要功能的基础上减少对RAM 的占用。

log4j 源码解析_深入源码解析日志框架Log4j2(一)

前言log4j2是apache在log4j的基础上,参考logback架构实现的一套新的日志系统(我感觉是apache害怕logback了)。log4j2的官方文档上写着一些它的优点:在拥有全部logback特性的情况下,还修复了一些隐藏问题API 分离:现在log4j2也是门面模式使用日志,默认的日志实现是log4j2,当然你也可以用logback(应该没有人会这么做)性能提升:log4j2包含...

weixin_29274969的博客 841

C# WinForm 基于DirectShow.dll编写的程序,可边预览边保存视频,音频和图片

基于微软的DirectShow.dll的二次开发1.自动识别连接的摄像头跟麦克风且可以对其进行选择(默认选中第一个)2.可录制wmv和avi两种格式的视频并且都带有音频,avi可以录制的分辨率和帧数进行选择,wmv默认进行了淸晰度优化如不喜欢可去除代码中有相关提示3.可在预览点开之后进行拍照,也可边录制视频边拍照4.添加了视频和音频的压缩功能(酌情使用)这次二次开发发现C#对DirectShow的相关信息太少,这次做完也不敢藏私共享出来,还望大家指正

将log4j的项目升级到log4j2

0.背景 虽然大部分的java项目使用的都是logback,但是仍然有很多开源项目使用log4j,例如kafka、zookeeper。但是log4j已经于2012年停止更新,并且log4j扫出如下安全漏洞: https://rnd-vulncenter.huawei.com/vuln/toViewOfficialDetail?cveId=CVE-2020-9488 https://rnd-vulncenter.huawei.com/vuln/toViewOfficialDetail?cveId=CVE-20

weixin_36142042的博客 7447

老版QQ原生表情gif动态

这是老版的QQ表情,不过全是动态的的,Windows照片查看器是不会动的,可以右击用浏览器打开查看,再说一遍是老版的,只是分享给宁愿用老版也要动态的人。

TMS_WebGMaps_1.9.8.0_Full_Source

TMS_WebGMaps_1.9.8.0_Full_Source 全源码 delphi cbuilder

log4j 源码解析_log4j2源码分析

目录1.概述1.1.组件概览1.2.灵活的配置1.2.1.插件发现机制1.2.2.插件装配机制1.2.3.配置文件基本元素与对象的映射关系2.属性占位符2.1.概述2.2.Interpolator插值器2.3.默认属性配置3.Logger3.1.配置示例3.2.配置详解3.3.Logger继承机制4.Appender4.1.概述4.2.框架支持的Appender实现4.3.常用Appender详解...

weixin_39995280的博客 1206

深入Log4J源码之Log4J Core

Log4J将写日志功能抽象成七个核心类/接口:Logger、LoggerRepository、Level、LoggingEvent、Appender、Layout、ObjectRender。其类图如下: 更详细的,实现Log4J主要功能相关的类图: 其实Log4J最核心的也就5个类:Logger用于对日志记录行为的抽象,提供记录不同级别日志的接口;Level对日志级别的抽象;Appen...

yz18931904的博客 1883

【日志框架-笔记】深入浅出 Log4j,理论-源码-配置

`Log4j 全称 Logging for Java`;它是一个用于Java编程语言的日志记录库(框架),支持多种日志记录格式。包括文本、xml、数据库。和其他日志框架一样,可以把日志信息发送到控制台、文件等不同目的地。有强大的配置功能,当然也可以进行过滤,根据实际情况而论,还是比较灵活的。

qq_63691275的博客 1489
上一篇: log4j配置详解
下一篇: logback源码浅析
陈*^_^*
博客等级 码龄6年 48粉丝 16原创
评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

个

红包个数最小为10个

元

红包金额最低5元

当前余额3.43元 前往充值 >
需支付:10.00元
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付元
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值