Spring Boot 核心
1. 基本配置
1.1 入口类和 @SpringBootApplication
Spring Boot 通常有一个 *Application 的入口类,入口类有一个 main 方法,这个 main 方法其实就是一个标准的 Java 应用的入口方法。在 main 方法中使用 SpringApplication.run(xx.class, args),启动 Spring Boot 应用项目。
@SpringBootApplication 是一个核心注解,也是一个组合注解,源码如下:
@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 {
@AliasFor(
annotation = EnableAutoConfiguration.class
)
Class<?>[] exclude() default {};
@AliasFor(
annotation = EnableAutoConfiguration.class
)
String[] excludeName() default {};
@AliasFor(
annotation = ComponentScan.class,
attribute = "basePackages"
)
String[] scanBasePackages() default {};
@AliasFor(
annotation = ComponentScan.class,
attribute = "basePackageClasses"
)
Class<?>[] scanBasePackageClasses() default {};
@AliasFor(
annotation = ComponentScan.class,
attribute = "nameGenerator"
)
Class<? extends BeanNameGenerator> nameGenerator() default BeanNameGenerator.class;
@AliasFor(
annotation = Configuration.class
)
boolean proxyBeanMethods() default true;
}
@AliasFor 功能详解
@SpringBootApplication 注解主要组合了 @Configuration、@EnableAutoConfiguration、@ComponentScan;若不使用 @SpringBootApplication 注解,则可以在入口类上直接使用 @Configuration、@EnableAutoConfiguration、@ComponentScan.
其中,@EnableAutoConfiguration 让 Spring Boot 根据类路径中的 jar 包依赖为当前项目进行自动配置
例如,添加了 spring-boot-starter-web 依赖,会自动添加 Tomcat 和 Spring MVC 的依赖,那么 Spring Boot 会对 Tomcat 和 Spring MVC 进行自动配置。
Spring Boot 会自动扫描 @SpringBootApplication 所在类的 同级包 以及 下级包 里的 Bean(若为 JPA 项目还可以扫描标注类 @Entity 的实体类)。
1.2 关闭特定的自动配置
关闭特定的自动配置,可以使用 @SpringBootApplication 注解的 exclude 参数,例如:
@SpringBootApplication(exclude = {DataSourceAutoConfiguration.class})
1.3 Spring Boot 的配置文件
Spring Boot 使用一个全局的配置文件 application.properties 或 application.yml,放置在 src/main/resource 目录或者类路径的 /config 下。
Spring Boot 不仅支持常规的 properties 配置文件,还支持 yaml 语言的配置文件。yaml 是数据为中心的语言,在配置数据的时候具有面向对象的特征
Spring Boot 的全局配置文件的作用是对一些默认配置的配置值进行修改。
1.3.1 简单示例
application.properties 中添加:
server.port=9090
server.servlet.context-path=/helloboot
application.yml 中添加:
server:
port: 9090
contextPath: /helloboot
1.4 starter pom
Spring Boot 为我们提供了简化企业级开发就大多数场景的 starter pom,只要使用了应用场景所需要的 starter pom,相关的技术配置将会消除,就可以得到 Spring Boot 为我们提供的自动配置的 Bean。
1.5 使用 xml 配置
Spring Boot 提倡零配置,即无 xml 配置,但有些时候就必须要使用 xml 配置,即可以使用 Spring 提供的 @ImportResource 来加载 xml 配置,例如:
@ImportResource({"classpath:some-context.xml","classpath:another-context.xml"})
2. 外部配置
Spring Boot 允许使用 porperties文件、yaml文件 或者 命令行参数 作为外部配置。
2.1 命令行参数配置
Spring Boot 可以是基于 jar 包进行的,打包成 jar 包的程序可以直接通过以下命令运行:
java -jar xx.jar
可以通过以下命令修改 Tomcat 端口号:
java -jar xx.jar --server.port=9090
2.2 常规属性配置
在 Spring Boot 中,只需要在 application.properties 定义属性,直接使用 @Value 注入即可。
2.2.1 示例
(1) application.properties 增加属性:
book.author=john
book.name=time
(2)修改入口类
import org.springframework.beans.factory.annotation.Value;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
@RestController
@SpringBootApplication
public class Sp01Application {
@Value("${book.author}")
private String bookAuthor;
@Value("${book.name}")
private String bookName;
@RequestMapping("/")
String index() {
return "book name is: " + bookName + "and book author is: " + bookAuthor;
}
public static void main(String[] args) { //2
SpringApplication.run(Sp01Application.class, args);
}
}
2.3 类型安全的配置( 基于 properties )
上例中的 @Value 显然不适合大量数据注入的情况,所以 Spring Boot 还提供了基于类型安全的配置方式,通过 @ConfigurationProperties 将 properties 属性和一个 Bean 及其属性关联,从而实现类型安全的配置。
2.3.1 示例
(1)properties 配置
book.author=john
book.name=time
当新建了一个 properties 文件时,我们需要在 @ConfiguraitonProperties 的属性 locations 里指定 properties 的位置,且需要在入口类上配置。
(2)类型安全的 Bean ,代码如下:
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.stereotype.Component;
@Component
@ConfigurationProperties(prefix = "book") //1
public class BookSettings {
private String bookAuthor;
private String bookName;
public String getBookAuthor() {
return bookAuthor;
}
public void setBookAuthor(String bookAuthor) {
this.bookAuthor = bookAuthor;
}
public String getBookName() {
return bookName;
}
public void setBookName(String bookName) {
this.bookName = bookName;
}
}
代码解释
- 注释1:通过 @ConfigurationProperties 加载 properties 文件内的配置,通过 prefix 属性指定 properties 的配置前缀,通过 locations 指定 properties 文件的位置,例如:
@ConfigurationProperties(prefix = "book", locations = {"classpath:config/xx.properties"})
(3)检验代码:
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
@RestController
@SpringBootApplication
public class Sp01Application {
@Autowired
private BookSettings bookSettings; //1
@RequestMapping("/")
String index() {
return "book name is: " + bookSettings.getBookName() + "and book author is: " + bookSettings.getBookAuthor();
}
public static void main(String[] args) {
SpringApplication.run(Sp01Application.class, args);
}
}
代码解释
- 注释1:可以直接用 @Autowired 注入该配置。
3. 日志配置
Spring Boot 支持 Java Util Logging、Log4J、Log4J2 和 Logback 作为日志框架,无论使用哪种日志框架,Spring Boot 已为当前使用日志框架的控制台输出及文件输出做好了配置。
默认情况下,Spring Boot 使用 Logback 作为日志框架。
配置日志级别:
logging.file=D:/mylog/log.log
配置日志文件,格式为 logging.level.包名=级别:
logging.level.org.springframework.web=DEBUG
4. Profile 配置
Profile 是 Spring 用来针对不同的环境对不同的配置提供支持的,全局 Profile 配置使用 application-{profile}.properties(如 application-prod.properties)。
通过在 application.properties 中设置 spring-profiles.active=prod 来指定活动的 Profile。
下面简单做个演示,我们设生产(prod)和开发(dev)环境,生产端口号为 80,开发环境下端口号为 8888.
4.1 示例
(1)生产和开发环境配置如下:
applicaiton-prod.properties
server.port=80
application-dev.properties
server.port=8888
(2)运行。
application.properties 增加:
spring.profiles.active=dev
则显示为 8888 端口。
5. Spring Boot 运行原理
Spring Boot 是基于条件来配置 Bean 的能力
Spring Boot 关于自动配置的源码在 org.springframework.boot.autoconfigure 里面
可以通过两种方法来查看当前项目中已启用和未启用的自动配置的报告。
(1)运行 jar 时增加 --debug 参数:
java -jar xx.jar --debug
(2)在 application.properties 中设置属性:
debug=true
5.1 运作原理
关于 Spring Boot 的运作原理,还是要回归到 @SpringBootApplication 注解上来,这个注解是一个组合注解,它的核心功能是由 @EnableAutoConfiguration 注解提供的。
以下是 @EnableAutoConfiguration 注解的源码:
@Target({ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Inherited
@AutoConfigurationPackage
@Import({AutoConfigurationImportSelector.class})
public @interface EnableAutoConfiguration {
String ENABLED_OVERRIDE_PROPERTY = "spring.boot.enableautoconfiguration";
Class<?>[] exclude() default {};
String[] excludeName() default {};
}
这里的关键功能是 @Import 注解导入的配置功能,EnableAutoConfigurationImportSelector 使用 SpringFactoriesLoader.loadFactoryNames 方法来扫描具有 META-INF/spring.factories 文件的 jar 包,而我们的 spring-boot-autoconfigure-xx.jar 里就有一个 spring.factories 文件,此文件中声明了有哪些自动配置。
5.2 核心注解
打开上面任意一个 AutoConfiguration 文件,一般都有以下注解:
注解名 | 作用 |
---|---|
@ConditionalOnBean | 当容器里有指定的 Bean 的条件下 |
@ConditionalOnClass | 当类路径下有指定的类的条件下 |
@ConditionalOnExpression | 基于 SpEL 表达式作为判断条件 |
@ConditionalOnJava | 基于 JVM 版本作为判断条件 |
@ConditionalOnJndi | 在 JNDI 存在的条件下查找指定的位置 |
@ConditionalOnMissingBean | 当容器里没有指定 Bean 的情况下 |
@ConditionalOnMissingClass | 当类路径下没有指定的类的条件下 |
@ConditionalOnNotWebApplication | 当前项目不是 Web 项目的条件下 |
@ConditionalOnProperty | 指定的属性是否有指定的值 |
@ConditionalOnResource | 类路径是否有指定的值 |
@ConditionalOnSingleCandidate | 当指定 Bean 在容器中只有一个,或者虽然有多个但是指定首选的 Bean。 |
@ConditionalOnWebApplication | 当前项目是 Web 项目的条件下 |
这些注解都是组合了 @Conditional 元注解,只是使用了不同的条件(Condition)。
下面简单分析一下 @ConditionalOnWebApplication 注解。
@Target({ElementType.TYPE, ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Conditional({OnWebApplicationCondition.class})
public @interface ConditionalOnWebApplication {
ConditionalOnWebApplication.Type type() default ConditionalOnWebApplication.Type.ANY;
public static enum Type {
ANY,
SERVLET,
REACTIVE;
private Type() {
}
}
}
从源码可以看出,此注解的条件是 OnWebApplicationCondition,并分为了三种不同的类型(ANY、SERVLET、REACTIVE)下面看看它是如何构造的:
@Order(-2147483628)
class OnWebApplicationCondition extends FilteringSpringBootCondition {
private static final String SERVLET_WEB_APPLICATION_CLASS = "org.springframework.web.context.support.GenericWebApplicationContext";
private static final String REACTIVE_WEB_APPLICATION_CLASS = "org.springframework.web.reactive.HandlerResult";
OnWebApplicationCondition() {
}
protected ConditionOutcome[] getOutcomes(String[] autoConfigurationClasses, AutoConfigurationMetadata autoConfigurationMetadata) {
ConditionOutcome[] outcomes = new ConditionOutcome[autoConfigurationClasses.length];
for(int i = 0; i < outcomes.length; ++i) {
String autoConfigurationClass = autoConfigurationClasses[i];
if (autoConfigurationClass != null) {
outcomes[i] = this.getOutcome(autoConfigurationMetadata.get(autoConfigurationClass, "ConditionalOnWebApplication"));
}
}
return outcomes;
}
private ConditionOutcome getOutcome(String type) {
if (type == null) {
return null;
} else {
Builder message = ConditionMessage.forCondition(ConditionalOnWebApplication.class, new Object[0]);
if (Type.SERVLET.name().equals(type) && !ClassNameFilter.isPresent("org.springframework.web.context.support.GenericWebApplicationContext", this.getBeanClassLoader())) {
return ConditionOutcome.noMatch(message.didNotFind("servlet web application classes").atAll());
} else if (Type.REACTIVE.name().equals(type) && !ClassNameFilter.isPresent("org.springframework.web.reactive.HandlerResult", this.getBeanClassLoader())) {
return ConditionOutcome.noMatch(message.didNotFind("reactive web application classes").atAll());
} else {
return !ClassNameFilter.isPresent("org.springframework.web.context.support.GenericWebApplicationContext", this.getBeanClassLoader()) && !ClassUtils.isPresent("org.springframework.web.reactive.HandlerResult", this.getBeanClassLoader()) ? ConditionOutcome.noMatch(message.didNotFind("reactive or servlet web application classes").atAll()) : null;
}
}
}
public ConditionOutcome getMatchOutcome(ConditionContext context, AnnotatedTypeMetadata metadata) {
boolean required = metadata.isAnnotated(ConditionalOnWebApplication.class.getName());
ConditionOutcome outcome = this.isWebApplication(context, metadata, required);
if (required && !outcome.isMatch()) {
return ConditionOutcome.noMatch(outcome.getConditionMessage());
} else {
return !required && outcome.isMatch() ? ConditionOutcome.noMatch(outcome.getConditionMessage()) : ConditionOutcome.match(outcome.getConditionMessage());
}
}
private ConditionOutcome isWebApplication(ConditionContext context, AnnotatedTypeMetadata metadata, boolean required) {
switch(this.deduceType(metadata)) {
case SERVLET:
return this.isServletWebApplication(context);
case REACTIVE:
return this.isReactiveWebApplication(context);
default:
return this.isAnyWebApplication(context, required);
}
}
private ConditionOutcome isAnyWebApplication(ConditionContext context, boolean required) {
Builder message = ConditionMessage.forCondition(ConditionalOnWebApplication.class, new Object[]{required ? "(required)" : ""});
ConditionOutcome servletOutcome = this.isServletWebApplication(context);
if (servletOutcome.isMatch() && required) {
return new ConditionOutcome(servletOutcome.isMatch(), message.because(servletOutcome.getMessage()));
} else {
ConditionOutcome reactiveOutcome = this.isReactiveWebApplication(context);
return reactiveOutcome.isMatch() && required ? new ConditionOutcome(reactiveOutcome.isMatch(), message.because(reactiveOutcome.getMessage())) : new ConditionOutcome(servletOutcome.isMatch() || reactiveOutcome.isMatch(), message.because(servletOutcome.getMessage()).append("and").append(reactiveOutcome.getMessage()));
}
}
private ConditionOutcome isServletWebApplication(ConditionContext context) {
Builder message = ConditionMessage.forCondition("", new Object[0]);
if (!ClassNameFilter.isPresent("org.springframework.web.context.support.GenericWebApplicationContext", context.getClassLoader())) {
return ConditionOutcome.noMatch(message.didNotFind("servlet web application classes").atAll());
} else {
if (context.getBeanFactory() != null) {
String[] scopes = context.getBeanFactory().getRegisteredScopeNames();
if (ObjectUtils.containsElement(scopes, "session")) {
return ConditionOutcome.match(message.foundExactly("'session' scope"));
}
}
if (context.getEnvironment() instanceof ConfigurableWebEnvironment) {
return ConditionOutcome.match(message.foundExactly("ConfigurableWebEnvironment"));
} else {
return context.getResourceLoader() instanceof WebApplicationContext ? ConditionOutcome.match(message.foundExactly("WebApplicationContext")) : ConditionOutcome.noMatch(message.because("not a servlet web application"));
}
}
}
private ConditionOutcome isReactiveWebApplication(ConditionContext context) {
Builder message = ConditionMessage.forCondition("", new Object[0]);
if (!ClassNameFilter.isPresent("org.springframework.web.reactive.HandlerResult", context.getClassLoader())) {
return ConditionOutcome.noMatch(message.didNotFind("reactive web application classes").atAll());
} else if (context.getEnvironment() instanceof ConfigurableReactiveWebEnvironment) {
return ConditionOutcome.match(message.foundExactly("ConfigurableReactiveWebEnvironment"));
} else {
return context.getResourceLoader() instanceof ReactiveWebApplicationContext ? ConditionOutcome.match(message.foundExactly("ReactiveWebApplicationContext")) : ConditionOutcome.noMatch(message.because("not a reactive web application"));
}
}
private Type deduceType(AnnotatedTypeMetadata metadata) {
Map<String, Object> attributes = metadata.getAnnotationAttributes(ConditionalOnWebApplication.class.getName());
return attributes != null ? (Type)attributes.get("type") : Type.ANY;
}
}