• 了解 spring 生态及框架的 java er 都知道,spring 应用的生命周期管理及配套接口较为优雅、可扩展。

  • 但脱离 spring 的 java 应用程序,如何优雅地启停、管理程序的生命周期呢?(以便应用程序在我们需要的运行阶段中进行相应的动作)

概述:Java普通应用程序的启停钩子框架

前置知识:java.lang.FunctionalInterface注解

Java普通应用程序的启停钩子框架

ApplicationStartupHook: 抽象的启动钩子接口

package org.example.app.hooks.startup;

@FunctionalInterface
public interface ApplicationStartupHook {
/**
* execute the task
* @throws Exception
*/
void execute() throws Exception;
}

ApplicationStartupHookManager : 统一管理启动钩子

package org.example.app.hooks.startup;

import java.util.ArrayList;
import java.util.List; public class ApplicationStartupHookManager {
private static final List<ApplicationStartupHook> hooks = new ArrayList<>();
private static boolean executed = false; // 注册启动任务
public static void registerHook(ApplicationStartupHook hook) {
if (executed) {
throw new IllegalStateException("Application startup hooks already executed");
}
hooks.add(hook);
} // 执行所有启动任务
public static void run() throws Exception {
if (!executed) {
for (ApplicationStartupHook hook : hooks) {
hook.execute();
}
executed = true;
}
}
}

ApplicationShutdownHook : 关闭钩子

package org.example.app.hooks.shutdown;

@FunctionalInterface
public interface ApplicationShutdownHook {
/**
* execute the task
* @throws Exception
*/
void execute() throws Exception;
}

ApplicationShutdownHookManager : 统一管理关闭钩子

package org.example.app.hooks.shutdown;

import lombok.Getter;
import org.example.app.hooks.startup.ApplicationStartupHook; import java.util.ArrayList;
import java.util.List; public class ApplicationShutdownHookManager {
private static final List<ApplicationShutdownHook> hooks = new ArrayList<>();
@Getter
private static boolean executed = false; // 注册启动任务
public static void registerHook(ApplicationShutdownHook hook) {
if (executed) {
throw new IllegalStateException("Application shutdown hooks already executed");
}
hooks.add(hook);
} // 执行所有启动任务
public static void run() throws Exception {
if (!executed) {
for (ApplicationShutdownHook hook : hooks) {
hook.execute();
}
executed = true;
}
}
}

Demo应用 : Slf4j + Log4j2 + Log4j2 KafkaAppender + Kafka

Maven 依赖

<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion> <parent>
<groupId>org.example</groupId>
<artifactId>demos-application-parent</artifactId>
<version>1.0.0-SNAPSHOT</version>
<relativePath>../pom.xml</relativePath>
</parent> <artifactId>log4j2-kafka-appender-demo-application</artifactId>
<packaging>jar</packaging> <name>bdp-diagnosticbox-model</name>
<url>http://maven.apache.org</url> <properties>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<lombok.version>1.18.22</lombok.version>
<slf4j.version>1.7.30</slf4j.version>
<log4j.version>2.20.0</log4j.version>
<kafka-clients.version>2.7.2</kafka-clients.version>
</properties> <dependencies>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<version>${lombok.version}</version>
</dependency> <!-- log -->
<dependency>
<groupId>org.slf4j</groupId>
<artifactId>slf4j-api</artifactId>
<version>${slf4j.version}</version>
</dependency>
<dependency>
<groupId>org.apache.logging.log4j</groupId>
<artifactId>log4j-api</artifactId>
<version>${log4j.version}</version>
</dependency>
<dependency>
<groupId>org.apache.logging.log4j</groupId>
<artifactId>log4j-core</artifactId>
<version>${log4j.version}</version>
</dependency>
<dependency>
<groupId>org.apache.logging.log4j</groupId>
<artifactId>log4j-slf4j-impl</artifactId>
<version>${log4j.version}</version>
<exclusions>
<exclusion>
<artifactId>slf4j-api</artifactId>
<groupId>org.slf4j</groupId>
</exclusion>
</exclusions>
</dependency>
<dependency>
<groupId>org.apache.logging.log4j</groupId>
<artifactId>log4j-jul</artifactId>
<!--<version>2.13.3</version>-->
<version>${log4j.version}</version>
<scope>compile</scope>
</dependency>
<!-- log [end] --> <!-- kafka client -->
<dependency>
<groupId>org.apache.kafka</groupId>
<artifactId>kafka-clients</artifactId>
<version>${kafka-clients.version}</version>
</dependency>
</dependencies>
</project>

配置文件

  • resource/log4j2.xml
