使用Log4j2代码方式配置实现线程级动态控制

目录
  • 一 需求
  • 二 对外暴露的接口
  • 三 代码方式配置Log4j2日志对象
  • 四 线程级日志对象的设计
  • 五 标准日志头
  • 六 异常日志的堆栈信息打印
  • 七 测试

一 需求

最近平台进行升级,要求日志工具从Log4j升级到Log4j2,以求性能上的提升。之前我写过以代码方式的配置Log4j,来实现线程级日志对象的管理,今天把版本升级到Log4j2,依然采用原有思路来做,但是实现上有诸多区别,这是因为Log4j2的实现较老版本改动太多。

至于以配置文件方式配置的方法,我不做介绍,对于Log4j2的详细实现亦然,这些部分有兴趣的朋友可以自己网络搜索,也可以自行跟踪源码查阅。

主要需求为每个线程单独一个日志对象处理,可动态控制日志输出级别等参数,可控制的同步及异步输出模式,标准的日志头格式等。

大体思路同我之前写的Log4j配置,只不过日志对象的创建管理等功能实现上略有区别。

二 对外暴露的接口

设计的重中之重,这部分一旦设计完成便不可轻易改动,后续的维护只能尽其可能的向前兼容。

首先我们需要提供对外可用的预设参数值,包括日志输出等级、日志的输出目标(文件、控制台、网络及数据库等)等,这些值设定我们均以static final来修饰。

第二部分是全局参数的设置,这些参数不随日志对象的状态而发生变动,反而是作为日志对象构造的属性值来源,比如说日志文件的路径、日志文件的最大容量、日志文件的备份数量,以及每个线程的日志对象初始的日志输出等级等等。这部分参数可以从外部配置读取,当然也需要有默认值设定,属全局参数配置,以static修饰。

第三部分为日志输出的接口定义,这部分可有可无,但是对于大型项目来说极为重要,以规范的接口进行日志输出,可以为日志的采集、分析带来巨大便利,如通讯日志、异常日志等内容的格式化输出。

所以,按总体思路预先定义日志工具的外对暴露接口,如下:

public class LogUtil
{
	public static final int LevelTrace = 0;
	public static final int LevelDebug = 1;
	public static final int LevelInfo = 2;
	public static final int LevelWarn = 3;
	public static final int LevelError = 4;
	public static final String[] LevelName = new String[]
	{ "TRACE", "DEBUG", "INFO", "WARN", "ERROR" };
	public static final String TypeCommunication = "comm";
	public static final String TypeProcess = "proc";
	public static final String TypeException = "exce";
	public static final int AppenderConsole = 1;
	public static final int AppenderFile = 2;
	private static int DefaultLogLevel = LevelDebug;
	private static String FilePath = null;
	private static String FileSize = "100MB";
	private static int BackupIndex = -1;
	private static int BufferSize = 0;
	private static String LinkChar = "-";
	private static int LogAppender = AppenderFile;
	public static void log(int logLevel, String logType, String logText)
	{
		getThreadLogger().log(logLevel, logType, logText);
	}
	public static void logCommunication()
	{
		// TODO
	}

	public static void logException()
	{
		// TODO
	}
	……
}

这里我暂定了一个公用的日志输出接口名为log(),参数列表为日志的输出等级、日志的类型以及日志内容。而通讯日志的输出接口为logCommunication(),其参数列表暂时空着,按各位读者的需求自行填写,异常日志的输出亦然。后文我会在测试部分对其填写。

为了描述方便,我仅定义了两个日志输出目标,一为控制台,二为文件。

三 代码方式配置Log4j2日志对象

接下来是重头戏,如果不采取配置文件的方式进行Log4j2的配置,那么Log4j2会自行采用默认的配置,这不是我们想要的。虽然这个过程我们无法选择,也规避不了,但是我们最后都是使用Logger对象来进行日志输出的,所以我们只需要按需构造Logger对象,并将其管理起来即可。

