SpringBoot项目启动成功后打印Banner

SpringBoot项目启动成功后打印Banner

背景

可能有些同学看到就觉得,这个都要发文章?这不是整个banner.txt再配置一下spring.banner.location=classpath:banner.txt就行了吗?还真不是,这个是在项目启动时,先打印的banner,你仔细回想是不是,一启动控制台就打印一个《spring》,我帮你回忆一下

在这里插入图片描述

而我这里说的是,项目启动成功之后,再打印banner

在这里插入图片描述

如果你想问,为啥要放在启动后?启动前不行吗?问就是**规定。

分析

梳理一下这个玩意大概要改动些什么:

  1. 搞到一个banner,如果原本就有baner那就直接拿过来
  2. 监听项目启动完成,完成后打印banner在屏幕上

看起来很简单,那就来实现一下吧

初步实现

1. 获取banner

这个我也是随便在网上找个网站生成一下的,没有刻意去做什么优化。我这里使用的网址是
http://patorjk.com/software/taag/#p=display&f=Graffiti&t=Type%20Something%20

生成后直接复制下来就好了

2. 监听项目启动完成

如果对Spring的生命周期不太熟悉的话,可能首先想到的是在main方法后直接打印banner。这确实是个可行的方法,为了主方法的简洁和考虑到时效性,我更建议在Spring生命周期里去监听并打印。我们直接对ApplicationReadyEvent事件进行监听即可。详细步骤看注释

package org.example.springboot.job;

import ch.qos.logback.classic.Logger;
import ch.qos.logback.classic.LoggerContext;
import ch.qos.logback.core.ConsoleAppender;
import ch.qos.logback.core.rolling.RollingFileAppender;
import lombok.extern.slf4j.Slf4j;
import org.slf4j.LoggerFactory;
import org.springframework.boot.context.event.ApplicationReadyEvent;
import org.springframework.context.ApplicationListener;
import org.springframework.stereotype.Component;

@Component
@Slf4j
public class PrintBanner implements ApplicationListener<ApplicationReadyEvent> {
    
    // 1. 将复制过来的BANNER按着样板放到String字符数组中,注意如果有【\】记得补上多一个斜杠
    public static final String[] BANNER = {
            " ____  ____  ____  ____ ",
            "(_  _)(  __)/ ___)(_  _)",
            "  )(   ) _) \\___ \\  )(",
            " (__) (____)(____/ (__) ",
            ":: TEST :: (v1.0.0.RELEASE)"
    };

    @Override
    public void onApplicationEvent(ApplicationReadyEvent event) {		
        // 1. 输出启动完成字样
        log.info("start finish");
        // 2. 输出banner数组
        for (String s : BANNER) {
            log.info(s);
        }

    }
}

启动完成之后,打印在控制台上的是这个样的

看起来有点怪怪的,为啥我的前面会打印那么多信息,人家Spring自己的又不会,为了应付交差,得先想出2.0版本。

应付方案

如果用数组的话,会打印多行的信息,那我直接用字符串在前面加个换行符,再把字符拼接起来,那变成只输出一行信息会不会好看点。

package org.example.springboot.job;

import ch.qos.logback.classic.Logger;
import ch.qos.logback.classic.LoggerContext;
import ch.qos.logback.core.ConsoleAppender;
import ch.qos.logback.core.rolling.RollingFileAppender;
import lombok.extern.slf4j.Slf4j;
import org.slf4j.LoggerFactory;
import org.springframework.boot.context.event.ApplicationReadyEvent;
import org.springframework.context.ApplicationListener;
import org.springframework.stereotype.Component;

@Component
@Slf4j
public class PrintBanner implements ApplicationListener<ApplicationReadyEvent> {
    
    // 1. 替换成字符串
    public static final String testString =
            "\n"+
            "ooooooooooo ooooooooooo  oooooooo8 ooooooooooo \n" +
            "88  888  88  888    88  888        88  888  88 \n" +
            "    888      888ooo8     888oooooo     888     \n" +
            "    888      888    oo          888    888     \n" +
            "   o888o    o888ooo8888 o88oooo888    o888o    \n" +
            "                                               ";

    @Override
    public void onApplicationEvent(ApplicationReadyEvent event) {		
        // 1. 输出启动完成字样
        log.info("start finish");
        // 2. 输出banner长字符串
        log.info(testString);

    }
}

输出的样式是达到初步目的的,只打印出一行,而且也是靠左排列,看起来好一点了,能忽悠过去了。

探究原理

