在logback作为日志框架的前提下,实现在代码中动态修改日志输出格式的需求(也许没什么用,不过可以在自己的程序启动时达到类似sprinboot的banner那样的效果-springboot的banner是没有日志的类名、时间戳之类的前缀的)。
实现,在一个日志文件中,可以输出不同格式的日志内容。
本demo的目标是:
程序启动时输出没有前缀的banner logo,后面的所有日志为带前缀的日志,比如这样:
____.___________
| |\_ _____/_____ ______ ______ ______ ______
| | | __)/ ____// ____// ____// ____// ____/
/\__| | | \< <_| < <_| < <_| < <_| < <_| |
\________| \___ / \__ |\__ |\__ |\__ |\__ |
\/ |__| |__| |__| |__| |__|
2021-07-07 17:41:37.363 [main] INFO com.jfqqqq.App - 程序启动【Wed Jul 07 17:41:37 CST 2021】...
2021-07-07 17:41:37.365 [main] INFO com.jfqqqq.App - 程序结束...
2021-07-07 17:41:37.365 [main] INFO com.jfqqqq.App - 程序结束...
2021-07-07 17:41:37.365 [main] INFO com.jfqqqq.App - 程序结束...
0. 一个日志文件只能被一个appender使用,如果定义了多个appender都指向同一个日志文件,那么logback在初始化时只会使用第一个appender,只有它被“started”。“第一个”的意思是,在logback.xml中,从上到下第一个出现的引用该日志文件的appender。如下面的例子,就是name为“FILE_INFO_BANNER”的appender为第一个:
${LOG_HOME}/info/jfqqqq.info.%d{yyyy-MM-dd}.%i.log
${maxFileSize}
${MaxHistory}
...省略
${LOG_HOME}/info/jfqqqq.info.%d{yyyy-MM-dd}.%i.log
${maxFileSize}
${MaxHistory}
...省略
说明:
started:在类ch.qos.logback.core.UnsynchronizedAppenderBase中,doAppend方法用来输出日志,输出时会判断当前logger是否"started",是才会输出:
1. additivity属性(在xml中配置为additivity,在代码中为additive)的作用。
简单的说,logback默认的日志行为,是按级别依次调用的,一个logger会有父logger,父logger又会有爷爷logger。。。以此类推。所以,如果这个属性为true,那么我们自己定义的logger在打印日志时,父logger也会打印。如果本logger和父logger的appender都被"started"了(比如一个被started的appender被子logger和父logger同时引用),就会出现打印重复的情况。
比如:
如果设为false,就不会重复打印了。
详情可以参考:http://skyao.github.io/2014/09/23/logback-additivity/
2. PatternLayoutEncoder,是RollingFileAppender的属性之一,用来控制输出格式。RollingFileAppender大家会很熟悉,在使用匹配模板的方式输出日志,这个会用的比较多,比如:
${LOG_HOME}/${SELF_BLOCK}/info/info.%d{yyyy-MM-dd}.%i.log
${maxFileSize}
${MaxHistory}
%d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger{50} - %msg%n
info
ACCEPT
DENY
我们可以动态修改RollingFileAppender中的PatternLayoutEncoder,实现格式的变换。
值得注意的时,这个PatternLayoutEncoder在更改pattern后,必须要执行start()方法,才能完成真正的刷新,否则是无效的--并且,在更改前,需要先执行stop()方法。然后再start()(不要问我怎么知道的)。等于说是这样的:
//伪代码
encoder.stop();
encoder.setPattern(pattern);
encoder.start();//才能生效
3.
综上所述,我们只要动态的修改PatternLayoutEncoder的pattern,在输出banner时改为没有前缀的格式,输出完成后再改回有前缀的格式即可。
1. 定义好有前缀的输出格式的appender(代码中叫“FILE_INFO”),把它交给root logger,这样保证了有前缀的输出。
2. 然后,定义一个bannerLogger,它也引用这个“FILE_INFO”。
3. 程序启动时,获取bannerLogger,再通过它获取“FILE_INFO”,然后获取“FILE_INFO”的pattern(这个pattern就是早xml配置的pattern)并放到缓存中,然后创建新的没有前缀的格式pattern,赋予给appender,用于输出banner的内容。
4. 输出完成后,再将缓存的pattern重新赋予“FILE_INFO”, 这样,后续的日志就全都有前缀了。
注意:上面的4个步骤中只说明大意,别忘了stop()和start()函数的使用。
xml
${LOG_HOME}/info/info.%d{yyyy-MM-dd}.%i.log
${maxFileSize}
${MaxHistory}
%d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger{50} - %msg%n
info
ACCEPT
DENY
xml中定义了两个logger(root 与 com.jfqqqq.Banner)和一个appender(FILE_INFO)。两个logger都使用 FILE_INFO 。
java
public class Banner {
public void println(String bannerPath) {
try {
ch.qos.logback.classic.Logger logger = (ch.qos.logback.classic.Logger) LoggerFactory.getLogger("com.tdt.tile.clip.command.Banner");
//一定要设为false,否则会出现知识点2中的重复输出的情况。会发现banner的图形有重影(每一行输出了两边,看上去像重影)
logger.setAdditive(false);
RollingFileAppender configAppender = (RollingFileAppender) logger.getAppender("FILE_INFO");
PatternLayoutEncoder encoder = (PatternLayoutEncoder)configAppender.getEncoder();
encoder.stop();
String pattern = encoder.getPattern();
encoder.setPattern("%msg%n");
encoder.start();
if (bannerPath!= null) {
File file = new File(bannerPath);
if (file.exists()) {
FileReader fileReader = new FileReader(file);
BufferedReader bufferedReader = new BufferedReader(fileReader);
String line;
while ((line = bufferedReader.readLine()) != null) {
logger.info(line);
}
bufferedReader.close();
fileReader.close();
}
}
encoder.stop();
encoder.setPattern(pattern);
encoder.start();
} catch (Exception e) {
e.printStackTrace();
}
}
}
----------------------------------------------------- 分割线 -----------------------------------------------------------------
App:
public void start(String bannerPath) {
assert bannerPath != null;
//banner 打印
new Banner().println(bannerPath);
logger.info("程序启动【{}】...", new Date());
logger.info("程序结束...");
logger.info("程序结束...");
logger.info("程序结束...");
}
Banner:
public class Banner {
public void println(String binPath) {
try {
ch.qos.logback.classic.Logger logger = (ch.qos.logback.classic.Logger) LoggerFactory.getLogger("com.jfqqqq.Banner");
String path = binPath.replace("bin", "conf" + SystemUtil.getSeparator() + "banner.txt");
if (path != null) {
File file = new File(path);
if (file.exists()) {
FileReader fileReader = new FileReader(file);
BufferedReader bufferedReader = new BufferedReader(fileReader);
String line;
while ((line = bufferedReader.readLine()) != null) {
logger.info(line);
}
bufferedReader.close();
fileReader.close();
}
}
} catch (Exception e) {
e.printStackTrace();
}
}
}
logback.xml:
有一个appender,称呼为”FILE_INFO”,日志级别为info,输出到指定目录下的日志文件中(后面简称路径/X/info.log),如(下面demo中有写多余的配置,可以忽略):
${LOG_HOME}/info/jfqqqq.info.%d{yyyy-MM-dd}.%i.log
${maxFileSize}
${MaxHistory}
%d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger{50} - %msg%n
info
ACCEPT
DENY
在xml中增加一个banner的banner_appender(name="FILE_INFO_BANNER"),设置好输出格式为没有前缀的pattern:”%msg%n“:
${LOG_HOME}/info/jfqqqq.info.%d{yyyy-MM-dd}.%i.log
${maxFileSize}
${MaxHistory}
%msg%n
info
ACCEPT
DENY
然后再定义一个单独的logger(name="com.jfqqqq.Banner"),使用banner_appender(FILE_INFO_BANNER):
${LOG_HOME}/info/jfqqqq.info.%d{yyyy-MM-dd}.%i.log
${maxFileSize}
${MaxHistory}
%d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger{50} - %msg%n
info
ACCEPT
DENY
${LOG_HOME}/info/jfqqqq.info.%d{yyyy-MM-dd}.%i.log
${maxFileSize}
${MaxHistory}
%msg%n
info
ACCEPT
DENY
失败。包含banner在内的所有日志都是有前缀的:
2021-07-07 17:41:37.281 [main] INFO com.jfqqqq.Banner - ____.___________
2021-07-07 17:41:37.283 [main] INFO com.jfqqqq.Banner - | |\_ _____/_____ ______ ______ ______ ______
2021-07-07 17:41:37.283 [main] INFO com.jfqqqq.Banner - | | | __)/ ____// ____// ____// ____// ____/
2021-07-07 17:41:37.283 [main] INFO com.jfqqqq.Banner - /\__| | | \< <_| < <_| < <_| < <_| < <_| |
2021-07-07 17:41:37.283 [main] INFO com.jfqqqq.Banner - \________| \___ / \__ |\__ |\__ |\__ |\__ |
2021-07-07 17:41:37.283 [main] INFO com.jfqqqq.Banner - \/ |__| |__| |__| |__| |__|
2021-07-07 17:41:37.363 [main] INFO com.jfqqqq.App - 程序启动【Wed Jul 07 17:41:37 CST 2021】...
2021-07-07 17:41:37.365 [main] INFO com.jfqqqq.App - 程序结束...
2021-07-07 17:41:37.365 [main] INFO com.jfqqqq.App - 程序结束...
2021-07-07 17:41:37.365 [main] INFO com.jfqqqq.App - 程序结束...
原因:banner的appender没能初始化,最后是由root logger打印的。
xml:
给banner的logger增加additive="false"属性,并在root logger中去掉这个banner:
其他不变
2021-07-07 18:35:03.073 [main] INFO com.tdt.tile.clip.command.App - 程序启动【Wed Jul 07 18:35:03 CST 2021】...
2021-07-07 17:41:37.365 [main] INFO com.jfqqqq.App - 程序结束...
2021-07-07 17:41:37.365 [main] INFO com.jfqqqq.App - 程序结束...
2021-07-07 17:41:37.365 [main] INFO com.jfqqqq.App - 程序结束...
失败
原因:虽然拒绝了父类的表现,但是banner的appender并没有“started”初始化,所以输出不了。
把banner的appender声明放在前边,root中也赋予这个appender:
${LOG_HOME}/info/jfqqqq.info.%d{yyyy-MM-dd}.%i.log
${maxFileSize}
${MaxHistory}
%msg%n
info
ACCEPT
DENY
${LOG_HOME}/info/jfqqqq.info.%d{yyyy-MM-dd}.%i.log
${maxFileSize}
${MaxHistory}
%d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger{50} - %msg%n
info
ACCEPT
DENY
____.___________
____.___________
| |\_ _____/_____ ______ ______ ______ ______
| |\_ _____/_____ ______ ______ ______ ______
| | | __)/ ____// ____// ____// ____// ____/
| | | __)/ ____// ____// ____// ____// ____/
/\__| | | \< <_| < <_| < <_| < <_| < <_| |
/\__| | | \< <_| < <_| < <_| < <_| < <_| |
\________| \___ / \__ |\__ |\__ |\__ |\__ |
\________| \___ / \__ |\__ |\__ |\__ |\__ |
\/ |__| |__| |__| |__| |__|
\/ |__| |__| |__| |__| |__|
程序启动【Wed Jul 07 19:06:03 CST 2021】...
程序结束...
程序结束...
程序结束...
原因:banner的appender被banner的logger和root logger同时引用,additivity为true,满足了知识点2的条件,因此用日志中,banner logger的图形会重复。