Spring Boot @EnableAutoConfiguration解析
刚做后端开发的时候,最早接触的是基础的spring,为了引用二方包提供bean,还需要在xml中增加对应的包<context:component-scan base-package="xxx" />
或者增加注解@componentscan({ "xxx"})
。当时觉得挺urgly的,但也没有去研究有没有更好的方式。
直到接触spring boot 后,发现其可以自动引入二方包的bean。不过一直没有看这块的实现原理。直到最近面试的时候被问到。所以就看了下实现逻辑。
使用姿势
讲原理前先说下使用姿势。
在project a中定义一个bean。
package com.wangzhi; import org.springframework.stereotype.service; @service public class dog { }
并在该project的resources/meta-inf/
下创建一个叫spring.factories
的文件,该文件内容如下
org.springframework.boot.autoconfigure.enableautoconfiguration=com.wangzhi.dog
然后在project b中引用project a的jar包。
projecta代码如下:
package com.wangzhi.springbootdemo; import org.springframework.boot.springapplication; import org.springframework.boot.autoconfigure.enableautoconfiguration; import org.springframework.boot.autoconfigure.springbootapplication; import org.springframework.context.configurableapplicationcontext; import org.springframework.context.annotation.componentscan; @enableautoconfiguration public class springbootdemoapplication { public static void main(string[] args) { configurableapplicationcontext context = springapplication.run(springbootdemoapplication.class, args); system.out.println(context.getbean(com.wangzhi.dog.class)); } }
打印结果:
com.wangzhi.dog@3148f668
原理解析
总体分为两个部分:一是收集所有spring.factories
中enableautoconfiguration
相关bean的类,二是将得到的类注册到spring容器中。
收集bean定义类
在spring容器启动时,会调用到autoconfigurationimportselector#getautoconfigurationentry
protected autoconfigurationentry getautoconfigurationentry( autoconfigurationmetadata autoconfigurationmetadata, annotationmetadata annotationmetadata) { if (!isenabled(annotationmetadata)) { return empty_entry; } // enableautoconfiguration注解的属性:exclude,excludename等 annotationattributes attributes = getattributes(annotationmetadata); // 得到所有的configurations list<string> configurations = getcandidateconfigurations(annotationmetadata, attributes); // 去重 configurations = removeduplicates(configurations); // 删除掉exclude中指定的类 set<string> exclusions = getexclusions(annotationmetadata, attributes); checkexcludedclasses(configurations, exclusions); configurations.removeall(exclusions); configurations = filter(configurations, autoconfigurationmetadata); fireautoconfigurationimportevents(configurations, exclusions); return new autoconfigurationentry(configurations, exclusions); } getcandidateconfigurations会调用到方法loadfactorynames: public static list<string> loadfactorynames(class<?> factoryclass, @nullable classloader classloader) { // factoryclassname为org.springframework.boot.autoconfigure.enableautoconfiguration string factoryclassname = factoryclass.getname(); // 该方法返回的是所有spring.factories文件中key为org.springframework.boot.autoconfigure.enableautoconfiguration的类路径 return loadspringfactories(classloader).getordefault(factoryclassname, collections.emptylist()); } public static final string factories_resource_location = "meta-inf/spring.factories"; private static map<string, list<string>> loadspringfactories(@nullable classloader classloader) { multivaluemap<string, string> result = cache.get(classloader); if (result != null) { return result; } try { // 找到所有的"meta-inf/spring.factories" enumeration<url> urls = (classloader != null ? classloader.getresources(factories_resource_location) : classloader.getsystemresources(factories_resource_location)); result = new linkedmultivaluemap<>(); while (urls.hasmoreelements()) { url url = urls.nextelement(); urlresource resource = new urlresource(url); // 读取文件内容,properties类似于hashmap,包含了属性的key和value properties properties = propertiesloaderutils.loadproperties(resource); for (map.entry<?, ?> entry : properties.entryset()) { string factoryclassname = ((string) entry.getkey()).trim(); // 属性文件中可以用','分割多个value for (string factoryname : stringutils.commadelimitedlisttostringarray((string) entry.getvalue())) { result.add(factoryclassname, factoryname.trim()); } } } cache.put(classloader, result); return result; } catch (ioexception ex) { throw new illegalargumentexception("unable to load factories from location [" + factories_resource_location + "]", ex); } }
注册到容器
在上面的流程中得到了所有在spring.factories
中指定的bean的类路径,在processgroupimports
方法中会以处理@import注解一样的逻辑将其导入进容器。
public void processgroupimports() { for (deferredimportselectorgrouping grouping : this.groupings.values()) { // getimports即上面得到的所有类路径的封装 grouping.getimports().foreach(entry -> { configurationclass configurationclass = this.configurationclasses.get( entry.getmetadata()); try { // 和处理@import注解一样 processimports(configurationclass, assourceclass(configurationclass), assourceclasses(entry.getimportclassname()), false); } catch (beandefinitionstoreexception ex) { throw ex; } catch (throwable ex) { throw new beandefinitionstoreexception( "failed to process import candidates for configuration class [" + configurationclass.getmetadata().getclassname() + "]", ex); } }); } } private void processimports(configurationclass configclass, sourceclass currentsourceclass, collection<sourceclass> importcandidates, boolean checkforcircularimports) { ... // 遍历收集到的类路径 for (sourceclass candidate : importcandidates) { ... //如果candidate是importselector或importbeandefinitionregistrar类型其处理逻辑会不一样,这里不关注 // candidate class not an importselector or importbeandefinitionregistrar -> // process it as an @configuration class this.importstack.registerimport( currentsourceclass.getmetadata(), candidate.getmetadata().getclassname()); // 当作 @configuration 处理 processconfigurationclass(candidate.asconfigclass(configclass)); ... } ... }
可以看到,在第一步收集的bean类定义,最终会被以configuration
一样的处理方式注册到容器中。
end
@enableautoconfiguration
注解简化了导入了二方包bean的成本。提供一个二方包给其他应用使用,只需要在二方包里将对外暴露的bean定义在spring.factories
中就好了。对于不需要的bean,可以在使用方用@enableautoconfiguration
的exclude
属性进行排除。
本人免费整理了java高级资料,涵盖了java、redis、mongodb、mysql、zookeeper、spring cloud、dubbo高并发分布式等教程,一共30g,需要自己领取。
传送门:https://mp.weixin.qq.com/s/jzddfh-7ynudmkjt0irl8q