SpringBoot源码系列文章
SpringBoot源码解析(一):SpringApplication构造方法
SpringBoot源码解析(二):引导上下文DefaultBootstrapContext
目录
前言
在前文中,我们深入解析了SpringBoot启动时应用环境的准备过程
。接下来将深入介绍启动Banner
打印的具体实现及流程。
SpringBoot版本2.7.18
SpringApplication的run方法的执行逻辑如下,本文将详细介绍第5小节:打印启动Banner
// SpringApplication类方法
public ConfigurableApplicationContext run(String... args) {
// 记录应用启动的开始时间
long startTime = System.nanoTime();
// 1.创建引导上下文,用于管理应用启动时的依赖和资源
DefaultBootstrapContext bootstrapContext = createBootstrapContext();
ConfigurableApplicationContext context = null;
// 配置无头模式属性,以支持在无图形环境下运行
// 将系统属性 java.awt.headless 设置为 true
configureHeadlessProperty();
// 2.获取Spring应用启动监听器,用于在应用启动的各个阶段执行自定义逻辑
SpringApplicationRunListeners listeners = getRunListeners(args);
// 启动开始方法(发布开始事件、通知应用监听器ApplicationListener)
listeners.starting(bootstrapContext, this.mainApplicationClass);
try {
// 3.解析应用参数
ApplicationArguments applicationArguments = new DefaultApplicationArguments(args);
// 4.准备应用环境,包括读取配置文件和设置环境变量
ConfigurableEnvironment environment = prepareEnvironment(listeners, bootstrapContext, applicationArguments);
// 配置是否忽略 BeanInfo,以加快启动速度
configureIgnoreBeanInfo(environment);
// 5.打印启动Banner
Banner printedBanner = printBanner(environment);
// 6.创建应用程序上下文
context = createApplicationContext();
// 设置应用启动的上下文,用于监控和管理启动过程
context.setApplicationStartup(this.applicationStartup);
// 7.准备应用上下文,包括加载配置、添加 Bean 等
prepareContext(bootstrapContext, context, environment, listeners, applicationArguments, printedBanner);
// 8.刷新上下文,完成 Bean 的加载和依赖注入
refreshContext(context);
// 9.刷新后的一些操作,如事件发布等
afterRefresh(context, applicationArguments);
// 计算启动应用程序的时间,并记录日志
Duration timeTakenToStartup = Duration.ofNanos(System.nanoTime() - startTime);
if (this.logStartupInfo) {
new StartupInfoLogger(this.mainApplicationClass).logStarted(getApplicationLog(), timeTakenToStartup);
}
// 10.通知监听器应用启动完成
listeners.started(context, timeTakenToStartup);
// 11.调用应用程序中的 `CommandLineRunner` 或 `ApplicationRunner`,以便执行自定义的启动逻辑
callRunners(context, applicationArguments);
}
catch (Throwable ex) {
// 12.处理启动过程中发生的异常,并通知监听器
handleRunFailure(context, ex, listeners);
throw new IllegalStateException(ex);
}
try {
// 13.计算应用启动完成至准备就绪的时间,并通知监听器
Duration timeTakenToReady = Duration.ofNanos(System.nanoTime() - startTime);
listeners.ready(context, timeTakenToReady);
}
catch (Throwable ex) {
// 处理准备就绪过程中发生的异常
handleRunFailure(context, ex, null);
throw new IllegalStateException(ex);
}
// 返回已启动并准备就绪的应用上下文
return context;
}
一、入口
// 5.打印启动Banner
Banner printedBanner = printBanner(environment);
// 打印启动 Banner 的方法,根据配置的 Banner 模式选择打印方式
private Banner printBanner(ConfigurableEnvironment environment) {
// 如果 Banner 模式被设置为 OFF,则不打印 Banner,直接返回 null
if (this.bannerMode == Banner.Mode.OFF) {
return null;
}
// 确定资源加载器。如果当前实例的 resourceLoader 不为空,则使用它;否则创建一个默认的资源加载器
ResourceLoader resourceLoader = (this.resourceLoader != null) ? this.resourceLoader
: new DefaultResourceLoader(null);
// 创建 Banner 打印器,负责加载和打印 Banner
SpringApplicationBannerPrinter bannerPrinter = new SpringApplicationBannerPrinter(resourceLoader, this.banner);
// 根据 Banner 模式决定打印到日志还是控制台
if (this.bannerMode == Mode.LOG) {
// 如果 Banner 模式为 LOG,则将 Banner 打印到日志中
return bannerPrinter.print(environment, this.mainApplicationClass, logger);
}
// 默认情况下(CONSOLE 模式),将 Banner 打印到标准输出(控制台)
return bannerPrinter.print(environment, this.mainApplicationClass, System.out);
}
二、Banner接口类
// 一个用于以编程方式输出 Banner 的接口类
@FunctionalInterface
public interface Banner {
// 将 Banner 输出到指定的打印流
void printBanner(Environment environment, Class<?> sourceClass, PrintStream out);
// 用于配置 Banner 的模式枚举
enum Mode {
// 禁用 Banner 的打印
OFF,
// 将 Banner 输出到 System.out
CONSOLE,
// 将 Banner 输出到日志文件
LOG
}
}
1、打印Banner开关
- 默认情况是打印到
控制台
- 可以通过
properties或yml
设置关闭打印Banner
spring.main.banner-mode=off
上一节有讲spring.main
开头的属性会绑定到SpringApplication
对象上,这样就可以通过配置文件的属性来决定Banner的打印模式。
三、打印Banner过程
1、console和log模式
- console控制台模式,默认设置
// SpringApplicationBannerPrinter类方法
Banner print(Environment environment, Class<?> sourceClass, PrintStream out) {
// 根据提供的环境信息获取横幅。
Banner banner = getBanner(environment);
// 将横幅打印到指定的输出流中。
banner.printBanner(environment, sourceClass, out);
// 返回一个 PrintedBanner 对象,包含打印的横幅和源类信息。
return new PrintedBanner(banner, sourceClass);
}
- log日志文件模式,通过在配置文件中设置
spring.main.banner-mode=log
,可以将应用启动Banner输出到日志文件中
// SpringApplicationBannerPrinter类方法
Banner print(Environment environment, Class<?> sourceClass, Log logger) {
// 根据提供的环境信息获取横幅。
Banner banner = getBanner(environment);
try {
logger.info(createStringFromBanner(banner, environment, sourceClass));
}
catch (UnsupportedEncodingException ex) {
logger.warn("Failed to create String for banner", ex);
}
// 返回一个 PrintedBanner 对象,包含打印的横幅和源类信息。
return new PrintedBanner(banner, sourceClass);
}
- log模式就是获取
打印流内容转换为字符串
,然后由log日志打印罢了
两种方式都会返回一个PrintedBanner
对象,主要是为以后在应用上下文中注册为Bean或供其他组件使用做准备。
2、四种Banner对象
- 图片和文本横幅组合的Banners
- 备用Banner
- 默认Banner
// SpringApplicationBannerPrinter类方法
// 默认Banner
private static final Banner DEFAULT_BANNER = new SpringBootBanner();
// 根据当前环境获取适当的横幅(Banner)
private Banner getBanner(Environment environment) {
// 创建一个 Banners 对象,用于存储图片和文本横幅
Banners banners = new Banners();
// 尝试获取图片横幅,并将其添加到 Banners 中(如果非空)
banners.addIfNotNull(getImageBanner(environment));
// 尝试获取文本横幅,并将其添加到 Banners 中(如果非空)
banners.addIfNotNull(getTextBanner(environment));
// 如果至少包含一个横幅,则返回组合的 Banners 对象
if (banners.hasAtLeastOneBanner()) {
return banners;
}
// 如果没有任何横幅但存在备用横幅,则返回备用横幅
if (this.fallbackBanner != null) {
return this.fallbackBanner;
}
// 如果没有任何横幅,则返回默认横幅
// Banner DEFAULT_BANNER = new SpringBootBanner();
return DEFAULT_BANNER;
}
Banners
对象内部持有多个Banner
实现类,遍历调用Banner的printBanner
方法
2.1、图片Banner
- 尝试根据环境信息获取图片横幅(Image Banner)
- 环境变量
spring.banner.image.location
用于指定图片路径;如果未设置,则默认加载路径为banner.gif
、banner.jpg
或banner.png
(按顺序查找)。
private Banner getImageBanner(Environment environment) {
// 从环境变量中获取横幅图片的路径
// String BANNER_IMAGE_LOCATION_PROPERTY = "spring.banner.image.location";
String location = environment.getProperty(BANNER_IMAGE_LOCATION_PROPERTY);
// 如果路径不为空,尝试加载对应的资源
if (StringUtils.hasLength(location)) {
Resource resource = this.resourceLoader.getResource(location);
// 如果资源存在,返回对应的 ImageBanner 对象
return resource.exists() ? new ImageBanner(resource) : null;
}
// 如果未指定路径,尝试加载默认图片横幅文件
// String[] IMAGE_EXTENSION = { "gif", "jpg", "png" };
for (String ext : IMAGE_EXTENSION) {
Resource resource = this.resourceLoader.getResource("banner." + ext);
// 如果找到存在的文件资源,返回对应的 ImageBanner 对象
if (resource.exists()) {
return new ImageBanner(resource);
}
}
// 如果没有找到任何图片横幅资源,返回 null
return null;
}
控制台效果
2.2、文字Banner
- 尝试根据环境信息获取文本横幅(Text Banner)
- 环境变量
spring.banner.location
用于指定文本路径;如果未设置,则默认加载路径为banner.txt
。
private Banner getTextBanner(Environment environment) {
// 获取横幅的路径,优先使用环境变量中的配置,如果没有配置则使用默认路径
// String BANNER_LOCATION_PROPERTY = "spring.banner.location";
// String DEFAULT_BANNER_LOCATION = "banner.txt";
String location = environment.getProperty(BANNER_LOCATION_PROPERTY, DEFAULT_BANNER_LOCATION);
// 使用 ResourceLoader 加载指定路径的资源
Resource resource = this.resourceLoader.getResource(location);
try {
// 检查资源是否存在,且路径中不包含 "liquibase-core"(防止加载到不相关的资源)
if (resource.exists() && !resource.getURL().toExternalForm().contains("liquibase-core")) {
// 如果资源有效,返回对应的 ResourceBanner 对象
return new ResourceBanner(resource);
}
} catch (IOException ex) {
// 忽略异常,可能是资源加载时出错或路径无效
// 在这里不抛出异常,而是返回 null,表示没有有效的横幅资源
}
// 如果资源无效或发生异常,返回 null
return null;
}
控制台效果
2.2、备用Banner
SpringApplicationBannerPrinter
对象的备用Banner属性fallbackBanner
是由SpringApplication
对象的私有属性banner
传递而来的。
- 可以在SpringBoot启动类中通过调用
SpringApplication
的setBanner
方法直接设置自定义的Banner
对象
@SpringBootApplication
public class Application {
public static void main(String[] args) {
SpringApplication app = new SpringApplication(Application.class);
// 设置全局备用横幅
app.setBanner((environment, sourceClass, out) -> {
out.println("===== 全局备用横幅 =====");
out.println(" 默认横幅已启用 ");
out.println("=====================");
});
app.run(args);
}
}
控制台效果
2.3、默认Banner
如果未设置自定义文字图片Banner或备用Banner,SpringBoot将使用默认的启动横幅(SpringBootBanner
)作为显示内容。
// 默认的 Banner 实现,用于打印 "Spring" 的启动横幅,和版本信息
class SpringBootBanner implements Banner {
// 预定义的 ASCII 艺术横幅,每行为一个数组元素
private static final String[] BANNER = {
"",
" . ____ _ __ _ _",
" /\\\\ / ___'_ __ _ _(_)_ __ __ _ \\ \\ \\ \\",
"( ( )\\___ | '_ | '_| | '_ \\/ _` | \\ \\ \\ \\",
" \\\\/ ___)| |_)| | | | | || (_| | ) ) ) )",
" ' |____| .__|_| |_|_| |_\\__, | / / / /",
" =========|_|==============|___/=/_/_/_/"
};
// 固定的 Spring Boot 标识符,用于横幅输出
private static final String SPRING_BOOT = " :: Spring Boot :: ";
// 横幅固定宽度,用于计算 padding 的空格数
private static final int STRAP_LINE_SIZE = 42;
/**
* 输出横幅到指定的 PrintStream(如控制台或日志)。
*/
@Override
public void printBanner(Environment environment, Class<?> sourceClass, PrintStream printStream) {
// 遍历并打印每一行 ASCII 艺术横幅
for (String line : BANNER) {
printStream.println(line);
}
// 获取 Spring Boot 的版本信息
String version = SpringBootVersion.getVersion();
// 如果版本信息不为空,则格式化为 "(vX.X.X)"
version = (version != null) ? " (v" + version + ")" : "";
// 构造 padding 空格,使横幅版本号对齐
StringBuilder padding = new StringBuilder();
while (padding.length() < STRAP_LINE_SIZE - (version.length() + SPRING_BOOT.length())) {
padding.append(" ");
}
// 打印 Spring Boot 标识符和版本信息,使用 ANSI 输出样式
printStream.println(AnsiOutput.toString(
AnsiColor.GREEN, // 绿色输出 Spring Boot 标识符
SPRING_BOOT,
AnsiColor.DEFAULT, // 恢复默认颜色
padding.toString(), // 填充的空格
AnsiStyle.FAINT, // 微弱样式(淡化显示版本信息)
version // 版本号
));
// 添加空行用于分隔横幅和其他输出
printStream.println();
}
}
控制台效果
总结
本文全面解析了SpringBoot启动横幅的实现原理、打印流程及自定义方法,介绍了文本横幅(默认路径为banner.txt
,可通过spring.banner.location
配置)、图片横幅(默认路径为banner.gif
、banner.jpg
或banner.png
,可通过spring.banner.image.location
配置)、备用横幅和默认横幅的使用,帮助开发者灵活运用横幅机制提升项目启动体验。