这里提供一个思路,首先LogUtil维护一组线程级日志对象,然后这一组线程级日志对象共同访问同一组Logger对象。

跟踪源码我发现Log4j2对Logger对象的构造还是较为复杂的,使用了大量的Builder,其实较为早期的版本中也提供了构造函数方式来初始化对象,但是后期的版本却都被标记了@depreciation。对于Builder模式大家自己查阅其他信息了解吧。

好消息是Log4j2的核心组件依然是Appender、Logger,只不过提供了更多的可配置内容,包括日志同时按日期和文件大小进行备份,这才是我想要的,之前写Log4j的时候我可是自己派生了一个Appender类才实现的同时备份。

向控制台输出的Appender具体类型为ConsoleAppender,我使用默认的构造函数来处理,这是唯一一个被公开出来,且没有被@depreciation修饰的构造函数,想来只要是能通过默认配置实现的都是被Log4j2认可的吧,不然为啥要弄这么多Builder嘞。

向文件输出的Appender我采用RollingFileAppender,无他,就是为了能够实现同时按日期及文件大小进行备份,而且该Appender可适用全局异步模式,性能较AsyncAppender高了不少。它的创建方式要麻烦许多,因为我需要设置触发器来控制何时触发备份,以及备份策略。

整体设计如下:

private Logger createLogger(String loggerName)
{
		Appender appender = null;
		PatternLayout.Builder layoutBuilder = PatternLayout.newBuilder();
		layoutBuilder.withCharset(Charset.forName(DefaultCharset));
		layoutBuilder.withConfiguration(loggerConfiguration);
		Layout<String> layout = layoutBuilder.build();
		if (LogUtil.AppenderConsole == LogUtil.getAppender())
		{
			appender = ConsoleAppender.createDefaultAppenderForLayout(layout);
		}
		if (LogUtil.AppenderFile == LogUtil.getAppender())
		{
			RollingFileAppender.Builder<?> loggerBuilder = RollingFileAppender.newBuilder();
			if (LogUtil.getBufferSize() > 0)
			{
				loggerBuilder.withImmediateFlush(false);
				loggerBuilder.withBufferedIo(true);
				loggerBuilder.withBufferSize(LogUtil.getBufferSize());
				System.setProperty(AsyncPropKey, AsyncPropVal);
			}
			else
			{
				loggerBuilder.withImmediateFlush(true);
				loggerBuilder.withBufferedIo(false);
				loggerBuilder.withBufferSize(0);
				System.getProperties().remove(AsyncPropKey);
			}
			loggerBuilder.withAppend(true);
			loggerBuilder.withFileName(getFilePath(loggerName));
			loggerBuilder.withFilePattern(spellBackupFileName(loggerName));
			loggerBuilder.withLayout(layout);
			loggerBuilder.withName(loggerName);
			loggerBuilder.withPolicy(CompositeTriggeringPolicy.createPolicy(
					SizeBasedTriggeringPolicy.createPolicy(LogUtil.getFileSize()),
					TimeBasedTriggeringPolicy.createPolicy("1", "true")));
			loggerBuilder.withStrategy(DefaultRolloverStrategy.createStrategy(
					LogUtil.getBackupIndex() > 0 ? String.valueOf(LogUtil.getBackupIndex()) : "-1", "1",
					LogUtil.getBackupIndex() > 0 ? null : "nomax", null, null, true, loggerConfiguration));
			appender = loggerBuilder.build();
		}
		appender.start();
		loggerConfiguration.addAppender(appender);
		AppenderRef appenderRef = AppenderRef.createAppenderRef(loggerName, Level.ALL, null);
		AppenderRef[] appenderRefs = new AppenderRef[]
		{ appenderRef };
		LoggerConfig loggerConfig = LoggerConfig.createLogger(false, Level.ALL, loggerName, "false", appenderRefs, null,
				loggerConfiguration, null);
		loggerConfig.addAppender(appender, null, null);
		loggerConfiguration.addLogger(loggerName, loggerConfig);
		loggerContext.updateLoggers();
		loggerConfiguration.start();
		Logger logger = LogManager.getLogger(loggerName);
		return logger;
}

