SpringBoot啟動控制台的banner是怎麼回事

来源:https://www.cnblogs.com/gxy2825/archive/2023/03/08/17168863.html
-Advertisement-
Play Games

前言 每次啟動SpringBoot項目時,總是能看到控制台列印了一串字元,隱約能辨認出是“Spring”,不知大家是否也好奇過是怎麼實現的,是直接列印固定的字元串,還是根據什麼演算法去生成的?於是閑暇無事,探究一番。 只想修改banner可以跳到文末查看 SpringBoot是怎麼列印的 Banner ...


前言

每次啟動SpringBoot項目時,總是能看到控制台列印了一串字元,隱約能辨認出是“Spring”,不知大家是否也好奇過是怎麼實現的,是直接列印固定的字元串,還是根據什麼演算法去生成的?於是閑暇無事,探究一番。
image

只想修改banner可以跳到文末查看

SpringBoot是怎麼列印的

Banner預設實現類 SpringBootBanner

1、根據控制台列印的字元進行全局搜索,筆者選取:: Spring Boot ::進行搜索,定位到了org.springframework.boot.SpringBootBanner

IDEA全局搜索:CTRL + SHIFT + R

image

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、往下看到類的屬性BANNERSPRING_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或者全局搜索可以發現SpringBootBannerSpringApplicationBannerPrinter類中作為類變數,大概能猜測出這個SpringApplicationBannerPrinter類是Banner列印的核心控制器。


2、進入SpringApplicationBannerPrinter類,照例先看註釋Class used by SpringApplication to print the application banner.,意思是當前類被SpringApplication用來列印banner。

這個SpringApplication好像有點眼熟,名字和我們SpringBoot項目的啟動類有點相似,翻翻啟動類的代碼,想起我們就是通過SpringApplicationrun方法啟動項目,banner列印調用也是由SpringApplication控制的,後續會詳細分析。(占坑,後續SpringBoot啟動流程也會出一篇博客去探討一下)
image

回歸正題,繼續從類的屬性開始看,根據名字猜測大概含義,留待後續驗證:

  1. BANNER_LOCATION_PROPERTY:Spring配置,大概是banner文件的路徑。
  2. BANNER_IMAGE_LOCATION_PROPERTY:Spring配置,banner圖片的路徑(存疑,控制台難道能列印圖片?)。
  3. DEFAULT_BANNER_LOCATION = "banner.txt":取值是txt文件,猜測是banner文件的預設位置。
  4. String[] IMAGE_EXTENSION = { "gif", "jpg", "png" }:取值是常見圖片的尾碼,結合第二個屬性猜測是用來對banner圖片類型做限制。
  5. DEFAULT_BANNER = new SpringBootBanner():把上節分析的SpringBootBanner當做Banner預設實現類
  6. ResourceLoader resourceLoaderResourceLoader簡單來說是Spring載入資源的統一抽象,由實現類提供具體邏輯。
    在Spring中讀取xml配置文件載入應用上下文的ClassPathXmlApplicationContext,就是ResourceLoader的子類。
  7. Banner fallbackBanner:翻譯過來是回退banner,暫時猜不出作用,等待後續填坑。

3、往下看方法,只有兩個非私有方法,都是print的重載方法,差別在於第三個參數,分別是Log loggerPrintStream out,代表這兩個方法分別負責日誌列印和控制台列印。

緊扣主題,先看負責控制台列印的方法。

	Banner print(Environment environment, Class<?> sourceClass, PrintStream out) {
		Banner banner = getBanner(environment);
		banner.printBanner(environment, sourceClass, out);
		return new PrintedBanner(banner, sourceClass);
	}

代碼很精簡,第一行獲取Banner類,第二行調用Bannerprint方法列印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);
    }

}

接著就是調用getImageBannergetTextBanner方法獲取Banner,如果Banner數組不為空則返回,否則檢查fallbackBanner

