SpringBoot启动控制台的banner是怎么回事

打印 上一主题 下一主题

主题 1045|帖子 1045|积分 3135

前言

每次启动SpringBoot项目时,总是能看到控制台打印了一串字符,隐约能辨认出是“Spring”,不知大家是否也好奇过是怎么实现的,是直接打印固定的字符串,还是根据什么算法去生成的?于是闲暇无事,探究一番。

只想修改banner可以跳到文末查看
SpringBoot是怎么打印的

Banner默认实现类 SpringBootBanner

1、根据控制台打印的字符进行全局搜索,笔者选取:: Spring Boot ::进行搜索,定位到了org.springframework.boot.SpringBootBanner。
IDEA全局搜索:CTRL + SHIFT + R

2、进入SpringBootBanner类,先看下注释Default Banner implementation which writes the 'Spring' banner.,说了两个信息:1、当前类是SpringBoot Banner的默认实现;2、打印的字符是“Spring”。
3、往下看,SpringBootBanner实现了Banner接口。Banner包括printBanner方法和枚举Mode。
根据Mode中的注释和枚举值可以看出,Banner有三种状态:关闭、打印到控制台、打印到日志。具体使用场景留待后续分析。
Banner源码
  1. /**
  2. * Interface class for writing a banner programmatically.
  3. */
  4. @FunctionalInterface
  5. public interface Banner {
  6.         /**
  7.          * Print the banner to the specified print stream.
  8.          * @param environment the spring environment
  9.          * @param sourceClass the source class for the application
  10.          * @param out the output print stream
  11.          */
  12.         void printBanner(Environment environment, Class<?> sourceClass, PrintStream out);
  13.         /**
  14.          * An enumeration of possible values for configuring the Banner.
  15.          */
  16.         enum Mode {
  17.                 /**
  18.                  * Disable printing of the banner.
  19.                  */
  20.                 OFF,
  21.                 /**
  22.                  * Print the banner to System.out.
  23.                  */
  24.                 CONSOLE,
  25.                 /**
  26.                  * Print the banner to the log file.
  27.                  */
  28.                 LOG
  29.         }
  30. }