注意!!我在初始化Logger对象的时候,是根据LogUtil是否开启了异步输出模式来判定是否需要开启全局异步模式的,这里简单说一些Log4j2的异步模式。

Log4j2提供了两种异步模式,第一种是使用AsyncAppender,这种方式跟我以前写的Log4j的异步输出模式一样,都是单独开启一个线程来输出日志。第二种方式是AsychLogger,这个就厉害了,官推,而且官方提供了两种使用模式,一个是混合异步,一个是全局异步,全局异步时不需要对配置文件进行任何改动,尽在应用启动时添加一个jvm参数即可,并且据压测数据显示,全局异步模式下性能飙升。

更多的配置信息建议读者朋友自行查阅官方文档。

最后还有一点需要注意的是,Log4j2的全局异步是要依赖队列缓存的,其实现采用的是disruptor,所以需要依赖这个外部jar,不然在初始化Logger对象的时候,你会看到相关异常。

贴一下依赖的Maven:

<dependency>
	<groupId>org.apache.logging.log4j</groupId>
	<artifactId>log4j-core</artifactId>
	<version>2.8.2</version>
</dependency>
<dependency>
	<groupId>org.apache.logging.log4j</groupId>
	<artifactId>log4j-api</artifactId>
	<version>2.8.2</version>
</dependency>
<dependency>
	<groupId>com.lmax</groupId>
	<artifactId>disruptor</artifactId>
	<version>3.3.6</version>
</dependency>

严重强调:AsyncAppender、AsyncLogger以及全局异步的System.property设置,不要同时出现!!!

最最后,我特么还是要嘴贱啰嗦一下,不见得“异步”就是好的,你想想看异步模式无非是额外线程或者缓存来实现,这些也是要吃资源的,日志量大的场景下其带来的收益很高,但小量日志场景下其对性能资源的消耗很可能大于其带来的性能收益,请酌情使用。

四 线程级日志对象的设计

按上文中的设计思路,LogUtil持有一组线程级日志对象,而这一组日志对象又共享一组Logger对象。延续Log4j版本的设计,线程级日志对象类型依然为ThreadLogger,其基础属性为线程ID、日志类型以及日志的输出级别等。

为了解决多并发问题,使用ConcurrentHashMap来存储Logger对象,其Key值为线程ID,这里需要注意的是ConcurrentHashMap的put操作尽管能保证可见性,但是不能保证操作的原子性,所以在其put操作上需要额外加锁。

class ThreadLogger
{
	private static final String AsyncPropKey = "log4j2.contextSelector";
	private static final String AsyncPropVal = "org.apache.logging.log4j.core.async.AsyncLoggerContextSelector";
	private static final String DefaultCharset = "UTF-8";
	private static final String DefaultDateFormat = "yyyy-MM-dd HH:mm:ss.SSSSSS";
	private static final Object LoggerMapLock = new Object();
	private static final ConcurrentHashMap<String, Logger> LoggerMap = new ConcurrentHashMap<>();
	private static LoggerContext loggerContext = (LoggerContext) LogManager.getContext(false);
	private static Configuration loggerConfiguration = loggerContext.getConfiguration();
	public static void cleanLoggerMap()
	{
		synchronized (LoggerMapLock)
		{
			LoggerMap.clear();
		}
	}

	private Logger getLogger(String loggerName)
	{
		if (StringUtil.isEmpty(loggerName))
			return null;
		Logger logger = LoggerMap.get(loggerName);
		if (logger == null)
		{
			synchronized (LoggerMapLock)
			{
				logger = createLogger(loggerName);
				LoggerMap.put(loggerName, logger);
			}
		}
		return logger;
	}
}

五 标准日志头

头部内容应包含当前的日志输出级别,日志打印所在的类名、行号,当前的线程号、进程号等信息。这里简单介绍下进程号及类名行号的获取。

