本文共 9372 字,大约阅读时间需要 31 分钟。
转:
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
@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引入了EnableDiscoveryClientImportSelector,通过查看其继承实现图
可以看到其最终实现了ImportSelector接口,查看其具体实现源码
知道其先得到父类注册的bean类,然后如果在查看AnnotationMetadata中是否存在autoRegister,是否需要注册该类,如果存在,则继续将org.springframework.cloud.client.serviceregistry.AutoServiceRegistrationConfiguration
该类返回,加入到spring容器中。
通过查看@Enable*源码,我们可以清楚知道其实现自动配置的方式的底层就是通过@Import注解引入相关配置类,然后再在配置类将所需的bean注册到spring容器中和实现组件相关处理逻辑去。
在这里我们利用@Import和ImportSelector动手自定义一个自己的EnableSelfBean。该Enable注解可以将某些包下的所有类自动注册到spring容器中,对于一些实体类的项目很多的情况下,可以考虑一下通过这种方式将某包下所有类自动加入到spring容器,不再需要每个类再加上@Component等注解。
/** * 测试自己的自动注解的实体类 * @author zhangcanlong * @since 2019/2/14 10:41 **/public class Role { public String test(){ return "hello"; }}
/** * 自己的定义的自动注解配置类 * @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 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 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 **/@Componentpublic class ClassUtils { private static final String FILE_STR= "file"; private static final String JAR_STR = "jar"; /** * 获取某包下所有类 * @param packageName 包名 * @param isRecursion 是否遍历子包 * @return 类的完整名称 */ public static Set getClassName(String packageName, boolean isRecursion) { Set 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 getClassNameFromDir(String filePath, String packageName, boolean isRecursion) { Set 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 getClassNameFromJar(Enumeration jarEntries, String packageName, boolean isRecursion){ Set 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 getClassNameFromJars(URL[] urls, String packageName, boolean isRecursion) { Set 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; }}
/** * 自定义注解类,将某个包下的所有类自动加载到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 "";}
@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的一些基础东西,如:
自定义Enable注解源码地址:https://github.com/KANLON/practice/tree/master/spring-boot-enable
参考:
转: