玩转 Spring Boot 的启动 Banner

1,735 阅读6分钟

Spring Boot 在启动的时候,默认会在控制台打印一个 Banner,如下所示。为了满足个性化的需求,Spring Boot 允许我们自由的定制这个 Banner,本文将从源码的角度讨论几个问题。

spring-banner

  1. 如何关闭 Spring Boot 的 Banner。
  2. 如何定制文本类型的 Banner。
  3. 如何定制图片类型的 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);
		...
	}
	...
}

具体的流程如下:

  1. 判断 bannerMode,如果是 OFF,表示不打印。如果是 LOG,表示打印到文件,否则打印到控制台。
  2. 依据指定位置是是否存在文件,判断 Banner 类型是文本还是图片,文本类型使用 ResourceBanner, 图片类型使用 ImageBanner, 如果都不是,使用 Spring Boot 默认的 SpringBootBanner。
  3. 调用 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}

如下是最终的结果

banner-color

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-cloud

打印:

banner-cloud-txt

对于图片类型的 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 如何实现图片转文本。功能点和知识点之间,需要并重,这也是正确的读源码的方式。

如果您觉得有所收获,就请点个赞吧!