Spring Boot @Enable*注解源码解析及自定义@Enable*
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
 

Spring Boot @Enable*注解源码解析及自定义@Enable*的更多相关文章
- Spring Boot系列(三):Spring Boot整合Mybatis源码解析
		
一.Mybatis回顾 1.MyBatis介绍 Mybatis是一个半ORM框架,它使用简单的 XML 或注解用于配置和原始映射,将接口和Java的POJOs(普通的Java 对象)映射成数据库中的记 ...
 - Spring Boot自动配置源码解析(基于Spring Boot 2.0.2.RELEASE)
		
在Spring Boot官方介绍中,首一段话是这样的(如下图).我们可以大概了解到其所表达的含义:我们可以利用Spring Boot写很少的配置来创建一个非常方便的基于Spring整合第三方类库的单体 ...
 - Spring Boot入门,源码解析
		
目录 1.Spring Boot简介 2.微服务 3.Spring Boot HelloWorld 3.1 创建一个Maven工程 3.2 导入依赖Spring Boot相关的依赖 3.3 编写一个主 ...
 - SpringBoot的条件注解源码解析
		
SpringBoot的条件注解源码解析 @ConditionalOnBean.@ConditionalOnMissingBean 启动项目 会在ConfigurationClassBeanDefini ...
 - Spring Boot 自动配置 源码分析
		
Spring Boot 最大的特点(亮点)就是自动配置 AutoConfiguration 下面,先说一下 @EnableAutoConfiguration ,然后再看源代码,到底自动配置是怎么配置的 ...
 - spring boot 2.0 源码分析(一)
		
在学习spring boot 2.0源码之前,我们先利用spring initializr快速地创建一个基本的简单的示例: 1.先从创建示例中的main函数开始读起: package com.exam ...
 - spring boot 2.0 源码分析(二)
		
在上一章学习了spring boot 2.0启动的大概流程以后,今天我们来深挖一下SpringApplication实例变量的run函数. 先把这段run函数的代码贴出来: /** * Run the ...
 - spring boot 2.0 源码分析(四)
		
在上一章的源码分析里,我们知道了spring boot 2.0中的环境是如何区分普通环境和web环境的,以及如何准备运行时环境和应用上下文的,今天我们继续分析一下run函数接下来又做了那些事情.先把r ...
 - spring boot 2.0 源码分析(三)
		
通过上一章的源码分析,我们知道了spring boot里面的listeners到底是什么(META-INF/spring.factories定义的资源的实例),以及它是创建和启动的,今天我们继续深入分 ...
 
随机推荐
- 软碟通制作win10镜像,无法打开install.wim的问题
			
打开软碟通,单击左上角"文件"→"打开",选择.iso文件的存放目录,再选择.iso映像文件打开,即可看到映像文件全部加载到UltraISO了,如下图. 将 ...
 - ORACLE查找占用临时表空间多的SESSION
			
需要使用SYS用户登录查看 /* Formatted on 2020/12/30 上午 11:17:12 (QP5 v5.163.1008.3004) */ SELECT k.inst_id &quo ...
 - Java集合List-差集、并集、交集
			
Java集合List的差集.并集.交集 转载于:https://www.cnblogs.com/qlqwjy/p/9812919.html 一.List的差集 @Test public void te ...
 - Windows下的python虚拟环境设置
			
Windows下的python虚拟环境设置: virtualenv 在python开发中,我们可能会遇到一种情况:就是当前的项目依赖的是某一个版本,但是另一个项目依赖的是另一个版本,这样就会造成依赖冲 ...
 - linux登陆欢迎信息及命令提示符修改
			
登录信息修改 登陆信息显示数据 : /etc/issue and /etc/motd 登陆终端机的时候,会有几行提示的字符串,这些设置在/etc/issue里面可以修改,提示内容在/etc/motd中 ...
 - Vue基础之Vue的模板语法
			
Vue基础之Vue的模板语法 数据绑定 01 数据绑定最常见的形式就是使用插值表达式(两个大括号!)[也就是小胡子语法!mustache] <body> <!-- Vue.js的应用 ...
 - (12)-Python3之--openpyxl模块
			
1.安装 pip install openpyxl 2.Excel操作的流程 1.打开excel,进入工作薄 workbook2.选择表单 Sheet 3.单元格 Cell4.读写操作 3.Exce ...
 - Axure RP 9版本最新版授权码和密钥 亲测可用
			
分享Axure RP 9版本最新版授权码和密钥 亲测可用 声明:以下资源的获取来源为网络收集,仅供学习参考,不作商业用途,如有侵权请联系博主删除,谢谢! 自新的Axure RP 9.0 Beta版发布 ...
 - ECMAScript6常用新特性总结
			
一.let声明变量 1.基本用法: ES6 新增了let命令,用来声明变量.它的用法类似于var,但是所声明的变量,只在let命令所在的代码块内有效. 如下代码: { let a = 10; var ...
 - 洛谷P3850 书架
			
题目描述 Knuth先生家里有个精致的书架,书架上有N本书,如今他想学到更多的知识,于是又买来了M本不同的新书.现在他要把新买的书依次插入到书架中,他已经把每本书要插入的位置标记好了,并且相应的将它们 ...