背景:
客户需要在我们提供的sdk接口服务中增加日志,能够实现配置日志路径,日志级别等实现记录info日志,debug日志,error日志。
预研以及方案选型:
1、使用java自带的jul记录日志
优点:不集成任何日志框架,不存在jar包版本冲突,可支持外部传入日志路径,日志级别等动态修改配置
缺点:功能单一,简单,性能较差,且需要写很多复杂代码
代码示例
package cn.com.infosec.ucypher.log;
import cn.com.infosec.ucypher.agent.sdf.UCypherSDFDev;
import java.io.IOException;
import java.io.PrintWriter;
import java.io.StringWriter;
import java.util.logging.*;
public class JulLogger implements AgentILogger {
private static Logger logger;
private static FileHandler fileHandler;
private static UCypherSDFDev dev;
public JulLogger(Class<?> clazz) {
logger = Logger.getLogger(clazz.getName());
// if(dev != null){
// configureLogger(dev);
// }
}
public void configureLogger(UCypherSDFDev sdfDev) {
dev = sdfDev;
try {
if (fileHandler != null) {
logger.removeHandler(fileHandler);
fileHandler.close();
}
fileHandler = new FileHandler(dev.getProperty(UCypherSDFDev.LOG_PATH), Integer.parseInt(dev.getProperty(UCypherSDFDev.LOG_MAX_SIZE)), Integer.parseInt(dev.getProperty(UCypherSDFDev.LOG_COUNT)), true); // append = true
fileHandler.setFormatter(new MyFormatter());
Level level = null;
switch (dev.getProperty(UCypherSDFDev.LOG_LEVEL)) {
case "1":
level = Level.INFO;
break;
case "2":
level = Level.FINE;
break;
case "3":
level = Level.SEVERE;
break;
}
fileHandler.setLevel(level);
// 设置日志级别
logger.setLevel(level);
// 添加 Handler
logger.addHandler(fileHandler);
// 禁用父 Logger 的 Handler,避免重复输出
logger.setUseParentHandlers(false);
} catch (IOException e) {
System.err.println("Failed to configure FileHandler: " + e.getMessage());
}
}
@Override
public void debug(String message) {
logger.log(Level.FINE, message);
}
@Override
public void info(String message) {
logger.log(Level.INFO, message);
}
@Override
public void error(String message) {
logger.log(Level.SEVERE, message);
}
@Override
public void error(String message, Throwable t) {
// 将堆栈信息转换为字符串
StringWriter sw = new StringWriter();
PrintWriter pw = new PrintWriter(sw);
t.printStackTrace(pw);
String stackTrace = sw.toString();
logger.log(Level.SEVERE, String.format("Exception occurred: %s%n%s", message, stackTrace));
}
}
2、集成logback与slf4j jar
优点:不需要写复杂的代码,现有框架已经成熟,可以直接调用,性能高,并且支持外部传入日志路径,日志级别等动态修改配置
代码示例:
package cn.com.infosec.ucypher.log;
import ch.qos.logback.classic.Level;
import ch.qos.logback.classic.Logger;
import ch.qos.logback.classic.LoggerContext;
import ch.qos.logback.classic.encoder.PatternLayoutEncoder;
import ch.qos.logback.classic.filter.LevelFilter;
import ch.qos.logback.classic.spi.ILoggingEvent;
import ch.qos.logback.core.rolling.RollingFileAppender;
import ch.qos.logback.core.rolling.TimeBasedRollingPolicy;
import ch.qos.logback.core.spi.FilterReply;
import cn.com.infosec.ucypher.agent.sdf.UCypherSDFDev;
public class LogbackLogger implements AgentILogger{
public static org.slf4j.Logger logger;
private static UCypherSDFDev dev;
public LogbackLogger(Class<?> clazz) {
logger = org.slf4j.LoggerFactory.getLogger(clazz.getName());
// if(dev != null){
// configureLogger(dev);
// }
}
public void configureLogger(UCypherSDFDev sdfDev) {
dev = sdfDev;
Level logLevel = null;
String logLevelString = dev.getProperty(UCypherSDFDev.LOG_LEVEL);
if (logLevelString != null) {
switch (logLevelString) {
case "1":
logLevel = Level.INFO;
break;
case "2":
logLevel = Level.DEBUG;
break;
case "3":
logLevel = Level.ERROR;
break;
default:
logLevel = Level.INFO;
System.err.println("Invalid log level in UCypherSDFDev.LOG_LEVEL. Using INFO.");
break;
}
} else {
logLevel = Level.INFO;
System.err.println("UCypherSDFDev.LOG_LEVEL property not found. Using INFO.");
}
LoggerContext loggerContext = (LoggerContext) org.slf4j.LoggerFactory.getILoggerFactory();
loggerContext.reset();
// 创建根Logger并设置全局日志级别(如DEBUG)
Logger rootLogger = loggerContext.getLogger(Logger.ROOT_LOGGER_NAME);
rootLogger.setLevel(logLevel);
// 动态创建不同级别的Appender
createAppenderForLevel(loggerContext, Level.DEBUG, "debug.log");
createAppenderForLevel(loggerContext, Level.INFO, "info.log");
createAppenderForLevel(loggerContext, Level.ERROR, "error.log");
}
// 工具方法:创建指定级别的Appender
private static void createAppenderForLevel(LoggerContext context, Level level, String fileName) {
String logPath = dev.getProperty(UCypherSDFDev.LOG_PATH);
if (logPath == null || logPath.isEmpty()) {
System.err.println("UCypherSDFDev.LOG_PATH property not found or empty. Using current directory.");
logPath = "."; // Use current directory as a fallback
}
// 1. 配置编码器
PatternLayoutEncoder encoder = new PatternLayoutEncoder();
encoder.setContext(context);
encoder.setPattern("%d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n");
encoder.start();
// 2. 创建 RollingFileAppender
RollingFileAppender<ILoggingEvent> rollingFileAppender = new RollingFileAppender<>();
rollingFileAppender.setContext(context);
rollingFileAppender.setFile(logPath + "/" + fileName);
// 3. 配置 Rolling Policy
TimeBasedRollingPolicy<ILoggingEvent> rollingPolicy = new TimeBasedRollingPolicy<>();
rollingPolicy.setContext(context);
rollingPolicy.setFileNamePattern(logPath + "/" + fileName + ".%d{yyyy-MM-dd}.log");
rollingPolicy.setMaxHistory(30);
rollingPolicy.setParent(rollingFileAppender);
rollingPolicy.start();
rollingFileAppender.setRollingPolicy(rollingPolicy);
rollingFileAppender.setEncoder(encoder);
rollingFileAppender.setAppend(true);
// 4. 添加级别过滤器
LevelFilter filter = new LevelFilter();
filter.setLevel(level);
filter.setOnMatch(FilterReply.ACCEPT); // 接受匹配级别的日志
filter.setOnMismatch(FilterReply.DENY); // 拒绝其他级别的日志
filter.start();
rollingFileAppender.addFilter(filter);
rollingFileAppender.start();
// 5. 绑定到根Logger
context.getLogger(Logger.ROOT_LOGGER_NAME).addAppender(rollingFileAppender);
}
@Override
public void trace(String message) {
}
@Override
public void trace(String message, Throwable t) {
}
@Override
public void debug(String message) {
logger.debug(message);
}
@Override
public void info(String message) {
logger.info(message);
}
@Override
public void error(String message) {
}
@Override
public void error(String message, Throwable t) {
logger.error(message,t);
}
}
缺点:需要引入logback-classic.jar logback-core.jar slf4j-api.jar,存在jar包版本冲突
3、只集成slf4j-api jar
优点:无需改动代码,只需引入统一日志规范进行记录日志的操作,但不支持代码动态更改日志级别,日志路径等参数
缺点:需要上层调用者去管控jar版本,如果存在不用slf4j的客户就无法使用
代码示例:
logger = org.slf4j.LoggerFactory.getLogger(clazz.getName());
直接使用slf4j的LoggerFactory.getLogger方法获取到logger对象即可
4、更改logback源码,更改包名,集成
优点:不需要自己手写记录日志代码,性能高
缺点:需要维护logback和slf4j的源码,非常非常麻烦!!!我就是整了好久才跑起来项目,然后改包名,集成调用
5、使用反射机制
使用反射机制,反射到上层应用使用的日志框架,得到哪个就用哪个
优点:sdk无需关注日志框架选型,并且不需要手写记录日志,完全交由上层调用者管控
缺点:存在加载顺序问题,如果上层存在多个日志框架,可能存在先取到哪个就用哪个,需要在上层调用者日志框架中配置我们自定义的日志
代码示例:
package cn.com.infosec.ucypher.log;
import java.lang.reflect.Method;
import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.logging.*;
/**
* @author Hoary (hoary.huang@infosec.com.cn)
* @org �����Ű����ͿƼ��ɷ�����˾
* @date 2024/7/8 17:18
* @since NetSignServer5.7.2.2_build20240426
*/
public class NsLogger {
private static boolean on;
private Object logger;
private Method logMethod;
private Method debugMethod;
private Method infoMethod;
private Method errorMethod;
private static final String INFO = "info";
private static final String DEBUG = "debug";
private static final String ERROR = "error";
private boolean isInfo;
private boolean isError;
private Class clazz;
private NsLogger() {
String level = System.getProperty("log.level");
if(level != null){
if(level.trim().equalsIgnoreCase("info")){
isInfo = true;
} else if (level.trim().equalsIgnoreCase("error")) {
isError = true;
}
}
}
public static NsLogger getLogger(Class clazz){
NsLogger log = new NsLogger();
log.clazz = clazz;
log.initLogger();
return log;
}
private void initLogger(){
if(initSlf4j() || initCommonLog() || initLogBack()|| initLog4j2() || initLog4j() || initJavaLog()){
logMethod = debugMethod;
if(isInfo){
logMethod = infoMethod;
} else if (isError) {
logMethod = errorMethod;
}
}
}
private boolean initSlf4j(){
try {
Class clazzFactory = Class.forName("org.slf4j.LoggerFactory");
Method mGetLogger = clazzFactory.getMethod("getLogger", Class.class);
logger = mGetLogger.invoke(null, clazz);
Class clazzLogger = Class.forName("org.slf4j.Logger");
debugMethod = clazzLogger.getMethod(DEBUG, String.class);
infoMethod = clazzLogger.getMethod(INFO, String.class);
errorMethod = clazzLogger.getMethod(ERROR, String.class);
return true;
}catch (Exception e){ }
return false;
}
private boolean initCommonLog(){
try{
Class clazzFactory = Class.forName("org.apache.commons.logging.LogFactory");
Method mGetLogger = clazzFactory.getMethod("getLog", Class.class);
logger = mGetLogger.invoke(null, clazz);
Class clazzLogger = Class.forName("org.apache.commons.logging.Log");
debugMethod = clazzLogger.getMethod(DEBUG, Object.class);
infoMethod = clazzLogger.getMethod(INFO, Object.class);
errorMethod = clazzLogger.getMethod(ERROR, String.class);
return true;
}catch (Exception e){ }
return false;
}
private boolean initJavaLog(){
// ���û�� slf4j Ҳû�� commons.logging, ����JDK�Դ���
logger = Logger.getLogger(clazz.getName());
Logger logger1 = (Logger) logger;
Handler consoleHandler = new ConsoleHandler();
consoleHandler.setFormatter(new DefaultFormatter());
logger1.addHandler(consoleHandler);
logger1.setUseParentHandlers(false);
try {
debugMethod = Logger.class.getMethod("config", String.class);
infoMethod = Logger.class.getMethod(INFO, String.class);
errorMethod = Logger.class.getMethod(ERROR, String.class);
return true;
} catch (Exception ex) { }
return false;
}
private boolean initLog4j(){
try{
Class clazzLogger = Class.forName("org.apache.log4j.Logger");
Method mGetLogger = clazzLogger.getMethod("getLogger", Class.class);
logger = mGetLogger.invoke(null, clazz);
debugMethod = clazzLogger.getMethod(DEBUG, String.class);
infoMethod = clazzLogger.getMethod(INFO, String.class);
errorMethod = clazzLogger.getMethod(ERROR, String.class);
return true;
}catch (Exception e){ }
return false;
}
private boolean initLog4j2(){
try{
Class clazzFactory = Class.forName("org.apache.logging.log4j.LogManager");
Method mGetLogger = clazzFactory.getMethod("getLogger", Class.class);
logger = mGetLogger.invoke(null, clazz);
Class clazzLogger = Class.forName("org.apache.logging.log4j.Logger");
debugMethod = clazzLogger.getMethod(DEBUG, String.class);
infoMethod = clazzLogger.getMethod(INFO, String.class);
errorMethod = clazzLogger.getMethod(ERROR, String.class);
return true;
}catch (Exception e){ }
return false;
}
private boolean initLogBack(){
try{
Class clazzFactory = Class.forName("ch.qos.logback.classic.LoggerContext");
Class clazzLogger = Class.forName("ch.qos.logback.classic.Logger");
Method mGetLogger = clazzFactory.getMethod("getLogger", Class.class);
logger = mGetLogger.invoke(clazzFactory.getConstructor().newInstance(), clazz);
debugMethod = clazzLogger.getMethod(DEBUG, String.class);
infoMethod = clazzLogger.getMethod(INFO, String.class);
errorMethod = clazzLogger.getMethod(ERROR, String.class);
return true;
}catch (Exception e){
}
return false;
}
class DefaultFormatter extends Formatter{
SimpleDateFormat dateFormat = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss.SSS");
@Override
public String format(LogRecord record) {
String msg = record.getMessage();
StringBuilder builder = new StringBuilder(100 + msg.length());
builder.append("[");
builder.append(dateFormat.format(new Date(record.getMillis())));
builder.append("][");
builder.append(Thread.currentThread().getName()).append("][");
builder.append(record.getLevel().getName());
builder.append("] ").append(msg).append("\n");
return builder.toString();
}
}
public void log(String msg){
if(!on){
return;
}
try {
logMethod.invoke(logger, msg);
} catch (Exception e) {
}
}
public void info(String msg){
// if(!on){
// return;
// }
try {
infoMethod.invoke(logger, msg);
} catch (Exception e) {
}
}
public void debug(String msg){
// if(!on){
// return;
// }
try {
debugMethod.invoke(logger, msg);
} catch (Exception e) {
}
}
public void error(String msg,Throwable t){
// if(!on){
// return;
// }
try {
errorMethod.invoke(logger, msg,t);
} catch (Exception e) {
}
}
public static void openLog(boolean on) {
NsLogger.on = on;
}
}
logback.xm配置如下:
<!-- 从 System Property 中读取 log.level,默认值为 INFO -->
<property name="log.level" value="${log.level:-DEBUG}" />
<logger level="${log.level}" name="cn.com.infosec.ucypher" additivity="false">
<appender-ref ref="app-log" />
</logger>
每种方案都有优缺点, 结合自己项目实际情况选择,我们最终选择使用反射完成,