SpringBoot项目启动成功后打印Banner

news/2024/3/29 18:10:19/文章来源:https://blog.csdn.net/interestANd/article/details/129257076

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)"};@Overridepublic 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" +"                                               ";@Overridepublic 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;@Componentpublic class PrintBanner implements ApplicationListener<ApplicationReadyEvent> {@Value("${test.version}")private String version;private String bannerName = " :: Test :: ";public static final String[] BANNER = {""," ____  ____  ____  ____ ","(_  _)(  __)/ ___)(_  _)","  )(   ) _) \\___ \\  )("," (__) (____)(____/ (__) "};@Overridepublic 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 = {
//            ""," ____  ____  ____  ____ ","(_  _)(  __)/ ___)(_  _)","  )(   ) _) \\___ \\  )("," (__) (____)(____/ (__) "};@Overridepublic 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();// 获取根节点rootLogger 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节点的appenderswitch (logType) {case "BANNER" : {Logger loggerForBanner = logger.getLogger("loggerForBanner");ConsoleAppender bannerAppender = (ConsoleAppender) loggerForBanner.getAppender("BANNER");// 这一步是为了输出的日志文件和控制台统一都使用banner的encoderrollingAppender.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也会带上时间、线程等信息
在这里插入图片描述

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.luyixian.cn/news_show_75434.aspx

如若内容造成侵权/违法违规/事实不符,请联系dt猫网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

Big_Data

Linux 计算机硬件软件体系 冯 诺依曼体系结构 计算机处理的数据和指令一律用二进制数表示 顺序执行程序 计算机硬件由运算器、控制器、存储器、输入设备和输出设备五大部分组成计算机硬件组成 输入设备输入设备用来将人们熟悉的信息形式转换为机器能够识别的信息形式常见的…

人工智能写的十段代码,九个通过测试了

“抢走你工作的不会是 AI &#xff0c;而是先掌握 AI 能力的人” 编程测试 1. 我想用golang实现二叉树前序&#xff0c;请你帮我写一下代码。 // 定义二叉树节点 type TreeNode struct {Val intLeft *TreeNodeRight *TreeNode }// 前序遍历 func PreOrderTraversal(root *Tre…

Nvidia jetson nano硬件架构

资料来源 官方文档中心 https://developer.nvidia.com/embedded/downloads -> 选jetson -> Jetson Nano Product Design Guide //产品设计指导(入口) //-> 1.1 References 列出了相关的文档 -> Jetson Nano Developer Kit Carrier Board Specification //板子标注…

torchserve安装、模型的部署与测试(基于docker)

问题描述 pytorch 一直很受大家的欢迎&#xff0c;但是作为一个深度模型&#xff0c;与外界复杂的业务需求交互其实是一件比较麻烦的事情&#xff0c;这里 torchserve 提供一个基于 TCP 的交互方法&#xff0c;算法模型部署后&#xff0c;用户可以通过提交 post 请求&#xff…

Linux服务器磁盘分区、挂载、卸载及报错处理

整体操作是&#xff1a;先对磁盘进行格式化&#xff0c;格式化后挂载到需要的挂载点&#xff0c;最后添加分区启动表&#xff0c;以便下次系统启动时自动挂载。一、linux分区1、Linux来说wulun有几个分区&#xff0c;分给哪一目录使用&#xff0c;他归根结底只有一个根目录&…

549、RocketMQ详细入门教程系列 -【消息队列之 RocketMQ(三)】 2023.02.28

目录一、Spring 整合 RocketMQ1.1 消息生产者1.2 消息消费者1.3 Spring 配置文件1.4 运行实例程序二、参考链接一、Spring 整合 RocketMQ 不同于 RabbitMQ、ActiveMQ、Kafka 等消息中间件&#xff0c;Spring 社区已经通过多种方式提供了对这些中间件产品集成&#xff0c;例如通…

Linux | 2. 用户管理

如有错误&#xff0c;恳请指出。 1. 设置文件权限 权限设置如下&#xff1a; root表示文件所有者&#xff0c;stud1表示文件所属组。其他用户无法访问。更改指令是chown。 更改目录文件所属组&#xff1a;chown .lab lossfound/更改目录文件所有者&#xff1a;chown lab loss…

html实现浪漫的爱情日记(附源码)

文章目录1.设计来源1.1 主界面1.2 遇见1.3 相熟1.4 相知1.5 相念2.效果和源码2.1 动态效果2.2 源代码2.3 代码结构源码下载更多爱情表白源码作者&#xff1a;xcLeigh 文章地址&#xff1a;https://blog.csdn.net/weixin_43151418/article/details/129264757 html实现浪漫的爱情…

Javaweb复习之HTTPTomcatServelet

1.Web概述 1.1 Web和JavaWeb的概念 Web是全球广域网&#xff0c;也称为万维网(www)&#xff0c;能够通过浏览器访问的网站。 JavaWeb就是用Java技术来解决相关web互联网领域的技术栈 1.2 JavaWeb技术栈 B/S 架构&#xff1a;Browser/Server&#xff0c;浏览器/服务器 架构模…

Linux: malloc()的指向指针发生指向偏移后,释放前需要将指针指向复原。

Linux: malloc()的指向指针发生指向偏移后&#xff0c;释放前需要将指针指向复原。 #include <stdlib.h> #include <stdio.h> #include <unistd.h> #include <sys/types.h> #include <sys/stat.h> #include <string.h> #include <time…

MIT:只需一层RF传感器,就能为AR头显赋予“X光”穿透视力

近年来&#xff0c;AR在仓库、工厂等场景得到应用&#xff0c;比如GlobalFoundries、亚马逊、菜鸟裹裹就使用摄像头扫描定位货品&#xff0c;并使用AR来导航和标记。目前&#xff0c;这种方案主要基于视觉算法&#xff0c;因此仅能定位视线范围内的目标。然而&#xff0c;在一些…

C++ string类(二)及深浅拷贝

一、string类方法使用举例1.迭代器迭代器本质&#xff1a;指针&#xff08;理解&#xff09;迭代器&#xff1a;正向迭代器&#xff1a; begin() | end() 反向迭代器&#xff1a; rbegin() | rend()2.find使用//找到s中某个字符 void TestString3() {string s("AAADEFNUIE…

携程面经1

面经 HDFS读写流程 1.读流程 客户端向NameNode发起读请求&#xff08;如果存在&#xff09;NameNode返回一批block地址客户端与第一个block的拓扑距离最近的节点建立连接以packet&#xff08;64kb&#xff09;的单位读取数据块。一个block读取完成后客户端会断开与该DataNod…

5个开源的Java项目快速开发脚手架

概览 &#xff1a; GunspigRuoYiJeecg-bootiBase4J 一、Guns 推荐指数 &#xff1a;⭐⭐⭐⭐⭐ 简介 采用主流框架 &#xff1a; 基于 Spring Boot2.0版本开发&#xff0c;并且支持 Spring Cloud Alibaba 微服务。功能齐全 &#xff1a;包含系统管理&#xff0c;代码生成&a…

这么强才给我28k,我头都不回,转身拿下40k~

时间真的过得很快&#xff0c;眨眼就从校园刚出来的帅气小伙变成了油腻大叔&#xff0c;给各位刚入道的测试朋友一点小建议&#xff0c;希望你们直通罗马吧&#xff01; 如何选择自己合适的方向 关于选择测试管理&#xff1a; 第一&#xff0c;你一定不会是一个喜欢技术&…

Vue的组件(注册、局部、组件复用、props、emit、生命周期)全解

文章目录前言知识点组件注册局部组件组件复用组件间通信props 类型检测子父组件通信之 emit动态组件生命周期函数前言 Vue 支持模块化和组件化开发&#xff0c;可以将整个页面进行模块化分割&#xff0c;低耦合高内聚&#xff0c;使得代码可以在各个地方使用。 知识点 组件注册…

accent-color一行代码,让你的表单组件变好看

不做切图仔,从关注本专栏开始 文章目录 不做切图仔,从关注本专栏开始前言兼容性语法继承性智能前言 在之前的网站开发中,我们是很难去更改的你某些控件的颜色。我们可能要使用各种技巧来自定义我们的控件。好消息是,今天如果我们想要去改变控件的颜色,css为我们提供了一些…

心系区域发展,高德用一体化出行服务平台“聚”力区域未来

交通&#xff0c;是城市的血脉。通过对人、资源、产业的连接&#xff0c;交通建设往往是城市和区域经济发展的前提。不过&#xff0c;在度过了“要想富&#xff0c;先修路”的初级建设阶段后&#xff0c;交通产业内部也出现了挑战&#xff0c;诸如城市秩序、发展成本、用户使用…

【目标检测】Dynamic Head Unifying Object Detection Heads with Attentions

文章目录一、背景二、方法2.1 scale-aware attention2.2 spatial-aware attention2.3 task-aware attention2.4 总体过程2.5 和现有的检测器适配2.6 和其他注意力机制的关联三、效果四、代码论文链接&#xff1a; https://arxiv.org/pdf/2106.08322.pdf代码链接&#xff1a;htt…

Windows 安装RocketMQ

文章目录一、RocketMQ是什么&#xff1f;二、准备工作1.环境要求2.下载与解压3.启动MQ4. 测试是否成功启动三、安装管理端1. 代码下载2. 修改配置文件3. 启动MQ客户端jar包四、rocketMQ代码的使用入门五、问题记录1. 启动mqbroker.cmd没有反应2.消费者重复消费消息一、RocketMQ…