1. 程式人生 > >spring boot 原始碼解析5-SpringApplication#run第5步

spring boot 原始碼解析5-SpringApplication#run第5步

前言

之前的文章我們分析了SpringApplication#run方法執行的前4步,這裡我們分析第5步,列印banner.

解析

  1. SpringApplication#run方法的第5步執行如下程式碼:

    private Banner printBanner(ConfigurableEnvironment environment) {
            // 1. 首先判斷banner的輸出級別。如果禁用了,則直接返回空。
            if (this.bannerMode == Banner.Mode.OFF) {
                return null;
            }
            // 2. 獲取資源載入器ResourceLoader
    ResourceLoader resourceLoader = this.resourceLoader != null ? this.resourceLoader : new DefaultResourceLoader(getClassLoader()); // 3. 例項化SpringApplicationBannerPrinter類 SpringApplicationBannerPrinter bannerPrinter = new SpringApplicationBannerPrinter( resourceLoader, this
    .banner); // 如果banner的輸出模式是Mode.LOG,則直接將其資訊輸出到logger日誌中,否則將其輸出到控制檯,也就是System.out if (this.bannerMode == Mode.LOG) { return bannerPrinter.print(environment, this.mainApplicationClass, logger); } return bannerPrinter.print(environment, this.mainApplicationClass, System.out
    ); }

    做了4件事

    1. 如果this.bannerMode等於Banner.Mode.OFF,則直接返回空。
    2. 獲取資源載入器ResourceLoader.程式碼如下:

      ResourceLoader resourceLoader = this.resourceLoader != null ? this.resourceLoader
              : new DefaultResourceLoader(getClassLoader());

      對於當前的場景來說, SpringApplication 中的 resourceLoader為null.因此會例項化DefaultResourceLoader.

    3. 例項化SpringApplicationBannerPrinter類.程式碼如下:

      SpringApplicationBannerPrinter(ResourceLoader resourceLoader, Banner fallbackBanner) {
      this.resourceLoader = resourceLoader;
      this.fallbackBanner = fallbackBanner;
      }

      注意.在當前場景下, SpringApplicationBannerPrinter中的fallbackBanner為null.

    4. 如果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
      }
    5. 將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件事:

      1. 獲取Banner
      2. 呼叫Banner中的printBanner方法.進行banner的列印.
      3. 例項化PrintedBanner類
  2. 獲取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件事

    1. 例項化Banners.然後為其設定ImageBanner和TextBanner.如果此時anners物件的banners不為空.則返回Banners.
    2. 如果fallbackBanner不為null的話,返回fallbackBanner.對於當前場景來說fallbackBanner為null.
    3. 返回預設的banner.預設的bannenr為 SpringBootBanner.

    這裡有必要說明一下banner的繼承體系.如下:

    banner-type-tree

    其只聲明瞭一個方法.如下:

    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;
    }
    

    邏輯如下:

    1. 首先判斷是否配置了系統屬性banner.image.location,如果有直接返回ImageBanner.
    2. 如果沒有配置則在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;
    }

    還是同樣的套路.

    1. 從environment中獲取banner.location屬性,預設為banner.txt
    2. 進行載入.如果存在的話,則返回ResourceBanner.否則返回null.

    對於當前場景來說.返回的是null.

    因此,對於當前場景來說. getBanner返回的是SpringBootBanner.

  3. 接下來呼叫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件事

    1. 迴圈遍歷BANNER陣列,並依次進行陣列內容的列印
    2. 呼叫SpringBootVersion#getVersion,進行springboot版本資訊的獲取.然後為了與之前的輸出字元進行對齊,在springboot版本資訊前加空格.SpringBootVersion#getVersion程式碼如下:

      public static String getVersion() {
      Package pkg = SpringApplication.class.getPackage();
      return (pkg != null ? pkg.getImplementationVersion() : null);
      }
    3. 通過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();
      }
      1. 例項化StringBuilder進行字串拼接.
      2. 判斷是否可用.如果可以呼叫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呢?由前可知有

  1. ImageBanner
  2. ResourceBanner

那麼我們就分別看下其printBanner方法.

  1. 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件事

    1. 獲取系統環境變數中的java.awt.headless變數。
    2. 設定java.awt.headless變數值為true。並呼叫printBanner方法進行圖案的列印工作
    3. 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件事

    1. 讀取banner.image.width,預設為 76 .
      讀取banner.image.height,預設為 0 .
      讀取banner.image.margin,預設為 2.
      讀取banner.image.invert,預設為 false.
    2. 呼叫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進行讀取,最後通過讀取圖片的配置引數,進行圖片的縮放處理.

    3. 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();
      }

      沒什麼可說的,圖片是由一個一個的畫素組成的,直接輸出每個畫素即可.

  2. 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步:

    1. 獲取resource中的輸入流,並將其轉化為字串 通過environment獲取banner.charset變數,如果不存在,則預設使用UTF-8編碼
    2. 迴圈遍歷所有的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件事:

      1. 例項化resolvers集合,並新增environment元素,Environment介面繼承自PropertyResolver介面
      2. 呼叫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);
        }

        邏輯如下:

        1. 首先通過呼叫getApplicationVersion方法獲得appVersion.其是通過獲取sourceClass所在包的版本號. sourceClass為應用的啟動類
        2. 獲取Boot版本號.同樣是通過獲得SpringApplication所在包的版本號完成的
        3. 在map中存入資料.

        該方法最終的資料為:

        {application.formatted-version=, application.version=, spring-boot.formatted-version=, spring-boot.version=}

      3. 呼叫getAnsiResolver(sourceClass)方法並將其返回值新增到resolvers集合 直接設定開啟了ansi.程式碼如下:

        private PropertyResolver getAnsiResolver() {
            MutablePropertySources sources = new MutablePropertySources();
            sources.addFirst(new AnsiPropertySource("ansi", true));
            return new PropertySourcesPropertyResolver(sources);
        }
      4. 呼叫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());
        }
      5. 列印字串資訊

參考連結