前言
每次啟動SpringBoot專案時,總是能看到控制臺列印了一串字符,隱約能辨認出是“Spring”,不知大家是否也好奇過是怎么實作的,是直接列印固定的字串,還是根據什么演算法去生成的?于是閑暇無事,探究一番,

只想修改banner可以跳到文末查看
SpringBoot是怎么列印的
Banner默認實作類 SpringBootBanner
1、根據控制臺列印的字符進行全域搜索,筆者選取:: Spring Boot ::進行搜索,定位到了org.springframework.boot.SpringBootBanner,
IDEA全域搜索:CTRL + SHIFT + R

2、進入SpringBootBanner類,先看下注釋Default Banner implementation which writes the 'Spring' banner.,說了兩個資訊:1、當前類是SpringBoot Banner的默認實作;2、列印的字符是“Spring”,
3、往下看,SpringBootBanner實作了Banner介面,Banner包括printBanner方法和列舉Mode,
根據Mode中的注釋和列舉值可以看出,Banner有三種狀態:關閉、列印到控制臺、列印到日志,具體使用場景留待后續分析,
Banner原始碼
/**
* Interface class for writing a banner programmatically.
*/
@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
}
}
4、往下看到類的屬性BANNER和SPRING_BOOT,也能辨認出是控制臺列印的那些字符,
類里面只有一個方法printBanner,負責列印Banner字符,邏輯比較清晰,第一部分逐行列印BANNER形成圖案;第二部分列印SpringBoot版本號,總長度由STRAP_LINE_SIZE控制,
SpringBootBanner完整代碼
/**
* Default Banner implementation which writes the 'Spring' banner.
*/
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 + ")" : "";
StringBuilder padding = new StringBuilder();
while (padding.length() < STRAP_LINE_SIZE - (version.length() + SPRING_BOOT.length())) {
padding.append(" ");
}
printStream.println(AnsiOutput.toString(AnsiColor.GREEN, SPRING_BOOT, AnsiColor.DEFAULT, padding.toString(),
AnsiStyle.FAINT, version));
printStream.println();
}
}
Banner核心控制類 SpringApplicationBannerPrinter
1、上節找到了負責存盤和列印Banner字符的類SpringBootBanner,現在向呼叫鏈上方繼續尋找,通過CTRL + B或者全域搜索可以發現SpringBootBanner在SpringApplicationBannerPrinter類中作為類變數,大概能猜測出這個SpringApplicationBannerPrinter類是Banner列印的核心控制器,
2、進入SpringApplicationBannerPrinter類,照例先看注釋Class used by SpringApplication to print the application banner.,意思是當前類被SpringApplication用來列印banner,
這個SpringApplication好像有點眼熟,名字和我們SpringBoot專案的啟動類有點相似,翻翻啟動類的代碼,想起我們就是通過SpringApplication的run方法啟動專案,banner列印呼叫也是由SpringApplication控制的,后續會詳細分析,(占坑,后續SpringBoot啟動流程也會出一篇博客去探討一下)