這個fallbackBanner光看名字看不出是什麼,使用CTRL+B查看引用,發現是在SpringApplication#printBanner里註入進來的,如下圖。
image

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

SpringApplicationBuilder是通過Constructor(構造器)模式實現的SpringApplication構造器。
查看banner方法的註釋,我們可以知道這裡註入的Banner實例會在沒有靜態banner文件時使用
回過頭來,fallbackBanner的坑填上了,它是在SpringApplicationBannerPrinter找不到txt文件或者圖片作為banner素材的時候使用。

如果fallbackBanner也為空,則最終返回兜底方案-SpringBootBanner

getBanner的結構分析完了,實際情況我們知道走的是兜底方案,也就是只要我們能讓getImageBannergetTextBanner或者fallbackBanner不為空,就能改變banner列印的圖案。
帶著這個想法,我們就去看看getImageBannergetTextBanner是咋回事。


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
image
繼續尋找,最終定位到了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);
    }
}

列印效果
列印到控制台
image

列印到日誌:INFO級別
image

修改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

列印效果
image

後語

本篇文章乾貨不多,主要記錄探究問題的心路歷程,鍛煉文筆,若觀看文章過程有任何不適,敬請斧正。

本文來自博客園,作者:冰兀朮,轉載請註明原文鏈接:https://www.cnblogs.com/gxy2825/p/17168863.html


您的分享是我們最大的動力!

-Advertisement-
Play Games
更多相關文章
  • 這篇文章描述如何使用消息隊列中的事務消息機制實現分散式事務。事務消息適用於需要非同步更新數據,並且對數據實時性要求不太高的場景。 ...
  • ##一、通訊錄準備 #####1. 通訊錄信息的準備 #####2. 通訊錄功能的框架 #####3. 文件安排 ##二、實現通訊錄的功能 #####1. 添加功能 #####2. 刪除功能 #####3. 展示功能 #####4. 更改功能 #####5. 查找功能 #####6. 排序功能 ## ...
  • C++中的仿函數(function object)是一個重載了函數調用運算符(operator())的類或結構體,在使用時可以像函數一樣調用。通過仿函數,C++程式員可以更加靈活地實現自己的演算法。 ...
  • 緩存的重要性 簡而言之,緩存的原理就是利用空間來換取時間。通過將數據存到訪問速度更快的空間里以便下一次訪問時直接從空間里獲取,從而節省時間。 我們以CPU的緩存體係為例: CPU緩存體系是多層級的。分成了CPU -> L1 -> L2 -> L3 -> 主存。我們可以得到以下啟示。 越頻繁使用的數據 ...
  • 前言: Servlet 容器初始化 Servlet 時,會為這個 Servlet 創建一個 ServletConfig 對象,並將 ServletConfig 對象作為參數傳遞給 Servlet 。通過 ServletConfig 對象即可獲得當前 Servlet 的初始化參數信息。一個 Web 應 ...
  • Tread多線程 什麼是線程? 線程(Thread)是一個程式內部的一條執行流程。 程式中如果只有一條執行流程,那這個程式就是單線程的程式。 多線程是什麼? 多線程是指從軟硬體上實現的多條執行流程的技術(多條線程由cpu負責調度執行)。 多線程的創建方式 方式一:繼承Thread ①定義一個子類My ...
  • python的基本認識 初識python: python是一種跨平臺的、開源的、免費的、解釋型的高級編程語言; python的應用領域十分廣泛、如web編程、圖像處理、黑客編程、網路爬蟲和科學計算等; python學習簡單、開發速度快、節省時間和精力。 python具有豐富和強大的庫,能夠把用其他語 ...
  • 昨天木子問我能不能做自動刷某音短視頻,還要自動刷小哥哥,不是小哥哥就划走。 我心想,這女人真麻煩,怎麼這麼多事。 不好好工作天天想著小哥哥! 為了不得罪她,當時我就先答應了下來,然而實際上我把小哥哥變成了小姐姐,刷什麼小哥哥,多沒品味! 好了,話不多說,我們直接上代碼! 代碼實戰 首先導入需要使用的 ...
