框架源码专题:Spring是如何集成Mybatis的?Spring怎么管理Mapper接口的动态代理

2022-08-06 13:48:49


1. Spring集成Mybatis代码示例

        Spring在集成Mybatis时,使用SqlSessionFactoryBean来完成Configuration的解析,代码如下:

@EnableTransactionManagement@Configuration@MapperScan(basePackages={"com.tuling.mapper"})@ComponentScan(basePackages={"com.tuling"})@RepositorypublicclassMyBatisConfig{// =====>   spring.xml//用SqlSessionFactoryBean来代替Mybatis中解析Configuration的过程@BeanpublicSqlSessionFactoryBeansqlSessionFactory()throwsIOException{SqlSessionFactoryBean factoryBean=newSqlSessionFactoryBean();//设置数据源
      factoryBean.setDataSource(dataSource());// 设置 MyBatis 配置文件路径
      factoryBean.setConfigLocation(newClassPathResource("mybatis/mybatis-config.xml"));// 设置 SQL 映射文件路径
      factoryBean.setMapperLocations(newPathMatchingResourcePatternResolver().getResources("classpath:mybatis/mapper/*.xml"));
      factoryBean.setTypeAliases(User.class);return factoryBean;}//数据源publicDataSourcedataSource(){DruidDataSource dataSource=newDruidDataSource();
        dataSource.setUsername("root");
        dataSource.setPassword("123456");
        dataSource.setDriverClassName("com.mysql.jdbc.Driver");
        dataSource.setUrl("jdbc:mysql://localhost:3306/mybatis_example");return dataSource;}


2. Spring 如何解析Mybatis配置文件

Spring 是根据bean对象SqlSessionFactoryBean 来解析xml文件到Configuration类中的,首先来看一下SqlSessionFactoryBean这个类:

//SqlSessionFactoryBean 实现了FactoryBean 和 InitializingBeanpublicclassSqlSessionFactoryBeanimplementsFactoryBean<SqlSessionFactory>,InitializingBean,ApplicationListener<ApplicationEvent>{//内部封装了Configuration 类privateConfiguration configuration;privateResource[] mapperLocations;privateDataSource dataSource;//类型处理器privateTypeHandler<?>[] typeHandlers;privateString typeHandlersPackage;//类型别名privateClass<?>[] typeAliases;privateString typeAliasesPackage;

	。。。。。。 不一一列举

可以看到SqlSessionFactoryBean内部封装了Configuration类,
实现了FactoryBean,会创建getObject中返回的单例sqlSessionFactory对象,这个对象就是我们Mybatis中最开始的DefaultSqlSessionFactory对象!

@OverridepublicSqlSessionFactorygetObject()throwsException{if(this.sqlSessionFactory==null){//如果sqlSessionFactory 为空,则先创建sqlSessionFactory !afterPropertiesSet();}returnthis.sqlSessionFactory;}

还实现了InitializingBean,InitializingBean中的afterPropertiesSet方法会在类初始化完成后调用,而上边的getObject() 方法在获取对象时会调用这个方法生成sqlSessionFactory

@OverridepublicvoidafterPropertiesSet()throwsException{notNull(dataSource,"Property 'dataSource' is required");notNull(sqlSessionFactoryBuilder,"Property 'sqlSessionFactoryBuilder' is required");state((configuration==null&& configLocation==null)||!(configuration!=null&& configLocation!=null),"Property 'configuration' and 'configLocation' can not specified with together");//创建sqlSessionFactory 的方法this.sqlSessionFactory=buildSqlSessionFactory();}

核心方法是buildSqlSessionFactory,在这个方法中,大部分都是为Configuration对象赋值!解析过程与Mybatis解析过程类似!最后通过建造者模式返回sqlSessionFactory的实现
DefaultSqlSessionFactory!

protectedSqlSessionFactorybuildSqlSessionFactory()throwsIOException{//局部变量ConfigurationConfiguration configuration;.......if(this.objectFactory!=null){
      configuration.setObjectFactory(this.objectFactory);}if(this.vfs!=null){
      configuration.setVfsImpl(this.vfs);}//设置类型别名if(!isEmpty(this.typeAliases)){for(Class<?> typeAlias:this.typeAliases){
        configuration.getTypeAliasRegistry().registerAlias(typeAlias);if(LOGGER.isDebugEnabled()){
          LOGGER.debug("Registered type alias: '"+ typeAlias+"'");}}}//添加插件if(!isEmpty(this.plugins)){for(Interceptor plugin:this.plugins){
        configuration.addInterceptor(plugin);if(LOGGER.isDebugEnabled()){
          LOGGER.debug("Registered plugin: '"+ plugin+"'");}}}//设置类型处理器if(!isEmpty(this.typeHandlers)){for(TypeHandler<?> typeHandler:this.typeHandlers){
        configuration.getTypeHandlerRegistry().register(typeHandler);if(LOGGER.isDebugEnabled()){
          LOGGER.debug("Registered type handler: '"+ typeHandler+"'");}}}//缓存if(this.cache!=null){
      configuration.addCache(this.cache);}//事务管理器//SpringManagedTransactionFactory是新定义的事务管理器,它使用Spring事务中的dataSource ,从而达到跟事务集成if(this.transactionFactory==null){this.transactionFactory=newSpringManagedTransactionFactory();}//环境
    configuration.setEnvironment(newEnvironment(this.environment,this.transactionFactory,this.dataSource));....... 省略//建造者模式 返回 DefaultSqlSessionFactoryreturnthis.sqlSessionFactoryBuilder.build(configuration);}