<?xml version="1.0" encoding="UTF-8"?>
<!--<Configuration status="debug" name="demo-application" packages="org.example.app">-->
<Configuration status="off">
<!-- 自定义属性 -->
<Properties>
<!-- 应用程序名称 -->
<Property name="application.name">bdp-xxx-app</Property>
<!-- 应用程序实例的主机地址 -->
<Property name="application.instance.host">${env:HOST_IP:-127.0.0.1}</Property>
<!-- 应用程序实例的名称,默认值: localInstance -->
<Property name="application.instance.name">${env:INSTANCE_NAME:-localInstance}</Property> <!-- 字符集 -->
<Property name="log.encoding">UTF-8</Property>
<!-- 日志等级,默认 INFO -->
<Property name="log.level" value="${env:LOG_ACCESS:-INFO}" />
<!--<Property name="log.access.level">${env:LOG_ACCESS:-INFO}</Property>-->
<!--<Property name="log.operation.level">${env:LOG_OPERATE:-INFO}</Property>-->
<Property name="log.threshold">${log.level}</Property> <!-- org.apache.log4j.PatternLayout -->
<!--<Property name="log.layout" value="CustomPatternLayout"></Property>-->
<Property name="log.layout" value="PatternLayout"></Property> <!-- property.log.layout.consolePattern=%d{yyyy/MM/dd HH:mm:ss.SSS} %-5p | %T | %t | (%C{1}.java:%L %M) | %m%n -->
<!-- [%d{yyyy/MM/dd HH:mm:ss.SSS}] [%traceId] [%-5p] [%t] [%C{1}.java:%L %M] %m%n -->
<!-- [%d{yyyy/MM/dd HH:mm:ss.SSS}] [%X{traceId}] [%-5p] [%t] [%C{1}.java:%L %M] %m%n -->
<!-- [%traceId] [${application.name}] [system] [%d{yyyy/MM/dd HH:mm:ss.SSS}] [%-5p] [%t] [%C{1}] %M:%L__|%X{traceId}|__%m%n -->
<!-- [${application.name}] [${instance.name}] [${env:HOST_IP}] [${env:CONTAINER_IP}] [%d{yyyy/MM/dd HH:mm:ss.SSS}] [%p] [%t] [%l] %m%n -->
<!-- ↓ sample: 2023-02-02 14:35:38,664 WARN main (MonitorController.java:141 lambda$null$0) name: cn.seres.bd.dataservice.common.query, configLevel(Level):DEBUG, effectiveLevel: DEBUG -->
<!-- %d %-5p %t (%C{1}.java:%L %M) %m%n -->
<!-- [%d %r] [%-5p] [%t] [%l] [%m]%n -->
<!-- %d{yyyy-MM-dd HH\:mm\:ss} %-5p[%t] : %m%n -->
<!-- ↓ sample: 2025-02-21 15:24:27 INFO | [2aa06a7b-a81f-469b-a0a0-679005bc35a3] | Log4jKafkaAppenderDemoEntry:36 - 这是一条信息日志 -->
<!--%d{HH:mm:ss.SSS} %-5p [%-7t] %F:%L - %m%n -->
<!--[%-4level] | %d{YYYY-MM-dd HH:mm:ss} | [%X{REQ_ID}] | %m| ${sys:java.home}%n-->
<!-- %d{yyyy-MM-dd HH:mm:ss} %-5p | [%X{REQ_ID}] | %c{1}:%L - %m%n -->
<!-- ${log.appender.kafka.producer.bootstrap.servers} | ${bundle:application:org.example.confgKey} | ${main:\\-logLevel} | ${main:\\-\-log\.appender\.kafka\.producer\.bootstrap\.servers} | %c{1}:%L - %m%n -->
<Property name="log.layout.consolePattern">
[%d{yyyy/MM/dd HH:mm:ss.SSS}] [%X{traceId}] [%-5p] [%t] [%C{1}.java:%L %M] %m%n
</Property>
<Property name="log.layout.mainPattern" value="${log.layout.consolePattern}" /> <!-- KafkaAppender 的属性值 -->
<!-- 方式1: 从环境变量获取 -->
<!--<Property name="log.appender.kafka.producer.bootstrap.servers" value="${env:KAFKA_PRODUCER_BOOTSTRAP_SERVERS:-127.0.0.1:9092}"/>-->
<!-- 方式2: 从 日志框架 MDC 中获取 -->
<!--<Property name="log.appender.kafka.producer.bootstrap.servers" value="%X{log.appender.kafka.producer.bootstrap.servers}"/>-->
<!-- 方式3: 从 应用程序的 main 方法启动入参中获取 -->
<Property name="log.appender.kafka.producer.bootstrap.servers" value="${main:\\-\-log\.appender\.kafka\.producer\.bootstrap\.servers}"/> <!-- 目标 Appenders | 注: 属性值(如: ConsoleAppender),对应的是 <Appender> 标签的 `name` 属性值 -->
<!-- 1. 标准输出/控制台的 Appender -->
<Property name="log.consoleAppender" value="MyConsoleAppender"/>
<!-- 2. 文件输出 系统类日志的 Appender -->
<Property name="log.systemFileAppender" value="MySystemFileAppender"/>
<!-- 3. 文件输出 访问类日志的 Appender -->
<!--<Property name="log.accessFileAppender">MyAccessFileAppender</Property>-->
<!-- 4. 文件输出 操作类日志的 Appender -->
<!--<Property name="log.operationFileAppender">MyOperationFileAppender</Property>-->
<!-- 5. 文件输出 协议类日志的 Appender -->
<!--<Property name="log.protocolFileAppender">MyProtocolFileAppender</Property>-->
<!-- 6. 远程 链路追踪系统的 Appender -->
<!--<Property name="log.linkTraceClientTargetAppender">MySkyWalkingClientAppender</Property>-->
<!-- 7. 远程 KAFKA/ELK 的 Appender -->
<Property name="log.loggingSystemMessageQueueAppender" value="MyKafkaAppender"/>
</Properties> <!-- 输出器 -->
<Appenders>
<Console name="MyConsoleAppender" target="SYSTEM_OUT">
<PatternLayout pattern="${log.layout.consolePattern}" />
<!-- com.platform.sp.framework.log.layout.CustomPatternLayout -->
<!--<CustomPatternLayout pattern="${log.layout.consolePattern}" />-->
</Console> <!--
@warn
1. 此 KafkaAppender 不建议在 生产环境 的 log4j2.xml/properties 中启用,因 无法从外部动态注入 kafka broker servers
2. 针对第1点,需通过 自定义的 {@link org.example.app.hooks.startup.impl.Log4j2KafkaAppenderInitializer } ,实现程序启动时动态注册 KafkaAppender
@Appender : KafkaAppender | org.apache.logging.log4j.core.appender.mom.kafka.KafkaAppender | log4j-core:2.20.0
@note
1. 计划在下一个主要版本中删除此附加程序!如果您正在使用此库,请使用官方支持渠道 与 Log4j 维护人员联系。
from https://logging.apache.org/log4j/2.x/manual/appenders/message-queue.html#KafkaAppender
2. 使用 Kafka Appender 需要额外的运行时依赖项 : org.apache.kafka:kafka-clients:{version}
from https://logging.apache.org/log4j/2.x/manual/appenders/message-queue.html#KafkaAppender
3. Kafka appender ignoreExceptions 必须设置为false,否则无法触发 FailOver Appender
4. 确保不要让 `org.apache.kafka`Logger 日志记录的日志级别为 DEBUG,因为这将导致`KafkaAppender`递归日志记录
from https://logging.apache.org/log4j/2.x/manual/appenders/message-queue.html#KafkaAppender
@property
//配置属性
* name: Log Framework's Appender 's Name
* topic : Kafka Topic Name key:String : Kafka Message(`ProducerRecord`) 的 key。 支持 运行时属性替换,并在全局上下文 中进行评估。
参考: https://logging.apache.org/log4j/2.x/manual/appenders/message-queue.html#KafkaAppender
参考 : https://logging.apache.org/log4j/2.x/manual/lookups.html#global-context
推荐值: key="$${web:contextName}" | contextName 是 log4j2 内置的变量
ignoreExceptions:boolean[DefaultValue:true] : 如果false,日志记录异常将被转发给日志记录语句的调用者。否则,它们将被忽略。
syncSend:boolean[DefaultValue:true] : 如果true,附加器将阻塞,直到 Kafka 服务器确认该记录为止。否则,附加器将立即返回,从而实现更低的延迟和更高的吞吐量。 //嵌套属性
Filter
Layout
Property[0..n] : 这些属性会直接转发给 Kafka 生产者。 有关更多详细信息,请参阅 Kafka 生产者属性。
参考: https://kafka.apache.org/documentation.html#producerconfigs
bootstrap.servers : 此属性是必需的
key.serializer : 不应使用这些属性
value.serializer : 不应使用这些属性
-->
<Kafka name="MyKafkaAppender" topic="flink_monitor_log" key="$${web:contextName}" syncSend="true" ignoreExceptions="false">
<!--<JsonTemplateLayout/>-->
<PatternLayout pattern="${log.layout.mainPattern}"/>
<Property name="bootstrap.servers" value="${log.appender.kafka.producer.bootstrap.servers}"/>
<Property name="max.block.ms">2000</Property>
</Kafka> <RollingFile name="MyFailoverKafkaLogAppender" fileName="../log/failover/request.log"
filePattern="../log/failover/request.%d{yyyy-MM-dd}.log">
<ThresholdFilter level="INFO" onMatch="ACCEPT" onMismatch="DENY"/>
<PatternLayout>
<Pattern>${log.layout.mainPattern}</Pattern>
</PatternLayout>
<Policies>
<TimeBasedTriggeringPolicy />
</Policies>
</RollingFile> <!--
<Failover name="Failover" primary="kafkaLog" retryIntervalSeconds="600">
<Failovers>
<AppenderRef ref="MyFailoverKafkaLogAppender"/>
</Failovers>
</Failover>
--> <!--
异步输出 | org.apache.logging.log4j.core.async.AsyncLoggerConfig
1. AsyncAppender接受对其他Appender的引用,并使LogEvents在单独的Thread上写入它们。
2. 默认情况下,AsyncAppender使用 java.util.concurrent.ArrayBlockingQueue ,它不需要任何外部库。
请注意,多线程应用程序在使用此appender时应小心:阻塞队列容易受到锁争用的影响,并且我们的 测试 表明,当更多线程同时记录时性能可能会变差。
考虑使用无锁异步记录器以获得最佳性能。
-->
<!-- <AsyncLogger name="kafkaAyncLogger" level="INFO" additivity="false">
<appender-ref ref="Failover"/>
</AsyncLogger>-->
</Appenders> <!-- 日志器-->
<Loggers>
<!-- 定义 RootLogger 等 全局性配置(不可随意修改) -->
<!-- rootLogger, 根记录器,所有记录器的父辈 | 指定根日志的级别 | All < Trace < Debug < Info < Warn < Error < Fatal < OFF -->
<Root level="${log.level}"> <!-- ${log.level} -->
<!-- 2.17.2 版本以下通过这种方式将 root 和 Appender关联起来 / 2.17.2 版本以上有更简便的写法 --> <!-- rootLogger.appenderRef.stdout.ref=${log.consoleAppender} -->
<AppenderRef ref="${log.consoleAppender}" level="INFO" /> <!-- rootLogger.appenderRef.kafka.ref=${log.loggingSystemMessageQueueAppender} -->
<!-- <AppenderRef ref="${log.loggingSystemMessageQueueAppender}"/> --><!-- MyKafkaAppender -->
</Root> <!-- 指定个别 Class 的 Logger (可随意修改,建议在 nacos 上修改) --> <!-- KafkaAppender | org.apache.logging.log4j.core.appender.mom.kafka.KafkaAppender
1. 确保不要让 org.apache.kafka Logger 的日志级别为 DEBUG,因为这将导致递归日志记录
2. 请记住将配置 additivity 属性设置为false
-->
<Logger name="org.apache.kafka" level="WARN" additivity="false">
<AppenderRef ref="${log.consoleAppender}"/>
</Logger> </Loggers>
</Configuration>

