Spring boot 自定义Banner

前言

最近看到各类框架每次加载都会打印各类各式各样的banner,发现打印banner其实很简单,想着Spring boot其实也可以定义banner,从源码的角度发现banner其实很简单。

1. Spring boot banner源码解析

1.1 demo

构建一个Spring boot demo

    
        
            org.springframework.boot
            spring-boot-starter-web
            2.3.5.RELEASE
        
        
            org.springframework.boot
            spring-boot-starter-test
            2.3.5.RELEASE
            test
        

    

加入Main类

@SpringBootApplication
public class BootMain {
    public static void main(String[] args) {
        SpringApplication.run(BootMain.class, args);
    }
}

 

1.2 Spring boot banner源码解析

banner的打印在Spring boot的run的过程中,在Spring容器创建之前打印的banner。

public ConfigurableApplicationContext run(String... args) {
		StopWatch stopWatch = new StopWatch();
		stopWatch.start();
		ConfigurableApplicationContext context = null;
		Collection exceptionReporters = new ArrayList<>();
		configureHeadlessProperty();
		SpringApplicationRunListeners listeners = getRunListeners(args);
		listeners.starting();
		try {
			ApplicationArguments applicationArguments = new DefaultApplicationArguments(args);
			ConfigurableEnvironment environment = prepareEnvironment(listeners, applicationArguments);
			configureIgnoreBeanInfo(environment);
            //可以看到是在Spring的Context创建之前
			Banner printedBanner = printBanner(environment);
			context = createApplicationContext();
			exceptionReporters = getSpringFactoriesInstances(SpringBootExceptionReporter.class,
					new Class[] { ConfigurableApplicationContext.class }, context);
			prepareContext(context, environment, listeners, applicationArguments, printedBanner);
			refreshContext(context);
			afterRefresh(context, applicationArguments);
			stopWatch.stop();
			if (this.logStartupInfo) {
				new StartupInfoLogger(this.mainApplicationClass).logStarted(getApplicationLog(), stopWatch);
			}
			listeners.started(context);
			callRunners(context, applicationArguments);
		}
		catch (Throwable ex) {
			handleRunFailure(context, ex, exceptionReporters, listeners);
			throw new IllegalStateException(ex);
		}

		try {
			listeners.running(context);
		}
		catch (Throwable ex) {
			handleRunFailure(context, ex, exceptionReporters, null);
			throw new IllegalStateException(ex);
		}
		return context;
	}

进一步跟踪

    private Banner printBanner(ConfigurableEnvironment environment) {
        //这里是关闭banner,
		if (this.bannerMode == Banner.Mode.OFF) {
			return null;
		}
        //资源加载器
		ResourceLoader resourceLoader = (this.resourceLoader != null) ? this.resourceLoader
				: new DefaultResourceLoader(null);
        //banner打印器
		SpringApplicationBannerPrinter bannerPrinter = new SpringApplicationBannerPrinter(resourceLoader, this.banner);
        //日志模式打印
		if (this.bannerMode == Mode.LOG) {
			return bannerPrinter.print(environment, this.mainApplicationClass, logger);
		}
        //console模式打印
		return bannerPrinter.print(environment, this.mainApplicationClass, System.out);
	}

首先是可以关闭banner的,在org.springframework.boot.SpringApplication的属性中,默认是CONSOLE,表示是console打印banner,可以通过set方法设置OFF关闭

private Banner.Mode bannerMode = Banner.Mode.CONSOLE; 

有3种模式

    enum Mode {

		/**
		 * Disable printing of the banner.
		 */
		OFF,

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

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

	}

接着定义了资源加载器,定义了banner打印器,关键是最后一步打印,可以看出只是打印的对象不一样,一个日志输出,一个System.out

    Banner print(Environment environment, Class sourceClass, PrintStream out) {
        //通过环境获取banner
		Banner banner = getBanner(environment);
        //打印
		banner.printBanner(environment, sourceClass, out);
		return new PrintedBanner(banner, sourceClass);
	}

首先拿到banner

getBanner(environment) 

Spring boot 自定义Banner_第1张图片

环境信息,里面存储了Spring存储的properties yaml等propertySource,还有解析占位符的解析器 

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

可以看到Banners,说明可以连续打印很多banner

    private static class Banners implements Banner {

		private final List 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);
			}
		}

	}
