目录
Spring注解发展史
Spring 1.X
Spring 2.X
Spring 2.5之前
@Required
@Repository
@Aspect
Spring2.5 之后
Spring 3.x
@ComponentScan
@Import
静态导入
ImportSelector
ImportBeanDefinitionRegistrar
@EnableXXX
Spring 4.x
Spring 5.x
什么是SPI
自动装配的流程演示
@EnableAutoConfiguration
那AutoConfigurationImportSelector是什么?
EnableDefineService
MyDefineImportSelector
EnableDemoTest
@EnableAutoConfiguration注解的实现原理
selectImports
getAutoConfigurationEntry
SpringFactoriesLoader
Spring Boot中的条件过滤
自己搓一个Starter来增进对自动装配的理解
创建一个Maven项目,quick-starter
定义Formate接口
定义相关的配置类
创建spring.factories文件
测试
自定义Starter关联配置信息
Spring注解发展史
为了更好的理解SpringBoot的内容,我们先梳理Spring注解编程的发展过程,由该过程的演变更理解SpringBoot的由来。
Spring 1.X
2004年3月24日,Spring1.0 正式发布,提供了IoC,AOP及XML配置的方式。
在Spring1.x版本中提供的是纯XML配置的方式,也就是在该版本中我们必须要提供xml的配置文件,在该文件中我们通过 <bean>
标签来配置需要被IoC容器管理的Bean。
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"xsi:schemaLocation="http://www.springframework.org/schema/beanshttp://www.springframework.org/schema/beans/spring-beans.xsd"><bean class="com.dura.demo01.UserService" />
</beans>public static void main(String[] args) {ApplicationContext ac = new FileSystemXmlApplicationContext("classpath:applicationContext01.xml");System.out.println("ac.getBean(UserService.class) = " + ac.getBean(UserService.class));
}
在Spring1.2版本的时候提供了@Transaction (org.springframework.transaction.annotation ) 注解。简化了事务的操作。
Spring 2.X
在2006年10月3日 Spring2.0问世了,在2.x版本中,比较重要的特点是增加了很多注解
Spring 2.5之前
在2.5版本之前新增的有 @Required
@Repository
@Aspect
,同时也扩展了XML的配置能力,提供了第三方的扩展标签,比如 <dubbo>
@Required
如果你在某个java类的某个set方法上使用了该注释,那么该set方法对应的属性在xml配置文件中必须被设置,否则就会报错!!!
如果在xml文件中我们不设置对应的属性就会给出错误的提示。
@Repository
@Repository 对应数据访问层Bean.这个注解在Spring2.0版本就提供的。
@Aspect
@Aspect是AOP相关的一个注解,用来标识配置类。
Spring2.5 之后
在2007年11月19日,Spring更新到了2.5版本,新增了很多常用注解,大大的简化配置操作。
注解 | 说明 |
@Autowired | 依赖注入 |
@Qualifier | 配置@Autowired注解使用 |
@Component | 声明组件 |
@Service | 声明业务层组件 |
@Controller | 声明控制层组件 |
@RequestMapping | 声明请求对应的处理方法 |
在这些注解的作用下,我们可以不用在xml文件中去注册没有bean,这时我们只需要指定扫码路径,然后在对应的Bean头部添加相关的注解即可,这大大的简化了我们的配置及维护工作。
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"xmlns:context="http://www.springframework.org/schema/context"xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsdhttp://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context.xsd"><context:component-scan base-package="com.dura" />
</beans>
虽然在Spring的2.5版本提供了很多的注解,也大大的简化了我们的开发,但是任然没有摆脱XML配置驱动。
Spring 3.x
在2009年12月16日发布了Spring3.0版本,这是一个注解编程发展的里程碑版本,在该版本中全面拥抱Java5。提供了 @Configuration
注解,目的就是去xml化。同时通过 @ImportResource
来实现Java配置类和XML配置的混合使用来实现平稳过渡。
/*** @Configuration 标注的Java类 相当于 application.xml 配置文件*/
@Configuration
public class JavaConfig {/*** @Bean 注解 标注的方法就相当于 <bean></bean> 标签也是 Spring3.0 提供的注解* @return*/@Beanpublic UserService userService(){return new UserService();}
}
在Spring3.1 版之前配置扫描路径我们还只能在 XML 配置文件中通过 component-scan
标签来实现,在3.1 版本到来的时候,提供了一个 @ComponentScan
注解,该注解的作用是替换掉 component-scan
标签,是注解编程很大的进步,也是Spring实现无配置化的坚实基础。
@ComponentScan
@ComponentScan的作用是指定扫码路径,用来替代在XML中的 <component-scan>
标签,默认的扫码路径是当前注解标注的类所在的包及其子包。
@Import
@Import注解只能用在类上,作用是快速的将实例导入到Spring的IoC容器中,将实例导入到IoC容器中的方式有很多种,比如 @Bean
注解,@Import注解可以用于导入第三方包。具体的使用方式有三种。
静态导入
静态导入的方式是直接将我们需要导入到IoC容器中的对象类型直接添加进去即可。这种方式的好处是简单,直接,但是缺点是如果要导入的比较多,则不太方便,而且也不灵活。
ImportSelector
@Import
注解中我们也可以添加一个实现了 ImportSelector
接口的类型,这时不会将该类型导入IoC容器中,而是会调用 ImportSelector
接口中定义的 selectImports
方法,将该方法的返回的字符串数组的类型添加到容器中。
定义ImportSelector接口的实现,方法返回的是需要添加到IoC容器中的对象对应的类型的全类路径的字符串数组,我们可以根据不同的业务需求而导入不同的类型,会更加的灵活些。
public class MyImportSelector implements ImportSelector {@Overridepublic String[] selectImports(AnnotationMetadata importingClassMetadata) {return new String[]{Logger.class.getName(),Cache.class.getName()};}
}
ImportBeanDefinitionRegistrar
除了上面所介绍的ImportSelector方式灵活导入以外还提供了 ImportBeanDefinitionRegistrar
接口,也可以实现,相比 ImportSelector
接口的方式,ImportBeanDefinitionRegistrar 的方式是直接在定义的方法中提供了 BeanDefinitionRegistry
,自己在方法中实现注册。
@EnableXXX
@Enable模块驱动,其实是在系统中我们先开发好各个功能独立的模块,比如 Web MVC 模块, AspectJ代理模块,Caching模块等。
Spring 4.x
2013年11月1 日更新的Spring 4.0 ,完全支持Java8.这是一个注解完善的时代,提供的核心注解是@Conditional条件注解。@Conditional 注解的作用是按照一定的条件进行判断,满足条件就给容器注册Bean实例。
@Conditional的定义为:类和方法中使用
// 该注解可以在 类和方法中使用
@Target({ElementType.TYPE, ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface Conditional {/*** 注解中添加的类型必须是 实现了 Condition 接口的类型*/Class<? extends Condition>[] value();
}
Condition是个接口,需要实现matches方法,返回true则注入bean,false则不注入。
@Conditional的作用就是给我们提供了对象导入IoC容器的条件机制,这也是SpringBoot中的自动装配的核心关键。当然在4.x还提供一些其他的注解支持,比如 @EventListener
,作为ApplicationListener接口编程的第二选择,@AliasFor
解除注解派生的时候冲突限制。@CrossOrigin
作为浏览器跨域资源的解决方案。
Spring 5.x
2017年9月28日,Spring来到了5.0版本。5.0同时也是SpringBoot2.0的底层。注解驱动的性能提升方面不是很明显。在Spring Boot应用场景中,大量使用@ComponentScan扫描,导致Spring模式的注解解析时间耗时增大,因此,5.0时代引入@Indexed,为Spring模式注解添加索引。
当我们在项目中使用了 @Indexed
之后,编译打包的时候会在项目中自动生成 META-INT/spring.components
文件。当Spring应用上下文执行 ComponentScan
扫描时,META-INT/spring.components
将会被 CandidateComponentsIndexLoader
读取并加载,转换为 CandidateComponentsIndex
对象,这样的话 @ComponentScan
不在扫描指定的package,而是读取 CandidateComponentsIndex
对象,从而达到提升性能的目的。
<dependency><groupId>org.springframework</groupId><artifactId>spring-context-indexer</artifactId>
</dependency>
什么是SPI
在SpringBoot的自动装配中其实有使用到SPI机制
SPI ,全称为 Service Provider Interface,是一种服务发现机制。它通过在ClassPath路径下的META-INF/services文件夹查找文件,自动加载文件里所定义的类。这一机制为很多框架扩展提供了可能,比如在Dubbo、JDBC中都使用到了SPI机制。我们先通过一个很简单的例子来看下它是怎么用的。
流程:A项目中仅声明个接口;在拓展的实现,导入A项目的依赖,创建接口的实现类。然后在resources目录下创建 META-INF/services 目录,然后在目录中创建一个文件,名称必须是定义的接口的全类路径名称。然后在文件中写上接口的实现类的全类路径名称。然后A项目、B项目均可以用于C项目。
ServiceLoader:
// 配置文件的路径private static final String PREFIX = "META-INF/services/";// 加载的服务 类或者接口private final Class<S> service;// 类加载器private final ClassLoader loader;// 访问权限的上下文对象private final AccessControlContext acc;// 保存已经加载的服务类private LinkedHashMap<String,S> providers = new LinkedHashMap<>();// 内部类,真正加载服务类private LazyIterator lookupIterator;
load方法创建了一些属性,重要的是实例化了内部类,LazyIterator。
public final class ServiceLoader<S> implements Iterable<S>private ServiceLoader(Class<S> svc, ClassLoader cl) {//要加载的接口service = Objects.requireNonNull(svc, "Service interface cannot be null");//类加载器loader = (cl == null) ? ClassLoader.getSystemClassLoader() : cl;//访问控制器acc = (System.getSecurityManager() != null) ? AccessController.getContext() : null;reload();}public void reload() {//先清空providers.clear();//实例化内部类 LazyIterator lookupIterator = new LazyIterator(service, loader);}
}
查找实现类和创建实现类的过程,都在LazyIterator完成。当我们调用iterator.hasNext和iterator.next方法的时候,实际上调用的都是LazyIterator的相应方法。
private class LazyIterator implements Iterator<S>{Class<S> service;ClassLoader loader;Enumeration<URL> configs = null;Iterator<String> pending = null;String nextName = null; private boolean hasNextService() {//第二次调用的时候,已经解析完成了,直接返回if (nextName != null) {return true;}if (configs == null) {//META-INF/services/ 加上接口的全限定类名,就是文件服务类的文件//META-INF/services/com.viewscenes.netsupervisor.spi.SPIServiceString fullName = PREFIX + service.getName();//将文件路径转成URL对象configs = loader.getResources(fullName);}while ((pending == null) || !pending.hasNext()) {//解析URL文件对象,读取内容,最后返回pending = parse(service, configs.nextElement());}//拿到第一个实现类的类名nextName = pending.next();return true;}
}
创建实例对象,当然,调用next方法的时候,实际调用到的是,lookupIterator.nextService。它通过反射的方式,创建实现类的实例并返回。
private class LazyIterator implements Iterator<S>{private S nextService() {//全限定类名String cn = nextName;nextName = null;//创建类的Class对象Class<?> c = Class.forName(cn, false, loader);//通过newInstance实例化S p = service.cast(c.newInstance());//放入集合,返回实例providers.put(cn, p);return p; }
}
在前面的分析中,Spring Framework一直在致力于解决一个问题,就是如何让bean的管理变得更简单,如何让开发者尽可能的少关注一些基础化的bean的配置,从而实现自动装配。所以,所谓的自动装配,实际上就是如何自动将bean装载到Ioc容器中来。
实际上在spring 3.x版本中,Enable模块驱动注解的出现,已经有了一定的自动装配的雏形,而真正能够实现这一机制,还是在spirng 4.x版本中,conditional条件注解的出现。我们就来分析SpringBoot 自动装配到底怎么个事儿?
自动装配的流程演示
<dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-data-redis</artifactId>
</dependency> spring:redis:host: 127.0.0.1 port: 6379@Autowired
private RedisTemplate<String,String>redisTemplate;
按照上面的顺序添加starter,然后添加配置,使用RedisTemplate就可以使用了?
为什么RedisTemplate可以被直接注入?而他又是什么时候加入到IOC容器的呢?
这就是自动装配-->能够使得ClassPath下依赖的包相关的Bean被自动装配到Spring IoC容器中。
@EnableAutoConfiguration
EnableAutoConfiguration的主要作用其实就是帮助Spring Boot应用把所有符合条件的@Configuration配置都加载到当前SpringBoot创建并使用的IoC容器中。
再回到EnableAutoConfiguration这个注解中,我们发现它的import是这样
@Import(AutoConfigurationImportSelector.class)
public @interface EnableAutoConfiguration { ----}
从EnableAutoConfiguration上面的import注解来看,这里面并不是引入另外一个Configuration。而是一个ImportSelector。这个是什么东西呢?
那AutoConfigurationImportSelector是什么?
Enable注解不仅仅可以实现多个Configuration的整合,还可以实现一些复杂的场景,比如可以根据上下文来激活不同类型的bean,@Import注解可以配置三种不同的class
-
第一种就是前面演示过的,基于普通bean或者带有@Configuration的bean进行诸如
-
实现ImportSelector接口进行动态注入
-
实现ImportBeanDefinitionRegistrar接口进行动态注入
CacheService public class CacheService { } LoggerService public class LoggerService { }
EnableDefineService
@Target(ElementType.TYPE) @Retention(RetentionPolicy.RUNTIME) @Documented @Inherited //允许被继承 @Import({MyDefineImportSelector.class}) public @interface EnableDefineService {String[] packages() default ""; }
MyDefineImportSelector
public class MyDefineImportSelector implements ImportSelector {@Overridepublic String[] selectImports(AnnotationMetadata annotationMetadata) {//获得指定注解的详细信息。我们可以根据注解中配置的属性来返回不同的class,//从而可以达到动态开启不同功能的目的annotationMetadata.getAllAnnotationAttributes(EnableDefineService.class.getName(),true).forEach((k,v) -> {log.info(annotationMetadata.getClassName());log.info("k:{},v:{}",k,String.valueOf(v));});return new String[]{CacheService.class.getName()};} }
EnableDemoTest
@SpringBootApplication @EnableDefineService(name = "dura",value = "dura") public class EnableDemoTest {public static void main(String[] args) {ConfigurableApplicationContext ca=SpringApplication.run(EnableDemoTest.class,args);System.out.println(ca.getBean(CacheService.class));System.out.println(ca.getBean(LoggerService.class));} }
了解了Selector的基本原理之后,后续再去分析AutoConfigurationImportSelector的原理就很简单了,它本质上也是对于bean的动态加载。
@EnableAutoConfiguration注解的实现原理
了解了ImportSelector和ImportBeanDefinitionRegistrar后,对于EnableAutoConfiguration的理解就容易一些了
它会通过import导入第三方提供的bean的配置类:AutoConfigurationImportSelector
@Import(AutoConfigurationImportSelector.class)
从名字来看,可以猜到它是基于ImportSelector来实现基于动态bean的加载功能。
我们知道SpringBoot @Enable*注解的工作原理ImportSelector接口 的selectImports 方法返回的数组(类的全类名)都会被纳入到Spring容器中。
那么可以猜想到这里的实现原理也应该是一样的,定位到AutoConfigurationImportSelector这个类中的selectImports方法
selectImports
public String[] selectImports(AnnotationMetadata annotationMetadata) {if (!isEnabled(annotationMetadata)) {return NO_IMPORTS;}
// 从配置文件(spring-autoconfigure-metadata.properties)中加载 AutoConfigurationMetadataAutoConfigurationMetadata autoConfigurationMetadata = AutoConfigurationMetadataLoader.loadMetadata(this.beanClassLoader);
// 获取所有候选配置类EnableAutoConfigurationAutoConfigurationEntry autoConfigurationEntry = getAutoConfigurationEntry(autoConfigurationMetadata, annotationMetadata);return StringUtils.toStringArray(autoConfigurationEntry.getConfigurations());
}
getAutoConfigurationEntry
protected AutoConfigurationEntry getAutoConfigurationEntry(AutoConfigurationMetadata autoConfigurationMetadata,AnnotationMetadata annotationMetadata) {if (!isEnabled(annotationMetadata)) {return EMPTY_ENTRY;}
//获取元注解中的属性AnnotationAttributes attributes = getAttributes(annotationMetadata);
//使用SpringFactoriesLoader 加载classpath路径下META-INF\spring.factories中,
//key= org.springframework.boot.autoconfigure.EnableAutoConfiguration对应的valueList<String> configurations = getCandidateConfigurations(annotationMetadata,attributes);
//去重configurations = removeDuplicates(configurations);
//应用exclusion属性Set<String> exclusions = getExclusions(annotationMetadata, attributes);checkExcludedClasses(configurations, exclusions);configurations.removeAll(exclusions);
//过滤,检查候选配置类上的注解@ConditionalOnClass,如果要求的类不存在,则这个候选类会被过滤不被加载configurations = filter(configurations, autoConfigurationMetadata);//广播事件
fireAutoConfigurationImportEvents(configurations, exclusions);return new AutoConfigurationEntry(configurations, exclusions);
}
本质上来说,其实EnableAutoConfiguration会帮助SpringBoot应用把所有符合@Configuration配置都加载到当前SpringBoot创建的IoC容器,而这里面借助了Spring框架提供的一个工具类SpringFactoriesLoader的支持,以及用到了Spring提供的条件注解@Conditional,选择性的针对需要加载的Bean进行条件过滤。
SpringFactoriesLoader
然后,我们先树立下SpringFactoriesLoader这个由Spring所提供的工具类的用途。
它其实和Java中的SPI机制原理是类似的。只不过是它比SPI更好的一点在于一次性不会加载所有的类,而是根据Key进行加载。
首先,SpringFactoriesLoader的作用是从classpath/META-INF/spring.factories文件中,根据key来加载对应的类到spring IoC容器中。
整体流程如图
Spring Boot中的条件过滤
在分析AutoConfigurationImportSelector的源码时,会先扫描spring-autoconfiguration-metadata.properties文件,最后在扫描spring.factories对应的类时,会结合前面的元数据进行过滤,为什么要过滤呢? 原因是很多的@Configuration其实是依托于其他的框架来加载的,如果当前的classpath环境下没有相关联的依赖,则意味着这些类没必要进行加载,所以,通过这种条件过滤可以有效的减少@configuration类的数量从而降低SpringBoot的启动时间。
自己搓一个Starter来增进对自动装配的理解
-
创建一个Maven项目,quick-starter
定义相关依赖
<dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter</artifactId><version>2.1.6.RELEASE</version>
</dependency>
<dependency><groupId>com.alibaba</groupId><artifactId>fastjson</artifactId><version>1.2.56</version><!-- 可选 --><optional>true</optional>
</dependency>
-
定义Formate接口
public interface FormatProcessor {/*** 定义一个格式化的方法* @param obj* @param <T>* @return*/<T> String formate(T obj);
}
public class JsonFormatProcessor implements FormatProcessor {@Overridepublic <T> String formate(T obj) {return "JsonFormatProcessor:" + JSON.toJSONString(obj);}
}
public class StringFormatProcessor implements FormatProcessor {@Overridepublic <T> String formate(T obj) {return "StringFormatProcessor:" + obj.toString();}
}
-
定义相关的配置类
@Configuration
public class FormatAutoConfiguration {@ConditionalOnMissingClass("com.alibaba.fastjson.JSON")@Bean@Primary // 优先加载public FormatProcessor stringFormatProcessor(){return new StringFormatProcessor();}@ConditionalOnClass(name="com.alibaba.fastjson.JSON")@Beanpublic FormatProcessor jsonFormatProcessor(){return new JsonFormatProcessor();}
}
定义一个模板工具类
public class HelloFormatTemplate {private FormatProcessor formatProcessor;public HelloFormatTemplate(FormatProcessor processor){this.formatProcessor = processor;}public <T> String doFormat(T obj){StringBuilder builder = new StringBuilder();builder.append("Execute format : ").append("<br>");builder.append("Object format result:" ).append(formatProcessor.formate(obj));return builder.toString();}
}
整合到SpringBoot的Java配置类
@Configuration
@Import(FormatAutoConfiguration.class)
public class HelloAutoConfiguration {@Beanpublic HelloFormatTemplate helloFormatTemplate(FormatProcessor formatProcessor){return new HelloFormatTemplate(formatProcessor);}
}
-
创建spring.factories文件
在resources下创建META-INF目录,再在其下创建spring.factories文件
org.springframework.boot.autoconfigure.EnableAutoConfiguration=\org.dura.autoconfiguration.HelloAutoConfiguration
install 打包,然后就可以在SpringBoot项目中依赖该项目来操作了。
-
测试
<dependency><groupId>org.example</groupId><artifactId>format-spring-boot-starter</artifactId><version>1.0-SNAPSHOT</version>
</dependency>@RestController
public class UserController {@Autowiredprivate HelloFormatTemplate helloFormatTemplate;@GetMapping("/format")public String format(){User user = new User();user.setName("BoBo");user.setAge(18);return helloFormatTemplate.doFormat(user);}
}
}
-
自定义Starter关联配置信息
有些情况下我们可以需要用户在使用的时候动态的传递相关的配置信息,比如Redis的Ip,端口等等,这些信息显然是不能直接写到代码中的,这时我们就可以通过SpringBoot的配置类来实现。
<dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-configuration-processor</artifactId><version>2.2.6.RELEASE</version><optional>true</optional>
</dependency>
@ConfigurationProperties(prefix = HelloProperties.HELLO_FORMAT_PREFIX)
public class HelloProperties {public static final String HELLO_FORMAT_PREFIX="mashibing.hello.format";private String name;private Integer age;private Map<String,Object> info;public Map<String, Object> getInfo() {return info;}public void setInfo(Map<String, Object> info) {this.info = info;}public String getName() {return name;}public void setName(String name) {this.name = name;}public Integer getAge() {return age;}public void setAge(Integer age) {this.age = age;}
}
然后再Java配置类中关联
@Configuration
@Import(FormatAutoConfiguration.class)
@EnableConfigurationProperties(HelloProperties.class)
public class HelloAutoConfiguration {@Beanpublic HelloFormatTemplate helloFormatTemplate(HelloProperties helloProperties,FormatProcessor formatProcessor){return new HelloFormatTemplate(helloProperties,formatProcessor);}
}
调整模板方法
public class HelloFormatTemplate {private FormatProcessor formatProcessor;private HelloProperties helloProperties;public HelloFormatTemplate(HelloProperties helloProperties,FormatProcessor processor){this.helloProperties = helloProperties;this.formatProcessor = processor;}public <T> String doFormat(T obj){StringBuilder builder = new StringBuilder();builder.append("Execute format : ").append("<br>");builder.append("HelloProperties:").append(formatProcessor.formate(helloProperties.getInfo())).append("<br>");builder.append("Object format result:" ).append(formatProcessor.formate(obj));return builder.toString();}
}
增加提示在这个工程的META-INF/下创建一个additional-spring-configuration-metadata.json,这个是设置属性的提示类型
{"properties": [{"name": "dura.hello.format.name","type": "java.lang.String","description": "账号信息","defaultValue": "root"},{"name": "dura.hello.format.age","type": "java.lang.Integer","description": "年龄","defaultValue": 18}]
}