每个应用进程的进程号唯一,所以仅在第一次获取该信息时获取即可,类名行号则通过方法栈的栈信息获取,如下:

private static String ProcessID = null;
private String logLocation = null;
public static String getProcessID()
{
	if (ProcessID == null)
	{
		ProcessID = ManagementFactory.getRuntimeMXBean().getName().split("@")[0];
	}
	return ProcessID;
}
private static String getLocation()
{
	StackTraceElement[] stackTraceElements = Thread.currentThread().getStackTrace();
	StringBuilder builder = new StringBuilder(stackTraceElements[3].getClassName());
	builder.append(":");
	builder.append(stackTraceElements[3].getLineNumber());
	return builder.toString();
}

这里需要注意,类名行号信息对于每一条日志都不尽相同,所以不能声明为static,并且需要在LogUtil中获取,并通过ThreadLogger的setLocation()方法传入到ThreadLogger对象,最后由ThreadLogger的日志头拼装函数拼接到日志头部信息中:

private String spellLogText(int logLevel, String logText)
{
	StringBuilder builder = new StringBuilder();
	SimpleDateFormat sdf = new SimpleDateFormat(DefaultDateFormat);
	builder.append(sdf.format(Calendar.getInstance().getTime()));
	builder.append("|");
	builder.append(LogUtil.LevelName[logLevel]);
	builder.append("|");
	builder.append(getProcessID());
	builder.append("|");
	builder.append(this.threadID);
	builder.append("|");
	builder.append(this.threadUUID);
	builder.append("|");
	builder.append(this.logLocation);
	builder.append("|");
	builder.append(logText);
	return builder.toString();
}

六 异常日志的堆栈信息打印

需要注意咯,异常的日志输出略微复杂些,这也是我经常被人问起的一个问题,很多从事400或大机开发的同事转入java开发后,最常问的就是异常堆栈的问题,看不懂,不知道怎么来的。

这里我只提一个事情,Exception的构造,其成员有两个,其一为cause,Throwable类型,其二为message,String类型,构造函数提供较多,请大家自己做一个测试,看看不同构造其输出的内容有何不同,cause和message成员又有何关系。

如果你弄明白了Exception的构造,那么下面的逻辑不难理解:

private static String getExceptionStackTrace(Exception e)
{
	StringBuilder builder = new StringBuilder();
	builder.append(e.toString());
	StackTraceElement[] stackTraceElements = e.getStackTrace();
	for (int i = 0; i < stackTraceElements.length; i++)
	{
		builder.append("\r\nat ");
		builder.append(stackTraceElements[i].toString());
	}
	Throwable throwable = e.getCause();
	while (throwable != null)
	{
		builder.append("\r\nCaused by:");
		builder.append(throwable.toString());
		stackTraceElements = throwable.getStackTrace();
		for (int i = 0; i < stackTraceElements.length; i++)
		{
			builder.append("\r\nat ");
			builder.append(stackTraceElements[i].toString());
		}
		throwable = throwable.getCause();
	}
	return builder.toString();
}

七 测试

补充一个逻辑实现,这里我以异常日志的打印作为测试接口,并在多线程并发场景下实现异步输出。

异常日志输出接口补充完整:

public static void logException(String desc, Exception exception)
{
	getThreadLogger().setLogLocation(getLocation());
	StringBuilder builder = new StringBuilder("Description=");
	builder.append(StringUtil.isEmpty(desc) ? "" : desc);
	builder.append(",Exception=");
	builder.append(getExceptionStackTrace(exception));
	log(LevelError, TypeException, builder.toString());
}

测试代码:

public static void main(String[] args)
{
	LogUtil.setAppender(LogUtil.AppenderFile);
	LogUtil.setBufferSize(1024); // 1KB缓存
	LogUtil.setFileSize("1MB");
	LogUtil.setBackupIndex(10);
	LogUtil.setFilePath("C:\\log");
	for (int i = 0; i < 4; i++)
	{
		Thread thread = new Thread(new Runnable()
		{
			@Override
			public void run()
			{
				LogUtil.setModule(Thread.currentThread().getId() + "");
				for (int j = 0; j < 100000; j++)
				{
					LogUtil.logException("test", new Exception("my test"));
				}
			}
		});
		thread.start();
	}
}