Log4j2KafkaAppenderInitializer implements ApplicationStartupHook : 负责实现具体的启动钩子

package org.example.app.hooks.startup.impl;

import lombok.Getter;
import lombok.extern.slf4j.Slf4j;
import org.apache.kafka.clients.producer.ProducerConfig;
import org.apache.logging.log4j.Level;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.core.Appender;
import org.apache.logging.log4j.core.Filter;
import org.apache.logging.log4j.core.LoggerContext;
import org.apache.logging.log4j.core.appender.mom.kafka.KafkaAppender;
import org.apache.logging.log4j.core.config.Configuration;
import org.apache.logging.log4j.core.config.Property;
import org.apache.logging.log4j.core.layout.PatternLayout;
import org.example.app.constant.Constants;
import org.example.app.hooks.startup.ApplicationStartupHook;
import org.slf4j.MDC; import java.nio.charset.Charset;
import java.util.Collections;
import java.util.Map;
import java.util.Optional;
import java.util.Properties; /**
* @description 基于 log4j2 日志框架,在程序启动时,根据程序的启动参数(kafka brokers地址)动态追加 KafkaAppender
* @refrence-doc
* [1] Log4j2 配置日志记录发送到 kafka 中 - CSDN | https://blog.csdn.net/u010454030/article/details/132589450 【推荐】
* [2] 使用代码动态进行 Log4j2 的日志配置 - CSDN | https://blog.csdn.net/scruffybear/article/details/130230414 【推荐】
* [3] Apache Log4j2.x - Kafka Appender 【推荐】
* https://logging.apache.org/log4j/2.x/manual/appenders.html#KafkaAppender
* https://logging.apache.org/log4j/2.x/manual/appenders/message-queue.html#KafkaAppender
* {@link org.apache.logging.log4j.core.appender.mom.kafka.KafkaAppender }
* -----
* [4] Log4j2 - 动态生成Appender - 博客园 | https://www.cnblogs.com/yulinlewis/p/10217385.html
* [5] springboot动态添加log4j2的Appender - CSDN | https://blog.csdn.net/qq_25379811/article/details/127620062
* [6] log4j2.xml中动态读取配置 - CSDN | https://blog.csdn.net/xiaokanfuchen86/article/details/126695010 【推荐】
* https://logging.apache.org/log4j/2.x/manual/lookups.html#global-context 【推荐】
* @gpt-promt
*/
@Slf4j
public class Log4j2KafkaAppenderInitializer implements ApplicationStartupHook { @Getter
private Properties applicationProperties; public Log4j2KafkaAppenderInitializer(Properties applicationProperties) {
this.applicationProperties = applicationProperties;
} @Override
public void execute() throws Exception {
log.debug("Initializing {} ...", this.getClass().getCanonicalName()); LoggerContext ctx = (LoggerContext) LogManager.getContext(false);
Configuration config = ctx.getConfiguration();
Appender kafkaAppender = createKafkaAppender(ctx, config, applicationProperties);
kafkaAppender.start();//防止错误: Attempted to append to non-started appender testName Level level = getLevel(applicationProperties);
config.getRootLogger().addAppender(kafkaAppender, level, null);// 添加 Appender 到配置中
ctx.updateLoggers(); log.debug("Initialized {} ...", this.getClass().getCanonicalName());
} /**
* @note
* 1. required properties:
* 1. {@link Constants.Log4j2KafkaAppender#LEVEL_PARAM}
* @param applicationProperties
* @return
*/
private static Level getLevel(Properties applicationProperties) {
Level level = null;
String levelStr = applicationProperties == null ? Constants.Log4j2KafkaAppender.LEVEL_DEFAULT : applicationProperties.getProperty( Constants.Log4j2KafkaAppender.LEVEL_PARAM );
levelStr = (levelStr == null || levelStr.equals("") ) ? Constants.Log4j2KafkaAppender.LEVEL_DEFAULT : levelStr.toUpperCase();
level = Level.getLevel(levelStr);
log.info("user config's `{}`'s log level: {}", KafkaAppender.class.getCanonicalName(), levelStr);
return level;
} /**
* create a kafka appender base on log4j2 framework
* @reference-doc
* 1. https://logging.apache.org/log4j/2.x/manual/appenders/message-queue.html#KafkaAppender
* @note
* 1. required properties:
* 1. {@link Constants.Log4j2KafkaAppender#KAFKA_PRODUCER_TOPIC_PARAM}
* 2. {@link ProducerConfig#BOOTSTRAP_SERVERS_CONFIG }
* 2. optional properties:
* {@link ProducerConfig } 's Config Properties
* @return
*/
private static Appender createKafkaAppender(LoggerContext loggerContext,Configuration configuration, Properties applicationProperties) {
KafkaAppender kafkaAppender = null;
if(loggerContext == null){
loggerContext = (LoggerContext) LogManager.getContext(false);
}
if(configuration == null){
configuration = loggerContext.getConfiguration();
} final PatternLayout layout = PatternLayout.newBuilder()
.withCharset(Charset.forName("UTF-8"))
.withConfiguration(configuration)
.withPattern("%d %p %c{1.} [%t] %m%n").build(); Filter filter = null; String topic = applicationProperties.getProperty(Constants.Log4j2KafkaAppender.KAFKA_PRODUCER_TOPIC_PARAM);
String appenderName = applicationProperties.getProperty(Constants.Log4j2KafkaAppender.KAFKA_PRODUCER_TOPIC_PARAM) + "Log4J2KafkaAppender"; Property [] propertyArray = propertiesToPropertyArray(applicationProperties); String messageKey = applicationProperties.getProperty( Constants.Log4j2KafkaAppender.KAFKA_PRODUCER_KEY_PARAM );
Boolean isIgnoreExceptions = Boolean.getBoolean(applicationProperties.getProperty(Constants.Log4j2KafkaAppender.KAFKA_APPENDER_IGNORE_EXCEPTIONS_PARAM, Constants.Log4j2KafkaAppender.KAFKA_APPENDER_IGNORE_EXCEPTIONS_DEFAULT));
Boolean syncSend = Boolean.getBoolean(applicationProperties.getProperty(Constants.Log4j2KafkaAppender.KAFKA_PRODUCER_SYNC_SEND_PARAM, Constants.Log4j2KafkaAppender.KAFKA_PRODUCER_SYNC_SEND_DEFAULT));
Boolean sendEventTimestamp = Boolean.getBoolean(applicationProperties.getProperty(Constants.Log4j2KafkaAppender.KAFKA_APPENDER_SEND_EVENT_TIMESTAMP_PARAM, Constants.Log4j2KafkaAppender.KAFKA_APPENDER_SEND_EVENT_TIMESTAMP_DEFAULT)); //kafkaAppender = KafkaAppender.createAppender(layout, filter, appenderName, isIgnoreExceptions, topic, propertyArray, configuration, key );//此方式不支持传入 syncSend 参数
//kafkaAppender = new KafkaAppender(name, layout, filter, isIgnoreExceptions, kafkaManager, getPropertyArray(), getRetryCount());//此方式,因构造器是 private,不支持 kafkaAppender = KafkaAppender.newBuilder()//此方式 √
.setName(appenderName)
.setConfiguration(configuration)
.setPropertyArray(propertyArray)
.setFilter(filter)
.setLayout(layout)
.setIgnoreExceptions(isIgnoreExceptions)
.setTopic(topic)
.setKey(messageKey)
.setSendEventTimestamp(sendEventTimestamp)
.setSyncSend(syncSend)
.setRetryCount(3)
.build(); return kafkaAppender; // 需要替换为实际的 Appender 创建代码
} /**
* Java Properties 转 Log4j2 的 Property []
* @return
*/
public static Property [] propertiesToPropertyArray(Properties properties){
if(properties == null){
return new Property[] {};
}
Property [] propertyArray = new Property[ properties.size() + 1];
int i = 0;
for(Map.Entry<Object, Object> entry : properties.entrySet() ) {
Property property = Property.createProperty((String) entry.getKey(), (String) entry.getValue());
propertyArray[i] = property;
i++;
} /**
* 注入 org.apache.logging.log4j.core.appender.mom.kafka.KafkaAppender 所需的必填参数 {@link ProducerConfig.BOOTSTRAP_SERVERS_CONFIG}
*/
String kafkaBrokerServers = properties.getProperty(Constants.Log4j2KafkaAppender.KAFKA_PRODUCER_BOOTSTRAP_SERVERS_PARAM);
if(kafkaBrokerServers != null && (!kafkaBrokerServers.trim().equals("")) ){
propertyArray[i] = Property.createProperty( ProducerConfig.BOOTSTRAP_SERVERS_CONFIG, kafkaBrokerServers);
} else {
throw new RuntimeException(
String.format("The Property `%s` must be not empty for `%s`!"
, Constants.Log4j2KafkaAppender.KAFKA_PRODUCER_BOOTSTRAP_SERVERS_PARAM
, KafkaAppender.class.getCanonicalName()
)
);
}
return propertyArray;
}
}

