文章目录
前言
这一节来看看banner从读取到输出的流程,这一步在springboot启动流程中其实并不是很关键,你想更改banner的输出,随便百度找找都知道怎么做。不过既然看到的教程特地提到了这一点,那就不妨顺便记录下了,没准以后能在这里面抄些代码呢~
SpringBoot Version:2.1.7/2.1.9
实验代码
两种banner配置文件
my_banner.txt
:
${spring.banner.location}
___ __ _______ ________ ___ ___ _______ ________ ___ ________ ________
|\ \ |\ \|\ ___ \ |\ ___ \ |\ \|\ \|\ ___ \ |\ __ \|\ \ |\ __ \|\ ____\
\ \ \ \ \ \ \ __/|\ \ \\ \ \ \ \ \ \ \ \ __/| \ \ \|\ /\ \ \ \ \ \|\ \ \ \___|
\ \ \ __\ \ \ \ \_|/_\ \ \\ \ \ __ \ \ \ \ \ \ \_|/__ \ \ __ \ \ \ \ \ \\\ \ \ \ ___
\ \ \|\__\_\ \ \ \_|\ \ \ \\ \ \|\ \\_\ \ \ \ \ \_|\ \ \ \ \|\ \ \ \____\ \ \\\ \ \ \|\ \
\ \____________\ \_______\ \__\\ \__\ \________\ \__\ \_______\ \ \_______\ \_______\ \_______\ \_______\
\|____________|\|_______|\|__| \|__|\|________|\|__|\|_______| \|_______|\|_______|\|_______|\|_______|
(POWERED BY HALO 1.2.0)
图像文件favorite.jpg
以上两文件位于resources目录下:
配置文件
还需要在application.properties中配置(如果文件名是banner.txt或banner.jpg/png等则忽略这一步):
spring.banner.location=my_banner.txt
spring.banner.image.location=favorite.jpg
Java代码
TestBannerApplication.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...)
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
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】:
org.springframework.boot.SpringApplicationBannerPrinter#print(org.springframework.core.env.Environment, java.lang.Class<?>, java.io.PrintStream)
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】:
org.springframework.boot.SpringApplicationBannerPrinter#getBanner
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
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】的getBanner方法,跟进getTextBanner方法:
org.springframework.boot.SpringApplicationBannerPrinter#getTextBanner
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】的print方法,我们跟进printBanner方法:
org.springframework.boot.SpringApplicationBannerPrinter.Banners#printBanner
@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)
@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)
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
private <T> T getProperty(Environment environment, String name, Class<T> targetType, T defaultValue) {
return environment.getProperty(PROPERTY_PREFIX + name, targetType, defaultValue);
}
- 不难看出代码逻辑是在加载配置。
看看PROPERTY_PREFIX是什么:
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)
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
中:
``
@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
@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的打印了:
spring.main.banner-mode=off