log4j2或者logback配置模版实现灵活输出服务名

2023-09-18 15:03:54

介绍

在我们使用log4j2或者logback打印日志时,输出的内容中通常是一定要加上服务名的。以log4j2为例:

<!--输出控制台的配置-->
<Console name="Console" target="SYSTEM_OUT">
    <!-- 输出日志的格式 -->
    <PatternLayout pattern="server-case %d{yyyy-MM-dd HH:mm:ss} %5p %c{1}:%L - %m%n"/>
</Console>

服务名为server-case,输出的内容为

server-case 2023-09-15 17:44:38  INFO ServerCaseApplication:648 - No active profile set, falling back to default profiles: default
server-case 2023-09-15 17:44:39  INFO TomcatWebServer:108 - Tomcat initialized with port(s): 7081 (http)
server-case 2023-09-15 17:44:39  INFO Http11NioProtocol:173 - Initializing ProtocolHandler ["http-nio-7081"]
server-case 2023-09-15 17:44:39  INFO StandardService:173 - Starting service [Tomcat]
...

但是有种情况,有的项目要部署在甲方内网或者连接甲方的资源。项目是同一套代码,但要服务于不同的甲方,所以一个项目会有不同的服务名的情况。

这样的话,服务名就不能写死,要根据不同的服务名来输出。

问题

有的人可能会想到直接设置一个对象实现EnvironmentAware接口中的setEnvironment(Environment environment),来获取environment来获取spring.application.name

但这样有问题,设置的这个对象是要在spring上下文中进行加载后才能获得environment,所以在这个对象加载之前的日志输出还是拿不到environment

@Component
public class Test implements EnvironmentAware {
    
    private Environment environment;
    
    @Override
    public void setEnvironment(final Environment environment) {
        this.environment = environment;
        System.setProperty("applicationName", Objects.requireNonNull(environment.getProperty("spring.application.name")));
    }
}
<!--输出控制台的配置-->
<Console name="Console" target="SYSTEM_OUT">
    <!-- 输出日志的格式 -->
    <PatternLayout pattern="${sys:applicationName} %d{yyyy-MM-dd HH:mm:ss} %5p %c{1}:%L - %m%n"/>
</Console>
${sys:applicationName} 2023-09-18 10:18:34  INFO ServerCaseApplication:55 - Starting ServerCaseApplication on lukuan with PID 21472 (D:\idea_work_my\gitee\cook-frame\server\server-case\target\classes started by lukuan in D:\idea_work_my\gitee\cook-frame)
${sys:applicationName} 2023-09-18 10:18:34  INFO ServerCaseApplication:648 - No active profile set, falling back to default profiles: default
${sys:applicationName} 2023-09-18 10:18:34  INFO TomcatWebServer:108 - Tomcat initialized with port(s): 7081 (http)
${sys:applicationName} 2023-09-18 10:18:34  INFO Http11NioProtocol:173 - Initializing ProtocolHandler ["http-nio-7081"]
${sys:applicationName} 2023-09-18 10:18:34  INFO StandardService:173 - Starting service [Tomcat]
${sys:applicationName} 2023-09-18 10:18:34  INFO StandardEngine:173 - Starting Servlet engine: [Apache Tomcat/9.0.46]
${sys:applicationName} 2023-09-18 10:18:34  INFO [/]:173 - Initializing Spring embedded WebApplicationContext
${sys:applicationName} 2023-09-18 10:18:34  INFO ServletWebServerApplicationContext:285 - Root WebApplicationContext: initialization completed in 795 ms
 _ _   |_  _ _|_. ___ _ |    _ 
| | |\/|_)(_| | |_\  |_)||_|_\ 
     /               |         
                        3.5.1 
