开启掘金成长之旅!这是我参与「掘金日新计划 · 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