继续上文的<<springboot源码分析2-springboot 之banner定制以及原理章节>>进行讲解,上一节我们详细详解了banner的三种输出模式、banner的输出模式设置、banner类的架构、SpringApplicationBannerPrinter类、ImageBanner以及TextBanner的处理方式。本小节我们来重点讲解一下各种banner处理类的相关实现逻辑以及设计意图和职责。
1.1 SpringBootBanner类
SpringBootBanner类为默认的banner处理类。该类的核心代码如下所示:
1 class SpringBootBanner implements Banner {
2 private static final String[] BANNER = { "",
3 " . ____ _ __ _ _",
4 " /\\\\ / ___'_ __ _ _(_)_ __ __ _ \\ \\ \\ \\",
5 "( ( )\\___ | '_ | '_| | '_ \\/ _` | \\ \\ \\ \\",
6 " \\\\/ ___)| |_)| | | | | || (_| | ) ) ) )",
7 " ' |____| .__|_| |_|_| |_\\__, | / / / /",
8 " =========|_|==============|___/=/_/_/_/" };
9 private static final String SPRING_BOOT = " :: Spring Boot :: ";
10 private static final int STRAP_LINE_SIZE = 42;
11 @Override
12 public void printBanner(Environment environment, Class<?> sourceClass,PrintStream printStream) {
13 for (String line : BANNER) {
printStream.println(line);
14 }
15 String version = SpringBootVersion.getVersion();
16 version = (version == null ? "" : " (v" + version + ")");
17 String padding = "";
18 while (padding.length() < STRAP_LINE_SIZE
19 - (version.length() + SPRING_BOOT.length())) {
20 padding += " ";
21 }
22 printStream.println(AnsiOutput.toString(AnsiColor.GREEN, SPRING_BOOT,
23 AnsiColor.DEFAULT, padding, AnsiStyle.FAINT, version));
24 printStream.println();
25 }
26
27 }
看到上述的BANNER变量,是不是很眼熟,没错这个就是springboot启动的时候,默认的输出图案。我们再次看一下springboot启动的时候默认输出的图案,如下图所示:
BANNER变量的内容正是上图中的上部分。也就是SPRING字符串。
注意:BANNER变量是一个字符串数组,数组的每一个元素内容均对应上图中的每一行字符串。为何这样设计呢?我们思考一下?这样设计的目的究其原因是为了防止定义一个大的字符串而出现跑位或者字符串便宜的问题。
接下来的重心看一下printBanner方法的执行逻辑:
1、循环遍历BANNER数组,并依次进行数组内容的打印。代码为printStream.println(line)。
2、调用SpringBootVersion.getVersion(),进行springboot版本信息的获取工作,这种方式我们在前面的系列文章中也详细讲解了,再次不再累赘。
3、对version字符串进行再加工,因此version最终的值为 (v2.0.0.M3)。
4、开始拼接:: Spring Boot ::和(v2.0.0.M3)的值,最终的效果如上图所示。通过while循环的逻辑可知、一行的字符串长度是42个。
5、开始打印字符串,这个大家有兴趣可以自己跟踪一下,相对而言比较简单。主要看下AnsiOutput.toString方法即可。
至此、springboot默认的banner图案以及输出逻辑我们已经梳理完毕。
1.2 PrintedBanner类
PrintedBanner类中的核心代码如下:
1 private static class PrintedBanner implements Banner {
2 private final Banner banner;
3 private final Class<?> sourceClass;
4 PrintedBanner(Banner banner, Class<?> sourceClass) {
5 this.banner = banner;
6 this.sourceClass = sourceClass;
7 }
8 public void printBanner(Environment environment, Class<?> sourceClass, PrintStream out) {
9 sourceClass = (sourceClass == null ? this.sourceClass : sourceClass);
10 this.banner.printBanner(environment, sourceClass, out);
11 }
12 }
PrintedBanner类看上去貌似没有什么神奇的作用,但是这个类却有点装饰者模式的味道,该类的构造函数需要一个Banner 类型以及Class类型的参数。在printBanner方法中,唯一做的事情就是判断并设置sourceClass参数,然后直接调用banner的printBanner方法。为何这样设计呢?其实这个地方病没有太多神秘的地方。我们只需要知道一点sourceClass是可以自己定义的即可。sourceClass类又是做什么的呢?关于这点我们稍有印象即可,稍后即可看到。
1.3 ImageBanner
ImageBanner类的核心实现如下所示:
1 public void printBanner(Environment environment, Class<?> sourceClass,PrintStream out) {
2 String headless = System.getProperty("java.awt.headless");
3 try {
4 System.setProperty("java.awt.headless", "true");
5 printBanner(environment, out);
6 }
7 catch (Throwable ex) {
8 ......
9 }
10 finally {
11 if (headless == null) {
12 System.clearProperty("java.awt.headless");
13 }
14 else {
15 System.setProperty("java.awt.headless", headless);
16 }
17 }
18 }
上述的代码主要从如下几个步骤进行。
1、获取系统环境变量中的java.awt.headless变量。
2、设置java.awt.headless变量值为true。并调用printBanner方法进行图案的打印工作。
3、finally中还原操作系统中的java.awt.headless环境变量值。
细心的朋友就会发现上述的步骤2有问题的。如果系统中已经设置java.awt.headless变量值为true,还有必要再设置一次吗?很显然,这个地方的代码可以改进下,加一个if判断。
1.3.1 java.awt.headless 模式
下面补充下java.awt.headless的相关知识点。
1. 什么是 java.awt.headless?
Headless模式是系统的一种配置模式。在该模式下,系统缺少了显示设备、键盘或鼠标。
2. 何时使用和headless mode?
Headless模式虽然不是我们愿意见到的,但事实上我们却常常需要在该模式下工作,尤其是服务器端程序开发者。因为服务器(如提供Web服务的主机)往往可能缺少前述设备,但又需要使用他们提供的功能,生成相应的数据,以提供给客户端(如浏览器所在的配有相关的显示设备、键盘和鼠标的主机)。
1.3.2 printBanner方法
继续看一下printBanner方法吧,该方法的实例代码如下:
1 private void printBanner(Environment environment, PrintStream out)throws IOException {
2 int width = environment.getProperty("banner.image.width", Integer.class, 76);
3 int height = environment.getProperty("banner.image.height", Integer.class, 0);
4 int margin = environment.getProperty("banner.image.margin", Integer.class, 2);
5 boolean invert = environment.getProperty("banner.image.invert", Boolean.class,false);
6 BufferedImage image = readImage(width, height);
7 printBanner(image, margin, invert, out);
8 }
上述的方法开始获取banner.image.width、banner.image.height、banner.image.margin、banner.image.invert四个属性值。这几个属性均可以在application.properties中,或者通过命令行参数进行相应的设置。如下图所示:
上述四个参数的含义、默认值说明:
banner.image.width":默认值76,图案的宽度
"banner.image.height":默认值0,图案的高度
"banner.image.margin":默认值 2,空字符的数量
"banner.image.invert":默认值false,是否颠倒
在这里,我们只需要记住上述的几个变量是可以配置的即可,关于图片流的读取以及输出,在这里我们就不详细讲解了。
1.4 ResourceBanner
ResourceBanner类为资源Banner。这个类可能很少有人使用到,但是麻雀虽小五脏俱全,这个类涉及到的知识点不少,我们先看下如何使用这个类。实例代码如下:
1 @SpringBootApplication
2 public class DemoApplication {
3 public static void main(String[] args) {
4 SpringApplication springApplication=new SpringApplication();
5 springApplication.setBannerMode(Banner.Mode.CONSOLE);
6 Resource resource=new ClassPathResource("banner.txt");
7 springApplication.setBanner(new ResourceBanner(resource));
8 springApplication.run(DemoApplication.class, args);
9 }
10 }
我们实例化了一个ClassPathResource类并传了字符串banner.txt。在这里还要脑补一下Spring中的东西,那就是Resource 。
Resource是抽象了所有的配置文件以及属性文件、在Spring框架看来所有的文件、网络资源、jar、属性配置文件等都是资源,因此也提供了不同的资源读取类,其中ClassPathResource就是读取ClassPath路径中的一些资源文件,这里我们传递的是banner.txt,该文件的内容信息如下:分享牛原创网:${application.title}
其中application.title定义在application.properties中,如下所示:
application.title=http://www.shareniu.com/
运行上述代码程序的输出如下:
分享牛原创:http://www.shareniu.com/
这个类确实有点意思,不仅支持自定义资源文件的读取,而且还支持Spring中的spel表达式。我们迫不及待的要去看看ResourceBanner类。
ResourceBanner类的printBanner方法如下所示:
1 public void printBanner(Environment environment, Class<?> sourceClass,PrintStream out) {
2 try {
3 String banner = StreamUtils.copyToString(this.resource.getInputStream(),
4 environment.getProperty("banner.charset", Charset.class,Charset.forName("UTF-8")));
5 for (PropertyResolver resolver : getPropertyResolvers(environment,sourceClass)) {
6 banner = resolver.resolvePlaceholders(banner);
7 }
8 out.println(banner);
9 }
10 catch (Exception ex) {
11 }
12 }
上述方法的执行逻辑进行如下的总结:
1、获取resource中的输入流,并将其转化为字符串。
2、通过environment获取banner.charset变量,如果不存在,则默认使用UTF-8编码。在这里我们再次啰嗦一句话,springboot中所有的配置属性信息最后都会封装为environment中去,因此可以通过environment获取到项目中所有的配置属性信息。
3、循环遍历所有的PropertyResolver 去解析banner中配置的spel表达式。比如上文中的${application.title}就是在这个步骤进行处理的。
4、打印字符串信息。
1.4.1 PropertyResolvers集合初始化
上述中的第二步调用了getPropertyResolvers方法,该方法的实例代码如下:
1 protected List<PropertyResolver> getPropertyResolvers(Environment environment,
2 Class<?> sourceClass) {
3 List<PropertyResolver> resolvers = new ArrayList<>();
4 resolvers.add(environment);
5 resolvers.add(getVersionResolver(sourceClass));
6 resolvers.add(getAnsiResolver());
7 resolvers.add(getTitleResolver(sourceClass));
8 return resolvers;
9 }
getPropertyResolvers方法的执行逻辑如下:
1、实例化resolvers集合,并添加environment元素,Environment接口继承自PropertyResolver接口。
2、调用getVersionResolver(sourceClass)方法并将其返回值添加到resolvers集合。getVersionResolver(sourceClass)方法的实现如下所示:
1 private PropertyResolver getVersionResolver(Class<?> sourceClass) {
2 MutablePropertySources propertySources = new MutablePropertySources();
3 propertySources.addLast(new MapPropertySource("version", getVersionsMap(sourceClass)));
4 return new PropertySourcesPropertyResolver(propertySources);
5 }
6 private Map<String, Object> getVersionsMap(Class<?> sourceClass) {
7 String appVersion = getApplicationVersion(sourceClass);//获取sourceClass所在包的版本号
8 String bootVersion = getBootVersion();//获取Boot版本号,我使用的版本是v2.0.0.M3
9 Map<String, Object> versions = new HashMap<>();
10 versions.put("application.version", getVersionString(appVersion, false));
11 versions.put("spring-boot.version", getVersionString(bootVersion, false));
12 versions.put("application.formatted-version", getVersionString(appVersion, true));
13 versions.put("spring-boot.formatted-version",getVersionString(bootVersion, true));
14 return versions;
15 }
上述代码中,直接实例化MutablePropertySources类,并将其添加到环境propertySources中,在这里在强调一点,环境变量的相关知识点后续会专门单独一章进行讲解。大家可以理解为propertySources是所有的属性容器就够了,我们可以通过propertySources获取到项目中配置的所有属性以及值。
上述的代码设置了如下几个属性以及值:application.version、spring-boot.version、application.formatted-version、spring-boot.formatted-version。其中application.version以及application.formatted-version两个我们可以自定义设置。
上述的几个属性,我将其打印了,输出信息如下:
{application.formatted-version=, application.version=, spring-boot.formatted-version= (v2.0.0.M3), spring-boot.version=2.0.0.M3}
3、调用getAnsiResolver(sourceClass)方法并将其返回值添加到resolvers集合。getAnsiResolver(sourceClass)方法的实现如下所示:
1 private PropertyResolver getAnsiResolver() {
2 MutablePropertySources sources = new MutablePropertySources();
3 sources.addFirst(new AnsiPropertySource("ansi", true));
4 return new PropertySourcesPropertyResolver(sources);
5 }
直接设置开启了ansi。讲解到环境变量的时候一起进行讲解。
4、调用getTitleResolver(sourceClass)方法并将其返回值添加到resolvers集合。getTitleResolver(sourceClass)方法的实现如下所示:
1 private PropertyResolver getTitleResolver(Class<?> sourceClass) {
2 MutablePropertySources sources = new MutablePropertySources();
3 String applicationTitle = getApplicationTitle(sourceClass);
4 Map<String, Object> titleMap = Collections.<String, Object>singletonMap(
5 "application.title", (applicationTitle == null ? "" : applicationTitle));
6 sources.addFirst(new MapPropertySource("title", titleMap));
7 return new PropertySourcesPropertyResolver(sources);
8 }
获取当前启动类中所在的包中的Implementation-Title属性值,并将其添加到sources中。
关于占位符的替换,我们后续的文章开始展开讲解。饭要一口口的吃嘛。至此各种Banner类已经讲解完毕。
欢迎关注我的微信公众号,第一时间获得博客更新提醒,以及更多成体系的Java相关原创技术干货。
扫一扫下方二维码或者长按识别二维码,即可关注。