还不知道FactoryBean有啥用?探寻FactoryBean的究极奥义之从Spring+MyBatis扫描源码说起
万字长文警告 !建议在首页看的,点击标题进入文章页查看,好看点。
本文章又名: 兼容 Spring 体系的 java 框架实现妙计
前排先提示一波: 读我这篇文章可以没看过 MyBatis 源码,但是 Spring 源码最好是要看过的,因为很多东西我不会解释,没看过 Spring 源码的估摸着会有些懵逼。
看完这篇文章的话,基本上可以自己手写兼容Spring体系的框架了。
本文章大概能解答这些问题:
如何使用Spring提供的扫描器?
MyBatis 注解生成 mapper 的流程是怎样的?
通过注解选择配置的启用与否是如何实现的?
如何注入自己的动态代理对象到Spring IOC容器 (重点) ?
在注入自定义 BeanDefinition 到Spring IOC容器中时,FactoryBean 起到个什么作用?
先看效果,再谈实现
首先,我写的这个demo项目里只有一个dependency
<dependencies> <dependency> <groupId>org.springframework</groupId> <artifactId>spring-context</artifactId> <version>${spring.version}</version> </dependency> </dependencies>
然后,我有俩Dao接口,都提供了一个query()方法
package com.skypyb.dao; import com.skypyb.core.ann.Select; public interface OneTestDao { @Select("SELECT name FROM user WHERE id = 1") String query(); }
package com.skypyb.dao; import com.skypyb.core.ann.Select; public interface TwoTestDao { @Select("SELECT name FROM user WHERE id = 2") String query(); }
最后,这是启动类和启动配置:
package com.skypyb; import com.skypyb.config.MainConfig; import com.skypyb.dao.OneTestDao; import com.skypyb.dao.TwoTestDao; import org.springframework.context.annotation.AnnotationConfigApplicationContext; public class Application { public static void main(String[] args) { //注解方式加载配置 AnnotationConfigApplicationContext applicationContext = new AnnotationConfigApplicationContext(MainConfig.class); OneTestDao dao1 = (OneTestDao) applicationContext.getBean("oneTestDao"); dao1.query(); TwoTestDao dao2 = (TwoTestDao) applicationContext.getBean("twoTestDao"); dao2.query(); } }
package com.skypyb.config; import com.skypyb.core.ann.EnableSkypyb; import org.springframework.context.annotation.ComponentScan; import org.springframework.context.annotation.Configuration; @ComponentScan("com.skypyb") @EnableSkypyb("com.skypyb.dao") @Configuration public class MainConfig { }
这是运行后的控制台打印:
——-> SELECT name FROM user WHERE id = 1
——-> SELECT name FROM user WHERE id = 2
这里先说明一下,JDBC的流程我是没写的,因为这个不是重点。我生成的动态代理对象只是单纯的解析了一下@Select注解 然后将其中的value值打印了出来而已。
如何实现 ?
首先,肯定是要实现这么个效果: 将自己自定义对象注入到Spring IOC 容器中。
但是Spring IOC容器内部,保存具体Bean实例的是 singletonObjects 这个ConcurrentHashMap (我这默认只说单例Bean啊)。而向这玩意里边塞值的方式 Spring 是没有提供给我们的,他只能通过Spring扫描Resouce资源然后解析为BeanDefinition再才能从getBean时解析BeanDefinition实例化对象放入此单例缓存中。
既然我们自己没有方法可以直接往单例缓存中塞值,那我们可以采取一点迂回战术。
想要实现注入这种效果,根据Spring IOC的加载Bean、使用Bean流程来说,提供了好几个不同的钩子方法给你自己实现然后在他整个Bean生命周期、IOC容器生命周期的不同阶段调用。
比如: BeanFactoryPostProcessor
BeanFactoryPostProcessor: bean工厂的bean属性处理容器,主要是可以实现该接口从而管理我们的bean工厂内所有的BeanDefinition数据,可以随心所欲的修改属性。
BeanFactoryPostProcessor 的机制就相当于给了我们在 bean 实例化之前最后一次修改 BeanDefinition 的机会,我们可以利用这个机会对 BeanDefinition 来进行一些额外的操作,比如更改某些 bean 的一些属性,给某些 Bean 增加一些其他的信息等等操作。
BeanFactoryPostProcessor提供给我们实现的方法 postProcessBeanFactory() 中可以得到当前beanFactory的对象,然后通过该对象的 registerSingleton() 方法就可以注册一个Bean。
使用起来也很简单,比如我这样,就成功的往Spring IOC 容器中注册了一个name为”123asd”的String对象:
@Component public class TestPostProcessor implements BeanFactoryPostProcessor { @Override public void postProcessBeanFactory(ConfigurableListableBeanFactory beanFactory) throws BeansException { beanFactory.registerSingleton("123asd", new String("666666")); } }
但是,用 BeanFactoryPostProcessor 不好。因为他是专门为了给你进行BeanDefinition的修改而生的,比如说你想改某个框架,就可能用到此类。
到这个钩子调用的阶段,BeanDefinition早就扫描完了,还没开始实例化。你在这强行注入已经实例化的对象到 singletonObject 里,用是可以用,但是违反了相应规范,也指不定会出什么未知bug。
我们最好还是能够在Spring的扫描阶段来注册BeanDefinition,保持和 Spring IOC 容器流程一致化。所幸Spring提供了专门的钩子用来给你注册BeanDefinition。只要有这个钩子,那就没问题了。
因为只要能够注入自己的BeanDefinition,在使用name获取对应的Bean时,要是获取不到,它就会试图获取此name对应的BeanDefinition,他只要一找到这玩意,就会开始实例化进程然后将其实例化,最后放入缓存后返回给调用者。
Spring提供的用来注册BeanDefinition的钩子有两个,一个是 BeanDefinitionRegistryPostProcessor,一个是 ImportBeanDefinitionRegistrar
BeanDefinitionRegistryPostProcessor也实现了BeanFactoryPostProcessor ,不过在实现此接口的时候可以无视BeanFactoryPostProcessor 提供的 postProcessBeanFactory()这个方法,一个类做一个事情,实现了BeanDefinitionRegistryPostProcessor就只用关心如何如何注册BeanDefinition就够了。
BeanDefinitionRegistryPostProcessor和ImportBeanDefinitionRegistrar的主要区别在于其提供给程序员实现的方法不同。
BeanDefinitionRegistryPostProcessor提供的方法仅带有一个BeanDefinitionRegistry参数 , 用于给你注册Bean。
而ImportBeanDefinitionRegistrar在其基础上,还额外带了一个参数 : AnnotationMetadata,此参数可以获取指定注解的属性
一般来说BeanDefinitionRegistryPostProcessor用来解析外部资源配置,ImportBeanDefinitionRegistrar解析注解配置。
以MyBatis来说,对应上边这俩的实现就是 :
MapperScannerConfigurer (实现BeanDefinitionRegistryPostProcessor)
MapperScannerRegistrar (实现 ImportBeanDefinitionRegistrar)
好,既然钩子有了,那么是不是就可以开始进行实例化了呢?
错!
首先,得用上这些钩子。咳咳,这不是在说废话啊!!
毕竟是一个写框架的流程,不是说想用就可以用的,不是说什么一个@Compent注解加上就完事了的。而是需要将配置摆在这里,用户需要的时候才进行配置加载。
如果是XML体系,那没啥好说的,用户就直接在 Spring 的 applicationContext.xml 配置里加载需要的配置Bean就行了。
那么问题来了,要是用注解呢?
只要用过Spring体系的,肯定用过一些Enable开头的注解,一般长这样: @EnableXXX
只要在某个配置类或者说启动类上加入这个注解,就会自动加载某些框架的配置。比如什么 @EnableHystrix、@EnableEurekaServer、@EnableWebSecurity、@EnableCaching、@EnableAsync等等等等奇奇怪怪的注解。
这又是什么操作?
这个问题很好解决,还是以MyBatis举例,只要看一下MyBatis源码就知道这套东西是个什么意思了。
MyBatis的注解配置,有一个必要的类注解叫MapperScan,它主要是来定义扫描哪些包的。然后在该注解中导入了配置类进行加载。
package org.mybatis.spring.annotation; import java.lang.annotation.Annotation; import java.lang.annotation.Documented; import java.lang.annotation.ElementType; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; import java.lang.annotation.Target; import org.mybatis.spring.mapper.MapperFactoryBean; import org.springframework.beans.factory.support.BeanNameGenerator; import org.springframework.context.annotation.Import; @Retention(RetentionPolicy.RUNTIME) @Target({ElementType.TYPE}) @Documented @Import({MapperScannerRegistrar.class}) public @interface MapperScan { String[] value() default {}; String[] basePackages() default {}; Class<?>[] basePackageClasses() default {}; Class<? extends BeanNameGenerator> nameGenerator() default BeanNameGenerator.class; Class<? extends Annotation> annotationClass() default Annotation.class; Class<?> markerInterface() default Class.class; String sqlSessionTemplateRef() default ""; String sqlSessionFactoryRef() default ""; Class<? extends MapperFactoryBean> factoryBean() default MapperFactoryBean.class; }
看16行高亮行,导入的配置即为我上边说的实现了ImportBeanDefinitionRegistrar 接口的MapperScannerRegistrar。
关于Spring 的注解扫描机制,在使用注解加载配置时,Spring会对所有配置类上的注解进行扫描,也会对注解头上修饰的注解进行递归扫描。
@Import 注解 ,就属于一个Spring提供的特殊注解,扫描到该注解引用的类时,就会自动加载该类资源(也就是装载进Spring IOC容器), 一般来说就是拿来导入配置的。当然,导入普通的Bean也不是不行,不过用@Import来导入Bean就属于画蛇添足了。
导入配置的这个阶段,处于Spring IOC容器的扫描阶段。
其实像是这种 @EnableXXX 的注解,你点进他源码看,肯定是用了@Import 的,比如,我也这么定义了一个注解。
看我之前最开始贴出来的实现效果。是不是发现我用的 MainConfig.java 启动配置中,在类上注解了一个@EnableSkypyb
这个 @EnableSkypyb 的底层我是这么写的
package com.skypyb.core.ann; import com.skypyb.core.strengthen.SkypybRegistrar; import org.springframework.context.annotation.Import; import java.lang.annotation.ElementType; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; import java.lang.annotation.Target; @Retention(RetentionPolicy.RUNTIME) @Target(ElementType.TYPE) @Import(SkypybRegistrar.class) public @interface EnableSkypyb { String[] value() default {}; }
他导入了 SkypybRegistrar.class 这个配置,而这个配置,就是重中之重了。
因为,我是完全看着 MyBatis源码 写出的这个配置类,流程可以说和 MyBatis 的加载 mapper 流程一模一样。只是相对于MyBatis而言,少了很多判定和处理而已。
现在配置现在也能动态导入了
那么就可以进入扫描、修改BeanDefinition、实例化代理对象的流程了
核心配置代码:
package com.skypyb.core.strengthen; import com.skypyb.core.ann.EnableSkypyb; import org.springframework.beans.factory.support.BeanDefinitionRegistry; import org.springframework.context.ResourceLoaderAware; import org.springframework.context.annotation.ImportBeanDefinitionRegistrar; import org.springframework.core.annotation.AnnotationAttributes; import org.springframework.core.io.ResourceLoader; import org.springframework.core.type.AnnotationMetadata; public class SkypybRegistrar implements ImportBeanDefinitionRegistrar, ResourceLoaderAware { private ResourceLoader resourceLoader; @Override public void registerBeanDefinitions(AnnotationMetadata annotationMetadata, BeanDefinitionRegistry beanDefinitionRegistry) { //通过 AnnotationMetadata 得到要扫描哪些包下的类 AnnotationAttributes annAttrs = AnnotationAttributes .fromMap(annotationMetadata.getAnnotationAttributes(EnableSkypyb.class.getName())); String[] enableSkypybValues = annAttrs.getStringArray("value"); //用自己自定义的扫描器扫描 ClassPathSkypybScanner scanner = new ClassPathSkypybScanner(beanDefinitionRegistry); if (this.resourceLoader != null) scanner.setResourceLoader(this.resourceLoader); scanner.registerFilters(); scanner.doScan(enableSkypybValues); } @Override public void setResourceLoader(ResourceLoader resourceLoader) { this.resourceLoader = resourceLoader; } }
逻辑清晰,看起来应该不困难。
理所当然的,为了使用@Import 导入实现了ImportBeanDefinitionRegistrar接口。同时还实现了一个ResourceLoaderAware 接口,其实实现这个ResourceLoaderAware 接口没什么鸟用,因为我当时是一直注入失败,所以MyBatis源码里那套东西我都试了一遍,他实现了ResourceLoaderAware 接口我也照葫芦画瓢实现的,实际上后来我全写完了后把这玩意删了也不影响。
SkypybRegistrar内部处理流程 :
1、先从 AnnotationMetadata 类中得到了我 @EnableSkypyb 注解内的的信息,主要是想知道,我应该扫描哪些包。
2、然后创建出自己自定义的扫描器,将 ImportBeanDefinitionRegistrar 提供的注册机 BeanDefinitionRegistry 传入进去
3、最后开始扫描,将需要扫描的包名数组enableSkypybValues 传入 doScan() 方法,扫描之前会先注册下扫描过滤器
就看这么个流程,肯定有点懵逼,因为到底是如何扫描出BeanDefinition的,又是如何将BeanDefinition改成我们自己定义的代理对象的,这里都没体现出来。
这个就必须要进入我自定义的 ClassPathSkypybScanner 这个类里了,不过我不会一股脑给他把代码全贴出来,不然可能会让人有点懵逼。
我接下来将以方法的粒度逐个方法进行讲解。探秘Spring IOC扫描器,以及我说的: FactoryBean 究极奥义
ClassPathSkypybScanner 类声明:
/** * 扫描器 * 继承Spring提供的类路径扫描器 */ public class ClassPathSkypybScanner extends ClassPathBeanDefinitionScanner { public ClassPathSkypybScanner(BeanDefinitionRegistry registry) { super(registry, false); } }
继承了ClassPathBeanDefinitionScanner ,只有一个构造方法,使用的是父类的构造器。
第一个参数是注册机,没什么好说的,第二个参数为是否使用默认过滤器,我传入false表示不使用。
关于ClassPathBeanDefinitionScanner :
ClassPathBeanDefinitionScanner作用就是将指定包下的类通过一定规则过滤后 将Class 信息包装成 BeanDefinition 的形式注册到IOC容器中。
可以继承他覆盖其方法,从而设置自定义的扫描器。实际上MyBatis就是这么做的,当然,我也是。
过滤器用来过滤从指定包下面查找到的 Class ,如果能通过过滤器,那么这个class 就会被转换成BeanDefinition 注册到容器。
这里我把它默认的过滤器禁掉,等会用自己写的过滤器
在我自己定义的自定义扫描器里边,最核心的就是 doScan() 方法了
这个直接贴代码:
@Override protected Set<BeanDefinitionHolder> doScan(String... basePackages) { //这一步调用父类的doScan() 会给注册进BeanDefinitionMap里边去 Set<BeanDefinitionHolder> beanDefinitions = super.doScan(basePackages); if (beanDefinitions.isEmpty()) { this.logger.warn("No Skypyb mapper was found in '" + Arrays.toString(basePackages) + "' package. Please check your configuration."); } else { this.processBeanDefinitions(beanDefinitions); } return beanDefinitions; }
流程也很简单:
1、直接调用父类(ClassPathBeanDefinitionScanner) 的 doScan() 方法,将我传入的包名中的类都扫描出来
2、ClassPathBeanDefinitionScanner 提供的 doScan() 在扫描后,会自动注册进 beanDefinitionMap
3、我获取了doScan()返回值后只需getBeanDefinition() 进行修改,反正指向的是一个堆内存地址
4、至于如何修改,之后再说,因为第一步调用父类 doScan() 方法有坑
ClassPathBeanDefinitionScanner的 doScan() 方法,你要是没做任何处理,他一个类都不会给你扫出来
需要你重写父类的 isCandidateComponent() 方法,在他扫描阶段,他会一个个走进这方法判断。
看其class是不是一个候选组件。只有是一个候选组件,才会进入到下一步判断是否通过过滤器
这是我重写的isCandidateComponent() 。只要是一个接口,并且是单独的,就判断其为一个候选组件。
protected boolean isCandidateComponent(AnnotatedBeanDefinition beanDefinition) { return beanDefinition.getMetadata().isInterface() && beanDefinition.getMetadata().isIndependent(); }
至于我注册的过滤器,其实就是无脑返回true。
但是过滤器这一块是有门道的,很重要的。
我这只是为了演示,才自定义了一个过滤器让其全部通过。
addIncludeFilter() 传入一个TypeFilter,任何一个TypeFilter返回true就代表可以通过
TypeFilter接口目前有AnnotationTypeFilter实现类(类是否有注解修饰)、RegexPatternTypeFilter(类名是否满足正则表达式)等。
可以方便的实现只扫描出符合条件的类,比如 Spring 扫描 @Compone 修饰的类,他就是用的 AnnotationTypeFilter
/** * 注册过滤器 * 设置要注册哪些类,我这是无脑全扫描出来 */ public void registerFilters() { boolean flag = true; //to do if (flag) this.addIncludeFilter((reader, readerFactory) -> true); }
只要这样写,调用父类的 doScan() 方法就可以成功扫描出指定的BeanDefinition了
然后,我在扫描完毕后,使用了自己自定义的方法 processBeanDefinitions() 对该BeanDefinition集合进行处理。
对,就到最关键的时机了!!!
总所周知,Spring IOC生命周期扫描出BeanDefinition后会放入beanDefinitionMap之中。
然后在 getBean() 的时候,才进行Bean的实例化进程,将Bean进行实例化。
我们现在,已经将mapper接口扫描出来了,并由Spring给我们封装完毕,将 BeanDefinition 注册进了 beanDefinitionMap。
这个时候,只要你启动Spring上下文。
就会报错: BeanInstantiationException: Failed to instantiate [xxx.xxx.Xxx]: Specified class is an interface
因为你扫描出的只是一个普通的接口哇 !
就算封装成了BeanDefinition,也改变不了这个BeanDefinition描述的是个接口的事实啊!
Spring表示,很疑惑啊!他无法实例化啊!
而且,我们的终极目的是,偷天换日,将BeanDefinition描述的接口,改为我们自己定义的动态代理对象
如何才能将 BeanDefinition 描述的接口,修改为代理对象?
要知道,我不可能为每个接口都写个单独的代理,鬼知道用户提供了多少个接口?
我得有个统一的封装来表示用户提供的所有接口,并且可以在 运行时生成动态代理。
此对象,还得有个 Class ,因为我得使用 BeanDefinition 的 setBeanClass() 方法,才能将该 BeanDefinition 的描述修改成我自定义的接口。
这个时候,就轮到 FactoryBean 出场了
这是 FactoryBean 接口,Spring 对实现了此接口的Bean进行了特殊处理。
package org.springframework.beans.factory; import org.springframework.lang.Nullable; public interface FactoryBean<T> { @Nullable T getObject() throws Exception; @Nullable Class<?> getObjectType(); default boolean isSingleton() { return true; } }
主要表现为:
1、FactoryBean本身是个Bean
2、试图获取FactoryBean时,得到的是 FactoryBean 的 getObject() 返回的对象
3、可以通过 “&”+”name” 得到FactoryBean 本体对象
了解 FactoryBean 的应该不少。毕竟一些面试官总喜欢问 BeanFactory 和 FactoryBean 有什么区别这种只要背就能背会的问题。
但是,真的有人知道怎么用,在什么场景用这玩意吗?
好,现在场景有了,那么怎么用呢?
这里就涉及到 FactoryBean 配合 BeanDefinition 的 究极奥义 了。
已得知条件:
1、BeanDefinition 表示对一个Bean的描述,可以在实例化之前自由的更改他。
2、FactoryBean 就是一个Bean,但是 Spring 对其做了特殊处理,试图获取时,获取的是 getObject() 返回的对象
从以上条件进行推断:是不是可以在 FactoryBean 里边返回我自定义的代理对象?
可是,所有的mapper都要用某一个 FactoryBean 来返回代理对象。这个 FactoryBean 要怎么设计?
我是不是可以这么设计?
package com.skypyb.core.factorybean; import com.skypyb.core.proxy.SkypybInvocationHandler; import org.springframework.beans.factory.FactoryBean; import java.lang.reflect.Proxy; public class SkypybFactoryBean<T> implements FactoryBean<T> { private Class<T> clazz; public SkypybFactoryBean(Class<T> clazz) { this.clazz = clazz; } @Override public T getObject() throws Exception { return (T) Proxy.newProxyInstance(this.getClass().getClassLoader(), new Class[]{clazz}, new SkypybInvocationHandler()); } @Override public Class<?> getObjectType() { return clazz; } }
我可以用一个构造方法来接收指定的 Class 对象,然后动态返回此 Class对象的代理对象。
比如说我可以在此代理里边实现JDBC的逻辑、或者什么远程服务调用的逻辑。
可是,就算是这么用了:
beanDefinition.setBeanClass(SkypybFactoryBean.class);
那尼玛我 getBean()的时候 Spring 给我试图实例化出来,他也实例化不了啊?
更别提什么调用 getObject() 方法了。
而且这个class对象,必须给我从构造方法传进去。因为就算Spring给你实例化完了FactoryBean,他在 getObject() 时,由于没有class对象,也会报错。
这其中可没什么钩子给你 setClass 。
那怎么搞?
看上边已知条件第一条: BeanDefinition 表示对一个Bean的描述,可以在实例化之前自由的更改他。
我在setBeanClass() 之前,可以使用
beanDefinition.getBeanClassName()
来得到这个Bean原来的名字。 比如我那个 OneTestDao 接口,通过此方法就可以得到 :com.skypyb.dao.OneTestDao
然后通过
beanDefinition.getConstructorArgumentValues().addGenericArgumentValue()
从而设置此 BeanDefinition 描述的 Bean 的构造参数
最终将刚刚获得的类名传入之后,Spring 解析BeanDefinition时 ,就会知道这个BeanDefinition 描述的 Bean 有个构造方法,要传的值是: com.skypyb.dao.OneTestDao
从而 成功实例化我们自定义的 FactoryBean。
最后的结果,就是通过我们自定义 FactoryBean 的实例对象的 getObject() 方法返回了自定义的代理,翻到文章最顶上演示,如你所见。
成功实现了 偷天换日,将BeanDefinition描述的接口,改为我们自己定义的动态代理对象
而且我的整个流程,和 MyBatis 可以说是 一模一样
不信? MyBatis 源码 : MapperScannerRegistrar.class 请
整个工程的完整代码地址: github