Log4j 2 指南

Posted 書陋堂Slowtown

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了Log4j 2 指南相关的知识,希望对你有一定的参考价值。

史料

  鸿濛初辟,濁元漸清。日誌江湖,群雄逐鹿,紛爭四起。至于公元一九九六年初,西域諸國圍桌而謀 1,欲止其亂象。經百轉千回,終道成肉身,於公元一九九九年之秋,著道法歸約一卷,謂之 Log4j 2 。

  Log4j 聚諸賢之智,可謂乃大成之作也。然同年末月,另有善此技者,递奏折于朝堂,望使其技入歸法典。彼時,滿朝文武,群策群力。又二年,終告之于天下,謂之 JSR-047 3 ,而坊間則以 JUL 稱之。至此,雙雄即現,JUL 居正統之位,而 Log4j 早已名揚于四海之外。

  有詩云:江山代有人才出,各領風騷數百年。時至公元二〇〇六年,或慨于壯士終暮年,或路遙方知馬漸疲,Log4j 作者另創 Logback ,將其視為 Log4j 之後繼也。縱觀日誌江湖,前有正當英氣風發之 Log4j、JUL,后有初出茅廬之 Logback 4 ,三足鼎立,不在話下。

  七度春秋,白馬過隙,青絲少年已然白髮如雪。公元二〇一二年五月,Log4j 迎其終版 5 。是年七月,Log4j 2 即公之于世 6 ,正所謂:千门万户曈曈日,总把新桃换旧符。

介绍

   Apache Log4j 2 是 Log4j 的升级版,相较后者有了显著的改进,引入了很多来自于 Logback 的功能,并解决了一些 Logback 架构上的固有问题,使得其青出于蓝而胜于蓝。

  • 特色

    • API 分离 清晰 高可维护性

    • 基于 LMAX Disruptor 库 的异步日志

    • 多 APIs 支持 Log4j 1.2 SLF4J JCL JUL

    • 代码高适应性 使用适配器可切换到其他日志框架

    • 自动化的配置重载 热替换 不丢失日志

    • 灵活的过滤 高可定制的过滤

    • 插入式架构 基于注解的自动识别

    • 属性的支持 可在文件、系统/环境变量等多处定义属性

    • Java 8 Lambda 支持

    • 灵活的日志等级定义 配置声明 代码变量声明

    • 无垃圾或少垃圾

  • 优势

    • 配置热替换不丢日志,可被用于审计

    • 应用可感知到输出源的异常信息

    • 可做到无须 GC 或较少的 GC

    • 使用注解方式引入输出源、过滤器等组件,更容易扩展

    • 代码无须显示判断日志级别

    • 通过 λ 表达式实现高开销的信息延迟构造

    • 输出源的格式高度可定制

    • Syslog 输出源支持 TCPUDP

    • 支持 BSD syslog 和 RFC 5424

    • 使用 Java 5 并发框架使得并发控制锁发生在低级别

注:文章默认 Log4j 版本 2.11.0   

资源

  • binary apache-log4j-2.11.0-bin.zip

    • md5:C0 E2 32 E6 7F EF FD 42 7E 49 74 0B 85 27 79 B5

  • source apache-log4j-2.11.0-src.zip

    • md5:23 38 66 93 16 BD 0C C7 DE 17 52 60 6A 5D 82 82

  • Maven 项目中,在 pom.xml 添加:

    <dependencies> <dependency> <groupId>org.apache.logging.log4j</groupId> <artifactId>log4j-api</artifactId> <version>2.11.0</version> </dependency> <dependency> <groupId>org.apache.logging.log4j</groupId> <artifactId>log4j-core</artifactId> <version>2.11.0</version> </dependency></dependencies>



  Log4j 2 将规范与实现做了 jar 包级别的分离,更好的引导开发者面向接口编程。规范部分名为 log4j-api,实现部分名为 log4j-core。严格来讲,编译期间只需要规范部分,实现部分到运行时再提供即可。然而,由于 log4j-core 部分提供了注解处理器,它在编译期间针对自定义插件提供元数据文件缓存及必要的自定义插件代码,从而加快自定义插件的启动速度。

架构

   Log4j 2 的主要类关系如下图所示:

Log4j 2 指南

Log4J 2 官方类关系图

  应用程序将获取 Logger 对象委托给 LogManager 类,后者通过 SPI 得到 LoggerContextFactory 工厂的实现类,并由它生产合适的 LoggerContext,而该上下文对象包含一个 Configuration,该对象包含若干个 Appenders、上下文级别的 Filters、StrSubstitutor、LoggerConfigs,其中 LoggerConfigs 每个都对象都可以根据 Logger 的 name 来定位,定位规则:

  1. LoggerConfig 的名称与 Logger 的 name 相同

  2. LoggerConfig 的名称与 Logger 的 父、先代包名相同

  3. 如果 1、2 条规则都没定位到具体 LoggerConfig 对象,则返回 root LoggerConfig

   

        Loggers、LoggerConfigs 都是被命名的实体,名字大小写敏感。Log4j 1.x 则直接通过 Logger 来维持层级关系,而 Log4j 2 通过 LoggerConfig 的层级关系来维持 Logger 的层级关系。例如:名为 "com.foo" 的 LoggerConfig 是名为 "com.foo.Bar" 的 LoggerConfig 的父级。而顶层根 LoggerConfig 的名称为 LogManager.ROOT_LOGGER_NAME 或 "" 空字符串。

LoggerContext

  整个日志系统的标记切入点。在一些情形中同一应用可以有多个活动的 LoggerContext。

Configuration

  每个 LoggerContext 都有一个活动的 Configuration。该 Configuration 包含所有的 Appenders、上下文作用域范围的 Filters、LoggerConfigs 以及指向 StrSubstitutor 的引用。在重新加载配置期间,会有两个 Configuration 对象存在,一旦所有的 Loggers 都被重定向到新加载的 Configuration,旧的 Configuration 将被停止和遗弃。

Logger

  本身不直接执行动作,仅包含一个与 LoggerConfig 关联的 name 属性,它继承 AbstractLogger 实现必要的方法,大部分记录日志的操作都在此抽象类中完成了。当 Configuration 被修改重新载入后,Loggers 将关联到不同的 LoggerCoinfig,从而更改其记录日志的行为。

