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并不是无脑用默认的。它的查找顺序是这样的:
- 先检查classpath根目录下有没有
banner.txt文件,有就用它。 - 如果没有
banner.txt,再检查有没有banner.gif、banner.jpg、banner.png这些图片文件,有就把图片转成字符画输出。 - 如果都没有,才回退到默认的
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里同时启动多个服务时,每个服务的启动画面都不一样,切换窗口的时候特别方便。这个技巧我用了好几年,推荐你也试试。