回歸正題,繼續從類的屬性開始看,根據名字猜測大概含義,留待后續驗證:
BANNER_LOCATION_PROPERTY:Spring配置,大概是banner檔案的路徑,BANNER_IMAGE_LOCATION_PROPERTY:Spring配置,banner圖片的路徑(存疑,控制臺難道能列印圖片?),DEFAULT_BANNER_LOCATION = "banner.txt":取值是txt檔案,猜測是banner檔案的默認位置,String[] IMAGE_EXTENSION = { "gif", "jpg", "png" }:取值是常見圖片的后綴,結合第二個屬性猜測是用來對banner圖片型別做限制,DEFAULT_BANNER = new SpringBootBanner():把上節分析的SpringBootBanner當做Banner默認實作類ResourceLoader resourceLoader:ResourceLoader簡單來說是Spring加載資源的統一抽象,由實作類提供具體邏輯,
在Spring中讀取xml組態檔加載應用背景關系的ClassPathXmlApplicationContext,就是ResourceLoader的子類,Banner fallbackBanner:翻譯過來是回退banner,暫時猜不出作用,等待后續填坑,
3、往下看方法,只有兩個非私有方法,都是print的多載方法,差別在于第三個引數,分別是Log logger和PrintStream out,代表這兩個方法分別負責日志列印和控制臺列印,
緊扣主題,先看負責控制臺列印的方法,
Banner print(Environment environment, Class<?> sourceClass, PrintStream out) {
Banner banner = getBanner(environment);
banner.printBanner(environment, sourceClass, out);
return new PrintedBanner(banner, sourceClass);
}
代碼很精簡,第一行獲取Banner類,第二行呼叫Banner的print方法列印banner圖案,最后生成PrintedBanner并回傳,
1. getBanner
getBanner原始碼
private Banner getBanner(Environment environment) {
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;
}
查看getBanner方法,首先創建Banners,底層就是Banner陣列,由于存在控制臺、日志兩種列印方式,使用此類方便批量處理,
Banners原始碼
/**
* {@link Banner} comprised of other {@link Banner 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);
}
}
接著就是呼叫getImageBanner和getTextBanner方法獲取Banner,如果Banner陣列不為空則回傳,否則檢查fallbackBanner,
這個fallbackBanner光看名字看不出是什么,使用CTRL+B查看參考,發現是在SpringApplication#printBanner里注入進來的,如下圖,

繼續查找this.banner會發現,最終Banner只能通過SpringApplicationBuilder#banner注入,

SpringApplicationBuilder是通過Constructor(構造器)模式實作的SpringApplication構造器,
查看banner方法的注釋,我們可以知道這里注入的Banner實體會在沒有靜態banner檔案時使用,
回過頭來,fallbackBanner的坑填上了,它是在SpringApplicationBannerPrinter找不到txt檔案或者圖片作為banner素材的時候使用,
如果fallbackBanner也為空,則最侄訓傳兜底方案-SpringBootBanner,
getBanner的結構分析完了,實際情況我們知道走的是兜底方案,也就是只要我們能讓getImageBanner、getTextBanner或者fallbackBanner不為空,就能改變banner列印的圖案,
帶著這個想法,我們就去看看getImageBanner和getTextBanner是咋回事,
2、getImageBanner
查看原始碼,首先environment.getProperty讀取配置spring.banner.image.location獲取圖片位置,
組態檔讀取若為空則遍歷圖片后綴陣列IMAGE_EXTENSION,采用"banner." + ext拼接方式得到圖片相對路徑,并嘗試加載,加載成功后會生成ImageBanner并回傳,
接收圖片資源并處理列印的邏輯都封裝在ImageBanner中,后續單獨寫一篇文章嘗試分析圖片列印邏輯,
按照我們的分析,只要在組態檔中添加spring.banner.image.location并賦值正確的圖片路徑,或者在resources目錄下存放一張名字為“banner”、后綴是gif,jpg, png其中之一的圖片,SpringApplicationBannerPrinter就會列印出來,
注: 為什么沒加前綴classpath:也可以放在resources目錄下,可以查看DefaultResourceLoader#getResource對于banner.jpg這種location的處理邏輯,
后續章節會有列印效果,
getImageBanner原始碼
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;
}
3、getTextBanner
查看原始碼,同樣是先從組態檔中讀取banner檔案的location并嘗試加載資源,和getImageBanner不同的是,這里讀取不到會使用默認值banner.txt,
加載資源后有一個Resource的限制條件!resource.getURL().toExternalForm().contains("liquibase-core"),這里不明白這個條件的含義,只查詢到了Liquibase是一個用于跟蹤、管理和應用資料庫變化的開源工具,
資源校驗通過后生成ResourceBanner并回傳,
getTextBanner原始碼
private Banner getTextBanner(Environment environment) {
String location = environment.getProperty(BANNER_LOCATION_PROPERTY, DEFAULT_BANNER_LOCATION);
Resource resource = this.resourceLoader.getResource(location);
try {
if (resource.exists() && !resource.getURL().toExternalForm().contains("liquibase-core")) {
return new ResourceBanner(resource);
}
}
catch (IOException ex) {
// Ignore
}
return null;
}
接下來進入ResourceBanner看下列印細節,
printBanner結構比較簡單,第一部分設定banner字符集,優先讀取配置spring.banner.charset,無配置則默認設定為UTF-8,
第二部分去決議banner字符,比如將${xxx}占位符決議成實際的值,
第三部分就是呼叫流列印輸出,
ResourceBanner#printBanner
@Override
public void printBanner(Environment environment, Class<?> sourceClass, PrintStream out) {
try {
// 設定banner字符集
String banner = StreamUtils.copyToString(this.resource.getInputStream(),
environment.getProperty("spring.banner.charset", Charset.class, StandardCharsets.UTF_8));
// 決議banner
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);
}
}
banner列印呼叫方-SpringApplication
上節看完SpringApplicationBannerPrinter,這節來尋找列印banner的呼叫方,
CTRL+B查看SpringApplicationBannerPrinter#print的參考,定位到了SpringApplication#printBanner,原始碼如下,
從整體結構來看,printBanner方法根據this.bannerMode取值不同,執行不同的列印策略:不列印、列印到日志、列印到控制臺,
那么這個
bannerMode是怎么設定的?查看初始化的代碼,默認值是CONSOLE,
繼續尋找,最終定位到了SpringApplicationBuilder#bannerMode,意味著bannerMode只能通過構造器進行注入,
繼續尋找printBanner的呼叫方,定位到了SpringApplication#run(String...),
上面有提到過,通常我們SpringBoot專案都是去呼叫SpringApplication#run(Class<?>, String...)去啟動專案,底層是通過new關鍵字創建SpringApplication物件,最后呼叫SpringApplication#run(String...)完成一系列的資源初始化,
所以這就可以解釋大多數情況下,我們的SpringBoot專案啟動時都會列印那個默認的“Spring”字符,
SpringApplication#printBanner原始碼
private Banner printBanner(ConfigurableEnvironment environment) {
if (this.bannerMode == Banner.Mode.OFF) {
return null;
}
ResourceLoader resourceLoader = (this.resourceLoader != null) ? this.resourceLoader
: new DefaultResourceLoader(null);
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);
}
如何修改專案啟動的banner
修改banner列印策略
經上分析,banner列印策略包括控制臺、日志、不列印,
1. 隱式
默認策略是控制臺,只需大多數情況一樣,專案啟動類通過SpringApplication.run(DistinctAppUserServiceApplication.class, args);啟動,無需指定,
2. 顯式注入
通過SpringApplicationBuilder構造器顯式注入banner列印策略,
@SpringBootApplication
public class DemoApplication {
public static void main(String[] args) {
new SpringApplicationBuilder(DemoApplication.class)
// Banner.Mode.LOG 列印到日志
// Banner.Mode.OFF 不列印
.bannerMode(Banner.Mode.CONSOLE)
.run(args);
}
}
列印效果
列印到控制臺

列印到日志:INFO級別

修改banner內容
文本
方式一:在src/main/resources下新建banner.txt,里面放入想要列印的內容即可,
方式二:修改組態檔
spring:
banner:
location: file/bannerText.txt #檔案位置 src/main/resources/file/bannerText.txt
內容生成網站
文字轉換成符號:http://patorjk.com/software/taag
http://life.chacuo.net/convertfont2char
圖片轉換成符號:https://www.bootschool.net/ascii-art
圖片
和文本方式相同,但是圖片型別有限制,只能是以下三種gif,、jpg、png,
方式一:在src/main/resources下新建banner.png,里面放入想要列印的內容即可,
方式二:修改組態檔
spring:
banner:
image:
location: file/bannerImage.png #檔案位置 src/main/resources/file/bannerImage.png
列印效果

后語
本篇文章干貨不多,主要記錄探究問題的心路歷程,鍛煉文筆,若觀看文章程序有任何不適,敬請斧正,
本文來自博客園,作者:冰兀朮,轉載請注明原文鏈接:https://www.cnblogs.com/gxy2825/p/17168863.html
轉載請註明出處,本文鏈接:https://www.uj5u.com/houduan/546184.html
標籤:其他

