# 文章目录
<div class="table-of-contents"><ul><li><a href="#%E5%AE%9E%E9%AA%8C%E4%BB%A3%E7%A0%81">实验代码</a><ul><li><a href="#%E4%B8%A4%E7%A7%8Dbanner%E9%85%8D%E7%BD%AE%E6%96%87%E4%BB%B6">两种banner配置文件</a></li><li><a href="#%E9%85%8D%E7%BD%AE%E6%96%87%E4%BB%B6">配置文件</a></li><li><a href="#java%E4%BB%A3%E7%A0%81">Java代码</a></li><li><a href="#%E6%8E%A7%E5%88%B6%E5%8F%B0%E8%BE%93%E5%87%BA%E7%BB%93%E6%9E%9C">控制台输出结果</a></li></ul></li><li><a href="#%E8%B7%9F%E8%BF%9B%E6%BA%90%E7%A0%81">跟进源码</a><ul><li><a href="#%E5%8A%A0%E8%BD%BDbanner">加载banner</a></li></ul></li><li><a href="#%E6%89%93%E5%8D%B0banner">打印banner</a><ul><li><a href="#%E6%89%93%E5%8D%B0%E5%9B%BE%E7%89%87%E7%B1%BB%E5%9E%8B%E7%9A%84banner">打印图片类型的banner</a></li><li><a href="#%E6%89%93%E5%8D%B0%E6%96%87%E6%9C%AC%E7%B1%BB%E5%9E%8B%E7%9A%84banner">打印文本类型的banner</a></li></ul></li><li><a href="#%E8%A1%A5%E5%85%85">补充</a><ul><li><a href="#%E5%85%B3%E9%97%ADbanner%E6%89%93%E5%8D%B0">关闭banner打印</a></li></ul></li></ul></div>
# 前言
这一节来看看banner从读取到输出的流程,这一步在springboot启动流程中其实并不是很关键,你想更改banner的输出,随便百度找找都知道怎么做。不过既然看到的教程特地提到了这一点,那就不妨顺便记录下了,没准以后能在这里面抄些代码呢~
> SpringBoot Version:2.1.7/2.1.9
<br/>
# 实验代码
## 两种banner配置文件
`my_banner.txt`:
```txt
${spring.banner.location}
___ __ _______ ________ ___ ___ _______ ________ ___ ________ ________
|\ \ |\ \|\ ___ \ |\ ___ \ |\ \|\ \|\ ___ \ |\ __ \|\ \ |\ __ \|\ ____\
\ \ \ \ \ \ \ __/|\ \ \\ \ \ \ \ \ \ \ \ __/| \ \ \|\ /\ \ \ \ \ \|\ \ \ \___|
\ \ \ __\ \ \ \ \_|/_\ \ \\ \ \ __ \ \ \ \ \ \ \_|/__ \ \ __ \ \ \ \ \ \\\ \ \ \ ___
\ \ \|\__\_\ \ \ \_|\ \ \ \\ \ \|\ \\_\ \ \ \ \ \_|\ \ \ \ \|\ \ \ \____\ \ \\\ \ \ \|\ \
\ \____________\ \_______\ \__\\ \__\ \________\ \__\ \_______\ \ \_______\ \_______\ \_______\ \_______\
\|____________|\|_______|\|__| \|__|\|________|\|__|\|_______| \|_______|\|_______|\|_______|\|_______|
(POWERED BY HALO 1.2.0)
```
`图像文件favorite.jpg`

以上两文件位于resources目录下:

## 配置文件
还需要在application.properties中配置(如果文件名是banner.txt或banner.jpg/png等则忽略这一步):
```properties
spring.banner.location=my_banner.txt
spring.banner.image.location=favorite.jpg
```
## Java代码
`TestBannerApplication.java`
```java
import org.springframework.boot.ResourceBanner;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.context.annotation.Bean;
import org.springframework.core.io.ClassPathResource;
@SpringBootApplication
@MapperScan("com.wenjie.sb2.mapper")
public class Sb2Application {
public static void main(String[] args) {
SpringApplication springApplication = new SpringApplication(TestBannerApplication.class);
springApplication.setBanner(new ResourceBanner(new ClassPathResource("my_banner.txt")));
springApplication.run(args);
}
```
- 如果前两banner文件都不存在,则使用这里配置的文件路径。
## 控制台输出结果