但是,我们像是只是为了交差的人吗?我们是追求艺术与完美的人。你看看人家SpringBoot启动的banner,同样是logger.info,为什么它可以不打印前面的那堆信息?而打印完banner之后,logger打印的信息就会有详细的时间、线程等信息?

在这里插入图片描述

这问题估计有点小刁钻,百度了一波也没看到答案。源码又不知道从何跟起,我### 原理猜测

猜测是两个可能:

第一种是打印banner的logger和打印其它的信息不属于同一个日志控件,比如说打印banner的log是org.apache.commons.logging.Log,而打印启动信息的是logback框架的。可是我用org.apache.commons.logging.Log打印出来的还是会自带时间、线程等信息。

第二种是用的是同一个Logger,但appender配置有两份,在打印banner时,不打印时间和线程等信息,打印完banner之后切换了appender。所以之后打印的都带上了时间和线程等信息。可是我阅读了SpringBoot源码的logback与Log4j都不存在两份配置。

源码分析

这就很神奇,于是我静下心来看一下SpringAppcation.run中关于banner的执行流程。我发现上面图上的那个其实是要打开banner的日志模式才能输出到日志里的。。。。。猜了半天原来是我摸错了。默认的情况下,使用的是System.out来输出banner,那肯定不会有时间、线程的信息啊。

在这里插入图片描述

于是我开启了Mode.LOG模式,再测试了一遍,这多余的时间和线程信息击碎了我的所有幻想,瞬间觉得不过如此。

在这里插入图片描述

在这里插入图片描述

不过也不是说一无是处,它的banner其实同样是一个字符串数组,但它很巧妙地用字节输出流来避免了我上面的情况,输出很多重复的日志信息。至于是使用字符串数组还是长字符串拼接,我们探究一下:

从性能的角度来看,输出一个字符串数组会比输出一个具有多个换行符的长字符串更好,尤其是当数组中的字符串数量较少时。这是因为在输出长字符串时,JVM需要一次性处理整个字符串,并在其中查找并替换所有换行符,这可能会导致性能下降。

相比之下,输出一个字符串数组只需要遍历数组并逐个输出每个字符串,这通常比处理整个字符串更快。此外,如果字符串数组已经存在,那么输出它将比创建一个新的具有多个换行符的字符串要快得多。

当然这点性能的优化几乎无感,只不过你没写好的话,被那些老懂哥看了少不了一顿批。

完善应急方案

我们结合SpringBoot的banner打印实现来优化我们的代码

package org.example.springboot.job;

import ch.qos.logback.classic.Logger;
import ch.qos.logback.classic.LoggerContext;
import ch.qos.logback.core.ConsoleAppender;
import ch.qos.logback.core.rolling.RollingFileAppender;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.SpringBootVersion;
import org.springframework.boot.ansi.AnsiColor;
import org.springframework.boot.ansi.AnsiOutput;
import org.springframework.boot.ansi.AnsiStyle;
import org.springframework.boot.context.event.ApplicationReadyEvent;
import org.springframework.context.ApplicationListener;
import org.springframework.stereotype.Component;

import java.io.ByteArrayOutputStream;
import java.io.PrintStream;
import java.io.UnsupportedEncodingException;

@Component

public class PrintBanner implements ApplicationListener<ApplicationReadyEvent> {

    @Value("${test.version}")
    private String version;

    private String bannerName = " :: Test :: ";
    
    public static final String[] BANNER = {
            "",
            " ____  ____  ____  ____ ",
            "(_  _)(  __)/ ___)(_  _)",
            "  )(   ) _) \\___ \\  )(",
            " (__) (____)(____/ (__) "
    };

    @Override
    public void onApplicationEvent(ApplicationReadyEvent event) {
        Log log = LogFactory.getLog(PrintBanner.class);
        log.info("start finish");

        ByteArrayOutputStream baos = new ByteArrayOutputStream();
        PrintStream printStream = new PrintStream(baos);
        for (String s : BANNER) {
            printStream.println(s);
        }

        version = (version != null) ? " (v" + version + ")" : "";
        StringBuilder padding = new StringBuilder();
        while (padding.length() < 24 - (version.length() + bannerName.length())) {
            padding.append(" ");
        }

        printStream.println(AnsiOutput.toString(AnsiColor.GREEN, bannerName, AnsiColor.DEFAULT, padding.toString(),
                AnsiStyle.FAINT, version));
        try {
            log.info(baos.toString("UTF-8"));
        } catch (UnsupportedEncodingException e) {
            throw new RuntimeException(e);
        }

    }

}

输出效果是这样的

在这里插入图片描述

