4. SpringBoot 自定义Banner使用与原理解析
一. banner概述
Spring banner是Spring Boot应用启动时在控制台显示的横幅内容,通常包含ASCII艺术字、项目信息等,用于增强应用标识或展示版本信息。
. ____ _ __ _ _/\\ / ___'_ __ _ _(_)_ __ __ _ \ \ \ \
( ( )\___ | '_ | '_| | '_ \/ _` | \ \ \ \\\/ ___)| |_)| | | | | || (_| | ) ) ) )' |____| .__|_| |_|_| |_\__, | / / / /=========|_|==============|___/=/_/_/_/:: Spring Boot :: (v2.7.18)
一般SpringBoot有一个默认banner,同时SpringBoot也支持对banner进行自定义设置。
演示环境:
java: 11
springboot: 2.7.18
二. 自定义banner
1. 文本
-
自定义文本banner时,只需要在resources下新建一个名为banner.txt的文件,然后编辑banner的文本即可,如下所示。
_ooOoo_o8888888o88" . "88(| -_- |)O\ = /O____/`---'\____.' \\| |// `./ \\||| : |||// \/ _||||| -:- |||||_ \| | \\\ - /// | || \_| ''\---/'' | |\ .-\__ `-` ___/-. /___`. .' /--.--\ `. . __."" '< `.___\_<|>_/___.' >'"".| | : `- \`.;`\ _ /`;.`/ - ` : | |\ \ `-. \_ __\ /__ _/ .-` / / ======`-.____`-.___\_____/___.-`____.-'======`=---='佛祖保佑 永无BUG ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ -
如果自定义banner的文件名不是banner.txt,则需要通过在配置文件中配置spring.banner.location,如下所示。
spring.banner.location: favorite.txt
2. 图片
- springboot还允许我们自定义图片banner。并且如果两种banner同时存在,则先输出图片banner、再输出文本banner。
- 默认地,springboot将从classpath类路径下获取banner.gif、banner.jpg、banner.png作为图片banner。当然也可以通过在配置文件中配置spring.banner.image.location来指定图片的位置。
- 在输出图片banner时,springboot将会把图片转化成ASCII艺术画输出,而非无脑式地将图片输出。

运行结果:

3. 兜底banner
SpringBoot自定义banner也支持通过实现Banner接口的方式,然后通过setBanner进行添加,一般这种方式添加的banner叫做兜底banner。
public class MyBanner implements Banner {@Overridepublic void printBanner(Environment environment, Class<?> sourceClass, PrintStream out) {System.out.println("This is 兜底 banner");}
}
@SpringBootApplication
public class Main {public static void main(String[] args) {SpringApplication springApplication = new SpringApplication(Main.class);springApplication.setBanner(new MyBanner());springApplication.run(args);}
}
运行结果:
10:48:33.316 [Thread-0] DEBUG org.springframework.boot.devtools.restart.classloader.RestartClassLoader - Created RestartClassLoader org.springframework.boot.devtools.restart.classloader.RestartClassLoader@49a2f430
This is 兜底 banner
2025-11-10 10:48:33.692 INFO 11488 --- [ restartedMain] org.example.Main : Starting Main using Java 11.0.15.1 on LAPTOP-8BL7V074 with PID 11488 (D:\project\springboot_demo\target\classes started by chenli in D:\project\springboot_demo)
2025-11-10 10:48:33.692 INFO 11488 --- [ restartedMain] org.example.Main : No active profile set, falling back to 1 default profile: "default"
2025-11-10 10:48:33.739 INFO 11488 --- [ restartedMain] .e.DevToolsPropertyDefaultsPostProcessor : Devtools property defaults active! Set 'spring.devtools.add-properties' to 'false' to disable
2025-11-10 10:48:33.739 INFO 11488 --- [ restartedMain] .e.DevToolsPropertyDefaultsPostProcessor : For additional web related logging consider setting the 'logging.level.web' property to 'DEBUG'
2025-11-10 10:48:34.873 INFO 11488 --- [ restartedMain] o.s.b.w.embedded.tomcat.TomcatWebServer : Tomcat initialized with port(s): 8080 (http)
4. placeholder占位符
springboot允许我们在banner中使用${}格式的占位符,但仅限于文本banner。常见的内置的占位符有应用版本、springboot版本、应用名。
| 占位符中文名 | 占位符英文名 |
|---|---|
| 应用版本 | ${application.version} 或 ${application.formatted-version} |
| springboot版本 | ${spring-boot.version} 或 ${spring-boot.formatted-version} |
| 应用名 | ${application.title} |
5. 关闭banner
如果项目启动时候不需要输出banner,可以选择关闭bannner,关闭banner的方式有两种:
-
配置文件
spring.main.banner_mode=off -
SpringApplication类setBannerMode方法设置
public class Main {public static void main(String[] args) {SpringApplication springApplication = new SpringApplication(Main.class);// 关闭banner输出springApplication.setBannerMode(Banner.Mode.OFF);springApplication.run(args);} }
三. 源码解析
SpringBoot中banner输出的逻辑全部都在SpringApplication的printBanner方法中,它在启动时被run方法调用。
private Banner printBanner(ConfigurableEnvironment environment) {if (this.bannerMode == Banner.Mode.OFF) {return null;}ResourceLoader resourceLoader = (this.resourceLoader != null) ? this.resourceLoader: new DefaultResourceLoader(null);SpringApplicationBannerPrinter bannerPrinter = new SpringApplicationBannerPrinter(resourceLoader, this.banner);if (this.bannerMode == Mode.LOG) {return bannerPrinter.print(environment, this.mainApplicationClass, logger);}return bannerPrinter.print(environment, this.mainApplicationClass, System.out);}
关于banner打印的核心逻辑可以分成两个部分:获取banner和打印banner。
1. 获取banner
获取banner的逻辑实现在getBanner()方法中
private Banner getBanner(Environment environment) {Banners banners = new Banners();banners.addIfNotNull(getImageBanner(environment));banners.addIfNotNull(getTextBanner(environment));if (banners.hasAtLeastOneBanner()) {return banners;}if (this.fallbackBanner != null) {return this.fallbackBanner;}return DEFAULT_BANNER;}
该方法的逻辑就是获取图片和文字banner,并添加到banners中,如果banners不为空则返回banners,否则返回兜底banner;如果兜底banner也不存在,则返回默认banner。
2. 打印banner
前面在getBanner()方法中获取到的banner集合被添加到banners中,类Banners是Banner的子类,在它实现的printBanner()方法中,通过遍历内部的banner集合并调用printBanner()方法对不同的banner进行打印。
@Override
public void printBanner(Environment environment, Class<?> sourceClass, PrintStream out) {for (Banner banner : this.banners) {banner.printBanner(environment, sourceClass, out);}
}
下面对不同类型bannner的打印逻辑进行分析
-
图片banner
图片banner被封装在ImageBanner对象中,在打印图片banner时,会对java.awt.headless的配置进行处理,然后再调用其私有方法printBanner真正输出图片banner。@Override public void printBanner(Environment environment, Class<?> sourceClass, PrintStream out) {try {if (System.getProperty(SYSTEM_PROPERTY_JAVA_AWT_HEADLESS) == null) {System.setProperty(SYSTEM_PROPERTY_JAVA_AWT_HEADLESS, "true");}printBanner(environment, out);}catch (Throwable ex) {logger.warn(LogMessage.format("Image banner not printable: %s (%s: '%s')", this.image, ex.getClass(),ex.getMessage()));logger.debug("Image banner printing failure", ex);} }以下是printBanner()私有方法,查看其真正输出图片banner的逻辑。
private void printBanner(Environment environment, PrintStream out) throws IOException {int width = getProperty(environment, "width", Integer.class, 76);int height = getProperty(environment, "height", Integer.class, 0);int margin = getProperty(environment, "margin", Integer.class, 2);boolean invert = getProperty(environment, "invert", Boolean.class, false);BitDepth bitDepth = getBitDepthProperty(environment);PixelMode pixelMode = getPixelModeProperty(environment);Frame[] frames = readFrames(width, height);for (int i = 0; i < frames.length; i++) {if (i > 0) {resetCursor(frames[i - 1].getImage(), out);}printBanner(frames[i].getImage(), margin, invert, bitDepth, pixelMode, out);sleep(frames[i].getDelayTime());}}在该方法中,从配置中获取图片banner的宽高等基本样式,然后将其输出,在输出过程中将图片转为ASCII艺术图。
-
文本banner
文本banner被封装在ResourceBanner对象中,banner打印的逻辑在printBanner()方法中。该方法的逻辑首先将文本banner按照配置文件中spring.banner.charset指定的字符集转换为对应的banner字符串;然后获取用于解析${}形式的占位符的解析器集合,利用解析器处理banner字符串中的占位符。最后将处理后的banner字符串输出。
@Overridepublic void printBanner(Environment environment, Class<?> sourceClass, PrintStream out) {try {String banner = StreamUtils.copyToString(this.resource.getInputStream(),environment.getProperty("spring.banner.charset", Charset.class, StandardCharsets.UTF_8));for (PropertyResolver resolver : getPropertyResolvers(environment, sourceClass)) {banner = resolver.resolvePlaceholders(banner);}out.println(banner);}catch (Exception ex) {logger.warn(LogMessage.format("Banner not printable: %s (%s: '%s')", this.resource, ex.getClass(),ex.getMessage()), ex);}}文本banner的占位符解析器主要有版本号解析器、文本格式解析器、应用标题解析器。
protected List<PropertyResolver> getPropertyResolvers(Environment environment, Class<?> sourceClass) {MutablePropertySources sources = new MutablePropertySources();if (environment instanceof ConfigurableEnvironment) {((ConfigurableEnvironment) environment).getPropertySources().forEach(sources::addLast);}sources.addLast(getTitleSource(sourceClass));sources.addLast(getAnsiSource());sources.addLast(getVersionSource(sourceClass));List<PropertyResolver> resolvers = new ArrayList<>();resolvers.add(new PropertySourcesPropertyResolver(sources));return resolvers;} -
默认banner
默认banner被封装在SpringBootBanner中,banner打印的逻辑在printBanner()方法中。该方法逻辑主要是按行输出BANNER字符串的内容,同时获取SpringBoot的版本号并输出。
@Overridepublic void printBanner(Environment environment, Class<?> sourceClass, PrintStream printStream) {for (String line : BANNER) {printStream.println(line);}String version = SpringBootVersion.getVersion();version = (version != null) ? " (v" + version + ")" : "";StringBuilder padding = new StringBuilder();while (padding.length() < STRAP_LINE_SIZE - (version.length() + SPRING_BOOT.length())) {padding.append(" ");}printStream.println(AnsiOutput.toString(AnsiColor.GREEN, SPRING_BOOT, AnsiColor.DEFAULT, padding.toString(),AnsiStyle.FAINT, version));printStream.println();}