LoggerConfig

  LoggerConfig 在日志配置文件中声明定义了 Loggers 时被创建。该对象包含一组 Filters,这些过滤器能够在 LogEvent 被传递给任何 Appender 之前对其进行过滤操作。同时,LoggerConfig 还包含一组处理日志事件的 Appenders。

  LoggerConfig 都将被分配一个日志级别,且存在 级别继承 的概念。前面讲过 LoggerConfig 通过名称实现层级关系,某个层级如果没有配置日志级别,默认就会继承其父、先代的,如果都没配置,根 LoggerConfig 默认的级别为 Level.ERROR

# 级别 权重 说明 ALL  MAX_VALUE  记录所有 TRACE  600    追踪方法调用流向 DEBUG  500    满足基本调试 INFO  400    信息提示为目的 WARN  300    需引起重视的警告 ERROR  200    可恢复的错误 FATAL  100    系统崩溃的错误 OFF  0    不记录  

注意:设置为 OFF 时,logger.log(Level.OFF, "msg") 的形式仍可被记录

Filter

  除上面提及的日志级别配置外,还提供了 Filters 对日志进行过滤。这些过滤器可以作用于:

  • 控制权移交给 LoggerConfig 之前

  • 控制权移交给 LoggerConfig 之后,未调用任何 Appender 之前

  • 控制权移交给 LoggerConfig 之后,调用特定 Appender 之前

  • 每个 Appender 之上

  每个过滤器可以返回以下三个结果之一:

  • Accept:日志事件直接由本过滤器接受并处理,且跳过其他过滤器

  • Neutral:日志事件在本过滤器不做处理,流转到其他过滤器

  • Deny:日志事件直接由本过滤器拒绝不处理,控制权直接交回调用方

  

        注意:被过滤器接受并不意味着就能被记录日志。例如:事件被控制权移交给 LoggerConfig 之前型的过滤器接受,却被控制权移交给 LoggerConfig 之后型的过滤器拒绝,或者直接被所有 Appenders 拒绝。

Appender

  除基于 Logger 来选择性的开启或关闭日志请求外,Log4j 还允许日志请求能够被打印到不同的目的地,这些目的地被称作 Appender,包括:控制台、文件、远程日志服务器、Apache Flume、JMS、Unix Syslog 进程,以及数据库。

  默认下,日志消息除输出到当前 logger 的 LoggerConfig 关联的 Appenders 之外,还会顺着 LoggerConfig 的继承链往上传播到父、先代的 LoggerConfig 关联的 Appenders 上,并且所传递日志事件能否在父、先代 Appenders 上最终打印只受 Appenders 上的过滤器影响,而不受父、先代 LoggerConfigs 上的level 属性的影响。可以通过在对应的 logger 配置中设置 additivity="false" 来关闭传播行为。

Layout

  Layout 负责对日志事件按照用户的格式进行格式化,而格式化之后的消息就交由 Appender 输出到指定目的地。PatternLayout 其格式化消息所用的格式非常类似于 C 语言的 printf 方法,%r [%t] %-5p %c -%m%n 的格式字符串将把日志信息格式化成:

176 [main] INFO  org.foo.Bar - Located nearest gas station.

StrSubstitutor & StrLookup

  两者结合能够使得 Configuration 可以引用一些变量,这些变量可在如下环境中被定义:系统属性、配置文件、线程上下文的 Map、日志事件的 StructuredData。在 Configuration 被处理时,这些变量能够被正常读取和解析。

源码分析

  一只南美洲亚马逊河流域热带雨林中的蝴蝶,偶尔扇动几下翅膀,可以在两周以后引起美国得克萨斯州的一场龙卷风。

—— Edward Norton Lorenz  


  而引发 Log4j 日志运作的那只“蝴蝶”来源于此:

Code_01   

private static final Logger log = LogManager.getLogger();

寻找 LoggerContextFactory 实现

  加载 LoggerManager 时,其静态初始化代码块首先执行的就是寻找 LoggerContextFactory 的实现类。分别依照如下顺序进行搜寻:

  1. 搜名为 "log4j2.loggerContextFactory" 的系统属性,其值为上下文工厂实现类的全限定名。

  2. 使用 Java SPI 技术 得到 org.apache.logging.log4j.spi.Provider 的实现类,借由该实现类定位到 LoggerContextFactory 接口的实现类。找到单个就返回,找到多个返回优先级最高的那个。Log4j-core-2.11.0.jar 包中 Provider 实现类为 Log4jProvider,此类给出的上下文工厂实现类为 Log4jContextFactory

  3. 前俩都没找到,返回 SimpleLoggerContextFactory

  

LoggerManager 静态代码块(有删减):

Code_02  

// LoggerManager 的静态初始化代码块static { // *********************** 步骤一 ***************************** // FACTORY_PROPERTY_NAME = "log4j2.loggerContextFactory"; final String factoryClassName = managerProps.getStringProperty(FACTORY_PROPERTY_NAME); factory = LoaderUtil.newCheckedInstanceOf(factoryClassName, LoggerContextFactory.class);  // *********************** 步骤二 ***************************** if (factory == null) { final SortedMap<Integer, LoggerContextFactory> factories = new TreeMap<>();  if (ProviderUtil.hasProviders()) { for (final Provider provider : ProviderUtil.getProviders()) { final Class<? extends LoggerContextFactory> factoryClass = provider.loadLoggerContextFactory(); if (factoryClass != null) { factories.put(provider.getPriority(), factoryClass.newInstance()); } }  if (factories.isEmpty()) { // *********************** 步骤三 ***************************** factory = new SimpleLoggerContextFactory(); } else if (factories.size() == 1) { factory = factories.get(factories.lastKey()); } else { factory = factories.get(factories.lastKey()); } } else { // *********************** 步骤三 ***************************** factory = new SimpleLoggerContextFactory(); } }}


  静态初始化后,LogManager.getLogger() 被正式调用,最终被调用的方法为:

Code_03  

// org.apache.logging.log4j.LogManager.getLogger(Class<?>)public static Logger getLogger(final Class<?> clazz) { final Class<?> cls = callerClass(clazz); return getContext(cls.getClassLoader(), false).getLogger(toLoggerName(cls));}

  

        不难发现,需要两部操作:得到上下文实例,通过上下文实例取得最终的 Logger,接下来我们逐步来分析。

获取 LoggerContext 实例

  由上面步骤找到的上下文工厂类 Log4jContextFactory,它委托 ContextSelector 来定位或生产出一个 LoggerContext 实例,而此日志上下文就是 Loggers 以及它们的配置对象 configuration 的载体。

