目錄
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}]
}