✅ 动态创建: 无需配置文件,通过代码动态创建logback日志对象
✅ Class对象支持: 使用LogUtil.getLogger(MyClass.class)的方式获取日志
✅ 日期格式文件: 自动生成info.%d{yyyy-MM-dd}.log格式的日志文件
✅ 文件数量管理: 只保留最近3个文件,自动删除历史文件
✅ 单例保证: 相同类名和目录的日志对象保证是同一个实例
✅ 启动时清理: 每次启动程序时自动清理超过保留数量的历史文件
✅ 控制台输出控制: 通过全局变量控制是否启用控制台输出
import java.io.File;
import java.util.concurrent.ConcurrentHashMap;
import org.slf4j.LoggerFactory;
import com.staryea.stream.runner.MainRunner;
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.spi.ILoggingEvent;
import ch.qos.logback.core.ConsoleAppender;
import ch.qos.logback.core.rolling.RollingFileAppender;
import ch.qos.logback.core.rolling.TimeBasedRollingPolicy;
/**
* 动态日志工具类
* ✅ 动态创建: 无需配置文件,通过代码动态创建logback日志对象
* ✅ Class对象支持: 使用LogUtil.getLogger(MyClass.class)的方式获取日志
* ✅ 日期格式文件: 自动生成info.%d{yyyy-MM-dd}.log格式的日志文件
* ✅ 文件数量管理: 只保留最近3个文件,自动删除历史文件
* ✅ 单例保证: 相同类名和目录的日志对象保证是同一个实例
* ✅ 启动时清理: 每次启动程序时自动清理超过保留数量的历史文件
* ✅ 控制台输出控制: 通过全局变量控制是否启用控制台输出
*/
public class LogUtil {
// 缓存日志对象,key为类名
private static final ConcurrentHashMap<String, Logger> loggerCache = new ConcurrentHashMap<>();
// 日志格式
private static final String LOG_PATTERN = "%d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger{200}:%L - %msg%n";
// 默认日志目录
private static final String DEFAULT_LOG_DIR = "logs";
/**
* 获取日志对象
* @param clazz 类对象
* @return Logger对象
*/
public static Logger getLogger(Class<?> clazz) {
return getLogger(clazz, DEFAULT_LOG_DIR);
}
/**
* 获取日志对象
* @param clazz 类对象
* @param logDir 日志文件目录
* @return Logger对象
*/
public static Logger getLogger(Class<?> clazz, String logDir) {
String className = clazz.getName();
String key = className + "_" + logDir;
return loggerCache.computeIfAbsent(key, k -> createLogger(className, logDir));
}
/**
* 创建日志对象
* @param className 类名
* @param logDir 日志文件目录
* @return Logger对象
*/
private static Logger createLogger(String className, String logDir) {
System.out.println("开始创建日志对象,类名: " + className + ", 目录: " + logDir);
// 确保目录存在
File dir = new File(logDir);
if (!dir.exists()) {
boolean created = dir.mkdirs();
System.out.println("创建目录结果: " + created);
if (!created) {
throw new RuntimeException("无法创建日志目录: " + logDir);
}
}
System.out.println("目录是否存在: " + dir.exists());
System.out.println("目录是否可写: " + dir.canWrite());
// 获取LoggerContext
LoggerContext loggerContext = (LoggerContext) LoggerFactory.getILoggerFactory();
// 创建Logger,使用类名作为Logger名称。这个有BUG:Logback的LoggerContext对于相同类名总是返回同一个Logger实例,导致后续配置覆盖了之前的配置(相同类名时路径被覆盖)。解决方案是使用包含目录信息的唯一Logger名称。
// Logger logger = loggerContext.getLogger(className);
// 使用类名 + 目录作为Logger名称,确保唯一性
String loggerName = className + "_" + logDir.hashCode();
Logger logger = loggerContext.getLogger(loggerName);
logger.setAdditive(false); // 不继承父Logger的Appender
// 清除已有的Appender
logger.detachAndStopAllAppenders();
// 根据全局开关决定是否添加控制台输出
if (MainRunner.isTest) {
ConsoleAppender<ILoggingEvent> consoleAppender = createConsoleAppender(loggerContext);
logger.addAppender(consoleAppender);
System.out.println("控制台Appender添加成功: " + consoleAppender.isStarted());
} else {
System.out.println("控制台输出已禁用,跳过控制台Appender添加");
}
// 添加文件输出(始终存在)
RollingFileAppender<ILoggingEvent> fileAppender = createFileAppender(loggerContext, logDir);
logger.addAppender(fileAppender);
System.out.println("文件Appender添加成功: " + fileAppender.isStarted());
// 设置日志级别
logger.setLevel(Level.INFO);
System.out.println("Logger创建完成,名称: " + className);
System.out.println("Logger级别: " + logger.getLevel());
System.out.println("Appender数量: " + logger.iteratorForAppenders().hasNext());
return logger;
}
/**
* 创建控制台Appender
*/
private static ConsoleAppender<ILoggingEvent> createConsoleAppender(LoggerContext loggerContext) {
ConsoleAppender<ILoggingEvent> consoleAppender = new ConsoleAppender<>();
consoleAppender.setContext(loggerContext);
consoleAppender.setName("console");
PatternLayoutEncoder encoder = new PatternLayoutEncoder();
encoder.setContext(loggerContext);
encoder.setPattern(LOG_PATTERN);
encoder.start();
consoleAppender.setEncoder(encoder);
consoleAppender.start();
return consoleAppender;
}
/**
* 创建文件Appender
*/
private static RollingFileAppender<ILoggingEvent> createFileAppender(LoggerContext loggerContext, String logDir) {
RollingFileAppender<ILoggingEvent> fileAppender = new RollingFileAppender<>();
fileAppender.setContext(loggerContext);
fileAppender.setName("file");
// 创建编码器
PatternLayoutEncoder encoder = new PatternLayoutEncoder();
encoder.setContext(loggerContext);
encoder.setPattern(LOG_PATTERN);
encoder.start();
fileAppender.setEncoder(encoder);
// 创建滚动策略 - 只按时间轮转
TimeBasedRollingPolicy<ILoggingEvent> rollingPolicy = new TimeBasedRollingPolicy<>();
rollingPolicy.setContext(loggerContext);
rollingPolicy.setParent(fileAppender);
// 设置滚动文件路径模式 - 固定为info.%d{yyyy-MM-dd}.log
String rollingFile = logDir + File.separator + "info.%d{yyyy-MM-dd}.log";
rollingPolicy.setFileNamePattern(rollingFile);
// 设置保留文件数量(只保留3个文件)
rollingPolicy.setMaxHistory(3);
// 启动时清理历史文件
rollingPolicy.setCleanHistoryOnStart(true);
rollingPolicy.start();
fileAppender.setRollingPolicy(rollingPolicy);
// 设置立即刷新
fileAppender.setImmediateFlush(true);
fileAppender.start();
System.out.println("文件Appender启动状态: " + fileAppender.isStarted());
System.out.println("文件Appender名称: " + fileAppender.getName());
System.out.println("滚动文件模式: " + rollingFile);
System.out.println("保留文件数量: " + rollingPolicy.getMaxHistory());
System.out.println("启动时清理: " + rollingPolicy.isCleanHistoryOnStart());
return fileAppender;
}
/**
* 重新创建所有已缓存的Logger(应用新的控制台输出设置)
*/
public static void refreshAllLoggers() {
System.out.println("开始刷新所有Logger");
clearAllLoggers();
System.out.println("所有Logger已刷新完成");
}
/**
* 清理指定类名的日志对象缓存
* @param clazz 类对象
*/
public static void clearLogger(Class<?> clazz) {
clearLogger(clazz, DEFAULT_LOG_DIR);
}
/**
* 清理指定类名和目录的日志对象缓存
* @param clazz 类对象
* @param logDir 日志文件目录
*/
public static void clearLogger(Class<?> clazz, String logDir) {
String className = clazz.getName();
String key = className + "_" + logDir;
Logger logger = loggerCache.remove(key);
if (logger != null) {
logger.detachAndStopAllAppenders();
}
}
/**
* 清理所有日志对象缓存
*/
public static void clearAllLoggers() {
loggerCache.values().forEach(logger -> logger.detachAndStopAllAppenders());
loggerCache.clear();
}
/**
* 获取缓存中的日志对象数量
* @return 日志对象数量
*/
public static int getLoggerCount() {
return loggerCache.size();
}
public static void main(String[] args) {
String logDir1 = "E:\\logs\\1";
String logDir2 = "E:\\logs\\2";
System.out.println("=== 开始测试日志功能 ===");
// 测试不同类名的日志对象 - 使用Class对象
Logger log1 = LogUtil.getLogger(TestClass1.class, logDir1);
Logger log2 = LogUtil.getLogger(TestClass2.class, logDir2);
// 测试日志输出
System.out.println("=== 开始输出日志 ===");
log1.info("TestClass1的日志信息: {}", "这是第一条日志");
log1.error("TestClass1的错误日志: {}", "这是错误信息");
log2.info("TestClass2的日志信息: {}", "这是第二条日志");
log2.warn("TestClass2的警告日志: {}", "这是警告信息");
}
// 测试用的内部类
public static class TestClass1 {}
public static class TestClass2 {}
}
执行结果