【Spring 学习笔记】8 Spring 整合Mybatis 原理分析

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&amp;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,所以整合后一级缓存失效
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值