ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

SpringBoot Banner自定义与动态打印原理实战

SpringBoot Banner自定义与动态打印原理实战

1. 启动横幅不只是花架子:从一次排查经历说起

很多人第一次看到SpringBoot项目启动时控制台打印的那段字符画,第一反应是"这玩意儿有啥用,花里胡哨的"。我一开始也这么想,直到有一次在同时维护七八个微服务的时候,本地起了三个实例,日志窗口切来切去,根本分不清哪个是订单服务、哪个是用户服务、哪个是网关。每次都要往上翻好几屏去看端口号才能确认,效率极低。后来我给每个服务配了不同的Banner,一眼就能认出来,这个问题才算彻底解决。

所以Banner这个东西,表面上看是个装饰,实际上在多服务并行开发、多环境切换、日志排查这些场景下,它能起到"视觉锚点"的作用。这篇文章就围绕SpringBoot的Banner机制,把它的加载原理、自定义方式、动态内容、关闭方法、以及实际项目里怎么用得漂亮,全部讲透。不管你是刚接触SpringBoot的新手,还是已经用了几年但从来没深究过Banner的老手,看完都能有所收获。

核心关键词先摆出来:SpringBoot Banner、Ascii-Art、banner.txt、自定义启动横幅、Banner打印原理、动态Banner、关闭Banner。这些是本文要覆盖的核心技术点,后面每个章节都会围绕它们展开。

先说一下本文适合谁看:如果你正在搭建SpringBoot项目脚手架,想给团队做一套统一的启动标识,那这篇可以直接抄作业;如果你只是好奇Banner是怎么加载出来的,想搞清楚SpringApplication.run背后干了什么,那原理部分能帮你补上这块知识;如果你嫌Banner碍事想关掉它,我也告诉你最干净的几种关法。

2. Banner的加载链路:从SpringApplication.run到控制台输出

2.1 默认Banner是怎么被找到的

SpringBoot启动的时候,SpringApplication.run()方法内部会调用一个叫printBanner()的方法。这个方法做的事情很直接:创建一个Banner对象,然后调用它的printBanner()把内容输出到控制台。

默认情况下,SpringBoot会使用SpringBootBanner这个实现类,它内部硬编码了一段"SPRING BOOT"的字符画,加上版本号。这就是你什么都没配的时候看到的那段默认输出。

但SpringBoot并不是无脑用默认的。它的查找顺序是这样的:

  1. 先检查classpath根目录下有没有banner.txt文件,有就用它。
  2. 如果没有banner.txt,再检查有没有banner.gif、banner.jpg、banner.png这些图片文件,有就把图片转成字符画输出。
  3. 如果都没有,才回退到默认的SpringBootBanner。

这个顺序很关键。很多人把banner.txt放在了src/main/resources下面却发现没生效,八成是因为文件位置放错了,或者文件名大小写不对。必须是classpath根目录,也就是src/main/resources/banner.txt,不能放在子目录里。

2.2 Banner接口的设计:为什么它这么灵活

SpringBoot把Banner抽象成了一个接口,只有两个方法:

public interface Banner { void printBanner(Environment environment, Class<?> sourceClass, PrintStream out); enum Mode { OFF, CONSOLE, LOG } }

这个设计非常聪明。printBanner方法接收Environment,意味着你可以在打印Banner的时候读取配置信息,比如当前激活的profile、应用名称、版本号等等。这就为动态Banner提供了可能。Mode枚举则控制Banner的输出位置:OFF关闭,CONSOLE输出到控制台(默认),LOG输出到日志文件。

我见过一些团队的做法是直接实现Banner接口,写一个自定义的Banner类,在里面读取Git提交信息、构建时间、当前环境等,打印出一段信息量很大的启动横幅。这种做法在需要严格追踪部署版本的场景下非常有用。

2.3 图片Banner的转换逻辑

