最近看到各类框架每次加载都会打印各类各式各样的banner,发现打印banner其实很简单,想着Spring boot其实也可以定义banner,从源码的角度发现banner其实很简单。
构建一个Spring boot demo
org.springframework.boot
spring-boot-starter-web
2.3.5.RELEASE
org.springframework.boot
spring-boot-starter-test
2.3.5.RELEASE
test
加入Main类
@SpringBootApplication
public class BootMain {
public static void main(String[] args) {
SpringApplication.run(BootMain.class, args);
}
}
banner的打印在Spring boot的run的过程中,在Spring容器创建之前打印的banner。
public ConfigurableApplicationContext run(String... args) {
StopWatch stopWatch = new StopWatch();
stopWatch.start();
ConfigurableApplicationContext context = null;
Collection exceptionReporters = new ArrayList<>();
configureHeadlessProperty();
SpringApplicationRunListeners listeners = getRunListeners(args);
listeners.starting();
try {
ApplicationArguments applicationArguments = new DefaultApplicationArguments(args);
ConfigurableEnvironment environment = prepareEnvironment(listeners, applicationArguments);
configureIgnoreBeanInfo(environment);
//可以看到是在Spring的Context创建之前
Banner printedBanner = printBanner(environment);
context = createApplicationContext();
exceptionReporters = getSpringFactoriesInstances(SpringBootExceptionReporter.class,
new Class[] { ConfigurableApplicationContext.class }, context);
prepareContext(context, environment, listeners, applicationArguments, printedBanner);
refreshContext(context);
afterRefresh(context, applicationArguments);
stopWatch.stop();
if (this.logStartupInfo) {
new StartupInfoLogger(this.mainApplicationClass).logStarted(getApplicationLog(), stopWatch);
}
listeners.started(context);
callRunners(context, applicationArguments);
}
catch (Throwable ex) {
handleRunFailure(context, ex, exceptionReporters, listeners);
throw new IllegalStateException(ex);
}
try {
listeners.running(context);
}
catch (Throwable ex) {
handleRunFailure(context, ex, exceptionReporters, null);
throw new IllegalStateException(ex);
}
return context;
}
进一步跟踪
private Banner printBanner(ConfigurableEnvironment environment) {
//这里是关闭banner,
if (this.bannerMode == Banner.Mode.OFF) {
return null;
}
//资源加载器
ResourceLoader resourceLoader = (this.resourceLoader != null) ? this.resourceLoader
: new DefaultResourceLoader(null);
//banner打印器
SpringApplicationBannerPrinter bannerPrinter = new SpringApplicationBannerPrinter(resourceLoader, this.banner);
//日志模式打印
if (this.bannerMode == Mode.LOG) {
return bannerPrinter.print(environment, this.mainApplicationClass, logger);
}
//console模式打印
return bannerPrinter.print(environment, this.mainApplicationClass, System.out);
}
首先是可以关闭banner的,在org.springframework.boot.SpringApplication的属性中,默认是CONSOLE,表示是console打印banner,可以通过set方法设置OFF关闭
private Banner.Mode bannerMode = Banner.Mode.CONSOLE;
有3种模式
enum Mode {
/**
* Disable printing of the banner.
*/
OFF,
/**
* Print the banner to System.out.
*/
CONSOLE,
/**
* Print the banner to the log file.
*/
LOG
}
接着定义了资源加载器,定义了banner打印器,关键是最后一步打印,可以看出只是打印的对象不一样,一个日志输出,一个System.out
Banner print(Environment environment, Class> sourceClass, PrintStream out) {
//通过环境获取banner
Banner banner = getBanner(environment);
//打印
banner.printBanner(environment, sourceClass, out);
return new PrintedBanner(banner, sourceClass);
}
首先拿到banner
getBanner(environment)
环境信息,里面存储了Spring存储的properties yaml等propertySource,还有解析占位符的解析器
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;
}
可以看到Banners,说明可以连续打印很多banner
private static class Banners implements Banner {
private final List 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);
}
}
}
banners.addIfNotNull(getImageBanner(environment));
banners.addIfNotNull(getTextBanner(environment));
这个表示Spring boot希望我们自定义Spring boot的banner的方式,两者大同小异
static final String BANNER_IMAGE_LOCATION_PROPERTY = "spring.banner.image.location";
static final String[] IMAGE_EXTENSION = { "gif", "jpg", "png" };
private Banner getImageBanner(Environment environment) {
//Spring配置文件获取,看看自定义没有
String location = environment.getProperty(BANNER_IMAGE_LOCATION_PROPERTY);
if (StringUtils.hasLength(location)) {
//如果自定义,就读取资源
Resource resource = this.resourceLoader.getResource(location);
return resource.exists() ? new ImageBanner(resource) : null;
}
//通过后缀获取banner.jpg之类的;默认从classpath获取,不过文件名被定义了
for (String ext : IMAGE_EXTENSION) {
Resource resource = this.resourceLoader.getResource("banner." + ext);
if (resource.exists()) {
return new ImageBanner(resource);
}
}
return null;
}
getSource,前面默认的
new DefaultResourceLoader(null)
public Resource getResource(String location) {
Assert.notNull(location, "Location must not be null");
for (ProtocolResolver protocolResolver : getProtocolResolvers()) {
Resource resource = protocolResolver.resolve(location, this);
if (resource != null) {
return resource;
}
}
if (location.startsWith("/")) {
return getResourceByPath(location);
}
else if (location.startsWith(CLASSPATH_URL_PREFIX)) {
return new ClassPathResource(location.substring(CLASSPATH_URL_PREFIX.length()), getClassLoader());
}
else {
try {
// Try to parse the location as a URL...
URL url = new URL(location);
return (ResourceUtils.isFileURL(url) ? new FileUrlResource(url) : new UrlResource(url));
}
catch (MalformedURLException ex) {
// No URL -> resolve as resource path.
return getResourceByPath(location);
}
}
}
protected Resource getResourceByPath(String path) {
return new ClassPathContextResource(path, getClassLoader());
}
与图像方式类似,可以配置,也可以在classpath默认banner.txt
static final String BANNER_LOCATION_PROPERTY = "spring.banner.location";
static final String DEFAULT_BANNER_LOCATION = "banner.txt";
private Banner getTextBanner(Environment environment) {
String location = environment.getProperty(BANNER_LOCATION_PROPERTY, DEFAULT_BANNER_LOCATION);
Resource resource = this.resourceLoader.getResource(location);
if (resource.exists()) {
return new ResourceBanner(resource);
}
return null;
}
Spring还默认了banner,我们平时绝大部分时间都是使用默认值
private static final Banner DEFAULT_BANNER = new SpringBootBanner();
banner.printBanner(environment, sourceClass, out);
打印banner,可以看到其实就是流向外输出到console,日志文件的过程,console还可以定义文本颜色。格式化了SPRING_BOOT与版本号
class SpringBootBanner implements Banner {
private static final String[] BANNER = { "", " . ____ _ __ _ _",
" /\\\\ / ___'_ __ _ _(_)_ __ __ _ \\ \\ \\ \\", "( ( )\\___ | '_ | '_| | '_ \\/ _` | \\ \\ \\ \\",
" \\\\/ ___)| |_)| | | | | || (_| | ) ) ) )", " ' |____| .__|_| |_|_| |_\\__, | / / / /",
" =========|_|==============|___/=/_/_/_/" };
private static final String SPRING_BOOT = " :: Spring Boot :: ";
private static final int STRAP_LINE_SIZE = 42;
@Override
public 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();
}
}
其中图像banner最复杂,需要读取图片,渲染图片
至此Spring boot打印banner的过程就结束了。过程就是读取文本或者图片,通过流写在console或者log文件,贼简单。
自定义Spring boot的banner其实就是选择一个banner.txt或者banner.jpg放在classpath中。或者通过配置自定义
static final String BANNER_LOCATION_PROPERTY = "spring.banner.location"; static final String BANNER_IMAGE_LOCATION_PROPERTY = "spring.banner.image.location";
笔者自定义一个banner
看到没有,就不会显示默认的了
banner是可以多个同时打印的,这里图像banner要注意,这个算法其实比较难写,如果要使用就直接扣代码吧,需要把图像读取转文本。可能对于做图像相关的工程师比较容易写算法
知道了banner的原理,我们可以在任意类打印banner,写一个static静态代码块,读取文件,然后流输出即可。甚至可以直接扣取Spring的打印图片与文本的banner代码,加载我们自己定义的文件与图片,调用print方法即可打印,非常简单。
ImageBanner、ResourceBanner需要Spring的这2个类,占位符可以通过Spring的环境信息格式化
Spring boot打印banner的原理非常简单,我们可以自定义。而且很多第三方插件也喜欢打印banner,基本上可以直接复用Spring boot打印banner的源码,加载我们自己写的txt或者图片,在类加载,或者方法start的时候打印即可。