注意:

  • Spring集成mybatis,在处理事务时,事务工厂会使用一个新的new SpringManagedTransactionFactory
  • 而不是MyBatis之前的ManagedTransactionFactory, 这个SpringManagedTransactionFactory会使用Spring事务同步管理器TransactionSynchronizationManager中的dataSource , 从而达到跟事务集成


3. Spring是怎么管理Mapper接口的动态代理的

        Spring与Mybatis整合的最主要目的就是:把Mapper接口的代理对象放入Spring容器,在使用时能够像使用一个普通的bean一样去使用这个代理对象,比如能被@Autowired自动注入!

整合结果,能够像下面的方式一样使用代理对象

@ComponentpublicclassUserService{@AutowiredprivateUserMapper userMapper;publicUsergetUserById(Integer id){return userMapper.selectById(id);}}

UserService中的userMapper属性就会被自动注入为Mybatis中的代理对象。如果你基于一个已经完成整合的项目去调试即可发现,userMapper的类型为:org.apache.ibatis.binding.MapperProxy@41a0aa7d。证明确实是Mybatis中的代理对象。

那么问题来了:

        要想使用@AutoWired注入xxxMapper代理对象,Spring容器中必须要存在才行啊? 那么Spring是怎么把不同的Mapper代理对象作为一个bean放入容器中呢?

  • 猜想一:Spring中可以通过注解把类加入容器,比如@Controller、@Service、@Component等等,如果为Mapper接口加上@Component注解,是否可以放入Spring容器?
    答案:不能。
    要验证这个猜想,我们需要对Spring的bean生成过程 有一个了解。Spring启动过程中,大致会经过如下步骤去生成bean:
    1. 扫描@ComponentScan中指定的包路径下的class文件
    2. 根据class信息判断是否符合生成对应的BeanDefinition的条件(接口和抽象类不符合条件),如果符合则生成对应的BeanDefinition
    3. 在此处,程序员可以利用某些机制去修改BeanDefinition,实现扩展
    4. 根据BeanDefinition中的class信息反射生成bean实例
    5. 把生成的bean实例放入Spring容器中

        由此步骤可见,要想生成bean对象,首先需要有BeanDefinition。由于Mapper接口上加@Component在第2步就会被过滤掉,无法生成BeanDefinition,更无法生成实例。


  • 猜想二:使用@Bean把代理类注册进去,在@Bean代码中生成Mapper接口的动态代理!

    这种方式其实是可行的,因为Spring在进行bean实例化时有两种方式。

    • ①:带有@Component等注解的类直接利用class反射生成实例
    • ②:带有@Bean注解的则会使用工厂的方式生成实例

        显然第二种猜想是可行的,但是存在一个问题,一个项目可能有上百个Mapper,难道每一个都要写一个@Bean生成代理类?这岂不是累死个人!!Spring绝对不是靠这种方式整合的!


Sping的解决方案:

        先回到猜想一中的思路,在Mapper接口上加@Component注解时,Spring会在扫描@CompopnentScan指定的路径时,过滤掉接口、抽象类,不为他们生成BeanDefinition,这就导致了Mapper接口无法实例化!