一周排行
    -Advertisement-
    Play Games
  • 移動開發(一):使用.NET MAUI開發第一個安卓APP 對於工作多年的C#程式員來說,近來想嘗試開發一款安卓APP,考慮了很久最終選擇使用.NET MAUI這個微軟官方的框架來嘗試體驗開發安卓APP,畢竟是使用Visual Studio開發工具,使用起來也比較的順手,結合微軟官方的教程進行了安卓 ...
  • 前言 QuestPDF 是一個開源 .NET 庫,用於生成 PDF 文檔。使用了C# Fluent API方式可簡化開發、減少錯誤並提高工作效率。利用它可以輕鬆生成 PDF 報告、發票、導出文件等。 項目介紹 QuestPDF 是一個革命性的開源 .NET 庫,它徹底改變了我們生成 PDF 文檔的方 ...
  • 項目地址 項目後端地址: https://github.com/ZyPLJ/ZYTteeHole 項目前端頁面地址: ZyPLJ/TreeHoleVue (github.com) https://github.com/ZyPLJ/TreeHoleVue 目前項目測試訪問地址: http://tree ...
  • 話不多說,直接開乾 一.下載 1.官方鏈接下載: https://www.microsoft.com/zh-cn/sql-server/sql-server-downloads 2.在下載目錄中找到下麵這個小的安裝包 SQL2022-SSEI-Dev.exe,運行開始下載SQL server; 二. ...
  • 前言 隨著物聯網(IoT)技術的迅猛發展,MQTT(消息隊列遙測傳輸)協議憑藉其輕量級和高效性,已成為眾多物聯網應用的首選通信標準。 MQTTnet 作為一個高性能的 .NET 開源庫,為 .NET 平臺上的 MQTT 客戶端與伺服器開發提供了強大的支持。 本文將全面介紹 MQTTnet 的核心功能 ...
  • Serilog支持多種接收器用於日誌存儲,增強器用於添加屬性,LogContext管理動態屬性,支持多種輸出格式包括純文本、JSON及ExpressionTemplate。還提供了自定義格式化選項,適用於不同需求。 ...
  • 目錄簡介獲取 HTML 文檔解析 HTML 文檔測試參考文章 簡介 動態內容網站使用 JavaScript 腳本動態檢索和渲染數據,爬取信息時需要模擬瀏覽器行為,否則獲取到的源碼基本是空的。 本文使用的爬取步驟如下: 使用 Selenium 獲取渲染後的 HTML 文檔 使用 HtmlAgility ...
  • 1.前言 什麼是熱更新 游戲或者軟體更新時,無需重新下載客戶端進行安裝,而是在應用程式啟動的情況下,在內部進行資源或者代碼更新 Unity目前常用熱更新解決方案 HybridCLR,Xlua,ILRuntime等 Unity目前常用資源管理解決方案 AssetBundles,Addressable, ...
  • 本文章主要是在C# ASP.NET Core Web API框架實現向手機發送驗證碼簡訊功能。這裡我選擇是一個互億無線簡訊驗證碼平臺,其實像阿裡雲,騰訊雲上面也可以。 首先我們先去 互億無線 https://www.ihuyi.com/api/sms.html 去註冊一個賬號 註冊完成賬號後,它會送 ...
  • 通過以下方式可以高效,並保證數據同步的可靠性 1.API設計 使用RESTful設計,確保API端點明確,並使用適當的HTTP方法(如POST用於創建,PUT用於更新)。 設計清晰的請求和響應模型,以確保客戶端能夠理解預期格式。 2.數據驗證 在伺服器端進行嚴格的數據驗證,確保接收到的數據符合預期格 ...