草庐IT

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

midas's blog 2023-03-28 原文

前言

每次启动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源码
/**
 * Interface class for writing a banner programmatically.
 */
@FunctionalInterface
public interface Banner {

	/**
	 * Print the banner to the specified print stream.
	 * @param environment the spring environment
	 * @param sourceClass the source class for the application
	 * @param out the output print stream
	 */
	void printBanner(Environment environment, Class<?> sourceClass, PrintStream out);

	/**
	 * An enumeration of possible values for configuring the Banner.
	 */
	enum Mode {
		/**
		 * Disable printing of the banner.
		 */
		OFF,

		/**
		 * Print the banner to System.out.
		 */
		CONSOLE,

		/**
		 * Print the banner to the log file.
		 */
		LOG
	}

}

4、往下看到类的属性BANNERSPRING_BOOT,也能辨认出是控制台打印的那些字符。

类里面只有一个方法printBanner,负责打印Banner字符。逻辑比较清晰,第一部分逐行打印BANNER形成图案;第二部分打印SpringBoot版本号,总长度由STRAP_LINE_SIZE控制。

SpringBootBanner完整代码
/**
 * Default Banner implementation which writes the 'Spring' banner.
 */
class SpringBootBanner implements Banner {

	private static final String[] BANNER = { "", "  .   ____          _            __ _ _",
			" /\\\\ / ___'_ __ _ _(_)_ __  __ _ \\ \\ \\ \\", "( ( )\\___ | '_ | '_| | '_ \\/ _` | \\ \\ \\ \\",
			" \\\\/  ___)| |_)| | | | | || (_| |  ) ) ) )", "  '  |____| .__|_| |_|_| |_\\__, | / / / /",
			" =========|_|==============|___/=/_/_/_/" };

	private static final String SPRING_BOOT = " :: Spring Boot :: ";

	private static final int STRAP_LINE_SIZE = 42;

	@Override
	public void printBanner(Environment environment, Class<?> sourceClass, PrintStream printStream) {
		for (String line : BANNER) {
			printStream.println(line);
		}
		String version = SpringBootVersion.getVersion();
		version = (version != null) ? " (v" + version + ")" : "";
		StringBuilder padding = new StringBuilder();
		while (padding.length() < STRAP_LINE_SIZE - (version.length() + SPRING_BOOT.length())) {
			padding.append(" ");
		}

		printStream.println(AnsiOutput.toString(AnsiColor.GREEN, SPRING_BOOT, AnsiColor.DEFAULT, padding.toString(),
				AnsiStyle.FAINT, version));
		printStream.println();
	}

}

Banner核心控制类 SpringApplicationBannerPrinter

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


2、进入SpringApplicationBannerPrinter类,照例先看注释Class used by SpringApplication to print the application banner.,意思是当前类被SpringApplication用来打印banner。

这个SpringApplication好像有点眼熟,名字和我们SpringBoot项目的启动类有点相似,翻翻启动类的代码,想起我们就是通过SpringApplicationrun方法启动项目,banner打印调用也是由SpringApplication控制的,后续会详细分析。(占坑,后续SpringBoot启动流程也会出一篇博客去探讨一下)

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

  1. BANNER_LOCATION_PROPERTY:Spring配置,大概是banner文件的路径。
  2. BANNER_IMAGE_LOCATION_PROPERTY:Spring配置,banner图片的路径(存疑,控制台难道能打印图片?)。
  3. DEFAULT_BANNER_LOCATION = "banner.txt":取值是txt文件,猜测是banner文件的默认位置。
  4. String[] IMAGE_EXTENSION = { "gif", "jpg", "png" }:取值是常见图片的后缀,结合第二个属性猜测是用来对banner图片类型做限制。
  5. DEFAULT_BANNER = new SpringBootBanner():把上节分析的SpringBootBanner当做Banner默认实现类
  6. ResourceLoader resourceLoaderResourceLoader简单来说是Spring加载资源的统一抽象,由实现类提供具体逻辑。
    在Spring中读取xml配置文件加载应用上下文的ClassPathXmlApplicationContext,就是ResourceLoader的子类。
  7. Banner fallbackBanner:翻译过来是回退banner,暂时猜不出作用,等待后续填坑。

