- 一、整合核心思路
- 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方法中:
publicT 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 MapperProxyimplements 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了)。



