自动装配
自动装配可以说是 SpringBoot 的一个核心,它的基础就是 Spring4.0 增加的条件化配置。涉及到的注解有:
@Condition
@Enable
@Import
@EnableAutoConfigure
…
其实说白了就是在 SpringBoot 启动过程中,根据预先定义好的条件,判断是否可以创建相应的 Bean,那么如果要完成自动装配的功能,就需要有这么几步
定义自动装配类 ,用于创建 Bean
配置自定义装配
使用自定义装配类
我们以 redisson-spring-boot-starter 为例,查看如何使用
定义自动装配类
我们在项目中引入 redisson-spring-boot-starter , 对于 starter 的命名有以下约定:
官方包命名一般是:spring-boot-starter-xxx,如 spring-boot-starter-data-redis
第三方包命令一般是: xxx-spring-boot-starter,如 mybatis-plus-boot-starter,pagehelper-spring-boot-starter
在其中我们可以看到有两个类: RedissonAutoConfiguration 与 RedissonProperties,其中 RedissonAutoConfiguration 中的代码如下:
@Configuration
@ConditionalOnClass({Redisson.class, RedisOperations.class})
@AutoConfigureBefore(RedisAutoConfiguration.class)
@EnableConfigurationProperties({RedissonProperties.class, RedisProperties.class})
public class RedissonAutoConfiguration {
@Autowired
private RedissonProperties redissonProperties;
@Autowired
private RedisProperties redisProperties;
@Autowired
private ApplicationContext ctx;
@Bean
@ConditionalOnMissingBean(name = "redisTemplate")
public RedisTemplate<Object, Object> redisTemplate(RedisConnectionFactory redisConnectionFactory) {
RedisTemplate<Object, Object> template = new RedisTemplate<Object, Object>();
template.setConnectionFactory(redisConnectionFactory);
return template;
}
//省略部分代码
@Bean(destroyMethod = "shutdown")
@ConditionalOnMissingBean(RedissonClient.class)
public RedissonClient redisson() throws IOException {
Config config = null;
Method clusterMethod = ReflectionUtils.findMethod(RedisProperties.class, "getCluster");
Method timeoutMethod = ReflectionUtils.findMethod(RedisProperties.class, "getTimeout");
Object timeoutValue = ReflectionUtils.invokeMethod(timeoutMethod, redisProperties);
int timeout;
if(null == timeoutValue){
timeout = 10000;
}else if (!(timeoutValue instanceof Integer)) {
Method millisMethod = ReflectionUtils.findMethod(timeoutValue.getClass(), "toMillis");
timeout = ((Long) ReflectionUtils.invokeMethod(millisMethod, timeoutValue)).intValue();
} else {
timeout = (Integer)timeoutValue;
}
if (redissonProperties.getConfig() != null) {
try {
InputStream is = getConfigStream();
config = Config.fromJSON(is);
} catch (IOException e) {
// trying next format
try {
InputStream is = getConfigStream();
config = Config.fromYAML(is);
} catch (IOException e1) {
throw new IllegalArgumentException("Can't parse config", e1);
}
}
} else if (redisProperties.getSentinel() != null) {
Method nodesMethod = ReflectionUtils.findMethod(Sentinel.class, "getNodes");
Object nodesValue = ReflectionUtils.invokeMethod(nodesMethod, redisProperties.getSentinel());
String[] nodes;
if (nodesValue instanceof String) {
nodes = convert(Arrays.asList(((String)nodesValue).split(",")));
} else {
nodes = convert((List<String>)nodesValue);
}
config = new Config();
config.useSentinelServers()
.setMasterName(redisProperties.getSentinel().getMaster())
.addSentinelAddress(nodes)
.setDatabase(redisProperties.getDatabase())
.setConnectTimeout(timeout)
.setPassword(redisProperties.getPassword());
} else if (clusterMethod != null && ReflectionUtils.invokeMethod(clusterMethod, redisProperties) != null) {
Object clusterObject = ReflectionUtils.invokeMethod(clusterMethod, redisProperties);
Method nodesMethod = ReflectionUtils.findMethod(clusterObject.getClass(), "getNodes");
List<String> nodesObject = (List) ReflectionUtils.invokeMethod(nodesMethod, clusterObject);
String[] nodes = convert(nodesObject);
config = new Config();
config.useClusterServers()
.addNodeAddress(nodes)
.setConnectTimeout(timeout)
.setPassword(redisProperties.getPassword());
} else {
config = new Config();
String prefix = "redis://";
Method method = ReflectionUtils.findMethod(RedisProperties.class, "isSsl");
if (method != null && (Boolean)ReflectionUtils.invokeMethod(method, redisProperties)) {
prefix = "rediss://";
}
config.useSingleServer()
.setAddress(prefix + redisProperties.getHost() + ":" + redisProperties.getPort())
.setConnectTimeout(timeout)
.setDatabase(redisProperties.getDatabase())
.setPassword(redisProperties.getPassword());
}
return Redisson.create(config);
}
//省略部分代码
}
复制代码
@Configuration 注解表明该类是一个配置类
@ConditionalOnClass(RedisOperations.class) 表明在存在 RedisOperations 类的前提下才会加载配置
@AutoConfigureBefore(RedisAutoConfiguration.class) 表明该配置文件的顺序,在 RedisAutoConfiguration 之前配置
@EnableConfigurationProperties({RedissonProperties.class, RedisProperties.class}) 表明配置信息可由其传入
@ConditionalOnMissingBean(name = “redisTemplate”) 当容器中没有名字为 redisTemplate
的 Bean 时,自动创建一个
深入 @ConditionalOnClass
@Target({ ElementType.TYPE, ElementType.METHOD })
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Conditional(OnClassCondition.class)
public @interface ConditionalOnClass {
/**
* The classes that must be present. Since this annotation is parsed by loading class
* bytecode, it is safe to specify classes here that may ultimately not be on the
* classpath, only if this annotation is directly on the affected component and
* <b>not</b> if this annotation is used as a composed, meta-annotation. In order to
* use this annotation as a meta-annotation, only use the {@link #name} attribute.
* @return the classes that must be present
*/
Class<?>[] value() default {};
/**
* The classes names that must be present.
* @return the class names that must be present.
*/
String[] name() default {};
}
复制代码
@ConditionalOnClass 的基础是 @Conditional ,它是由 Spring4 引入的条件装配, 它关注于在运行阶段动态选择配置(与 @Profile 相比),
@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();
}
复制代码
Conditional 可以指定多个 Condition ,如果所有的 Condition.matches() 方法返回 true,则条件满足,可以创建。
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);
}
复制代码
以 @ConditionalOnClass 为例,其对应的条件是 OnClassCondition ,其继承自 FilteringSpringBootCondition , FilteringSpringBootCondition.matches 方法调用了一些模板方法用来完成条件校验
@Order(Ordered.HIGHEST_PRECEDENCE)
class OnClassCondition extends FilteringSpringBootCondition {
//省略部分代码
@Override
public ConditionOutcome getMatchOutcome(ConditionContext context, AnnotatedTypeMetadata metadata) {
ClassLoader classLoader = context.getClassLoader();
ConditionMessage matchMessage = ConditionMessage.empty();
List<String> onClasses = getCandidates(metadata, ConditionalOnClass.class);
if (onClasses != null) {
List<String> missing = filter(onClasses, ClassNameFilter.MISSING, classLoader);
if (!missing.isEmpty()) {
return ConditionOutcome.noMatch(ConditionMessage.forCondition(ConditionalOnClass.class)
.didNotFind("required class", "required classes").items(Style.QUOTE, missing));
}
matchMessage = matchMessage.andCondition(ConditionalOnClass.class)
.found("required class", "required classes")
.items(Style.QUOTE, filter(onClasses, ClassNameFilter.PRESENT, classLoader));
}
List<String> onMissingClasses = getCandidates(metadata, ConditionalOnMissingClass.class);
if (onMissingClasses != null) {
List<String> present = filter(onMissingClasses, ClassNameFilter.PRESENT, classLoader);
if (!present.isEmpty()) {
return ConditionOutcome.noMatch(ConditionMessage.forCondition(ConditionalOnMissingClass.class)
.found("unwanted class", "unwanted classes").items(Style.QUOTE, present));
}
matchMessage = matchMessage.andCondition(ConditionalOnMissingClass.class)
.didNotFind("unwanted class", "unwanted classes")
.items(Style.QUOTE, filter(onMissingClasses, ClassNameFilter.MISSING, classLoader));
}
return ConditionOutcome.match(matchMessage);
}
//省略部分代码
}
复制代码
OnClassCondition 对 ConditionalOnMissingClass 与 ConditionalOnClass 都有判断
判断类是否存在,最终都是判断能否用类加载其加载
配置自定义装配类
SpringBoot 能够识别自动依赖, 是因为其会去扫描 classpath:META-INF/spring.factories 中配置的信息,对于 redisson-spring-boot-starter ,在 spring.factories 中有如下配置:
org.springframework.boot.autoconfigure.EnableAutoConfiguration=\
org.redisson.spring.starter.RedissonAutoConfiguration
复制代码
激活自动装配
一般我们的 SpringBoot 项目的启动类都会用注解@SpringBootApplication
,而自动装配的激活就隐藏在其中。
@SpringBootApplication
public class OnlineShopApplication {
public static void main(String[] args) {
SpringApplication.run(OnlineShopApplication.class, args);
}
}
复制代码
@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 {
// 省略
}
复制代码
从以上得知 @SpringBootApplication 这个注解由: @SpringBootConfiguration、@EnableAutoConfiguration、@ComponentScan 三个注解组成,我们则可以接着往下分析这三个注解。
@SpringBootConfiguration 只是对 @Configuration 注解进行了包装, 任何一个加了 @Configuration 注解的类都是一个 IOC 容器配置类,可以创建 Bean
@ComponentScan 注解的主要作用是扫描指定路径下标识了需要装配的类(默认扫描当前类路径),自动装配到 Spring IOC 容器管理。标识需要装配的类主要形式是:@Component 及派生自 @Component 注解的注解。
@EnableAutoConfiguration 自动装配类到 Spring IOC 容器管理,用于激活 @Enable 开头的注解,比如常见的 @EnableWebMvc 引入 springmvc 框架需要使用到的 bean
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Inherited
@AutoConfigurationPackage
@Import(AutoConfigurationImportSelector.class)
public @interface EnableAutoConfiguration {
// 省略
}
复制代码
@Import 主要用于导入 @Configuration 类,ImportSelector 和 ImportBeanDefinitionRegistrar 接口的实现类
ImportSelector 接口的主要作用是 判断那些类需要被导入
AutoConfigurationImportSelector 中会对在 spring.factories 中定义的 org.springframework.boot.autoconfigure.EnableAutoConfiguration 进行解析,然后通过过滤器等形式将符合条件的 Bean 导入
举例说明
假设我们对外提供一个组件,其需要提供自动装配功能,那么我们可以提供自己的 starter ,
public class MyComponent {
public void doSomething() {
System.out.println("该组件会做一些事情");
}
}
复制代码
所以我们的 starter 可以命名为 mystart-spring-boot-starter ,创建新项目在其中引入 SpringBoot 的依赖支持
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter</artifactId>
<version>2.1.7.RELEASE</version>
<optional>true</optional>
</dependency>
复制代码
创建自动配置类
创建配置类的作用是为了使 Spring 能够将组件纳入管理,以便其他类使用
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
@Configuration
public class MyComponentAutoConfiguration {
@Bean
public MyComponent myComponent() {
return new MyComponent();
}
}
复制代码
目前的配置比较简单,没有配置一些依赖条件
配置自定义装配
如果要配置自定义装配,那么需要让 SpringBoot 能够识别自动依赖, SpringBoot 会去扫描 classpath:META-INF/spring.factories 中配置的信息, 所以在其中配置
# Auto Configure
org.springframework.boot.autoconfigure.EnableAutoConfiguration=top.shaozuo.mystart.MyComponentAutoConfiguration
复制代码
使用自定义装配
将自动装配设置好之后,就可以在另外的项目中使用了, 比如我们新建一个 first-app 的 SpringBoot 项目,在其中引入 mystart-spring-boot-starter 依赖,
<dependency>
<groupId>top.shaozuo</groupId>
<artifactId>mystart-spring-boot-starter</artifactId>
<version>0.0.1-SNAPSHOT</version>
</dependency>
复制代码
使用所依赖的组件
@RunWith(SpringRunner.class)
@SpringBootTest
class OrderServiceTest {
@Autowired
private MyComponent myComponent;
@BeforeEach
void setUp() throws Exception {
}
@Test
void test() {
myComponent.doSomething();
}
}
复制代码
执行后如下:
评论