到了这一步,几乎就满足大多数人的刁难要求了。别人家的SpringBoot也是这样,一启动也打印了时间和线程信息。

精益求精

可是,如果是**领导的强制要求对吧,或者是你自己想探究一下,能不能真的做到日志打印banner不要带有时间线程信息?还真的行。

方法探究

要实现这个功能,就得回顾一下我们前面分析方案的时候提到的两个方案:

  1. 维护一个额外的日志框架,只用于打印banner;
  2. 以logback为例,新建一个appender用于打印banner时只打印%msg,打印完banner之后再设置回来,原理是动态设置root节点的appender

研究一下可行性,第一种方案多维护一个日志框架,且只用于打印banner,这会给后续的维护带来麻烦,且如果框架发生了什么漏洞,又得给你维护一套。第二种方案维护多一个appender,但要来回切换,会对性能造成一定的损耗,且如果改造了异步日志,在打印banner的时候需要睡眠1ms以达到打印的图案没有出现乱序。

权衡各种情况,我觉得可行方案是第二种。

实现步骤

在原有的logback-spring.xml文件中,新配置了一个BANNER appender。添加一个Pattern,只打印%msg,这个appender不用先放到root节点下。在程序启动的过程中去获取设置。

<?xml version="1.0" encoding="UTF-8" ?>
<configuration>
    <include resource="org/springframework/boot/logging/logback/default.xml"/>
    <property name="LOG_FOR_ROLLING" value="%d{yyyy-MM-dd HH:mm:ss.SSS} %-5level[%thread]%logger.%method:%L: %msg%n"/>
    <property name="LOG_FOR_BANNER" value="%msg%n"/>

    <appender name="BANNER" class="ch.qos.logback.core.ConsoleAppender">
        <encoder>
            <pattern>${LOG_FOR_BANNER}</pattern>
            <charset>utf8</charset>
        </encoder>
    </appender>
    <appender name="ROLLING" class="ch.qos.logback.core.rolling.RollingFileAppender">
        <file>test-app.log</file>
        <rollingPolicy class="ch.qos.logback.core.rolling.SizeAndTimeBasedRollingPolicy">
            <fileNamePattern>test-%d{yyyy-MM-dd}.%i.log</fileNamePattern>
            <maxFileSize>100MB</maxFileSize>
            <maxHistory>360</maxHistory>
            <totalSizeCap>50GB</totalSizeCap>
        </rollingPolicy>

        <encoder>
            <pattern>${LOG_FOR_ROLLING}</pattern>
            <charset>utf8</charset>
        </encoder>
    </appender>
    <appender name="CONSOLE" class="ch.qos.logback.core.ConsoleAppender">
        <encoder>
            <pattern>${LOG_FOR_ROLLING}</pattern>
            <charset>utf8</charset>
        </encoder>
    </appender>

    <logger name="loggerForBanner">
        <appender-ref ref="BANNER"/>
    </logger>
    <logger name="loggerForRolling">
        <appender-ref ref="ROLLING"/>
    </logger>
    <logger name="loggerForConsole">
        <appender-ref ref="CONSOLE"/>
    </logger>

    <root level="INFO">
        <appender-ref ref="ROLLING"/>
        <appender-ref ref="CONSOLE"/>
    </root>

</configuration>

程序改造

package org.example.springboot.job;

import ch.qos.logback.classic.Logger;
import ch.qos.logback.classic.LoggerContext;
import ch.qos.logback.core.ConsoleAppender;
import ch.qos.logback.core.rolling.RollingFileAppender;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.SpringBootVersion;
import org.springframework.boot.ansi.AnsiColor;
import org.springframework.boot.ansi.AnsiOutput;
import org.springframework.boot.ansi.AnsiStyle;
import org.springframework.boot.context.event.ApplicationReadyEvent;
import org.springframework.context.ApplicationListener;
import org.springframework.stereotype.Component;

import java.io.ByteArrayOutputStream;
import java.io.PrintStream;
import java.io.UnsupportedEncodingException;

@Component
@Slf4j
public class PrintBanner implements ApplicationListener<ApplicationReadyEvent> {

    @Value("${test.version}")
    private String version;

    private String bannerName = " :: Test :: ";
    
    public static final String[] BANNER = {
//            "",
            " ____  ____  ____  ____ ",
            "(_  _)(  __)/ ___)(_  _)",
            "  )(   ) _) \\___ \\  )(",
            " (__) (____)(____/ (__) "
    };