3、往下看方法,只有两个非私有方法,都是print的重载方法,差别在于第三个参数,分别是Log loggerPrintStream out,代表这两个方法分别负责日志打印和控制台打印。

紧扣主题,先看负责控制台打印的方法。

	Banner print(Environment environment, Class<?> sourceClass, PrintStream out) {
		Banner banner = getBanner(environment);
		banner.printBanner(environment, sourceClass, out);
		return new PrintedBanner(banner, sourceClass);
	}

代码很精简,第一行获取Banner类,第二行调用Bannerprint方法打印banner图案,最后生成PrintedBanner并返回。


1. getBanner

getBanner源码
 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;
 }

查看getBanner方法,首先创建Banners,底层就是Banner数组,由于存在控制台、日志两种打印方式,使用此类方便批量处理。

Banners源码
/**
 * {@link Banner} comprised of other {@link Banner Banners}.
 */
private static class Banners implements Banner {

    private final List<Banner> banners = new ArrayList<>();

    void addIfNotNull(Banner banner) {
        if (banner != null) {
            this.banners.add(banner);
        }
    }

    boolean hasAtLeastOneBanner() {
        return !this.banners.isEmpty();
    }

    @Override
    public void printBanner(Environment environment, Class<?> sourceClass, PrintStream out) {
        for (Banner banner : this.banners) {
            banner.printBanner(environment, sourceClass, out);
    }

}

接着就是调用getImageBannergetTextBanner方法获取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的结构分析完了,实际情况我们知道走的是兜底方案,也就是只要我们能让getImageBannergetTextBanner或者fallbackBanner不为空,就能改变banner打印的图案。
带着这个想法,我们就去看看getImageBannergetTextBanner是咋回事。


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源码
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;
}

3、getTextBanner
查看源码,同样是先从配置文件中读取banner文件的location并尝试加载资源,和getImageBanner不同的是,这里读取不到会使用默认值banner.txt

加载资源后有一个Resource的限制条件!resource.getURL().toExternalForm().contains("liquibase-core"),这里不明白这个条件的含义,只查询到了Liquibase是一个用于跟踪、管理和应用数据库变化的开源工具。

资源校验通过后生成ResourceBanner并返回。

getTextBanner源码
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看下打印细节。
printBanner结构比较简单,第一部分设置banner字符集,优先读取配置spring.banner.charset,无配置则默认设置为UTF-8
第二部分去解析banner字符,比如将${xxx}占位符解析成实际的值。
第三部分就是调用流打印输出。

