ARTICLE · INTELLIGENCE

战地情报 · 详情页

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

Spring Boot自定义Banner完全指南:从banner.txt到动态接口

Spring Boot自定义Banner完全指南:从banner.txt到动态接口

Spring Boot 项目启动时,控制台最先出现的那只绿油油的 Spring 字母,看久了是真的会腻。我第一次动改它的念头,是在一次连续部署十几个服务版本的下午——启动日志一屏接一屏,全是一样的图案,想看哪个服务已经启动都得靠滚动找。后来把自定义banner完整研究了一遍,才发现这个看似“只能换张图”的小功能,背后其实藏着一条完整的解析链路:banner.txt、内置占位符、ANSI颜色、Banner接口,甚至图片转字符画。

这篇文章不打算讲太虚的东西,只讲你真正会遇到的:怎么快速把默认图案换成自己的,用什么方式做动态信息,以及那些网上教程很少写的坑——比如中文乱码、打包后banner失效、日志里出现一堆[32m。阅读门槛不高,但实用性拉满。只要你在用Spring Boot,不管你是写业务代码还是负责团队基础服务,这篇内容都能直接用上。

1. 自定义Banner到底解决什么问题:不只是一张启动图

1.1 一个被忽略的“第一印象”入口

Spring Boot 默认启动时会打印一个绿色的 Spring 字符图和版本号。这个图案本身没什么问题,但在一套多模块、多实例的微服务里,几十个服务的启动日志如果全都长一个样,人工翻日志找服务实例就成了很痛苦的事。自定义banner能把“我是谁、我是什么版本、当前跑在哪个环境”这些最关键的信息放到最前面。

这个“第一印象”入口在本地开发、测试环境和生产环境都有价值。运维同学扫一眼启动日志就能知道哪个实例是哪个环境的;新同学接手老旧项目时,也能第一眼看到服务名、端口、版本号,而不是去翻配置文件。很多人只把自定义banner当成恶搞入口,其实它更像一个轻量级的可观测性组件,只是它出现在服务启动的那一瞬间。

1.2 什么时候值得动它

适合动手的场景我整理过几个,按性价比排序:

  • 团队统一交付物:在公共依赖里放一个团队banner,所有服务启动时都展示统一标识和联系方式,出问题能快速找到负责人。
  • 构建信息展示:用占位符或编程Banner输出应用版本、Spring Boot版本、环境Profile,省得每次都翻配置。
  • 演示项目或公开Demo:加一段字符画能明显提升观感,很多开源Demo就是这么做的。
  • 减少噪音:自动化测试、CI批量启动时,把banner关掉反而更清爽,这种情况用off模式即可。

我不太建议的是为了花哨在banner里放一大堆无关字符画。启动时控制台通常有宽度限制,字符画一旦超出终端宽度就会被换行,整个排版会碎掉,反而降低可读性。克制一点,信息放在第一位。

1.3 先理解它背后的解析链路

Spring Boot 对banner的处理其实是一条固定的启动子流程。在SpringApplication.run()早期,SpringApplicationBannerPrinter会检查当前设置的Banner.Mode,取值有三种:OFF、CONSOLE、LOG。控制台模式就是输出到标准输出,日志模式会走日志框架输出到文件,OFF则完全跳过。

如果是文本banner,Spring Boot 会把banner.txt的内容按行读取,并对文本中的${...}占位符逐一解析。解析器按顺序查系统属性、环境变量、配置文件里的配置,还会处理一些内建的ANSI颜色占位符。最后再把解析后的文本交给Banner或ImageBanner输出。所以banner看起来是纯文本,实际是可以在启动阶段拿到很多运行参数的。理解了这条链路,后面排查问题就会很有方向。

2. 两种主流方案怎么选:静态文本还是动态接口

2.1 banner.txt方案:最省事的“约定大于配置”

如果你只需要一个固定的字符画、固定的品牌名、固定版本的展示,直接走最常用的方式:在src/main/resources目录下放一个banner.txt文件。Spring Boot默认就会优先使用这个文件替换内置图案,不需要任何Java代码,不需要改配置。

这个方案最大的优点是零侵入、可维护性强。想改文案或换图案,直接改文本文件重新打包就行,不会碰代码逻辑。配合内置占位符,你甚至可以在字符画下方输出Spring Boot版本、应用版本这类变化不频繁但又很关键的信息。团队里就算是不熟Java的人,也能在文本文件里维护banner内容,这对团队协作很友好。

2.2 Banner接口方案:动态信息的唯一出路

如果你的banner需要展示“当前启动时间”“内存大小”“激活的Profile”“端口号”这类运行时才能确定的信息,banner.txt就力不从心了。虽然它支持占位符,但占位符只能解析已有配置或系统属性,做不了逻辑判断,更读不了JVM运行时数据。

这时需要自定义一个Java类实现org.springframework.boot.Banner接口。接口里只有一个方法:

void printBanner(Environment environment, Class<?> sourceClass, PrintStream out)

方法参数直接给了Environment,你可以从中读取激活环境、server.port、spring.application.name等运行时配置;PrintStream则负责把内容输出到终端或日志。因为这段代码是在启动早期执行的,你甚至可以在banner里输出一段基于当前日期的动态字符画。

2.3 我建议的组合拳:文本打底、接口补充

实际项目中我不推荐二选一,而是组合使用:静态部分用banner.txt快速落地,比如服务名、Logo、团队信息;动态部分通过Banner接口在启动时追加几行关键运行指标。需要注意,如果你在代码里手动调用了setBanner(...),它会覆盖banner.txt,所以两者要决定好优先级,不要让后续接手的人搞不清为什么改了banner.txt却没反应。

组合拳的核心原则是:banner.txt负责“长什么样”,Banner接口负责“说什么变化的信息”。这样静态文案和动态数据互相不干扰,维护成本最低。

3. 实操:banner.txt从零到一的完整步骤

3.1 生成ASCII艺术字并检查宽度

字符画不用手写,网上有大量文本转ASCII生成工具,输入“Spring Boot”或你服务的名字,就能生成不同字体风格的字符画。选一个看起来清楚、宽度适中的字体,宽度控制在80列以内最稳妥,因为大部分终端默认显示宽度都在80字符左右,超了必然换行。

生成之后有三点容易踩坑。第一,全角空格和Tab尽量不要出现在banner里,不同终端对它们的渲染宽度不完全一致,很容易出现错位。第二,如果字符画里含有中文,终端字体对中英文字符宽度支持不一,建议中文文案和字符画分开区域展示,不要混在同一行。第三,文件末尾保留一个换行符,否则最后一行的显示可能会被下一行命令紧贴。

3.2 创建banner.txt并放到resources目录

在项目里直接建文件:

src/main/resources/banner.txt

我举个简化例子,内容不必多高级,重点是清晰:

___________ __ / _ _ ____| / /_ __ __ | | | | __| / __ \/ / / / | | | | |_ | / /_/ / /_/ / |_| |_|____|/_.___/\__, / /____/ Service: ${application.title} Spring Boot: ${spring-boot.formatted-version}

重新启动主类,控制台第一屏就会被这段内容替代。如果没生效,优先检查三件事:文件是不是真的在classpath根路径、文件名是否大小写完全一致、项目是否重新触发了资源复制。

3.3 内置占位符和颜色语法是精华

banner.txt支持的内置占位符通常包括:

占位符说明
${spring-boot.version}Spring Boot版本号
${spring-boot.formatted-version}格式化版本号,通常带括号
${application.title}应用标题,打包成jar后取MANIFEST里的Implementation-Title
${application.version}应用版本号
${application.formatted-version}格式化应用版本号

我用下来最要注意的是application.title和application.version。这组占位符依赖jar包MANIFEST文件里的信息,用java -jar直接跑打包产物时基本都能正常解析;但在开发工具里直接run主类时,经常解析不到,占位符会原样出现在banner里,看起来像是bug。这不是代码写错了,是运行方式决定的。

颜色方面,banner.txt里可以写${AnsiColor.RED}、${AnsiBackground.BLUE}、${AnsiStyle.BOLD}这类标记,它们会被解析成终端能识别的ANSI转义序列。注意,在日志文件模式(LOG)下,这些颜色标记可能会原样写入日志文件,导致日志里出现[31m之类的字符,所以我更建议在纯粹的控制台展示场景里才用颜色。

3.4 用配置项控制输出模式和路径

banner的输出位置和文件位置都可以用配置控制。在配置文件中:

spring.main.banner-mode=console spring.banner.location=classpath:my-banner.txt spring.banner.charset=UTF-8

spring.main.banner-mode可选console、log、off。console只输出到控制台,log会尝试把banner写入日志文件,off则什么都不输出。需要临时关闭时,命令行里加--spring.main.banner-mode=off比改配置文件更快。

spring.banner.location可以指向classpath:下任意路径,适合不想把banner文件放在根目录、或者文件被公共模块复用的场景。此时Spring Boot不再去找默认的classpath:banner.txt,而是优先加载这个自定义路径。还要注意spring.banner.charset默认是UTF-8,但如果你发现乱码,问题通常不在这个配置,而在终端代码页或编辑器保存编码。

3.5 一个快速验证的套路

我自己验证banner改动有个固定动作:先开一个最小化Spring Boot工程,只保留启动类、配置文件、banner.txt,改完直接点启动按钮看控制台。不要在一个几十个模块的大工程里反复打包验证,太浪费编译时间。最小工程里确认效果后,再把banner文件复制回正式工程,这样定位问题会快很多。

还有一个细节:如果项目里用了热加载组件,banner是写在启动阶段的,热重启往往不会重新打印,容易让人误以为“没改成功”。手动重启一次,或者清理掉构建目录重新编译,看到的才是真实结果。

4. 实操:Banner接口打造动态启动信息

4.1 写一个能显示环境、时间和内存的Banner

当你想让banner包含运行时数据,就轮到Banner接口上场了。下面这个实现是我经常用的“最小动态Banner”,直接复制就能跑:

import org.springframework.boot.Banner; import org.springframework.core.env.Environment; import java.io.PrintStream; import java.time.LocalDateTime; public class DynamicBanner implements Banner { @Override public void printBanner(Environment environment, Class<?> sourceClass, PrintStream out) { out.println("============================================="); out.println(" Service: " + environment.getProperty("spring.application.name", "unknown")); String[] profiles = environment.getActiveProfiles(); out.println(" Profile: " + (profiles.length == 0 ? "default" : String.join(",", profiles))); out.println(" Port : " + environment.getProperty("server.port", "8080")); out.println(" Time : " + LocalDateTime.now()); long maxMemory = Runtime.getRuntime().maxMemory() / 1024 / 1024; out.println(" Memory : " + maxMemory + " MB"); out.println("============================================="); } }

然后在主类里接上它:

import org.springframework.boot.SpringApplication; public class DemoApplication { public static void main(String[] args) { SpringApplication app = new SpringApplication(DemoApplication.class); app.setBanner(new DynamicBanner()); app.run(args); } }

如果你的项目用SpringApplicationBuilder,用法更简洁:

new SpringApplicationBuilder(DemoApplication.class) .banner(new DynamicBanner()) .run(args);

启动后,banner区域就会变成一段包含服务名、当前Profile、端口、启动时间和JVM最大内存的文本。注意这里用的是out.println而不是System.out.println,因为框架传入的PrintStream会根据Banner.Mode决定输出到哪,自己硬写System.out会绕过这个逻辑,在LOG模式下就会失控。

4.2 动态图案和动态信息的更多玩法

Banner接口能做的远不止打印文本。可以在printBanner里根据当前月份输出不同字符画;也可以根据激活的Profile调整颜色,比如本地环境用绿色文字,生产环境用红色文字,这样一眼就能看出是不是高危环境。

我做过一个相对实用的例子:读取构建产物里生成的构建信息文件,把构建时间和Git提交号拼进banner里。因为Environment里不一定有这些数据,可以在主启动类构造banner之前先读文件,然后把值塞进System.setProperty,这样printBanner里直接取系统属性就能用。虽然不算优雅,但它是在不引额外组件的情况下满足“启动时看到提交号”的最简单解法。

4.3 注意别在banner打印阶段做重活

一个容易被低估的坑是性能。printBanner在启动序列里的位置很靠前,如果你在里面发起HTTP请求、开数据库连接、读远程配置,会直接拖慢服务启动速度,甚至在配置中心还没就绪时报错。这个阶段适合做的事情只有:读本机静态文件、读系统属性、读Environment、拿JVM运行时数据。

如果你确实需要把“启动耗时”展示在banner里,可以在banner打印前记录一个起始时间戳,在printBanner里用System.currentTimeMillis()减去它。但括号内的计算要控制在微秒级,不要在这里做任何可能阻塞的IO。

5. 避坑指南:从乱码到banner不生效的完整排查

5.1 编码:中文乱码是最高频问题

乱码问题九成出在两个地方:一个是编辑器保存编码不对,一个是终端代码页和文件编码不匹配。banner.txt要无BOM的UTF-8,打开文件时确认右下角编码是UTF-8。Windows下如果用了记事本“另存为UTF-8”,文件开头很可能会带一个BOM,这个不可见字符会出现在banner第一行前面,有时候表现为奇怪的空字符。

终端侧,Windows的命令行窗口默认代码页可能是GBK,和UTF-8文件对不上就会乱码。临时切到UTF-8可以执行chcp 65001,但如果团队里有其他人也在用Windows,建议统一要求使用新版Windows终端,它对UTF-8的兼容性更稳定。macOS和Linux终端一般没有这个问题。

5.2 ANSI颜色:该亮的没亮,日志还多出一堆转义码

banner文本里的${AnsiColor.RED}能不能生效,取决于Spring Boot对ANSI输出的全局控制。相关配置默认是detect,意思是它会检测输出流是不是终端,不是终端就不输出ANSI转义码。这在很多时候是聪明的,但也造成一个现象:同样一份banner,在开发工具里颜色正常,在管道重定向到文件时不显示颜色,在旧版cmd里可能显示一堆[31m的乱码。

处理办法有三条:如果只关心控制台效果,就把模式设成console,不要设成log;如果日志文件里总出现ANSI码,考虑把banner里的颜色标记去掉,或者在配置里关闭ANSI输出;如果是老终端不支持ANSI,没有特别好的办法,换终端比换代码成本低。别在banner颜色上做复杂嵌套,红底白字这种组合在不同终端渲染经常不一致。

5.3 占位符没替换:先看运行方式和属性来源

占位符解析失败通常分两种情况。第一种是application.title、application.version这类占位符在IDE直接运行时解析不到,因为MANIFEST信息没有生成。第二种是自定义属性拼写和配置文件不一致,或者根本没定义。排查时先在启动配置的Environment里看属性是否存在,不要在banner文本上猜。

另外提醒一下,banner.txt的占位符解析器会把解析不了的占位符原样输出,这就给排查增加了一点迷惑性。我见过有人因为banner里出现${unknown.key}就以为配置中心没生效,折腾半天,其实只是banner里写错了变量名。想要快速验证某个占位符能不能被解析,最简单的办法是临时把它放在配置文件里定义同名属性的方式,然后看启动输出。

5.4 改了banner.txt却没生效:路径、缓存和覆盖三层原因

第一层是路径问题。文件没有放在src/main/resources根目录下,或者大小写不对,自然不会被加载。第二层是缓存问题,开发工具里有时资源没有重新复制到构建目录,需要清理一下。第三层是覆盖问题,代码里用了app.setBanner(...),这会让banner.txt直接失去意义;如果项目里存在多个地方调用了setBanner,还要注意先后顺序,后调用的会覆盖前者。

还有一种隐蔽情况:项目里引入了某个公共依赖,这个依赖里也有自己的banner.txt,而你的项目里没有,那么打出来的包会使用依赖里的banner。这时候需要显式设置spring.banner.location来绕过默认路径,否则你会怀疑自己明明改了文件却没有变化。

5.5 构建工具的资源过滤会悄悄破坏banner

如果你的构建配置里对src/main/resources开启了属性过滤,banner文件会在打包阶段被当成模板,${...}可能会被提前解析或替换。这在打包后看起来就是banner内容变掉、乱码或者占位符消失。解决办法是给过滤配置加exclude,单独把banner.txt排除掉:

<resources> <resource> <directory>src/main/resources</directory> <filtering>true</filtering> <excludes> <exclude>banner.txt</exclude> </excludes> </resource> <resource> <directory>src/main/resources</directory> <filtering>false</filtering> <includes> <include>banner.txt</include> </includes> </resource> </resources>

这段配置的意思是:除了banner.txt之外的其他资源继续走过滤,banner.txt用不过滤的方式原样打入包。这个坑平时不出现,但一旦出现会让人非常困惑,建议先记下来。

6. 常见问题速查表与图片Banner扩展

6.1 问题排查速查表

现象可能原因处理建议
启动还是默认Spring图案banner.txt不在resources根目录或文件名大小写不对确认路径和文件名
启动时banner完全不显示banner-mode设置了off,或代码设置了OFF模式检查配置和主类代码
改banner.txt无变化热重启不发banner,或构建目录未更新手动重启并清理构建目录
中文乱码文件编码非UTF-8,或终端代码页不对存成UTF-8无BOM,用chcp 65001
日志文件出现[31mLOG模式下ANSI颜色原样写入去掉颜色或设console模式
${application.version}没替换开发工具运行读取不到MANIFEST信息用java -jar或在代码里读取
打包后banner内容变了资源过滤影响在构建配置里排除banner.txt
自定义Banner不生效代码setBanner被后续覆盖,或mode是OFF确认调用顺序和mode

这张表是我平时帮同事排障用的,基本上所有banner相关问题都能归到其中的某一类。排障顺序建议先看现象,再按“配置→代码→资源文件→编码”这个顺序逐步排查,不要一上来就怀疑框架。

6.2 图片Banner:把Logo变成字符画

除了文本banner,Spring Boot还支持把图片转换成字符画展示。需要配置图片路径:

spring.banner.image.location=classpath:logo.png spring.banner.image.width=80 spring.banner.image.height=20 spring.banner.image.margin=2

Spring Boot启动时会用ImageBanner把像素点映射成不同颜色的字符。实际用下来,这个功能更适合线条简单、颜色对比明显的Logo,如果是一张复杂照片转出来的字符画,启动日志会变得特别长且看不清。

图片Banner存在时,banner.txt会被忽略,所以不要指望两者同时输出。我用这个功能做团队Logo展示时,都会把width压到80以内,height按比例调整,并设置一定的margin避免贴边。其他同事终端宽度不够时,会自动换行,排出来很丑,所以细节要自己把控。

6.3 影响范围:从启动体验到自动运维

自定义banner表面看只是启动日志美化,但它实际影响的范围比想象中大。在监控平台、容器编排平台的Pod日志、统一日志采集系统里,第一条日志往往就是banner,如果它包含了应用名、版本、Profile,值班人员可以在不登录服务的情况下快速判断是哪个实例、哪个版本。这个信息有时候比后面的日志摘要还有用。

另一个容易被忽略的是会影响日志量。banner文本如果特别长,每次启动都会产生几十行日志,在大量服务滚动发布时,日志中心会积累不少无结构化数据。所以我建议banner保持精简,输出到控制台的尽量控制在十几行以内,输出到LOG模式的更要控制字号,能用一行关键信息解决的,不要画一头几百行的复杂图案。

7. 一点个人经验:banner是启动体验,更是团队默契

7.1 我踩过最值的三个教训

第一个教训是关于等宽字体。第一次设计banner时我用的是变宽字体复制出来的字符画,本地看着挺整齐,换到同事机器上就对不齐了。后来才明白终端里的字符画必须建立在等宽字体的前提下,生成时要选看上去比较规整的字体,排版才会稳定。

第二个教训是不要在生产日志里留ANSI颜色。某个项目把banner设成了LOG模式,里面塞了红黄蓝几段颜色,结果日志采集系统里每天都要多出一堆[31m。从那以后我的处理原则是:控制台展示可以有色,日志文件里绝对不用色。

第三个教训是banner不是越炫越好。我曾经在一个内部工具里放过一个很大的字符Logo,每次启动都要刷几十行,实用性很差。后来简化成三行关键信息,大家反而说更好用。banner的第一职责是提供信息,不是炫技。

7.2 一个值得收进工具箱的扩展思路

如果你在一个多服务团队里,我建议把banner做成公共配置:公共依赖里放一份默认banner.txt,里面只有团队标识和公共说明;每个服务通过spring.banner.location指向自己的专属banner,补充服务名、版本、环境,这样既统一又不呆板。

另一个扩展思路是用Profile切换banner内容。开发环境放一份信息丰富的banner,生产环境放一份精简且不含颜色标记的banner。通过不同Profile的配置切换spring.banner.location,可以在不改代码的前提下控制不同环境的启动展示。这个方案我在实际项目里用了很久,省去了反复改同一个文件的麻烦。

说回开头那个下午,我现在再看banner,心态已经完全不一样:它不是用来炫技的,而是启动日志里第一行可以被人类直接读取的信息。banner虽小,值得认真对待;把这块小地方用好,团队日志的识别效率会明显不一样。

RELATED READING

延伸阅读

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