Slf4Initializer implements ApplicationStartupHook : 负责具体实现的启动钩子

package org.example.app.hooks.startup.impl;

import lombok.Getter;
import lombok.extern.slf4j.Slf4j;
import org.example.app.constant.Constants;
import org.example.app.hooks.startup.ApplicationStartupHook;
import org.slf4j.MDC; import java.util.Optional;
import java.util.Properties; @Slf4j
public class Slf4Initializer implements ApplicationStartupHook {
@Getter
private Properties applicationProperties; public Slf4Initializer(Properties applicationProperties) {
this.applicationProperties = applicationProperties;
} @Override
public void execute() throws Exception {
log.debug("Initializing {} ...", this.getClass().getCanonicalName()); //设置 kafka 主机地址
String kafkaProducerBootstrapServers = applicationProperties.getProperty(Constants.Log4j2KafkaAppender.KAFKA_PRODUCER_BOOTSTRAP_SERVERS_PARAM);
kafkaProducerBootstrapServers = Optional.ofNullable(kafkaProducerBootstrapServers).<RuntimeException>orElseThrow(() -> {
throw new RuntimeException(String.format("`{}` must be not empty!", Constants.Log4j2KafkaAppender.KAFKA_PRODUCER_BOOTSTRAP_SERVERS_PARAM));
});
MDC.put(Constants.Log4j2KafkaAppender.KAFKA_PRODUCER_BOOTSTRAP_SERVERS_PARAM, kafkaProducerBootstrapServers);
log.info("MDC | {} : {}", Constants.Log4j2KafkaAppender.KAFKA_PRODUCER_BOOTSTRAP_SERVERS_PARAM, MDC.get(Constants.Log4j2KafkaAppender.KAFKA_PRODUCER_BOOTSTRAP_SERVERS_PARAM) ); log.debug("Initialized {} ...", this.getClass().getCanonicalName());
}
}

