Spring Boot 一個重要的特點就是自動配置,約定大於配置,幾乎所有組件使用其本身約定好的默認配置就可以使用,大大減輕配置的麻煩。其實現自動配置一個方式就是使用@Enable*注解,見其名知其意也,即“使什么可用或開啟什么的支持”。
Spring Boot 常用@Enable*
首先來簡單介紹一下Spring Boot 常用的@Enable*注解及其作用吧。
@EnableAutoConfiguration
開啟自動掃描裝配Bean,組合成@SpringBootApplication注解之一@EnableScheduling
開啟計划任務的支持@EnableTransactionManagement
開啟注解式事務的支持。@EnableCaching
開啟注解式的緩存支持。@EnableAspectJAutoProxy
開啟對AspectJ自動代理的支持。@EnableEurekaServer
開啟Euraka Service 的支持,開啟spring cloud的服務注冊與發現@EnableDiscoveryClient
開啟服務提供者或消費者,客戶端的支持,用來注冊服務或連接到如Eureka之類的注冊中心@EnableFeignClients
開啟Feign功能
還有一些不常用的比如:
@EnableAsync
開啟異步方法的支持@EnableWebMvc
開啟Web MVC的配置支持。@EnableConfigurationProperties
開啟對@ConfigurationProperties注解配置Bean的支持。@EnableJpaRepositories
開啟對Spring Data JPA Repository的支持。
參考:http://tangxiaolin.com/learn/show?id=402881d2648c88cc01648c89d8730001
@Enable*的源碼解析
查看它們的源碼
@EnableAutoConfiguration
@EnableCaching 開啟注解式的緩存支持。
@EnableDiscoveryClient(@EnableEurekaServer 也是使用了這個組合注解) 開啟服務提供者或消費者,客戶端的支持,用來注冊服務或連接到如Eureka之類的注冊中心
@EnableAspectJAutoProxy 開啟對AspectJ自動代理的支持。
@EnableFeignClients 開啟Feign功能
@EnableScheduling(這個比較特殊,為自己直接新建相關類,不繼承Selector和Registrar) 開啟計划任務的支持
源碼規律及解析
可以發現它們都使用了@Import注解(其中@Target:注解的作用目標,@Retention:注解的保留位置,@Inherited:說明子類可以繼承父類中的該注解,@Document:說明該注解將被包含在javadoc中)
該元注解是被用來整合所有在@Configuration注解中定義的bean配置,即相當於我們將多個XML配置文件導入到單個文件的情形。
而它們所引入的配置類,主要分為Selector和Registrar,其分別實現了ImportSelector
和ImportBeanDefinitionRegistrar
接口,
兩個的大概意思都是說,會根據AnnotationMetadata元數據注冊bean類,即返回的Bean 會自動的被注入,被Spring所管理。
既然他們功能都相同,都是用來返回類,為什么 Spring 有這兩種不同的接口類的呢?
其實剛開始的時候我也以為它們功能應該都是一樣的,后面我在組內分享的時候,我的導師就問了我這個問題,然后當時我沒有留意這個點所以答不出來😂。后面回去細看了一下和搜索了相關資料,發現它們的功能有些細微差別。首先我們從上面截圖可以清楚地看到ImportBeanDefinitionRegistrar
接口類中 registerBeanDefinitions
方法多了一個參數 BeanDefinitionRegistry
(點擊這個參數進入看這個參數的Javadoc,可以知道,它是用於保存bean定義的注冊表的接口),所以如果是實現了這個接口類的首先可以應用比較復雜的注冊類的判斷條件,例如:可以判斷之前的類是否有注冊到 Spring 中了。另外就是實現了這個接口類能修改或新增 Spring 類定義BeanDefinition
的一些屬性(查看其中一個實現了這個接口例子如:AspectJAutoProxyRegistrar
,追查 BeanDefinitionRegistry
參數可以查看到)。
具體實現例子@EnableDiscoveryClient
可以看一下具體的一個例子在@EnableDiscoveryClient引入了EnableDiscoveryClientImportSelector,通過查看其繼承實現圖
可以看到其最終實現了ImportSelector接口,查看其具體實現源碼
知道其先得到父類注冊的bean類,然后如果在查看AnnotationMetadata中是否存在autoRegister,是否需要注冊該類,如果存在,則繼續將org.springframework.cloud.client.serviceregistry.AutoServiceRegistrationConfiguration
該類返回,加入到spring容器中。
源碼小結
通過查看@Enable*源碼,我們可以清楚知道其實現自動配置的方式的底層就是通過@Import注解引入相關配置類,然后再在配置類將所需的bean注冊到spring容器中和實現組件相關處理邏輯去。
自定義@Enable*注解(EnableSelfBean)
在這里我們利用@Import和ImportSelector動手自定義一個自己的EnableSelfBean。該Enable注解可以將某些包下的所有類自動注冊到spring容器中,對於一些實體類的項目很多的情況下,可以考慮一下通過這種方式將某包下所有類自動加入到spring容器,不再需要每個類再加上@Component等注解。
- 先創建一個spring boot項目。
- 創建包entity,並新建類Role,將其放入到entity包中。
/**
* 測試自己的自動注解的實體類
* @author zhangcanlong
* @since 2019/2/14 10:41
**/
public class Role {
public String test(){
return "hello";
}
}
- 創建自定義配置類SelfEnableAutoConfig並實現ImportSelector接口。其中使用到ClassUtils類是用來獲取自己某個包下的所有類的名稱的。
/**
* 自己的定義的自動注解配置類
* @author zhangcanlong
* @since 2019/2/14 10:45
**/
public class SelfEnableAutoConfig implements ImportSelector {
Logger logger = LoggerFactory.getLogger(SelfEnableAutoConfig.class);
@Override
public String[] selectImports(AnnotationMetadata annotationMetadata) {
//獲取EnableEcho注解的所有屬性的value
Map<String, Object> attributes = annotationMetadata.getAnnotationAttributes(EnableSelfBean.class.getName());
if(attributes==null){return new String[0]; }
//獲取package屬性的value
String[] packages = (String[]) attributes.get("packages");
if(packages==null || packages.length<=0 || StringUtils.isEmpty(packages[0])){
return new String[0];
}
logger.info("加載該包所有類到spring容器中的包名為:"+ Arrays.toString(packages));
Set<String> classNames = new HashSet<>();
for(String packageName:packages){
classNames.addAll(ClassUtils.getClassName(packageName,true));
}
//將類打印到日志中
for(String className:classNames){
logger.info(className+"加載到spring容器中");
}
String[] returnClassNames = new String[classNames.size()];
returnClassNames= classNames.toArray(returnClassNames);
return returnClassNames;
}
}
ClassUtil類
/**
* 獲取所有包下的類名的工具類。參考:https://my.oschina.net/cnlw/blog/299265
* @author zhangcanlong
* @since 2019/2/14
**/
@Component
public class ClassUtils {
private static final String FILE_STR= "file";
private static final String JAR_STR = "jar";
/**
* 獲取某包下所有類
* @param packageName 包名
* @param isRecursion 是否遍歷子包
* @return 類的完整名稱
*/
public static Set<String> getClassName(String packageName, boolean isRecursion) {
Set<String> classNames = null;
ClassLoader loader = Thread.currentThread().getContextClassLoader();
String packagePath = packageName.replace(".", "/");
URL url = loader.getResource(packagePath);
if (url != null) {
String protocol = url.getProtocol();
if (FILE_STR.equals(protocol)) {
classNames = getClassNameFromDir(url.getPath(), packageName, isRecursion);
} else if (JAR_STR.equals(protocol)) {
JarFile jarFile = null;
try{
jarFile = ((JarURLConnection) url.openConnection()).getJarFile();
} catch(Exception e){
e.printStackTrace();
}
if(jarFile != null){
getClassNameFromJar(jarFile.entries(), packageName, isRecursion);
}
}
} else {
/*從所有的jar包中查找包名*/
classNames = getClassNameFromJars(((URLClassLoader)loader).getURLs(), packageName, isRecursion);
}
return classNames;
}
/**
* 從項目文件獲取某包下所有類
* @param filePath 文件路徑
* @param isRecursion 是否遍歷子包
* @return 類的完整名稱
*/
private static Set<String> getClassNameFromDir(String filePath, String packageName, boolean isRecursion) {
Set<String> className = new HashSet<>();
File file = new File(filePath);
File[] files = file.listFiles();
if(files==null){return className;}
for (File childFile : files) {
if (childFile.isDirectory()) {
if (isRecursion) {
className.addAll(getClassNameFromDir(childFile.getPath(), packageName+"."+childFile.getName(), isRecursion));
}
} else {
String fileName = childFile.getName();
if (fileName.endsWith(".class") && !fileName.contains("$")) {
className.add(packageName+ "." + fileName.replace(".class", ""));
}
}
}
return className;
}
private static Set<String> getClassNameFromJar(Enumeration<JarEntry> jarEntries, String packageName, boolean isRecursion){
Set<String> classNames = new HashSet<>();
while (jarEntries.hasMoreElements()) {
JarEntry jarEntry = jarEntries.nextElement();
if(!jarEntry.isDirectory()){
String entryName = jarEntry.getName().replace("/", ".");
if (entryName.endsWith(".class") && !entryName.contains("$") && entryName.startsWith(packageName)) {
entryName = entryName.replace(".class", "");
if(isRecursion){
classNames.add(entryName);
} else if(!entryName.replace(packageName+".", "").contains(".")){
classNames.add(entryName);
}
}
}
}
return classNames;
}
/**
* 從所有jar中搜索該包,並獲取該包下所有類
* @param urls URL集合
* @param packageName 包路徑
* @param isRecursion 是否遍歷子包
* @return 類的完整名稱
*/
private static Set<String> getClassNameFromJars(URL[] urls, String packageName, boolean isRecursion) {
Set<String> classNames = new HashSet<>();
for (URL url : urls) {
String classPath = url.getPath();
//不必搜索classes文件夾
if (classPath.endsWith("classes/")) {
continue;
}
JarFile jarFile = null;
try {
jarFile = new JarFile(classPath.substring(classPath.indexOf("/")));
} catch (IOException e) {
e.printStackTrace();
}
if (jarFile != null) {
classNames.addAll(getClassNameFromJar(jarFile.entries(), packageName, isRecursion));
}
}
return classNames;
}
}
- 創建自定義注解類EnableSelfBean
/**
* 自定義注解類,將某個包下的所有類自動加載到spring 容器中,不管有沒有注解,並打印出
* @author zhangcanlong
* @since 2019/2/14 10:42
**/
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Import(SelfEnableAutoConfig.class)
public @interface EnableSelfBean {
//傳入包名
String[] packages() default "";
}
- 創建啟動類SpringBootEnableApplication
@SpringBootApplication
@EnableSelfBean(packages = "com.kanlon.entity")
public class SpringBootEnableApplication {
@Autowired
Role abc;
public static void main(String[] args) {
ConfigurableApplicationContext context = SpringApplication.run(SpringBootEnableApplication.class, args);
//打印出所有spring中注冊的bean
String[] allBeans = context.getBeanDefinitionNames();
for(String bean:allBeans){
System.out.println(bean);
}
System.out.println("已注冊Role:"+context.getBean(Role.class));
SpringBootEnableApplication application = context.getBean(SpringBootEnableApplication.class);
System.out.println("Role的測試方法:"+application.abc.test());
}
}
啟動類測試的一些感悟:重新復習了回了spring的一些基礎東西,如:
- @Autowired是默認通過by type(即類對象)得到注冊的類,如果有多個實現才使用by name來確定。
- 所有注冊的類的信息存儲在ApplicationContext中,可以通過ApplicationContext得到注冊類,這個是很基礎的,但是真的很久沒看,沒想到竟然又忘記了。
- Spring boot中如果@ComponentScan沒有,則默認是指掃描當前啟動類所在的包里的對象。
自定義Enable注解源碼地址:https://github.com/KANLON/practice/tree/master/spring-boot-enable
參考:
- http://tangxiaolin.com/learn/show?id=402881d2648c88cc01648c89d8730001
- SpringBoot @Enable* 注解 https://segmentfault.com/a/1190000015188776
- 獲取指定包名下的所有類的類名(全名) https://my.oschina.net/cnlw/blog/299265)
- Spring-Boot之@Enable*注解的工作原理 https://www.jianshu.com/p/3da069bd865c
- Spring源碼解析------@Import注解解析與ImportSelector,ImportBeanDefinitionRegistrar以及DeferredImportSelector區別 https://www.xiaoquan.work/articles/2020/01/03/1578016154544.html
- @import和@Bean的區別,以及ImportSelector和ImportBeanDefinitionRegistrar兩個接口的簡單實用 https://blog.csdn.net/qq_22701869/article/details/102561494