//判断是否是顶级类、非接口类、非抽象类等等,整合Mybatis时,需要重写这个方法,让Mapper接口也加入集合!if(isCandidateComponent(sbd)){if(debugEnabled){
                logger.debug("Identified candidate component class: "+ resource);}//如果不是顶级类、接口类、抽象类,则加入到集合中!!!
            candidates.add(sbd);}else{//如果是顶级类、接口类、抽象类,则不予处理!!!if(debugEnabled){
                logger.debug("Ignored because not a concrete top-level class: "+ resource);}}========  isCandidateComponent 判断是否是顶级类、非接口类、非抽象类等等=======protectedbooleanisCandidateComponent(AnnotatedBeanDefinition beanDefinition){AnnotationMetadata metadata= beanDefinition.getMetadata();/**
		 * 判断是否是顶级类、非接口类、非抽象类等等
		 */return(metadata.isIndependent()&&(metadata.isConcrete()||(metadata.isAbstract()&& metadata.hasAnnotatedMethods(Lookup.class.getName()))));}

        然而,Spring在整合Mybatis时,为了让Mapper接口注册成BeanDefinition,重写了isCandidateComponent 方法!!,他让这个方法忽略了接口,让接口也可以被扫描到。

        但是接口也可以被扫描到也不能改变什么啊,因为Mapper接口只是一个接口,还没有为他生成代理类呢!!Spring为了解决这个问题,使用了FactoryBean的getObject方法进行了偷天换日,Mapper接口的BeanDefinition中设置其beanClassFactorybean.class,这样每个Mapper接口在生成实例的时候生成的是FactorybeangetObject方法中返回的代理类了!!

        再通过自定义类实现bean工厂后置处理器BeanDefinitionRegistryPostProcessor,重写postProcessBeanDefinitionRegistry(BeanDefinitionRegistry registry) 方法,修改成带有Factorybean.classBeanDefinition,这样就会按照我们自定义的BeanDefinition去生成对象! 这就完美解决了问题。

伪代码如下:

//扫描到的 MappersSet mappers=newHashSet();//遍历 Mappersfor(Object mapper: mappers){//新建一个bean定义RootBeanDefinition beanDefinition=newRootBeanDefinition();//偷天换日,设置MyfactoryBean的getObject方法返回的对象为mapper的Class
			beanDefinition.setBeanClass(MyfactoryBean.class);//每一次循环都为当前mapper创建动态代理
			beanDefinition.getPropertyValues().add("mapperInterFace",mapper.class);//最后注册成bean定义
			registry.registerBeanDefinition("userMapper",beanDefinition);}


4. Spring整合Mybatis源码大致流程

  1. spring会重写isCandidateComponent方法,来扫描到所有的mapper接口,并将所有mapper 的 bean定义中的class类型指向MapperFactoryBean
  2. Spring在创建UserServiceImpl实例的时候,发现其内部有@AutowiredUserMapper接口,那么就会去spring容器获取UserMapper实例,没有则进行创建
  3. 创建UserMapper实例的时候,根据bean定义创建的实例 实际上是MapperFactoryBean实例,然后再利用MapperFactoryBeangetObject方法获取mapper的代理实例(调用MapperFactoryBean的getObject方法,mybatis会利用jdk的动态代理创建mapper代理对象);

MapperFactoryBean 类如下:

publicclassMapperFactoryBean<T>extendsSqlSessionDaoSupportimplementsFactoryBean<T>{privateClass<T> mapperInterface;privateboolean addToConfig=true;publicMapperFactoryBean(){}publicMapperFactoryBean(Class<T> mapperInterface){this.mapperInterface= mapperInterface;}protectedvoidcheckDaoConfig(){super.checkDaoConfig();Assert.notNull(this.mapperInterface,"Property 'mapperInterface' is required");Configuration configuration=this.getSqlSession().getConfiguration();if(this.addToConfig&&!configuration.hasMapper(this.mapperInterface)){try{
                configuration.addMapper(this.mapperInterface);}catch(Exception var6){this.logger.error("Error while adding the mapper '"+this.mapperInterface+"' to configuration.", var6);thrownewIllegalArgumentException(var6);}finally{ErrorContext.instance().reset();}}}// getObject获取对应Mapper的代理类publicTgetObject()throwsException{returnthis.getSqlSession().getMapper(this.mapperInterface);}publicClass<T>getObjectType(){returnthis.mapperInterface;}publicbooleanisSingleton(){returntrue;}publicvoidsetMapperInterface(Class<T> mapperInterface){this.mapperInterface= mapperInterface;}publicClass<T>getMapperInterface(){returnthis.mapperInterface;}publicvoidsetAddToConfig(boolean addToConfig){this.addToConfig= addToConfig;}publicbooleanisAddToConfig(){returnthis.addToConfig;}}

在这里插入图片描述

  • 作者:知识分子_
  • 原文链接:https://blog.csdn.net/qq_45076180/article/details/109326712
    更新时间:2022-08-06 13:48:49