Slf4Finalizer implements ApplicationShutdownHook : 负责具体实现的钩子

package org.example.app.hooks.shutdown.impl;

import lombok.Getter;
import lombok.extern.slf4j.Slf4j;
import org.example.app.constant.Constants;
import org.example.app.hooks.shutdown.ApplicationShutdownHook;
import org.slf4j.MDC; import java.util.Properties; @Slf4j
public class Slf4Finalizer implements ApplicationShutdownHook {
@Getter
private Properties applicationProperties; public Slf4Finalizer(Properties applicationProperties) {
this.applicationProperties = applicationProperties;
} @Override
public void execute() throws Exception {
log.debug("Finalizing {} ...", Slf4Finalizer.class.getCanonicalName()); // 清理MDC
log.info("clear MDC before | {} : {}", Constants.Log4j2KafkaAppender.KAFKA_PRODUCER_BOOTSTRAP_SERVERS_PARAM, MDC.get(Constants.Log4j2KafkaAppender.KAFKA_PRODUCER_BOOTSTRAP_SERVERS_PARAM) );
//MDC.clear();
MDC.remove( Constants.Log4j2KafkaAppender.KAFKA_PRODUCER_BOOTSTRAP_SERVERS_PARAM );//或仅清理需要的属性
log.info("clear MDC after | {} : {}", Constants.Log4j2KafkaAppender.KAFKA_PRODUCER_BOOTSTRAP_SERVERS_PARAM, MDC.get(Constants.Log4j2KafkaAppender.KAFKA_PRODUCER_BOOTSTRAP_SERVERS_PARAM) ); log.debug("Finalized {} ...", Slf4Finalizer.class.getCanonicalName());
}
}

