由很多框架都需要和Spring进行整合,而整合的核心思想就是把其他框架所产生的对象放到Spring容器中,让其成为Bean。
比如Mybatis,Mybatis框架可以单独使用,而单独使用Mybatis框架就需要用到Mybatis所提供的一些类构造出对应的对象,然后使用该对象,就能使用到Mybatis框架给我们提供的功能,和Mybatis整合Spring就是为了将这些对象放入Spring容器中成为Bean,只要成为了Bean,在我们的Spring项目中就能很方便的使用这些对象了,也就能很方便的使用Mybatis框架所提供的功能了。
我们平时使用mybatis的时候往往是和Spring一起使用的。其实Mybatis本身是可以单独使用的。
我们知道,我们平时只需要写一个Mapper接口就可以了,不需要写实现类。那mybatis是怎么构建Mapper的呢?
我们进入到sqlSession.getMapper
方法中:
public <T> T getMapper(Class<T> type, SqlSession sqlSession) {
MapperProxyFactory<T> 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<T> 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<T> 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<T> implements InvocationHandler, Serializable {
private static final long serialVersionUID = -6424540398559729838L;
private final SqlSession sqlSession;
// 定义Mapper对应的Class,这里是写的比较灵活,因为可能有非常多的Mapper
private final Class<T> mapperInterface;
private final Map<Method, MapperMethod> methodCache;
public MapperProxy(SqlSession sqlSession, Class<T> mapperInterface, Map<Method, MapperMethod> 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方法中实现代理逻辑并返回一个代理结果,甚至都不需要反射调用真正的方法(也无法调用,因为没有实现类意味着没有真正的对象)。
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);
}
}
我们平时在项目中使用mybatis的时候往往是这样的:
@Service
public class UserService {
@Autowired
private UserMapper userMapper;
}
如果说现在我们来启动Spring容器的话:
思考:此时能启动成功吗?
有spring使用经验的小伙伴应该可以看出来,此时一定会报错:NoSuchBeanDefinition的错误。因为我们自己定义的UserMapper此时只是一个接口,根本就没有实现类,所以是无法将其注册成一个Bean的。实例化的时候就报错了。
所以说,此时我们必须要给UserMapper一个实例化对象,但是又不能创建实现类,那该怎么办到呢?艾,我们之前分析mybatis中Mapper的工作原理的时候,不是看到mybatis为所有的mapper创建了代理对象吗?那我们能不能将那个代理对象赋值给这个UserMapper呢?这时候我们使用userMapper执行具体方法的时候,不就相当于在使用mybaits了吗?
这样看来,我们需要在Spring的生命周期中找到合适的地方,自己来将这些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注入到容器中。
思路: 我们回到使用BeanDefinition注册mapper的思路,我们可以将每个Mapper的beanDefiniton的类型设置为MapperFactoryBean,然后他们的名字不同。这样就可以成功的将所有Mapper注入到容器中去了。
修改后的MapperFactoryBean:
// 有多少个Mapper,就应该要生成多少个代理对象,如果加上@Component,就只能生成一个代理对象了。所以不能将其注入到容器!
public class MapperFactoryBean implements FactoryBean {
// 将Mapper的类型写活
private Class mapperInterface;
public MapperFactoryBean(Class mapperInterface) {
this.mapperInterface = mapperInterface;
}
@Override
public Object getObject() throws Exception {
// 这里获取并返回Mybatis中创建的代理对象(暂时使用JDK动态代理代替...)
// ====> 使用JDK代理生成代理对象
UserMapper proxyInstance = (UserMapper) Proxy.newProxyInstance(UserMapper.class.getClassLoader(), new Class[]{mapperInterface}, new InvocationHandler() {
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
return null;
}
});
// 这样就返回了代理对象了
return proxyInstance;
}
@Override
public Class<?> getObjectType() {
return mapperInterface;
}
}
注入beanDefinition:
AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext(AppConfig.class);
// ===> UserMapper
AbstractBeanDefinition beanDefinition = BeanDefinitionBuilder.genericBeanDefinition().getBeanDefinition();
// 指定bean的类型为MapperFactoryBean
beanDefinition.setBeanClass(MapperFactoryBean.class);
// 使用构造方法注入,让其对mapperInterface属性赋值
beanDefinition.getConstructorArgumentValues().addGenericArgumentValue(UserMapper.class);
context.registerBeanDefinition("userMapper", beanDefinition); // userMapper --> UserMapper代理对象
// ===> OrderMapper
AbstractBeanDefinition beanDefinition1 = BeanDefinitionBuilder.genericBeanDefinition().getBeanDefinition();
// 指定bean的类型为MapperFactoryBean
beanDefinition.setBeanClass(MapperFactoryBean.class);
// 使用构造方法注入,让其对mapperInterface属性赋值
beanDefinition.getConstructorArgumentValues().addGenericArgumentValue(OrderMapper.class);
context.registerBeanDefinition("orderMapper", beanDefinition); // orderMapper --> OrderMapper代理对象
我们知道,当FactoryBean作为一个bean的类型,在注入到spring容器中的时候,并不会实例化这个FactoryBean对象然后将其设置给当前bean,而是去调用其getObject方法获取到返回的对象,然后将这个结果作为当前bean的实例化对象。
这样一来,每一个Mapper注入的实例化对象其实就是getObject方法返回的对象。而我们在getObject方法中获取到mybatis的代理对象并返回,这样就成功将mybatis的代理对象设置给mapper。 这样当我们执行mapper对应方法的时候,就会执行mybatis中定义的代理逻辑了。
思考:此时问题是我们这样显示的创建beanDefinition不是很好,有没有其他的方式创建呢?
使用BeanDefinitionRegistryPostProcessor
完成注册:
@Component
public class MapperBeanDefinitionRegistryProcessor implements BeanDefinitionRegistryPostProcessor {
@Override
public void postProcessBeanDefinitionRegistry(BeanDefinitionRegistry registry) throws BeansException {
// ===> UserMapper
AbstractBeanDefinition beanDefinition = BeanDefinitionBuilder.genericBeanDefinition().getBeanDefinition();
// 指定bean的类型为MapperFactoryBean
beanDefinition.setBeanClass(MapperFactoryBean.class);
// 使用构造方法注入,让其对mapperInterface属性赋值
beanDefinition.getConstructorArgumentValues().addGenericArgumentValue(UserMapper.class);
registry.registerBeanDefinition("userMapper", beanDefinition); // userMapper --> UserMapper代理对象
// ===> OrderMapper
AbstractBeanDefinition beanDefinition1 = BeanDefinitionBuilder.genericBeanDefinition().getBeanDefinition();
// 指定bean的类型为MapperFactoryBean
beanDefinition.setBeanClass(MapperFactoryBean.class);
// 使用构造方法注入,让其对mapperInterface属性赋值
beanDefinition.getConstructorArgumentValues().addGenericArgumentValue(OrderMapper.class);
registry.registerBeanDefinition("orderMapper", beanDefinition); // orderMapper --> OrderMapper代理对象
}
@Override
public void postProcessBeanFactory(ConfigurableListableBeanFactory configurableListableBeanFactory) throws BeansException {
// ...
}
}
这样也可以完成beanDefinintion的注册,而且不用写在spring启动的时候注册了。
但是此时依然存在的问题是,我们无法知道有哪些Mapper。这时候有没有想起来我们平常使用的@MapperScan注解?我们可以扫描得所有的mapper,然后一个一个注入。所有还需要有扫描的支持。
我们先定义一个扫描注解然后将其添加到配置类上:
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.TYPE)
public @interface MyMapperScan {
String value();
}
@ComponentScan("com.jihu")
@MyMapperScan("com.jihu.mapper")
@Import(MapperImportBeanDefinitionRegistry.class)
public class AppConfig {
}
但是我们之前使用的BeanDefinitionRegistryPostProcessor 只有注册beanDefinition的功能,没有扫描的功能,所以我们要使用带扫描功能的ImportBeanDefinitionRegistrar
来完成整个Mapper的扫描和注入:
// 不能通过注解添加到Spring容器中,只能通过@Import
public class MapperImportBeanDefinitionRegistry implements ImportBeanDefinitionRegistrar {
@Override
public void registerBeanDefinitions(AnnotationMetadata importingClassMetadata, BeanDefinitionRegistry registry, BeanNameGenerator importBeanNameGenerator) {
// importingClassMetadata 注解的元数据信息
Map<String, Object> annotationAttributes = importingClassMetadata.getAnnotationAttributes(MyMapperScan.class.getName());
// 获取到扫描路径
String scanPath = (String) annotationAttributes.get("value");
// 扫描mapper(利用spring的扫描器)
// 1、此时mapper都是接口,spring是无法扫描到的。所以使用自定义的scanner,只扫描接口
MapperBeanDefinitionScanner scanner = new MapperBeanDefinitionScanner(registry);
// 2、spring将类扫描成bean的时候会判断有没有加@Component,这个逻辑是在一个includeFilter中完成的
// 我们使用mybatis不会在mapper中添加注解,所以需要自己定义一个includeFilter
// 增加一个包含过滤器,将所有的类(不管有没有添加注解)都扫描成beanDefinition
scanner.addExcludeFilter(new TypeFilter() {
@Override
public boolean match(MetadataReader metadataReader, MetadataReaderFactory metadataReaderFactory) throws IOException {
// 扫描到任何类或者接口都进行注入,不再判断是否加了@Component注解
return true;
}
});
// 完成扫描和beanDefiniton的注册
scanner.scan(scanPath);
}
}
// 自己实现扫描器,目的是为了能扫描到接口。Spring默认的不会扫描接口
public class MapperBeanDefinitionScanner extends ClassPathBeanDefinitionScanner {
public MapperBeanDefinitionScanner(BeanDefinitionRegistry registry) {
super(registry);
}
@Override
protected Set<BeanDefinitionHolder> doScan(String... basePackages) {
Set<BeanDefinitionHolder> beanDefinitionHolders = super.doScan(basePackages);
// spring扫描出来的beanDefinition的类型默认是不对的,我们需要自己修改一下
for (BeanDefinitionHolder beanDefinitionHolder : beanDefinitionHolders) {
BeanDefinition beanDefinition = beanDefinitionHolder.getBeanDefinition();
// 给MapperFactoryBean.mapperInterface传值(string会在类加载后自动转化成class)
beanDefinition.getConstructorArgumentValues().addGenericArgumentValue(beanDefinition.getBeanClassName());
beanDefinition.setBeanClassName(MapperFactoryBean.class.getName());
}
return beanDefinitionHolders;
}
@Override
protected boolean isCandidateComponent(AnnotatedBeanDefinition beanDefinition) {
// 支持接口扫描
return beanDefinition.getMetadata().isInterface();
}
}
现在我们可以扫描注册所有的Mapper,唯一还存在问题的就是如何在FactoryBean中返回mybatis的代理对象,因为我们之前使用的JDK代理对象进行替代的。
进行改造:
首先需要自己注入一个SqlSessionFactory用于实例化sqlSession:
@ComponentScan("com.jihu")
@MyMapperScan("com.jihu.mapper")
@Import(MapperImportBeanDefinitionRegistry.class)
public class AppConfig {
@Bean
public SqlSessionFactory sqlSessionFactory() throws IOException {
// 构建一个SqlSessionFactory
InputStream inputStream = Resources.getResourceAsStream("mybatis.xml");
SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build((InputStream) null);
return sqlSessionFactory;
}
}
然后修改BeanFactory:
public class MapperFactoryBean implements FactoryBean {
// 将Mapper的类型写活
private Class mapperInterface;
private SqlSession sqlSession;
public MapperFactoryBean(Class mapperInterface) {
this.mapperInterface = mapperInterface;
}
@Autowired // 调用setter方法之前,spring会先找到sqlSessionFactory对应的bean。所以我们需要自己注入一个SqlSessionFactory
public void setSqlSession(SqlSessionFactory sqlSessionFactory) {
sqlSessionFactory.getConfiguration().addMapper(mapperInterface);
// 实例化sqlSession
this.sqlSession = sqlSessionFactory.openSession();
}
@Override
public Object getObject() throws Exception {
// 返回mybatis代理对象
return sqlSession.getMapper(mapperInterface);
}
@Override
public Class<?> getObjectType() {
return mapperInterface;
}
}
这里其实可以省略掉setter方法上的@Autowired注解,这样可以完全脱离spring。但是需要修改自定义扫描器中的doScan方法:
@Override
protected Set<BeanDefinitionHolder> doScan(String... basePackages) {
Set<BeanDefinitionHolder> beanDefinitionHolders = super.doScan(basePackages);
// spring扫描出来的beanDefinition的类型默认是不对的,我们需要自己修改一下
for (BeanDefinitionHolder beanDefinitionHolder : beanDefinitionHolders) {
GenericBeanDefinition beanDefinition = (GenericBeanDefinition) beanDefinitionHolder.getBeanDefinition();
// 给MapperFactoryBean.mapperInterface传值(string会在类加载后自动转化成class)
beanDefinition.getConstructorArgumentValues().addGenericArgumentValue(beanDefinition.getBeanClassName());
beanDefinition.setBeanClassName(MapperFactoryBean.class.getName());
// 让MapperFactoryBean中的setter方法可以被自动执行(省去添加的@Autowired注解)
beanDefinition.setAutowireMode(AbstractBeanDefinition.AUTOWIRE_BY_TYPE);
}
return beanDefinitionHolders;
}
这样一来,我们就逐渐完善了spring整合mybatis的所有流程和思想。加下来我们就在源码中来验证我们的思路是否正确。
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框架的底层执行流程。
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;
}
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了)。
spring整合mybatis源码位置 :org.mybatis.spring.SqlSessionUtils#registerSessionHolder:
private static void registerSessionHolder(SqlSessionFactory sessionFactory, ExecutorType executorType, PersistenceExceptionTranslator exceptionTranslator, SqlSession session) {
// 如果开启了Spring事务,这里返回true,代表才会将SqlSessionHolder对象缓存到ThreadLocal中
// 如果没有开启,则不进行缓存。这也就是为什么mybatis整合spring之后缓存失效的原因。
if (TransactionSynchronizationManager.isSynchronizationActive()) {
Environment environment = sessionFactory.getConfiguration().getEnvironment();
if (environment.getTransactionFactory() instanceof SpringManagedTransactionFactory) {
LOGGER.debug(() -> {
return "Registering transaction synchronization for SqlSession [" + session + "]";
});
SqlSessionHolder holder = new SqlSessionHolder(session, executorType, exceptionTranslator);
TransactionSynchronizationManager.bindResource(sessionFactory, holder);
TransactionSynchronizationManager.registerSynchronization(new SqlSessionUtils.SqlSessionSynchronization(holder, sessionFactory));
holder.setSynchronizedWithTransaction(true);
holder.requested();
} else {
if (TransactionSynchronizationManager.getResource(environment.getDataSource()) != null) {
throw new TransientDataAccessResourceException("SqlSessionFactory must be using a SpringManagedTransactionFactory in order to use Spring transaction synchronization");
}
LOGGER.debug(() -> {
return "SqlSession [" + session + "] was not registered for synchronization because DataSource is not transactional";
});
}
} else {
LOGGER.debug(() -> {
return "SqlSession [" + session + "] was not registered for synchronization because synchronization is not active";
});
}
}
这样来讲,当mybatis整合spring之后,如果我们在方法上加了@Transaction注解,则方法中mybatis的一级缓存生效,否则不生效,因为每一个sql执行的时候都会创建新的sqlSession。
因为我们认为数据库的隔离级别要更加重要。比如我们配置了数据库隔离级别为读未提交,同时开启了mybaits的一级缓存。那么当我们在代码中第二次执行某个sql的时候,要以缓存为主(查询到的是第一次查询的结果),还是以隔离级别为主(期间可能产生新数据,所以会查询到更多的结果)为准呢?所以一般会关闭mybatis的一级缓存。
先来看单独使用mybatis的代码:
public static void main(String[] args) throws IOException {
InputStream inputStream = Resources.getResourceAsStream("mybatis.xml");
SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build((InputStream) null);
SqlSession sqlSession = sqlSessionFactory.openSession();
OrderDao mapper = sqlSession.getMapper(OrderDao.class);
String result = mapper.selectById();
}
再来看sqlSession对象:
public class DefaultSqlSession implements SqlSession {
private final Configuration configuration;
private final Executor executor;
private final boolean autoCommit;
private boolean dirty;
private List<Cursor<?>> cursorList;
public DefaultSqlSession(Configuration configuration, Executor executor, boolean autoCommit) {
this.configuration = configuration;
this.executor = executor;
this.dirty = false;
this.autoCommit = autoCommit;
}
public DefaultSqlSession(Configuration configuration, Executor executor) {
this(configuration, executor, false);
}
public <T> T selectOne(String statement) {
return this.selectOne(statement, (Object)null);
}
.......
可以看到里面有很多的共享属性,并且方法上都没有加锁。那么多个线程执行的时候,应该是有线程安全问题的???
其实在源码中我们可以发现,mybatis的代理对象是通过SqlSessionTemplate中的getMapper方法获取的:
public <T> T getMapper(Class<T> type) {
return this.getConfiguration().getMapper(type, this);
}
当我们执行某个Mapper.selectOne方法的时候,其实就是在调用sqlSession.selectOne
方法。
但是SqlSessionTemplate中的getMapper方法中参数传是this,相当于调用selectOne方法的时候是在调用sqlSessionTemplate.selectOne方法:
public <T> T selectOne(String statement, Object parameter) {
return this.sqlSessionProxy.selectOne(statement, parameter);
}
可以看到,这里又会调用sqlSessionProxy.selectOne方法,那么SqlSessionProxy又是什么呢?
当我们构造SqlSessionTemplate的时候就会创建这个SqlSessionProxy:
public SqlSessionTemplate(SqlSessionFactory sqlSessionFactory, ExecutorType executorType, PersistenceExceptionTranslator exceptionTranslator) {
Assert.notNull(sqlSessionFactory, "Property 'sqlSessionFactory' is required");
Assert.notNull(executorType, "Property 'executorType' is required");
this.sqlSessionFactory = sqlSessionFactory;
this.executorType = executorType;
this.exceptionTranslator = exceptionTranslator;
this.sqlSessionProxy = (SqlSession)Proxy.newProxyInstance(SqlSessionFactory.class.getClassLoader(), new Class[]{SqlSession.class}, new SqlSessionTemplate.SqlSessionInterceptor());
}
引入SqlSessionTemplate就是为了解决线程安全问题,方式是通过ThreadLocal。
这样当我们执行方法的时候都会进入到SqlSessionTemplate.SqlSessionInterceptor()代理逻辑中:
private class SqlSessionInterceptor implements InvocationHandler {
private SqlSessionInterceptor() {
}
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
SqlSession sqlSession = SqlSessionUtils.getSqlSession(SqlSessionTemplate.this.sqlSessionFactory, SqlSessionTemplate.this.executorType, SqlSessionTemplate.this.exceptionTranslator);
Object unwrapped;
try {
Object result = method.invoke(sqlSession, args);
if (!SqlSessionUtils.isSqlSessionTransactional(sqlSession, SqlSessionTemplate.this.sqlSessionFactory)) {
sqlSession.commit(true);
}
unwrapped = result;
} catch (Throwable var11) {
unwrapped = ExceptionUtil.unwrapThrowable(var11);
if (SqlSessionTemplate.this.exceptionTranslator != null && unwrapped instanceof PersistenceException) {
SqlSessionUtils.closeSqlSession(sqlSession, SqlSessionTemplate.this.sqlSessionFactory);
sqlSession = null;
Throwable translated = SqlSessionTemplate.this.exceptionTranslator.translateExceptionIfPossible((PersistenceException)unwrapped);
if (translated != null) {
unwrapped = translated;
}
}
throw (Throwable)unwrapped;
} finally {
if (sqlSession != null) {
SqlSessionUtils.closeSqlSession(sqlSession, SqlSessionTemplate.this.sqlSessionFactory);
}
}
return unwrapped;
}
我们重点关注SqlSession的获取,来看SqlSessionUtils.getSqlSession
方法:
public static SqlSession getSqlSession(SqlSessionFactory sessionFactory, ExecutorType executorType, PersistenceExceptionTranslator exceptionTranslator) {
Assert.notNull(sessionFactory, "No SqlSessionFactory specified");
Assert.notNull(executorType, "No ExecutorType specified");
// 从threadLocal中获取
SqlSessionHolder holder = (SqlSessionHolder)TransactionSynchronizationManager.getResource(sessionFactory);
SqlSession session = sessionHolder(executorType, holder);
// 找到了返回
if (session != null) {
return session;
} else {
// 找不到则创建,并添加到ThreadLocal中
LOGGER.debug(() -> {
return "Creating a new SqlSession";
});
session = sessionFactory.openSession(executorType);
registerSessionHolder(sessionFactory, executorType, exceptionTranslator, session);
return session;
}
}
我们看看SqlSessionHolder 是存在哪里的:
public abstract class TransactionSynchronizationManager {
private static final Log logger = LogFactory.getLog(TransactionSynchronizationManager.class);
private static final ThreadLocal<Map<Object, Object>> resources = new NamedThreadLocal("Transactional resources");
private static final ThreadLocal<Set<TransactionSynchronization>> synchronizations = new NamedThreadLocal("Transaction synchronizations");
private static final ThreadLocal<String> currentTransactionName = new NamedThreadLocal("Current transaction name");
private static final ThreadLocal<Boolean> currentTransactionReadOnly = new NamedThreadLocal("Current transaction read-only status");
private static final ThreadLocal<Integer> currentTransactionIsolationLevel = new NamedThreadLocal("Current transaction isolation level");
private static final ThreadLocal<Boolean> actualTransactionActive = new NamedThreadLocal("Actual transaction active");
......
这就是我们平常说的,mybatis数据库连接为何是线程安全的,我们都知道是存储在ThreadLocal中的,这回终于得到了验证。
ThreadLocal中存储的SqlSessionHolder 对象。这样每个sqlSession都在自己的线程栈中拥有一个独立的DefaultSqlSession对象,自然就不会存在线程安全问题了。