SpringBoot——starter解析
Conditional注解解析
- 含义:基于条件的注解
- 作用:根据是否满足某个特定条件来决定是否创建某个特定的bean
- 意义:是SpringBoot实现自动配置的关键基础能力
@Conditional表示仅当所有指定条件都匹配时,组件才有资格注册 。
该@Conditional注释可以在以下任一方式使用:
- 作为任何@Bean方法的方法级注释
- 作为任何类的直接或间接注释的类型级别注释 @Component,包括@Configuration类
- 作为元注释,目的是组成自定义构造型注释
该注解主要源码之一,通过match匹配,符合条件才装载到Spring容器
@Target({ElementType.TYPE, ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface Conditional {
/**
* All {@link Condition Conditions} that must {@linkplain Condition#matches match}
* in order for the component to be registered.
*/
Class<? extends Condition>[] value();
}
@FunctionalInterface
public interface Condition {
/**
* Determine if the condition matches.
* @param context the condition context
* @param metadata metadata of the {@link org.springframework.core.type.AnnotationMetadata class}
* or {@link org.springframework.core.type.MethodMetadata method} being checked
* @return {@code true} if the condition matches and the component can be registered,
* or {@code false} to veto the annotated component's registration
*/
boolean matches(ConditionContext context, AnnotatedTypeMetadata metadata);
}
class ProfileCondition implements Condition {
@Override
public boolean matches(ConditionContext context, AnnotatedTypeMetadata metadata) {
MultiValueMap<String, Object> attrs = metadata.getAllAnnotationAttributes(Profile.class.getName());
if (attrs != null) {
for (Object value : attrs.get("value")) {
if (context.getEnvironment().acceptsProfiles(Profiles.of((String[]) value))) {
return true;
}
}
return false;
}
return true;
}
}
作用:总而言之,只有@Conditional指定的条件成立,才给容器添加组件
@Conditional派生注解:@Conditional派生了很多注解,下面给个表格列举一下派生注解的用法
@Conditional派生注解 | 作用(都是判断是否符合指定的条件) |
---|---|
@ConditionalOnJava | 系统的java版本是否符合要求 |
@ConditionalOnBean | 有指定的Bean类 |
@ConditionalOnMissingBean | 没有指定的bean类 |
@ConditionalOnExpression | 符合指定的SpEL表达式 |
@ConditionalOnClass | 有指定的类 |
@ConditionalOnMissingClass | 没有指定的类 |
@ConditionalOnSingleCandidate | 容器只有一个指定的bean,或者这个bean是首选bean |
@ConditionalOnProperty | 指定的property属性有指定的值 |
@ConditionalOnResource | 路径下存在指定的资源 |
@ConditionalOnWebApplication | 系统环境是web环境 |
@ConditionalOnNotWebApplication | 系统环境不是web环境 |
@ConditionalOnjndi | JNDI存在指定的项 |
自定义Conditional注解
- 新建MyConditionAnnotation注解,引入Condition实现类
@Target({ ElementType.TYPE, ElementType.METHOD })
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Conditional(MyCondition.class)
public @interface MyConditionAnnotation {
String[] value() default {};
}
- 新建MyCondition实现Condition,重写matches方法,符合条件返回true
public class MyCondition implements Condition {
@Override
public boolean matches(ConditionContext context, AnnotatedTypeMetadata metadata) {
String[] properties = (String[])metadata
.getAnnotationAttributes("com.yibo.source.code.condi.MyConditionAnnotation")
.get("value");
for (String property : properties) {
if(StringUtils.isEmpty(context.getEnvironment().getProperty(property))){
return false;
}
}
return true;
}
}
- 新建A类使用@MyConditionAnnotation注解
@Component
@MyConditionAnnotation({"com.yibo.condition1","com.yibo.condition2"})
public class A {
}
- 在application.properties中添加属性
com.yibo.condition1=test1
com.yibo.condition2=test2
- 在测试类中测试
@RunWith(SpringRunner.class)
@SpringBootTest(classes = Application.class)
public class ApplicationTest implements ApplicationContextAware {
@Autowired
private ApplicationContext applicationContext;
@Override
public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
this.applicationContext = applicationContext;
}
@Test
public void test2(){
System.out.println(applicationContext.getBean(A.class));
}
}
动手搭建starter
starter简介
SpringBoot可以省略SpringFramework众多的繁琐配置,它的众多starter可以说是功不可没。
springboot非常的流行,就是因为starter的存在,starter是springboot的核心,可以理解成可插拔的插件,你想要什么插件配置什么插件就可以,比如我想要使用mybatis,那么配置starter-mybatis就可以。但是有人会说我用mybatis自己导入jar不行吗???实际上starter和jar区别在于,它能够自己实现配置,这样大大提高了开发效率,使得使用spring开发变得非常简单方便。
- starter类似于SpringBoot的可插拔插件
- 与jar包区别:starter能实现自动配置
- 作用:大幅提升开发效率
常用starter
名称 | 描述 |
---|---|
spring-boot-starter-thymeleaf | 使MVC Web applications 支持Thymeleaf |
spring-boot-starter-mail | 使用Java Mail、Spring email发送支持 |
spring-boot-starter-data-redis | 通过Spring Data Redis 、Jedis client使用Redis键值存储数据库 |
spring-boot-starter-web | 构建Web,包含RESTful风格框架SpringMVC和默认的嵌入式容器Tomcat |
spring-boot-starter-activemq | 为JMS使用Apache ActiveMQ |
spring-boot-starter-data-elasticsearch | 使用Elasticsearch、analytics engine、Spring Data Elasticsearch |
spring-boot-starter-aop | 通过Spring AOP、AspectJ面向切面编程 |
spring-boot-starter-security | 使用 Spring Security |
spring-boot-starter-data-jpa | 通过 Hibernate 使用 Spring Data JPA |
spring-boot-starter | Core starter,包括 自动配置支持、 logging and YAML |
spring-boot-starter-freemarker | 使MVC Web applications 支持 FreeMarker |
spring-boot-starter-batch | 使用Spring Batch |
spring-boot-starter-data-solr | 通过 Spring Data Solr 使用 Apache Solr |
spring-boot-starter-data-mongodb | 使用 MongoDB 文件存储数据库、Spring Data MongoDB |
手写starter
新建一个starter
首先我们在IDEA中通过Spring Initializer新建一个weather-starter的SpringBoot项目,然后我们可以在其他SpringBoot项目中作为依赖引入.
- 1、修改weather-starter的pom文件,命名合理一点
<!--修改weather-starter的pom文件,命名合理一点-->
<groupId>com.yibo</groupId>
<artifactId>weather-spring-boot-starter</artifactId>
<version>0.0.1-SNAPSHOT</version>
<properties>
<java.version>1.8</java.version>
</properties>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter</artifactId>
</dependency>
<!--pom文件要加入-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-autoconfigure</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
- 2、新建一个类作为属性源,读取配置
@ConfigurationProperties(prefix = "weather")
public class WeatherSource {
private String type;
private String rate;
public String getType() {
return type;
}
public void setType(String type) {
this.type = type;
}
public String getRate() {
return rate;
}
public void setRate(String rate) {
this.rate = rate;
}
}
- 3、新建一个AutoConfiguration自动配置类,用来让SpringBoot依据它自动配置
@Configuration
@EnableConfigurationProperties(WeatherSource.class)
//自动注入的条件
@ConditionalOnProperty(name="weather.enable",havingValue = "enable")
public class WeatherAutoConfiguration {
@Autowired
private WeatherSource weatherSource;
@Bean
@ConditionalOnMissingBean(WeatherService.class)
public WeatherService weatherService(){
return new WeatherService(weatherSource);
}
}
- 4、写一个类用来对外提供服务
public class WeatherService {
private WeatherSource weatherSource;
public WeatherService(WeatherSource weatherSource) {
this.weatherSource = weatherSource;
}
public String getType(){
return weatherSource.getType();
}
public String getRate(){
return weatherSource.getRate();
}
}
- 5、利用SPI机制,在resources目录下新建一个META-INF目录,新建一个spring.factories文件,内容如下:
org.springframework.boot.autoconfigure.EnableAutoConfiguration=com.example.weather.WeatherAutoConfiguration
- 6、一切完毕之后,需要将我们的starter打包发布,让别人使用,本地使用的话可以通过maven helper插件install到本地仓库,也可以直接使用命令行打包
使用Starter
- 1、在pom文件中引入依赖
<dependency>
<groupId>com.example</groupId>
<artifactId>weather-spring-boot-starter</artifactId>
<version>0.0.1-SNAPSHOT</version>
</dependency>
- 2、在application.properties中编写配置,用来配置自动配置类
weather.type=rain
weather.rate=serious
weather.enable=enable
- 3、可以直接注入自动配置类中提供的服务
@RestController
@RequestMapping("/demo")
public class DemoController {
@Autowired
private WeatherService weatherService;
@GetMapping("/weather")
public String test(){
return weatherService.getType() + ", " + weatherService.getRate();
}
}
starter原理解析
spring-boot启动的时候会找到starter jar包中的resources/META-INF/spring.factories文件,根据spring.factories文件中的配置,找到需要自动配置的类
字段配置的原理就是这边,在之前的SpringBoot--配置类解析已经分析过了,它会将容器中配置的自动配置类加载进来,见下图,
// 这就引入了我们自己的WeatherAutoConfigration
public class AutoConfigurationImportSelector implements DeferredImportSelector, BeanClassLoaderAware,
ResourceLoaderAware, BeanFactoryAware, EnvironmentAware, Ordered {
protected AutoConfigurationEntry getAutoConfigurationEntry(AutoConfigurationMetadata autoConfigurationMetadata,
AnnotationMetadata annotationMetadata) {
if (!isEnabled(annotationMetadata)) {
return EMPTY_ENTRY;
}
AnnotationAttributes attributes = getAttributes(annotationMetadata);
List<String> configurations = getCandidateConfigurations(annotationMetadata, attributes);
configurations = removeDuplicates(configurations);
Set<String> exclusions = getExclusions(annotationMetadata, attributes);
checkExcludedClasses(configurations, exclusions);
configurations.removeAll(exclusions);
configurations = filter(configurations, autoConfigurationMetadata);
fireAutoConfigurationImportEvents(configurations, exclusions);
return new AutoConfigurationEntry(configurations, exclusions);
}
}
starter自动配置类导入
starter过滤
我们发现WeatherAutoConfiguration自动配置类上面使用的是@ConditionalOnProperty注解,点进去看它用来判断的类是OnPropertyCondition,而OnPropertyCondition实现了SpringBootCondition接口,那么我们先看SpringBootCondition中的matches方法
@Configuration
@EnableConfigurationProperties({WeatherSource.class})
@ConditionalOnProperty(
name = {"weather.enable"},
havingValue = "enable"
)
public class WeatherAutoConfiguration {}
@Retention(RetentionPolicy.RUNTIME)
@Target({ ElementType.TYPE, ElementType.METHOD })
@Documented
@Conditional(OnPropertyCondition.class)
public @interface ConditionalOnProperty {
@Order(Ordered.HIGHEST_PRECEDENCE + 40)
class OnPropertyCondition extends SpringBootCondition {}
public abstract class SpringBootCondition implements Condition {
private final Log logger = LogFactory.getLog(getClass());
@Override
public final boolean matches(ConditionContext context, AnnotatedTypeMetadata metadata) {
String classOrMethodName = getClassOrMethodName(metadata);
try {
// 这边调用子类实现的getMatchOutcome方法得到ConditionOutcome
// 看了下ConditionOutcome,它只是封装了判断结果和分析,所以我们直接看OnPropertyCondition的getMatchOutCome方法
ConditionOutcome outcome = getMatchOutcome(context, metadata);
logOutcome(classOrMethodName, outcome);
recordEvaluation(context, classOrMethodName, outcome);
return outcome.isMatch();
}
catch (NoClassDefFoundError ex) {
throw new IllegalStateException("Could not evaluate condition on " + classOrMethodName + " due to "
+ ex.getMessage() + " not " + "found. Make sure your own configuration does not rely on "
+ "that class. This can also happen if you are "
+ "@ComponentScanning a springframework package (e.g. if you "
+ "put a @ComponentScan in the default package by mistake)", ex);
}
catch (RuntimeException ex) {
throw new IllegalStateException("Error processing condition on " + getName(metadata), ex);
}
}
}
public class ConditionOutcome {
private final boolean match;
private final ConditionMessage message;
}
接下来我们看OnPropertyCondition的getMatchOutcome方法
@Override
public ConditionOutcome getMatchOutcome(ConditionContext context, AnnotatedTypeMetadata metadata) {
// 首先获取@ConditionalOnProperty注解上的属性,如下图
List<AnnotationAttributes> allAnnotationAttributes = annotationAttributesFromMultiValueMap(
metadata.getAllAnnotationAttributes(ConditionalOnProperty.class.getName()));
// 构造noMatch、match集合,noMatch表示那个注解的属性没有满足
List<ConditionMessage> noMatch = new ArrayList<>();
List<ConditionMessage> match = new ArrayList<>();
// 依次遍历这些注解
for (AnnotationAttributes annotationAttributes : allAnnotationAttributes) {
// 对每个注解,遍历其属性判断符不符合,返回outcome结果,下面我们也是重点看这个方法
ConditionOutcome outcome = determineOutcome(annotationAttributes, context.getEnvironment());
(outcome.isMatch() ? match : noMatch).add(outcome.getConditionMessage());
}
if (!noMatch.isEmpty()) {
return ConditionOutcome.noMatch(ConditionMessage.of(noMatch));
}
return ConditionOutcome.match(ConditionMessage.of(match));
}
继续跟进determineOutcome(annotationAttributes, context.getEnvironment())方法
private ConditionOutcome determineOutcome(AnnotationAttributes annotationAttributes, PropertyResolver resolver) {
// 获取Spec,里面就是获取注解的属性封装一下,见下图
Spec spec = new Spec(annotationAttributes);
// 两个集合,一个存放缺失的属性,一个存放不匹配的属性
List<String> missingProperties = new ArrayList<>();
List<String> nonMatchingProperties = new ArrayList<>();
// 遍历这些属性,判断符不符合,放入集合中
spec.collectProperties(resolver, missingProperties, nonMatchingProperties);
// 根据结果返回
if (!missingProperties.isEmpty()) {
return ConditionOutcome.noMatch(ConditionMessage.forCondition(ConditionalOnProperty.class, spec)
.didNotFind("property", "properties").items(Style.QUOTE, missingProperties));
}
if (!nonMatchingProperties.isEmpty()) {
return ConditionOutcome.noMatch(ConditionMessage.forCondition(ConditionalOnProperty.class, spec)
.found("different value in property", "different value in properties")
.items(Style.QUOTE, nonMatchingProperties));
}
return ConditionOutcome
.match(ConditionMessage.forCondition(ConditionalOnProperty.class, spec).because("matched"));
}
Spec(AnnotationAttributes annotationAttributes) {
String prefix = annotationAttributes.getString("prefix").trim();
if (StringUtils.hasText(prefix) && !prefix.endsWith(".")) {
prefix = prefix + ".";
}
this.prefix = prefix;
this.havingValue = annotationAttributes.getString("havingValue");
this.names = getNames(annotationAttributes);
this.matchIfMissing = annotationAttributes.getBoolean("matchIfMissing");
}
private void collectProperties(PropertyResolver resolver, List<String> missing, List<String> nonMatching) {
// 依次遍历属性
for (String name : this.names) {
String key = this.prefix + name;
// 从上图中可以看出,resovler其实就是环境
if (resolver.containsProperty(key)) {
if (!isMatch(resolver.getProperty(key), this.havingValue)) {
nonMatching.add(name);
}
}
else {
if (!this.matchIfMissing) {
missing.add(name);
}
}
}
}
private boolean isMatch(String value, String requiredValue) {
if (StringUtils.hasLength(requiredValue)) {
return requiredValue.equalsIgnoreCase(value);
}
return !"false".equalsIgnoreCase(value);
}