复制代码
4、往下看到类的属性BANNER和SPRING_BOOT,也能辨认出是控制台打印的那些字符。
类里面只有一个方法printBanner,负责打印Banner字符。逻辑比较清晰,第一部分逐行打印BANNER形成图案;第二部分打印SpringBoot版本号,总长度由STRAP_LINE_SIZE控制。
SpringBootBanner完整代码
  1. /**
  2. * Default Banner implementation which writes the 'Spring' banner.
  3. */
  4. class SpringBootBanner implements Banner {
  5.         private static final String[] BANNER = { "", "  .   ____          _            __ _ _",
  6.                         " /\\\\ / ___'_ __ _ _(_)_ __  __ _ \\ \\ \\ \", "( ( )\\___ | '_ | '_| | '_ \\/ _` | \\ \\ \\ \",
  7.                         " \\\\/  ___)| |_)| | | | | || (_| |  ) ) ) )", "  '  |____| .__|_| |_|_| |_\\__, | / / / /",
  8.                         " =========|_|==============|___/=/_/_/_/" };
  9.         private static final String SPRING_BOOT = " :: Spring Boot :: ";
  10.         private static final int STRAP_LINE_SIZE = 42;
  11.         @Override
  12.         public void printBanner(Environment environment, Class<?> sourceClass, PrintStream printStream) {
  13.                 for (String line : BANNER) {
  14.                         printStream.println(line);
  15.                 }
  16.                 String version = SpringBootVersion.getVersion();
  17.                 version = (version != null) ? " (v" + version + ")" : "";
  18.                 StringBuilder padding = new StringBuilder();
  19.                 while (padding.length() < STRAP_LINE_SIZE - (version.length() + SPRING_BOOT.length())) {
  20.                         padding.append(" ");
  21.                 }
  22.                 printStream.println(AnsiOutput.toString(AnsiColor.GREEN, SPRING_BOOT, AnsiColor.DEFAULT, padding.toString(),
  23.                                 AnsiStyle.FAINT, version));
  24.                 printStream.println();
  25.         }
  26. }
复制代码
Banner核心控制类 SpringApplicationBannerPrinter

1、上节找到了负责存储和打印Banner字符的类SpringBootBanner,现在向调用链上方继续寻找,通过CTRL + B或者全局搜索可以发现SpringBootBanner在SpringApplicationBannerPrinter类中作为类变量,大概能猜测出这个SpringApplicationBannerPrinter类是Banner打印的核心控制器。


2、进入SpringApplicationBannerPrinter类,照例先看注释Class used by SpringApplication to print the application banner.,意思是当前类被SpringApplication用来打印banner。
这个SpringApplication好像有点眼熟,名字和我们SpringBoot项目的启动类有点相似,翻翻启动类的代码,想起我们就是通过SpringApplication的run方法启动项目,banner打印调用也是由SpringApplication控制的,后续会详细分析。(占坑,后续SpringBoot启动流程也会出一篇博客去探讨一下)

回归正题,继续从类的属性开始看,根据名字猜测大概含义,留待后续验证:

  • BANNER_LOCATION_PROPERTY:Spring配置,大概是banner文件的路径。
  • BANNER_IMAGE_LOCATION_PROPERTY:Spring配置,banner图片的路径(存疑,控制台难道能打印图片?)。
  • DEFAULT_BANNER_LOCATION = "banner.txt":取值是txt文件,猜测是banner文件的默认位置。
  • String[] IMAGE_EXTENSION = { "gif", "jpg", "png" }:取值是常见图片的后缀,结合第二个属性猜测是用来对banner图片类型做限制。
  • DEFAULT_BANNER = new SpringBootBanner():把上节分析的SpringBootBanner当做Banner默认实现类
  • ResourceLoader resourceLoader:ResourceLoader简单来说是Spring加载资源的统一抽象,由实现类提供具体逻辑。
    在Spring中读取xml配置文件加载应用上下文的ClassPathXmlApplicationContext,就是ResourceLoader的子类。
  • Banner fallbackBanner:翻译过来是回退banner,暂时猜不出作用,等待后续填坑。
3、往下看方法,只有两个非私有方法,都是print的重载方法,差别在于第三个参数,分别是Log logger和PrintStream out,代表这两个方法分别负责日志打印和控制台打印。
紧扣主题,先看负责控制台打印的方法。
  1.         Banner print(Environment environment, Class<?> sourceClass, PrintStream out) {
  2.                 Banner banner = getBanner(environment);
  3.                 banner.printBanner(environment, sourceClass, out);
  4.                 return new PrintedBanner(banner, sourceClass);
  5.         }
复制代码
代码很精简,第一行获取Banner类,第二行调用Banner的print方法打印banner图案,最后生成PrintedBanner并返回。
1. getBanner
  getBanner源码
  1. private Banner getBanner(Environment environment) {
  2.   Banners banners = new Banners();
  3.   banners.addIfNotNull(getImageBanner(environment));
  4.   banners.addIfNotNull(getTextBanner(environment));
  5.   if (banners.hasAtLeastOneBanner()) {
  6.    return banners;
  7.   }
  8.   if (this.fallbackBanner != null) {
  9.    return this.fallbackBanner;
  10.   }
  11.   return DEFAULT_BANNER;
  12. }
复制代码
查看getBanner方法,首先创建Banners,底层就是Banner数组,由于存在控制台、日志两种打印方式,使用此类方便批量处理。
Banners源码
  1. /**
  2. * {@link Banner} comprised of other {@link Banner Banners}.
  3. */
  4. private static class Banners implements Banner {
  5.     private final List<Banner> banners = new ArrayList<>();
  6.     void addIfNotNull(Banner banner) {
  7.         if (banner != null) {
  8.             this.banners.add(banner);
  9.         }
  10.     }
  11.     boolean hasAtLeastOneBanner() {
  12.         return !this.banners.isEmpty();
  13.     }
  14.     @Override
  15.     public void printBanner(Environment environment, Class<?> sourceClass, PrintStream out) {
  16.         for (Banner banner : this.banners) {
  17.             banner.printBanner(environment, sourceClass, out);
  18.     }
  19. }
复制代码
接着就是调用getImageBanner和getTextBanner方法获取Banner,如果Banner数组不为空则返回,否则检查fallbackBanner。
这个fallbackBanner光看名字看不出是什么,使用CTRL+B查看引用,发现是在SpringApplication#printBanner里注入进来的,如下图。

继续查找this.banner会发现,最终Banner只能通过SpringApplicationBuilder#banner注入。

SpringApplicationBuilder是通过Constructor(构造器)模式实现的SpringApplication构造器。
查看banner方法的注释,我们可以知道这里注入的Banner实例会在没有静态banner文件时使用
回过头来,fallbackBanner的坑填上了,它是在SpringApplicationBannerPrinter找不到txt文件或者图片作为banner素材的时候使用。
如果fallbackBanner也为空,则最终返回兜底方案-SpringBootBanner。
getBanner的结构分析完了,实际情况我们知道走的是兜底方案,也就是只要我们能让getImageBanner、getTextBanner或者fallbackBanner不为空,就能改变banner打印的图案。
带着这个想法,我们就去看看getImageBanner和getTextBanner是咋回事。
2、getImageBanner
查看源码,首先environment.getProperty读取配置spring.banner.image.location获取图片位置。
配置文件读取若为空则遍历图片后缀数组IMAGE_EXTENSION,采用"banner." + ext拼接方式得到图片相对路径,并尝试加载。加载成功后会生成ImageBanner并返回。
接收图片资源并处理打印的逻辑都封装在ImageBanner中,后续单独写一篇文章尝试分析图片打印逻辑。
按照我们的分析,只要在配置文件中添加spring.banner.image.location并赋值正确的图片路径,或者在resources目录下存放一张名字为“banner”、后缀是gif,jpg, png其中之一的图片,SpringApplicationBannerPrinter就会打印出来。
注: 为什么没加前缀classpath:也可以放在resources目录下,可以查看DefaultResourceLoader#getResource对于banner.jpg这种location的处理逻辑。
后续章节会有打印效果。
getImageBanner源码
  1. private Banner getImageBanner(Environment environment) {
  2.     String location = environment.getProperty(BANNER_IMAGE_LOCATION_PROPERTY);
  3.     if (StringUtils.hasLength(location)) {
  4.         Resource resource = this.resourceLoader.getResource(location);
  5.         return resource.exists() ? new ImageBanner(resource) : null;
  6.     }
  7.     for (String ext : IMAGE_EXTENSION) {
  8.         Resource resource = this.resourceLoader.getResource("banner." + ext);
  9.         if (resource.exists()) {
  10.             return new ImageBanner(resource);
  11.         }
  12.     }
  13.     return null;
  14. }
复制代码
3、getTextBanner
查看源码,同样是先从配置文件中读取banner文件的location并尝试加载资源,和getImageBanner不同的是,这里读取不到会使用默认值banner.txt。
加载资源后有一个Resource的限制条件!resource.getURL().toExternalForm().contains("liquibase-core"),这里不明白这个条件的含义,只查询到了Liquibase是一个用于跟踪、管理和应用数据库变化的开源工具。
资源校验通过后生成ResourceBanner并返回。
getTextBanner源码
  1. private Banner getTextBanner(Environment environment) {
  2.     String location = environment.getProperty(BANNER_LOCATION_PROPERTY, DEFAULT_BANNER_LOCATION);
  3.     Resource resource = this.resourceLoader.getResource(location);
  4.     try {
  5.         if (resource.exists() && !resource.getURL().toExternalForm().contains("liquibase-core")) {
  6.             return new ResourceBanner(resource);
  7.         }
  8.     }
  9.     catch (IOException ex) {
  10.         // Ignore
  11.     }
  12.     return null;
  13. }
复制代码
接下来进入ResourceBanner看下打印细节。
printBanner结构比较简单,第一部分设置banner字符集,优先读取配置spring.banner.charset,无配置则默认设置为UTF-8。
第二部分去解析banner字符,比如将${xxx}占位符解析成实际的值。
第三部分就是调用流打印输出。
ResourceBanner#printBanner
  1. @Override
  2. public void printBanner(Environment environment, Class<?> sourceClass, PrintStream out) {
  3.     try {
  4.         // 设置banner字符集
  5.         String banner = StreamUtils.copyToString(this.resource.getInputStream(),
  6.                 environment.getProperty("spring.banner.charset", Charset.class, StandardCharsets.UTF_8));
  7.         // 解析banner
  8.         for (PropertyResolver resolver : getPropertyResolvers(environment, sourceClass)) {
  9.             banner = resolver.resolvePlaceholders(banner);
  10.         }
  11.         out.println(banner);
  12.     }
  13.     catch (Exception ex) {
  14.         logger.warn(LogMessage.format("Banner not printable: %s (%s: '%s')", this.resource, ex.getClass(),
  15.                 ex.getMessage()), ex);
  16.     }
  17. }
复制代码
banner打印调用方-SpringApplication

上节看完SpringApplicationBannerPrinter,这节来寻找打印banner的调用方。
CTRL+B查看SpringApplicationBannerPrinter#print的引用,定位到了SpringApplication#printBanner。源码如下。
从整体结构来看,printBanner方法根据this.bannerMode取值不同,执行不同的打印策略:不打印、打印到日志、打印到控制台。
那么这个bannerMode是怎么设置的?查看初始化的代码,默认值是CONSOLE。

继续寻找,最终定位到了SpringApplicationBuilder#bannerMode,意味着bannerMode只能通过构造器进行注入。
继续寻找printBanner的调用方,定位到了SpringApplication#run(String...)。
上面有提到过,通常我们SpringBoot项目都是去调用SpringApplication#run(Class, String...)去启动项目,底层是通过new关键字创建SpringApplication对象,最后调用SpringApplication#run(String...)完成一系列的资源初始化。
所以这就可以解释大多数情况下,我们的SpringBoot项目启动时都会打印那个默认的“Spring”字符。
SpringApplication#printBanner源码
  1. private Banner printBanner(ConfigurableEnvironment environment) {
  2.     if (this.bannerMode == Banner.Mode.OFF) {
  3.         return null;
  4.     }
  5.     ResourceLoader resourceLoader = (this.resourceLoader != null) ? this.resourceLoader
  6.             : new DefaultResourceLoader(null);
  7.     SpringApplicationBannerPrinter bannerPrinter = new SpringApplicationBannerPrinter(resourceLoader, this.banner);
  8.     if (this.bannerMode == Mode.LOG) {
  9.         return bannerPrinter.print(environment, this.mainApplicationClass, logger);
  10.     }
  11.     return bannerPrinter.print(environment, this.mainApplicationClass, System.out);
  12. }
复制代码
如何修改项目启动的banner

修改banner打印策略

经上分析,banner打印策略包括控制台日志不打印
1. 隐式
默认策略是控制台,只需大多数情况一样,项目启动类通过SpringApplication.run(DistinctAppUserServiceApplication.class, args);启动,无需指定。
2. 显式注入
通过SpringApplicationBuilder构造器显式注入banner打印策略。
  1. @SpringBootApplication
  2. public class DemoApplication {
  3.     public static void main(String[] args) {
  4.         new SpringApplicationBuilder(DemoApplication.class)
  5.                 // Banner.Mode.LOG 打印到日志
  6.                 // Banner.Mode.OFF 不打印
  7.                 .bannerMode(Banner.Mode.CONSOLE)
  8.                 .run(args);
  9.     }
  10. }
复制代码
打印效果
打印到控制台

打印到日志:INFO级别

修改banner内容

文本

方式一:在src/main/resources下新建banner.txt,里面放入想要打印的内容即可。
方式二:修改配置文件
  1. spring:
  2.   banner:
  3.     location: file/bannerText.txt #文件位置 src/main/resources/file/bannerText.txt
复制代码
内容生成网站
文字转换成符号:http://patorjk.com/software/taag
                           http://life.chacuo.net/convertfont2char
图片转换成符号:https://www.bootschool.net/ascii-art
图片

和文本方式相同,但是图片类型有限制,只能是以下三种gif,、jpg、png。
方式一:在src/main/resources下新建banner.png,里面放入想要打印的内容即可。
方式二:修改配置文件
  1. spring:
  2.   banner:
  3.     image:
  4.       location: file/bannerImage.png #文件位置 src/main/resources/file/bannerImage.png
复制代码
打印效果

后语

本篇文章干货不多,主要记录探究问题的心路历程,锻炼文笔,若观看文章过程有任何不适,敬请斧正。

免责声明:如果侵犯了您的权益,请联系站长,我们会及时删除侵权内容,谢谢合作!

本帖子中包含更多资源

您需要 登录 才可以下载或查看,没有账号?立即注册

x
回复

使用道具 举报

0 个回复

倒序浏览

快速回复

您需要登录后才可以回帖 登录 or 立即注册

本版积分规则

汕尾海湾

论坛元老
这个人很懒什么都没写!
快速回复 返回顶部 返回列表