Alian解读SpringBoot 2.6.0 源码(五):启动流程分析之打印Banner

目录

    • 一、背景
      • 1.1、run方法整体流程
      • 1.2、本文解读范围
    • 二、打印banner
      • 2.1、整体流程
        • 2.1.1、图片Banner获取流程
        • 2.1.2、文本Banner获取流程
      • 2.2、具体实现
      • 2.3、打印效果
      • 2.4、Banner生成网址

一、背景

  上两篇我们解读了环境准备及配置文件的加载,本计划是打印banner和创建容器一起解读的,但是创建容器的内容也不少,又会超出字数,编辑特别的慢,希望官方优化下,这次就单独把打印banner分出来了,虽说我觉得这个打印这个东西意义不是很大,不管怎么样,我们还是去了解下,首先我们还是先回顾下启动的整体流程。

1.1、run方法整体流程

  接下来的几个方法所在类的具体路径:org.springframework.boot.SpringApplication

	public ConfigurableApplicationContext run(String... args) {
		// 1、记录启动的开始时间(单位纳秒)
		long startTime = System.nanoTime();
		
		// 2、初始化启动上下文、初始化应用上下文
		DefaultBootstrapContext bootstrapContext = createBootstrapContext();
		ConfigurableApplicationContext context = null;

		// 3、设置无头属性:“java.awt.headless”,默认值为:true(没有图形化界面)
		configureHeadlessProperty();

		// 4、获取所有 Spring 运行监听器
		SpringApplicationRunListeners listeners = getRunListeners(args);
		// 发布应用启动事件
		listeners.starting(bootstrapContext, this.mainApplicationClass);
		try {
			// 5、初始化默认应用参数类(命令行参数)
			ApplicationArguments applicationArguments = new DefaultApplicationArguments(args);

			// 6、根据运行监听器和应用参数 来准备 Spring 环境
			ConfigurableEnvironment environment = prepareEnvironment(listeners, bootstrapContext, applicationArguments);
			// 配置忽略bean信息
			configureIgnoreBeanInfo(environment);

			// 7、创建 Banner 并打印
			Banner printedBanner = printBanner(environment);

			// 8、创建应用上下文
			context = createApplicationContext();
			// 设置applicationStartup
			context.setApplicationStartup(this.applicationStartup);

			// 9、准备应用上下文
			prepareContext(bootstrapContext, context, environment, listeners, applicationArguments, printedBanner);

			// 10、刷新应用上下文(核心)
			refreshContext(context);

			// 11、应用上下文刷新后置处理
			afterRefresh(context, applicationArguments);

			// 13、时间信息、输出日志记录执行主类名
			Duration timeTakenToStartup = Duration.ofNanos(System.nanoTime() - startTime);
			if (this.logStartupInfo) {
				new StartupInfoLogger(this.mainApplicationClass).logStarted(getApplicationLog(), timeTakenToStartup);
			}

			// 14、发布应用上下文启动完成事件
			listeners.started(context, timeTakenToStartup);

			// 15、执行所有 Runner 运行器
			callRunners(context, applicationArguments);
		} catch (Throwable ex) {
			// 运行错误处理
			handleRunFailure(context, ex, listeners);
			throw new IllegalStateException(ex);
		}
		try {
			// 16、发布应用上下文就绪事件(可以使用了)
			Duration timeTakenToReady = Duration.ofNanos(System.nanoTime() - startTime);
			listeners.ready(context, timeTakenToReady);
		} catch (Throwable ex) {
			// 运行错误处理
			handleRunFailure(context, ex, null);
			throw new IllegalStateException(ex);
		}
		// 17、返回应用上下文
		return context;
	}

1.2、本文解读范围

  本文主要讲解到Banner打印,也就是:

	// 7、创建 Banner 并打印
	Banner printedBanner = printBanner(environment);

二、打印banner

2.1、整体流程

  此方法所在类的具体路径:org.springframework.boot.SpringApplication

	// banner打印模式默认是控制台
	private Banner.Mode bannerMode = Banner.Mode.CONSOLE;

	private Banner printBanner(ConfigurableEnvironment environment) {
		// 默认是控制台
		if (this.bannerMode == Banner.Mode.OFF) {
			return null;
		}
		// 由于此时resourceLoader 为null,所以resourceLoader 最终为DefaultResourceLoader
		ResourceLoader resourceLoader = (this.resourceLoader != null) ? this.resourceLoader
				: new DefaultResourceLoader(null);
		// 实例化一个SpringApplicationBannerPrinter
		SpringApplicationBannerPrinter bannerPrinter = new SpringApplicationBannerPrinter(resourceLoader, this.banner);
		// 默认是控制台
		if (this.bannerMode == Mode.LOG) {
			return bannerPrinter.print(environment, this.mainApplicationClass, logger);
		}
		// 调用SpringApplicationBannerPrinter的打印方法
		return bannerPrinter.print(environment, this.mainApplicationClass, System.out);
	}
  • 如果Banner的打印模式是关闭的就不再执行
  • 获取资源加载器
  • 实例化SpringApplicationBannerPrinter
  • 如果Banner的打印模式是输出到日志,则通过SpringApplicationBannerPrinter 进行打印
  • 默认是输出到控制台,也是通过SpringApplicationBannerPrinter

  可以通过配置文件修改打印的模式(log,console,off),比如:

spring:
  main:
    banner-mode: log

  类的具体路径:org.springframework.boot.SpringApplicationBannerPrinter

class SpringApplicationBannerPrinter {

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

	static final String BANNER_IMAGE_LOCATION_PROPERTY = "spring.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;
	}
	
	// 打印Banner到日志文件
	Banner print(Environment environment, Class<?> sourceClass, Log logger) {
		// 通过环境配置获取Banner
		Banner banner = getBanner(environment);
		try {
			// 打印Banner到日志文件
			logger.info(createStringFromBanner(banner, environment, sourceClass));
		} catch (UnsupportedEncodingException ex) {
			logger.warn("Failed to create String for banner", ex);
		}
		// 返回打印对象
		return new PrintedBanner(banner, sourceClass);
	}

	// 打印Banner到控制台
	Banner print(Environment environment, Class<?> sourceClass, PrintStream out) {
		// 通过环境配置获取Banner
		Banner banner = getBanner(environment);
		// 打印Banner到控制台
		banner.printBanner(environment, sourceClass, out);
		// 返回打印对象
		return new PrintedBanner(banner, sourceClass);
	}

	// 获取banners 
	private Banner getBanner(Environment environment) {
		// 实例化Banners,Banners是Banner的实现类
		Banners banners = new Banners();
		// 获取图片形式的Banner,如果不为空则加入列表
		banners.addIfNotNull(getImageBanner(environment));
		// 获取文本形式的Banner,如果不为空则加入列表
		banners.addIfNotNull(getTextBanner(environment));
		// 只要列表不为空则返回Banners
		if (banners.hasAtLeastOneBanner()) {
			return banners;
		}
		// 此处为null
		if (this.fallbackBanner != null) {
			return this.fallbackBanner;
		}
		// 默认是SpringBootBanner
		return DEFAULT_BANNER;
	}

	// 获取文本形式的banner
	private Banner getTextBanner(Environment environment) {
		// 获取属性"spring.banner.location"的值,没有取到就使用默认值"banner.txt"
		String location = environment.getProperty(BANNER_LOCATION_PROPERTY, DEFAULT_BANNER_LOCATION);
		// 通过资源加载器去指定的路径加载资源
		Resource resource = this.resourceLoader.getResource(location);
		try {
			// 如果文本资源存在,且路径中不含有"liquibase-core"
			if (resource.exists() && !resource.getURL().toExternalForm().contains("liquibase-core")) {
				// 符合条件时构建ResourceBanner对象返回
				return new ResourceBanner(resource);
			}
		} catch (IOException ex) {
			// Ignore
		}
		// 默认返回null
		return null;
	}

	// 获取图片形式的banner
	private Banner getImageBanner(Environment environment) {
		// 获取属性"spring.banner.image.location"的值
		String location = environment.getProperty(BANNER_IMAGE_LOCATION_PROPERTY);
		// 如果配置了值
		if (StringUtils.hasLength(location)) {
			// 通过资源加载器加载资源
			Resource resource = this.resourceLoader.getResource(location);
			// 如果图片资源存在,则构建ImageBanner返回,否则返回null
			return resource.exists() ? new ImageBanner(resource) : null;
		}
		//如果没有配置值
		for (String ext : IMAGE_EXTENSION) {
			// 则尝试加载"banner.gif", "banner.jpg", "banner.png"
			Resource resource = this.resourceLoader.getResource("banner." + ext);
			if (resource.exists()) {
				// 只要加载到了,则构建ImageBanner返回
				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));
		// 获取"spring.banner.charset",默认编码为"UTF-8"
		String charset = environment.getProperty("spring.banner.charset", "UTF-8");
		// 转为字符串
		return baos.toString(charset);
	}

	// 静态内部类Banners 
	private static class Banners implements Banner {

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

	}

	// 静态内部类PrintedBanner 
	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) ? sourceClass : this.sourceClass;
			this.banner.printBanner(environment, sourceClass, out);
		}

	}

}
  • 通过环境配置获取Banner,先获取图片Banner(ImageBanner),然后是文本Banner(ResourceBanner),都没有获取到则使用默认的SpringBootBanner
  • 打印Banner到控制台或者日志文件,如果是图片Banner,需要先转为打印流,图片Banner和文本Banner同时存在时,都会打印,先打印图片banner
  • 返回打印对象

2.1.1、图片Banner获取流程

  • 通过在配置文件配置spring.banner.image.location,通过资源加载器加载资源,获取到资源则返回对象ImageBanner,未获取到则返回null
  • 未指定spring.banner.image.location的值时,可以在classPath中添加banner.gif、banner.jpg、banner.png任意一个文件,但是如果添加了多个,则会打印一个,优先级:gif > jpg > png

2.1.2、文本Banner获取流程

  • 通过在配置文件配置spring.banner.location,通过环境对象Environment获取路径,如果没有获取到则默认使用classPath"banner.txt"
  • 通过资源加载器去加载,如果加载到了,并且满足条件,则返回对象ResourceBanner,否则返回null

2.2、具体实现

  ResourceBanner的打印需要处理占位符,然后转为字符串进行输出

public class ResourceBanner implements Banner {

	private Resource resource;

	public void printBanner(Environment environment, Class<?> sourceClass, PrintStream out) {
		try {
			String banner = StreamUtils.copyToString(this.resource.getInputStream(),
					environment.getProperty("spring.banner.charset", Charset.class, StandardCharsets.UTF_8));
			// 获取属性解析器
			for (PropertyResolver resolver : getPropertyResolvers(environment, sourceClass)) {
				// 解析处理占位符
				banner = resolver.resolvePlaceholders(banner);
			}
			// 输出
			out.println(banner);
		} catch (Exception ex) {
			logger.warn(LogMessage.format("Banner not printable: %s (%s: '%s')", this.resource, ex.getClass(), ex.getMessage()), ex);
		}
	}

	protected List<PropertyResolver> getPropertyResolvers(Environment environment, Class<?> sourceClass) {
		List<PropertyResolver> resolvers = new ArrayList<>();
		resolvers.add(environment);
		resolvers.add(getVersionResolver(sourceClass));
		resolvers.add(getAnsiResolver());
		resolvers.add(getTitleResolver(sourceClass));
		return resolvers;
	}
}

  ImageBanner 的打印时会先将资源转为输入流,然后再转为图片输入流,然后转为对象数组Frame[ ],Frame是 ImageBanner 静态内部类,里包含BufferedImagedelayTime,然后遍历数组进行打印。

public class ImageBanner implements Banner {
	
	private final Resource image;

	@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(LogMessage.format("Image banner not printable: %s (%s: '%s')", 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);
			}
		}
	}

	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);
		// 获取位属性
		BitDepth bitDepth = getBitDepthProperty(environment);
		// 获取像素属性
		PixelMode pixelMode = getPixelModeProperty(environment);
		// 获取Frame[]
		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, bitDepth, pixelMode, out);
			// 延迟后继续打印
			sleep(frames[i].getDelayTime());
		}
	}

	private Frame[] readFrames(int width, int height) throws IOException {
		// Java7的语法,自动关闭流
		// 图片资源转为输入流
		try (InputStream inputStream = this.image.getInputStream()) {
			// 输入流转为图片输入流
			try (ImageInputStream imageStream = ImageIO.createImageInputStream(inputStream)) {
				// 根据宽高和图片输入流获取Frame[]
				return readFrames(width, height, imageStream);
			}
		}
	}

	private Frame[] readFrames(int width, int height, ImageInputStream stream) throws IOException {
		// 返回包含所有当前已注册 ImageReader 的 Iterator
		Iterator<ImageReader> readers = ImageIO.getImageReaders(stream);
		Assert.state(readers.hasNext(), "Unable to read image banner source");
		ImageReader reader = readers.next();
		try {
			// 返回一个适合此格式的默认 ImageReadParam 对象
			ImageReadParam readParam = reader.getDefaultReadParam();
			// 设置指定的 ImageInputStream 输入源
			reader.setInput(stream);
			// 返回当前输入源中可用的图像数
			int frameCount = reader.getNumImages(true);
			Frame[] frames = new Frame[frameCount];
			for (int i = 0; i < frameCount; i++) {
				// 通过ImageReader 把图片输入流转为Frame
				frames[i] = readFrame(width, height, reader, i, readParam);
			}
			// 返回数组对象
			return frames;
		} finally {
			reader.dispose();
		}
	}

	private Frame readFrame(int width, int height, ImageReader reader, int imageIndex, ImageReadParam readParam)
			throws IOException {
		// 使用所提供的 ImageReadParam 来读取通过索引 imageIndex 指定的对象
		BufferedImage image = reader.read(imageIndex, readParam);
		// 跳转图像大小
		BufferedImage resized = resizeImage(image, width, height);
		// 获取延迟时间
		int delayTime = getDelayTime(reader, imageIndex);
		// 返回Frame
		return new Frame(resized, delayTime);
	}

	private static class Frame {

		private final BufferedImage image;

		private final int delayTime;

		Frame(BufferedImage image, int delayTime) {
			this.image = image;
			this.delayTime = delayTime;
		}

		BufferedImage getImage() {
			return this.image;
		}

		int getDelayTime() {
			return this.delayTime;
		}
	}
	
}

  有兴趣的小伙伴可以看看具体的实现,详情见代码注释