service-case 2023-09-18 10:18:35  INFO ThreadPoolTaskExecutor:181 - Initializing ExecutorService 'applicationTaskExecutor'
service-case 2023-09-18 10:18:35  INFO PropertySourcedRequestMappingHandlerMapping:69 - Mapped URL path [/v2/api-docs] onto method [springfox.documentation.swagger2.web.Swagger2Controller#getDocumentation(String, HttpServletRequest)]
service-case 2023-09-18 10:18:35  INFO Http11NioProtocol:173 - Starting ProtocolHandler ["http-nio-7081"]
service-case 2023-09-18 10:18:35  INFO TomcatWebServer:220 - Tomcat started on port(s): 7081 (http) with context path ''

可以看到比较靠前的日志输出中的applicationName变量是没有被替换成真正的服务名的。

那怎么办呢?

分析

所以我们要从SpringBoot的启动过程入手

SpringApplication#run(String… args)
public ConfigurableApplicationContext run(String... args) {
    StopWatch stopWatch = new StopWatch();
    stopWatch.start();
    ConfigurableApplicationContext context = null;
    configureHeadlessProperty();
    SpringApplicationRunListeners listeners = getRunListeners(args);
    listeners.starting();
    try {
        ApplicationArguments applicationArguments = new DefaultApplicationArguments(args);
        ConfigurableEnvironment environment = prepareEnvironment(listeners, applicationArguments);
        configureIgnoreBeanInfo(environment);
        Banner printedBanner = printBanner(environment);
        context = createApplicationContext();
        prepareContext(context, environment, listeners, applicationArguments, printedBanner);
        refreshContext(context);
        afterRefresh(context, applicationArguments);
        stopWatch.stop();
        if (this.logStartupInfo) {
            new StartupInfoLogger(this.mainApplicationClass).logStarted(getApplicationLog(), stopWatch);
        }
        listeners.started(context);
        callRunners(context, applicationArguments);
    }
    catch (Throwable ex) {
        handleRunFailure(context, ex, listeners);
        throw new IllegalStateException(ex);
    }

    try {
        listeners.running(context);
    }
    catch (Throwable ex) {
        handleRunFailure(context, ex, null);
        throw new IllegalStateException(ex);
    }
    return context;
}

突破点在environment的创建这步骤,所以分析prepareEnvironment(listeners, applicationArguments)看能不能实现我们想要的需求

SpringApplication#prepareEnvironment
private ConfigurableEnvironment prepareEnvironment(SpringApplicationRunListeners listeners,
        ApplicationArguments applicationArguments) {
    // Create and configure the environment
    ConfigurableEnvironment environment = getOrCreateEnvironment();
    configureEnvironment(environment, applicationArguments.getSourceArgs());
    ConfigurationPropertySources.attach(environment);
    //经过debug发现当这步方法执行完后,environment.getProperty("spring.application.name")是可以获取值
    listeners.environmentPrepared(environment);
    bindToSpringApplication(environment);
    if (!this.isCustomEnvironment) {
        environment = new EnvironmentConverter(getClassLoader()).convertEnvironmentIfNecessary(environment,
                deduceEnvironmentClass());
    }
    ConfigurationPropertySources.attach(environment);
    return environment;
}

listeners.environmentPrepared(environment);为关键点,environment.getProperty("spring.application.name")就是在此进行完成的,所以我们要进入分析。

SpringApplicationRunListeners#environmentPrepared
void environmentPrepared(ConfigurableEnvironment environment) {
    for (SpringApplicationRunListener listener : this.listeners) {
        listener.environmentPrepared(environment);
    }
}

listeners有一个实现类EventPublishingRunListener

public void environmentPrepared(ConfigurableEnvironment environment) {
    this.initialMulticaster
            .multicastEvent(new ApplicationEnvironmentPreparedEvent(this.application, this.args, environment));
}

这里是发布了一个ApplicationEnvironmentPreparedEvent事件。

既然有发布,就有监听。我们接下来分析下监听事件的逻辑

ConfigFileApplicationListener#onApplicationEvent
public void onApplicationEvent(ApplicationEvent event) {
    if (event instanceof ApplicationEnvironmentPreparedEvent) {
        onApplicationEnvironmentPreparedEvent((ApplicationEnvironmentPreparedEvent) event);
    }
    if (event instanceof ApplicationPreparedEvent) {
        onApplicationPreparedEvent(event);
    }
}
private void onApplicationEnvironmentPreparedEvent(ApplicationEnvironmentPreparedEvent event) {
    List<EnvironmentPostProcessor> postProcessors = loadPostProcessors();
    postProcessors.add(this);
    AnnotationAwareOrderComparator.sort(postProcessors);
    for (EnvironmentPostProcessor postProcessor : postProcessors) {
        postProcessor.postProcessEnvironment(event.getEnvironment(), event.getSpringApplication());
    }
}

postProcessors有多个,其中ConfigFileApplicationListener就是设置spring.applicaton.name的值。至于怎么设置的,就不进去分析了,有兴趣的同学可以自行去仔细研究。

通常上述分析,我们知道了Environment设置spring.applicaton.name值的步骤,那么我们可不可以紧接着,这个设置步骤之后就进行自定义设值,然后让log4j2来取呢?

答案是当然可以!

让我们回到SpringApplicationRunListeners#environmentPrepared

SpringApplicationRunListeners#environmentPrepared
void environmentPrepared(ConfigurableEnvironment environment) {
    for (SpringApplicationRunListener listener : this.listeners) {
        listener.environmentPrepared(environment);
    }
}

默认listeners为一个,那我们只要自己实现一个,让listeners为2个,第一个默认执行完后,不就能紧接着执行我们的了吗,那我们分析下listeners是怎么来的

SpringApplication#getRunListeners
private SpringApplicationRunListeners getRunListeners(String[] args) {
    Class<?>[] types = new Class<?>[] { SpringApplication.class, String[].class };
    return new SpringApplicationRunListeners(logger,
            getSpringFactoriesInstances(SpringApplicationRunListener.class, types, this, args));
}
private <T> Collection<T> getSpringFactoriesInstances(Class<T> type, Class<?>[] parameterTypes, Object... args) {
    ClassLoader classLoader = getClassLoader();
    // Use names and ensure unique to protect against duplicates
    Set<String> names = new LinkedHashSet<>(SpringFactoriesLoader.loadFactoryNames(type, classLoader));
    List<T> instances = createSpringFactoriesInstances(type, parameterTypes, classLoader, args, names);
    AnnotationAwareOrderComparator.sort(instances);
    return instances;
}

SpringFactoriesLoader.loadFactoryNames(type, classLoader)和自动装配的方法很像啊,那去看一眼

org.springframework.boot.SpringApplicationRunListener=\
org.springframework.boot.context.event.EventPublishingRunListener

果然在文件中指定了org.springframework.boot.context.event.EventPublishingRunListener,也就是默认的SpringApplicationRunListener实现类。

然后在分析createSpringFactoriesInstances看看是如何加载的

private <T> List<T> createSpringFactoriesInstances(Class<T> type, Class<?>[] parameterTypes,
        ClassLoader classLoader, Object[] args, Set<String> names) {
    List<T> instances = new ArrayList<>(names.size());
    for (String name : names) {
        try {
            Class<?> instanceClass = ClassUtils.forName(name, classLoader);
            Assert.isAssignable(type, instanceClass);
            Constructor<?> constructor = instanceClass.getDeclaredConstructor(parameterTypes);
            T instance = (T) BeanUtils.instantiateClass(constructor, args);
            instances.add(instance);
        }
        catch (Throwable ex) {
            throw new IllegalArgumentException("Cannot instantiate " + type + " : " + name, ex);
        }
    }
    return instances;
}

这里就是创建对象的过程了,用的是有参构造方法需要两个参数constructor, args,所以我们参考EventPublishingRunListener的结构来实现即可

实现

自定义SpringApplicationRunListener的实现类CustomEventPublishingRunListener

CustomEventPublishingRunListener
public class CustomEventPublishingRunListener implements SpringApplicationRunListener, Ordered {
    
    private static final String SPRING_APPLICATION_NAME = "spring.application.name";
    
    private final SpringApplication application;
    
    private final String[] args;
    
    public CustomEventPublishingRunListener(SpringApplication application, String[] args){
        this.application = application;
        this.args = args;
    }
    @Override
    public int getOrder() {
        return 1;
    }
    
    @Override
    public void environmentPrepared(ConfigurableEnvironment environment) {
        String applicationName = environment.getProperty(SPRING_APPLICATION_NAME);
        if (StringUtil.isNotEmpty(applicationName)) {
            System.setProperty("applicationName", applicationName);
        }
    }
}
  • 有参构造方法中的SpringApplication application, String[] args参数为固定的
  • getOrder返回的值要为1,因为EventPublishingRunListener中的getOrder返回值为0

在自动装配文件spring.factories指定位置

org.springframework.boot.SpringApplicationRunListener=\
  com.example.runlistener.CustomEventPublishingRunListener

在log4j2的xml文件中进行取值

<!--输出控制台的配置-->
<Console name="Console" target="SYSTEM_OUT">
    <!-- 输出日志的格式 -->
    <PatternLayout pattern="${sys:applicationName} %d{yyyy-MM-dd HH:mm:ss} %5p %c{1}:%L - %m%n"/>
</Console>

输出结果

service-case 2023-09-18 14:57:25  INFO ServerCaseApplication:55 - Starting ServerCaseApplication on lukuan with PID 20004 (D:\idea_work_my\gitee\cook-frame\server\server-case\target\classes started by lukuan in D:\idea_work_my\gitee\cook-frame)
service-case 2023-09-18 14:57:25  INFO ServerCaseApplication:648 - No active profile set, falling back to default profiles: default
service-case 2023-09-18 14:57:25  INFO TomcatWebServer:108 - Tomcat initialized with port(s): 7081 (http)
service-case 2023-09-18 14:57:25  INFO Http11NioProtocol:173 - Initializing ProtocolHandler ["http-nio-7081"]
service-case 2023-09-18 14:57:25  INFO StandardService:173 - Starting service [Tomcat]
service-case 2023-09-18 14:57:25  INFO StandardEngine:173 - Starting Servlet engine: [Apache Tomcat/9.0.46]
service-case 2023-09-18 14:57:25  INFO [/]:173 - Initializing Spring embedded WebApplicationContext
service-case 2023-09-18 14:57:25  INFO ServletWebServerApplicationContext:285 - Root WebApplicationContext: initialization completed in 716 ms
 _ _   |_  _ _|_. ___ _ |    _ 
| | |\/|_)(_| | |_\  |_)||_|_\ 
     /               |         
                        3.5.1 
