持续创作,加速成长!这是我参与「掘金日新计划 · 10 月更文挑战」的第 01 天,点击查看活动详情
1、SpringApplication 与 Spring 容器
再给大家详细讲讲「主启动类」中的一些细节。
先来看 SpringApplication,它是 Spring Boot 提供的一个工具类,提供的 run() 方法是用来启动 Spring 容器,运行 Spring Boot 应用的。
1.1 配置形式:类配置、XML 配置
在 Spring Boot 中,它推荐使用 Java 配置类来作为配置文件,也就是使用了 @Configuration 注解的类。
不过与使用 XML 文件作为配置文件,在本质上没有太多区别。
当你使用了 @Configuration 注解,被修饰的类,将作为主配置源。
比如,在主启动类中,我们点进 @SpringBootApplication 注解,可以看到它使用了 @SpringBootConfiguration 注解:
@Target({ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Inherited
@SpringBootConfiguration
@EnableAutoConfiguration
@ComponentScan(
excludeFilters = {@Filter(
type = FilterType.CUSTOM,
classes = {TypeExcludeFilter.class}
), @Filter(
type = FilterType.CUSTOM,
classes = {AutoConfigurationExcludeFilter.class}
)}
)
public @interface SpringBootApplication {
……
}
再点进来,你可以看到 @SpringBootConfiguration 使用了 @Configuration:
@Target({ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Configuration
@Indexed
public @interface SpringBootConfiguration {
……
}
你看,除了去加载主配置源中所有的配置,Spring Boot 还会用 @ComponentScan 注解自动扫描主配置源所在的包,以及它子包下所有带 @Component 注解(包括了 @Configuration、@Controller、@Service、@Repository 等注解)的配置类或 Bean 组件。
Spring Boot 在加载其他配置类、扫描其他包下的配置类或 Bean 组件,还可以用 @Import 注解,它能显示指定 Spring Boot 要加载的配置类:
// 加载指定包下的 MyConfig 类作为配置类
@Import(itman.mall.CommonConfig.class)
还有一种情况,如果项目非要让你使用 XML 配置文件,那你可以用 @ImportResource 注解来导入 XML 文件。
// 加载类路径下的 spring.xml 文件作为配置文件
@ImportResource("classpath:spring.xml")
案例代码,如下:
// 额外指定这俩包,还有子包下所有的配置类和 Bean 组件
@SpringBootApplication(scanBasePackages = {"itman.boot", "itman.boot2"})
// 加载类路径下的 spring.xml 文件作为配置文件
@ImportResource("classpath:spring.xml")
// 加载指定包下的 CommonConfig 类作为配置类
@Import(itman.mall.CommonConfig.class)
public class SpBootApplication {
public static void main(String[] args) {
SpringApplication.run(SpBootApplication.class, args);
}
}
接着上面的代码,我们继续深入看一个东西 —— @SpringBootApplication 中的 scanBasePackages 属性。
在 @SpringBootApplication 注解的源码中,你往下找,就可以看到一段代码:
@AliasFor(
annotation = ComponentScan.class,
attribute = "basePackages"
)
String[] scanBasePackages() default {};
你看,@SpringBootApplication 中的 scanBasePackages 属性,其实就是 @ComponentScan 的 basePackages 属性,通过这属性,就可以显示指定 Spring Boot 扫描指定包及其子包下所有的配置类和 Bean 组件。
如果你不为 @SpringBootApplication 指定 scanBasePackages 属性,则它会默认加载主配置类,也就是 @SpringBootApplication 所修饰的类,所在的包及其子包下所有的配置类和 Bean 组件。
当在指定的包中,扫描到被 @Component 修饰的类,则会被扫描、配置成 Spring 容器中的 Bean。
案例代码,如下:
@Component
public class User{
public String sayHello(){
return "hello...";
}
}
当我们使用 @ImportResource("classpath:spring.xml"),其实就是去加载一个 Spring 框架传统的配置文件。
案例代码,如下:
<?xml version="1.0" encoding="UTF-8"?>
<beans
xmlns="http://www.springframework.org/schema/beans"
xmlns:context="http://www.springframework.org/schema/context"
xmlns:tx="http://www.springframework.org/schema/tx"
xmlns:aop="http://www.springframework.org/schema/aop"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://www.springframework.org/schema/beans
http://www.springframework.org/schema/beans/spring-beans.xsd
http://www.springframework.org/schema/context
http://www.springframework.org/schema/context/spring-context.xsd
http://www.springframework.org/schema/tx
http://www.springframework.org/schema/tx/spring-tx.xsd
http://www.springframework.org/schema/aop
http://www.springframework.org/schema/aop/spring-aop.xsd">
<!-- 配置 Bean 组件 -->
<bean id="dept" class="itman.boot.po.Dept"/>
</beans>
@Import(itman.mall.CommonConfig.class),指定了 CommonConfig 作为额外的配置类。
案例代码,如下:
@Configuration
public class CommonConfig {
@Bean
public DateFormat dateFormat(){
return DateFormat.getDateInstance();
}
}
最后,咱们可以提供一个 HelloController 控制器类,然后 Spring 容器会将上面这几种不同方式配置的 Bean,注入进来。
案例代码,如下:
@RestController
public class HelloController {
@Autowired
private User user;
@Autowired
private Dept dept;
@Autowired
private DateFormat dateFormat;
@RequestMapping("/")
public String test(){
return user.sayHello() + "--" + dept + "--" + dateFormat.format(new Date());
}
}
最后,先去运行 SpBootApplication 主启动类,启动 Spring Boot 应用,在浏览器中访问 http://localhost:8080 就可以测试上面的 test() 方法了。
观察,上面的三个实例变量是否能够实现依赖注入,这也是我们的三种不同配置方式,通过这个案例我们理解得更清楚了。
1.2 启动日志和失败分析器
我们通过 SpringApplication 的 run() 方法来启动应用时,默认会显示 INFO 级别的日志消息,包括一些与启动相关的信息。
如果你想关闭的话,可以把如下属性设为 false 即可,也会同时关闭应用程序的活动 Profile 的日志:
spring.main.log-startup-info = false
在我们程序启动的过程中,有时会碰到启动失败的情况,一般在控制台上可以看到相关的错误信息,这都是 Spring Boot 的失败分析器(Failure Analyzer) 所提供的,它会给出详细的错误信息和修复建议。
另外,我们还可以通过开启 debug 属性,或者将 ConditionEvaluationReportLoggingListener 的日志级别设为 DEBUG 即可实现。
如果是通过 JAR 包的方式来运行 Spring Boot 应用,那么就通过 --debug 来开启 debug 属性。
java -jar firstboot-0.0.1-SNAPSHOT.jar --debug
或者在 application.properties 文件中添加下面这个配置信息,它能将 org.springframework.boot.autoconfigure.logging 包下所有类的日志级别都设为 DEBUG,上面说的 ConditionEvaluationReportLoggingListener 监听器就在这个包位置下。
logging.level.org.springframework.boot.autoconfigure.logging = debug
像这种失败分析器,我们也可以自定义的。
自定义失败分析器需要在 META-INF/spring.factories 文件中注册,首先在项目的 resources 目录下创建 META-INF 文件夹(注意大小写和短横线),然后在 META-INF 文件夹内创建 spring.factories 文件。
该文件的内容如下:
org.springframework.boot.diagnostics.FailureAnalyzer=\
itman.boot.HelloAnalyzer
自定义的失败分析器,应继承 AbstractFailureAnalyzer﹤T﹥,该基类中的泛型 T 代表该失败分析器要处理的异常。
接着,实现它的 analyze() 抽象方法,该方法返回的 FailureAnalysis 代表了对该异常的分析结果。
假如,你不想让该失败分析器分析该异常,而是希望将该异常留给下一个分析器去分析,那就让该方法返回 null。
public class HelloAnalyzer extends AbstractFailureAnalyzer<BindException>{
public FailureAnalysis analyze(Throwable rootFailure, BindException cause){
cause.printStackTrace();
return new FailureAnalysis("注意,你的程序绑定的端口,被占用了:"
+ cause.getMessage(),
"先找出、停止占用 8080 端口的程序,然后再启动本应用"
+ cause);
}
}
analyze() 方法返回了一个 FailureAnalysis 对象,表明该失败分析器会对 BindException 进行分析。本质上,FailureAnalysis 就是包装这三个信息:
- description:失败的描述信息,第一个构造参数;
- action:对该失败的修复建议。第二个构造参数;
- cause:导致失败的异常。第三个构造参数。
1.3 延迟初始化
我们使用的是 ApplicationContext 作为 Spring 容器,Spring Boot 默认会对容器中所有的 singleton Bean 做预初始化。
但在某些特殊情况下,如何取消预初始化,改为延迟初始化呢?有三种方式:
-
调用 SpringApplicationBuilder 对象的 lazyInitialization(true) 方法;
-
调用 SpringApplication 对象的 setLazyInitialization(true) 方法;
-
在 application.properties 文件中添加如下配置:
spring.main.lazy-initialization=true
延迟初始化,是可以等到程序需要调用 Bean 的方法时才执行初始化,因此可降低 Spring Boot 应用的启动时间。但,也是有一定缺点的:
- 特别是在 Web 应用中,很多与 Web 相关的 Bean 要等到 HTTP 请求第一次到来时才会初始化,如果延迟初始化则会降低第一次处理 HTTP 请求的响应效率;
- Bean 错误被延迟发现;
- 运行过程中的内存紧张。
1.4 自定义 Banner
Spring Boot 应用启动后,在控制台上可以看到 Spring 的 Banner,如果你想关掉它,可以在 application.properties 文件中,设置 spring.main.banner-mode 属性为 off 即可:
spring.main.banner-mode=off
spring.main.banner-mode 属性有三个属性值:
- console:在控制台中输出 Banner;
- log:在日志文件中输出 Banner;
- off:彻底关闭 Banner。
如果你想自定义 Banner 的话,可以在类加载路径下,添加一个 banner.txt 文件,或者设置 spring.banner.location 来指定 Banner 文件的位置。
默认情况下,Spring Boot 基于 UTF-8 字符集的方式,来读取 Banner 文件中的内容,如果想改其他字符集,可以在文件中设置:
spring.banner.charset=GBK
推荐一些能自动生成 banner 字符画的网站:
Spring Boot 也可以使用图片文件来添加 Banner,只要在类加载路径下添加一个 banner.gif、banner.jpg 或 banner.png 等图片文件。
也可以通过 spring.banner.image.location 属性设置图片 Banner 的加载路径,Spring Boot就会自动把该图片转换为字符画(ASCII art)形式,并作为应用程序的 Banner 显示。
如果图片和文本类型的 Banner 同时存在,会先显示图片类型的 Banner 所对应的字符画,再显示文本类型的 Banner 的内容。
在 application.properties 文件中,添加一些相关的配置:
spring.banner.image.height = 20
spring.banner.image.width = 60
# 设置字符串的色度
spring.banner.image.bitdepth = 4
1.5 设置 SpringApplication 与流式 API
我们知道,在主启动类中,直接调用 SpringApplication 的 run() 方法可以运行 Spring Boot 应用,这是创建 SpringAoolication 时自动采用的默认配置。
但如果你想自定义设置 SpringApplication 的话,你就需要先定义 SpringApplication 对象,然后比如你想延迟初始化,可以调用 setLazyInitlization(true),如果你想设置 Banner,则可以调用 setBanner(),最后再调用该对象的 run() 方法来启动。
案例代码,如下:
@SpringBootApplication
public class SpBootApplication {
public static void main(String[] args) {
// 1、创建 SpringApplication 对象
var app = new SpringApplication(SpBootApplication.class);
// 2、设置延迟初始化
app.setLazyInitlization(true);
// 3、设置 Banner
Banner banner = 构造你的 Banner
app.setBanner(banner)
// 4、启动
app.run(args);
}
}
除了直接调用构造器来创建 SpringApplication 对象,Spring Boot 还提供了 SpringApplicationBuilder 工具类,通过该工具类能以流式 API 创建 SpringApplication,并启动 Spring Boot 应用。
SpringApplicationBuilder 主要提供了几个方法来加载配置文件,而且可以构建 ApplicationContext 的层次结构(让 Spring 容器具有父子关系):
- sources(Class﹤?﹥...sources):为应用添加配置类;
- child(Class﹤?﹥...sources):为当前容器添加子容器配置类;
- parent(Class﹤?﹥...sources):为当前容器添加父容器配置类。
@SpringBootApplication
public class SpBootApplication {
public static void main(String[] args) {
new SpringAppliationBuilder()
.sources(Parent.class) // 加载父容器对应的配置类
.child(SpBootApplication.class) // SpBootApplication 类对应的配置作为子容器
.setLazyInitlization(true) // 设置延迟初始化
.bannerMode(Banner.Mode.OFF) // 关掉 Banner
.run(args); // 启动
}
}
其实,让多个 Spring 容器具有层次结构具有很多好处。
比如,由于子容器是由父容器负责加载的,因此子容器中的Bean可访问父容器中的 Bean,但父容器中的 Bean 不能访问子容器中的 Bean。