手动实现spring-mybatis集成
我们知道在使用mybatis的时候,需要导入spring-mybatis的集成jar包,才能将mybatis集成到spring容器中,而这集成的jar包具体做了哪些事情,使用什么技术把mybatis的mapper接口当成一个bean注册到spring容器中去的呢?
首先前面提过FactoryBean的功能,它就是把实现的getObject()返回的对象解析为spring的一个bean,beanName为FactoryBean;而集成mybatis的第一步就是通过FactoryBean导入mybatis的mepper接口的代理对象
public class RickFactoryBean implements FactoryBean {
private Class mapper;
private SqlSession sqlSession;
public RickFactoryBean(Class mapper) {
this.mapper = mapper;
}
@Autowired
public void setSqlSessionFactory (SqlSessionFactory sqlSessionFactory) {
sqlSessionFactory.getConfiguration().addMapper(mapper);
this.sqlSession = sqlSessionFactory.openSession();
}
@Override
public Object getObject() throws Exception {
return sqlSession.getMapper(mapper);
}
@Override
public Class<?> getObjectType() {
return mapper;
}
}
setSqlSessionFactory()会从spring容器中获得mybatis的会话工厂用来构建sqlSession,其次这个类的构造方法就是传入mapper接口的class,用来从mybatis从获取Mapper代理对象返回;
接下来就需要将这个FactoryBean解析为BeanDefinition,注册到spring容器中去。它本身可以直接通过家@Component注解的方式去实现,但是一般会有很多Mapper接口,故不能作为单例的方式注册到spring容器中,我们可以通过新建一个BeanDefinition来把它注册到spring容器中
AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext();
context.register(AppConfig.class);
AbstractBeanDefinition beanDefinition = BeanDefinitionBuilder.genericBeanDefinition().getBeanDefinition();
beanDefinition.setBeanClass(RickFactoryBean.class);
beanDefinition.setBeanClassName(RickFactoryBean.class.getName());
beanDefinition.getConstructorArgumentValues().addGenericArgumentValue(UserMapper.class);
context.registerBeanDefinition("userMapper", beanDefinition);
那么如何拿到所有mapper接口呢?我们知道mybatis有个@MapperScan的注解。我们可以手写一个@RickMapperScan通过扫描路径拿到mapper接口一个一个的去实例化FactoryBean并注册到spring容器中去。
@Target({ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Import({RickImportBeanDefinitionRegistry.class})
public @interface RickMapperScan {
String value();
}
而这个注解上还导入了一个很重要的类RickImportBeanDefinitionRegistry,这个类里面可以拿到@RickMapperScan传入的路径来实现扫描。
public class RickImportBeanDefinitionRegistry implements ImportBeanDefinitionRegistrar {
@Override
public void registerBeanDefinitions(AnnotationMetadata metadata, BeanDefinitionRegistry registry) {
Map<String, Object> map = metadata.getAnnotationAttributes(RickMapperScan.class.getName());
String path = (String) map.get("value");
RickClassPathBeanDefinitionScanner scanner = new RickClassPathBeanDefinitionScanner(registry);
scanner.doScan(path);
}
}
spring已经提供了扫描器(ClassPathBeanDefinitionScanner),我们只需要继承它并重写一些方法,就可以实现扫描逻辑:首先调用doScan方法,再调用父类的doScan()方法,此时spring会做类的判断,如果是接口类是不能注册到spring容器中去的,需要重写isCandidateComponent()方法来判断如果是接口就可以进行扫描生成BeanDefinition。拿到spring扫描出来的BeanDefinition,遍历所有BeanDefinition,通过FactoryBean来构建一个构造方法,构造方法的入参就是扫描出来的mapper类,并重置BeanDefinition的beanClass和beanClassName,最后就实现了将mybatis的mapper代理对象注册到spring容器中去了。
public class RickClassPathBeanDefinitionScanner extends ClassPathBeanDefinitionScanner {
public RickClassPathBeanDefinitionScanner(BeanDefinitionRegistry registry) {
super(registry);
}
@Override
protected Set<BeanDefinitionHolder> doScan(String... basePackages) {
Set<BeanDefinitionHolder> beanDefinitionHolders = super.doScan(basePackages);
beanDefinitionHolders.forEach(holder -> {
AbstractBeanDefinition bd = (AbstractBeanDefinition) holder.getBeanDefinition();
bd.getConstructorArgumentValues().addGenericArgumentValue(bd.getBeanClassName());
bd.setBeanClass(RickFactoryBean.class);
bd.setBeanClassName(RickFactoryBean.class.getName());
});
return beanDefinitionHolders;
}
@Override
protected boolean isCandidateComponent(AnnotatedBeanDefinition beanDefinition) {
return beanDefinition.getMetadata().isInterface();
}
@Override
protected boolean isCandidateComponent(MetadataReader metadataReader) throws IOException {
return true;
}
}
spring-mybatis 源码实现核心逻辑
看看MapperScan源码,它导入了MapperScannerRegistrar类,跟手写实现一样,该类也是实现了ImportBeanDefinitionRegistrar类。
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.TYPE)
@Documented
@Import(MapperScannerRegistrar.class)
@Repeatable(MapperScans.class)
public @interface MapperScan {
...
}
故启动时会调用registerBeanDefinitions()方法,该方法会向spring容器注册MapperScannerConfigurer对象,这个对象本质是一个BeanPostProcessor,spring就会执行postProcessBeanDefinitionRegistry()方法。重点来了,最终会创建ClassPathMapperScanner对象调用扫描方法,而这之前会把FactoryBean作为一个属性设置到ClassPathMapperScanner对象中。
public void registerBeanDefinitions(AnnotationMetadata importingClassMetadata, BeanDefinitionRegistry registry) {
AnnotationAttributes mapperScanAttrs = AnnotationAttributes
.fromMap(importingClassMetadata.getAnnotationAttributes(MapperScan.class.getName()));
if (mapperScanAttrs != null) {
registerBeanDefinitions(importingClassMetadata, mapperScanAttrs, registry,
generateBaseBeanName(importingClassMetadata, 0));
}
}
而FactoryBean中getObject()方法,最终拿到SqlSession类型为SqlSessionTemplate,但是我们在自己使用mybatis框架查询的时候用的是DefaultSqlSession,那么为什么要这么设计呢?其实这里真正是为了解决DefaultSqlSession类线程不安全的问题,因为他有很多属性,比如:autoCommit。
当我们在执行某个需要执行sql的业务方法时,假设有多个sql需要执行,那就在每次执行sql的时候都会创建一个DefaultSqlSession对象;此时如果业务方法加了@Transaction注解,那执行该方法用得就是同一个DefaultSqlSession对象;实现技术就是ThreadLocal缓存,通过判断方法上是否加了@Transaction注解,加了就会缓存第一次创建的DefaultSqlSession对象,所以同一个线程就会使用同一个SqlSession对象。
最后回到MapperScannerConfigurer#postProcessBeanDefinitionRegistry()方法,scanner设置完相关的属性之后就会执行扫描逻辑,将所有mybatis的mapper代理对象注册到spring容器中。
仅个人笔记。