2.3、打印效果

  我们再配置文件中加入如下配置:

server:
  port: 8080
  servlet:
    context-path: /springboot

spring:
  main:
    sources: com.alian.springboot
    banner-mode: console
  banner:
    location: classpath:banner.txt
    image:
      location: classpath:csdn.jpg

  也就是我们指定了图片Banner和文本Banner资源的路径,然后分别把 banner.txt csdn.png 放到 classpath 下,当然这里不配置文本Banner的地址也可以,只要放了那个文本文件,因为默认会读取 classpath 下的 banner.txt

             &#########     *@@@@@@@@@@  &@@@@@#@@*        :@@@@@@@#@.        
         &##@##########  @@@@#@@@@@@@@#  @@@@@@@@@@@#@@   @##@@@@@@@@@@#      
        ####@#           @@@@@           #@@@      @@@@@  @@@@*     @@@@@     
       #@###             @@@@#@@@@       @@@@       @@@@  #@@@      @@@@@     
       ####@                 @##@@@@#@   #@@@       @@@#  #@@#      @@@#8     
       #@####                    @@@@@8  #@#*     o@@@@   #@@@      @@@#      
        ##@##########@  @#@@@@@#@@@@@@  :@@@@@@@@#@@@@    #@@#      @@@@      
           8#########:  @#@@@@@@@#@     @@@@@@@#@@o      @@@@       @@@@      
                                                                              
                                                      

      ___                                     ___           ___     
     /  /\                      ___          /  /\         /__/\    
    /  /::\                    /  /\        /  /::\        \  \:\   
   /  /:/\:\    ___     ___   /  /:/       /  /:/\:\        \  \:\  
  /  /:/~/::\  /__/\   /  /\ /__/::\      /  /:/~/::\   _____\__\:\ 
 /__/:/ /:/\:\ \  \:\ /  /:/ \__\/\:\__  /__/:/ /:/\:\ /__/::::::::\
 \  \:\/:/__\/  \  \:\  /:/     \  \:\/\ \  \:\/:/__\/ \  \:\~~\~~\/
  \  \::/        \  \:\/:/       \__\::/  \  \::/       \  \:\  ~~~ 
   \  \:\         \  \::/        /__/:/    \  \:\        \  \:\     
    \  \:\         \__\/         \__\/      \  \:\        \  \:\    
     \__\/                                   \__\/         \__\/    

2021-12-07 11:45:32 895 [main] INFO initialize 108:Tomcat initialized with port(s): 8080 (http)
2021-12-07 11:45:32 900 [main] INFO log 173:Initializing ProtocolHandler ["http-nio-8080"]
2021-12-07 11:45:32 900 [main] INFO log 173:Starting service [Tomcat]
2021-12-07 11:45:32 900 [main] INFO log 173:Starting Servlet engine: [Apache Tomcat/9.0.55]
2021-12-07 11:45:32 941 [main] INFO log 173:Initializing Spring embedded WebApplicationContext
2021-12-07 11:45:32 942 [main] INFO prepareWebApplicationContext 290:Root WebApplicationContext: initialization completed in 474 ms
2021-12-07 11:45:33 132 [main] INFO log 173:Starting ProtocolHandler ["http-nio-8080"]
2021-12-07 11:45:33 142 [main] INFO start 220:Tomcat started on port(s): 8080 (http) with context path '/springboot'
2021-12-07 11:45:33 157 [main] INFO logStarted 61:Started SpringbootApplication in 1.116 seconds (JVM running for 1.621)

2.4、Banner生成网址

  比较常用的Banner制作网站如下:

  • http://patorjk.com/software/taag/
  • https://www.bootschool.net/ascii
  • http://www.network-science.de/ascii/
  • http://www.degraeve.com/img2txt.php

你可能感兴趣的