实现自动装配的一个小demo及其原理

2020-07-18  本文已影响0人  guessguess

首先说一下情景,很多时候我们可能会做一些小组件,如果不使用自动装配的话,那么是有点麻烦的,需要手动指定扫描的路径,才可以交给spring去做处理。那么问题来了,有没有一种比较简单的方式可以做到,不用去指定扫描路径,仅仅通过添加maven的依赖就解决了呢。那就是通过spring的自动装配机制。

首先先创建一个工程,作为被依赖的组件,后续再创建一个工程使用这个组件。

组件工程的结构如下图所示


截屏2020-07-18上午11.25.49.png

maven依赖

    <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>2.0.5.RELEASE</version>
    </parent>
    
    <dependencies>
        <!-- https://mvnrepository.com/artifact/org.springframework.boot/spring-boot-starter-web -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter</artifactId>
        </dependency>
    </dependencies>

配置类代码

@Configuration
@ComponentScan
public class Configutation {

}

服务的代码

@Service
public class HelloService {
    public String sayHello() {
        return "auto-config hello";
    }
}

随后在resource目录下创建MATE-INF目录,并且放置一个spring.factories文件
spring.factories的内容,其实就是我们自定义需要被自动装配的配置类

org.springframework.boot.autoconfigure.EnableAutoConfiguration=\
auto.Configutation

maven install一下

随后我们来创建一个工程,用于测试这个组件是否可以使用
首先添加依赖

<project xmlns="http://maven.apache.org/POM/4.0.0"
    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>
    <groupId>com.gee</groupId>
    <artifactId>sb-demo</artifactId>
    <version>0.0.1-SNAPSHOT</version>

    <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>2.0.5.RELEASE</version>
    </parent>
    <dependencies>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter</artifactId>
        </dependency>
        <!-- 开启热部署 -->
        <!-- https://mvnrepository.com/artifact/org.springframework.boot/spring-boot-devtools -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-devtools</artifactId>
            <optional>true</optional>
        </dependency>

        <!-- 单元测试 -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
            <scope>test</scope>
        </dependency>
        <!-- 自定义的组件-->
        <dependency>
            <groupId>com.gee</groupId>
            <artifactId>my-auto-configuration</artifactId>
            <version>0.0.1-SNAPSHOT</version>
        </dependency>
    </dependencies>

    <build>
        <plugins>
            <!-- 指定maven编译的jdk的版本 -->
            <plugin>
                <groupId>org.apache.maven.plugins</groupId>
                <artifactId>maven-compiler-plugin</artifactId>
                <configuration>
                    <source>1.8</source>
                    <target>1.8</target>
                    <encoding>UTF-8</encoding>
                </configuration>
            </plugin>
        

            <!-- 打包成springboot专用的jar包,指定入口信息等等 -->
            <plugin>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-maven-plugin</artifactId>
            </plugin>

            <plugin>
                <groupId>org.apache.maven.plugins</groupId>
                <artifactId>maven-surefire-plugin</artifactId>
                <configuration>
                    <skipTests>true</skipTests>
                </configuration>
            </plugin>

        </plugins>
    </build>
</project>

配置类

@SpringBootApplication
public class Config {
    public static void main(String args[]) {
        SpringApplication.run(Config.class, args);
    }
}

获取bean的一个工具类,必须位于配置类的包/子包路径下,因为没有指定ComponentScan的包路径,默认就是配置类的包/子包路径下

@Component
public class ApplicationContextUtils implements ApplicationContextAware{
    private ApplicationContext context;
    public static ApplicationContext APP_CONTEXT;
    public <T> T getBean(Class<T> t) {
        return context.getBean(t);
    }
    
    public String[] getBeanNames() {
        return context.getBeanDefinitionNames();
    }

    @Override
    public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
        this.context = applicationContext;
        APP_CONTEXT = applicationContext;
    }

    public ApplicationContext getContext() {
        return context;
    }

    public void setContext(ApplicationContext context) {
        this.context = context;
    }
}

