2026/10/12 22:53:02

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则完全跳过。如果是文本bannerSpring 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-modeconsole spring.banner.locationclasspath:my-banner.txt spring.banner.charsetUTF-8spring.main.banner-mode可选console、log、off。console只输出到控制台log会尝试把banner写入日志文件off则什么都不输出。需要临时关闭时命令行里加--spring.main.banner-modeoff比改配置文件更快。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 directorysrc/main/resources/directory filteringtrue/filtering excludes excludebanner.txt/exclude /excludes /resource resource directorysrc/main/resources/directory filteringfalse/filtering includes includebanner.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变成字符画除了文本bannerSpring Boot还支持把图片转换成字符画展示。需要配置图片路径spring.banner.image.locationclasspath:logo.png spring.banner.image.width80 spring.banner.image.height20 spring.banner.image.margin2Spring 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虽小值得认真对待把这块小地方用好团队日志的识别效率会明显不一样。