Code_04  

// Log4jContextFactory.getContext(String, ClassLoader, Object, boolean)public LoggerContext getContext(final String fqcn, final ClassLoader loader, final Object externalContext, final boolean currentContext) { // selector 为 ClassLoaderContextSelector,下文有介绍 final LoggerContext ctx = selector.getContext(fqcn, loader, currentContext); if (externalContext != null && ctx.getExternalContext() == null) { ctx.setExternalContext(externalContext); } // 刚初始化后的 LoggerContext 需调用 start 方法进配置的加载 if (ctx.getState() == LifeCycle.State.INITIALIZED) { ctx.start(); } return ctx;}

  

        而 ContextSelector 的实例的获取,Log4j 又给出了一个可配置的切入点:名为 "Log4jContextSelector" 的系统属性。如果配置了此属性,那么其值是一个实现了 ContextSelector 接口的类的全限定名。Log4j 提供了五种 selector :

  • BasicContextSelector
    使用已存储在 ThreadLocal 中的 LoggerContext 或 通用的 LoggerContext


Code_05  

//org.apache.logging.log4j.core.selector.BasicContextSelector// 通用的 LoggerContextprivate static final LoggerContext CONTEXT = new LoggerContext("Default"); public LoggerContext getContext(final String fqcn, final ClassLoader loader, final boolean currentContext) { // ThreadLocal 中的 LoggerContext final LoggerContext ctx = ContextAnchor.THREAD_CONTEXT.get(); return ctx != null ? ctx : CONTEXT;}


  • ClassLoaderContextSelector
    默认的 selector,将 LoggerContexts 与 getLogger 方法调用者的 ClassLoader 关联


Code_06  

//org.apache.logging.log4j.core.selector.ClassLoaderContextSelectorprotected static final ConcurrentMap<String, AtomicReference<WeakReference<LoggerContext>>> CONTEXT_MAP = new ConcurrentHashMap<>();// 代码有删减private LoggerContext locateContext(final ClassLoader loaderOrNull, final URI configLocation) { final ClassLoader loader = loaderOrNull != null ? loaderOrNull : ClassLoader.getSystemClassLoader(); // 将 loader 对象转化为 Map 的键值字符串,即对象存储地址的十六进制字符串 final String name = toContextMapKey(loader); // 根据转化的键值在 Map 中查找是否存在,存在就返回,不存在就创建后添加入 MAP AtomicReference<WeakReference<LoggerContext>> ref = CONTEXT_MAP.get(name); if (ref == null) { // ★★★ 没找到创建后,添加到 Map ★★★ LoggerContext ctx = createContext(name, configLocation); final AtomicReference<WeakReference<LoggerContext>> r = new AtomicReference<>(); r.set(new WeakReference<>(ctx)); CONTEXT_MAP.putIfAbsent(name, r); ctx = CONTEXT_MAP.get(name).get().get(); return ctx; } final WeakReference<LoggerContext> weakRef = ref.get(); LoggerContext ctx = weakRef.get(); // 找到就返回 if (ctx != null) { return ctx; } ctx = createContext(name, configLocation); ref.compareAndSet(weakRef, new WeakReference<>(ctx)); return ctx;}
  • JndiContextSelector
    通过查询 JNDI 来查找 LoggerContext

  • AsyncLoggerContextSelector
    由它创建的 AsyncLoggerContext 关联的 Logger 全部是异步的,即 AsyncLogger。它继承至 ClassLoaderContextSelector,对比后者,它存储在 Map 对象 CONTEXT_MAP 中的键值仅仅是加了前缀 "AsyncContext@" 或 "DefaultAsyncContext@"

  • BundleContextSelector
    OSGi 环境下的默认 selector,将 LoggerContexts 与 getLogger 方法调用者所在的 bundle 的 ClassLoader关联。它同样继承至 ClassLoaderContextSelector,对比后者,它存储在 Map 对象 CONTEXT_MAP 中的键值是来源于 bundle 的getSymbolicName 方法。

  默认的 ClassLoaderContextSelector 在 Map 缓存中没找到当前的 loader 对应的 loggerContext,调用上面加 ‘★’ 处的代码进行创建并添加到 Map 缓存中,并返回给委托方 Log4jContextFactory,并由它检测 loggerContext 的生命周期状态,如果是刚完成初始化,则需调用 start 方法进行配置的加载,参见下小节。

Tips:

/** * LoggerContext 实现 LifeCycle2 生命周期接口 * 而 LifeCycle2 继承 LifeCycle,它定义了如下声明周期状态 */// org.apache.logging.log4j.core.LifeCycle.Stateenum State { /** 对象正在初始化. */ INITIALIZING, /** 初始化完成,还没启动. */ INITIALIZED, /** 正在调用 start 方法,启动中. */ STARTING, /** start 方法执行完毕,已启动. */ STARTED, /** stop 方法执行中,停止中. */ STOPPING, /** stop 方法执行完毕,已停止 */ STOPPED}

启动 LoggerContext 加载配置

  上一节已经得到了 LoggerContext 的实例,接下来继续执行 Code_04 中的 ctx.start() 进行配置加载,具体代码:

Code_07  

// org.apache.logging.log4j.core.LoggerContext.start()// 代码有删减public void start() { if (configLock.tryLock()) { try { if (this.isInitialized() || this.isStopped()) { // 生命周期调整为 启动中 状态 this.setStarting(); // 加载配置 reconfigure(); if (this.configuration.isShutdownHookEnabled()) { setUpShutdownHook(); } // 启动完毕,生命周期调整为 已启动 状态 this.setStarted(); } } finally { configLock.unlock(); } }}// org.apache.logging.log4j.core.LoggerContext.reconfigure(URI)// 上面的 reconfigure() 方法最终调用到此方法// 代码有删减private void reconfigure(final URI configURI) { // configURI 此时为 null,故 cl 也为 null final ClassLoader cl = ClassLoader.class.isInstance(externalContext) ? (ClassLoader) externalContext : null; // ★★★ 委托给 ConfigurationFactory ★★★ final Configuration instance = ConfigurationFactory.getInstance().getConfiguration(this, contextName, configURI, cl); if (instance == null) { // 记录异常 } else { // 将配置实例赋值给成员变量 setConfiguration(instance); }}


  上面代码显示,LoggerContext 启动后,将获取 Configuration 实例的操作委托给了 ConfigurationFactory 工厂,先来看如何获取配置工厂实例:

Code_08  

// org.apache.logging.log4j.core.config.ConfigurationFactory.getInstance()// 代码有删减public static ConfigurationFactory getInstance() { if (factories == null) { LOCK.lock(); try { if (factories == null) { final List<ConfigurationFactory> list = new ArrayList<>(); // ① 系统属性方式收集配置工厂实例 // CONFIGURATION_FACTORY_PROPERTY = "log4j.configurationFactory" final String factoryClass = PropertiesUtil.getProperties().getStringProperty(CONFIGURATION_FACTORY_PROPERTY); if (factoryClass != null) { addFactory(list, factoryClass); }
// ② 利用插件管理器搜集注解方式的配置工厂实例 // CATEGORY = "ConfigurationFactory" final PluginManager manager = new PluginManager(CATEGORY); /* * 按照下面顺序进行追加合并收集: * 1. 搜寻 CLASSPATH 中的 Log4j2Plugin.dat 插件缓存文件,里面都是 Log4j 内建的插件 * 2. 在 OSGi bundles 中搜寻 Log4j2Plugin.dat * 3. 搜寻通过静态的 `addPackage` 方法添加的包中搜寻插件 * 4. 搜寻方法参数给定的包列表中的插件 */ manager.collectPlugins(); final Map<String, PluginType<?>> plugins = manager.getPlugins(); final List<Class<? extends ConfigurationFactory>> ordered = new ArrayList<>(plugins.size()); for (final PluginType<?> type : plugins.values()) { try { ordered.add(type.getPluginClass().asSubclass(ConfigurationFactory.class)); } catch (final Exception ex) { // 记录异常日志 } } // 插件找到的工厂,按照注解 @Order 的顺序排序,数值越大优先级越高 Collections.sort(ordered, OrderComparator.getInstance()); for (final Class<? extends ConfigurationFactory> clazz : ordered) { // 系统属性参数配置的工厂高于插件找到的,由此可知 addFactory(list, clazz); } factories = Collections.unmodifiableList(list); } } finally { LOCK.unlock(); } } /* ★★★ * 返回私有静态内部类 ConfigurationFactory.Factory * 由它的 getConfiguration 方法获取 Configuration */ return configFactory;}


  在获取抽象类 ConfigurationFactory 子类实例时,Log4j 2 有两种方式:

  1. 提供了一个可供配置扩展的系统属性参数(最高优先级)

    • 属性名:"log4j.configurationFactory"

    • 属性值:ConfigurationFactory 子类全限定名字符串

  2. 利用插件管理器搜寻注解式的插件工厂
      如代码注释的步骤搜寻子类,篇幅原因具体 Log4j 的插件体系,不在这里展开,详见 Plugins。值得一提的是,为了加快插件搜寻载入速度,Log4j 内建的插件采用了 Log4j2Plugin.dat 文件进行缓存。目前内建的配置工厂有如下几种,@Order 数值越大优先级越高:


    • PropertiesConfigurationFactory @Order(8)

    • YamlConfigurationFactory @Order(7)

    • JsonConfigurationFactory @Order(6)

    • XmlConfigurationFactory @Order(5)


  不知大家有没有注意到,其实抽象类 ConfigurationFactory 还有一个非插件式的子类 ConfigurationFactory.Factory,它是前者的静态私有内部类,它不仅包装了对前面两种方式收集到的工厂子类的各种操作,还定义了配置文件加载的先后顺序。

  

  • 配置文件加载的先后顺序:

    1. 优先加载以 "log4j2-test" 为前缀的文件

    2. 其次加载以 "log4j2" + contextName 7 为前缀的文件

    3. 最后加载以 "log4j2" 为前缀的文件

  • 相同前缀时,不同文件类型的优先顺序:

    1. ".properties"

    2. ".yml" 或 ".yaml"

    3. ".json" 或 ".jsn"

    4. "xml"

  优先级先考虑前缀,其次才是文件类型。

  例如:
    "log4j2-test.xml" > "log4j2.properties"

  虽然 .xml 文件优先级低于 .properties,但 log4j2-test 前缀优先级高于 log4j2 前缀。

  若有名为 "log4j2.properties" 的配置文件,ConfigurationFactory.Factory 会执行如下代码:

Code_09  