Log4jKafkaAppenderDemoEntry

package org.example.app;

import org.apache.logging.log4j.core.layout.PatternLayout;
import org.example.app.constant.Constants;
import org.example.app.hooks.shutdown.ApplicationShutdownHookManager;
import org.example.app.hooks.shutdown.impl.Slf4Finalizer;
import org.example.app.hooks.startup.ApplicationStartupHookManager;
import org.example.app.hooks.startup.impl.Log4j2KafkaAppenderInitializer;
import org.example.app.hooks.startup.impl.Slf4Initializer;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory; import java.util.Properties; public class Log4jKafkaAppenderDemoEntry {
private static final Logger logger = LoggerFactory.getLogger(Log4jKafkaAppenderDemoEntry.class); private static final String APPLICATION_NAME = "Log4jKafkaAppenderDemoApplication"; public static void main(String[] args) throws Exception {
//从 nacos 等处动态获取配置 (此处可视为在模拟)
Properties applicationProperties = new Properties();
applicationProperties.put(Constants.Log4j2KafkaAppender.LEVEL_PARAM, "WARN");
applicationProperties.put(Constants.Log4j2KafkaAppender.KAFKA_PRODUCER_BOOTSTRAP_SERVERS_PARAM, "127.0.0.1:9092");
applicationProperties.put(Constants.Log4j2KafkaAppender.KAFKA_PRODUCER_TOPIC_PARAM, "flink_monitor_log");
applicationProperties.put(Constants.Log4j2KafkaAppender.KAFKA_PRODUCER_KEY_PARAM, APPLICATION_NAME);
applicationProperties.put(Constants.Log4j2KafkaAppender.KAFKA_PRODUCER_SYNC_SEND_PARAM, "true");
applicationProperties.put(Constants.Log4j2KafkaAppender.KAFKA_APPENDER_IGNORE_EXCEPTIONS_PARAM, "false"); enableLog4j2MainLookup(args);//可选步骤(非必须) runStartupHooks(applicationProperties);//运行启动钩子
// 测试不同级别的日志
logger.info("这是一条信息日志");
logger.warn("这是一条警告日志");
try {
throw new RuntimeException("测试异常");
} catch (Exception e) {
logger.error("发生错误", e);
} //关停钩子
runShutdownHooks(applicationProperties);
} public static void enableLog4j2MainLookup(String [] args){
/**
* 若 log4j2.[xml/properties/yaml] 中 Appender 的 pattern 欲以 `${main:\\-logLevel}` ,则需启用如下代码
*/
try {
Class.forName("org.apache.logging.log4j.core.lookup.MainMapLookup")
.getDeclaredMethod("setMainArguments", String[].class)
.invoke(null, (Object) args);
} catch (final ReflectiveOperationException e) {
// Log4j Core is not used.
}
} public static void runStartupHooks(Properties applicationProperties) throws Exception {
ApplicationStartupHookManager.registerHook( new Slf4Initializer(applicationProperties) );
ApplicationStartupHookManager.registerHook( new Log4j2KafkaAppenderInitializer(applicationProperties) );
ApplicationStartupHookManager.run();
} public static void runShutdownHooks(Properties applicationProperties) throws Exception {
ApplicationShutdownHookManager.registerHook( new Slf4Finalizer(applicationProperties) );
ApplicationShutdownHookManager.run();
}
}

