写点什么

SpringBoot 系列(7)- 自动装配

用户头像
引花眠
关注
发布于: 2020 年 12 月 28 日

自动装配

自动装配可以说是 SpringBoot 的一个核心,它的基础就是 Spring4.0 增加的条件化配置。涉及到的注解有:


  1. @Condition

  2. @Enable

  3. @Import

  4. @EnableAutoConfigure


其实说白了就是在 SpringBoot 启动过程中,根据预先定义好的条件,判断是否可以创建相应的 Bean,那么如果要完成自动装配的功能,就需要有这么几步


  1. 定义自动装配类 ,用于创建 Bean

  2. 配置自定义装配

  3. 使用自定义装配类


我们以 redisson-spring-boot-starter 为例,查看如何使用


定义自动装配类

我们在项目中引入 redisson-spring-boot-starter , 对于 starter 的命名有以下约定:


  1. 官方包命名一般是:spring-boot-starter-xxx,如 spring-boot-starter-data-redis

  2. 第三方包命令一般是: 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); }
//省略部分代码

}
复制代码


  1. @Configuration 注解表明该类是一个配置类

  2. @ConditionalOnClass(RedisOperations.class) 表明在存在 RedisOperations 类的前提下才会加载配置

  3. @AutoConfigureBefore(RedisAutoConfiguration.class) 表明该配置文件的顺序,在 RedisAutoConfiguration 之前配置

  4. @EnableConfigurationProperties({RedissonProperties.class, RedisProperties.class}) 表明配置信息可由其传入

  5. @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)@Documentedpublic @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); }
//省略部分代码
}
复制代码


  1. OnClassCondition 对 ConditionalOnMissingClass 与 ConditionalOnClass 都有判断

  2. 判断类是否存在,最终都是判断能否用类加载其加载


配置自定义装配类

SpringBoot 能够识别自动依赖, 是因为其会去扫描 classpath:META-INF/spring.factories 中配置的信息,对于 redisson-spring-boot-starter ,在 spring.factories 中有如下配置:


org.springframework.boot.autoconfigure.EnableAutoConfiguration=\org.redisson.spring.starter.RedissonAutoConfiguration
复制代码


激活自动装配

一般我们的 SpringBoot 项目的启动类都会用注解@SpringBootApplication,而自动装配的激活就隐藏在其中。


@SpringBootApplicationpublic 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 三个注解组成,我们则可以接着往下分析这三个注解。


  1. @SpringBootConfiguration 只是对 @Configuration 注解进行了包装, 任何一个加了 @Configuration 注解的类都是一个 IOC 容器配置类,可以创建 Bean

  2. @ComponentScan 注解的主要作用是扫描指定路径下标识了需要装配的类(默认扫描当前类路径),自动装配到 Spring IOC 容器管理。标识需要装配的类主要形式是:@Component 及派生自 @Component 注解的注解。

  3. @EnableAutoConfiguration 自动装配类到 Spring IOC 容器管理,用于激活 @Enable 开头的注解,比如常见的 @EnableWebMvc 引入 springmvc 框架需要使用到的 bean


@Target(ElementType.TYPE)@Retention(RetentionPolicy.RUNTIME)@Documented@Inherited@AutoConfigurationPackage@Import(AutoConfigurationImportSelector.class)public @interface EnableAutoConfiguration {    // 省略}
复制代码


  1. @Import 主要用于导入 @Configuration 类,ImportSelector 和 ImportBeanDefinitionRegistrar 接口的实现类

  2. ImportSelector 接口的主要作用是 判断那些类需要被导入

  3. 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;
@Configurationpublic class MyComponentAutoConfiguration {
@Bean public MyComponent myComponent() { return new MyComponent(); }
}
复制代码


目前的配置比较简单,没有配置一些依赖条件


配置自定义装配

如果要配置自定义装配,那么需要让 SpringBoot 能够识别自动依赖, SpringBoot 会去扫描 classpath:META-INF/spring.factories 中配置的信息, 所以在其中配置


# Auto Configureorg.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)@SpringBootTestclass OrderServiceTest {

@Autowired private MyComponent myComponent;
@BeforeEach void setUp() throws Exception { }
@Test void test() { myComponent.doSomething(); }
}
复制代码


执行后如下: 


发布于: 2020 年 12 月 28 日阅读数: 61
用户头像

引花眠

关注

还未添加个人签名 2018.06.11 加入

还未添加个人简介

评论

发布
暂无评论
SpringBoot系列(7)- 自动装配