在src/test/java下,创建一个包,顺便写一个测试的启动类,以及对应的测试方法,测试一下

@RunWith(SpringRunner.class)
@SpringBootTest(classes={Config.class})
public class ConfigTest {
    @Test
    public void getProducer() {
        System.out.println(ApplicationContextUtils.APP_CONTEXT.getBean(HelloService.class).sayHello());

    }
}

输出结果如下

auto-config hello
2020-07-18 11:38:50.332  INFO 4859 --- [      Thread-10] o.s.w.c.s.GenericWebApplicationContext   : Closing org.springframework.web.context.support.GenericWebApplicationContext@306e95ec: startup date [Sat Jul 18 11:38:22 CST 2020]; root of context hierarchy
2020-07-18 11:38:50.342  WARN 4859 --- [      Thread-10] o.s.b.f.support.DisposableBeanAdapter    : Invocation of destroy method failed on bean with name 'rocketMQTemplate': java.lang.IllegalStateException: Shutdown in progress
2020-07-18 11:38:50.342  WARN 4859 --- [      Thread-10] o.s.b.f.support.DisposableBeanAdapter    : Destroy method 'shutdown' on bean with name 'defaultMQProducer' threw an exception: java.lang.IllegalStateException: Shutdown in progress

说明这种用法是可行的。
那么下面来说说原理。这里就不说的太详细了,下面是之前对自动装配原理的一些分析
https://www.jianshu.com/p/3ae65b2b4087
https://www.jianshu.com/p/f6966f2c7d8d

那么为什么组件中的spring.factories必须得以org.springframework.boot.autoconfigure.EnableAutoConfiguration作为key呢?
答案也是很简单的。spring给自动装配定义了好几种类型,而org.springframework.boot.autoconfigure.EnableAutoConfiguration是其中一种类型。

先来看看流程吧。


截屏2020-07-18上午11.53.21.png

从流程我们可以看出,入口还是在容器刷新的时候,通过执行beanDefinitionRegistryPostProcesor,完成自动装配。

那么我们看看执行的关键代码。注释中,会讲解每个步骤的大致作用。

class ConfigurationClassParser {
    private void processDeferredImportSelectors() {
        //如果成员变量中的导入选择器为null,则说明不需要进行处理
        //成员中的导入选择器其实是在处理@import标签时,判断如果是importSelector类型,则会放置到成员变量中。
        //@SpringBootApplication->@EnableAutoConfiguration->@Import(AutoConfigurationImportSelector.class)
        //所以实际上,这里的导入选择器只会是AutoConfigurationImportSelector
        List<DeferredImportSelectorHolder> deferredImports = this.deferredImportSelectors;
        this.deferredImportSelectors = null;
        if (deferredImports == null) {
            return;
        }
        //先进行排序
        deferredImports.sort(DEFERRED_IMPORT_COMPARATOR);
        //这里其实就是组->组对应的导入选择器,grouping封装了group以及List<ImportSelector>
        Map<Object, DeferredImportSelectorGrouping> groupings = new LinkedHashMap<>();
        Map<AnnotationMetadata, ConfigurationClass> configurationClasses = new HashMap<>();
        //根据importSelector中的group进行分组
        for (DeferredImportSelectorHolder deferredImport : deferredImports) {
            //获取该导入选择器的组
            Class<? extends Group> group = deferredImport.getImportSelector().getImportGroup();
            //若该分组不存在则进行创建
            DeferredImportSelectorGrouping grouping = groupings.computeIfAbsent(
                    (group != null ? group : deferredImport),
                    key -> new DeferredImportSelectorGrouping(createGroup(group)));
            //将该导入选择器加入到对应的分组中
            grouping.add(deferredImport);
            configurationClasses.put(deferredImport.getConfigurationClass().getMetadata(),
                    deferredImport.getConfigurationClass());
        }
        //遍历执行每个分组,由于这里只有AutoConfiurationImportSelector所以我们只关注这个类即可。
        for (DeferredImportSelectorGrouping grouping : groupings.values()) {
            //获取需要自动装配的所有类,逐个去处理,那么如何获取则是关键。
            grouping.getImports().forEach(entry -> {
                ConfigurationClass configurationClass = 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);
                }
            });
        }
    }
}