X 参考文献

[Jaav SE/程序生命周期] 优雅的Java应用程序的启停钩子框架的更多相关文章

  1. IOS应用程序生命周期&启动周期函数

    —程序的生命周期         a.程序的生命周期是指应用程序启动到应用程序结束整个阶段的全过程         b.每一个IOS应用程序都包含一个UIApplication对象,IOS系统通过该U ...

  2. 微信小程序生命周期——小程序的生命周期及页面的生命周期。

    最近在做微信小程序开发,也发现一些坑,分享一下自己踩过的坑. 生命周期是指一个小程序从创建到销毁的一系列过程. 在小程序中 ,通过App()来注册一个小程序 ,通过Page()来注册一个页面. 首先来 ...

  3. 4-K8S 部署Java应用及应用程序生命周期管理

    1.在kubernetes中部署应用程序流程 准备项目源码-->编译构建-->产出war包,打包到镜像中-->推送到镜像仓库 获取源代码是开发人员提交代码的代码托管地址,有Git.S ...

  4. .net学习笔记----Asp.net的生命周期之一应用程序生命周期

    Http请求刚刚到达服务器的时候 当服务器接收到一个 Http请求的时候,IIS (Internet Information Services,互联网信息服务)首先需要决定如何去处理这个请求. 什么是 ...

  5. Asp.net的生命周期之应用程序生命周期

    参考:http://msdn.microsoft.com/zh-cn/library/ms178473(v=vs.100).aspx 参考:http://www.cnblogs.com/JimmyZh ...

  6. C# MVC 5 - 生命周期(应用程序生命周期&请求生命周期)

    本文是根据网上的文章总结的. 1.介绍 本文讨论ASP.Net MVC框架MVC的请求生命周期. MVC有两个生命周期,一为应用程序生命周期,二为请求生命周期. 2.应用程序生命周期 应用程序生命周期 ...

  7. [转]ASP.NET应用程序生命周期趣谈(五) IIS7瞎说

    Ps:建议初学者在阅读本文之前,先简要了解一下之前的几篇文章,以便于熟悉本文提到的一些关于IIS6的内容,方便理解.仅供参考. PS:为什么叫瞎说呢?我觉得自己理解的并不到位,只能是作为一个传声筒,希 ...

  8. [转]ASP.NET应用程序生命周期趣谈(三) HttpModule

    在之前的文章中,我们提到过P_Module(HttpModule)这个能干的程序员哥们儿,它通过在项目经理HttpApplication那里得到的授权,插手整个应用程序级别的事件处理.所有的HttpM ...

  9. [转]ASP.NET应用程序生命周期趣谈(一)

    这几天一直在看ASP.NET应用程序生命周期,真是太难了,我理解起来费了劲了,但偏偏它又是那么重要,所以我希望能给大家带来一篇容易理解又好用的文章来帮助学习ASP.NET应用程序生命周期.这篇就是了. ...

  10. 图解ios程序生命周期

    图解ios程序生命周期 应用程序启动后状态有Active.Inactive.Background.Suspended.Not running这5种状态,几种状态的转换见下图: 在AppDelegate ...

