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

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

Scroll Down

文章目录

前言

这一节来看看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
favorite.jpg

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

配置文件

还需要在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文件都不存在,则使用这里配置的文件路径。

控制台输出结果

输出结果.png

跟进源码

加载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();
	}

上面的代码执行完后,就可以见到控制台如下输出了:
控制台输出.png

打印文本类型的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);之后就能看到控制台打印如下:
控制台输出.png

好了,现在打印用户自定义banner的流程就走完了,如果你有兴趣还可以跟一下默认情况下是如何加载、打印banner的,由于大致逻辑都差不多,我这里就不再赘述了。

补充

关闭banner打印

只要在application配置文件中配置如下,就能关闭banner的打印了:

spring.main.banner-mode=off