ResourceBanner#printBanner
@Override
public void printBanner(Environment environment, Class<?> sourceClass, PrintStream out) {
    try {
	// 设置banner字符集
        String banner = StreamUtils.copyToString(this.resource.getInputStream(),
                environment.getProperty("spring.banner.charset", Charset.class, StandardCharsets.UTF_8));

	// 解析banner
        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打印调用方-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源码
private Banner printBanner(ConfigurableEnvironment environment) {
    if (this.bannerMode == Banner.Mode.OFF) {
        return null;
    }
    ResourceLoader resourceLoader = (this.resourceLoader != null) ? this.resourceLoader
            : new DefaultResourceLoader(null);
    SpringApplicationBannerPrinter bannerPrinter = new SpringApplicationBannerPrinter(resourceLoader, this.banner);
    if (this.bannerMode == Mode.LOG) {
        return bannerPrinter.print(environment, this.mainApplicationClass, logger);
    }
    return bannerPrinter.print(environment, this.mainApplicationClass, System.out);
}

如何修改项目启动的banner

修改banner打印策略

经上分析,banner打印策略包括控制台日志不打印

1. 隐式
默认策略是控制台,只需大多数情况一样,项目启动类通过SpringApplication.run(DistinctAppUserServiceApplication.class, args);启动,无需指定。

2. 显式注入
通过SpringApplicationBuilder构造器显式注入banner打印策略。

@SpringBootApplication
public class DemoApplication {
    public static void main(String[] args) {
        new SpringApplicationBuilder(DemoApplication.class)
		// Banner.Mode.LOG 打印到日志
		// Banner.Mode.OFF 不打印
                .bannerMode(Banner.Mode.CONSOLE)
                .run(args);
    }
}

打印效果
打印到控制台

打印到日志:INFO级别

修改banner内容

文本

方式一:在src/main/resources下新建banner.txt,里面放入想要打印的内容即可。

方式二:修改配置文件

spring:
  banner:
    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,里面放入想要打印的内容即可。

方式二:修改配置文件

spring:
  banner:
    image:
      location: file/bannerImage.png #文件位置 src/main/resources/file/bannerImage.png

打印效果

后语

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

有关SpringBoot启动控制台的banner是怎么回事的更多相关文章

  1. Ruby Readline 在向上箭头上使控制台崩溃 - 2

    当我在Rails控制台中按向上或向左箭头时,出现此错误:irb(main):001:0>/Users/me/.rvm/gems/ruby-2.0.0-p247/gems/rb-readline-0.4.2/lib/rbreadline.rb:4269:in`blockin_rl_dispatch_subseq':invalidbytesequenceinUTF-8(ArgumentError)我使用rvm来管理我的ruby​​安装。我正在使用=>ruby-2.0.0-p247[x86_64]我使用bundle来管理我的gem,并且我有rb-readline(0.4.2)(人们推荐的最少

  2. ruby-on-rails - 带 Spring 锁的 Rails 4 控制台 - 2

    我正在使用Ruby2.1.1和Rails4.1.0.rc1。当执行railsc时,它被锁定了。使用Ctrl-C停止,我得到以下错误日志:~/.rvm/gems/ruby-2.1.1/gems/spring-1.1.2/lib/spring/client/run.rb:47:in`gets':Interruptfrom~/.rvm/gems/ruby-2.1.1/gems/spring-1.1.2/lib/spring/client/run.rb:47:in`verify_server_version'from~/.rvm/gems/ruby-2.1.1/gems/spring-1.1.

  3. ruby-on-rails - 启动 Rails 服务器时 ImageMagick 的警告 - 2

    最近,当我启动我的Rails服务器时,我收到了一长串警告。虽然它不影响我的应用程序,但我想知道如何解决这些警告。我的估计是imagemagick以某种方式被调用了两次?当我在警告前后检查我的git日志时。我想知道如何解决这个问题。-bcrypt-ruby(3.1.2)-better_errors(1.0.1)+bcrypt(3.1.7)+bcrypt-ruby(3.1.5)-bcrypt(>=3.1.3)+better_errors(1.1.0)bcrypt和imagemagick有关系吗?/Users/rbchris/.rbenv/versions/2.0.0-p247/lib/ru

  4. ruby-on-rails - openshift 上的 rails 控制台 - 2

    我将我的Rails应用程序部署到OpenShift,它运行良好,但我无法在生产服务器上运行“Rails控制台”。它给了我这个错误。我该如何解决这个问题?我尝试更新ruby​​gems,但它也给出了权限被拒绝的错误,我也无法做到。railsc错误:Warning:You'reusingRubygems1.8.24withSpring.UpgradetoatleastRubygems2.1.0andrun`gempristine--all`forbetterstartupperformance./opt/rh/ruby193/root/usr/share/rubygems/rubygems

  5. Ruby——嵌套类和子类是一回事吗? - 2

    下面例子中的Nested和Child有什么区别?是否只是同一事物的不同语法?classParentclassNested...endendclassChild 最佳答案 不,它们是不同的。嵌套:Computer之外的“Processor”类只能作为Computer::Processor访问。嵌套为内部类(namespace)提供上下文。对于ruby​​解释器Computer和Computer::Processor只是两个独立的类。classComputerclassProcessor#Tocreateanobjectforthisc

  6. ruby - Ruby 中的隐式返回值是怎么回事? - 2

    所以我开始关注ruby​​,很多东西看起来不错,但我对隐式return语句很反感。我理解默认情况下让所有内容返回self或nil但不是语句的最后一个值。对我来说,它看起来非常脆弱(尤其是)如果你正在使用一个不打算返回某些东西的方法(尤其是一个改变状态/破坏性方法的函数!),其他人可能最终依赖于一个返回对方法的目的并不重要,并且有很大的改变机会。隐式返回有什么意义?有没有办法让事情变得更简单?总是有返回以防止隐含返回被认为是好的做法吗?我是不是太担心这个了?附言当人们想要从方法中返回特定的东西时,他们是否经常使用隐式返回,这不是让你组中的其他人更容易破坏彼此的代码吗?当然,记录一切并给出

  7. ruby - 怎么来的(a_method || :other) returns :other only when assigning to a var called a_method? - 2

    给定以下方法:defsome_method:valueend以下语句按我的预期工作:some_method||:other#=>:valuex=some_method||:other#=>:value但是下面语句的行为让我感到困惑:some_method=some_method||:other#=>:other它按预期创建了一个名为some_method的局部变量,随后对some_method的调用返回该局部变量的值。但为什么它分配:other而不是:value呢?我知道这可能不是一件明智的事情,并且可以看出它可能有多么模棱两可,但我认为应该在考虑作业之前评估作业的右侧...我已经在R

  8. ruby-on-rails - 我该怎么办 :remote location validation with CarrierWave? - 2

    我在我的Rails3示例应用程序上使用CarrierWave。我想验证远程位置上传,因此当用户提交无效URL(空白或非图像)时,我不会收到标准错误异常:CarrierWave::DownloadErrorinImageController#createtryingtodownloadafilewhichisnotservedoverHTTP这是我的模型:classPaintingtrue,:length=>{:minimum=>5,:maximum=>100}validates:image,:presence=>trueend这是我的Controller:classPaintingsC

  9. 电脑0x0000001A蓝屏错误怎么U盘重装系统教学 - 2

      电脑0x0000001A蓝屏错误怎么U盘重装系统教学分享。有用户电脑开机之后遇到了系统蓝屏的情况。系统蓝屏问题很多时候都是系统bug,只有通过重装系统来进行解决。那么蓝屏问题如何通过U盘重装新系统来解决呢?来看看以下的详细操作方法教学吧。  准备工作:  1、U盘一个(尽量使用8G以上的U盘)。  2、一台正常联网可使用的电脑。  3、ghost或ISO系统镜像文件(Win10系统下载_Win10专业版_windows10正式版下载-系统之家)。  4、在本页面下载U盘启动盘制作工具:系统之家U盘启动工具。  U盘启动盘制作步骤:  注意:制作期间,U盘会被格式化,因此U盘中的重要文件请注

  10. UE4 源码阅读:从引擎启动到Receive Begin Play - 2

    一、引擎主循环UE版本:4.27一、引擎主循环的位置:Launch.cpp:GuardedMain函数二、、GuardedMain函数执行逻辑:1、EnginePreInit:加载大多数模块int32ErrorLevel=EnginePreInit(CmdLine);PreInit模块加载顺序:模块加载过程:(1)注册模块中定义的UObject,同时为每个类构造一个类默认对象(CDO,记录类的默认状态,作为模板用于子类实例创建)(2)调用模块的StartUpModule方法2、FEngineLoop::Init()1、检查Engine的配置文件找出使用了哪一个GameEngine类(UGame

随机推荐