SpringBoot之Banner源码深度分解

Owen Jia 2019年10月25日 921次浏览

SpringBoot Banner 架构原理

本篇侧重点是源码层面的分析,SpringBoot基础知识需要先有所了解,才能更好跟上节奏。

Banner更多的作为一种人性化的标志,比如企业的Flag、某个知名产品的Flag、不同环境的Flag、等等。SpringBoot大道至简的思想就是要将Banner非功能需求和部分功能需求都封装好,给用户提供最傻瓜的操作步骤去使用它。

每一个应用都应该有自己的Banner,独一无二。

常规使用方式

采用SpringBoot默认配置方式

不需要改动任何东西,只需要在resources目录下添加banner.txt文件即可。

目录结构

banner.txt内容格式:

  .   ____          _            __ _ _
 /\\ / ___'_ __ _ _(_)_ __  __ _ \ \ \ \
( ( )\___ | '_ | '_| | '_ \/ _` | \ \ \ \
 \\/  ___)| |_)| | | | | || (_| |  ) ) ) )
  '  |____| .__|_| |_|_| |_\__, | / / / /
 =========|_|==============|___/=/_/_/_/
 :: Hello World ::       ${application.formatted-version}

Banner.txt支持变量参数:

  • ${AnsiColor.BRIGHT_RED}:设置控制台中输出内容的颜色。
  • ${application.version}:用来获取MANIFEST.MF文件中的版本号。
  • ${application.formatted-version}:格式化后的${application.version}版本信息。
  • ${spring-boot.version}:Spring Boot的版本号。
  • ${spring-boot.formatted-version}:格式化后的${spring-boot.version}版本信息。

类组织关系解析:spring-boot.jar

Banner组织结构图

Banner相关的类都在spring-boot包中。

  • Banner:一个抽象接口,值得注意的是里面的enum Mode用来控制Banner打印的方式:LOG\CONSOLE\OFF。
  • SpringBootBanner:实现Banner接口,是SpringBoot默认类,用来打印“SpringBoot”图案。
  • ImageBanner:实现Banner接口,是一个用来把banner.git|jpg|png类图片转换为ASCII字符图案的类。
  • ResouceBanner: 实现Banner接口,一般的banner.txt的资源文件类。
  • Banners:实现Banner接口,S..A..BannerPrinter的私有静态类,封装多个Banner的集合类。
  • PrintedBanner:实现Banner接口,S..A..BannerPrinter的私有静态类,封装资源和对应Banner。
  • SpringApplicationBannerPrinter:执行逻辑控制单元,核心类,负责同SpringApplication对接。
  • SpringApplication:它不属于Banner架构部分,但是它是banner启动执行的源头。

class SpringApplication

private Banner printBanner(ConfigurableEnvironment environment) {
	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);
	if (this.bannerMode == Mode.LOG) {
		return bannerPrinter.print(environment, this.mainApplicationClass, logger);
	}
	return bannerPrinter.print(environment, this.mainApplicationClass, System.out);
}

/**
 * Sets the {@link Banner} instance which will be used to print the banner when no
 * static banner file is provided.
 * @param banner the Banner instance to use
 */
public void setBanner(Banner banner) {
	this.banner = banner;
}

/**
 * Sets the mode used to display the banner when the application runs. Defaults to
 * {@code Banner.Mode.CONSOLE}.
 * @param bannerMode the mode used to display the banner
 */
public void setBannerMode(Banner.Mode bannerMode) {
	this.bannerMode = bannerMode;
}

重点:

setBanner(Banner)方法告诉我们Banner是支持自定义扩展类的,只有通过这个方法在启动时设置就可以了。

class SpringApplicationBannerPrinter


static final String BANNER_LOCATION_PROPERTY = "banner.location";

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

static final String DEFAULT_BANNER_LOCATION = "banner.txt";

static final String[] IMAGE_EXTENSION = { "gif", "jpg", "png" };

private static final Banner DEFAULT_BANNER = new SpringBootBanner();

private final ResourceLoader resourceLoader;

private final Banner fallbackBanner;

SpringApplicationBannerPrinter(ResourceLoader resourceLoader, Banner fallbackBanner) {
	this.resourceLoader = resourceLoader;
	this.fallbackBanner = fallbackBanner;
}

public Banner print(Environment environment, Class<?> sourceClass, Log logger) {
	Banner banner = getBanner(environment, this.fallbackBanner);
	try {
		logger.info(createStringFromBanner(banner, environment, sourceClass));
	}
	catch (UnsupportedEncodingException ex) {
		logger.warn("Failed to create String for banner", ex);
	}
	return new PrintedBanner(banner, sourceClass);
}

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

private Banner getBanner(Environment environment, Banner definedBanner) {
	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;
}

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

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

private String createStringFromBanner(Banner banner, Environment environment,
		Class<?> mainApplicationClass) throws UnsupportedEncodingException {
	ByteArrayOutputStream baos = new ByteArrayOutputStream();
	banner.printBanner(environment, mainApplicationClass, new PrintStream(baos));
	String charset = environment.getProperty("banner.charset", "UTF-8");
	return baos.toString(charset);
}

private static class Banners implements Banner {...}
private static class PrintedBanner implements Banner {...}

重点:

  1. ImageBanner和ResouceBanner是同级共存的,如何两者同时有就会都打印出来,Image先打印。
  2. Banners和PrintedBanner是它私有静态类,工具类概念使用。

interface Banner

/**
 * Interface class for writing a banner programmatically.
 *
 * @author Phillip Webb
 * @author Michael Stummvoll
 * @author Jeremy Rickard
 * @since 1.2.0
 */
@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
	}
}

SpringBootBanner

SpringBoot框架默认Banner,什么都不配置就打印它。

/**
 * Default Banner implementation which writes the 'Spring' banner.
 *
 * @author Phillip Webb
 */
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 + ")");
		String padding = "";
		while (padding.length() < STRAP_LINE_SIZE
				- (version.length() + SPRING_BOOT.length())) {
			padding += " ";
		}

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

}

ImageBanner

原理同SpringBootBanner类似,多了image模块逻辑,源码不贴了

图片格式Banner,SpringBoot加载配置项banner.image.location,从配置项中获取真实的路径,SpringBoot 会根据配置项的路径加载文件。如果没有配置banner.image.location,转而依次加载resource目录下的banner.gif、banner.jpg、 banner.png这三个中存在的文件,转化成txt进行输出。

ResourceBanner

原理同SpringBootBanner类似,多了resource模块逻辑,源码不贴了

Banner.txt支持PlaceHolder变量使用“${}”,支持的参与有application.version、spring-boot.version、application.formatted-version、spring-boot.formatted-version、application.title。

有个特别的参数是AnsiColor类,如“AnsiColor.GREEN”,可以控制输出字符颜色,在banner.txt里面可以加多个进行分段控制。

SpringApplicationBannerPrinter.PrintedBanner

Banner调用返回类,作为一个封装的数据结果返回给SpringApplication。

private static class PrintedBanner implements Banner {

	private final Banner banner;

	private final Class<?> sourceClass;

	PrintedBanner(Banner banner, Class<?> sourceClass) {
		this.banner = banner;
		this.sourceClass = sourceClass;
	}

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

}

SpringApplicationBannerPrinter.Banners

起到封装多个Banner的作用,随着发展而出现的一个类。

private static class Banners implements Banner {

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

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

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

}

总结归纳

这块的源码建议多去仔细品味一点点的解读,没有什么难度的Code,很适合作为SpringBoot思想的入门学习。Banner总体来说是比较简单的架构设计,其中把OOP思想发挥的非常好很值得我辈学习,值得一提的“PrintedBanner”类设计就值得考究,OOP发挥到极致!

OOP思想存在已经很多年了,架构设计中一般是OOP+AOP结合使用。

仔细思考“面向过程编程、面向对象编程、面向切面编程、面向服务编程”这些设计思想的演变,它们有一种规律在其中。可以说是技术生产力的能力在不断提高不断释放,它们是递进的逐步提出的概念并发展开来。

友链推荐

自动生成ASCII字符图案网站:

http://www.network-science.de/ascii/

http://patorjk.com/software/taag

https://www.degraeve.com/img2txt.php