以下是一个完整、标准、可直接使用的 Spring Boot 3 自定义 Banner Starter 实现方案,基于 SpringApplicationRunListener 实现,在应用启动最早期(日志输出前)替换默认 Banner,并支持通过 application.yaml 动态配置 Banner 内容。
✅ 项目目标
开发一个名为 custom-banner-starter 的 Spring Boot Starter,实现:
- 自动替换 Spring Boot 默认 Banner;
- Banner 内容可通过
application.yaml配置; - 启动时机与官方一致:在日志输出前第一个打印;
- 无需手动注册,通过
spring.factories自动加载; - 支持 Spring Boot 3.x(兼容 Java 17+);
📁 项目结构
spring-boot-custom-banner-starter/
├── src/
│ └── main/
│ ├── java/
│ │ └── com/example/banner/
│ │ ├── CustomBanner.java # 自定义 Banner 实现
│ │ ├── CustomBannerAutoConfiguration.java # 自动配置类(可选,用于配置绑定)
│ │ └── CustomBannerProperties.java # 配置属性类
│ └── resources/
│ ├── META-INF/
│ │ ├── spring.factories # ✅ SPI 注册文件
│ │ └── spring/
│ │ └── org.springframework.boot.autoconfigure.AutoConfiguration.imports # ✅ 自动配置注册(仅用于绑定配置)
│ └── application.yml.example # 配置示例
└── pom.xml
🔧 1. CustomBannerProperties.java —— 配置属性类
package com.example.custombanner;
import org.springframework.boot.context.properties.ConfigurationProperties;
/**
* 自定义 Banner 配置属性类
* 通过 application.yaml 配置,支持动态定制 Banner 内容
* 前缀:custom.banner
*/
@Setter
@Getter
@ConfigurationProperties(prefix = CustomBannerProperties.PREFIX)
public class CustomBannerProperties {
public static final String PREFIX = "custom.banner";
// 是否启用自定义 Banner(默认 true)
private boolean enabled = true;
// 默认标题(可覆盖)
private String applicationName = "CUSTOM SPRING BOOT";
// 项目版本号(默认使用 pom.xml 中的版本,也可自定义)
private String version = "1.0.0";
// 项目作者
private String author;
// 项目描述,默认取 pom.xml 中的 description
private String description;
// 是否显示系统信息
private boolean showSystemInfo = true;
// 是否显示版本信息(默认 true)
private boolean showVersion = true;
// 输出类型
private BannerType type = BannerType.TEXT;
@Getter
@RequiredArgsConstructor
public enum BannerType {
TEXT("text", "Text banner"),
FIGLET("figlet", "Figlet banner"),
IMAGE("image", "Image banner");
private final String value;
private final String description;
}
}
✅ 说明:使用
@ConfigurationProperties绑定custom.banner.*配置项,支持默认值和动态覆盖。
🔧 2. CustomSpringApplicationRunListener.java —— 核心监听器
package com.example.custombanner;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.SpringApplicationRunListener;
import org.springframework.boot.env.EnvironmentPostProcessor;
import org.springframework.context.ConfigurableApplicationContext;
import org.springframework.core.env.ConfigurableEnvironment;
import org.springframework.core.env.MutablePropertySources;
import org.springframework.core.env.PropertiesPropertySource;
import org.springframework.core.io.ClassPathResource;
import org.springframework.core.io.Resource;
import org.springframework.util.StringUtils;
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.nio.charset.StandardCharsets;
import java.util.Arrays;
import java.util.Properties;
/**
* 自定义 SpringApplicationRunListener,用于在 Spring Boot 启动最初始阶段(日志输出前)替换 Banner
* 该监听器会在 SpringApplication.run() 调用时,由 Spring Boot 自动发现并执行
* 优先级高于默认 Banner,确保在任何日志输出之前打印自定义内容
*/
package com.example.banner.listener;
import com.example.banner.printer.FigletBannerPrinter;
import com.example.banner.printer.ImageBannerPrinter;
import com.example.banner.printer.TextBannerPrinter;
import org.springframework.boot.ConfigurableBootstrapContext;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.SpringApplicationRunListener;
import org.springframework.context.ConfigurableApplicationContext;
import org.springframework.core.Ordered;
import org.springframework.core.env.ConfigurableEnvironment;
import java.time.Duration;
import java.time.Instant;
/**
* 自定义 SpringApplicationRunListener,用于在 Spring Boot 启动最初始阶段(日志输出前)替换 Banner
* 该监听器会在 SpringApplication.run() 调用时,由 Spring Boot 自动发现并执行
* 优先级高于默认 Banner,确保在任何日志输出之前打印自定义内容
*/
// 可参考 Spring Boot 官方的示例类 EventPublishingRunListener
public class CustomSpringApplicationRunListener implements SpringApplicationRunListener, Ordered {
private final SpringApplication application;
private final String[] args;
private Instant startTime;
private Instant contextPreparedTime;
/**
* 必须的构造函数
* Spring Boot 会通过反射调用这个构造函数
*/
public CustomSpringApplicationRunListener(SpringApplication application, String[] args) {
this.application = application;
this.args = args;
System.out.println("CustomSpringApplicationRunListener 构造函数被调用");
}
@Override
public void starting(ConfigurableBootstrapContext bootstrapContext) {
startTime = Instant.now();
System.out.println("🚀 [阶段1 - starting] 应用启动开始");
System.out.println(" 启动时间: " + startTime);
System.out.println(" 主应用类: " + application.getMainApplicationClass());
System.out.println(" 命令行参数: " + String.join(", ", args));
}
@Override
public void environmentPrepared(ConfigurableBootstrapContext bootstrapContext, ConfigurableEnvironment environment) {
System.out.println("🌍 [阶段2 - environmentPrepared] 环境准备完成");
System.out.println(" 激活的配置文件: " + String.join(", ", environment.getActiveProfiles()));
System.out.println(" 默认配置文件: " + String.join(", ", environment.getDefaultProfiles()));
System.out.println(" 应用名称: " + environment.getProperty("spring.application.name", "未设置"));
switch (environment.getProperty("custom.banner.type", "figlet")) {
case "text":
new TextBannerPrinter().printBanner(environment, application.getMainApplicationClass(), System.out);
break;
case "figlet":
new FigletBannerPrinter().printBanner(environment, application.getMainApplicationClass(), System.out);
break;
case "image":
new ImageBannerPrinter().printBanner(environment, application.getMainApplicationClass(), System.out);
break;
}
}
@Override
public void contextPrepared(ConfigurableApplicationContext context) {
contextPreparedTime = Instant.now();
System.out.println("🔧 [阶段3 - contextPrepared] 应用上下文创建完成");
System.out.println(" 上下文ID: " + context.getId());
System.out.println(" 上下文显示名称: " + context.getDisplayName());
System.out.println(" 启动阶段耗时: " +
Duration.between(startTime, contextPreparedTime).toMillis() + "ms");
}
@Override
public void contextLoaded(ConfigurableApplicationContext context) {
Instant contextLoadedTime = Instant.now();
System.out.println("📦 [阶段4 - contextLoaded] Bean定义加载完成");
System.out.println(" Bean定义数量: " + context.getBeanDefinitionCount());
System.out.println(" 上下文准备耗时: " +
Duration.between(contextPreparedTime, contextLoadedTime).toMillis() + "ms");
}
@Override
public void started(ConfigurableApplicationContext context, Duration timeTaken) {
System.out.println("✅ [阶段5 - started] 应用上下文刷新完成");
System.out.println(" 启动总耗时: " + timeTaken.toMillis() + "ms");
System.out.println(" 所有Bean定义: " + String.join(", ", context.getBeanDefinitionNames()));
}
@Override
public void ready(ConfigurableApplicationContext context, Duration timeTaken) {
System.out.println("🎉 [阶段6 - ready] 应用完全启动完成");
System.out.println(" 就绪阶段耗时: " + timeTaken.toMillis() + "ms");
System.out.println(" 总启动时间: " +
Duration.between(startTime, Instant.now()).toMillis() + "ms");
}
@Override
public void failed(ConfigurableApplicationContext context, Throwable exception) {
SpringApplicationRunListener.super.failed(context, exception);
System.out.println("❌ [阶段7 - failed] 应用启动失败");
System.out.println(" 失败原因: " + exception.getMessage());
System.out.println(" 失败时间: " + Instant.now());
}
@Override
public int getOrder() {
return 0;
}
}
✅ 关键点说明:
starting()方法是唯一能在日志输出前打印 Banner 的时机;environmentPrepared()用于提前读取配置,因为此时ApplicationContext还未创建,不能使用@Autowired;- 使用
System.out.println()直接输出,确保在日志框架(Logback)初始化前完成;- 配置绑定通过
Properties手动解析,兼容 Spring Boot 3 的启动顺序。
🔧 3. CustomBannerAutoConfiguration.java —— 自动配置类
package com.example.custombanner;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.context.annotation.Configuration;
/**
* 自动配置类:启用自定义 Banner Starter
* 该类仅用于注册 @ConfigurationProperties 和确保监听器被加载
* 实际的监听器注册通过 META-INF/spring.factories 完成
*/
@Configuration
@EnableConfigurationProperties(CustomBannerProperties.class)
@ConditionalOnProperty(
prefix = "custom.banner",
name = "enabled",
havingValue = "true",
matchIfMissing = true // 默认启用
)
public class CustomBannerAutoConfiguration {
// 此类无需任何 Bean 定义,仅用于激活配置绑定
// 真正的监听器注册在 spring.factories 中
}
✅ 说明:此配置类确保
@ConfigurationProperties生效,并通过@ConditionalOnProperty控制是否启用(默认开启)。
📦 4. META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports —— Spring Boot 3 注册 IOC 组件的新方式
✅ 这是 Spring Boot 3 的新的 IOC 组件注册机制,替代旧版
spring.factories但是SpringApplicationRunListener监听器的注册依旧依赖于spring.factories来完成,这一点需要特别注意
在 src/main/resources/META-INF/ 目录下创建 spring.factories 文件:
# src/main/resources/META-INF/spring.factories
org.springframework.boot.SpringApplicationRunListener=\
com.example.banner.listener.CustomSpringApplicationRunListener
在 src/main/resources/META-INF/spring/ 目录下创建 org.springframework.boot.autoconfigure.AutoConfiguration.imports 文件:
# src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports
com.example.banner.CustomBannerAutoConfiguration
艺术字输出工具类:
package com.example.banner.utils;
import lombok.Getter;
import java.io.*;
import java.nio.charset.StandardCharsets;
import java.util.StringTokenizer;
/**
* Figlet 艺术字体工具类
* 用于将普通文本转换为ASCII艺术字体
* <p>
* Figlet 是一种将普通文本转换为大型字母的程序,
* 字母由文本字符组成,类似于早期电脑和游戏机上的字符艺术。</p>
*
* @author urbane
* @since 1.0.0
*/
@Getter
public class FigletUtil {
/**
* 支持的最大字符数
*/
private static final int MAX_CHARS = 1024;
/**
* 硬空格字符,用于替换普通空格
*/
private final char hardBlank;
/**
* 字体高度(行数)
*/
private final int height;
/**
* 不包含下降部分的字体高度
*/
private final int heightWithoutDescenders;
/**
* 最大行长度
*/
private final int maxLine;
/**
* 字符压缩模式
*/
private final int smushMode;
/**
* 字体数据数组
* 三维数组:[字符编码][行数][列数]
*/
private final char[][][] font; // [charCode][row][col]
/**
* 字体名称
*/
private final String fontName;
/**
* 构造函数,从输入流加载字体文件
*
* @param fontStream 字体文件输入流
* @throws IOException 当读取字体文件出错时抛出
*/
public FigletUtil(InputStream fontStream) throws IOException {
if (fontStream == null) {
throw new IllegalArgumentException("Font stream cannot be null");
}
this.font = new char[MAX_CHARS][][];
try (BufferedReader bufferedReader = new BufferedReader(
new InputStreamReader(new BufferedInputStream(fontStream), StandardCharsets.UTF_8))) {
// 读取字体文件头部信息
String headerLine = bufferedReader.readLine();
if (headerLine == null) {
throw new IOException("Invalid font file: empty header");
}
StringTokenizer tokenizer = new StringTokenizer(headerLine, " ");
if (tokenizer.countTokens() < 6) {
throw new IOException("Invalid font header format");
}
// 解析字体基本信息
String signature = tokenizer.nextToken();
this.hardBlank = signature.charAt(signature.length() - 1);
this.height = Integer.parseInt(tokenizer.nextToken());
this.heightWithoutDescenders = Integer.parseInt(tokenizer.nextToken());
this.maxLine = Integer.parseInt(tokenizer.nextToken());
this.smushMode = Integer.parseInt(tokenizer.nextToken());
// 读取注释行数
int commentLines = Integer.parseInt(tokenizer.nextToken());
// 读取字体名称
tokenizer = new StringTokenizer(bufferedReader.readLine(), " ");
if (tokenizer.hasMoreElements()) {
this.fontName = tokenizer.nextToken();
} else {
this.fontName = "";
}
// 跳过注释行
for (int i = 0; i < commentLines - 1; i++) {
bufferedReader.readLine();
}
// 读取字符数据
int charCode = 31;
while (headerLine != null) {
++charCode;
for (int lineIndex = 0; lineIndex < this.height; ++lineIndex) {
headerLine = bufferedReader.readLine();
if (headerLine != null) {
boolean isAbnormalFormat = true;
if (lineIndex == 0) {
try {
String codeTag = headerLine.concat(" ").split(" ")[0];
if (codeTag.length() > 2 && "x".equals(codeTag.substring(1, 2))) {
// 十六进制编码
charCode = Integer.parseInt(codeTag.substring(2), 16);
} else {
// 十进制编码
charCode = Integer.parseInt(codeTag);
}
} catch (NumberFormatException var18) {
isAbnormalFormat = false;
}
// 如果是异常格式,读取下一行并恢复字符编码
if (isAbnormalFormat) {
headerLine = bufferedReader.readLine();
}
}
// 初始化字符数据数组
if (lineIndex == 0) {
this.font[charCode] = new char[this.height][];
}
// 计算当前行的有效长度
int effectiveLength = headerLine.length() - 1 - (lineIndex == this.height - 1 ? 1 : 0);
if (this.height == 1) {
++effectiveLength;
}
// 分配数组空间
this.font[charCode][lineIndex] = new char[effectiveLength];
// 处理字符数据,将硬空格替换为空格
// 处理字符数据,将硬空格替换为空格
for (int charIndex = 0; charIndex < effectiveLength; charIndex++) {
char currentChar = headerLine.charAt(charIndex);
this.font[charCode][lineIndex][charIndex] =
(currentChar == this.hardBlank) ? ' ' : currentChar;
}
}
}
}
}
}
/**
* 将普通文本转换为 Figlet 艺术字体
*
* @param message 要转换的文本
* @return 转换后的艺术字体字符串
*/
public String convert(String message) {
StringBuilder result = new StringBuilder();
// 按行处理,逐行构建艺术字体
for (int lineIndex = 0; lineIndex < this.height; lineIndex++) {
// 处理每个字符
for (int charIndex = 0; charIndex < message.length(); charIndex++) {
String charLine = this.getCharLineString(message.charAt(charIndex), lineIndex);
if (charLine != null) {
result.append(charLine);
}
}
// 每行结束后添加换行符
result.append('\n');
}
return result.toString();
}
/**
* 获取指定字符某一行的字符串表示
*
* @param characterCode 字符的ASCII码
* @param lineIndex 行索引
* @return 指定行的字符串,如果不存在则返回null
*/
public String getCharLineString(int characterCode, int lineIndex) {
return this.font[characterCode][lineIndex] == null ? null : new String(this.font[characterCode][lineIndex]);
}
/**
* 使用默认字体(Standard.flf)转换单行文本
*
* @param message 要转换的文本
* @return 转换后的艺术字体字符串
* @throws IOException 当读取字体文件或转换过程中出错时抛出
*/
public static String convertOneLine(String message) throws IOException {
return convertOneLine(FigletUtil.class.getClassLoader().getResourceAsStream("/fonts/Standard.flf"), message);
}
/**
* 使用指定字体流转换单行文本
*
* @param fontFileStream 字体文件输入流
* @param message 要转换的文本
* @return 转换后的艺术字体字符串
* @throws IOException 当读取字体文件或转换过程中出错时抛出
*/
public static String convertOneLine(InputStream fontFileStream, String message) throws IOException {
return (new FigletUtil(fontFileStream)).convert(message);
}
/**
* 使用指定字体文件转换单行文本
*
* @param fontFile 字体文件
* @param message 要转换的文本
* @return 转换后的艺术字体字符串
* @throws IOException 当读取字体文件或转换过程中出错时抛出
*/
public static String convertOneLine(File fontFile, String message) throws IOException {
return convertOneLine(new FileInputStream(fontFile), message);
}
}
package com.example.banner.printer;
import com.example.banner.utils.FigletUtil;
import org.springframework.boot.Banner;
import org.springframework.boot.SpringBootVersion;
import org.springframework.core.env.Environment;
import java.io.InputStream;
import java.io.PrintStream;
import java.net.InetAddress;
import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;
public class FigletBannerPrinter implements Banner {
private static final String RESET = "\u001B[0m";
private static final String GREEN = "\u001B[32m";
private static final String CYAN = "\u001B[36m";
private static final String YELLOW = "\u001B[33m";
@Override
public void printBanner(Environment environment, Class<?> sourceClass, PrintStream out) {
try {
// 获取服务名(动态!)
String appName = environment.getProperty("spring.application.name", "Unknown-Service").toUpperCase();
// ✅ 动态生成 ASCII Art 标题
String asciiArtTitle = generateAsciiArt(appName);
// 其他信息
String serverPort = environment.getProperty("server.port", "8080");
String profiles = String.join(",", environment.getActiveProfiles());
String version = environment.getProperty("info.app.version", "1.0.0");
String springBootVersion = SpringBootVersion.getVersion();
String startTime = LocalDateTime.now().format(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss"));
String author = environment.getProperty("custom.banner.author", "unknown");
String hostAddress = getHostAddress();
String baseUrl = "https://" + hostAddress + ":" + serverPort;
String healthUrl = baseUrl + "/actuator/health";
String swaggerUrl = baseUrl + (isSwaggerV3(environment) ? "/swagger-ui/index.html" : "/swagger-ui.html");
// 打印横幅
out.println(GREEN + "===============================================" + RESET);
out.println(CYAN + asciiArtTitle + RESET); // ✅ 动态 ASCII Art 标题
out.println(GREEN + "===============================================" + RESET);
out.println(" 服务端口 : " + YELLOW + "🌐 " + serverPort + RESET);
out.println(" 服务IP : " + CYAN + "📍 " + hostAddress + RESET);
out.println(" 激活环境 : " + GREEN + "🧪 " + (profiles.isEmpty() ? "default" : profiles) + RESET);
out.println(" 服务版本 : " + YELLOW + "🏷️ " + version + RESET);
out.println(" Spring Boot : " + GREEN + "🌱 " + springBootVersion + RESET);
out.println(" 启动时间 : " + YELLOW + "🕙 " + startTime + RESET);
out.println(" 项目作者 : " + CYAN + "📍 " + author + RESET);
out.println(" 健康检查 : " + CYAN + "🩺 " + healthUrl + RESET);
out.println(" Swagger文档 : " + GREEN + "📚 " + swaggerUrl + RESET);
out.println(GREEN + "===============================================" + RESET);
} catch (Exception e) {
out.println("⚠️ 横幅打印异常: " + e.getMessage());
}
}
// ✅ 核心方法:动态生成 ASCII Art
private String generateAsciiArt(String text) {
try {
// 可选的字体文件下载,参考官网 https://github.com/xero/figlet-fonts/tree/master
// 或者直接去 ASCII 艺术字生成工具推荐:https://patorjk.com/software/taag/#p=testall 去在线挑选
// figlet 内置字体 "slant"(推荐),也可换 "standard",
// return FigletFont.convertOneLine(text); // 默认 slant
// return FigletFont.convertOneLine(text, "standard");
// 自定义字体,figlet 工具包默认只提供了两个字体,我不喜欢。可以到 https://github.com/xero/figlet-fonts/tree/master 选择自己喜欢的字体
InputStream fontStream = getClass().getResourceAsStream("/fonts/ANSI Shadow.flf");
// InputStream fontStream = getClass().getResourceAsStream("/fonts/ANSI Regular.flf");
// 使用内置字体 "slant", "standard"(默认) 两种可选字体,也可以下载并使用自定义字体
return FigletUtil.convertOneLine(fontStream, text);
} catch (Exception e) {
// 降级:如果字体转换失败,返回原始文本
return "🚀 " + text;
}
}
// 判断是否 Swagger V3
private boolean isSwaggerV3(Environment environment) {
return environment.getProperty("springdoc.api-docs.enabled", Boolean.class, false) ||
environment.containsProperty("springdoc.swagger-ui.path");
}
// 获取主机 IP
private String getHostAddress() {
try {
InetAddress address = InetAddress.getLocalHost();
if (address.isLoopbackAddress()) {
return String.valueOf(InetAddress.getByName(java.net.NetworkInterface
.getNetworkInterfaces().nextElement().getInetAddresses().nextElement().getHostAddress()));
}
return address.getHostAddress();
} catch (Exception e) {
return "127.0.0.1";
}
}
}
📄 5. 可选:提供示例配置文件 application.yml.example
在 src/main/resources/ 下创建:
# custom-banner-starter 示例配置
# 请复制到你的 application.yaml 中并按需修改
custom:
banner:
enabled: true # 是否启用自定义 Banner(默认 true)
title: "MY INSURANCE SYSTEM" # 自定义标题(默认:CUSTOM SPRING BOOT)
version: "v1.0.0-INSURANCE" # 自定义版本(默认:自动获取 Spring Boot 版本)
border-char: "=" # 边框字符(默认:=)
show-spring-logo: true # 是否显示 Spring 徽标(默认 true)
show-version: true # 是否显示版本(默认 true)
✅ 用户只需将此内容复制到自己的
application.yaml中即可定制。
📦 6. pom.xml(Maven 依赖)
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>com.example</groupId>
<artifactId>custom-banner-starter</artifactId>
<version>1.0.0</version>
<packaging>jar</packaging>
<name>custom-banner-starter</name>
<description>自定义 Spring Boot Banner 启动器</description>
<properties>
<java.version>17</java.version>
<spring-boot.version>3.5.4</spring-boot.version>
</properties>
<dependencyManagement>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-dependencies</artifactId>
<version>${spring-boot.version}</version>
<type>pom</type>
<scope>import</scope>
</dependency>
</dependencies>
</dependencyManagement>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter</artifactId>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-autoconfigure</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-configuration-processor</artifactId>
<optional>true</optional>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
<configuration>
<skip>true</skip> <!-- 此 starter 不需打包为可执行 jar -->
</configuration>
</plugin>
</plugins>
</build>
</project>
✅ 7. 使用示例(在目标 Spring Boot 项目中)
① 引入依赖(Maven)
<dependency>
<groupId>com.example</groupId>
<artifactId>custom-banner-starter</artifactId>
<version>1.0.0</version>
</dependency>
② 在 application.yaml 中配置(可选)
custom:
banner:
title: "人寿保险核心系统"
version: "v2.3.1-PROD"
border-char: "#"
show-spring-logo: false
show-version: true
③ 启动应用,效果如下:
============================================================
:: 人寿保险核心系统 ::
v2.3.1-PROD
============================================================
✅ 在任何日志输出之前打印,完全替代默认 Banner。
🔍 8. 与 ApplicationListener / CommandLineRunner 的区别
| 特性 | SpringApplicationRunListener | ApplicationListener<ContextRefreshedEvent> | CommandLineRunner / ApplicationRunner |
|---|---|---|---|
| 执行时机 | 最早(日志前) | 上下文刷新后(应用已启动) | 应用启动完成后 |
| 能否改 Banner | ✅ 唯一正确方式 | ❌ 太晚,日志已输出 | ❌ 太晚 |
| 是否需要手动注册 | ❌ 自动发现(通过 imports) | ✅ 需要 @Component | ✅ 需要 @Component |
| 是否影响启动流程 | ✅ 影响启动早期阶段 | ❌ 仅在启动后触发 | ❌ 启动后触发 |
✅ 结论:只有
SpringApplicationRunListener能在日志输出前修改 Banner,是唯一合法方案。
⚠️ 9. 注意事项与最佳实践
| 项目 | 说明 |
|---|---|
| 文件命名必须精确 | META-INF/spring/org.springframework.boot.SpringApplicationRunListener 必须完全匹配,大小写敏感 |
不要使用 @Component | CustomSpringApplicationRunListener 不要加 @Component,否则会被重复加载 |
| 避免使用 Spring Bean | 在 starting() 中不能注入 @Autowired Bean,因为上下文尚未初始化 |
使用 System.out.println | 必须使用原生输出,避免 Logger,否则可能被日志框架拦截或延迟 |
| 配置优先级 | custom.banner.enabled=false 可关闭此 Starter,不影响其他功能 |
| 兼容性 | 本方案兼容 Spring Boot 3.0+,不支持 2.x(因注册机制不同) |
| 测试建议 | 在独立 Spring Boot 项目中测试,避免与已有 Banner 冲突 |
✅ 10. 总结:为什么这个方案是“标准”的?
| 优势 | 说明 |
|---|---|
| ✅ 时机精准 | 使用 SpringApplicationRunListener.starting(),确保 Banner 在日志前输出 |
| ✅ 配置灵活 | 支持通过 application.yaml 动态定制,无需重新打包 |
| ✅ 自动注册 | 使用 Spring Boot 3 推荐的 AutoConfiguration.imports,无 spring.factories 兼容问题 |
| ✅ 无侵入性 | 不修改用户代码,引入即生效 |
| ✅ 企业级可用 | 适合银行、保险等对启动信息有品牌要求的场景 |
| ✅ 符合规范 | 严格遵循 Spring Boot 官方扩展机制 |
📎 附:如何发布到私有仓库?
- 执行
mvn clean install→ 本地仓库; - 或部署到公司 Nexus/Artifactory;
- 项目中引入依赖即可自动生效。
✅ 最终效果:你的保险系统启动时,不再显示“Spring Boot :: (v3.5.4)”,而是优雅地展示 “人寿保险核心系统 v2.3.1-PROD”,体现专业性和品牌一致性。
1534

被折叠的 条评论
为什么被折叠?