// org.apache.logging.log4j.core.config.ConfigurationFactory.Factory.getConfiguration(LoggerContext, boolean, String)private Configuration getConfiguration(final LoggerContext loggerContext, final boolean isTest, final String name) { final boolean named = Strings.isNotEmpty(name); final ClassLoader loader = LoaderUtil.getThreadContextClassLoader(); // 按照优先级顺序,依次遍历插件工厂寻找与之相对应的配置文件类型,找到立即返回 for (final ConfigurationFactory factory : getFactories()) { String configName; final String prefix = isTest ? TEST_PREFIX : DEFAULT_PREFIX; final String [] types = factory.getSupportedTypes(); if (types == null) { continue; }
for (final String suffix : types) { if (suffix.equals(ALL_TYPES)) { continue; } configName = named ? prefix + name + suffix : prefix + suffix;
// 根据配置文件名 log4j2.properties 使用 ClassLoader.getResource 的到实际的资源 final ConfigurationSource source = ConfigurationSource.fromResource(configName, loader); if (source != null) { if (!factory.isActive()) { // 记录警告信息 } // ★★★ 加载配置的操作交给真正的配置工厂实例 ★★★ return factory.getConfiguration(loggerContext, source); } } } return null;}


  由于配置的是 ".properties" 文件,所有 ‘★’ 标处的 factory 是 PropertiesConfigurationFactory 的实例。转到所调用的代码:

Code_10  

// PropertiesConfigurationFactory.getConfiguration(LoggerContext, ConfigurationSource)public PropertiesConfiguration getConfiguration(final LoggerContext loggerContext, final ConfigurationSource source) { final Properties properties = new Properties(); try (final InputStream configStream = source.getInputStream()) { // 读取 log4j2.properties properties.load(configStream); } catch (final IOException ioe) { // 记录异常 } // 采用 Builder 模式构建 PropertiesConfiguration return new PropertiesConfigurationBuilder() .setConfigurationSource(source) .setRootProperties(properties) .setLoggerContext(loggerContext) .build();}


  从创建一个 PropertiesConfigurationBuilder 实例到调用它的 build 方法拿到配置对象 PropertiesConfiguration 涉及很多的类,先通过类图来理清下关系:


加载配置相关的类




  从图底部开始,工厂类 PropertiesConfigurationFactory (简称 pcf) 构建 PropertiesConfigurationBuilder (简称 pcb),它有 ConfigurationBuilder 类型的成员变量,该变量引用的对象由图中未画出来的 ConfigurationBuilderFactory 创建,目前该工厂创建的都是 DefaultConfigurationBuilder (简称 dcb)对象。pcb 读取配置文件,对全局的 rootProperties 按照组件分类成不同的子集,然后调用 dcb 的 add(XxxComponentBuilder ) 方法生成 root 组件的各个子组件,最后由它的 build(boolean) 将这些组件信息去初始化继承链上的各个成员变量,最后新生成未初始化的 PropertiesConfiguration 返回给最初调用 pcf 工厂的 LoggerContext,赋值给它的成员变量 configuration

Code_11  

// org.apache.logging.log4j.core.LoggerContext.reconfigure(URI)// 代码有删减private void reconfigure(final URI configURI) { final Configuration instance = ConfigurationFactory.getInstance().getConfiguration(this, contextName, configURI, cl); if (instance == null) { // 记录异常 } else { // ★★★ 可不光仅仅是给成员变量赋值 ★★★ setConfiguration(instance); }}


   setConfiguration(instance) 不是简单的赋值操作,它是另外一只 “蝴蝶” :

Code_12  

// LoggerContext.setConfiguration(Configuration)// 代码有删减private Configuration setConfiguration(final Configuration config) { configLock.lock(); try { // 存档一份当前配置的引用 final Configuration prev = this.configuration; // 给新传入的配置注册一个监听器 config.addListener(this); //★★★ 新配置启动,【已初始化】->【已启动】 config.start(); // 将新传入的配置更新为当前的配置 this.configuration = config; // 根据新配置更新所有 Logger 的配置信息 updateLoggers(); // 完成后清理旧的配置 if (prev != null) { prev.removeListener(this); prev.stop(); }
// 触发属性变更事件 firePropertyChangeEvent(new PropertyChangeEvent(this, PROPERTY_CONFIG, prev, config));
// AsyncLoggers update their nanoClock when the configuration changes Log4jLogEvent.setNanoClock(configuration.getNanoClock());
return prev; } finally { configLock.unlock(); }}


  重点来看下 config.start() 方法调用栈:

org.apache.logging.log4j.core.LoggerContext.setConfiguration(Configuration) org.apache.logging.log4j.core.config.AbstractConfiguration.start() org.apache.logging.log4j.core.config.AbstractConfiguration.initialize()  org.apache.logging.log4j.core.config.builder.impl.BuiltConfiguration.setup() // ① 把 Component组件树 转变为 Node节点树 org.apache.logging.log4j.core.config.builder.impl.BuiltConfiguration.convertToNode(Node, Component) org.apache.logging.log4j.core.config.AbstractConfiguration.doConfigure() org.apache.logging.log4j.core.config.AbstractConfiguration.createConfiguration(Node, LogEvent) // ② Node 节点关联的插件类的具体实例设置到 Node.object 成员变量上 org.apache.logging.log4j.core.config.Node.setObject(Object) org.apache.logging.log4j.core.config.AbstractConfiguration.createPluginObject(PluginType<?>, Node, LogEvent) // ③ 解析插件注解,反射调用被 `@PluginFactory` 或 `@PluginBuilderFactory` 注解的方法,得到插件类实例 org.apache.logging.log4j.core.config.plugins.util.PluginBuilder.build()

  该方法对配置内的所有具备生命周期的组件进行启动操作,其中重要操作就包括:

  1. 将配置文件中构建出的 Component 组件树转为 Node 节点树

  2. 反射调用被 @PluginFactory 或 @PluginBuilderFactory 注解的方法构建插件实例,赋值给 node.object


组件树转为节点树相关类


  举个从配置文件读取过滤器设置的例子来更直观的了解这个过程:

  • log4j2.properties 文件

# 除过滤器配置外,其他省略# 可以定义一组过滤器,以逗号隔开,例: foo,barfilters = foo# 定义一个过滤级别为 info 的 ThresholdFilterfilter.foo.type = ThresholdFilterfilter.foo.level = info

        

  • 解析成为 Component

{ "pluginType": "ThresholdFilter", "value": null, "attributes": { "onMatch": null, "onMismatch": null, "level": "info" },  "components":[]


  • 插件管理器由 Component 的 pluginType 值找到 PluginType<ThresholdFilter>

{ "elementName": "filter", "pluginEntry": { "key": "thresholdfilter", "className":"org.apache.logging.log4j.core.filter.ThresholdFilter", "name": "ThresholdFilter", "printable": true, "defer": false, "category": "core" }, "pluginClass": {  "name": "org.apache.logging.log4j.core.filter.ThresholdFilter" }}
  • 结合 Component 和 PluginType<> 转为 Node

{ "parent": null, "name": "ThresholdFilter", "value": null, "type": { "elementName": "filter", "pluginEntry": { "key": "thresholdfilter", "className":"org.apache.logging.log4j.core.filter.ThresholdFilter", "name": "ThresholdFilter", "printable": true, "defer": false, "category": "core" }, "pluginClass": {  "name": "org.apache.logging.log4j.core.filter.ThresholdFilter" } }, "attributes": { "onMatch": null, "onMismatch": null, "level": "info" }, "children": [], "object": null}
  • PluginBuilder build() 方法生成最终插件类,填充 Node 的 object 字段

{ "object": { "level": { "name": "INFO", "intLevel": 400, "standardLevel": { "intLevel": 500 } }, "onMatch": "NEUTRAL", "onMismatch": "DENY", "state": "INITIALIZED"  }}

注:TypeConverters 处理类型转换。(例:"info" 转化为 "Level.INFO" )  


  自此完成了 Code_03 的前半部分,拿到了一个已经加载了完整的配置,且已启动完成的 context 对象,接下来就委托该上下文对象,来获取 Logger。

取得 Logger

   LoggerContext 包含 LoggerRegistry 注册器,当 context 把所需的记录器的 name 交给它,它就会在自身的 map 中检索是否包含该 name 的记录器,有就返回,没有就创建后添加到自身的 map 中,后返回新创建的 记录器。

Code_13  

// org.apache.logging.log4j.core.LoggerContext.getLogger(String, MessageFactory)public Logger getLogger(final String name, final MessageFactory messageFactory) { // Note: This is the only method where we add entries to the 'loggerRegistry' ivar. Logger logger = loggerRegistry.getLogger(name, messageFactory); if (logger != null) { AbstractLogger.checkMessageFactory(logger, messageFactory); return logger; }
logger = newInstance(this, name, messageFactory); loggerRegistry.putIfAbsent(name, messageFactory, logger); return loggerRegistry.getLogger(name, messageFactory);}


   LoggerContext 得到 Logger 后将其返回给 LoggerManager ,自此最初的那只“蝴蝶” Code_01 所带来的全部效应已经完成,得到的记录器对象就可以完成业务代码的记录工作了。

