spring源码版本 spring-framework-5.3.10
Spring 是如何配置Mybatis的
在了解Spring 整合Mybatis 原理之前,我们先来了解开发过程中开发者是如何配置Mybatis的,可以访问如下链接:Mybatis-Spring start
Mybatis整合到Spring的思路
整合的核心思想就是把Mybatis框架所产生的对象放到Spring容器中,让其成为Spring Bean。
如何注册到Spring容器中?
在Spring容器启动中去装配Beandefinitin的过程中会有ConfigurationClassPostProcessor实现了BeanDefinitionRegistryPostProcessor接口,这样可以重写postProcessBeanDefinitionRegistry方法,方法提供了registry可以去注册将解析出的BeanDefinition到容器中,那么我们是否也可以自己写一个这样的MybatisPostProcesso去重写postProcessBeanDefinitionRegistry,去向容器中注册Mybatis的类。
- 例如Mybatis中我们常会有这样的Mapper接口
public interface UserMapper {
@Select("SELECT * FROM users WHERE id = #{userId}")
User getUser(@Param("userId") String userId);
}
- 那么我们现在来建一个MybatisPostProcessor, 如下代码所示:
public class NingMybatisRegistryPostProcesser implements BeanDefinitionRegistryPostProcessor {
@Override
public void postProcessBeanDefinitionRegistry(BeanDefinitionRegistry registry) throws BeansException {
BeanDefinitionBuilder beanDefinitionBuilder = BeanDefinitionBuilder.genericBeanDefinition(UserMapper.class);
AbstractBeanDefinition beanDefinition = beanDefinitionBuilder.getBeanDefinition();
registry.registerBeanDefinition("userMapper",beanDefinition);
}
@Override
public void postProcessBeanFactory(ConfigurableListableBeanFactory beanFactory) throws BeansException {
}
}
-
那么问题是 实际应用中不可能只有一个Mapper ,会有很多个,我们不可能一个一个这样去注册,那么如何解决问题呢?
我们可以利用spring的doscan去扫描,我们可以定义一个@NingMapperScan注解,去记录Mapper包的路径,利用doscan()去扫描Bean,可以修改代码如下:// 1. 定义一个注解,配置上扫描路径 @Retention(RetentionPolicy.RUNTIME) @Target(ElementType.TYPE) public @interface NingMapperScan { String value(); }
// 2. postProcessBeanDefinitionRegistry方法将会修改成如下逻辑 @Component public class NingBeanDefinitonRegistryPostProcesser implements BeanDefinitionRegistryPostProcessor { @Override public void postProcessBeanDefinitionRegistry(BeanDefinitionRegistry registry) throws BeansException { // 1.拿到扫描路径 // 2.doscan } @Override public void postProcessBeanFactory(ConfigurableListableBeanFactory beanFactory) throws BeansException {} }
// 3. 配置类上配置上扫描路径 @NingMapperScan("com.ning.mapper") @ComponentScan("com.ning") @EnableScheduling @PropertySource("classpath:spring.properties") @NingMapperScan("com.ning.mapper") public class AppConfig {}
-
那么如何在postProcessBeanDefinitionRegistry方法中拿到扫描路径呢? 我们发现这个方法registry并不能直接获取到路径,这样我们可以用到另一个postprocessor, ImportBeanDefinitionRegistrar, 代码可以修改为:
// 1. 修改定义注解,加上@Import , 这样Spring容器启动的时候回去执行ImportBeanDefinitionRegistrar的 registerBeanDefinitions方法 @Retention(RetentionPolicy.RUNTIME) @Target(ElementType.TYPE) @Import(NingImportBeanDefinitionRegistrar.class) public @interface NingMapperScan { String value(); }
// 2. postProcessBeanDefinitionRegistry方法将会修改成如下逻辑 @Component public class NingBeanDefinitonRegistryPostProcesser implements BeanDefinitionRegistryPostProcessor { @Override public void postProcessBeanDefinitionRegistry(BeanDefinitionRegistry registry) throws BeansException { // 1.拿到扫描路径 MergedAnnotation<NingMapperScan> ningMapperScanMergedAnnotation = importingClassMetadata.getAnnotations().get(NingMapperScan.class); String packagePath = ningMapperScanMergedAnnotation.getString("value"); // 2.使用Spring的扫描器去扫描 ClassPathBeanDefinitionScanner classPathBeanDefinitionScanner = new ClassPathBeanDefinitionScanner(registry); classPathBeanDefinitionScanner.scan(packagePath); } @Override public void postProcessBeanFactory(ConfigurableListableBeanFactory beanFactory) throws BeansException {} }
// 3. 配置类上配置上扫描路径 @NingMapperScan("com.ning.mapper") @ComponentScan("com.ning") @EnableScheduling @PropertySource("classpath:spring.properties") @NingMapperScan("com.ning.mapper") public class AppConfig {}
-
那么又会出现问题,Spring的扫描器是不会把接口的BeanDefinition注册到容器中的,所以我们需要自己定义扫描器去做一些调整,看spring源码可以知道在doscan的时候是isCandidateComponent(metadataReader) 和 isCandidateComponent(beanDefinition) 同时满足的时候才会注册,前者是判断是否有@Component, 后者则排除了接口类所以我们可以自定义一个扫描器去继承ClassPathBeanDefinitionScanner,然后重写这两个方法,修改如下:
// 0 自定义一个 ClassPathBeanDefinitionScanner 扫描器 public class NingClassPathBeanDefinitionScanner extends ClassPathBeanDefinitionScanner { public NingClassPathBeanDefinitionScanner(BeanDefinitionRegistry registry) { super(registry); } // Spring源码中的该方法的判断逻辑是接口不生成BeanDefiniton, 而Mybatias中定义的Mapper类都是接口,所以此处可以修改成只要是接口的类就可以认为是需要被注册到BeanDefinition的 @Override protected boolean isCandidateComponent(AnnotatedBeanDefinition beanDefinition) { return beanDefinition.getMetadata().isInterface(); } }
// 1. 定义注解,加上@Import , 这样Spring容器启动的时候回去执行ImportBeanDefinitionRegistrar的 registerBeanDefinitions方法 @Retention(RetentionPolicy.RUNTIME) @Target(ElementType.TYPE) @Import(NingImportBeanDefinitionRegistrar.class) public @interface NingMapperScan { String value(); }
// 2. postProcessBeanDefinitionRegistry方法将会修改成如下逻辑 @Component public class NingBeanDefinitonRegistryPostProcesser implements BeanDefinitionRegistryPostProcessor { @Override public void postProcessBeanDefinitionRegistry(BeanDefinitionRegistry registry) throws BeansException { // 1.拿到扫描路径 MergedAnnotation<NingMapperScan> ningMapperScanMergedAnnotation = importingClassMetadata.getAnnotations().get(NingMapperScan.class); String packagePath = ningMapperScanMergedAnnotation.getString("value"); // 2.使用自定义的扫描器去扫描 NingClassPathBeanDefinitionScanner scanner = new NingClassPathBeanDefinitionScanner(registry); /* 此处添加 includeFilter的原因是在spring源码中scan方法扫描的时候会判断是否加@Component注解,而我们使用的Mapper类中往往不加这个注解,所以这块直接让macth方法是true来跳过校验。*/ scanner.addIncludeFilter(new TypeFilter() { @Override public boolean match(MetadataReader metadataReader, MetadataReaderFactory metadataReaderFactory) throws IOException { return true; } }); classPathBeanDefinitionScanner.scan(packagePath); } @Override public void postProcessBeanFactory(ConfigurableListableBeanFactory beanFactory) throws BeansException {} }
// 3. 配置类上配置上扫描路径 @NingMapperScan("com.ning.mapper") @ComponentScan("com.ning") @EnableScheduling @PropertySource("classpath:spring.properties") @NingMapperScan("com.ning.mapper") public class AppConfig {}
-
现在可以启动代码验证一下是否将Mybatis Bean 注入到了spring 容器中
public class ningApplication {
public static void main(String[] args) {
// 创建一个Spring容器
AnnotationConfigApplicationContext applicationContext = new AnnotationConfigApplicationContext(AppConfig.class);
System.out.println(applicationContext.getBean("userMapper"));
}
}
- 经验证发现仍然抛异常如下,发现是在实例化Bean的时候报错,进入源码看发现通过Beandefinition去拿class实例化的时候如果class是接口则不能实例化的。
Caused by: org.springframework.beans.BeanInstantiationException: Failed to instantiate [com.ning.mapper.OrderMapper]: Specified class is an interface
at org.springframework.beans.factory.support.SimpleInstantiationStrategy.instantiate(SimpleInstantiationStrategy.java:74)
at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.instantiateBean(AbstractAutowireCapableBeanFactory.java:1349)
... 13 more
-
那我们如何自行实例化呢?这样就想到了一个类,FactoryBean----Spring在容器启动创建实例化Bean的时候,允许开发者有一定的权限来干涉自己所创建的Bean的条件,那么我们的创建一个FactoryBean,用代理类来实例化一个对象,代码做如下修改
//1 新增一个MapperFactoryBean @Component public class NingMapperFactoryBean implements FactoryBean { // 定义一个属性来接收Mapper接口类 private Class mapperInterface; // 定义一个构造器接受 private Class mapperInterface; 的值,可在Beandefiniton的时候用到此构造器给mapperInterface赋值 public NingMapperFactoryBean(Class mapperInterface) { this.mapperInterface = mapperInterface; } // 一定要有一个空构造器,实例化NingMapperFactoryBean Bean的时候用 public NingMapperFactoryBean() { } @Override public Object getObject() throws Exception { // 运用代理类去实例化一个null对象 Object newProxyInstance = Proxy.newProxyInstance(NingMapperFactoryBean.class.getClassLoader(), new Class[]{mapperInterface }, new InvocationHandler() { @Override public Object invoke(Object proxy, Method method, Object[] args) throws Throwable { System.out.println(method.getName()); return null; } }); return newProxyInstance; } @Override public Class getObjectType() { return mapperInterface; } }
// 1 在自定义 扫描器中重写doscan 方法,因为要对生成的BeanDefiniton改造成 我们自己定义的FactoryBean public class NingClassPathBeanDefinitionScanner extends ClassPathBeanDefinitionScanner { public NingClassPathBeanDefinitionScanner(BeanDefinitionRegistry registry) { super(registry); } @Override protected Set<BeanDefinitionHolder> doScan(String... basePackages) { // 拿到扫描的所有beanDefinitionHolders Set<BeanDefinitionHolder> beanDefinitionHolders = super.doScan(basePackages); // 逐个去遍历,把beanDefinition的className 改成NingMapperFactoryBean,给那个有参构造器传值,将Mapper的Bean 传入 for(BeanDefinitionHolder beanDefinitionHolder : beanDefinitionHolders){ BeanDefinition beanDefinition = beanDefinitionHolder.getBeanDefinition(); // 给FactoryBean 构造器传参数 beanDefinition.getConstructorArgumentValues().addGenericArgumentValue(beanDefinition.getBeanClassName()); // 修改className beanDefinition.setBeanClassName(NingMapperFactoryBean.class.getName()); } return beanDefinitionHolders; } // Spring源码中的该方法的判断逻辑是接口不生成BeanDefiniton, 而Mybatias中定义的Mapper类都是接口,所以此处可以修改成只要是接口的类就可以认为是需要被注册到BeanDefinition的 @Override protected boolean isCandidateComponent(AnnotatedBeanDefinition beanDefinition) { return beanDefinition.getMetadata().isInterface(); } }
// 2 定义注解,加上@Import , 这样Spring容器启动的时候回去执行ImportBeanDefinitionRegistrar的 registerBeanDefinitions方法 @Retention(RetentionPolicy.RUNTIME) @Target(ElementType.TYPE) @Import(NingImportBeanDefinitionRegistrar.class) public @interface NingMapperScan { String value(); }
// 3. postProcessBeanDefinitionRegistry方法将会修改成如下逻辑 @Component public class NingBeanDefinitonRegistryPostProcesser implements BeanDefinitionRegistryPostProcessor { @Override public void postProcessBeanDefinitionRegistry(BeanDefinitionRegistry registry) throws BeansException { // 1.拿到扫描路径 MergedAnnotation<NingMapperScan> ningMapperScanMergedAnnotation = importingClassMetadata.getAnnotations().get(NingMapperScan.class); String packagePath = ningMapperScanMergedAnnotation.getString("value"); // 2.使用自定义的扫描器去扫描 NingClassPathBeanDefinitionScanner scanner = new NingClassPathBeanDefinitionScanner(registry); /* 此处添加 includeFilter的原因是在spring源码中scan方法扫描的时候会判断是否加@Component注解,而我们使用的Mapper类中往往不加这个注解,所以这块直接让macth方法是true来跳过校验。*/ scanner.addIncludeFilter(new TypeFilter() { @Override public boolean match(MetadataReader metadataReader, MetadataReaderFactory metadataReaderFactory) throws IOException { return true; } }); classPathBeanDefinitionScanner.scan(packagePath); } @Override public void postProcessBeanFactory(ConfigurableListableBeanFactory beanFactory) throws BeansException {} }
// 4. 配置类上配置上扫描路径 @NingMapperScan("com.ning.mapper") @ComponentScan("com.ning") @EnableScheduling @PropertySource("classpath:spring.properties") @NingMapperScan("com.ning.mapper") public class AppConfig {}
// 5. 现在可以启动代码验证一下是否将Mybatis Bean 注入到了spring 容器中 public class ningApplication { public static void main(String[] args) { // 创建一个Spring容器 AnnotationConfigApplicationContext applicationContext = new AnnotationConfigApplicationContext(AppConfig.class); System.out.println(applicationContext.getBean("userMapper")); } }
-
分析到这,Mapper的这些Bean已经能注册到Spring 容器中了,但是最终的目的还是要去执行方法上的SQL去操作数据库数据,而,所以Mybatis中不是通过直接去生成代理对象,而是通过SqlSession获取Mapper接口代理对象,最后通过代理对象发起数据库操作,所以可以将上述代码做如下修改
//1 给MapperFactoryBean 新增 Sqlsession属性 @Component public class NingMapperFactoryBean implements FactoryBean { // 定义一个属性来接收Mapper接口类 private Class mapperInterface; private SqlSession sqlSession; @Autowired public void setSqlSession(SqlSessionFactory sqlSessionFactory) { this.sqlSession = sqlSessionFactory.openSession(); } // 定义一个构造器接受 private Class mapperInterface; 的值,可在Beandefiniton的时候用到此构造器给mapperInterface赋值 public NingMapperFactoryBean(Class mapperInterface) { this.mapperInterface = mapperInterface; } // 一定要有一个空构造器,实例化NingMapperFactoryBean Bean的时候用 public NingMapperFactoryBean() { } @Override public Object getObject() throws Exception { // 运用代理类去实例化一个null对象 Object newProxyInstance = Proxy.newProxyInstance(NingMapperFactoryBean.class.getClassLoader(), new Class[]{mapperInterface }, new InvocationHandler() { @Override public Object invoke(Object proxy, Method method, Object[] args) throws Throwable { System.out.println(method.getName()); return null; } }); return newProxyInstance; } @Override public Class getObjectType() { return mapperInterface; } }
// 2. 配置类上配置上扫描路径 @NingMapperScan("com.ning.mapper"), 并添加 SqlSessionFactory 的Bean @ComponentScan("com.ning") @EnableScheduling @PropertySource("classpath:spring.properties") @NingMapperScan("com.ning.mapper") public class AppConfig { @Bean public SqlSessionFactory sqlSessionFactory() throws IOException { InputStream resourceAsStream = Resources.getResourceAsStream("mybatis.xml"); SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(resourceAsStream); return sqlSessionFactory; } }
// 3 添加数据库信息的配置文件 <?xml version="1.0" encoding="UTF-8" ?> <!DOCTYPE configuration PUBLIC "-//mybatis.org//DTD Config 3.0//EN" "http://mybatis.org/dtd/mybatis-3-config.dtd"> <configuration> <environments default="development"> <environment id="development"> <!-- 使用jdbc事务管理 --> <transactionManager type="JDBC"/> <!-- 数据库连接池 --> <dataSource type="POOLED"> <property name="driver" value="com.mysql.jdbc.Driver"/> <property name="url" value="jdbc:mysql://127.0.0.1:3306/xxx?characterEncoding=utf-8&useSSL=false"/> <property name="username" value="root"/> <property name="password" value="xxxxxxx***"/> </dataSource> </environment> </environments> </configuration>
// 4 在自定义 扫描器中重写doscan 方法,因为要对生成的BeanDefiniton改造成 我们自己定义的FactoryBean public class NingClassPathBeanDefinitionScanner extends ClassPathBeanDefinitionScanner { public NingClassPathBeanDefinitionScanner(BeanDefinitionRegistry registry) { super(registry); } @Override protected Set<BeanDefinitionHolder> doScan(String... basePackages) { // 拿到扫描的所有beanDefinitionHolders Set<BeanDefinitionHolder> beanDefinitionHolders = super.doScan(basePackages); // 逐个去遍历,把beanDefinition的className 改成NingMapperFactoryBean,给那个有参构造器传值,将Mapper的Bean 传入 for(BeanDefinitionHolder beanDefinitionHolder : beanDefinitionHolders){ BeanDefinition beanDefinition = beanDefinitionHolder.getBeanDefinition(); // 给FactoryBean 构造器传参数 beanDefinition.getConstructorArgumentValues().addGenericArgumentValue(beanDefinition.getBeanClassName()); // 修改className beanDefinition.setBeanClassName(NingMapperFactoryBean.class.getName()); } return beanDefinitionHolders; } // Spring源码中的该方法的判断逻辑是接口不生成BeanDefiniton, 而Mybatias中定义的Mapper类都是接口,所以此处可以修改成只要是接口的类就可以认为是需要被注册到BeanDefinition的 @Override protected boolean isCandidateComponent(AnnotatedBeanDefinition beanDefinition) { return beanDefinition.getMetadata().isInterface(); } }
// 5 定义注解,加上@Import , 这样Spring容器启动的时候回去执行ImportBeanDefinitionRegistrar的 registerBeanDefinitions方法 @Retention(RetentionPolicy.RUNTIME) @Target(ElementType.TYPE) @Import(NingImportBeanDefinitionRegistrar.class) public @interface NingMapperScan { String value(); }
// 6. postProcessBeanDefinitionRegistry方法将会修改成如下逻辑 @Component public class NingBeanDefinitonRegistryPostProcesser implements BeanDefinitionRegistryPostProcessor { @Override public void postProcessBeanDefinitionRegistry(BeanDefinitionRegistry registry) throws BeansException { // 1.拿到扫描路径 MergedAnnotation<NingMapperScan> ningMapperScanMergedAnnotation = importingClassMetadata.getAnnotations().get(NingMapperScan.class); String packagePath = ningMapperScanMergedAnnotation.getString("value"); // 2.使用自定义的扫描器去扫描 NingClassPathBeanDefinitionScanner scanner = new NingClassPathBeanDefinitionScanner(registry); /* 此处添加 includeFilter的原因是在spring源码中scan方法扫描的时候会判断是否加@Component注解,而我们使用的Mapper类中往往不加这个注解,所以这块直接让macth方法是true来跳过校验。*/ scanner.addIncludeFilter(new TypeFilter() { @Override public boolean match(MetadataReader metadataReader, MetadataReaderFactory metadataReaderFactory) throws IOException { return true; } }); classPathBeanDefinitionScanner.scan(packagePath); } @Override public void postProcessBeanFactory(ConfigurableListableBeanFactory beanFactory) throws BeansException {} }
上述就是Mybatis整合到Spring的源码中的思路,但Mybatis在整合的时候肯定比上述示例的代码复杂,很多细节这里不赘述,主要体会思想,明白了这个思路再去看Mybatis整合这块的源码就会好理解很多。
Mybatis整合到Spring后一级缓存失效问题
- 首先说一下一级缓存是什么?
Mybatis中的一级缓存是基于SqlSession来实现的,所以在执行同一个sql时,如果使用的是同一个SqlSession对象,那么就能利用到一级缓存,提高sql的执行效率。 - 理解了Mybatis在执行SQL时的代码逻辑,就很容易理解为什么一级缓存会失效了
SqlSession生成Mapper代理对象在执行SQL的时候会进入到SqlSessinTelplate的SqlSessionInterceptor中,从而从resource的ThreadLocal中获取Sqlsession, 为空会新生成一个SqlSession对象放入其中,在不开启事务的情况下,会始终是空,每次执行都会得到一个新的Sqlsession,所以整合后一级缓存失效