栏目分类:
子分类:
返回
名师互学网用户登录
快速导航关闭
当前搜索
当前分类
子分类
实用工具
热门搜索
名师互学网 > IT > 软件开发 > 后端开发 > Java

Spring之整合MyBatis底层源码解析

Java 更新时间: 发布时间: IT归档 最新发布 模块sitemap 名妆网 法律咨询 聚返吧 英语巴士网 伯小乐 网商动力

Spring之整合MyBatis底层源码解析

文章目录
  • 一、整合核心思路
    • 1.1 Mybatis中Mapper的工作原理分析
        • JDK动态代理代理无实现类的接口
    • 1.2 mybatis整合spring思路梳理
      • 1.2.1 如果优雅的将mybatis中mapper的代理对象注入到容器中
  • 二、Mybatis-Spring 1.3.2版本底层源码执行流程
  • 三、Mybatis-Spring 2.0.6版本(最新版)底层源码执行流程
  • 四、Spring整合Mybatis后一级缓存失效问题

一、整合核心思路

由很多框架都需要和Spring进行整合,而整合的核心思想就是把其他框架所产生的对象放到Spring容器中,让其成为Bean。​

比如Mybatis,Mybatis框架可以单独使用,而单独使用Mybatis框架就需要用到Mybatis所提供的一些类构造出对应的对象,然后使用该对象,就能使用到Mybatis框架给我们提供的功能,和Mybatis整合Spring就是为了将这些对象放入Spring容器中成为Bean,只要成为了Bean,在我们的Spring项目中就能很方便的使用这些对象了,也就能很方便的使用Mybatis框架所提供的功能了。

1.1 Mybatis中Mapper的工作原理分析

我们平时使用mybatis的时候往往是和Spring一起使用的。其实Mybatis本身是可以单独使用的。


我们知道,我们平时只需要写一个Mapper接口就可以了,不需要写实现类。那mybatis是怎么构建Mapper的呢?

我们进入到sqlSession.getMapper方法中:

public  T getMapper(Class type, SqlSession sqlSession) {
   MapperProxyFactory mapperProxyFactory = (MapperProxyFactory)this.knownMappers.get(type);
   if (mapperProxyFactory == null) {
       throw new BindingException("Type " + type + " is not known to the MapperRegistry.");
   } else {
       try {
           return mapperProxyFactory.newInstance(sqlSession);
       } catch (Exception var5) {
           throw new BindingException("Error getting mapper instance. Cause: " + var5, var5);
       }
   }
}

public T newInstance(SqlSession sqlSession) {
    MapperProxy mapperProxy = new MapperProxy(sqlSession, this.mapperInterface, this.methodCache);
    return this.newInstance(mapperProxy);
}

// org.apache.ibatis.binding.MapperProxyFactory#newInstance(org.apache.ibatis.binding.MapperProxy)
// JDK动态代理返回代理的Mapper对象
protected T newInstance(MapperProxy mapperProxy) {
    return Proxy.newProxyInstance(this.mapperInterface.getClassLoader(), new Class[]{this.mapperInterface}, mapperProxy);
}

在最终的newInstance方法中,我们可以看到是使用的JDK的动态代理来返回一个代理的Mapper对象的。

那么现在我们要关注的应该是当我们使用这个代理对象执行某个Mapper中的方法的时候,代理逻辑是怎么样的呢?所以我们现在要关注这个mapperProxy,它里面应该是定义了具体的InvocationHandler中invoke方法的逻辑。

我们进入到org.apache.ibatis.binding.MapperProxy类中:

public class MapperProxy implements InvocationHandler, Serializable {
    private static final long serialVersionUID = -6424540398559729838L;
    private final SqlSession sqlSession;
    // 定义Mapper对应的Class,这里是写的比较灵活,因为可能有非常多的Mapper
    private final Class mapperInterface;
    private final Map methodCache;

    public MapperProxy(SqlSession sqlSession, Class mapperInterface, Map methodCache) {
        this.sqlSession = sqlSession;
        this.mapperInterface = mapperInterface;
        this.methodCache = methodCache;
    }