配置详解

参数配置

Log4j 2 可供配置参数的地方(数值小优先级越高):

  • 环境变量(优先级:-100)

    • 以 LOG4J_ 开头,以 _ 分隔单词的全大写的配置参数

    • 新参数配置方案,无历史兼容参数

  • log4j2.component.properties (优先级:0)
    在 classpath 中的此文件,也可用来配置 log4j 2 的配置参数

  • 系统属性(优先级:100)

    具体有哪些可配置项请参考:系统参数。

    • -D 形式配置在 JVM 的启动参数中

    • 代码中调用 System.setProperty 设置

    • 2.10 版本统一以 log4j2. 开头命名参数,并兼容历史参数命名

    • 会被上面两种按优先级顺序覆盖掉相同的配置项

组件配置

  log4j2 开头的配置文件,用来配置 Log4j 2 各种重要组件的配置。配置文件支持 propertiesymljsonxml 格式,加载顺序详见: 配置文件加载规则  。

   注意: properties 格式的配置文件是 2.4 版本后才被支持。

   Log4j 2 目前的组件包括:Configuration、Properties、Scripts、CustomLevels、Filters、Appenders、Loggers、RootLogger, 共八种组件。点击 » log4j2.properties «  下载完整样板配置文件说明,排版要求我们拆散分别介绍:

Configuration

# 本配置的名称name = nameOfThisConfig# log4j2 内部日志打印级别status = [trace|debug|info|warn|error|fatal]# 加载插件是否显示诊断信息,默认 falseverbose = [false|true]# 内部日志输出位置,可以是 stderr、文件、URLdest = [err|filePath|URL]# 配置文件是否变更检查,单位:秒monitorInterval = 60# JVM 关闭时是否 log4j2 也自动关闭,默认 enableshutdownHook = [enable|disable]# JVM 关闭时 appenders、后台任务多久后也关闭,单位:毫秒。# 默认 0,表示 appenders 按其自身的超时时间,其忽略后台任务是否完成# 设置时间太短有丢失记录,如果 shutdownHook 关闭时,最好不要用此参数shutdownTimeout = 1000# 插件搜索所在的包名,可含多个包,以逗号隔开packages = org.foo,org.bar# Advertiser 插件名,用来发布 FileAppender 或 SocketAppender 的配置信息# 目前仅有的支持此功能的插件名为 multicastdnsadvertiser = multicastdns

Properties

# 自定义属性变量,下面定义了两个变量:pro1、pro2property.pro1 = value_1property.pro2 = value_2

Scripts

# 类型一:直接在配置文件中写入脚本script.s1.type = scriptscript.s1.name = s1script.s1.language = javascriptscript.s1.text = textOfScript# 类型二:配置文件仅指名脚本文件路径script.s2.type = scriptFilescript.s2.name = s2script.s2.path = scriptFilePath

CustomLevels

# 下面定义两个自定义级别:level1、level2,权重分别为:101、102customLevel.level1 = 101customLevel.level2 = 102

        

        详细关于自定义 Logger Level 的方式,及详细配置方法,请查阅:Log4j2 - Custom Log Levels

Filters

# 2.6 版本之前的写法# --------------------------------------------# 定义两过滤器变量 f1、f2filters = f1,f2# f1 的过滤器插件类型,值来源于具体过滤器类的注解 `@Plugin` 的 name 属性值# 必填filter.f1.type = nameOfFilterPlugin# 可以不设置,默认 neutralfilter.f1.onMatch = [accept|neutral|deny]# 可以不设置,默认 denyfilter.f1.onMismatch = [accept|neutral|deny]# 可能还有其他属性,取决于具体过滤器类型filter.f1.xxx = xxxfilter.f2.type = nameOfFilterPluginfilter.f2.onMatch = [accept|neutral|deny]filter.f2.onMismatch = [accept|neutral|deny]# DENY - 丢弃当前日志事件,且不传给其它过滤器# NEUTRAL - 中立,当前日志事件直接传给其他过滤器# ACCEPT - 接受当前日志事件,且不传给其他过滤器# 2.6 版本及之后的写法,自推断出不同过滤器,无须定义变量列表# ----------------------------------------------filter.f1.type = nameOfFilterPluginfilter.f1.onMatch = [accept|neutral|deny]filter.f1.onMismatch = [accept|neutral|deny]filter.f1.xxx = xxxfilter.f2.type = nameOfFilterPluginfilter.f2.onMatch = [accept|neutral|deny]filter.f2.onMismatch = [accept|neutral|deny]

   

        上面没介绍各种过滤器类型及各类型可配置的属性,请自行查阅:Log4j2 手册 - Filters

Appenders

# 2.6 版本前也需要定义变量列表,由于与过滤器类似,这里直接写 2.6版本之后的写法# appender 类型,值来源于具体过滤器类的注解 `@Plugin` 的 name 属性值# 必填appender.ap1.type = Console# 此 name 用来给下面的 Loggers 引用# 必填appender.ap1.name = nameOfAppender1# appender 其他属性取决于具体的 appender 类型# 本例中 Console 类型的它就含有一个 target 的属性appender.ap1.target = [SYSTEM_OUT|SYSTEM_ERR]# appender 自身的过滤器(选配),具体配置同上面独立的过滤器类似appender.ap1.filter.type = [accept|neutral|deny]appender.ap1.filter.onMatch = [accept|neutral|deny]appender.ap1.filter.onMisMatch = [accept|neutral|deny]# appender 自身的layout(选配),其 type 属性必填# 值来源于具体过滤器类的注解 `@Plugin` 的 name 属性值appender.ap1.layout.type = PatternLayout# layout 其他属性取决于具体的 layout 类型# 本例中 PatternLayout 具有 pattern 属性appender.ap1.layout.pattern = [%t] %-5p %c - %m%nappender.ap2.type = typeOfAppender2appender.ap2.name = nameOfAppender2appender.ap2.xxx = xxxappender.ap2.layout.type = xxxappender.ap2.layout.xxx = xxxappender.ap2.filter.type = [accept|neutral|deny]appender.ap2.filter.onMatch = [accept|neutral|deny]appender.ap2.filter.onMisMatch = [accept|neutral|deny]


        上面没介绍各种 Appender、Layout 类型及各类型可配置的属性及格式,请自行查阅:

  • Log4j2 手册 - Appenders

  • Log4j2 手册 - Layouts

