Spring底层原理分析-五(beanFactory后处理器)

121 阅读7分钟

开启掘金成长之旅!这是我参与「掘金日新计划 · 12 月更文挑战」的第1天,点击查看活动详情

ConfigurationClassPostProcessor

public class ApplicationDemo01 {
    public static void main(String[] args) {
        GenericApplicationContext context = new GenericApplicationContext();
        context.registerBean("config", Config.class);
        context.refresh();
        for (String name : context.getBeanDefinitionNames()) {
            System.out.println(name);
        }
        context.close();
    }
}

@Configuration
@ComponentScan("com.component")
public class Config {
    @Bean
    public Bean3 bean3(){
        return new Bean3();
    }
    @Bean
    public SqlSessionFactoryBean sqlSessionFactoryBean(DataSource dataSource){
        SqlSessionFactoryBean sqlSessionFactoryBean = new SqlSessionFactoryBean();
        sqlSessionFactoryBean.setDataSource(dataSource);
        return sqlSessionFactoryBean;
    }
    @Bean(initMethod = "init")
    public DruidDataSource dataSource(){
        DruidDataSource druidDataSource = new DruidDataSource();
        druidDataSource.setUrl("jdbc:mysql://localhost:3306/test");
        druidDataSource.setUsername("root");
        druidDataSource.setPassword("123456");
        return druidDataSource;
    }
}

//Bean1和Bean2在com.component包下
@Component
public class Bean1 {}

public class Bean2 {}

//Bean3不在com.component包下
public class Bean3 {}

  这里创建Config类,目的为注册三个Bean的实例,并且在main方法中循环打印。我们查看输出结果

输出结果:
config

  根据输出结果看到并没有达到预期,只加载了config配置类本身的实例。Bean1的包扫描方式和Bean3的@Bean注解方式的实例都没有加载到容器中。这里就是需要一个后处理器来解析这些。这个后处理器是一个属于BeanFactory的处理器,可以解析@ComponentScan、@Bean、@Import、@ImportResource注解,添加代码如下。

context.registerBean(ConfigurationClassPostProcessor.class);
输出结果:
config
bean1
bean3
sqlSessionFactoryBean
dataSource

  有了上面例子,再结合上节Bean后处理器的使用可以这么理解。bean后处理器是用来处理Bean被实例后内部的额外处理逻辑,比如额外注入其他的Bean等。而BeanFactory后处理器,正如上面例子的功能,用来对Bean的生产做一定的增强,比如上面例子中通过对注解的解析、对包的扫描来增加对Bean获取来源。类似的BeanFactory后处理器还有MapperScannerConfigurer等,通过类名可以看出是用来解析mapper文件的注解,这个也是mybaits底层使用的后处理器之一。

MapperScannerConfigurer

  当与Mybatis做结合的时候,就可以使用这个后处理器。从名字看,它不像其他后处理器似的,不以PostProcessor结尾,但是看继承关系发现它和ConfigurationClassPostProcessor一样,都继承了BeanDefinitionRegistryPostProcessor,同样是为Bean定义的注册来服务的后处理器。那么它的作用就是用来解析被@Mapper注解的相关类。以及@MapperScanner底层也是用了该处理器。

context.registerBean(MapperScannerConfigurer.class,bd -> {
    bd.getPropertyValues().add("basePackage","com.a05");
});

  这里比上面的ConfigurationClassPostProcessor多了一个属性,根据代码可以看出,这也是我们所熟悉的MapperScanner所扫描的包位置的一个指定。

ConfigurationClassPostProcessor原理解析

  这里模拟ConfigurationClassPostProcessor的实现原理。先去掉上面添加的两个后处理器的注入代码,添加如下代码。

//获取Config类上的@ComponentScan注解
ComponentScan componentScan =AnnotationUtils.findAnnotation(Config.class, ComponentScan.class);
//判断是否含有@ComponentScan注解
if(componentScan!=null){
    //循环获取@ComponentScan注解配置的包路径都有哪些
    for (String basePackage : componentScan.basePackages()) {
        System.out.println(basePackage);//com.a05.component
        //根据包路径转换为资源通配符加斜杠路径形式
        String path = "classpath*:" + basePackage.replace(".", "/") + "/**/*.class";
        System.out.println(path);//classpath*:com/a05/component/**/*.class    
    }
}

  根据输出结果可以看出,正是上面配置的包路径信息。这里回顾第一节中容器的四大作用之一:通配符匹配资源能力。