service-case 2023-09-18 14:57:26  INFO ThreadPoolTaskExecutor:181 - Initializing ExecutorService 'applicationTaskExecutor'
service-case 2023-09-18 14:57:26  INFO PropertySourcedRequestMappingHandlerMapping:69 - Mapped URL path [/v2/api-docs] onto method [springfox.documentation.swagger2.web.Swagger2Controller#getDocumentation(String, HttpServletRequest)]
service-case 2023-09-18 14:57:26  INFO Http11NioProtocol:173 - Starting ProtocolHandler ["http-nio-7081"]
service-case 2023-09-18 14:57:26  INFO TomcatWebServer:220 - Tomcat started on port(s): 7081 (http) with context path ''
....

可以看到实现了我们想要的功能

更多推荐

采棉机工作过程的关键动作

1、起动:确认机械变速手柄和液压手柄在空挡位置——确认手油门在最小位置——关闭所有照明开关和电气——打开钥匙开关——检查所有仪表是否符合起动的要求——灯检——响喇叭警示周围人员——起动(800—900转/分稳定后)——怠速(1200转/分)运行2分钟(冬天4分钟)。2、熄火:停车(一般不要使用制动器,防止采棉机前倾栽头

TypeScript项目配置

前言我们需要建立tsconfig.json作用用于标识TypeScript项目的根路径;用于配置TypeScript编译器;用于指定编译的文件。重要字段files-设置要编译的文件的名称;include-设置需要进行编译的文件,支持路径模式匹配;exclude-设置无需进行编译的文件,支持路径模式匹配;compiler

ThinkPHP5,使用unionAll取出两个毫无相关字段表的数据且分页

一:首先来了解一下union和unionAll1:取结果的并集,是否去重union:对两个结果集进行并集操作,不包括重复行,相当于distinct,同时进行默认规则的排序;unionAll:对两个结果集进行并集操作,包括重复行,即所有的结果全部显示,不管是不是重复;2:获取结果后的操作,是否排序union:会对获取的结

基于Java+SpringBoot+Vue的旧物置换网站设计和实现

基于Java+SpringBoot+Vue的旧物置换网站设计和实现源码传送入口前言主要技术系统设计功能截图数据库设计代码论文目录订阅经典源码专栏Java项目精品实战案例《500套》源码获取源码传送入口前言摘要随着时代在一步一步在进步,旧物也成人们的烦恼,许多平台网站都在推广自已的产品像天猫、咸鱼、京东。所以开发出一套关

云计算(Docker)

Docker简介Docker是一个开源的应用容器引擎,基于Go语言,并遵从Apache2.0协议开源。它可以让开发者打包应用以及依赖包到一个轻量级、可移植的容器中,然后发布到任何流行的Linux机器上,也可以实现虚拟化。Docker可用于开发应用、交付应用、运行应用等场景。容器是完全使用沙箱机制,相互之间不会有任何接口

【ICCV 2023】FocalFormer3D : Focusing on Hard Instance for 3D Object Detection

原文链接:https://arxiv.org/abs/2308.045561.引言目前的3D目标检测方法没有显式地去考虑漏检问题。本文提出了困难实例探测(HIP)。受目标检测的级联解码头启发,HIP逐步探测误检样本,极大提高召回率。在每个阶段,HIP抑制TP样本并关注之前阶段的FN样本,则通过迭代HIP,可以处理困难的

ES6中新增加的Proxy对象及其使用方式

聚沙成塔·每天进步一点点⭐专栏简介⭐Proxy对象的基本概念Proxy对象的主要陷阱(Traps)⭐使用Proxy对象⭐写在最后⭐专栏简介前端入门之旅:探索Web开发的奇妙世界记得点击上方或者右侧链接订阅本专栏哦几何带你启航前端之旅欢迎来到前端入门之旅!这个专栏是为那些对Web开发感兴趣、刚刚踏入前端领域的朋友们量身打

ffmpeg & ffplay

gif->jpg:```ffmpeg-i4.gif-r25-q:v4-pix_fmtyuv420pjpg2/frame%03d.jpg-y```#ffplay```ffplay[选项]['输入文件']```option```'-xwidth'强制以"width"宽度显示'-yheight'强制以"height"高度显示

【农业生产模拟】WOFOST模型与PCSE模型教程

查看原文>>>【农业生产模拟】WOFOST模型与PCSE模型实践目录第一章:理论基础农作物生长模型概述第二章:数据准备第三章:WOFOST模型基础第四章:PythonCropSimulationEnvironment第五章:案例拓展WOFOST(WorldFoodStudies)和PCSE(PythonCropSimu

多频超声波清洗机有什么特点?使用需要注意什么?

多频超声波清洗机是指具备多个频率可调的超声波发生器的清洗机,是在一只清洗槽内,安装有两种或三种以上不同频率的超声波换能器,由多只发生器分别推动各自频率的超声波进行清洗。传统的超声波清洗机通常只能在固定的频率下工作,而多频超声波清洗机具有更广泛的应用能力。梵英超声(fanyingsonic)是专业超声波设备制造商,推荐使

Springboot整合ElasticSearch(1)- 环境搭建 -非自动注入的方式

Springboot整合ElasticSearch(1)1、基本信息1、SpringBoot版本:2.7.14<parent><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-parent</artifactId><ve

热文推荐