测试结果:

以上为个人经验,希望能给大家一个参考,也希望大家多多支持我们。

(0)

相关推荐

  • 深入浅析java web log4j 配置及在web项目中配置Log4j的技巧

    在上篇文章给大家介绍了Java log4j详细教程,本文给大家介绍java web log4j配置及web项目中配置log4j的技巧.具体详情请看下文吧. 首先给大家提供log4j.jar下载:http://logging.apache.org/log4j/1.2/download.html 一.java web项目使用log4j 1.在web.xml文件中添加 <!-- 配置log4j --> <context-param> <param-name>webAppRoo

  • log4j2 自动删除过期日志文件的配置及实现原理

    日志文件自动删除功能必不可少,当然你可以让运维去做这事,只是这不地道.而日志组件是一个必备组件,让其多做一件删除的工作,无可厚非.本文就来探讨下 log4j 的日志文件自动删除实现吧. 0.自动删除配置参考样例: (log4j2.xml) <?xml version="1.0" encoding="UTF-8" ?> <Configuration status="warn" monitorInterval="30&qu

  • java自定义日志输出文件(log4j日志文件输出多个自定义日志文件)

    log4j输出多个自定义日志文件 如果在实际应用中需要输出独立的日志文件,怎样才能把所需的内容从原有日志中分离,形成单独的日志文件呢? 先看一个常见的log4j.properties文件,它是在控制台和test.log文件中记录日志: 复制代码 代码如下: log4j.rootLogger=DEBUG, stdout, logfile log4j.appender.stdout=org.apache.log4j.ConsoleAppender log4j.appender.stdout.layo

  • 使用Log4j2代码方式配置实现线程级动态控制

    目录 一 需求 二 对外暴露的接口 三 代码方式配置Log4j2日志对象 四 线程级日志对象的设计 五 标准日志头 六 异常日志的堆栈信息打印 七 测试 一 需求 最近平台进行升级,要求日志工具从Log4j升级到Log4j2,以求性能上的提升.之前我写过以代码方式的配置Log4j,来实现线程级日志对象的管理,今天把版本升级到Log4j2,依然采用原有思路来做,但是实现上有诸多区别,这是因为Log4j2的实现较老版本改动太多. 至于以配置文件方式配置的方法,我不做介绍,对于Log4j2的详细实现亦

  • Spring装配Bean之用Java代码安装配置bean详解

    前言 本文主要给大家介绍了关于Spring之利用Java代码安装配置bean的相关内容,尽管通过组件扫描和自动装配实现Spring的自动化配置很方便也推荐,但是有时候自动配置的方式实现不了,就需要明确显示的配置Spring.比如说,想要将第三方库中的组件装配到自己的应用中,这样的情况下,是没办法在它的类上添加 @Compnent和 @Autowired注解的. 在这种情况下,需要使用显示装配的方式,可以分别通过Java和XML实现,推荐使用Java的方式,因为更加强大,类型安全并且重构友好,因为

  • Java版超大整数阶乘算法代码详解-10,0000级

    当计算超过20以上的阶乘时,阶乘的结果值往往会很大.一个很小的数字的阶乘结果就可能超过目前个人计算机的整数范围.如果需求很大的阶乘,比如1000以上完全无法用简单的递归方式去解决.在网上我看到很多用C.C++和C#写的一些关于大整数阶乘的算法,其中不乏经典但也有很多粗糙的文章.数组越界,一眼就可以看出程序本身无法运行.转载他人文章的时候,代码倒是仔细看看啊.唉,粗糙.过年了,在家闲来蛋疼,仔细分析分析,用Java实现了一个程序计算超大整数阶乘.思想取自网上,由我个人优化和改进. 这个方法采用"数

  • SpringBoot使用编程方式配置DataSource的方法

    Spring Boot使用固定算法来扫描和配置DataSource.这使我们可以在默认情况下轻松获得完全配置的DataSource实现. Spring Boot还会按顺序快速的自动配置连接池(HikariCP, Apache Tomcat或Commons DBCP),具体取决于路径中的哪些类. 虽然Spring Boot的DataSource自动配置在大多数情况下运行良好,但有时我们需要更高级别的控制,因此我们必须设置自己的DataSource实现,因此忽略自动配置过程. Maven依赖 总体而

  • spring cloud gateway使用 uri: lb://方式配置时,服务名的特殊要求

    在gateway中配置uri配置有三种方式,包括 第一种:ws(websocket)方式: uri: ws://localhost:9000 第二种:http方式: uri: http://localhost:8130/ 第三种:lb(注册中心中服务名字)方式: uri: lb://brilliance-consumer 其中ws和http方式不容易出错,因为http格式比较固定,但是lb方式比较灵活自由.不考虑网关,只考虑服务时,服务名命名时比较自由,都能启动被访问,被注册到注册中心,但是如果

  • Hadoop 使用IntelliJ IDEA 进行远程调试代码的配置方法

    一 .前言 昨天晚上遇到一个奇葩的问题, 搞好的环境DataNode启动报错. 报错信息提示的模棱两可,没办法定位原因. 办法,开启远程调试- 注意 : 开启远程调试的代码,必须与本地idea的代码必须保持一致. 二 .服务器端配置 2.1. 设置启动远程debug端口 修改 服务器上的配置文件 ${HADOOP_HOME}/etc/hadoop/hadoop-env.sh 增加 环境变量即可. 组件 环境变量设置 NameNode export HADOOP_NAMENODE_OPTS="-a

  • Mybatis 中Mapper使用package方式配置报错的解决方案

    踩了个坑,写出来 Mybatis 中Mapper使用package方式配置报错 org.apache.ibatis.binding.BindingException: Invalid bound statement (not found) UserDaoTest中调用了UserDao的insert方法. 1.项目结构如下 2.UserDao接口 package com.mybatis.dao; import org.apache.ibatis.annotations.Mapper; import

  • nginx 负载均衡轮询方式配置详解

    一.概述 Nginx的upstream目前支持的分配算法:1.round-robin 轮询1:1轮流处理请求(默认)每个请求按时间顺序逐一分配到不同的应用服务器,如果应用服务器down掉,自动剔除,剩下的继续轮询.2.weight 权重(加权轮询)通过配置权重,指定轮询几率,权重和访问比率成正比,用于应用服务器性能不均的情况.3.ip_hash 哈希算法每个请求按访问ip的hash结果分配,这样每个访客固定访问一个应用服务器,可以解决session共享的问题.应用服务器如果故障需要手工down掉

  • springboot实现以代码的方式配置sharding-jdbc水平分表

    目录 关于依赖 shardingsphere-jdbc-core-spring-boot-starter shardingsphere-jdbc-core 数据源DataSource 原DataSource ShardingJdbcDataSource 完整的ShardingJdbcDataSource配置 分表策略 主要的类 其他的分表配置类 groovy行表达式说明 properties配置 Sharding-jdbc的坑 结语 多数项目可能是已经运行了一段时间,才开始使用sharding-

  • 关于.prettierrc代码格式化配置方式

    目录 prettierrc的使用 prettierrc规则配置 一些问题 代码的规范规则很多很繁琐,不可能每个都去手动修改,有时候一个页面能有上百个规范问题,那么这时候代码自动格式化就很有用了,最有名的就是prettierrc了. 当然还有其他的比如vue用的vetur.beautify格式化插件等. 格式化插件再配合eslint规范这样写出来的代码又好看效率又高,至于eslintrc的介绍可以点击=>eslintrc介绍及使用学习下,这样一个负责检查,一个负责改,完美! prettierrc的

随机推荐