随机推荐

  1. ARCGIS 拓扑检查步骤与修正拓扑错误技巧

    转自http://xygszsh.blog.163.com/blog/static/19221517201111831348533/,写得没有说多好,贵在详细. 将数据装载如个人地理数据库,用拓扑功能 ...

  2. Vue3 面试题 (2023-09-26更新)

    Vue3 对比 Vue2 做了那些改进? 1. 响应式系统 vue2 中使用的 Object.defineProperty 实现的响应式,劫持整个对象,递归遍历所有属性,给每个属性添加 getter ...

  3. JS 面试题(2023-09-20更新)

    因JS代码实现面试题较多,移至另外一篇文章:JS面试题-代码实现 基础 JavaScript 是什么? JavaScript 是一种属于网络的脚本语言,被广泛应用于 Web 应用开发 JavaScri ...

  4. 中电金信:GienTech动态|中标、入选、参会...近期精彩呈现!

    中电金信参编业内首个银行核心系统分级度量标准 2024年6月6日,由中国信息通信研究院云计算与大数据研究所主办的"应用现代化赋能银行核心系统升级"交流会议在京召开.会议发布了业内首 ...

  5. 程序员出海做 AI 工具:如何用 similarweb 找到最佳流量渠道?

    如题,今天给大家带来实操的一个小教程.这里先抛出个问题:"做海外流量增长,如何为产品制定营销渠道?" 分享一个方法只需要 3 步,方法如下: 找到和你产品最接近的细分 Top 竞争 ...

  6. Linux新用户登录时出现“-bash-4.2$”的解决办法

    Linux服务器新建的用户在登录时显示"-bash-4.2$",而不是"user@hostname"的显示方式,出现此问题的原因是在添加普通用户时,用户家目录下 ...

  7. mysql转换类型、类型转换、查询结果类型转换

    一.问题来源 数据库一张表的主键id设为了自增,那就是int型的,但是其他表的关联字段又设置成了字符串! 而且已经开发了很久才发现问题,既然出现了问题肯定需要解决 如图 很明显id是不一样的,花了点时 ...

  8. 创建没有构造函数的NumberFormat

    我有以下课程: import java.text.NumberFormat; public static class NF { public static NumberFormat formatSha ...

  9. getway网关跨域问题记录

    一.问题产生环境 1.1 为什么会产生跨域问题? 跨域不一定都会有跨域题. 因为跨域问题是浏览器对于ajax请求的一种安全限制: 一个页面发起的 ajax请求,只能是与当前页域名相同的路径,这能有效的 ...

  10. RxSqlUtils(base R2dbc)

    一.前言 随着 Solon 3.0 和 Solon-Rx 3.0 发布,又迎来了的 RxSqlUtils 扩展插件,用于"响应式"操作数据库.RxSqlUtils 是基于 R2db ...