如果你放的是图片,SpringBoot会用ImageBanner来处理。它会把图片的每个像素读取出来,根据像素的亮度映射到不同的字符上,亮度高的用密集字符(比如@、#),亮度低的用稀疏字符(比如.、空格),从而在控制台上还原出图片的轮廓。

这里有个实操经验:图片不要太大。我试过用一张1920x1080的图片,结果控制台刷了几千行,启动日志全被冲掉了。比较合适的尺寸是宽80到120像素、高30到40像素,这样在标准终端窗口里刚好能完整显示,又不会太占地方。另外,图片最好是高对比度的黑白图,彩色图转出来的字符画往往糊成一团,辨识度很差。

2.4 字符画生成的工具选择

自己手敲字符画不现实,一般都用工具生成。常见的几类工具:

  • 在线字符画生成器:把文字输进去,选字体和字符集,直接出结果。优点是快,缺点是样式受限于工具提供的字体。
  • figlet命令行工具:Linux和macOS上可以直接装,支持多种字体,figlet -f slant "MY APP"就能出一段斜体风格的字符画。
  • ASCII艺术字库:网上有很多现成的字库,直接复制粘贴就行。

我个人最常用的是figlet,因为可以脚本化。比如在CI流程里根据项目名自动生成Banner,就不用每次手动去在线工具里敲了。字体方面,slant、big、standard这几个比较耐看,banner字体太宽,在窄终端里会换行,不太推荐。

3. 动手定制:banner.txt的写法与占位符实战

3.1 最基础的banner.txt

在src/main/resources下新建一个banner.txt,里面写上你想要的字符画,启动项目就能看到效果。这是最直接的方式,不需要任何额外配置。

但这里有个坑:字符画里的反斜杠需要转义。因为SpringBoot在解析banner.txt的时候会处理占位符,反斜杠在某些情况下会被当成转义字符。如果你发现字符画显示出来缺了一块或者变形了,先检查是不是反斜杠的问题。解决办法是用双反斜杠\\,或者干脆换一种不含反斜杠的字体。

3.2 占位符:让Banner活起来

banner.txt里可以使用SpringBoot提供的占位符,启动时会自动替换成实际值。常用的有这些:

占位符含义示例输出
${spring-boot.version}SpringBoot版本3.2.0
${spring-boot.formatted-version}格式化版本(v3.2.0)
${application.version}应用版本(来自MANIFEST)1.0.0
${application.formatted-version}格式化应用版本(v1.0.0)
${AnsiColor.BRIGHT_GREEN}颜色代码设置后续文字颜色
${AnsiStyle.BOLD}样式代码加粗
${application.title}应用标题My Application

颜色占位符特别实用。你可以在字符画的不同部分插入不同的颜色代码,让Banner有层次感。比如标题用亮绿色,版本号用黄色,环境信息用青色。注意颜色代码是ANSI转义序列,在支持ANSI的终端里才有效,Windows的cmd默认不支持,但Windows Terminal和PowerShell是支持的。

3.3 一个实际可用的banner.txt模板

下面这个模板是我在多个项目里用过的,结构清晰,信息量适中:

${AnsiColor.BRIGHT_CYAN} ___ ___ _ _ ___ _ _ _____ ___ | \/ | | | | | / _ \ | | | || ___|/ _ \ | . . | | |_| |/ /_\ \| | | || |__ / /_\ \ | |\/| | | _ || _ || | | || __|| _ | | | | | | | | || | | || |_| || |___| | | | \_| |_/ \_| |_/\_| |_/ \___/ \____/\_| |_/ ${AnsiColor.BRIGHT_YELLOW} :: SpringBoot :: ${spring-boot.formatted-version} ${AnsiColor.BRIGHT_GREEN} :: Application :: ${application.title} ${application.formatted-version} :: Profile :: ${spring.profiles.active:default} :: Started At :: ${timestamp} ${AnsiColor.DEFAULT}

这里用到了${spring.profiles.active:default},冒号后面的default是默认值,当没有激活任何profile时显示default。${timestamp}不是SpringBoot内置的占位符,需要自己扩展,后面讲动态Banner的时候会说怎么实现。

3.4 配置项:控制Banner的行为

在application.yml或application.properties里,有几个和Banner相关的配置:

spring: banner: location: classpath:banner.txt # 指定Banner文件位置 charset: UTF-8 # 指定编码 mode: console # off / console / log main: banner-mode: console # 另一种关闭方式

spring.banner.location可以指定任意路径的Banner文件,不限于classpath根目录。spring.banner.charset在字符画包含中文或特殊符号时很有用,不设的话可能乱码。spring.banner.mode和spring.main.banner-mode功能有重叠,前者是较新的配置方式,后者是早期版本的方式,建议统一用spring.banner.mode。

注意:如果同时设置了spring.banner.location指向一个不存在的文件,SpringBoot不会报错,而是静默回退到默认Banner。这个行为容易让人困惑,排查时记得先确认文件路径是否正确。

4. 动态Banner:把构建信息和运行环境印在启动画面上

4.1 为什么需要动态Banner

静态的字符画只能解决"这是哪个服务"的问题,但解决不了"这是哪个版本、哪个环境、什么时候构建的"的问题。在微服务架构下,同一个服务可能有dev、test、staging、prod多个环境,每个环境跑的版本可能不一样。如果启动横幅里能直接显示这些信息,排查问题时能省很多事。

我经历过一次线上问题,两个实例的代码版本不一致导致行为差异,排查了半天才发现是发布的时候有一个实例没更新。如果当时Banner里带了构建版本号,一眼就能看出来。

4.2 通过实现Banner接口做动态内容

实现自定义Banner的核心是拿到Environment对象,从中读取配置,再结合其他信息源拼出最终输出。下面是一个示例:

public class CustomBanner implements Banner { @Override public void printBanner(Environment environment, Class<?> sourceClass, PrintStream out) { String appName = environment.getProperty("spring.application.name", "Unknown"); String profile = String.join(",", environment.getActiveProfiles()); String buildVersion = environment.getProperty("build.version", "dev"); String buildTime = environment.getProperty("build.time", "unknown"); out.println("========================================"); out.println(" Application : " + appName); out.println(" Profile : " + (profile.isEmpty() ? "default" : profile)); out.println(" Build : " + buildVersion); out.println(" Build Time : " + buildTime); out.println("========================================"); } }

然后在启动类里注册:

public static void main(String[] args) { SpringApplication app = new SpringApplication(MyApp.class); app.setBanner(new CustomBanner()); app.run(args); }

build.version和build.time这些属性可以通过Maven的resource filtering在打包时写入application.properties,这样每次构建都会自动更新。

4.3 用Maven资源过滤注入构建信息

在pom.xml里配置:

<build> <resources> <resource> <directory>src/main/resources</directory> <filtering>true</filtering> </resource> </resources> <plugins> <plugin> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-maven-plugin</artifactId> <configuration> <excludes> <exclude> <groupId>org.projectlombok</groupId> <artifactId>lombok</artifactId> </exclude> </excludes> </configuration> </plugin> </plugins> </build>

然后在application.properties里写:

build.version=${project.version} build.time=${maven.build.timestamp}

这样打包的时候,Maven会把${project.version}和${maven.build.timestamp}替换成实际值。启动时Banner就能显示真实的构建版本和时间。

提示:maven.build.timestamp默认是UTC时间,如果需要本地时间,可以在properties里设置<maven.build.timestamp.format>,或者用其他插件生成。

4.4 结合Git信息做版本追踪

更进一步的做法是把Git的commit id和分支名也打到Banner里。可以用git-commit-id-plugin这个Maven插件,它会在打包时生成一个git.properties文件,SpringBoot的GitProperties会自动加载它。

配置好插件后,在自定义Banner里注入GitProperties:

@Component public class GitInfoBanner implements Banner { @Autowired private GitProperties gitProperties; @Override public void printBanner(Environment environment, Class<?> sourceClass, PrintStream out) { out.println("Git Branch : " + gitProperties.getBranch()); out.println("Git Commit : " + gitProperties.getShortCommitId()); } }

不过要注意,Banner的打印时机非常早,早于Spring容器的依赖注入。所以直接@Autowired是拿不到Bean的。正确的做法是在SpringApplication.run()之后手动获取,或者用ApplicationContextInitializer。这也是很多人踩过的坑:在自定义Banner里注入Bean,结果启动报NPE。

5. 关闭Banner的几种方式与适用场景

5.1 代码方式关闭

最直接的方式是在启动类里设置:

SpringApplication app = new SpringApplication(MyApp.class); app.setBannerMode(Banner.Mode.OFF); app.run(args);

这种方式适合你完全确定不需要Banner的场景,比如单元测试、集成测试。测试的时候控制台输出越干净越好,Banner只会干扰日志阅读。

5.2 配置文件方式关闭

在application.yml里:

spring: main: banner-mode: off

或者在application.properties里:

spring.main.banner-mode=off

这种方式的好处是可以按环境区分。比如在application-dev.yml里保持Banner开启,在application-test.yml里关闭。用profile来控制,灵活度更高。

5.3 启动参数方式关闭

命令行启动的时候加参数:

java -jar myapp.jar --spring.main.banner-mode=off

这种方式适合临时关闭,比如在CI/CD流水线里跑集成测试的时候,不想改代码也不想改配置文件,直接加个启动参数就行。

5.4 几种方式的优先级对比

方式优先级适用场景是否可动态调整
代码设置最高测试类、固定不需要Banner的应用否
配置文件中按环境区分是(改配置重启)
启动参数最高(覆盖配置文件)临时关闭、CI流水线是(每次启动可不同)

实际项目中,我一般是在测试环境的配置文件里关掉Banner,生产和开发环境保留。因为测试环境日志量大,Banner占的那几十行虽然不多,但积少成多。开发和线上保留Banner,方便快速识别服务。

注意:spring.main.banner-mode和spring.banner.mode同时存在时,前者的优先级更高。如果你发现配置了spring.banner.mode=off但Banner还在打印,检查一下是不是spring.main.banner-mode把它覆盖了。

6. 踩过的坑与实战经验汇总

6.1 中文乱码问题

banner.txt里如果有中文,默认编码可能不对,导致控制台显示乱码。解决办法是在配置里显式指定编码:

spring: banner: charset: UTF-8

但即使这样,Windows的cmd默认代码页是GBK,还是可能乱码。所以我的建议是:Banner里尽量只用ASCII字符,中文信息放在Banner下面的日志行里输出。这样跨平台兼容性最好。

6.2 颜色代码在日志文件里的问题

如果你把spring.banner.mode设成了log,Banner会输出到日志文件。但ANSI颜色代码在日志文件里是乱码一样的转义字符,看起来很不舒服。所以用颜色占位符的时候,要么只用在console模式,要么在日志采集端做过滤。我一般是在Banner里不用颜色,保持纯文本,这样console和log都干净。

6.3 Banner文件位置放错

这是新手最常见的错误。banner.txt必须放在src/main/resources根目录下,打包后位于classpath根目录。如果你放在了src/main/resources/static/或者src/main/resources/config/下面,SpringBoot是找不到的。排查方法很简单:打包后打开jar,看BOOT-INF/classes/下面有没有banner.txt。

6.4 图片Banner的性能影响

图片Banner在启动时会做像素到字符的转换,如果图片很大,这个转换过程会消耗一定时间。我实测过一张2000x2000的PNG,转换花了将近1秒。虽然1秒对启动时间影响不大,但没必要。控制在100x40以内,转换基本是毫秒级的。

6.5 自定义Banner里注入Bean的时机问题

前面提过,Banner的打印发生在Spring容器刷新之前,所以任何依赖注入都是不可用的。如果你需要在Banner里用到某个Bean的信息,有两个办法:一是把信息提前写到Environment里(比如通过ApplicationContextInitializer),二是在Banner打印完之后再输出一段补充信息。我倾向于第二种,因为更简单,不破坏SpringBoot的启动流程。

6.6 多模块项目的Banner管理

在多模块项目里,如果每个模块都有自己的banner.txt,最终打包后classpath根目录只会有一个生效(取决于打包顺序)。所以正确的做法是:只在最终的启动模块里放banner.txt,其他模块不要放。如果确实需要区分,可以用spring.banner.location指向不同路径的文件,通过profile来切换。

7. 把Banner用出花:几个进阶玩法

7.1 根据环境显示不同颜色的Banner

利用Environment判断当前profile,在自定义Banner里输出不同颜色的文字。比如dev环境用绿色,prod环境用红色,起到警示作用。实现方式是在printBanner方法里读取environment.getActiveProfiles(),然后拼接对应的ANSI颜色代码。

这个做法在团队里推广之后,效果很好。开发同学一眼就能看出自己连的是哪个环境,避免了误操作生产数据库这种低级错误。

7.2 在Banner里显示启动耗时

SpringBoot启动完成后会输出启动耗时,但那是启动之后的事。如果你想在Banner里就显示"上次启动耗时"或者"预计启动时间",可以在Banner里读取一个记录文件。不过这个玩法比较小众,实现起来也麻烦,我试过一次就放弃了,性价比不高。

7.3 用Banner做简单的健康提示

在Banner里显示当前数据库连接、Redis连接的状态。这个需要在Banner打印之前就完成连接检查,会拖慢启动速度。我的建议是不要在Banner里做这种事,用SpringBoot的HealthIndicator更合适。Banner就让它安安静静做个视觉标识就好。

7.4 团队统一的Banner规范

如果你带团队,建议制定一个Banner规范:统一字体风格、统一信息字段、统一颜色方案。这样所有服务的启动画面看起来是一套的,专业感很强。我们团队的规范是:第一行是服务名(slant字体),第二行是版本和环境,第三行是构建时间,下面空一行再开始正常的启动日志。

这个规范落地之后,新同学入职看到启动画面就能大概知道项目结构,减少了沟通成本。

7.5 Banner与日志系统的配合

Banner输出到console,日志也输出到console,两者会混在一起。如果Banner太长,会把日志冲散。我的做法是:Banner控制在15行以内,并且在Banner最后加一个明显的分隔线,比如==========,这样日志从哪开始一目了然。

另外,如果用了logback或log4j2,可以配置日志格式,在启动阶段用不同的pattern,让Banner和日志在视觉上区分开。不过这个配置有点复杂,一般项目没必要。

8. 我个人的使用体会

Banner这个东西,说大不大,说小不小。它不影响功能,但影响开发体验。我在实际项目里的做法是:开发和生产环境保留一个简洁的Banner,包含服务名、版本、环境三个信息;测试环境直接关掉,保持日志干净。Banner文件用figlet生成,字体固定用slant,颜色只在服务名上用亮青色,其他都是默认色。

踩过几次坑之后,我总结了一条原则:Banner的信息密度要低,视觉辨识度要高。不要试图在Banner里塞太多东西,那只会让它变成另一段需要阅读的日志。它的核心价值就是让你在众多窗口里一眼认出"这是哪个服务",其他的都是锦上添花。

最后分享一个小技巧:如果你用IntelliJ IDEA,可以在Run/Debug Configurations里给每个启动配置设置不同的Before launch任务,配合不同的Banner文件,这样在IDE里同时启动多个服务时,每个服务的启动画面都不一样,切换窗口的时候特别方便。这个技巧我用了好几年,推荐你也试试。

RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进