    @Override
    public void onApplicationEvent(ApplicationReadyEvent event) {
        log.info("start finish");
        changeAppender("BANNER");
        printBanner();
        changeAppender("NORMAL");
    }

    private void printBanner() {
        ByteArrayOutputStream baos = new ByteArrayOutputStream();
        PrintStream printStream = new PrintStream(baos);
        for (String s : BANNER) {
            printStream.println(s);
        }

        version = (version != null) ? " (v" + version + ")" : "";
        StringBuilder padding = new StringBuilder();
        while (padding.length() < 24 - (version.length() + bannerName.length())) {
            padding.append(" ");
        }

        printStream.println(AnsiOutput.toString(AnsiColor.GREEN, bannerName, AnsiColor.DEFAULT, padding.toString(),
                AnsiStyle.FAINT, version));
        try {
            log.info(baos.toString("UTF-8"));
        } catch (UnsupportedEncodingException e) {
            throw new RuntimeException(e);
        }
    }

    private static void changeAppender(String logType) {
        // 获取log配置文件上下文
        LoggerContext logger = (LoggerContext) LoggerFactory.getILoggerFactory();
        // 获取根节点root
        Logger loggerForRoot = logger.getLogger("root");
        Logger loggerForRolling = logger.getLogger("loggerForRolling");
        // root节点移除appender组件
        loggerForRoot.detachAppender("CONSOLE");
        loggerForRoot.detachAppender("ROLLING");
        loggerForRoot.detachAppender("BANNER");

        RollingFileAppender rollingAppender = (RollingFileAppender) loggerForRolling.getAppender("ROLLING");
        // 由于打印banner和打印普通日志不一样,则要根据不同的模式切换root节点的appender
        switch (logType) {
            case "BANNER" : {
                Logger loggerForBanner = logger.getLogger("loggerForBanner");
                ConsoleAppender bannerAppender = (ConsoleAppender) loggerForBanner.getAppender("BANNER");
                // 这一步是为了输出的日志文件和控制台统一都使用banner的encoder
                rollingAppender.setEncoder(bannerAppender.getEncoder());
                // 把appender放回去root就能激活使用
                loggerForRoot.addAppender(rollingAppender);
                loggerForRoot.addAppender(bannerAppender);
            }
            break;
            case "NORMAL" : {
                Logger loggerForConsole = logger.getLogger("loggerForConsole");
                ConsoleAppender consoleAppender = (ConsoleAppender) loggerForConsole.getAppender("CONSOLE");
                rollingAppender.setEncoder(consoleAppender.getEncoder());
                loggerForRoot.addAppender(rollingAppender);
                loggerForRoot.addAppender(consoleAppender);
            }
            break;
        }

    }

}

输出的效果是

在这里插入图片描述
输出的日志与控制台一致,且访问打印log也会带上时间、线程等信息
在这里插入图片描述

  • 7
    点赞
  • 5
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
SpringBoot项目启动彩蛋是指在项目启动时,通过在控制台输出特定的Logo或者文字画面来增加启动的趣味性。 要实现SpringBoot项目启动彩蛋,可以通过修改`banner.txt`文件来替换默认的Spring Logo。这个文件可以包含自定义的Logo或者文字画面。 如果你想生成自己的Logo或文字画面,可以使用在线工具来生成,比如http://patorjk.com/software/taag、http://www.network-science.de/ascii/、http://www.degraeve.com/img2txt.php。这些工具可以根据你输入的内容生成相应的Logo或文字画面,你可以将生成的内容复制到`banner.txt`文件中,然后替换默认的Spring Logo。<span class="em">1</span><span class="em">2</span><span class="em">3</span> #### 引用[.reference_title] - *1* *2* [SpringBoot项目启动彩蛋与启动完成提示修改](https://blog.csdn.net/shouchenchuan5253/article/details/107647377)[target="_blank" data-report-click={"spm":"1018.2226.3001.9630","extra":{"utm_source":"vip_chatgpt_common_search_pc_result","utm_medium":"distribute.pc_search_result.none-task-cask-2~all~insert_cask~default-1-null.142^v93^chatsearchT3_1"}}] [.reference_item style="max-width: 50%"] - *3* [git启动文件彩蛋](https://download.csdn.net/download/weixin_41847607/10683713)[target="_blank" data-report-click={"spm":"1018.2226.3001.9630","extra":{"utm_source":"vip_chatgpt_common_search_pc_result","utm_medium":"distribute.pc_search_result.none-task-cask-2~all~insert_cask~default-1-null.142^v93^chatsearchT3_1"}}] [.reference_item style="max-width: 50%"] [ .reference_list ]

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值