# 跟进源码
## 加载banner
首先来到我们熟悉的SpringApplication#run方法:
`org.springframework.boot.SpringApplication#run(java.lang.String...)`
```java
StopWatch stopWatch = new StopWatch();
stopWatch.start();
ConfigurableApplicationContext context = null;
Collection<SpringBootExceptionReporter> exceptionReporters = new ArrayList<>();
configureHeadlessProperty();
SpringApplicationRunListeners listeners = getRunListeners(args);
listeners.starting();
try {
ApplicationArguments applicationArguments = new DefaultApplicationArguments(args);
ConfigurableEnvironment environment = prepareEnvironment(listeners, applicationArguments);
configureIgnoreBeanInfo(environment);
Banner printedBanner = StopWatch stopWatch = new StopWatch();
stopWatch.start();
ConfigurableApplicationContext context = null;
Collection<SpringBootExceptionReporter> exceptionReporters = new ArrayList<>();
configureHeadlessProperty();
SpringApplicationRunListeners listeners = getRunListeners(args);
listeners.starting();
try {
ApplicationArguments applicationArguments = new DefaultApplicationArguments(args);
ConfigurableEnvironment environment = prepareEnvironment(listeners, applicationArguments);
configureIgnoreBeanInfo(environment);
Banner printedBanner = printBanner(environment);(environment);
...(略)
```
- 执行完`Banner printedBanner = printBanner(environment);(environment);`这一行就能看到控制台打印了。
我们跟进printBanner方法:
`org.springframework.boot.SpringApplication#printBanner`
```java
private Banner printBanner(ConfigurableEnvironment environment) {
// 如果spring.main.banner-mode设置为off,则不打印banner
if (this.bannerMode == Banner.Mode.OFF) {
return null;
}
// 获取资源加载器
ResourceLoader resourceLoader = (this.resourceLoader != null) ? this.resourceLoader
: new DefaultResourceLoader(getClassLoader());
// 构造打印器
SpringApplicationBannerPrinter bannerPrinter = new SpringApplicationBannerPrinter(resourceLoader, this.banner);
// 三个级别,分别是LOG,CONSOLE,OFF,可以通过spring.main.banner-mode配置
// 默认是CONSOLE级别
if (this.bannerMode == Mode.LOG) {
return bannerPrinter.print(environment, this.mainApplicationClass, logger);
}
// 实验代码跟进这里。
return bannerPrinter.print(environment, this.mainApplicationClass, System.out);
}
```
跟进print方法,此处【坐标1】<span id="tag1"></span>:
`org.springframework.boot.SpringApplicationBannerPrinter#print(org.springframework.core.env.Environment, java.lang.Class<?>, java.io.PrintStream)`
```java
public Banner print(Environment environment, Class<?> sourceClass, PrintStream out) {
// 获取banner逻辑,不同配置方式会返回不同banner实现
Banner banner = getBanner(environment);
// printBanner是个抽象方法,不同banner实现的具体实现各有不同
// 打印banner逻辑
banner.printBanner(environment, sourceClass, out);
return new PrintedBanner(banner, sourceClass);
}
```
- 关于printBanner的各种实现,这里就不一一分析了,逻辑大同小异,本篇博客只记录实验代码的情况,有兴趣的可以自己改配置跟进跟进。
这里先跟进getBanner方法,此处【坐标2】<span id="tag2"></span>:
`org.springframework.boot.SpringApplicationBannerPrinter#getBanner`
```java
private Banner getBanner(Environment environment) {
// 内部就是List<Banner>,以及一个添加元素方法。
Banners banners = new Banners();
// 获取图片格式
banners.addIfNotNull(getImageBanner(environment));
// 获取文本格式
banners.addIfNotNull(getTextBanner(environment));
if (banners.hasAtLeastOneBanner()) {
return banners;
}
// fallbackBanner就是启动函数中配置的资源路径
if (this.fallbackBanner != null) {
return this.fallbackBanner;
}
// springboot默认的banner
return DEFAULT_BANNER;
}
```
先跟进ImageBanner方法:
`org.springframework.boot.SpringApplicationBannerPrinter#getImageBanner`
```java
private Banner getImageBanner(Environment environment) {
// BANNER_IMAGE_LOCATION_PROPERTY = "spring.banner.image.location"
// 获取到配置的资源路径
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;
}
// 如果用户没有自定义配置,则扫描banner.gif/jpg/png
for (String ext : IMAGE_EXTENSION) {
Resource resource = this.resourceLoader.getResource("banner." + ext);
if (resource.exists()) {
return new ImageBanner(resource);
}
}
return null;
}
```
在尝试获取图片banner资源后,还会尝试获取文本banner资源,回到[【坐标2】](#tag2)的getBanner方法,跟进getTextBanner方法:
`org.springframework.boot.SpringApplicationBannerPrinter#getTextBanner`
```java
private Banner getTextBanner(Environment environment) {
// BANNER_LOCATION_PROPERTY = "spring.banner.location"
// DEFAULT_BANNER_LOCATION = "banner.txt"
String location = environment.getProperty(BANNER_LOCATION_PROPERTY, DEFAULT_BANNER_LOCATION);
Resource resource = this.resourceLoader.getResource(location);
if (resource.exists()) {
return new ResourceBanner(resource);
}
return null;
}
```
- 尝试获取spring.banner.location配置路径,没有配置则尝试获取banner.txt,如果banner.txt都没有则返回null。
好了上面已经获取到banner的资源路径并加载资源了,下面看看打印banner的代码逻辑。
# 打印banner
## 打印图片类型的banner
回到[【坐标1】](#tag1)的print方法,我们跟进printBanner方法:
`org.springframework.boot.SpringApplicationBannerPrinter.Banners#printBanner`
```java
@Override
public void printBanner(Environment environment, Class<?> sourceClass, PrintStream out) {
for (Banner banner : this.banners) {
// printBanner是抽象方法,不同banner实现不同
banner.printBanner(environment, sourceClass, out);
}
}
```
结合实验代码的熟悉怒,集合中第一个是图片类型的banner,跟进printBanner方法:
`org.springframework.boot.ImageBanner#printBanner(org.springframework.core.env.Environment, java.lang.Class<?>, java.io.PrintStream)`
```java
@Override
public void printBanner(Environment environment, Class<?> sourceClass, PrintStream out) {
String headless = System.getProperty("java.awt.headless");
try {
// 解除对硬件的依赖
System.setProperty("java.awt.headless", "true");
printBanner(environment, out);
}
catch (Throwable ex) {
logger.warn("Image banner not printable: " + this.image + " (" + ex.getClass() + ": '" + ex.getMessage()
+ "')");
logger.debug("Image banner printing failure", ex);
}
finally {
if (headless == null) {
System.clearProperty("java.awt.headless");
}
else {
System.setProperty("java.awt.headless", headless);
}
}
}
```
继续跟进上面的printBanner方法:
`org.springframework.boot.ImageBanner#printBanner(org.springframework.core.env.Environment, java.io.PrintStream)`
```java
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);
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, out);
sleep(frames[i].getDelayTime());
}
}
```
- `printBanner(frames[i].getImage(), margin, invert, out)`之前的代码大概是在调整图片分辨率等操作,跟java的绘画组件有关,我对这块不是那么熟悉,有兴趣的请自行补充吧。
再跟进getProperty看看会不会有新发现:
`org.springframework.boot.ImageBanner#getProperty`
```java
private <T> T getProperty(Environment environment, String name, Class<T> targetType, T defaultValue) {
return environment.getProperty(PROPERTY_PREFIX + name, targetType, defaultValue);
}
```
- 不难看出代码逻辑是在加载配置。
看看PROPERTY_PREFIX是什么:
```java
private static final String PROPERTY_PREFIX = "spring.banner.image.";
```
- 这意味着我们可以在配置文件中配置图片的宽、高、边距、是否反转等,如配置`spring.banner.image.width=100`,就是设置图片宽度100。
返回到上面的`org.springframework.boot.ImageBanner#printBanner(org.springframework.core.env.Environment, java.io.PrintStream)`,继续跟进里面的printBanner方法,到这里基本就是jdk的原生逻辑了:
`org.springframework.boot.ImageBanner#printBanner(java.awt.image.BufferedImage, int, boolean, java.io.PrintStream)`
```java
private void printBanner(BufferedImage image, int margin, boolean invert, PrintStream out) {
AnsiElement background = invert ? AnsiBackground.BLACK : AnsiBackground.DEFAULT;
out.print(AnsiOutput.encode(AnsiColor.DEFAULT));
out.print(AnsiOutput.encode(background));
out.println();
out.println();
AnsiColor lastColor = AnsiColor.DEFAULT;
for (int y = 0; y < image.getHeight(); y++) {
// 调整打印缩进
for (int i = 0; i < margin; i++) {
out.print(" ");
}
// 从图片中获取色块,用于调整打印文字的颜色。
for (int x = 0; x < image.getWidth(); x++) {
Color color = new Color(image.getRGB(x, y), false);
AnsiColor ansiColor = AnsiColors.getClosest(color);
if (ansiColor != lastColor) {
out.print(AnsiOutput.encode(ansiColor));
lastColor = ansiColor;
}
out.print(getAsciiPixel(color, invert));
}
out.println();
}
out.print(AnsiOutput.encode(AnsiColor.DEFAULT));
out.print(AnsiOutput.encode(AnsiBackground.DEFAULT));
out.println();
}
```
上面的代码执行完后,就可以见到控制台如下输出了:

## 打印文本类型的banner
打印完图片类型banner后,返回到`org.springframework.boot.SpringApplicationBannerPrinter.Banners#printBanner`中:
``
```java
@Override
public void printBanner(Environment environment, Class<?> sourceClass, PrintStream out) {
for (Banner banner : this.banners) {
// 不用banner的printBanner实现不一样
banner.printBanner(environment, sourceClass, out);
}
}
```
下一个取到的就是文本类型的banner,跟进printBanner方法:
`org.springframework.boot.ResourceBanner#printBanner`
```java
@Override
public void printBanner(Environment environment, Class<?> sourceClass, PrintStream out) {
try {
// 获取到实验代码的my_banner.txt的文本内容
String banner = StreamUtils.copyToString(this.resource.getInputStream(),
environment.getProperty("spring.banner.charset", Charset.class, StandardCharsets.UTF_8));
// 解析特殊定义符号
// 如my_banner.txt中设置的${spring.banner.location},将它解析成对应值=my_banner.txt
for (PropertyResolver resolver : getPropertyResolvers(environment, sourceClass)) {
banner = resolver.resolvePlaceholders(banner);
}
// 控制台打印
out.println(banner);
}
catch (Exception ex) {
logger.warn(
"Banner not printable: " + this.resource + " (" + ex.getClass() + ": '" + ex.getMessage() + "')",
ex);
}
}
```
执行完`out.println(banner);`之后就能看到控制台打印如下:

好了,现在打印用户自定义banner的流程就走完了,如果你有兴趣还可以跟一下默认情况下是如何加载、打印banner的,由于大致逻辑都差不多,我这里就不再赘述了。
# 补充
## 关闭banner打印
只要在application配置文件中配置如下,就能关闭banner的打印了:
```java
spring.main.banner-mode=off
```

【SpringBoot源码】banner从读取到输出流程