Loggers

# 2.6 版本前也需要定义变量列表,由于与过滤器类似,这里直接写 2.6版本之后的写法# 记录器名称,一般是包或类的全限定名称,注意:会忽略名称叫 `root` 的 logger# 必填logger.log1.name = org.foo# 设置是否将日志事件打印到父、先辈的 appender 上# 注意:向上传的是所有日志事件,未经过自己的 logger 过滤# 具体在父、先辈如何过滤,要看父、先辈的 appender 上的过滤器logger.log1.additivity = true# 是否包含记录器所在业务代码的位置信息(高开销)# 同步 logger 默认为 true,# 异步 logger 默认为 false,因为涉及位置信息快照在不同线程之间传递logger.log1.includeLocation = [true|false]# 日志记录级别,左到右权重越来越大,确定级别后,小于等于其权重的都被记录logger.log1.level = [off|fatal|error|warn|info|debug|trace|all]# 需要异步 logger,就需要 type属性,其值为 AsyncLoggerlogger.log1.type = AsyncLogger# 该 logger 的记录信息需要发送到的 appender 引用,其值为 appender 定义中的 name 属性# 一个 logger 可以配置多个 appender,通过 appenderRef.XXX 变量名来区分# 必填logger.log1.appenderRef.ap1.ref = nameOfAppender1# 确定小于等于哪个日志级别的消息需要传递到该 appenderlogger.log1.appenderRef.ap1.level = [off|fatal|error|warn|info|debug|trace|all]# 在上面参数定义的 levle 范围内再次进行过滤,且只能包含一个过滤器logger.log1.appenderRef.ap1.filter.f.type = xxxlogger.log1.appenderRef.ap1.filter.f.onMatch = [accept|neutral|deny]logger.log1.appenderRef.ap1.filter.f.onMisMatch = [accept|neutral|deny]# 如果想要多组过滤器,只能将这些过滤器组合成一个组合过滤器 CompositeFilter,格式如下:# type 设置为 `filters`,就能找到 CompositeFilter 插件logger.file.appenderRef.random.filter.f.type = filterslogger.file.appenderRef.random.filter.f.f1.type = xxxlogger.file.appenderRef.random.filter.f.f1.xxx = xxxlogger.file.appenderRef.random.filter.f.f2.type = xxxlogger.file.appenderRef.random.filter.f.f2.xxx = xxx# 一个 logger 可以有多个 appenderRef,如下所示为第二个定义logger.log1.appenderRef.ap2.ref = nameOfAppender2logger.log1.appenderRef.ap2.level = [off|fatal|error|warn|info|debug|trace|all]logger.log1.appenderRef.ap2.filter.f1.type = xxxlogger.log1.appenderRef.ap2.filter.f1.onMatch = [accept|neutral|deny]logger.log1.appenderRef.ap2.filter.f1.onMisMatch = [accept|neutral|deny]# 注意 logger 中日志级别的优先级:# log1.level > log1.appenderRef.ap1.level > log1.appenderRef.ap1.filter# 也就是说,ap1.filter 在 ap1.level定义的范围做过滤,# 而 ap1.level 又在 log1.level 的范围内限制

        上面没介绍 AsyncLogger 及其具体配置方式,请自行查阅:Log4j2 手册 - AsyncLoggers

RootLogger

# 与上面 Loggers 相比,除不能有 name、additivity 属性定义外# 最重要是配置节点元素变成了 rootLogger,且它后面必要根变量名,直接写它的属性,因为只有一个 root# rootLogger 不能配置名称,不代表它没有名称,它的名称为:“” 空字符串,不是 null# 需要异步 logger,就需要 type属性,其值为 AsyncLoggerrootLogger.type = AsyncLogger# 是否包含记录器所在业务代码的位置信息(高开销)# 同步 logger 默认为 true,# 异步 logger 默认为 false,因为涉及位置信息快照在不同线程之间传递rootLogger.includeLocation = [true|false]# 默认为 errorrootLogger.level = [off|fatal|error|warn|info|debug|trace|all]rootLogger.appenderRef.ap1.ref = xxxrootLogger.appenderRef.ap1.level = [off|fatal|error|warn|info|debug|trace|all]# 在上面参数定义的 levle 范围内再次进行过滤,且只能包含一个过滤器rootLogger.appenderRef.ap1.filter.f.type = xxxrootLogger.appenderRef.ap1.filter.f.onMatch = [accept|neutral|deny]rootLogger.appenderRef.ap1.filter.f.onMisMatch = [accept|neutral|deny]

配置实践

下面的配置假设的场景为:

  • 每隔一分钟检测配置文件是否变化

  • 标准控制台只打印警告及以上的更为严重的信息

  • 更详细的包含提示信息的日志都保存到当前应用的 logs/rolling 目录下的 log.log

  • 每次应用启动、每隔 1 小时时,归档当前活动的文件,并创建新的活动文件 logs/rolling/log.log

  • 活动文件限制 500 MB,超过归档处理,每小时内只允许保留最近的 5 个归档文件,如此循环

    • log-2018-05-15-19-1.log (最旧归档一直是此名称)

    • log-2018-05-15-19-2.log

    • log-2018-05-15-19-3.log

    • log-2018-05-15-19-4.log

    • log-2018-05-15-19-5.log (最新归档一直是此名称)

    • log.log (活动的日志文件一直是此名称)


