1. springboot源码分析2-springboot 之banner定制以及原理
springboot在启动的时候,默认会在控制台输出默认的banner。也就是我们经常所说的图案,输出的图案如下所示:
. ____ _ __ _ _
/\\ / ___'_ __ _ _(_)_ __ __ _ \ \ \ \
( ( )\___ | '_ | '_| | '_ \/ _` | \ \ \ \
\\/ ___)| |_)| | | | | || (_| | ) ) ) )
' |____| .__|_| |_|_| |_\__, | / / / /
=========|_|==============|___/=/_/_/_/
:: Spring Boot :: (v2.0.0.M3)
本小节围绕上述的图案也就是banner展开进行说明:包括banner的输出模式、定制、关闭、原理分析。
1.1 banner的三种输出模式
banner的输出默认有三种种模式,LOG、CONSOLE、OFF。
1. LOG:将banner信息输出到日志文件。
2. CONSOLE:将banner信息输出到控制台。
3. OFF:禁用banner的信息输出。
1.2 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 springApplication.run(DemoApplication.class, args);
7 }
8 }
springApplication.setBannerMode方法用于设置banner的输出模式,该方法需要一个Model类型的参数,如下所示:
1 public interface Banner {
2 void printBanner(Environment environment, Class<?> sourceClass, PrintStream out);
3 enum Mode {
4 OFF,
5 CONSOLE,
6 LOG
7 }
8 }
1.3 原理分析
由于springboot中的源码比较复杂,因此我们讲解的时候采取分模块拆解的方式去学习,而不是全盘托出。对于需要讲解的我们要重点关注,对于自己暂时还不熟悉的,我们就留到后面的章节再去单独讲解。这样就可以尽量避免在源码中跟踪来跟踪去,最后不知所云了。
1.3.1 banner类的架构
上述我们看到了banner的三种输出模式,那么问题来了,这些不同的输出模式框架是怎么处理的呢?banner内容如何进行定义呢?banner的定义以及内容有没有限制呢?带着这些问题,我们先看下banner类的顶级接口Banner(org.springframework.boot.Banner)的核心方法如下所示:
1 public interface Banner {
2 void printBanner(Environment environment, Class<?> sourceClass, PrintStream out);
}
Banner类中只有printBanner(打印Banner)一个方法,该方法顾名思义就是将Banner打印到指定的流中(前面我们也提到过、可以是文件也可以是控制台)。这个接口的实现类如下图所示:
1 Banners:该类内部持有一系列的Banner处理类集合,并在打印流的时候,循环遍历所有的Banner处理类集合进行处理。
2 ImageBanner:打印图片形式的Banner。
3 PrintedBanner:负责装饰其他的Banner类,并内部进行调用。这个后续再详细讲解,现有一个印象即可。
4 ResourceBanner:从一个文本资源中获取需要打印的Banner并输出。
5 SpringBootBanner:默认的Banner打印类。(默认的)
好了,接下来看一下Banner类的初始化以及内部处理逻辑吧。
首先,我们看一下SpringApplication类中的run(String... args)方法,如下所示:
暂时将精力放置到printBanner方法的执行逻辑中,该方法如下所示:
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);
}
上述函数的处理逻辑如下:
1、首先判断banner的输出级别。如果禁用了,则直接返回空。关于日志的输出级别可以参考上文的讲解。
2、获取资源加载器ResourceLoader 。ResourceLoader我们也是可以自定义的,如果你觉得有必要。资源加载器主要加载项目或者jar包中定义好的资源。因为spring将所有的配置文件或者属性文件抽象为资源Resource,所以资源加载器的目的就是加载指定的配置文件或者属性文件并将其封装为Resource实例对象。
3、实例化SpringApplicationBannerPrinter 类,注意一点:this.banner我们是可以自定义的,操作方式如下:
SpringApplication springApplication=new SpringApplication();
springApplication.setBannerMode(Banner.Mode.CONSOLE);
Resource resource=new ClassPathResource("banner.txt");
springApplication.setBanner(new ResourceBanner(resource));
上述代码中,我们指定了自己的ResourceBanner并让其加载项目根目录中的banner.txt资源。
注意:spring将项目中的所有配置文件或者属性文件抽象为资源Resource。关于这一点一定要牢记于心。
4、如果banner的输出模式是Mode.LOG,则直接将其信息输出到logger日志中,否则将其输出到控制台,也就是System.out。
logger的定义如下所示:
private static final Log logger = LogFactory.getLog(SpringApplication.class);
通过这个地方的处理逻辑可以看出,框架并没有给我们一个自定义的开关来做这个事情。也是写死的方式处理的。
1.3.2 SpringApplicationBannerPrinter类
下面开始重点讲解第三个步骤,也就是SpringApplicationBannerPrinter类的实例化操作。该类的构造函数定义如下所示:
1 private final ResourceLoader resourceLoader;
2 private final Banner fallbackBanner;
3 SpringApplicationBannerPrinter(ResourceLoader resourceLoader, Banner fallbackBanner) {
4 this.resourceLoader = resourceLoader;
5 this.fallbackBanner = fallbackBanner;
6 }
在这里,我们暂时记住SpringApplicationBannerPrinter类中持有resourceLoader和fallbackBanner即可。
接下来,我们看一下bannerPrinter.print(environment, this.mainApplicationClass, System.out)这行代码所做的事情,该方法的定义如下所示:
1 public Banner print(Environment environment, Class<?> sourceClass, PrintStream out) {
2 Banner banner = getBanner(environment);//获取Banner
3 banner.printBanner(environment, sourceClass, out);//打印
4 return new PrintedBanner(banner, sourceClass);//实例化PrintedBanner类。
5 }
亦然是三部曲:1、获取Banner;2、调用Banner中的printBanner方法;3、实例化PrintedBanner类。
1.3.2.1. 获取Banner
getBanner(environment)方法的定义如下所示:
1 private Banner getBanner(Environment environment) {
2 Banners banners = new Banners();
3 banners.addIfNotNull(getImageBanner(environment));
4 banners.addIfNotNull(getTextBanner(environment));
5 if (banners.hasAtLeastOneBanner()) {
6 return banners;
7 }
8 if (this.fallbackBanner != null) {
9 return this.fallbackBanner;
10 }
11 return DEFAULT_BANNER;
12 }
我们将其执行逻辑进行如下的总结。
1、实例化Banners类,该类内部持有一系列的Banner实例集合。我们可以将其理解为容器。
2、获取ImageBanner、获取TextBanner。
3、如果Banners类中已经包含至少一个Banner实例了,则直接返回,防止Banner太多了。
4、如果Banners类中没有任何Banner实例,则判断fallbackBanner是否为空,如果不为空,则直接返回。注意:这个Banner我们可以自定义,前面已经讲解了,相信大家还有一定的印象。
5、如果fallbackBanner为空,则表示系统内置的一系列Banner没找到,fallbackBanner用户也没有定义,那就没办法了,直接返回默认的Banner,也就是SpringBootBanner。
1.3.2.2. 获取ImageBanner
接下来一个个的看吧。首先看一下getImageBanner的逻辑。该方法的实例代码如下所示:
1 static final String BANNER_LOCATION_PROPERTY = "banner.location";
2 static final String BANNER_IMAGE_LOCATION_PROPERTY = "banner.image.location";
3 static final String DEFAULT_BANNER_LOCATION = "banner.txt";
4 static final String[] IMAGE_EXTENSION = { "gif", "jpg", "png" }
5 private Banner getImageBanner(Environment environment) {
6 String location = environment.getProperty(BANNER_IMAGE_LOCATION_PROPERTY);
7 if (StringUtils.hasLength(location)) {
8 Resource resource = this.resourceLoader.getResource(location);
9 return (resource.exists() ? new ImageBanner(resource) : null);
10 }
11 for (String ext : IMAGE_EXTENSION) {
12 Resource resource = this.resourceLoader.getResource("banner." + ext);
13 if (resource.exists()) {
14 return new ImageBanner(resource);
15 }
16 }
17 return null;
18 }
1、首先,通过environment获取banner.image.location变量的值,在这里如果对于environment不太了解的小伙伴不要担心,这个后续会详细的讲解,我们在这里只需要知道可以通过environment获取到所有的属性信息即可,比如可以通过environment获取到application.properties文件中所有的属性以及值。
2、如果banner.image.location变量存在并且路径是正确的,则直接实例化ImageBanner类并返回,否则开始获取banner.gif、banner.jpg banner.png。三者只要任意一个存在则直接实例化ImageBanner类并返回。
1.3.2.3. 获取TextBanner
getTextBanner(environment)方法的代码如下:
1 static final String BANNER_LOCATION_PROPERTY = "banner.location";
2 static final String DEFAULT_BANNER_LOCATION = "banner.txt";
3 private Banner getTextBanner(Environment environment) {
4 String location = environment.getProperty(BANNER_LOCATION_PROPERTY,
5 DEFAULT_BANNER_LOCATION);
6 Resource resource = this.resourceLoader.getResource(location);
7 if (resource.exists()) {
8 return new ResourceBanner(resource);
9 }
10 return null;
11 }
getTextBanner核心逻辑可以将其总结如下:
1、获取banner.location值,如果没有配置,则直接使用内置的banner.txt。看到这里貌似明白了,我们默认的行为就是该方式,如果期望打印banner信息,只需要在项目的根目录中建立一个banner.txt文件,并填写相应的信息即可替换默认的输出内容(上文已经提及到)。
2、如果banner.location值不为空,并在其配置的文件存在,则直接实例化ResourceBanner并返回。
关于banner.txt目录结构的配置如下所示:
如果我们需要通过banner.location方式指定,则可以直接在application.properties文件中进行如下的设置:
banner.location=banner.txt。当然了这种方式的话banner.txt文件的名称可以自定义。
本文我们暂时先讲解到这里,关于一系列的banner子类,我们下一篇文章一个个的讲解。大家有兴趣的话可以自行跟踪一下。
欢迎关注我的微信公众号,第一时间获得博客更新提醒,以及更多成体系的Java相关原创技术干货。
扫一扫下方二维码或者长按识别二维码,即可关注。