SpringApplication#run⽅法第5步,打印banner(四)

    技术2022-07-12  77

    分析第5步,打印banner。

    解析

    1. SpringApplication#run⽅法的第5步执⾏如下代码:

    private Banner printBanner(ConfigurableEnvironment environment) { // 1. ⾸先判断banner的输出级别。如果禁⽤了,则直接返回空。 if (this.bannerMode == Mode.OFF) { return null; } else { // 2. 获取资源加载器ResourceLoader ResourceLoader resourceLoader = this.resourceLoader != null ? this.resourceLoader : new DefaultResourceLoader(this.getClassLoader()); // 3. 实例化SpringApplicationBannerPrinter类 SpringApplicationBannerPrinter bannerPrinter = new SpringApplicationBannerPrinter((ResourceLoader)resourceLoader, this.banner); // 如果banner的输出模式是Mode.LOG,则直接将其信息输出到logger⽇志中,否则将其输出到控制台,也就是System.out return this.bannerMode == Mode.LOG ? bannerPrinter.print(environment, this.mainApplicationClass, logger) : bannerPrinter.print(environment, this.mainApplicationClass, System.out); } }

    做了4件事

    1. 如果this.bannerMode等于Banner.Mode.OFF,则直接返回空。

    2. 获取资源加载器ResourceLoader.代码如下:

    ResourceLoader resourceLoader = this.resourceLoader != null ? this.resourceLoader : new DefaultResourceLoader(this.getClassLoader());

    对于当前的场景来说, SpringApplication 中的 resourceLoader为null.因此会实例化DefaultResourceLoader。

    3. 实例化SpringApplicationBannerPrinter类.代码如下:

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

    注意.在当前场景下, SpringApplicationBannerPrinter中的fallbackBanner为null.

    4. 如果banner的输出模式是Mode.LOG,则直接将其信息输出到logger⽇志中。

    注意 默认情况下. SpringApplication中的banner输出模式为CONSOLE.因此是不会输出到⽇志的.

    banner的输出默认如下:

    enum Mode { /** * Disable printing of the banner. */ OFF, /** * Print the banner to System.out. */ CONSOLE, /** * Print the banner to the log file. */ LOG }

    5. 将banner输出到控制台,也就是System.out.代码如下:

    public Banner print(Environment environment, Class<?> sourceClass, PrintStream out) { // 1. 获取Banner Banner banner = getBanner(environment, this.fallbackBanner); // 2. 调⽤Banner中的printBanner⽅法 banner.printBanner(environment, sourceClass, out); // 3. 实例化PrintedBanner类 return new PrintedBanner(banner, sourceClass); }

    做了3件事: 1. 获取Banner 2. 调⽤Banner中的printBanner⽅法.进⾏banner的打印. 3. 实例化PrintedBanner类

    2. 获取banner的⽅法如下:

    private Banner getBanner(Environment environment, Banner definedBanner) { SpringApplicationBannerPrinter.Banners banners = new SpringApplicationBannerPrinter.Banners(); banners.addIfNotNull(this.getImageBanner(environment)); banners.addIfNotNull(this.getTextBanner(environment)); // 如果Banners对象的banners不为空,也就是⾄少找到了banner.gif, banner.jpg, banner.png, banner.txt其中的⼀个,那么返回该Banners对象,否则返回默认的SpringBootBanner对象 if (banners.hasAtLeastOneBanner()) { return banners; } else { return this.fallbackBanner != null ? this.fallbackBanner : DEFAULT_BANNER; } }

    做了3件事 1. 实例化Banners.然后为其设置ImageBanner和TextBanner.如果此时anners对象的banners不为空.则 返回Banners。 2. 如果fallbackBanner不为null的话,返回fallbackBanner.对于当前场景来说fallbackBanner为null。 3. 返回默认的banner.默认的bannenr为 SpringBootBanner。

    这⾥有必要说明⼀下banner的继承体系.如下:

    其只声明了⼀个⽅法.如下:

    public interface Banner { void printBanner(Environment var1, Class<?> var2, PrintStream var3); public static enum Mode { OFF, CONSOLE, LOG; private Mode() { } } }

    Banners实例化后,会调⽤getImageBanner⽅法进⾏加载.代码如下:

    static final String[] IMAGE_EXTENSION = { "gif", "jpg", "png" }; private Banner getImageBanner(Environment environment) { String location = environment.getProperty("banner.image.location"); if (StringUtils.hasLength(location)) { Resource resource = this.resourceLoader.getResource(location); return resource.exists() ? new ImageBanner(resource) : null; } else { String[] var3 = IMAGE_EXTENSION; int var4 = var3.length; for(int var5 = 0; var5 < var4; ++var5) { String ext = var3[var5]; Resource resource = this.resourceLoader.getResource("banner." + ext); if (resource.exists()) { return new ImageBanner(resource); } } return null; } }

    逻辑如下: 1. ⾸先判断是否配置了系统属性banner.image.location,如果有直接返回ImageBanner. 2. 如果没有配置则在classpath中查找banner.gif, banner.jpg, banner.png,如果找到,则创建⼀个 ImageBanner对象并添加到Banners对象的banners属性中,该属性是⼀个List.代码如下: 

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

    很明显 对于当前场景来说. getImageBanner返回的是null。

    接下来调⽤getTextBanner.来加载TextBanner.代码如下:

    private Banner getTextBanner(Environment environment) { String location = environment.getProperty("banner.location", "banner.txt"); Resource resource = this.resourceLoader.getResource(location); return resource.exists() ? new ResourceBanner(resource) : null; }

    还是同样的套路。 1. 从environment中获取banner.location属性,默认为banner.txt。 2. 进⾏加载.如果存在的话,则返回ResourceBanner.否则返回null。 对于当前场景来说.返回的是null。 因此,对于当前场景来说. getBanner返回的是SpringBootBanner。

    3. 接下来调⽤SpringBootBanner#printBanner⽅法.代码如下:  

    public void printBanner(Environment environment, Class<?> sourceClass, PrintStream printStream) { String[] var4 = BANNER; int var5 = var4.length; for(int var6 = 0; var6 < var5; ++var6) { String line = var4[var6]; printStream.println(line); } String version = SpringBootVersion.getVersion(); version = version == null ? "" : " (v" + version + ")"; String padding; for(padding = ""; padding.length() < 42 - (version.length() + " :: Spring Boot :: ".length()); padding = padding + " ") { ; } printStream.println(AnsiOutput.toString(new Object[]{AnsiColor.GREEN, " :: Spring Boot :: ", AnsiColor.DEFAULT, padding, AnsiStyle.FAINT, version})); printStream.println(); }

    做了3件事 1. 循环遍历BANNER数组,并依次进⾏数组内容的打印 2. 调⽤SpringBootVersion#getVersion,进⾏springboot版本信息的获取.然后为了与之前的输出字符 进⾏对⻬,在springboot版本信息前加空格.SpringBootVersion#getVersion代码如下:

    public static String getVersion() { Package pkg = SpringApplication.class.getPackage(); return pkg != null ? pkg.getImplementationVersion() : null; }

    版本信息那些事MANIFEST.MF

    3. 通过AnsiOutput#toString⽅法⽣成字符.输出到PrintStream.最后输出⼀个回⻋换⾏. 代码如下:

    public static String toString(Object... elements) { StringBuilder sb = new StringBuilder(); if (isEnabled()) { buildEnabled(sb, elements); } else { buildDisabled(sb, elements); } return sb.toString(); }

    1. 实例化StringBuilder进⾏字符串拼接. 2. 判断是否可⽤.如果可以调⽤buildEnabled.否则调⽤buildDisabled. isEnabled⽅法如下:

    private static boolean isEnabled() { if (enabled == AnsiOutput.Enabled.DETECT) { if (ansiCapable == null) { // 对于当前场景来说.ansiCapable 为 null.因此会执⾏detectIfAnsiCapable⽅法 ansiCapable = detectIfAnsiCapable(); } return ansiCapable; } else { return enabled == AnsiOutput.Enabled.ALWAYS; } }

    这⾥⽤到了我们之前分析过的知识.springApplication run ⽅法执⾏前4步的过程中.发送了ApplicationEnvironmentPreparedEvent 时间. 其中AnsiOutputApplicationListener 对该事件进⾏了处理.代码如下:  

    public void onApplicationEvent(ApplicationEnvironmentPreparedEvent event) { RelaxedPropertyResolver resolver = new RelaxedPropertyResolver(event.getEnvironment(), "spring.output.ansi."); if (resolver.containsProperty("enabled")) { String enabled = resolver.getProperty("enabled"); AnsiOutput.setEnabled((Enabled)Enum.valueOf(Enabled.class, enabled.toUpperCase())); } if (resolver.containsProperty("console-available")) { AnsiOutput.setConsoleAvailable((Boolean)resolver.getProperty("console-available", Boolean.class)); } }

    对于当前场景来说. resolver中是含有spring.output.ansi.enabled 的配置的.默认为true.注意,此时需 要我们在sts中通过右键-->run-as--> spring boot app 来启动项⽬.如图:

    因此会将AnsiOutput的enabled 设置为Enabled.ALWAYS.

    因此这⾥会执⾏buildEnabled⽅法.代码如下:

    private static void buildDisabled(StringBuilder sb, Object[] elements) { Object[] var2 = elements; int var3 = elements.length; for(int var4 = 0; var4 < var3; ++var4) { Object element = var2[var4]; if (!(element instanceof AnsiElement) && element != null) { sb.append(element); } } }

    这⾥返回的字符串为:

    [32m :: Spring Boot :: ​[39m ​[2m​[0;39m

     ⾃定义banner

     通过之前的分析,我们知道了SpringApplicationBannerPrinter#getBanner 默认返回的是SpringBootBanner.但是当我们在类路径下 放⼊banner.txt或者在banner.image.location 放⼊图⽚.⼜该如何呢? 此时返回的是Banners.在打印时会调⽤Banners#printBanner⽅法.代码如下:

    public void printBanner(Environment environment, Class<?> sourceClass,PrintStream out) { for (Banner banner : this.banners) { banner.printBanner(environment, sourceClass, out); } }

    很简单循环遍历banners调⽤其printBanner进⾏打印.那么Banners会有哪些banner呢?由前可知有

    1. ImageBanner

    2. ResourceBanner

    那么我们就分别看下其printBanner⽅法:

    1. ImageBanner#printBanner 代码如下:  

    ublic void printBanner(Environment environment, Class<?> sourceClass, PrintStream out) { // 1. 获取系统环境变量中的java.awt.headless变量 String headless = System.getProperty("java.awt.headless"); try { // 2. 设置java.awt.headless变量值为true。并调⽤printBanner⽅法进⾏图案的打印⼯作 System.setProperty("java.awt.headless", "true"); this.printBanner(environment, out); } catch (Throwable var9) { logger.warn("Image banner not printable: " + this.image + " (" + var9.getClass() + ": '" + var9.getMessage() + "')"); logger.debug("Image banner printing failure", var9); } finally { // 3. finally中还原操作系统中的java.awt.headless环境变量值 if (headless == null) { System.clearProperty("java.awt.headless"); } else { System.setProperty("java.awt.headless", headless); } } }

    做了3件事 1. 获取系统环境变量中的java.awt.headless变量。 2. 设置java.awt.headless变量值为true。并调⽤printBanner⽅法进⾏图案的打印⼯作 3. finally中还原操作系统中的java.awt.headless环境变量值。

    指定⼀提的是,java.awt.headless 默认就是true。

    printBanner⽅法代码如下:

    private void printBanner(Environment environment, PrintStream out) throws IOException { PropertyResolver properties = new RelaxedPropertyResolver(environment, "banner.image."); int width = (Integer)properties.getProperty("width", Integer.class, 76); int height = (Integer)properties.getProperty("height", Integer.class, 0); int margin = (Integer)properties.getProperty("margin", Integer.class, 2); boolean invert = (Boolean)properties.getProperty("invert", Boolean.class, false); BufferedImage image = this.readImage(width, height); this.printBanner(image, margin, invert, out); }

    还是3件事 1. 读取banner.image.width,默认为 76 . 读取banner.image.height,默认为 0 . 读取 banner.image.margin,默认为 2. 读取banner.image.invert,默认为 false. 2. 调⽤readImage 进⾏图⽚的读取.代码如下:

    private BufferedImage readImage(int width, int height) throws IOException { InputStream inputStream = this.image.getInputStream(); BufferedImage var5; try { BufferedImage image = ImageIO.read(inputStream); var5 = this.resizeImage(image, width, height); } finally { inputStream.close(); } return var5; }

    通过ImageIO进⾏读取,最后通过读取图⽚的配置参数,进⾏图⽚的缩放处理。

    3. printBanner 实现如下:  

    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) { int x; for(x = 0; x < margin; ++x) { out.print(" "); } for(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(this.getAsciiPixel(color, invert)); } out.println(); } out.print(AnsiOutput.encode(AnsiColor.DEFAULT)); out.print(AnsiOutput.encode(AnsiBackground.DEFAULT)); out.println(); }

    没什么可说的,图⽚是由⼀个⼀个的像素组成的,直接输出每个像素即可。

    2. ResourceBanner#printBanner,代码如下:  

    public void printBanner(Environment environment, Class<?> sourceClass,PrintStream out) { try { // 1. 获取resource中的输⼊流,并将其转化为字符串 通过environment获取banner.charset变量,如果不存在,则默认使⽤UTF-8编码 String banner = StreamUtils.copyToString(this.resource.getInputStream(),environment.getProperty("banner.charset", Charset.class, Charset.forName("UTF-8"))); // 2. 循环遍历所有的PropertyResolver 去解析banner中配置的spel表达式 for (PropertyResolver resolver : getPropertyResolvers(environment, sourceClass)) { banner = resolver.resolvePlaceholders(banner); } // 3. 打印字符串信息 out.println(banner); }catch (Exception ex) { logger.warn("Banner not printable: " + this.resource + " (" + ex.getClass()+ ": '" + ex.getMessage() + "')", ex); } }

    还是3步: 1. 获取resource中的输⼊流,并将其转化为字符串 通过environment获取banner.charset变量,如果不 存在,则默认使⽤UTF-8编码 2. 循环遍历所有的PropertyResolver 去解析banner中配置的spel表达式. ⾸先通过getPropertyResolvers 获得所有的PropertyResolver.代码如下:  

    protected List<PropertyResolver> getPropertyResolvers(Environment environment, Class<?> sourceClass) { // 1. 实例化resolvers集合,并添加environment元素, Environment接⼝继承⾃PropertyResolver接⼝ List<PropertyResolver> resolvers = new ArrayList<PropertyResolver>(); resolvers.add(environment); // 2. 调⽤getVersionResolver(sourceClass)⽅法并将其返回值添加到resolvers集合 resolvers.add(getVersionResolver(sourceClass)); // 3. 调⽤getAnsiResolver(sourceClass)⽅法并将其返回值添加到resolvers集合 直接设置开启了ansi resolvers.add(getAnsiResolver()); // 4. 调⽤getTitleResolver(sourceClass)⽅法并将其返回值添加到resolvers集合 resolvers.add(getTitleResolver(sourceClass)); return resolvers; }

    4件事: 1. 实例化resolvers集合,并添加environment元素, Environment接⼝继承⾃PropertyResolver接 ⼝ 2. 调⽤getVersionResolver(sourceClass)⽅法并将其返回值添加到resolvers集合。

    代码如下:  

    private PropertyResolver getVersionResolver(Class<?> sourceClass) { MutablePropertySources propertySources = new MutablePropertySources(); propertySources.addLast(new MapPropertySource("version", getVersionsMap(sourceClas s))); return new PropertySourcesPropertyResolver(propertySources); }

    其构建了⼀个MapPropertySource,名为version,value是通过getVersionsMap⽅法获得的.最后 返回⼀个PropertySourcesPropertyResolver.代码如下:

    private Map<String, Object> getVersionsMap(Class<?> sourceClass) { // 获取sourceClass所在包的版本号 String appVersion = getApplicationVersion(sourceClass); // 获取Boot版本号 String bootVersion = getBootVersion(); Map<String, Object> versions = new HashMap<String, Object>(); versions.put("application.version", getVersionString(appVersion, false)); versions.put("spring-boot.version", getVersionString(bootVersion, false)); versions.put("application.formatted-version", getVersionString(appVersion, true)); versions.put("spring-boot.formatted-version",getVersionString(bootVersion, true)); return versions; } protected String getApplicationVersion(Class<?> sourceClass) { Package sourcePackage = (sourceClass == null ? null : sourceClass.getPackage()); return (sourcePackage == null ? null : sourcePackage.getImplementationVersion()); } protected String getBootVersion() { return SpringBootVersion.getVersion(); } private String getVersionString(String version, boolean format) { if (version == null) { return ""; } return (format ? " (v" + version + ")" : version); }

    逻辑如下: 1. ⾸先通过调⽤getApplicationVersion⽅法获得appVersion.其是通过获取sourceClass所在 包的版本号. sourceClass为应⽤的启动类 2. 获取Boot版本号.同样是通过获得SpringApplication所在包的版本号完成的 3. 在map中存⼊数据. 该⽅法最终的数据为:

    {application.formatted-version=, application.version=, spring-boot.formatted-version=, springboot.version=}

     3. 调⽤getAnsiResolver(sourceClass)⽅法并将其返回值添加到resolvers集合 直接设置开启了ansi.代码如下:

    private PropertyResolver getAnsiResolver() { MutablePropertySources sources = new MutablePropertySources(); sources.addFirst(new AnsiPropertySource("ansi", true)); return new PropertySourcesPropertyResolver(sources); }

     4. 调⽤getTitleResolver(sourceClass)⽅法并将其返回值添加到resolvers集合.代码如下:

    private PropertyResolver getTitleResolver(Class<?> sourceClass) { MutablePropertySources sources = new MutablePropertySources(); String applicationTitle = getApplicationTitle(sourceClass); // 获取当前启动类中所在的包中的Implementation-Title属性值,并将其添加到sources中。 Map<String, Object> titleMap = Collections.<String, Object>singletonMap("application.title", (applicationTitle == null ? "" : applicationTitle)); sources.addFirst(new MapPropertySource("title", titleMap)); return new PropertySourcesPropertyResolver(sources); }

    调⽤getApplicationTitle获得title.代码如下:

    protected String getApplicationTitle(Class<?> sourceClass) { Package sourcePackage = (sourceClass == null ? null : sourceClass.getPackage()); return (sourcePackage == null ? null : sourcePackage.getImplementationTitle()); }

    参考链接

    新年彩蛋: Spring Boot⾃定义BannerSpring Boot Logback应⽤⽇志springboot源码分析3-springboot之banner类架构以及原理Spring Boot⼲货系列:(七)默认⽇志logback配置解析  

    Processed: 0.026, SQL: 9