经过前面2节,我们大概知道了如果去使用Spring Boot,现在我们就Spring Boot的原理做个介绍,这节也会有Spring 的补充讲解
该节主要会讲解Spring Boot的自动装配、@Enable的工作原理、@Import的工作原理,以及@EnableAutoConfiguration的原理
3.1、Condition接口的用法
Spring Boot应该是“约定优于配置”的最佳实践者了,比如说我们加入了spring-data-jpa的jar包,那么Spring Boot就会默认给我们创建jap的相关链接(虽然很多时间这种默认的并不好,但是可以通过配置文件来配置我们想要的)。那么有时候我们需要按照我们自己的方案来装配对应的Bean,那这时候应该如何去做呢?
3.1.1、Condition的基本用法
我们先来看下面的示例
a、建立一个接口以及2个实现类
package com.dragon.condition;
public interface EncodingConvert {
}
package com.dragon.condition;
public class UTF8EncodingConvert implements EncodingConvert {
}
package com.dragon.condition;
public class GBKEncodingConvert implements EncodingConvert{
}
b、创建一个配置文件
package com.dragon.condition;
import org.springframework.boot.SpringBootConfiguration;
import org.springframework.context.annotation.Bean;
@SpringBootConfiguration
public class EncodingConvertConfiguration {
@Bean
public EncodingConvert createUTF8EncodingConvert(){
return new UTF8EncodingConvert();
}
@Bean
public EncodingConvert createGBKEncodingConvert(){
return new GBKEncodingConvert();
}
}
在上面的配置文件中,我们创建了上一步的2个类的bean。
c、启动类:
@SpringBootApplication
public class SpringBootConfigApplication {
public static void main(String[] args) {
ConfigurableApplicationContext application = SpringApplication.run(SpringBootConfigApplication.class, args);
System.out.println(application.getBeansOfType(EncodingConvert.class));
application.close();
}
}
运行该启动类,会打印如下:
不意外,我们可以看到2个类都加载进来了。那么我们如何才能够按照自己的意愿去装配Bean呢?
在spring-context中提供了一个org.springframework.context.annotation.Condition的接口,这个接口的作用就是按照条件来进行装配的。该接口中只有一个方法:
boolean matches(ConditionContext context, AnnotatedTypeMetadata metadata);
我们可以实现这个接口,然后根据返回值来达到时候装配的目的(返回true就装配,否则不装配)。
同样spring-context还提供了@Conditional这个注解
package org.springframework.context.annotation;
import java.lang.annotation.Documented;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
@Target({ElementType.TYPE, ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface Conditional {
/**
* All {@link Condition}s that must {@linkplain Condition#matches match}
* in order for the component to be registered.
*/
Class<? extends Condition>[] value();
}
该注解包含一个value的属性。
@Conditional注解和Condition一般是配合起来使用的。只有接口的实现类返回true才装配,否则不装配。
我们来改造上面的示例。
在上面的代码基础上,我们添加2个实现了Condition的类。
public class GBKCondition implements Condition {
@Override
public boolean matches(ConditionContext context, AnnotatedTypeMetadata metadata) {
String encoding = System.getProperty("file.encoding");
if(encoding != null){
return encoding.equalsIgnoreCase("gbk");
}
return false;
}
}
public class UTFCondition implements Condition {
@Override
public boolean matches(ConditionContext context, AnnotatedTypeMetadata metadata) {
String encoding = System.getProperty("file.encoding");
if(encoding != null){
return encoding.equalsIgnoreCase("utf-8");
}
return false;
}
}
然后我们修改EncodingConvertConfiguration类的内容
@SpringBootConfiguration
public class EncodingConvertConfiguration {
@Bean
@Conditional(UTFCondition.class)
public EncodingConvert createUTF8EncodingConvert(){
return new UTF8EncodingConvert();
}
@Bean
@Conditional(GBKCondition.class)
public EncodingConvert createGBKEncodingConvert(){
return new GBKEncodingConvert();
}
}
主要是在每个Bean上面添加了@Conditional的注解。然后我们修改启动类:
@SpringBootApplication
public class SpringBootConfigApplication {
public static void main(String[] args) {
ConfigurableApplicationContext application = SpringApplication.run(SpringBootConfigApplication.class, args);
System.out.println(System.getProperty("file.encoding"));
System.out.println(application.getBeansOfType(EncodingConvert.class));
application.close();
}
}
我们增加了file.encoding这个环境变量的打印。我们来看打印结果
我们可以看到当前file.encoding的值,以及加载的类,这时候就没有加载GBK的类了.
而如果我们修改环境变量为:
然后再运行,结果如下
这个时候,就是装配的GBK的了。
上面的就是Condition的基本示例。
@Conditional除了使用在方法上面,还可以使用在类上面,如果是使用在类上面,就表示该类下面的所有方法都起作用。
从上面的@Conditional里面的value属性我们知道,这是一个数组,也就说可以有多个class的值。如果value是多个类的数组,那么表示这多个Condition都返回true了,那么才装配。
上面演示了@Condition注解和Condition接口的基本用法,结合这2个类就可以实现我们的按需加载。这个组合是Spring Boot实现自动加载的基本原因之一。
3.1.2、Spring Boot的自动加载
Spring Boot为我们提供了大量有用的利于自动装配的注解,这些注解位于spring-boot-autoconfiguration包下:
一些解释:
- ConditionalOnBean 当context有bean的时候才装配。与之相反的是ConditionalOnMissingBean
- ConditionalOnClass 当classpath有类的时候才装配。
- ConditionalOnExpression 当使用SpEL表达式为true的时候才装配
- ConditionalOnJava 根据当前JVM的版本号进行装配。这个版本号有个范围,范围是大于等于或者小于。比如说大于等于某个版本才进行装配
- ConditionalOnNotWebApplication 当不是web环境的才进行装配
- ConditionalOnProperty 当某个配置文件存在,或者等于某个值的时候才装配。比如
@ConditionalOnClass(DruidDataSource.class)
@ConditionalOnProperty(
name = "spring.datasource.type",
havingValue = "com.alibaba.druid.pool.DruidDataSource",
matchIfMissing = true)
matchIfMissing就表示当spring.datasource.type没有的时候,也会生效。记住是没有该属性。
我们打开任意一个ConditionalOn*的源代码都可以看到@Condition注解和Condition接口的影子。
当我们发现Spring Boots提供的Condition不满足条件的时候,我们可以3.1小节的Condition接口和@Condition注解来实现满足我们自己业务需要的场景。做法就是声明一个Condition接口的类,在Bean上配合@Condition注解来达到动态装配的效果。
3.2、@Import的用法
在一些复杂应用里面,一般会有多个配置文件,比如在一些传统工程里面:
我们发现上面有很多的import的标签,这就是导入其他的配置文件。import标签对应的annotation就是@Import。
我们先来看一个简单的示例
先声明2个普通的类:
public class Role {
}
public class User {
}
由于这2个类没有添加任何的注解,所以是不能被容器托管的。这时候如果运行下面的代码:
@ComponentScan
public class SpringBootEnableApplication2 {
public static void main(String[] args) {
ConfigurableApplicationContext context = SpringApplication.run(SpringBootEnableApplication2.class, args);
System.out.println(context.getBean("user"));
System.out.println(context.getBean("role"));
context.close();
}
}
肯定会报下面的错:
org.springframework.beans.factory.NoSuchBeanDefinitionException: No bean named 'user' available
要想获取user这个bean,可以在User类上面加@Component注解来使的User类纳入到容器里面。
3.2.1、@Import导入类
除了使用上面的方式,我们还可以通过@Import来导入类,从而将该类纳入到Spring容器中:
@ComponentScan
@Import({User.class,Role.class})
public class SpringBootEnableApplication2 {
public static void main(String[] args) {
ConfigurableApplicationContext context = SpringApplication.run(SpringBootEnableApplication2.class, args);
System.out.println(context.getBean(User.class));
System.out.println(context.getBean(Role.class));
context.close();
}
}
再运行这个启动类,可以看到下面的打印结果:
可以看到已经可以获取Bean了。这里的Bean需要使用context.getBean(User.class)方式获取,而不能使用context.getBean(“user”)这种方式获取。
3.2.2、@Import导入配置类
除了导入具体的类外,还可以导入配置类,比如有下面的配置类:
public class MyConfiguration {
@Bean
public Runnable createRunnable(){
return () -> {};
}
@Bean
public Runnable createRunnable2(){
return () -> {};
}
}
(虽然该类没有使用@Configuration注解)
然后修改我们的启动类:
@ComponentScan
@Import(MyConfiguration.class)
public class SpringBootEnableApplication2 {
public static void main(String[] args) {
ConfigurableApplicationContext context = SpringApplication.run(SpringBootEnableApplication2.class, args);
System.out.println(context.getBeansOfType(Runnable.class));
context.close();
}
}
我们来看打印效果
可以看到这里打印了MyConfiguration里面定义的类。当然我们也可以在MyConfiguration使用@Configuration注解来标志这是一个配置类。如果这样做了,那么在启用类上就不需要@Import了。
总结:@Import用来导入一个或多个类(会被spring容器托管),或者配置类(配置类里面的bean都会被spring容器托管)。
3.2.3、@Import导入ImportSelector
我们来看看ImportSelecctor接口:
package org.springframework.context.annotation;
import org.springframework.core.type.AnnotationMetadata;
public interface ImportSelector {
String[] selectImports(AnnotationMetadata importingClassMetadata);
}
该接口的作用就是将selectImports返回的类都装配到Spring中去。入参是AnnotationMetadata,就是说可以根据业务逻辑来动态的返回类。我们来看下面的代码:
public class MyImportSelector implements ImportSelector {
public String[] selectImports(AnnotationMetadata importingClassMetadata) {
return new String[]{"com.dragon.User"};
}
}
@ComponentScan
@Import(MyImportSelector.class)
public class SpringBootEnableApplication2 {
public static void main(String[] args) {
ConfigurableApplicationContext context = SpringApplication.run(SpringBootEnableApplication2.class, args);
System.out.println(context.getBeansOfType(User.class));
System.out.println(context.getBeansOfType(Role.class));
context.close();
}
}
然后运行SpringBootEnableApplication2,我们会发现打印如下
我们发现User这个Bean打印了,但是Role这个Bean确是没有的。也就是说Spring容器只装置了User这个Bean。
如果我们把MyImportSelector改为下面:
public class MyImportSelector implements ImportSelector {
public String[] selectImports(AnnotationMetadata importingClassMetadata) {
return new String[]{"com.dragon.User", Role.class.getName()};
}
}
那么2个Bean都是可以拿到的。
ImportSelector接口中,selectImports方法的返回值,必须是一个class(全称),该class会被spring容器所托管起来。
selectImports方法还可以获取到注解的详细信息,然后根据信息去动态的返回需要被spring容器管理的bean。比如说我们增加一个EnableLog的自定义注解
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.TYPE)
@Documented
@Import(MyImportSelector.class)
public @interface EnableLog {
String name();
}
然后修改MyImportSelector:
/**
* selectImports方法的返回值,必须是一个class(全称),该class会被spring容器所托管起来
*/
public class MyImportSelector implements ImportSelector {
public String[] selectImports(AnnotationMetadata importingClassMetadata) {
Map<String, Object> annotationAttributes = importingClassMetadata.getAnnotationAttributes(EnableLog.class.getName());
Object value=annotationAttributes.get("name");
if(value.equals("my springboot")){
//这里可以做些逻辑处理返回一些Bean。
System.out.println("===> 一些逻辑处理");
}
return new String[]{"com.dragon.User", Role.class.getName()};
}
}
修改后的启动类:
@ComponentScan
@EnableLog(name = "my springboot")
public class SpringBootEnableApplication2 {
public static void main(String[] args) {
ConfigurableApplicationContext context = SpringApplication.run(SpringBootEnableApplication2.class, args);
System.out.println(context.getBeansOfType(User.class));
System.out.println(context.getBeansOfType(Role.class));
context.close();
}
}
运行启动类,打印如下
从而达到了动态加载bean的效果。
3.2.4、ImportBeanDefinitionRegistrar
除了ImportSelector接口,还有个接口ImportBeanDefinitionRegistrar也是需要关注的。该接口的声明如下:
package org.springframework.context.annotation;
import org.springframework.beans.factory.support.BeanDefinitionRegistry;
import org.springframework.beans.factory.support.BeanDefinitionRegistryPostProcessor;
import org.springframework.core.type.AnnotationMetadata;
public interface ImportBeanDefinitionRegistrar {
public void registerBeanDefinitions(
AnnotationMetadata importingClassMetadata, BeanDefinitionRegistry registry);
}
该接口的方法没有返回值。但是该方法有个BeanDefinitionRegistry的参数,BeanDefinitionRegistry的作用就是往容器里面注入Bean。
ImportSelector和ImportBeanDefinitionRegistrar的作用效果是一样的。只不过Spring通过ImportSelector的返回值来进行注入。而ImportBeanDefinitionRegistrar是让我们自己注入。
看下面的代码:
/**
* registerBeanDefinitions方法的参数有一个BeanDefinitionRegistry,BeanDefinitionRegistry可以用来往spring容器中注入bean
* 如此,我们就可以在registerBeanDefinitions方法里面动态的注入bean
*/
public class MyImportBeanDefinitionRegistrar implements ImportBeanDefinitionRegistrar {
public void registerBeanDefinitions(AnnotationMetadata importingClassMetadata, BeanDefinitionRegistry registry) {
BeanDefinitionBuilder bdb = BeanDefinitionBuilder.rootBeanDefinition(User.class);
registry.registerBeanDefinition("user", bdb.getBeanDefinition());
BeanDefinitionBuilder bdb2 = BeanDefinitionBuilder.rootBeanDefinition(Role.class);
registry.registerBeanDefinition("role", bdb2.getBeanDefinition());
BeanDefinitionBuilder bdb3 = BeanDefinitionBuilder.rootBeanDefinition(MyConfiguration.class);
registry.registerBeanDefinition(MyConfiguration.class.getName(), bdb3.getBeanDefinition());
}
}
这样,我们就在容器中”注入”了3个Bean。因为MyImportBeanDefinitionRegistrar并没有加上诸如@Component注解。所以,还需要有其他的步骤:
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.TYPE)
@Documented
@Import(MyImportBeanDefinitionRegistrar.class)
public @interface EnableLog {
String name();
}
我们只需要修改上一节的EnableLog自定义注解,然后倒入上面的MyImportBeanDefinitionRegistrar。再次运行SpringBootEnableApplication2类就可以达到理想的效果。
理解了上面的原理后,我们再来看看第2小节里面的@EnableConfigurationProperties的源码:
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Import(EnableConfigurationPropertiesImportSelector.class)
public @interface EnableConfigurationProperties {
Class<?>[] value() default {};
}
该源码导入了EnableConfigurationPropertiesImportSelector这个类。然后我们看看这个类的源码:
class EnableConfigurationPropertiesImportSelector implements ImportSelector {
@Override
public String[] selectImports(AnnotationMetadata metadata) {
MultiValueMap<String, Object> attributes = metadata.getAllAnnotationAttributes(
EnableConfigurationProperties.class.getName(), false);
Object[] type = attributes == null ? null
: (Object[]) attributes.getFirst("value");
if (type == null || type.length == 0) {
return new String[] {
ConfigurationPropertiesBindingPostProcessorRegistrar.class
.getName() };
}
return new String[] { ConfigurationPropertiesBeanRegistrar.class.getName(),
ConfigurationPropertiesBindingPostProcessorRegistrar.class.getName() };
}
//省略部分代码
}
这个类其实也是将一些类装载到Spring里面。从而达到了主动装配的效果。