	// 代理逻辑
    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
        try {
        	// 如果对象是Object,直接反射执行该方法即可
            if (Object.class.equals(method.getDeclaringClass())) {
                return method.invoke(this, args);
            }

			// 如果方法是一个默认的方法:公共的、非抽象的、非static的方法,也是直接执行方法
            if (method.isDefault()) {
                return this.invokeDefaultMethod(proxy, method, args);
            }
        } catch (Throwable var5) {
            throw ExceptionUtil.unwrapThrowable(var5);
        }

		// 如果是其他的,比如接口中的抽象方法,就会执行一些额外的代理逻辑,比如缓存方法
        MapperMethod mapperMethod = this.cachedMapperMethod(method);
        // 执行Mapper中方法的时候的代理逻辑
        return mapperMethod.execute(this.sqlSession, args);
    }
 ........   

我们重点看一下mapperMethod.execute(this.sqlSession, args);方法:

public Object execute(SqlSession sqlSession, Object[] args) {
    Object result;
    Object param;
    switch(this.command.getType()) {
    case INSERT:
        param = this.method.convertArgsToSqlCommandParam(args);
        result = this.rowCountResult(sqlSession.insert(this.command.getName(), param));
        break;
    case UPDATe:
        param = this.method.convertArgsToSqlCommandParam(args);
        result = this.rowCountResult(sqlSession.update(this.command.getName(), param));
        break;
    case DELETE:
        param = this.method.convertArgsToSqlCommandParam(args);
        result = this.rowCountResult(sqlSession.delete(this.command.getName(), param));
        break;
    case SELECT:
        if (this.method.returnsVoid() && this.method.hasResultHandler()) {
            this.executeWithResultHandler(sqlSession, args);
            result = null;
        } else if (this.method.returnsMany()) {
            result = this.executeForMany(sqlSession, args);
        } else if (this.method.returnsMap()) {
            result = this.executeForMap(sqlSession, args);
        } else if (this.method.returnsCursor()) {
            result = this.executeForCursor(sqlSession, args);
        } else {
            param = this.method.convertArgsToSqlCommandParam(args);
            result = sqlSession.selectOne(this.command.getName(), param);
            if (this.method.returnsOptional() && (result == null || !this.method.getReturnType().equals(result.getClass()))) {
                result = Optional.ofNullable(result);
            }
        }
        break;
    case FLUSH:
        result = sqlSession.flushStatements();
        break;
    default:
        throw new BindingException("Unknown execution method for: " + this.command.getName());
    }

    if (result == null && this.method.getReturnType().isPrimitive() && !this.method.returnsVoid()) {
        throw new BindingException("Mapper method '" + this.command.getName() + " attempted to return null from a method with a primitive return type (" + this.method.getReturnType() + ").");
    } else {
        return result;
    }
}

public SqlCommandType getType() {
   return this.type;
}

这一下就明白了,我们在Mapper接口中定义的方法上一般会添加@Select、@Update、@Delete等注解,相当于规定了方法的类型。然后执行代理逻辑的时候会在switch(this.command.getType())中判断方法类型,然后执行org.apache.ibatis.binding.MapperMethod.MethodSignature#convertArgsToSqlCommandParam方法,将注解或者配置文件中的参数解析为sql语句,然后调用org.apache.ibatis.session.SqlSession#insert(java.lang.String, java.lang.Object)(或者其他的更新、删除、查询)方法执行sql并获得返回结果!

这样一来,我们对mybatis的整体工作流程应该是有了一个比较清晰的认知。

注意:JDK动态代理是代理的接口,即使这个接口没有任何的实现类,我们也能自己在invoke方法中实现代理逻辑并返回一个代理结果,甚至都不需要反射调用真正的方法(也无法调用,因为没有实现类意味着没有真正的对象)。

JDK动态代理代理无实现类的接口
public class ProxyTest2 {
    public static void main(String[] args) {
        UserService proxyInstance = (UserService) Proxy.newProxyInstance(UserService.class.getClassLoader(), new Class[]{UserService.class},
                new InvocationHandler() {
                    @Override
                    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
                        System.out.println("代理方法被执行了(执行的是代理逻辑,没有执行真正的方法)...");
                        return "sayHelloTo " + args[0];
                    }
                });

        String result = proxyInstance.sayHelloTo("jihu");
        System.out.println("代理逻辑返回的方法结果" + result);

    }
}

1.2 mybatis整合spring思路梳理