Resource[] resources = context.getResources(path);
for (Resource resource : resources) {
    System.out.println(resource);
}

输出结果:
file [D:\workSpace\...\Bean1.class] 
file [D:\workSpace\...\Bean2.class] 

  这里在Bean1同目录下再增加一个空白的Bean2,不过Bean2不加任何注解。这时候发现打印结果将Bean1和Bean2的磁盘路径都打印了出来。因为这两个Bean都在需要扫描的包下放着。那下一步,可能有小伙伴就提前猜到了。那就是继续筛选带@Component注解的Bean,毕竟咱们Bean2上没有加任何注解,怎么能让它加载到容器中,继续修改上面的代码。

Resource[] resources = context.getResources(path);
//该工厂类用来解析资源信息
CachingMetadataReaderFactory factory = new CachingMetadataReaderFactory();
for (Resource resource : resources) {
    System.out.println(resource);
    //获取具体某个资源的信息
    MetadataReader metadataReader = factory.getMetadataReader(resource);
    //获取类信息
    ClassMetadata classMetadata = metadataReader.getClassMetadata();
    String className = classMetadata.getClassName();
    System.out.println(className);
    //获取注解信息
    AnnotationMetadata annotationMetadata = metadataReader.getAnnotationMetadata();
    boolean a = annotationMetadata.hasAnnotation(Component.class.getName());
    boolean b = annotationMetadata.hasMetaAnnotation(Component.class.getName());
    System.out.println(a);
    System.out.println(b);
}


输出结果:
file [D:\workSpace\...\Bean1.class]
com.a05.component.Bean1
true
false
file [D:\workSpace\...\Bean2.class]
com.a05.component.Bean2
false
false

  根据打印得出结果,Bean2的注解信息里并没有@Component注解,hasMetaAnnotation方法不仅会检查@Component注解本身,还会检查是否包含该注解的派生注解,如@Controller、@Service等,所以检查Bean1的时候该项会返回false。根据经验,这两个条件只要满足其一,就应该被定义加入容器中。

//用来生成beanName后续会用到,这两行写再循环之外,不用每次循环都创建新的对象。
AnnotationBeanNameGenerator generator = new AnnotationBeanNameGenerator();
DefaultListableBeanFactory beanFactory = context.getDefaultListableBeanFactory();

//由上面返回的a和b包含判断
if(a||b){
    //满足上面描述的条件之一,则创建该Bean的定义对象。
    AbstractBeanDefinition beanDefinition =
            BeanDefinitionBuilder.genericBeanDefinition(className).getBeanDefinition();
    //生成beanName
    String beanName = generator.generateBeanName(beanDefinition, beanFactory);
    //将Bean注册到Bean工厂
    beanFactory.registerBeanDefinition(beanName,beanDefinition);
}

  文章第一段代码中,循环打印了容器中的所有Bean,没有添加后处理器的时候只有Config被加入的容器,现在再次打印一次。

输出结果:
config
bean1

  至此,满足容器加载条件的Bean1被成功的加入到了容器之中

自定义后处理器

  上面我们通过调用Spring内部的api的方式,模拟了ConfigurationClassPostProcessor的基础实现原理,这里我们再做一个优化,把我们写的逻辑自定义为一个后处理器。这样,我们和添加其他后处理器一样,直接注册到容器中就行了。

