上两篇我们解读了环境准备及配置文件的加载,本计划是打印banner和创建容器一起解读的,但是创建容器的内容也不少,又会超出字数,编辑特别的慢,希望官方优化下,这次就单独把打印banner分出来了,虽说我觉得这个打印这个东西意义不是很大,不管怎么样,我们还是去了解下,首先我们还是先回顾下启动的整体流程。
接下来的几个方法所在类的具体路径:org.springframework.boot.SpringApplication
public ConfigurableApplicationContext run(String... args) {
// 1、记录启动的开始时间(单位纳秒)
long startTime = System.nanoTime();
// 2、初始化启动上下文、初始化应用上下文
DefaultBootstrapContext bootstrapContext = createBootstrapContext();
ConfigurableApplicationContext context = null;
// 3、设置无头属性:“java.awt.headless”,默认值为:true(没有图形化界面)
configureHeadlessProperty();
// 4、获取所有 Spring 运行监听器
SpringApplicationRunListeners listeners = getRunListeners(args);
// 发布应用启动事件
listeners.starting(bootstrapContext, this.mainApplicationClass);
try {
// 5、初始化默认应用参数类(命令行参数)
ApplicationArguments applicationArguments = new DefaultApplicationArguments(args);
// 6、根据运行监听器和应用参数 来准备 Spring 环境
ConfigurableEnvironment environment = prepareEnvironment(listeners, bootstrapContext, applicationArguments);
// 配置忽略bean信息
configureIgnoreBeanInfo(environment);
// 7、创建 Banner 并打印
Banner printedBanner = printBanner(environment);
// 8、创建应用上下文
context = createApplicationContext();
// 设置applicationStartup
context.setApplicationStartup(this.applicationStartup);
// 9、准备应用上下文
prepareContext(bootstrapContext, context, environment, listeners, applicationArguments, printedBanner);
// 10、刷新应用上下文(核心)
refreshContext(context);
// 11、应用上下文刷新后置处理
afterRefresh(context, applicationArguments);
// 13、时间信息、输出日志记录执行主类名
Duration timeTakenToStartup = Duration.ofNanos(System.nanoTime() - startTime);
if (this.logStartupInfo) {
new StartupInfoLogger(this.mainApplicationClass).logStarted(getApplicationLog(), timeTakenToStartup);
}
// 14、发布应用上下文启动完成事件
listeners.started(context, timeTakenToStartup);
// 15、执行所有 Runner 运行器
callRunners(context, applicationArguments);
} catch (Throwable ex) {
// 运行错误处理
handleRunFailure(context, ex, listeners);
throw new IllegalStateException(ex);
}
try {
// 16、发布应用上下文就绪事件(可以使用了)
Duration timeTakenToReady = Duration.ofNanos(System.nanoTime() - startTime);
listeners.ready(context, timeTakenToReady);
} catch (Throwable ex) {
// 运行错误处理
handleRunFailure(context, ex, null);
throw new IllegalStateException(ex);
}
// 17、返回应用上下文
return context;
}
本文主要讲解到Banner打印,也就是:
// 7、创建 Banner 并打印
Banner printedBanner = printBanner(environment);
此方法所在类的具体路径:org.springframework.boot.SpringApplication
// banner打印模式默认是控制台
private Banner.Mode bannerMode = Banner.Mode.CONSOLE;
private Banner printBanner(ConfigurableEnvironment environment) {
// 默认是控制台
if (this.bannerMode == Banner.Mode.OFF) {
return null;
}
// 由于此时resourceLoader 为null,所以resourceLoader 最终为DefaultResourceLoader
ResourceLoader resourceLoader = (this.resourceLoader != null) ? this.resourceLoader
: new DefaultResourceLoader(null);
// 实例化一个SpringApplicationBannerPrinter
SpringApplicationBannerPrinter bannerPrinter = new SpringApplicationBannerPrinter(resourceLoader, this.banner);
// 默认是控制台
if (this.bannerMode == Mode.LOG) {
return bannerPrinter.print(environment, this.mainApplicationClass, logger);
}
// 调用SpringApplicationBannerPrinter的打印方法
return bannerPrinter.print(environment, this.mainApplicationClass, System.out);
}
可以通过配置文件修改打印的模式(log,console,off),比如:
spring:
main:
banner-mode: log
类的具体路径:org.springframework.boot.SpringApplicationBannerPrinter
class SpringApplicationBannerPrinter {
static final String BANNER_LOCATION_PROPERTY = "spring.banner.location";
static final String BANNER_IMAGE_LOCATION_PROPERTY = "spring.banner.image.location";
static final String DEFAULT_BANNER_LOCATION = "banner.txt";
static final String[] IMAGE_EXTENSION = { "gif", "jpg", "png" };
private static final Banner DEFAULT_BANNER = new SpringBootBanner();
private final ResourceLoader resourceLoader;
private final Banner fallbackBanner;
SpringApplicationBannerPrinter(ResourceLoader resourceLoader, Banner fallbackBanner) {
this.resourceLoader = resourceLoader;
this.fallbackBanner = fallbackBanner;
}
// 打印Banner到日志文件
Banner print(Environment environment, Class<?> sourceClass, Log logger) {
// 通过环境配置获取Banner
Banner banner = getBanner(environment);
try {
// 打印Banner到日志文件
logger.info(createStringFromBanner(banner, environment, sourceClass));
} catch (UnsupportedEncodingException ex) {
logger.warn("Failed to create String for banner", ex);
}
// 返回打印对象
return new PrintedBanner(banner, sourceClass);
}
// 打印Banner到控制台
Banner print(Environment environment, Class<?> sourceClass, PrintStream out) {
// 通过环境配置获取Banner
Banner banner = getBanner(environment);
// 打印Banner到控制台
banner.printBanner(environment, sourceClass, out);
// 返回打印对象
return new PrintedBanner(banner, sourceClass);
}
// 获取banners
private Banner getBanner(Environment environment) {
// 实例化Banners,Banners是Banner的实现类
Banners banners = new Banners();
// 获取图片形式的Banner,如果不为空则加入列表
banners.addIfNotNull(getImageBanner(environment));
// 获取文本形式的Banner,如果不为空则加入列表
banners.addIfNotNull(getTextBanner(environment));
// 只要列表不为空则返回Banners
if (banners.hasAtLeastOneBanner()) {
return banners;
}
// 此处为null
if (this.fallbackBanner != null) {
return this.fallbackBanner;
}
// 默认是SpringBootBanner
return DEFAULT_BANNER;
}
// 获取文本形式的banner
private Banner getTextBanner(Environment environment) {
// 获取属性"spring.banner.location"的值,没有取到就使用默认值"banner.txt"
String location = environment.getProperty(BANNER_LOCATION_PROPERTY, DEFAULT_BANNER_LOCATION);
// 通过资源加载器去指定的路径加载资源
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) {
// Ignore
}
// 默认返回null
return null;
}
// 获取图片形式的banner
private Banner getImageBanner(Environment environment) {
// 获取属性"spring.banner.image.location"的值
String location = environment.getProperty(BANNER_IMAGE_LOCATION_PROPERTY);
// 如果配置了值
if (StringUtils.hasLength(location)) {
// 通过资源加载器加载资源
Resource resource = this.resourceLoader.getResource(location);
// 如果图片资源存在,则构建ImageBanner返回,否则返回null
return resource.exists() ? new ImageBanner(resource) : null;
}
//如果没有配置值
for (String ext : IMAGE_EXTENSION) {
// 则尝试加载"banner.gif", "banner.jpg", "banner.png"
Resource resource = this.resourceLoader.getResource("banner." + ext);
if (resource.exists()) {
// 只要加载到了,则构建ImageBanner返回
return new ImageBanner(resource);
}
}
return null;
}
private String createStringFromBanner(Banner banner, Environment environment, Class<?> mainApplicationClass)
throws UnsupportedEncodingException {
// 构建字节数组输出流
ByteArrayOutputStream baos = new ByteArrayOutputStream();
// 构建打印流
banner.printBanner(environment, mainApplicationClass, new PrintStream(baos));
// 获取"spring.banner.charset",默认编码为"UTF-8"
String charset = environment.getProperty("spring.banner.charset", "UTF-8");
// 转为字符串
return baos.toString(charset);
}
// 静态内部类Banners
private static class Banners implements Banner {
private final List<Banner> banners = new ArrayList<>();
void addIfNotNull(Banner banner) {
if (banner != null) {
this.banners.add(banner);
}
}
boolean hasAtLeastOneBanner() {
return !this.banners.isEmpty();
}
@Override
public void printBanner(Environment environment, Class<?> sourceClass, PrintStream out) {
for (Banner banner : this.banners) {
banner.printBanner(environment, sourceClass, out);
}
}
}
// 静态内部类PrintedBanner
private static class PrintedBanner implements Banner {
private final Banner banner;
private final Class<?> sourceClass;
PrintedBanner(Banner banner, Class<?> sourceClass) {
this.banner = banner;
this.sourceClass = sourceClass;
}
@Override
public void printBanner(Environment environment, Class<?> sourceClass, PrintStream out) {
sourceClass = (sourceClass != null) ? sourceClass : this.sourceClass;
this.banner.printBanner(environment, sourceClass, out);
}
}
}
ResourceBanner的打印需要处理占位符,然后转为字符串进行输出
public class ResourceBanner implements Banner {
private Resource resource;
public 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);
}
}
protected List<PropertyResolver> getPropertyResolvers(Environment environment, Class<?> sourceClass) {
List<PropertyResolver> resolvers = new ArrayList<>();
resolvers.add(environment);
resolvers.add(getVersionResolver(sourceClass));
resolvers.add(getAnsiResolver());
resolvers.add(getTitleResolver(sourceClass));
return resolvers;
}
}
ImageBanner 的打印时会先将资源转为输入流,然后再转为图片输入流,然后转为对象数组Frame[ ],Frame是 ImageBanner 静态内部类,里包含BufferedImage及delayTime,然后遍历数组进行打印。
public class ImageBanner implements Banner {
private final Resource image;
@Override
public void printBanner(Environment environment, Class<?> sourceClass, PrintStream out) {
String headless = System.getProperty("java.awt.headless");
try {
System.setProperty("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);
} finally {
if (headless == null) {
System.clearProperty("java.awt.headless");
} else {
System.setProperty("java.awt.headless", headless);
}
}
}
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[]
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());
}
}
private Frame[] readFrames(int width, int height) throws IOException {
// Java7的语法,自动关闭流
// 图片资源转为输入流
try (InputStream inputStream = this.image.getInputStream()) {
// 输入流转为图片输入流
try (ImageInputStream imageStream = ImageIO.createImageInputStream(inputStream)) {
// 根据宽高和图片输入流获取Frame[]
return readFrames(width, height, imageStream);
}
}
}
private Frame[] readFrames(int width, int height, ImageInputStream stream) throws IOException {
// 返回包含所有当前已注册 ImageReader 的 Iterator
Iterator<ImageReader> readers = ImageIO.getImageReaders(stream);
Assert.state(readers.hasNext(), "Unable to read image banner source");
ImageReader reader = readers.next();
try {
// 返回一个适合此格式的默认 ImageReadParam 对象
ImageReadParam readParam = reader.getDefaultReadParam();
// 设置指定的 ImageInputStream 输入源
reader.setInput(stream);
// 返回当前输入源中可用的图像数
int frameCount = reader.getNumImages(true);
Frame[] frames = new Frame[frameCount];
for (int i = 0; i < frameCount; i++) {
// 通过ImageReader 把图片输入流转为Frame
frames[i] = readFrame(width, height, reader, i, readParam);
}
// 返回数组对象
return frames;
} finally {
reader.dispose();
}
}
private Frame readFrame(int width, int height, ImageReader reader, int imageIndex, ImageReadParam readParam)
throws IOException {
// 使用所提供的 ImageReadParam 来读取通过索引 imageIndex 指定的对象
BufferedImage image = reader.read(imageIndex, readParam);
// 跳转图像大小
BufferedImage resized = resizeImage(image, width, height);
// 获取延迟时间
int delayTime = getDelayTime(reader, imageIndex);
// 返回Frame
return new Frame(resized, delayTime);
}
private static class Frame {
private final BufferedImage image;
private final int delayTime;
Frame(BufferedImage image, int delayTime) {
this.image = image;
this.delayTime = delayTime;
}
BufferedImage getImage() {
return this.image;
}
int getDelayTime() {
return this.delayTime;
}
}
}
有兴趣的小伙伴可以看看具体的实现,详情见代码注释
我们再配置文件中加入如下配置:
server:
port: 8080
servlet:
context-path: /springboot
spring:
main:
sources: com.alian.springboot
banner-mode: console
banner:
location: classpath:banner.txt
image:
location: classpath:csdn.jpg
也就是我们指定了图片Banner和文本Banner资源的路径,然后分别把 banner.txt 和 csdn.png 放到 classpath 下,当然这里不配置文本Banner的地址也可以,只要放了那个文本文件,因为默认会读取 classpath 下的 banner.txt 。
######## *@@@@@@@@@@ &@@@@@#@@* :@@@@@@@#@.
#@########## @@@@#@@@@@@@@# @@@@@@@@@@@#@@ @##@@@@@@@@@@#
####@# @@@@@ #@@@ @@@@@ @@@@* @@@@@
#@### @@@@#@@@@ @@@@ @@@@ #@@@ @@@@@
####@ @##@@@@#@ #@@@ @@@# #@@# @@@#8
#@#### @@@@@8 #@#* o@@@@ #@@@ @@@#
##@##########@ @#@@@@@#@@@@@@ :@@@@@@@@#@@@@ #@@# @@@@
8#########: @#@@@@@@@#@ @@@@@@@#@@o @@@@ @@@@
___ ___ ___
/ /\ ___ / /\ /__/\
/ /::\ / /\ / /::\ \ \:\
/ /:/\:\ ___ ___ / /:/ / /:/\:\ \ \:\
/ /:/~/::\ /__/\ / /\ /__/::\ / /:/~/::\ _____\__\:\
/__/:/ /:/\:\ \ \:\ / /:/ \__\/\:\__ /__/:/ /:/\:\ /__/::::::::\
\ \:\/:/__\/ \ \:\ /:/ \ \:\/\ \ \:\/:/__\/ \ \:\~~\~~\/
\ \::/ \ \:\/:/ \__\::/ \ \::/ \ \:\ ~~~
\ \:\ \ \::/ /__/:/ \ \:\ \ \:\
\ \:\ \__\/ \__\/ \ \:\ \ \:\
\__\/ \__\/ \__\/
2021-12-07 11:45:32 895 [main] INFO initialize 108:Tomcat initialized with port(s): 8080 (http)
2021-12-07 11:45:32 900 [main] INFO log 173:Initializing ProtocolHandler ["http-nio-8080"]
2021-12-07 11:45:32 900 [main] INFO log 173:Starting service [Tomcat]
2021-12-07 11:45:32 900 [main] INFO log 173:Starting Servlet engine: [Apache Tomcat/9.0.55]
2021-12-07 11:45:32 941 [main] INFO log 173:Initializing Spring embedded WebApplicationContext
2021-12-07 11:45:32 942 [main] INFO prepareWebApplicationContext 290:Root WebApplicationContext: initialization completed in 474 ms
2021-12-07 11:45:33 132 [main] INFO log 173:Starting ProtocolHandler ["http-nio-8080"]
2021-12-07 11:45:33 142 [main] INFO start 220:Tomcat started on port(s): 8080 (http) with context path '/springboot'
2021-12-07 11:45:33 157 [main] INFO logStarted 61:Started SpringbootApplication in 1.116 seconds (JVM running for 1.621)
比较常用的Banner制作网站如下: