[Jaav SE/程序生命周期] 优雅的Java应用程序的启停钩子框架
序[*]了解 spring 生态及框架的 java er 都知道,spring 应用的生命周期管理及配套接口较为优雅、可扩展。
[*]但脱离 spring 的 java 应用程序,如何优雅地启停、管理程序的生命周期呢?(以便应用程序在我们需要的运行阶段中进行相应的动作)
概述:Java普通应用程序的启停钩子框架
前置知识
java.lang.FunctionalInterface注解
[*]推荐文献
[*]java.lang.FunctionalInterface : 函数式接口(JDK8+) - Java 注解机制 - 博客园/千千寰宇
Java普通应用程序的启停钩子框架
ApplicationStartupHook: 抽象的启动钩子接口
package org.example.app.hooks.startup;@FunctionalInterfacepublic 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;@FunctionalInterfacepublic 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 --> <!-- 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}] [%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 WARNmain (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| | 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 : 如果false,日志记录异常将被转发给日志记录语句的调用者。否则,它们将被忽略。 syncSend:boolean : 如果true,附加器将阻塞,直到 Kafka 服务器确认该记录为止。否则,附加器将立即返回,从而实现更低的延迟和更高的吞吐量。 //嵌套属性 Filter Layout Property : 这些属性会直接转发给 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 * Log4j2 配置日志记录发送到 kafka 中 - CSDN | https://blog.csdn.net/u010454030/article/details/132589450 【推荐】 * 使用代码动态进行 Log4j2 的日志配置 - CSDN | https://blog.csdn.net/scruffybear/article/details/130230414 【推荐】 * 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 } *----- * Log4j2 - 动态生成Appender - 博客园 | https://www.cnblogs.com/yulinlewis/p/10217385.html * springboot动态添加log4j2的Appender - CSDN | https://blog.csdn.net/qq_25379811/article/details/127620062 * 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 */@Slf4jpublic 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 = 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 = 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;@Slf4jpublic 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;@Slf4jpublic 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. 中 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(); }}框架的软件设计模式分析
策略模式(Strategy Pattern)
[*]推荐文献
[*]设计模式之策略模式【5】 - 博客园/千千寰宇
[*]策略模式允许在运行时选择算法或行为。
在文档中,ApplicationStartupHook 和 ApplicationShutdownHook 接口定义了启动和关闭钩子的行为,而具体的实现类(如 Log4j2KafkaAppenderInitializer 和 Slf4Initializer)则提供了具体的策略。
[*]这种模式允许在【运行时】动态选择和执行不同的钩子逻辑。
接口定义:
public interface ApplicationStartupHook { void execute() throws Exception;}具体实现:
public class Log4j2KafkaAppenderInitializer implements ApplicationStartupHook { @Override public void execute() throws Exception { // 具体逻辑 }}单例模式(Singleton Pattern)
[*]单例模式确保一个类只有一个实例,并提供一个全局访问点。
在文档中,ApplicationStartupHookManager 和 ApplicationShutdownHookManager 类使用了单例模式来管理启动和关闭钩子。
这些管理器类维护了一个静态的钩子列表,并提供统一的注册和执行方法。
[*]单例管理器类:
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; } }}组合模式(Composite Pattern)
[*]组合模式允许将对象组合成树形结构,以表示“部分-整体”的层次结构。在文档中,ApplicationStartupHookManager 和 ApplicationShutdownHookManager 类管理了一个钩子列表,这些钩子可以被视为一个组合结构。
每个钩子可以独立执行,而管理器类则负责统一管理和执行这些钩子。
[*]组合结构
public class ApplicationStartupHookManager { private static final List<ApplicationStartupHook> hooks = new ArrayList<>(); // ...}这些模式共同实现了灵活、可扩展且优雅的启停钩子框架。
X 参考文献
[*]无
页:
[1]