我们平时在项目中使用mybatis的时候往往是这样的:

@Service
public class UserService {
    @Autowired
    private UserMapper userMapper;
}

如果说现在我们来启动Spring容器的话:

思考:此时能启动成功吗?

有spring使用经验的小伙伴应该可以看出来,此时一定会报错:NoSuchBeanDefinition的错误。因为我们自己定义的UserMapper此时只是一个接口,根本就没有实现类,所以是无法将其注册成一个Bean的。实例化的时候就报错了。

所以说,此时我们必须要给UserMapper一个实例化对象,但是又不能创建实现类,那该怎么办到呢?艾,我们之前分析mybatis中Mapper的工作原理的时候,不是看到mybatis为所有的mapper创建了代理对象吗?那我们能不能将那个代理对象赋值给这个UserMapper呢?这时候我们使用userMapper执行具体方法的时候,不就相当于在使用mybaits了吗?

这样看来,我们需要在Spring的生命周期中找到合适的地方,自己来将这些mapper注入到容器中。

1.2.1 如果优雅的将mybatis中mapper的代理对象注入到容器中

思考,我们能通过普通的BeanDefinition来完成注册码?

显然是不行的,因为我们通过BeanDefinition来注册Bean的时候必须要指定类型,但是此时类型只能指定成接口,这显然是不行的,因为启动容器会直接报错,依然无法实例化对象。

那该如何解决呢?有没有可以自动生成一个对象然后赋值给Bean的呢?而不是让Spring自己去实例化对象?

熟悉Spring的小伙伴应该能想到FactoryBean。我们知道FactoryBean实例化的对象正是其getObject方法中返回的对象。那我们能够在getObject方法中获取到mybatis的代理对象然后将其返回,这样不就可以将代理对象注入到容器中了吗!!!

@Component  // 需要将factoryBean注入到Spring容器中
public class MapperFactoryBean implements FactoryBean {

    @Override
    public Object getObject() throws Exception {
        // 这里获取并返回Mybatis中创建的代理对象(暂时使用JDK动态代理代替...)
        // ====> 使用JDK代理生成代理对象
        UserMapper proxyInstance = (UserMapper) Proxy.newProxyInstance(UserMapper.class.getClassLoader(), new Class[]{UserMapper.class}, new InvocationHandler() {
            @Override
            public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
            	// 指定代理逻辑,解析sql并执行,然后返回执行结果
                return null;
            }
        });
        // 这样就返回了代理对象了
        return proxyInstance;
    }

    @Override
    public Class getObjectType() {
        return UserMapper.class;
    }
}

这样可以处理UserMapper了,但是思考一下,我们项目中一般会有非常多的Mpper,而且整合的时候也不知道项目中是哪些mapper呀???所以我们要把这个Mapper的类型写活:

@Component
public class MapperFactoryBean implements FactoryBean {

    // 将Mapper的类型写活
    private Class mapperInterface;