# ------------------------------------------------------------------- loggerConfigdest = errstatus = errorname = PropertiesConfig# 每隔 1 分钟检测一次配置文件是否有变动monitorInterval = 60# -------------------------------------------------------------------- properties# 利用全局属性定义一个日志格式属性,方便下文引用property.pattern = %d{yyyy-MM-dd HH:mm:ss,SSS} [%t] %-5p %c - %m%n# -------------------------------------------------------------------- appenders# ************* Console APPENDER **********************# 控制台 Appender,只打印 警告及以上的信息appender.console.type = Consoleappender.console.name = STDOUTappender.console.target = SYSTEM_OUTappender.console.layout.type = PatternLayout# 这里引用了全局属性变量:patternappender.console.layout.pattern = ${pattern}appender.console.filter.threshold.type = ThresholdFilterappender.console.filter.threshold.level = warn# ************* RollingFile APPENDER **********************# 可循环的日志记录文件 Appender,appender.rolling.type = RollingFileappender.rolling.name = ROLLINGFILEappender.rolling.fileName = logs/rolling/log.logappender.rolling.filePattern = logs/rolling/log-%d{yyyy-MM-dd-HH}-%i.log.gzappender.rolling.layout.type = PatternLayoutappender.rolling.layout.pattern = ${pattern}# 缓存大小,默认8192,单位:字节appender.rolling.bufferSize = 8192# 默认使用缓存,如想禁用取消注释#appender.rolling.bufferedIO = false# 是否立即刷新,默认false,建议保持默认#appender.rolling.immediateFlush = true# ************* Policies **********************# 触发归档文件的原则(组合型):# 1. 当前日志文件大小已达到 500 MB# 2. 基于时间的触发# %d{yyyy-MM-dd-HH} 最小单位为小时,interval = 1# 表示:每 1 小时触发一次appender.rolling.policies.type = Policiesappender.rolling.policies.size.type = SizeBasedTriggeringPolicyappender.rolling.policies.size.size= 500MBappender.rolling.policies.time.type = TimeBasedTriggeringPolicyappender.rolling.policies.time.interval = 1# 是否规整均分,例如:现在是凌晨3点,interval设置为4# true - 均分为 0,4,8,12,16,20,则下一次应该要凌晨4点才触发# false - 不均分,3,7,11,15,19,23appender.rolling.policies.time.modulate = true# ************* Strategy_1 **********************# 文件发生重名时,控制 filePattern 里 '%i' 的数值变化策略# 此处为默认策略:DefaultRolloverStrategy,它需要 appender 的 fileName 属性# 因为活动状态的文件一直用 fileName 的名称appender.rolling.strategy.type = DefaultRolloverStrategy# 最小值,默认为 1,如果想从 0 开始编号,取消注释# appender.rolling.strategy.min = 0# 默认为 7,这里设置为5appender.rolling.strategy.max = 5# ************* Strategy_2 **********************# 设置最新归档文件的 `%i` 是数值最大,还是最小,或是一直累加# 1. max -最新的归档数值最大;# 2. min -最新的归档数值最小;# 3. nomax -最新的归档数值最大,且不在 [min,max] 中循环,而是一直累加# 这里采用默认的 max,故无需设置#appender.rolling.strategy.fileIndex = nomax# 文件发生重名时,控制 filePattern 里 '%i' 的数值变化策略,这里的 i 是一直累加# 此处为默认策略:DirectWriteRolloverStrategy,它【不】需要 appender 的 fileName 属性# 因为活动状态的文件名就是 filePattern 去除 .gz 的文件名#appender.rolling.strategy.type = DirectWriteRolloverStrategy# 允许保留的归档文件数,默认全部保留,如果设置,值必须 ≥ 1# 如下面设置,归档文件超过 3,就删除最旧的那个归档包#appender.rolling.strategy.maxFiles = 3# ------------------------------------------------------------------------ loggers# ************* ROOT LOGGER **********************rootLogger.level = warnrootLogger.appenderRef.stdout.ref = STDOUT# ************* org.reion LOGGER **********************logger.file.name = org.reion# 是否打印到父、先辈的 appender,默认为 true,如果关闭,请取消注释#logger.file.additivity = falselogger.file.level = infologger.file.includeLocation = falselogger.file.appenderRef.rolling.ref = ROLLINGFILE

总结

  自此完成了基础的日志配置,Log4j 2 的一些新特性,如:异步日志、Free GC、MDC、Lookups 等内容还没有完全展开,另外其记录性能如何有待测试,有空再续……

  • 参考资料:

    • Log4j 1.x 手册 http://logging.apache.org/log4j/1.2/manual.html

    • Log4j 2 官方文档 https://logging.apache.org/log4j/2.x/

    • Which Java Logging Framework Has the Best Performance?

代码索引

  • Code_01 - LogManager.getLogger() 最初的“蝴蝶”

  • Code_02 - LogManager 静态初始化代码

  • Code_03 - LogManager 委托上下文获取记录器

  • Code_04 - Log4jContextFactory.getContext

  • Code_05 - BasicContextSelector.getContext

  • Code_06 - ClassLoaderContextSelector.getContext 默认

  • Code_07 - LoggerContext.start

  • Code_08 - ConfigurationFactory.getInstance

  • Code_09 - ConfigurationFactory.Factory.getConfiguration

  • Code_10 - PropertiesConfigurationFactory.getConfiguration

  • Code_11 - LoggerContext.reconfigure

  • Code_12 - LoggerContext.setConfiguration 第二只“蝴蝶”

  • Code_13 - LoggerContext.getLogger

脚注

  1. 1996 年初,E.U. SEMPER 制定日志 API 来统一规则 ↩

  2. 1999-10-15,log4j 初始版发布 ↩

  3. 1999-12-17,Sun、IBM 提交 JSR-047, 2001-12-17 纳入 Java 规范 ↩

  4. 2006-5-14,Log4j 作者 Ceki Gülcü 提交初始 Logback 代碼,详见 Maillist ↩

  5. 2012-05-13,Log4j 发布终版,并于 2015-08-05 停止维护 ↩

  6. 2012-07-29, Ralph Goers 发布初始版 Log4j 2,详见 Changes ↩


以上是关于Log4j 2 指南的主要内容,如果未能解决你的问题,请参考以下文章

Log4j 2 指南

Apache开源日志框架Log4j配置指南

markdown 打字稿...编码说明,提示,作弊,指南,代码片段和教程文章

Log4j for C++ 实用指南

Log4j-----Log4j使用指南

Vue3官网-高级指南(十七)响应式计算`computed`和侦听`watchEffect`(onTrackonTriggeronInvalidate副作用的刷新时机`watch` pre)(代码片段