Mybatis整合到Spring中需要考虑两个问题
1.Mybatis中的Mapper文件都是接口形式的,接口无法生成对象,该如何放到Spring的单例池中
2.一个项目有很多个Mapper文件,Spring启动的时候该如何查找到对应的Mapper文件
解决了以上两个问题,那么Mybatis就能顺利的整合到Spring中,解决思路如下,借助于Spring中的扫描器Scanner,自己定义扫描路径和规则,那么便能扫描到对应的Mapper接口,然后针对于这些接口生成代理对象(jdk动态代理),然后将动态代理生成的代理对象放到Spring的容器中,那么便是完成了Spring和mybatis的整合
手写Spring整合Mybatis的步骤如下
1.自定义一个MyMapperScan注解,然后自定义一个类(MyImportBeanDefinitionRegistrar)实现ImportBeanDefinitionRegistrar接口,用于生成自定义扫描器去扫描MyMapperScan传进来的路径
2.继承了Spring中自带的Scanner,父类的doScan方法会把路径下的Mapper都生成BeanDefinition,我们要做的,是对其生成的BeanDefinition进行一个修改(通过FactoryBean),让其在后面生成Bean的过程中生成代理对象。最后扔到单例池中
自定义注解(在启动类上添加该注解,value是扫描路径),直接在该注解中引入自定义的ImportBeanDefinitionRegistrar类,用于扫描和注册BeanDefinition的逻辑
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.TYPE)
@Import(MyImportBeanDefinitionRegistrar.class)
public @interface MyMapperScan {
String value();
}
自定义ImportBeanDefinitionRegistrar,这个接口是如何注册BeanDefinition,可以参考文章(Spring配置类源码解析(下))
//mapperscan直接会import这个类,然后会回调里面的方法,以此来进行扫描和注入bean
public class MyImportBeanDefinitionRegistrar implements ImportBeanDefinitionRegistrar {
@Override
public void registerBeanDefinitions(AnnotationMetadata importingClassMetadata, BeanDefinitionRegistry registry, BeanNameGenerator importBeanNameGenerator) {
// 扫描自定义注解MyMapperScan中用户传进来的路径,用于作为扫描路径
Map<String, Object> annotationAttributes = importingClassMetadata.getAnnotationAttributes(MyMapperScan.class.getName());
String path = (String) annotationAttributes.get("value");
MyBeanDefinitionScanner scanner = new MyBeanDefinitionScanner(registry);
//mapper全是接口,自定义添加过滤器,不然会被规则过滤掉
scanner.addIncludeFilter(new TypeFilter() {
@Override
public boolean match(@NotNull MetadataReader metadataReader, MetadataReaderFactory metadataReaderFactory) throws IOException {
return metadataReader.getAnnotationMetadata().isInterface();
}
});
scanner.scan(path);
}
}
自定义扫描器,扫描规则是,只要路径下的接口,只要是接口的,我都统一返回true,然后改造一下生成的BeanDefinition,将其BeanClassName设置成FactoryBean的(这样的话后续生成Bean的时候就会走我们自定义的FactoryBean的特殊逻辑)
public class MyBeanDefinitionScanner extends ClassPathBeanDefinitionScanner {
public MyBeanDefinitionScanner(BeanDefinitionRegistry registry) {
super(registry);
}
@Override
protected Set<BeanDefinitionHolder> doScan(String... basePackages) {
Set<BeanDefinitionHolder> beanDefinitionHolders = super.doScan(basePackages);
//父类的doScan方法会将扫描到的符合匹配规则的全部生成BeanDefinition
//但是这些BeanDefinition肯定不会是我们想要的,我们要的是根据接口生成的代理对象
//所以要对这些BeanDefinition进行一个人为的修改,修改其BeanClass(将其设置为FactoryBean)
//后面在生成Bean的时候会对FactoryBean进行额外逻辑的处理,会生成getObject方法中返回的对象
for (BeanDefinitionHolder beanDefinitionHolder : beanDefinitionHolders) {
BeanDefinition beanDefinition = beanDefinitionHolder.getBeanDefinition();
//指定构造方法,只用一个参数的,这边传入对应的Class下面的代码会有相呼应
beanDefinition.getConstructorArgumentValues().addGenericArgumentValue(beanDefinition.getBeanClassName());
//然后要改变一下这里面的BeanClass,后续生成Bean的时候会用到
beanDefinition.setBeanClassName(MyFactoryBean.class.getName());
}
return beanDefinitionHolders;
}
//自定义规则, 只有扫描到的,并且是接口,才会被注册BeanDefinition
@Override
protected boolean isCandidateComponent(AnnotatedBeanDefinition beanDefinition) {
return beanDefinition.getMetadata().isInterface();
}
}
自定义实现FactoryBean,里面的getObject方法直接返回sqlSession中的代理对象(不过这边也可以人为的使用jdk代理,但是这样的话那些@Select注解有关的Aop啥的也要自己写一套了)
public class MyFactoryBean implements FactoryBean { // MyFactoryBean--->UserMapper代理对象
private Class mapperInterface;
private SqlSession sqlSession;
这里和上面那坨代码传进来的构造参数相呼应
public MyFactoryBean(Class mapperInterface) {
this.mapperInterface = mapperInterface;
}
@Autowired
public void setSqlSession(SqlSessionFactory sqlSessionFactory) {
sqlSessionFactory.getConfiguration().addMapper(mapperInterface);
this.sqlSession = sqlSessionFactory.openSession();
}
@Override
public Object getObject() throws Exception {
//要对哪一个Mapper接口进行动态代理(这里默认使用了sqlSession的,也可以自己通过jdk动态代理生成)
return sqlSession.getMapper(mapperInterface);
}
@Override
public Class<?> getObjectType() {
return mapperInterface;
}
}
最后,我们可以在启动类上加上我们自定义的@MyMapperScan注解,然后传入我们想要扫描的路径,就可以直接使用我们自定义的这套Spring整合mybatis的功能了