可能有些同学看到就觉得,这个都要发文章?这不是整个banner.txt再配置一下spring.banner.location=classpath:banner.txt
就行了吗?还真不是,这个是在项目启动时,先打印的banner,你仔细回想是不是,一启动控制台就打印一个《spring》,我帮你回忆一下
而我这里说的是,项目启动成功之后,再打印banner
如果你想问,为啥要放在启动后?启动前不行吗?问就是**规定。
梳理一下这个玩意大概要改动些什么:
看起来很简单,那就来实现一下吧
这个我也是随便在网上找个网站生成一下的,没有刻意去做什么优化。我这里使用的网址是
http://patorjk.com/software/taag/#p=display&f=Graffiti&t=Type%20Something%20
生成后直接复制下来就好了
如果对Spring的生命周期不太熟悉的话,可能首先想到的是在main方法后直接打印banner。这确实是个可行的方法,为了主方法的简洁和考虑到时效性,我更建议在Spring生命周期里去监听并打印。我们直接对ApplicationReadyEvent
事件进行监听即可。详细步骤看注释
package org.example.springboot.job;
import ch.qos.logback.classic.Logger;
import ch.qos.logback.classic.LoggerContext;
import ch.qos.logback.core.ConsoleAppender;
import ch.qos.logback.core.rolling.RollingFileAppender;
import lombok.extern.slf4j.Slf4j;
import org.slf4j.LoggerFactory;
import org.springframework.boot.context.event.ApplicationReadyEvent;
import org.springframework.context.ApplicationListener;
import org.springframework.stereotype.Component;
@Component
@Slf4j
public class PrintBanner implements ApplicationListener<ApplicationReadyEvent> {
// 1. 将复制过来的BANNER按着样板放到String字符数组中,注意如果有【\】记得补上多一个斜杠
public static final String[] BANNER = {
" ____ ____ ____ ____ ",
"(_ _)( __)/ ___)(_ _)",
" )( ) _) \\___ \\ )(",
" (__) (____)(____/ (__) ",
":: TEST :: (v1.0.0.RELEASE)"
};
@Override
public void onApplicationEvent(ApplicationReadyEvent event) {
// 1. 输出启动完成字样
log.info("start finish");
// 2. 输出banner数组
for (String s : BANNER) {
log.info(s);
}
}
}
启动完成之后,打印在控制台上的是这个样的
看起来有点怪怪的,为啥我的前面会打印那么多信息,人家Spring自己的又不会,为了应付交差,得先想出2.0版本。
如果用数组的话,会打印多行的信息,那我直接用字符串在前面加个换行符,再把字符拼接起来,那变成只输出一行信息会不会好看点。
package org.example.springboot.job;
import ch.qos.logback.classic.Logger;
import ch.qos.logback.classic.LoggerContext;
import ch.qos.logback.core.ConsoleAppender;
import ch.qos.logback.core.rolling.RollingFileAppender;
import lombok.extern.slf4j.Slf4j;
import org.slf4j.LoggerFactory;
import org.springframework.boot.context.event.ApplicationReadyEvent;
import org.springframework.context.ApplicationListener;
import org.springframework.stereotype.Component;
@Component
@Slf4j
public class PrintBanner implements ApplicationListener<ApplicationReadyEvent> {
// 1. 替换成字符串
public static final String testString =
"\n"+
"ooooooooooo ooooooooooo oooooooo8 ooooooooooo \n" +
"88 888 88 888 88 888 88 888 88 \n" +
" 888 888ooo8 888oooooo 888 \n" +
" 888 888 oo 888 888 \n" +
" o888o o888ooo8888 o88oooo888 o888o \n" +
" ";
@Override
public void onApplicationEvent(ApplicationReadyEvent event) {
// 1. 输出启动完成字样
log.info("start finish");
// 2. 输出banner长字符串
log.info(testString);
}
}
输出的样式是达到初步目的的,只打印出一行,而且也是靠左排列,看起来好一点了,能忽悠过去了。
但是,我们像是只是为了交差的人吗?我们是追求艺术与完美的人。你看看人家SpringBoot启动的banner,同样是logger.info,为什么它可以不打印前面的那堆信息?而打印完banner之后,logger打印的信息就会有详细的时间、线程等信息?
这问题估计有点小刁钻,百度了一波也没看到答案。源码又不知道从何跟起,我### 原理猜测
猜测是两个可能:
第一种是打印banner的logger和打印其它的信息不属于同一个日志控件,比如说打印banner的log是org.apache.commons.logging.Log
,而打印启动信息的是logback
框架的。可是我用org.apache.commons.logging.Log
打印出来的还是会自带时间、线程等信息。
第二种是用的是同一个Logger,但appender配置有两份,在打印banner时,不打印时间和线程等信息,打印完banner之后切换了appender。所以之后打印的都带上了时间和线程等信息。可是我阅读了SpringBoot源码的logback与Log4j都不存在两份配置。
这就很神奇,于是我静下心来看一下SpringAppcation.run中关于banner的执行流程。我发现上面图上的那个其实是要打开banner的日志模式才能输出到日志里的。。。。。猜了半天原来是我摸错了。默认的情况下,使用的是System.out来输出banner,那肯定不会有时间、线程的信息啊。
于是我开启了Mode.LOG模式,再测试了一遍,这多余的时间和线程信息击碎了我的所有幻想,瞬间觉得不过如此。
不过也不是说一无是处,它的banner其实同样是一个字符串数组,但它很巧妙地用字节输出流来避免了我上面的情况,输出很多重复的日志信息。至于是使用字符串数组还是长字符串拼接,我们探究一下:
从性能的角度来看,输出一个字符串数组会比输出一个具有多个换行符的长字符串更好,尤其是当数组中的字符串数量较少时。这是因为在输出长字符串时,JVM需要一次性处理整个字符串,并在其中查找并替换所有换行符,这可能会导致性能下降。
相比之下,输出一个字符串数组只需要遍历数组并逐个输出每个字符串,这通常比处理整个字符串更快。此外,如果字符串数组已经存在,那么输出它将比创建一个新的具有多个换行符的字符串要快得多。
当然这点性能的优化几乎无感,只不过你没写好的话,被那些老懂哥看了少不了一顿批。
我们结合SpringBoot的banner打印实现来优化我们的代码
package org.example.springboot.job;
import ch.qos.logback.classic.Logger;
import ch.qos.logback.classic.LoggerContext;
import ch.qos.logback.core.ConsoleAppender;
import ch.qos.logback.core.rolling.RollingFileAppender;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.SpringBootVersion;
import org.springframework.boot.ansi.AnsiColor;
import org.springframework.boot.ansi.AnsiOutput;
import org.springframework.boot.ansi.AnsiStyle;
import org.springframework.boot.context.event.ApplicationReadyEvent;
import org.springframework.context.ApplicationListener;
import org.springframework.stereotype.Component;
import java.io.ByteArrayOutputStream;
import java.io.PrintStream;
import java.io.UnsupportedEncodingException;
@Component
public class PrintBanner implements ApplicationListener<ApplicationReadyEvent> {
@Value("${test.version}")
private String version;
private String bannerName = " :: Test :: ";
public static final String[] BANNER = {
"",
" ____ ____ ____ ____ ",
"(_ _)( __)/ ___)(_ _)",
" )( ) _) \\___ \\ )(",
" (__) (____)(____/ (__) "
};
@Override
public void onApplicationEvent(ApplicationReadyEvent event) {
Log log = LogFactory.getLog(PrintBanner.class);
log.info("start finish");
ByteArrayOutputStream baos = new ByteArrayOutputStream();
PrintStream printStream = new PrintStream(baos);
for (String s : BANNER) {
printStream.println(s);
}
version = (version != null) ? " (v" + version + ")" : "";
StringBuilder padding = new StringBuilder();
while (padding.length() < 24 - (version.length() + bannerName.length())) {
padding.append(" ");
}
printStream.println(AnsiOutput.toString(AnsiColor.GREEN, bannerName, AnsiColor.DEFAULT, padding.toString(),
AnsiStyle.FAINT, version));
try {
log.info(baos.toString("UTF-8"));
} catch (UnsupportedEncodingException e) {
throw new RuntimeException(e);
}
}
}
输出效果是这样的
到了这一步,几乎就满足大多数人的刁难要求了。别人家的SpringBoot也是这样,一启动也打印了时间和线程信息。
可是,如果是**领导的强制要求对吧,或者是你自己想探究一下,能不能真的做到日志打印banner不要带有时间线程信息?还真的行。
要实现这个功能,就得回顾一下我们前面分析方案的时候提到的两个方案:
%msg
,打印完banner之后再设置回来,原理是动态设置root节点的appender研究一下可行性,第一种方案多维护一个日志框架,且只用于打印banner,这会给后续的维护带来麻烦,且如果框架发生了什么漏洞,又得给你维护一套。第二种方案维护多一个appender,但要来回切换,会对性能造成一定的损耗,且如果改造了异步日志,在打印banner的时候需要睡眠1ms以达到打印的图案没有出现乱序。
权衡各种情况,我觉得可行方案是第二种。
在原有的logback-spring.xml文件中,新配置了一个BANNER appender。添加一个Pattern,只打印%msg
,这个appender不用先放到root节点下。在程序启动的过程中去获取设置。
<configuration>
<include resource="org/springframework/boot/logging/logback/default.xml"/>
<property name="LOG_FOR_ROLLING" value="%d{yyyy-MM-dd HH:mm:ss.SSS} %-5level[%thread]%logger.%method:%L: %msg%n"/>
<property name="LOG_FOR_BANNER" value="%msg%n"/>
<appender name="BANNER" class="ch.qos.logback.core.ConsoleAppender">
<encoder>
<pattern>${LOG_FOR_BANNER}pattern>
<charset>utf8charset>
encoder>
appender>
<appender name="ROLLING" class="ch.qos.logback.core.rolling.RollingFileAppender">
<file>test-app.logfile>
<rollingPolicy class="ch.qos.logback.core.rolling.SizeAndTimeBasedRollingPolicy">
<fileNamePattern>test-%d{yyyy-MM-dd}.%i.logfileNamePattern>
<maxFileSize>100MBmaxFileSize>
<maxHistory>360maxHistory>
<totalSizeCap>50GBtotalSizeCap>
rollingPolicy>
<encoder>
<pattern>${LOG_FOR_ROLLING}pattern>
<charset>utf8charset>
encoder>
appender>
<appender name="CONSOLE" class="ch.qos.logback.core.ConsoleAppender">
<encoder>
<pattern>${LOG_FOR_ROLLING}pattern>
<charset>utf8charset>
encoder>
appender>
<logger name="loggerForBanner">
<appender-ref ref="BANNER"/>
logger>
<logger name="loggerForRolling">
<appender-ref ref="ROLLING"/>
logger>
<logger name="loggerForConsole">
<appender-ref ref="CONSOLE"/>
logger>
<root level="INFO">
<appender-ref ref="ROLLING"/>
<appender-ref ref="CONSOLE"/>
root>
configuration>
程序改造
package org.example.springboot.job;
import ch.qos.logback.classic.Logger;
import ch.qos.logback.classic.LoggerContext;
import ch.qos.logback.core.ConsoleAppender;
import ch.qos.logback.core.rolling.RollingFileAppender;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.SpringBootVersion;
import org.springframework.boot.ansi.AnsiColor;
import org.springframework.boot.ansi.AnsiOutput;
import org.springframework.boot.ansi.AnsiStyle;
import org.springframework.boot.context.event.ApplicationReadyEvent;
import org.springframework.context.ApplicationListener;
import org.springframework.stereotype.Component;
import java.io.ByteArrayOutputStream;
import java.io.PrintStream;
import java.io.UnsupportedEncodingException;
@Component
@Slf4j
public class PrintBanner implements ApplicationListener<ApplicationReadyEvent> {
@Value("${test.version}")
private String version;
private String bannerName = " :: Test :: ";
public static final String[] BANNER = {
// "",
" ____ ____ ____ ____ ",
"(_ _)( __)/ ___)(_ _)",
" )( ) _) \\___ \\ )(",
" (__) (____)(____/ (__) "
};
@Override
public void onApplicationEvent(ApplicationReadyEvent event) {
log.info("start finish");
changeAppender("BANNER");
printBanner();
changeAppender("NORMAL");
}
private void printBanner() {
ByteArrayOutputStream baos = new ByteArrayOutputStream();
PrintStream printStream = new PrintStream(baos);
for (String s : BANNER) {
printStream.println(s);
}
version = (version != null) ? " (v" + version + ")" : "";
StringBuilder padding = new StringBuilder();
while (padding.length() < 24 - (version.length() + bannerName.length())) {
padding.append(" ");
}
printStream.println(AnsiOutput.toString(AnsiColor.GREEN, bannerName, AnsiColor.DEFAULT, padding.toString(),
AnsiStyle.FAINT, version));
try {
log.info(baos.toString("UTF-8"));
} catch (UnsupportedEncodingException e) {
throw new RuntimeException(e);
}
}
private static void changeAppender(String logType) {
// 获取log配置文件上下文
LoggerContext logger = (LoggerContext) LoggerFactory.getILoggerFactory();
// 获取根节点root
Logger loggerForRoot = logger.getLogger("root");
Logger loggerForRolling = logger.getLogger("loggerForRolling");
// root节点移除appender组件
loggerForRoot.detachAppender("CONSOLE");
loggerForRoot.detachAppender("ROLLING");
loggerForRoot.detachAppender("BANNER");
RollingFileAppender rollingAppender = (RollingFileAppender) loggerForRolling.getAppender("ROLLING");
// 由于打印banner和打印普通日志不一样,则要根据不同的模式切换root节点的appender
switch (logType) {
case "BANNER" : {
Logger loggerForBanner = logger.getLogger("loggerForBanner");
ConsoleAppender bannerAppender = (ConsoleAppender) loggerForBanner.getAppender("BANNER");
// 这一步是为了输出的日志文件和控制台统一都使用banner的encoder
rollingAppender.setEncoder(bannerAppender.getEncoder());
// 把appender放回去root就能激活使用
loggerForRoot.addAppender(rollingAppender);
loggerForRoot.addAppender(bannerAppender);
}
break;
case "NORMAL" : {
Logger loggerForConsole = logger.getLogger("loggerForConsole");
ConsoleAppender consoleAppender = (ConsoleAppender) loggerForConsole.getAppender("CONSOLE");
rollingAppender.setEncoder(consoleAppender.getEncoder());
loggerForRoot.addAppender(rollingAppender);
loggerForRoot.addAppender(consoleAppender);
}
break;
}
}
}
输出的效果是