spring boot 原始碼解析5-SpringApplication#run第5步
前言
之前的文章我們分析了SpringApplication#run方法執行的前4步,這裡我們分析第5步,列印banner.
解析
SpringApplication#run方法的第5步執行如下程式碼:
private Banner printBanner(ConfigurableEnvironment environment) { // 1. 首先判斷banner的輸出級別。如果禁用了,則直接返回空。 if (this.bannerMode == Banner.Mode.OFF) { return null; } // 2. 獲取資源載入器ResourceLoader
做了4件事
- 如果this.bannerMode等於Banner.Mode.OFF,則直接返回空。
獲取資源載入器ResourceLoader.程式碼如下:
ResourceLoader resourceLoader = this.resourceLoader != null ? this.resourceLoader : new DefaultResourceLoader(getClassLoader());
對於當前的場景來說, SpringApplication 中的 resourceLoader為null.因此會例項化DefaultResourceLoader.
例項化SpringApplicationBannerPrinter類.程式碼如下:
SpringApplicationBannerPrinter(ResourceLoader resourceLoader, Banner fallbackBanner) { this.resourceLoader = resourceLoader; this.fallbackBanner = fallbackBanner; }
注意.在當前場景下, SpringApplicationBannerPrinter中的fallbackBanner為null.
如果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 }
將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件事:
- 獲取Banner
- 呼叫Banner中的printBanner方法.進行banner的列印.
- 例項化PrintedBanner類
獲取banner的方法如下:
private Banner getBanner(Environment environment, Banner definedBanner) { Banners banners = new Banners(); banners.addIfNotNull(getImageBanner(environment)); banners.addIfNotNull(getTextBanner(environment)); // 如果Banners物件的banners不為空,也就是至少找到了banner.gif,banner.jpg,banner.png,banner.txt其中的一個,那麼返回該Banners物件,否則返回預設的SpringBootBanner物件 if (banners.hasAtLeastOneBanner()) { return banners; } if (this.fallbackBanner != null) { return this.fallbackBanner; } return DEFAULT_BANNER; }
做了3件事
- 例項化Banners.然後為其設定ImageBanner和TextBanner.如果此時anners物件的banners不為空.則返回Banners.
- 如果fallbackBanner不為null的話,返回fallbackBanner.對於當前場景來說fallbackBanner為null.
- 返回預設的banner.預設的bannenr為 SpringBootBanner.
這裡有必要說明一下banner的繼承體系.如下:
其只聲明瞭一個方法.如下:
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); }
Banners例項化後,會呼叫getImageBanner方法進行載入.程式碼如下:
static final String[] IMAGE_EXTENSION = { "gif", "jpg", "png" }; 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; }
邏輯如下:
- 首先判斷是否配置了系統屬性banner.image.location,如果有直接返回ImageBanner.
如果沒有配置則在classpath中查詢banner.gif,banner.jpg,banner.png,如果找到,則建立一個ImageBanner物件並新增到Banners物件的banners屬性中,該屬性是一個List.程式碼如下:
private final List<Banner> banners = new ArrayList<Banner>(); 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_PROPERTY, DEFAULT_BANNER_LOCATION); Resource resource = this.resourceLoader.getResource(location); if (resource.exists()) { return new ResourceBanner(resource); } return null; }
還是同樣的套路.
- 從environment中獲取banner.location屬性,預設為banner.txt
- 進行載入.如果存在的話,則返回ResourceBanner.否則返回null.
對於當前場景來說.返回的是null.
因此,對於當前場景來說. getBanner返回的是SpringBootBanner.
接下來呼叫SpringBootBanner#printBanner方法.程式碼如下:
private static final String[] BANNER = { "", " . ____ _ __ _ _", " /\\\\ / ___'_ __ _ _(_)_ __ __ _ \\ \\ \\ \\", "( ( )\\___ | '_ | '_| | '_ \\/ _` | \\ \\ \\ \\", " \\\\/ ___)| |_)| | | | | || (_| | ) ) ) )", " ' |____| .__|_| |_|_| |_\\__, | / / / /", " =========|_|==============|___/=/_/_/_/" }; private static final String SPRING_BOOT = " :: Spring Boot :: "; private static final int STRAP_LINE_SIZE = 42; 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 + ")"); String padding = ""; while (padding.length() < STRAP_LINE_SIZE - (version.length() + SPRING_BOOT.length())) { padding += " "; } printStream.println(AnsiOutput.toString(AnsiColor.GREEN, SPRING_BOOT, AnsiColor.DEFAULT, padding, AnsiStyle.FAINT, version)); printStream.println(); }
做了3件事
- 迴圈遍歷BANNER陣列,並依次進行陣列內容的列印
呼叫SpringBootVersion#getVersion,進行springboot版本資訊的獲取.然後為了與之前的輸出字元進行對齊,在springboot版本資訊前加空格.SpringBootVersion#getVersion程式碼如下:
public static String getVersion() { Package pkg = SpringApplication.class.getPackage(); return (pkg != null ? pkg.getImplementationVersion() : null); }
通過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(); }
- 例項化StringBuilder進行字串拼接.
判斷是否可用.如果可以呼叫buildEnabled.否則呼叫buildDisabled. isEnabled方法如下:
private static boolean isEnabled() { if (enabled == Enabled.DETECT) { // 預設走到這裡. if (ansiCapable == null) { // 對於當前場景來說.ansiCapable 為 null.因此會執行detectIfAnsiCapable方法 ansiCapable = detectIfAnsiCapable(); } return ansiCapable; } return enabled == 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(Enum.valueOf(Enabled.class, enabled.toUpperCase())); } if (resolver.containsProperty("console-available")) { AnsiOutput.setConsoleAvailable( resolver.getProperty("console-available", Boolean.class)); } }
對於當前場景來說. resolver中是含有spring.output.ansi.enabled 的配置的.預設為true
因此會將AnsiOutput的enabled 設定為Enabled.ALWAYS.
因此這裡會執行buildEnabled方法.程式碼如下:
private static void buildEnabled(StringBuilder sb, Object[] elements) { boolean writingAnsi = false; boolean containsEncoding = false; for (Object element : elements) { if (element instanceof AnsiElement) { containsEncoding = true; if (!writingAnsi) { sb.append(ENCODE_START); writingAnsi = true; } else { sb.append(ENCODE_JOIN); } } else { if (writingAnsi) { sb.append(ENCODE_END); writingAnsi = false; } } sb.append(element); } if (containsEncoding) { sb.append(writingAnsi ? ENCODE_JOIN : ENCODE_START); sb.append(RESET); sb.append(ENCODE_END); } }
這裡返回的字串為
[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呢?由前可知有
- ImageBanner
- ResourceBanner
那麼我們就分別看下其printBanner方法.
ImageBanner#printBanner 程式碼如下:
public 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"); printBanner(environment, out); } catch (Throwable ex) { logger.warn("Image banner not printable: " + this.image + " (" + ex.getClass() + ": '" + ex.getMessage() + "')"); logger.debug("Image banner printing failure", ex); } finally { // 3. finally中還原作業系統中的java.awt.headless環境變數值 if (headless == null) { System.clearProperty("java.awt.headless"); } else { System.setProperty("java.awt.headless", headless); } } }
做了3件事
- 獲取系統環境變數中的java.awt.headless變數。
- 設定java.awt.headless變數值為true。並呼叫printBanner方法進行圖案的列印工作
- 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 = properties.getProperty("width", Integer.class, 76); int height = properties.getProperty("height", Integer.class, 0); int margin = properties.getProperty("margin", Integer.class, 2); boolean invert = properties.getProperty("invert", Boolean.class, false); BufferedImage image = readImage(width, height); printBanner(image, margin, invert, out); }
還是3件事
- 讀取banner.image.width,預設為 76 .
讀取banner.image.height,預設為 0 .
讀取banner.image.margin,預設為 2.
讀取banner.image.invert,預設為 false. 呼叫readImage 進行圖片的讀取.程式碼如下:
private BufferedImage readImage(int width, int height) throws IOException { InputStream inputStream = this.image.getInputStream(); try { BufferedImage image = ImageIO.read(inputStream); return resizeImage(image, width, height); } finally { inputStream.close(); } }
通過ImageIO進行讀取,最後通過讀取圖片的配置引數,進行圖片的縮放處理.
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++) { for (int i = 0; i < margin; i++) { out.print(" "); } for (int 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(getAsciiPixel(color, invert)); } out.println(); } out.print(AnsiOutput.encode(AnsiColor.DEFAULT)); out.print(AnsiOutput.encode(AnsiBackground.DEFAULT)); out.println(); }
沒什麼可說的,圖片是由一個一個的畫素組成的,直接輸出每個畫素即可.
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步:
- 獲取resource中的輸入流,並將其轉化為字串 通過environment獲取banner.charset變數,如果不存在,則預設使用UTF-8編碼
迴圈遍歷所有的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件事:
- 例項化resolvers集合,並新增environment元素,Environment介面繼承自PropertyResolver介面
呼叫getVersionResolver(sourceClass)方法並將其返回值新增到resolvers集合
程式碼如下:
private PropertyResolver getVersionResolver(Class<?> sourceClass) { MutablePropertySources propertySources = new MutablePropertySources(); propertySources .addLast(new MapPropertySource("version", getVersionsMap(sourceClass))); 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); }
邏輯如下:
- 首先通過呼叫getApplicationVersion方法獲得appVersion.其是通過獲取sourceClass所在包的版本號. sourceClass為應用的啟動類
- 獲取Boot版本號.同樣是通過獲得SpringApplication所在包的版本號完成的
- 在map中存入資料.
該方法最終的資料為:
{application.formatted-version=, application.version=, spring-boot.formatted-version=, spring-boot.version=}
呼叫getAnsiResolver(sourceClass)方法並將其返回值新增到resolvers集合 直接設定開啟了ansi.程式碼如下:
private PropertyResolver getAnsiResolver() { MutablePropertySources sources = new MutablePropertySources(); sources.addFirst(new AnsiPropertySource("ansi", true)); return new PropertySourcesPropertyResolver(sources); }
呼叫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()); }
列印字串資訊