public class ComponentScanPostProcessor implements BeanFactoryPostProcessor {
    @Override
    public void postProcessBeanFactory(ConfigurableListableBeanFactory configurableListableBeanFactory) throws BeansException {
        try {
            //获取Config类上的@ComponentScan注解
            ComponentScan componentScan =
                    AnnotationUtils.findAnnotation(Config.class, ComponentScan.class);
            //判断是否含有@ComponentScan注解
            if (componentScan != null) {
                AnnotationBeanNameGenerator generator = new AnnotationBeanNameGenerator();
                CachingMetadataReaderFactory factory = new CachingMetadataReaderFactory();
                //循环获取@ComponentScan注解配置的包路径都有哪些
                for (String basePackage : componentScan.basePackages()) {
                    System.out.println(basePackage);//com.a05.component
                    String path = "classpath*:" + basePackage.replace(".", "/") + "/**/*.class";
                    System.out.println(path);
                    //该类中无法获得容器本身,这里换方式来获取资源信息
                    Resource[] resources = new PathMatchingResourcePatternResolver().getResources(path);
                    for (Resource resource : resources) {
                        System.out.println(resource);
                        MetadataReader metadataReader = factory.getMetadataReader(resource);
                        ClassMetadata classMetadata = metadataReader.getClassMetadata();
                        String className = classMetadata.getClassName();
                        System.out.println(className);
                        AnnotationMetadata annotationMetadata = metadataReader.getAnnotationMetadata();
                        boolean a = annotationMetadata.hasAnnotation(Component.class.getName());
                        boolean b = annotationMetadata.hasMetaAnnotation(Component.class.getName());
                        System.out.println(a);
                        System.out.println(b);
                        if(a||b){
                            AbstractBeanDefinition beanDefinition =
                                    BeanDefinitionBuilder.genericBeanDefinition(className).getBeanDefinition();
                                    //实现方法的BeanFactory是个接口,转换为实现类。
                            if(configurableListableBeanFactory instanceof DefaultListableBeanFactory){
                                DefaultListableBeanFactory beanFactory = (DefaultListableBeanFactory)configurableListableBeanFactory;
                                String beanName = generator.generateBeanName(beanDefinition, beanFactory);
                                beanFactory.registerBeanDefinition(beanName,beanDefinition);
                            }
                        }
                    }
                }
            }
        } catch (IOException e) {
            throw new RuntimeException(e);
        }
    }
}

  这里做了两个改动,一个是BeanFactory的获取,实现方法中有BeanFactory的参数,但是由于是接口参数,需要改用实现类,这里做一个转换。还有一个是通过容器获取资源信息那里,后处理器类中无法获得容器,改用PathMatchingResourcePatternResolver类来获取资源信息。和最初的注册后处理器方式一样,将我们自定义的后处理器注册到容器中,结果就和上面写在一起的是一样的。

@Bean解析

  接着来解析Config类中的@Bean注解,根据前面解析@Component注解的经验,我们还是先获取资源信息中的类注解信息。

CachingMetadataReaderFactory factory = new CachingMetadataReaderFactory();
MetadataReader metadataReader = factory.getMetadataReader(new ClassPathResource("com/a05/Config.class"));
//获取注解信息中的方法注解信息,并筛选@Bean注解的方法信息
Set<MethodMetadata> annotatedMethods =
        metadataReader.getAnnotationMetadata().getAnnotatedMethods(Bean.class.getName());
for (MethodMetadata annotatedMethod : annotatedMethods) {
    System.out.println(annotatedMethod);
    BeanDefinitionBuilder builder = BeanDefinitionBuilder.genericBeanDefinition();
    builder.setFactoryMethodOnBean(annotatedMethod.getMethodName(),"config");
    AbstractBeanDefinition beanDefinition = builder.getBeanDefinition(); 
    context.getDefaultListableBeanFactory().registerBeanDefinition(annotatedMethod.getMethodName(),beanDefinition);
    
}

  在循环里面继续用上面所说的思路,根据获得的方法创建具体的Bean定义,不过看这段代码的话,定义的方式好像和上面不太一样,因为这里的@Bean使用的是工厂方法的形式创建的Bean。所以这里不需要和上面的@Component注解一样,需要填入该Bean类的名字。而是要指定具体的方法和该方法所在的Bean,也就是config。
  直接说结果,这里启动程序会报错,为什么?回到文章最上面,仔细观看Config类,发现里面有三个@Bean注解,一个普通的Bean,两个是数据源相关的Bean。原因就在第二个SqlSessionFactoryBean上,这个工厂Bean有参数,来源是第三个DruidDataSource的Bean,难到是加载顺序问题。并不是,我们还要做一个处理,就是处理参数的Bean注入。参数的Bean是靠自动装配完成的,我们需要手动添加该逻辑。

builder.setAutowireMode(AbstractBeanDefinition.AUTOWIRE_CONSTRUCTOR);

  在上面添加自动装配模式,就可以完整加载所有的Bean实例了。

输出结果:
config
bean3
sqlSessionFactoryBean
dataSource

image.png