Spring Boot 在启动的时候,默认会在控制台打印一个 Banner,如下所示。为了满足个性化的需求,Spring Boot 允许我们自由的定制这个 Banner,本文将从源码的角度讨论几个问题。
- 如何关闭 Spring Boot 的 Banner。
- 如何定制文本类型的 Banner。
- 如何定制图片类型的 Banner。
Banner 打印流程
从 Spring Boot 的 run 方法可以看出,printBanner 方法在 prepareEnvironment 之后,这是因为 application.properties 中有一些关于 Banner 的配置项。需要先解析 application.properties 的值,并将其绑定到对应的 bean 之后,才好进行后续的操作。
public ConfigurableApplicationContext run(String... args) {
...
try {
ApplicationArguments applicationArguments = new DefaultApplicationArguments(args);
ConfigurableEnvironment environment = prepareEnvironment(listeners, bootstrapContext, applicationArguments);
configureIgnoreBeanInfo(environment);
// 打印 Banner
Banner printedBanner = printBanner(environment);
...
}
...
}
具体的流程如下:
- 判断 bannerMode,如果是 OFF,表示不打印。如果是 LOG,表示打印到文件,否则打印到控制台。
- 依据指定位置是是否存在文件,判断 Banner 类型是文本还是图片,文本类型使用 ResourceBanner, 图片类型使用 ImageBanner, 如果都不是,使用 Spring Boot 默认的 SpringBootBanner。
- 调用 Banner 对象的的 printBanner 方法。不同类型的 Banner 的 printBanner 方法不同。
如下是 Banner 对象的获的方法,可以看出,Spring Boot 首先获得 ImageBanner,然后是 ResourceBanner,需要注意的是,这两者可以同时存在,此时会一次性打印两中 Banner。如果都不满足,还会去获得 fallbackBanner,这个是用户自己设定的兜底 Banner,但是我们很少使用,因为 Spring Boot 内置了兜底方案,也就是 SpringBootBanner。
private Banner getBanner(Environment environment) {
Banners banners = new Banners();
banners.addIfNotNull(getImageBanner(environment));
banners.addIfNotNull(getTextBanner(environment));
if (banners.hasAtLeastOneBanner()) {
return banners;
}
if (this.fallbackBanner != null) {
return this.fallbackBanner;
}
return DEFAULT_BANNER;
}
如何关闭 Spring Boot 的 Banner
从 printBanner 方法可以看出。当我们将 bannerMode 设置为 Banner.Mode.OFF 的时候,该方法返回 null。也就是此时不会打印 Banner。所以,设置 bannerMode 是关闭 Banner 功能的关键。
private Banner printBanner(ConfigurableEnvironment environment) {
if (this.bannerMode == Banner.Mode.OFF) {
return null;
}
...
}
bannerMode 是 SpringApplication 的成员变量,Spring Boot 中提供了两种方案来设置 bannerMode。
第一种就是在启动代码中设置。如下所示。
public static void main(String[] args) {
SpringApplication app
= new SpringApplication(XiangApplication.class);
app.setBannerMode(Banner.Mode.OFF);
app.run(args);
}
第二种是在 application.properties 中配置 spring.main.banner-mode
spring.main.banner-mode=off
这两种方式都可以关闭 Banner,那当它们同时存在的时候,哪个生效呢?我们可以这样分析,启动代码中调用 setBannerMode 方法,改变了 bannerMode 的值,之后 SpringApplication 对象执行 run 方法,在 run 方法中会解析 application.properties 的值,并将其绑定到对应的 bean,后者覆盖前者,所以 application.properties 中的配置优先级更高。
bannerMode 除了设置 off 外,还可以设置为 off 和 log,这个值来自于 Banner.Mode。Banner.Mode 是个枚举值,OFF 表示关闭,LOG 表示打印到文件中,CONSOLE 表示使用 System.out 打印到当前控制台。本文中不讨论 LOG 模式,只讨论 CONSOLE 模式。
enum Mode {
OFF,
CONSOLE,
LOG
}
定制文本类型的 Banner
Spring Boot 中定制文本类型的 Banner 很简单,我们只要在 resources 下增加一个 banner.txt 就可以了。比如我想让 Banner 显示 ShenMAX。那我就可以在 banner.txt 中增加以下文本。
_________.__ _____ _____ ____ ___
/ _____/| |__ ____ ____ / \ / _ \ \ \/ /
\_____ \ | | \_/ __ \ / \ / \ / \ / /_\ \ \ /
/ \| Y \ ___/| | \ / Y \/ | \/ \
/_______ /|___| /\___ >___| / \____|__ /\____|__ /___/\ \
\/ \/ \/ \/ \/ \/ \_/
当然,如果让我们自己来编辑这一串文本,显然太麻烦了,我们可以借助一些工具来完成这件事,比如 patorjk.com/software/ta… 支持将各种文本转换为文本图。www.degraeve.com/img2txt.php 支持将各种图片转换为文本图。
这段文本中可以增加一些占位符,这些占位符会在打印之前被替换,比如我们想要打印当前的版本可以增加如下代码。
Spring Boot Version: ${spring-boot.version}
除了系统自带的一些占位符外,我们还可以使用配置在 application.properties 中的环境变量作为占位符。比如我们在 application.properties 中配置了 app.placeholder=no code,no kill,那我们就能在 banner.txt 中使用 ${app.placeholder} 来引用这句话。
很少有人知道,System.out.println() 其实是可以控制其中的字体颜色的,不仅是颜色,背景色,文字是否加粗都能控制。比如我们想让文字输出是红色的,我们可以使用以下代码。
System.out.println("\033[31m" + "字体是红色");
Spring Boot 中将这个能力封装为 AnsiColor,我们可以在 banner.txt 中使用 AnsiColor 指定后续的文本的颜色。
${AnsiColor.BRIGHT_YELLOW}
_________.__ _____ _____ ____ ___
/ _____/| |__ ____ ____ / \ / _ \ \ \/ /
\_____ \ | | \_/ __ \ / \ / \ / \ / /_\ \ \ /
/ \| Y \ ___/| | \ / Y \/ | \/ \
/_______ /|___| /\___ >___| / \____|__ /\____|__ /___/\ \
\/ \/ \/ \/ \/ \/ \_/
${AnsiColor.BLUE}
Spring Boot Version: ${spring-boot.version}
如下是最终的结果
Spring Boot 是如何自动加载这个资源文件的呢?在打印默认的 Banner 之前,Spring Boot 会调用 getTextBanner 方法,尝试读取环境变量 spring.banner.location 的值,如果我们没有在 application.properties 配置这个值的话,默认就是 banner.txt。使用 resourceLoader 加载这个资源文件,如果加载到了,那就使用加载到的资源文件作为 Banner。
static final String BANNER_LOCATION_PROPERTY = "spring.banner.location";
static final String DEFAULT_BANNER_LOCATION = "banner.txt";
private Banner getTextBanner(Environment environment) {
String location = environment.getProperty(BANNER_LOCATION_PROPERTY, DEFAULT_BANNER_LOCATION);
Resource resource = this.resourceLoader.getResource(location);
try {
if (resource.exists() && !resource.getURL().toExternalForm().contains("liquibase-core")) {
return new ResourceBanner(resource);
}
}
catch (IOException ex) {
// Ignore
}
return null;
}
ResourceBanner 将读取到的文本流转换为一串 String。但是此时并不会直接打印这段文本,而是要替换文本中的占位符。
@Override
public void printBanner(Environment environment, Class<?> sourceClass, PrintStream out) {
try {
String banner = StreamUtils.copyToString(this.resource.getInputStream(),
environment.getProperty("spring.banner.charset", Charset.class, StandardCharsets.UTF_8));
for (PropertyResolver resolver : getPropertyResolvers(environment, sourceClass)) {
banner = resolver.resolvePlaceholders(banner);
}
out.println(banner);
}
catch (Exception ex) {
logger.warn(LogMessage.format("Banner not printable: %s (%s: '%s')", this.resource, ex.getClass(),
ex.getMessage()), ex);
}
}
为了解析 Banner 中的占位符,这里使用了四个解析器,environment 对应 application.properties 中的配置;VersionResolver 解析 spring boot 版本,AnsiResolver 解析颜色或字体等样式配置,TitleResolver 解析当前应用的版本,名称等。
protected List<PropertyResolver> getPropertyResolvers(Environment environment, Class<?> sourceClass) {
List<PropertyResolver> resolvers = new ArrayList<>();
resolvers.add(environment);
resolvers.add(getVersionResolver(sourceClass));
resolvers.add(getAnsiResolver());
resolvers.add(getTitleResolver(sourceClass));
return resolvers;
}
定制图片类型的 Banner
Spring Boot 支持直接配置一张图片作为 Banner,做法和文本类型 Banner 配置是一样的,选择一张图片(后缀是 jpg | png | gif)。修改名称为 banner,放到 resource 目录下,就可以了。
我找了一张云盘的图片,转换后的效果如下图所示。
原图:
打印:
对于图片类型的 Banner,Spring Boot 会将其装换成可打印的文本格式,然后将其输出。我们可以大概看下这部分的逻辑。
加载图片类资源的方式和加载文本类资源的方式是一致的,首先判断是否配置了 spring.banner.image.location 变量,默认是 resources 目录。在该目录下依次找 banner.gif,banner.jpg,banner.png。只要找到一个就算存在图片类型的 Banner。
static final String BANNER_IMAGE_LOCATION_PROPERTY = "spring.banner.image.location";
static final String[] IMAGE_EXTENSION = { "gif", "jpg", "png" };
private Banner getImageBanner(Environment environment) {
String location = environment.getProperty(BANNER_IMAGE_LOCATION_PROPERTY);
if (StringUtils.hasLength(location)) {
Resource resource = this.resourceLoader.getResource(location);
return resource.exists() ? new ImageBanner(resource) : null;
}
for (String ext : IMAGE_EXTENSION) {
Resource resource = this.resourceLoader.getResource("banner." + ext);
if (resource.exists()) {
return new ImageBanner(resource);
}
}
return null;
}
图片类型 Banner 的打印流程比较繁琐,思路是这样的,程序会根据我们配置的长宽等信息,先将原图进行压缩,然后遍历原图的每一个像素点,解析该点的颜色,找到和该点颜色最相近的可打印的颜色(AnsiColor)。这里涉及到较多的配置,比如 spring.banner.image.width 配置图片的宽度,spring.banner.image.bitdepth 用于 ANSI 颜色的位深度。支持的值为4(16色)或8(256色)。
private void printBanner(Environment environment, PrintStream out) throws IOException {
int width = getProperty(environment, "width", Integer.class, 76);
int height = getProperty(environment, "height", Integer.class, 0);
int margin = getProperty(environment, "margin", Integer.class, 2);
boolean invert = getProperty(environment, "invert", Boolean.class, false);
BitDepth bitDepth = getBitDepthProperty(environment);
PixelMode pixelMode = getPixelModeProperty(environment);
Frame[] frames = readFrames(width, height);
for (int i = 0; i < frames.length; i++) {
if (i > 0) {
resetCursor(frames[i - 1].getImage(), out);
}
printBanner(frames[i].getImage(), margin, invert, bitDepth, pixelMode, out);
sleep(frames[i].getDelayTime());
}
}
想要一张图片比较好的显示,这些配置是一定需要调整的,默认的配置下只能输出一些简单的图片,复杂一些的图片显示的效果很差,这也是我不推荐使用图片作为 Banner 的原因之一。
由于图片转字符的过程中需要经过图片压缩,遍历像素点等操作,在打印较大图片的时候会减慢启动速度,得不偿失。而且在图片中我们无法使用占位符来输出一些配置信息,如果仅仅是为了好看的话完全可以提前将图片转为文字,没必要让 Spring Boot 做这些工作。
还有一个比较有趣的点是,Spring Boot 支持将 gif 设置为 Banner,本来以为是黑科技,结果是将 gif 分隔成一张张图片,然后依序输出,搞了半天还是简单的图片输出,功能有点鸡肋啊。
最后
Spring Boot 有很多的有意思的细节,Banner 是其中之一。我们在看 Banner 打印逻辑的时候,不仅要知道如何自定义一个文本 Banner 或 图片 Banner,如何配置自定义 Banner,更要了解其中的一些底层实现原理。比如在了解文字 Banner 的时候,我们知道了 System.out.println() 是可以输出带颜色文本的,这也是 Spring Boot 启动日志带颜色的秘诀。比如在了解图片 Banner 的时候,顺便看看 Java 如何实现图片转文本。功能点和知识点之间,需要并重,这也是正确的读源码的方式。
如果您觉得有所收获,就请点个赞吧!