如何获取自动装配的类,定位到最后,与预期一致AutoConfigurationGroup,AutoConfigurationImportSelector是核心

public class AutoConfigurationImportSelector
        implements DeferredImportSelector, BeanClassLoaderAware, ResourceLoaderAware,
        BeanFactoryAware, EnvironmentAware, Ordered {

      private static class AutoConfigurationGroup implements DeferredImportSelector.Group,
            BeanClassLoaderAware, BeanFactoryAware, ResourceLoaderAware {

        @Override
        //AutoConfigurationGroup对应的importSelector只有AutoConfigurationImportSelector,所以看看AutoConfigurationImportSelector的selectImports方法即可。
        public void process(AnnotationMetadata annotationMetadata,
                DeferredImportSelector deferredImportSelector) {
            String[] imports = deferredImportSelector.selectImports(annotationMetadata);
            for (String importClassName : imports) {
                this.entries.put(importClassName, annotationMetadata);
            }
        }
}

AutoConfigurationImportSelector如何找到自动装配的类,看下面方法

    protected List<String> getCandidateConfigurations(AnnotationMetadata metadata,
            AnnotationAttributes attributes) {
        //获取配置类的方法,获取key=EnableAutoConfiguration的className对应的所有配置类
        List<String> configurations = SpringFactoriesLoader.loadFactoryNames(
                getSpringFactoriesLoaderFactoryClass(), getBeanClassLoader());
        Assert.notEmpty(configurations,
                "No auto configuration classes found in META-INF/spring.factories. If you "
                        + "are using a custom packaging, make sure that file is correct.");
        return configurations;
    }
    
    //这个方法其实就帮我们指定了,我们要获取spring.factories内容中key为EnableAutoConfiguration的className的内容
    protected Class<?> getSpringFactoriesLoaderFactoryClass() {
        return EnableAutoConfiguration.class;
    }

SpringFactoriesLoader的工作原理

    //这个方法其实就是加载所有的spring.factories,获取所有的文件内容,并且生成一个map,然后根据传进来的 factoryClassName作为key,取factoryClassName对应的values,我们刚刚传进来的factoryClassName不就是org.springframework.boot.autoconfigure.EnableAutoConfiguration吗?这个key也跟我们自定义的spring.factories的key一样,所以key是必须使用spring这里规定的.如果我们自己随意定义一个key,则并不会被spring处理。
    public static List<String> loadFactoryNames(Class<?> factoryClass, @Nullable ClassLoader classLoader) {
        String factoryClassName = factoryClass.getName();
        return loadSpringFactories(classLoader).getOrDefault(factoryClassName, Collections.emptyList());
    }

    //这个类就是加载meta-inf下的spring.factories的内容,并且生成map
    private static Map<String, List<String>> loadSpringFactories(@Nullable ClassLoader classLoader) {
        MultiValueMap<String, String> result = cache.get(classLoader);
        if (result != null) {
            return result;
        }

        try {
            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 properties = PropertiesLoaderUtils.loadProperties(resource);
                for (Map.Entry<?, ?> entry : properties.entrySet()) {
                    List<String> factoryClassNames = Arrays.asList(
                            StringUtils.commaDelimitedListToStringArray((String) entry.getValue()));
                    result.addAll((String) entry.getKey(), factoryClassNames);
                }
            }
            cache.put(classLoader, result);
            return result;
        }
        catch (IOException ex) {
            throw new IllegalArgumentException("Unable to load factories from location [" +
                    FACTORIES_RESOURCE_LOCATION + "]", ex);
        }
    }
上一篇下一篇

猜你喜欢

热点阅读