banners.addIfNotNull(getImageBanner(environment));
banners.addIfNotNull(getTextBanner(environment));

这个表示Spring boot希望我们自定义Spring boot的banner的方式,两者大同小异

 

1.3 图像方式

    static final String BANNER_IMAGE_LOCATION_PROPERTY = "spring.banner.image.location";
    static final String[] IMAGE_EXTENSION = { "gif", "jpg", "png" };
    private Banner getImageBanner(Environment environment) {
        //Spring配置文件获取,看看自定义没有
		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.jpg之类的;默认从classpath获取,不过文件名被定义了
		for (String ext : IMAGE_EXTENSION) {
			Resource resource = this.resourceLoader.getResource("banner." + ext);
			if (resource.exists()) {
				return new ImageBanner(resource);
			}
		}
		return null;
	}

getSource,前面默认的

new DefaultResourceLoader(null)
    public Resource getResource(String location) {
		Assert.notNull(location, "Location must not be null");

		for (ProtocolResolver protocolResolver : getProtocolResolvers()) {
			Resource resource = protocolResolver.resolve(location, this);
			if (resource != null) {
				return resource;
			}
		}

		if (location.startsWith("/")) {
			return getResourceByPath(location);
		}
		else if (location.startsWith(CLASSPATH_URL_PREFIX)) {
			return new ClassPathResource(location.substring(CLASSPATH_URL_PREFIX.length()), getClassLoader());
		}
		else {
			try {
				// Try to parse the location as a URL...
				URL url = new URL(location);
				return (ResourceUtils.isFileURL(url) ? new FileUrlResource(url) : new UrlResource(url));
			}
			catch (MalformedURLException ex) {
				// No URL -> resolve as resource path.
				return getResourceByPath(location);
			}
		}
	}

    protected Resource getResourceByPath(String path) {
		return new ClassPathContextResource(path, getClassLoader());
	}

1.4 文本方式 

与图像方式类似,可以配置,也可以在classpath默认banner.txt

    static final String BANNER_LOCATION_PROPERTY = "spring.banner.location";

	static final String DEFAULT_BANNER_LOCATION = "banner.txt";
    private Banner getTextBanner(Environment environment) {
		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,我们平时绝大部分时间都是使用默认值

private static final Banner DEFAULT_BANNER = new SpringBootBanner();

banner.printBanner(environment, sourceClass, out);

打印banner,可以看到其实就是流向外输出到console,日志文件的过程,console还可以定义文本颜色。格式化了SPRING_BOOT与版本号

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最复杂,需要读取图片,渲染图片

Spring boot 自定义Banner_第2张图片

至此Spring boot打印banner的过程就结束了。过程就是读取文本或者图片,通过流写在console或者log文件,贼简单。

 

2. 自定义Spring boot的banner

自定义Spring boot的banner其实就是选择一个banner.txt或者banner.jpg放在classpath中。或者通过配置自定义

static final String BANNER_LOCATION_PROPERTY = "spring.banner.location";

static final String BANNER_IMAGE_LOCATION_PROPERTY = "spring.banner.image.location";

笔者自定义一个banner 

Spring boot 自定义Banner_第3张图片

看到没有,就不会显示默认的了

Spring boot 自定义Banner_第4张图片

banner是可以多个同时打印的,这里图像banner要注意,这个算法其实比较难写,如果要使用就直接扣代码吧,需要把图像读取转文本。可能对于做图像相关的工程师比较容易写算法

Spring boot 自定义Banner_第5张图片

3. 自定义任意banner

知道了banner的原理,我们可以在任意类打印banner,写一个static静态代码块,读取文件,然后流输出即可。甚至可以直接扣取Spring的打印图片与文本的banner代码,加载我们自己定义的文件与图片,调用print方法即可打印,非常简单。

ImageBanner、ResourceBanner需要Spring的这2个类,占位符可以通过Spring的环境信息格式化

Spring boot 自定义Banner_第6张图片

总结

Spring boot打印banner的原理非常简单,我们可以自定义。而且很多第三方插件也喜欢打印banner,基本上可以直接复用Spring boot打印banner的源码,加载我们自己写的txt或者图片,在类加载,或者方法start的时候打印即可。

 

你可能感兴趣的:(spring,boot,spring,boot)