    public MapperFactoryBean(Class mapperInterface) {
        this.mapperInterface = mapperInterface;
    }
 ......   

这样我们对不同的Mapper传入不同的 mapperInterface即可。但是此时还有另一个问题,我们这里的MapperFactoryBean 上加了@Component注解,意味着只能生成一个MapperFactoryBean,这显然是不对的。应该是每个mapper都生成一个对应的代理对象。所以我们不能将MapperFactoryBean注入到容器中。

思路: 可以将每个MapperBe

那我们整合Spring的时候,我们知道需要引入一个mybatis-spring的依赖。

二、Mybatis-Spring 1.3.2版本底层源码执行流程

Spring整合Mybatis之后SQL执行流程:

1、通过@MapperScan导入了MapperScannerRegistrar类

2、MapperScannerRegistrar类实现了importBeanDefinitionRegistrar接口,所以Spring在启动时会调用MapperScannerRegistrar类中的registerBeanDefinitions方法

3、在registerBeanDefinitions方法中定义了一个ClassPathMapperScanner对象,用来扫描mapper

4、设置ClassPathMapperScanner对象可以扫描到接口,因为在Spring中是不会扫描接口的

5、同时因为ClassPathMapperScanner中重写了isCandidateComponent方法,导致isCandidateComponent只会认为接口是备选者Component

6、通过利用Spring的扫描后,会把接口扫描出来并且得到对应的BeanDefinition

7、接下来把扫描得到的BeanDefinition进行修改,把BeanClass修改为MapperFactoryBean,把AutowireMode修改为byType

8、扫描完成后,Spring就会基于BeanDefinition去创建Bean了,相当于每个Mapper对应一个FactoryBean

9、在MapperFactoryBean中的getObject方法中,调用了getSqlSession()去得到一个sqlSession对象,然后根据对应的Mapper接口生成一个Mapper接口代理对象,这个代理对象就成为Spring容器中的Bean

10、sqlSession对象是Mybatis中的,一个sqlSession对象需要SqlSessionFactory来产生

11、MapperFactoryBean的AutowireMode为byType,所以Spring会自动调用set方法,有两个set方法,一个setSqlSessionFactory,一个setSqlSessionTemplate,而这两个方法执行的前提是根据方法参数类型能找到对应的bean,所以Spring容器中要存在SqlSessionFactory类型的bean或者SqlSessionTemplate类型的bean。

12、如果你定义的是一个SqlSessionFactory类型的bean,那么最终也会被包装为一个SqlSessionTemplate对象,并且赋值给sqlSession属性

13、而在SqlSessionTemplate类中就存在一个getMapper方法,这个方法中就产生一个Mapper接口代理对象

14、到时候,当执行该代理对象的某个方法时,就会进入到Mybatis框架的底层执行流程。

三、Mybatis-Spring 2.0.6版本(最新版)底层源码执行流程

1、通过@MapperScan导入了MapperScannerRegistrar类

2、MapperScannerRegistrar类实现了importBeanDefinitionRegistrar接口,所以Spring在启动时会调用MapperScannerRegistrar类中的registerBeanDefinitions方法

3、在registerBeanDefinitions方法中注册一个MapperScannerConfigurer类型的BeanDefinition

4、而MapperScannerConfigurer实现了BeanDefinitionRegistryPostProcessor接口,所以Spring在启动过程中时会调用它的postProcessBeanDefinitionRegistry()方法

5、在postProcessBeanDefinitionRegistry方法中会生成一个ClassPathMapperScanner对象,然后进行扫描

6、后续的逻辑和1.3.2版本一样。

带来的好处是,可以不使用@MapperScan注解,而可以直接定义一个Bean,比如:

@Bean
public MapperScannerConfigurer mapperScannerConfigurer() {
	MapperScannerConfigurer mapperScannerConfigurer = new MapperScannerConfigurer();
	mapperScannerConfigurer.setbasePackage("com.jihu");
	return mapperScannerConfigurer;
}
四、Spring整合Mybatis后一级缓存失效问题

Mybatis中的一级缓存是基于SqlSession来实现的,所以在执行同一个sql时,如果使用的是同一个SqlSession对象,那么就能利用到一级缓存,提高sql的执行效率。​

但是在Spring整合Mybatis后,如果没有执行某个方法时,该方法上没有加@Transactional注解,也就是没有开启Spring事务,那么后面在执行具体sql时,没执行一个sql时都会新生成一个SqlSession对象来执行该sql,这就是我们说的一级缓存失效(也就是没有使用同一个SqlSession对象),而如果开启了Spring事务,那么该Spring事务中的多个sql,在执行时会使用同一个SqlSession对象,从而一级缓存生效,具体的底层执行流程在上图。​

实际上Spring整合Mybatis后一级缓存失效并不是问题,是正常的实现,因为,一个方法如果没有开启Spring事务,那么在执行sql时候,那就是每个sql单独一个事务来执行,也就是单独一个SqlSession对象来执行该sql,如果开启了Spring事务,那就是多个sql属于同一个事务,那自然就应该用一个SqlSession来执行这多个sql。

所以,在没有开启Spring事务的时候,SqlSession的一级缓存并不是失效了,而是存在的生命周期太短了(执行完一个sql后就被销毁了,下一个sql执行时又是一个新的SqlSession了)。​

转载请注明:文章转载自 www.mshxw.com
本文地址:https://www.mshxw.com/it/685611.html
我们一直用心在做
关于我们 文章归档 网站地图 联系我们

版权所有 (c)2021-2022 MSHXW.COM

ICP备案号:晋ICP备2021003244-6号