万字长文详解:SpringBoot-Mybatis源码剖析

news2024/11/27 7:25:54

目录

25d7158584b7836ec7d1abc366d09045.png

背景

传统的Mybaits开发方式,是通过mybatis-config.xml对框架进行全局配置,比如:一级缓存、主键生成器等。

而在SpringBoot发布后,通过引入 mybatis-spring-boot-starter依赖包,可以大大减少工作量,实现快速落地,可以参考此前的文章案例:SpringBoot集成Mybatis;下面我们结合SpringBoot分析Mybatis的初始化流程和执行流程。

预备知识

首先是预备知识,因为SpringBoot自动装配才使得Mybatis许多配置变得简洁,我们必须引入springboot的bean自动导入流程。

预备1:@SpringBootApplication注解加载

springboot启动的时候会读取META-INF\spring.factories文件,
把key=org.springframework.boot.autoconfigure.EnableAutoConfiguration的字符串作为类名去加载
(启动会配合META-INF\spring-autoconfigure-metadata.properties中的内容过滤掉不符合当前场景的)

预备2:Bean的实例化和初始化顺序

可以参考文章:《Spring源码:bean加载流程》

我们明确一下的几个原则:

  1. Bean的实例化和初始化是分开的步骤

  2. Bean实例化之后,不等初始化完成,就提前暴露了一个BeanFactory到内存,方便解决循环依赖问题

  3. 当前Bean实例化后,会先完成所有依赖注入,再进行初始化(包括afterProperties()接口/postProcessAfterInstantiation()接口实现等)

预备3:ObjectFactory、FactoryBean、BeanFactory

bc8513a0d0518b1eb981250e59c0d45d.png

源码分析

按照惯例,我们的源码分析,还是按照两条路线:框架Bean注册定义、框架Bean运行期。

一、框架Bean注册定义

1、spring.factories文件和Mapper接口文件的加载

1.1 pom依赖   
<!-- 整合mybatis相关依赖 -->
<dependency>
    <groupId>org.mybatis.spring.boot</groupId>
    <artifactId>mybatis-spring-boot-starter</artifactId>
    <version>2.1.2</version>
</dependency>

我们首先加入了mybatis-spring-boot-starter依赖,我们点进去可以看到,mybatis-spring-boot-starter还依赖了其他的第三方jar包:

<dependencies>
  <dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter</artifactId>
  </dependency>
  <dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-jdbc</artifactId>
  </dependency>
  <dependency>
    <groupId>org.mybatis.spring.boot</groupId>
    <artifactId>mybatis-spring-boot-autoconfigure</artifactId>
  </dependency>
  <dependency>
    <groupId>org.mybatis</groupId>
    <artifactId>mybatis</artifactId>
  </dependency>
  <dependency>
    <groupId>org.mybatis</groupId>
    <artifactId>mybatis-spring</artifactId>
  </dependency>
</dependencies>

留意还有:mybatis-spring-boot-autoconfigure这个依赖

1.2 UserServer启动类
// ...其他注解
@SpringBootApplication
@MapperScan("com.bryant.mapper") // mapper扫描的包路径
public class UserServer {


   public static void main(String[] args) {
       SpringApplication.run(UserServer.class, args);
   }


}

我们看到@SpringBootApplication,也就代表了会触发以下的自动装配流程

  • @SpringBootApplication注解 修饰了 启动类

  • @EnableAutoConfiguration注解 修饰了 @SpringBootApplication注解

  • @Import(AutoConfigurationImportSelector.class) 修饰了 @EnableAutoConfiguration注解

  • AutoConfigurationImportSelector会在@SpringBootApplication启动类启动时,触发自动加载jar包的META-INF/spring.factories里面的所有配置候选类

我们回到这个mybatis-spring-boot-autoconfigure依赖包,发现确有spring.factories文件

/mybatis-spring-boot-autoconfigure/2.1.2/mybatis-spring-boot-autoconfigure-2.1.2.jar!/META-INF/spring.factories

1.3 spring.factories
# Auto Configure
org.springframework.boot.autoconfigure.EnableAutoConfiguration=\
org.mybatis.spring.boot.autoconfigure.MybatisLanguageDriverAutoConfiguration,\
org.mybatis.spring.boot.autoconfigure.MybatisAutoConfiguration
  • MybatisAutoConfiguration这个是主要的Mybatis自动配置类

1.4 MapperScannerRegistrar的mapper接口加载

我们在启动类加了这么一个注解@MapperScan

@MapperScan("com.bryant.mapper") // mapper扫描的包路径

由于使用了@MapperScan,会触发其他资源引入

@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.TYPE)
@Documented
@Import(MapperScannerRegistrar.class)
@Repeatable(MapperScans.class)
public @interface MapperScan {
}

@Import会触发自动引入MapperScannerRegistrar类

public class MapperScannerRegistrar implements ImportBeanDefinitionRegistrar, ResourceLoaderAware {
    void registerBeanDefinitions(AnnotationMetadata annoMeta, AnnotationAttributes annoAttrs,
        BeanDefinitionRegistry registry, String beanName) {


      BeanDefinitionBuilder builder = BeanDefinitionBuilder.genericBeanDefinition(MapperScannerConfigurer.class);


      // 设置了MapperFactoryBean
      Class<? extends MapperFactoryBean> mapperFactoryBeanClass = annoAttrs.getClass("factoryBean");
        if (!MapperFactoryBean.class.equals(mapperFactoryBeanClass)) {
          builder.addPropertyValue("mapperFactoryBeanClass", mapperFactoryBeanClass);
        }
       // ...省略很多细节
      builder.addPropertyValue("basePackage", StringUtils.collectionToCommaDelimitedString(basePackages));
      registry.registerBeanDefinition(beanName, builder.getBeanDefinition());
    }
}
  • 设置了一个beanClass = MapperScannerConfigurer的BeanDefinitionBuilder

  • MapperScannerRegistrar的bean定义,设置了属性mapperFactoryBeanClass:即MapperFactoryBean工厂

  • 给MapperScannerRegistrar的bean定义,设置了属性basePackage:这里直接就是用的用户定义的路径值com.bryant.mapper

  • MapperScannerRegistrar实现了 ImportBeanDefinitionRegistrar接口,因此会触发方法,从而调用registerBeanDefinitions方法,完成BeanDefinitionRegistry对当前的MapperScannerRegistrar的bean定义注册(就是往BeanFactory加了一个BeanDefinition)

  • 最终触发了MapperScannerConfigurer的postProcessBeanDefinitionRegistry(等后续的【3.2】MapperScannerConfigurer)

2、MybatisAutoConfiguration的加载

@org.springframework.context.annotation.Configuration
@ConditionalOnClass({ SqlSessionFactory.class, SqlSessionFactoryBean.class })
@ConditionalOnSingleCandidate(DataSource.class)
@EnableConfigurationProperties(MybatisProperties.class)
@AutoConfigureAfter({ DataSourceAutoConfiguration.class, MybatisLanguageDriverAutoConfiguration.class })
public class MybatisAutoConfiguration implements InitializingBean {
}

代码分析:

  • @Configuration:声明这是配置类的注解

  • @ConditionalOnClass:条件注解,MybatisAutoConfiguration需要同时存在这两个类 - SqlSessionFactory、SqlSessionFactoryBean

  • @ConditionalOnSingleCandidate:条件注解,判断spring容器是否只有一个类型DataSource.class的bean的定义,或者有多个,但有一个主要的

  • @EnableConfigurationProperties:MybatisAutoConfiguration启用并加载配置MybatisProperties属性类

  • @AutoConfigureAfter:

    • 数据库自动加载:DataSourcePoolMetadataProvidersConfiguration

    • 处理动态SQL:MybatisLanguageDriverAutoConfiguration用于在 Spring Boot 应用程序中自动配置 MyBatis 的 LanguageDriver。LanguageDriver 是 MyBatis 中的一个组件,用于处理 SQL 映射文件中的动态 SQL 语句。

    • 先完成2个配置类加载 - DataSourceAutoConfiguration、MybatisLanguageDriverAutoConfiguration

Mybatis的自动化配置步骤非常多,Bean也很多,实际上可以用一个类加载流程图搞定:

c18d0beba5e16aaed5b7f9e74cdfb4fb.png

2.1、MybatisProperties

读取解析mybatis开头的配置,比如:

#指定mapper的配置文件的路径是mapper文件夹下的所有 xml文件。
mybatis.mapper-locations=classpath:mapper/*.xml
2.2、DataSourceAutoConfiguration

引入了DataSourceProperties,读取解析spring.data开头的配置,比如:

#数据源配置
spring.datasource.url = jdbc:mysql://localhost:3306/xxx?useSSL=false&useUnicode=true&characterEncoding=utf8&serverTimezone=Asia/Shanghai&rewriteBatchedStatements=true&allowPublicKeyRetrieval=true
spring.datasource.driver-class-name=com.mysql.cj.jdbc.Driver
spring.datasource.username=root
spring.datasource.password=12345678
spring.datasource.name=xxx

3、MapperScannerRegistrarNotFoundConfiguration配置类

这里跟1.5 的MapperScannerRegistrar的mapper接口加载是互斥关系,所以并不会同时发生。

正如其名,如果MapperScan相关的bean没有定义,那么就执行该配置类

@org.springframework.context.annotation.Configuration
@Import(AutoConfiguredMapperScannerRegistrar.class)
@ConditionalOnMissingBean({ MapperFactoryBean.class, MapperScannerConfigurer.class })
public static class MapperScannerRegistrarNotFoundConfiguration implements InitializingBean {
    @Override
    public void afterPropertiesSet() {
      logger.debug(
          "Not found configuration for registering mapper bean using @MapperScan, MapperFactoryBean and MapperScannerConfigurer.");
        }
      }
}
  • @ConditionalOnMissingBean:如果找不到 MapperFactoryBean 和 MapperScannerConfigurer,就执行该配置类初始化

    • 其实并没有别的成员属性要初始化的,有的只是引入AutoConfiguredMapperScannerRegistrar重新扫描注册bean

3.1、AutoConfiguredMapperScannerRegistrar

实现了2个接口:BeanFactoryAware、ImportBeanDefinitionRegistrar

public static class AutoConfiguredMapperScannerRegistrar implements BeanFactoryAware, ImportBeanDefinitionRegistrar {


  private BeanFactory beanFactory;


  @Override
  public void registerBeanDefinitions(AnnotationMetadata importingClassMetadata, BeanDefinitionRegistry registry) {


    if (!AutoConfigurationPackages.has(this.beanFactory)) {
      logger.debug("Could not determine auto-configuration package, automatic mapper scanning disabled.");
      return;
    }


    List<String> packages = AutoConfigurationPackages.get(this.beanFactory);
    if (logger.isDebugEnabled()) {
      packages.forEach(pkg -> logger.debug("Using auto-configuration base package '{}'", pkg));
    }


    BeanDefinitionBuilder builder = BeanDefinitionBuilder.genericBeanDefinition(MapperScannerConfigurer.class);
    builder.addPropertyValue("processPropertyPlaceHolders", true);
    builder.addPropertyValue("annotationClass", Mapper.class);
    builder.addPropertyValue("basePackage", StringUtils.collectionToCommaDelimitedString(packages));
    BeanWrapper beanWrapper = new BeanWrapperImpl(MapperScannerConfigurer.class);
    Stream.of(beanWrapper.getPropertyDescriptors())
        // Need to mybatis-spring 2.0.2+
        .filter(x -> x.getName().equals("lazyInitialization")).findAny()
        .ifPresent(x -> builder.addPropertyValue("lazyInitialization", "${mybatis.lazy-initialization:false}"));
    registry.registerBeanDefinition(MapperScannerConfigurer.class.getName(), builder.getBeanDefinition());
  }


  @Override
  public void setBeanFactory(BeanFactory beanFactory) {
    this.beanFactory = beanFactory;
  }


}
  • BeanFactoryAware接口:其实Spring框架会对setBeanFactory方法进行回调,填入BeanFactory属性,让当前bean获得对 BeanFactory 的访问权限

  • ImportBeanDefinitionRegistrar接口:会注册相关的BeanDefinition,允许我们注册额外的Bean定义,使得应用SpringContenxt启动时能够动态地加载和管理Bean;这里是用BeanDefinitionRegistry对 MapperScannerConfigurer 进行注册

3.2、MapperScannerConfigurer

由于MapperScannerConfigurer这个类实现了BeanDefinitionRegistryPostProcessor接口,所以它就会被生成bean之前加载,调用它的postProcessBeanDefinitionRegistry方法。

@Override
public void postProcessBeanDefinitionRegistry(BeanDefinitionRegistry registry) {
      if (this.processPropertyPlaceHolders) {
        // (1)设置属性值
        processPropertyPlaceHolders();
      }


      ClassPathMapperScanner scanner = new ClassPathMapperScanner(registry);
      scanner.setAddToConfig(this.addToConfig);
      scanner.setAnnotationClass(this.annotationClass);
      scanner.setMarkerInterface(this.markerInterface);
      scanner.setSqlSessionFactory(this.sqlSessionFactory);
      scanner.setSqlSessionTemplate(this.sqlSessionTemplate);
      scanner.setSqlSessionFactoryBeanName(this.sqlSessionFactoryBeanName);
      scanner.setSqlSessionTemplateBeanName(this.sqlSessionTemplateBeanName);
      scanner.setResourceLoader(this.applicationContext);
      scanner.setBeanNameGenerator(this.nameGenerator);
      // (2)这里设置了mapperFactoryBeanClass是MapperFactoryBean
      scanner.setMapperFactoryBeanClass(this.mapperFactoryBeanClass);
      if (StringUtils.hasText(lazyInitialization)) {
        scanner.setLazyInitialization(Boolean.valueOf(lazyInitialization));
      }
      scanner.registerFilters();
      // (3)依据属性值进行mapper接口扫描
      scanner.scan(StringUtils.tokenizeToStringArray(this.basePackage, 
          ConfigurableApplicationContext.CONFIG_LOCATION_DELIMITERS));
}


// (1)设置属性值
private void processPropertyPlaceHolders() {
      Map<String, PropertyResourceConfigurer> prcs = applicationContext.getBeansOfType(PropertyResourceConfigurer.class);
      if (!prcs.isEmpty() && applicationContext instanceof ConfigurableApplicationContext) {
        // 通过BeanFactory获取Bean定义
        BeanDefinition mapperScannerBean = 
        ((ConfigurableApplicationContext) applicationContext).getBeanFactory()
            .getBeanDefinition(beanName);
        // 取出"basePackage"属性值,并赋值
        PropertyValues values = mapperScannerBean.getPropertyValues();
        this.basePackage = updatePropertyValue("basePackage", values);
      }
      this.basePackage = Optional.ofNullable(this.basePackage).map(getEnvironment()::resolvePlaceholders).orElse(null);
}
  • (1)processPropertyPlaceHolders:完成"basePackage"属性值赋值

  • (2)setMapperFactoryBeanClass:完成“mapperFactoryBeanClass”属性值复制,声明是用MapperFactoryBean工厂去初始化Mapper对象

  • (3)这里就是要根据传入的包名去做扫描了,这里的this.basePackage就是上面说的 mapper文件基础路径

题外话:basePackage的值从何而来呢?

参考【1】的mapper接口配置注入,MapperScannerRegistrar最终往BeanFactory加入了一个名为MapperScannerRegistrar的BeanDefinition,它的basePackage的值 = com.bryant.mapper。

那么在【3】的MapperScannerConfigurer会通过beanName=MapperScannerRegistrar,从BeanFactory取出一个名为MapperScannerRegistrar的BeanDefinition,然后再提取basePackage的值 = com.bryant.mapper。

好了,现在我们聊到如何去扫描对应的包路径文件。

3.3、ClassPathMapperScanner

继承了ClassPathBeanDefinitionScanner父类,doScan内部会回调父类方法,然后进行bean定义处理。

org.springframework.context.annotation.ClassPathBeanDefinitionScanner#scan


@Override
public Set<BeanDefinitionHolder> doScan(String... basePackages) {
    // (1)调用父类ClassPathBeanDefinitionScanner#doScan方法
  Set<BeanDefinitionHolder> beanDefinitions = super.doScan(basePackages);


  if (beanDefinitions.isEmpty()) {
    LOGGER.warn(() -> "No MyBatis mapper was found in '" + Arrays.toString(basePackages)
        + "' package. Please check your configuration.");
  } else {
      // (2)处理bean定义集合
    processBeanDefinitions(beanDefinitions);
  }


  return beanDefinitions;
}


//(1)org.springframework.context.annotation.ClassPathBeanDefinitionScanner#doScan
protected Set<BeanDefinitionHolder> doScan(String... basePackages) {
   Assert.notEmpty(basePackages, "At least one base package must be specified");
   Set<BeanDefinitionHolder> beanDefinitions = new LinkedHashSet<>();
   for (String basePackage : basePackages) {
       //这里就是扫描类路径下的mapper注解类了。
      Set<BeanDefinition> candidates = findCandidateComponents(basePackage);
      for (BeanDefinition candidate : candidates) {
         ScopeMetadata scopeMetadata = this.scopeMetadataResolver.resolveScopeMetadata(candidate);
         candidate.setScope(scopeMetadata.getScopeName());
         //获取bean的名字
         String beanName = this.beanNameGenerator.generateBeanName(candidate, this.registry);
         if (candidate instanceof AbstractBeanDefinition) {
            postProcessBeanDefinition((AbstractBeanDefinition) candidate, beanName);
         }
         if (candidate instanceof AnnotatedBeanDefinition) {
            AnnotationConfigUtils.processCommonDefinitionAnnotations((AnnotatedBeanDefinition) candidate);
         }
         //这个是判断beanFactory是否包含beanName的bean的定义,不包含就会进入分支,这个分支也没啥特殊的,就是把bean的定义添加到beanFactory中
         if (checkCandidate(beanName, candidate)) {
            BeanDefinitionHolder definitionHolder = new BeanDefinitionHolder(candidate, beanName);
            definitionHolder =
                  AnnotationConfigUtils.applyScopedProxyMode(scopeMetadata, definitionHolder, this.registry);
            beanDefinitions.add(definitionHolder);
            registerBeanDefinition(definitionHolder, this.registry);
         }
      }
   }
   return beanDefinitions;
}


private void processBeanDefinitions(Set<BeanDefinitionHolder> beanDefinitions) {
  GenericBeanDefinition definition;
  for (BeanDefinitionHolder holder : beanDefinitions) {
    // the mapper interface is the original class of the bean
    // but, the actual class of the bean is MapperFactoryBean
    definition.getConstructorArgumentValues().addGenericArgumentValue(beanClassName); 
    definition = (GenericBeanDefinition) holder.getBeanDefinition();


    String beanClassName = definition.getBeanClassName();
    definition.setBeanClass(this.mapperFactoryBeanClass);


    if (!explicitFactoryUsed) {
      LOGGER.debug(() -> "Enabling autowire by type for MapperFactoryBean with name '" + holder.getBeanName() + "'.");
      definition.setAutowireMode(AbstractBeanDefinition.AUTOWIRE_BY_TYPE);
    }
  }
}
  • (1)findCandidateComponents:这里就是扫描类路径下的mapper注解类了

    • candidates:传入的包名是com.bryant.mapper,就会被转换成classpath*:com/bryant/mapper/**/*.class这个路径进行解析查找,将找到的类作为BeanDefinition的定义返回

  • (2)postProcessBeanDefinition

    • 因为mapper是接口,是无法new一个对象的,所以这里要赋予BeanDefinition一个构造器参数

    • 该bean的属性注入模式为AUTOWIRE_BY_TYPE

经过上面步骤,我们写好的Mapper接口,会被框架扫描到,并做好了Bean定义的准备,那么这个MapperBean究竟是生成啥样的对象呢?

3.4、MapperFactoryBean:Mapper对象初始化

我们回顾一下上面【3】的第3点 MapperScannerConfigurer;

它创建的 ClassPathMapperScanner 扫描器过程里,其中一个步骤是setMapperFactoryBeanClass,会去赋值工厂类为MapperFactoryBean,就是说后面的Mapper动态代理对象,都是基于MapperFactoryBean#GetObject生产出来的。

我们进入MapperFactoryBean源码

MapperFactoryBean是继承了SqlSessionDaoSupport,SqlSessionDaoSupport封装了一个SqlSessionTemplate。

public class MapperFactoryBean<T> extends SqlSessionDaoSupport implements FactoryBean<T> {


    Override
    public T getObject() throws Exception {
      return getSqlSession().getMapper(this.mapperInterface);
    }


}
  • 当 Spring 容器需要实例化一个 Mapper 接口的 Bean 时,它会调用 MapperFactoryBean 的 getObject 方法。

  • MapperFactoryBean 内部会使用 MyBatis 的 SqlSession 来创建 Mapper 接口的动态代理对象。

  • MapperFactoryBean#getObject():本质是SqlSessionTemplate#getMapper

我们进入DefaultSqlSession
public class DefaultSqlSession implements SqlSession {
    private final Configuration configuration;
    private final Executor executor;


    @Override
    public <T> T getMapper(Class<T> type) {
      return configuration.getMapper(type, this);
    }
}
  • 果然里面是封装了Configuration连接和Executor执行器

  • SqlSessionTemplate#getMapper:本质是Configuration#getMapper

我们进入Configuration
public class Configuration {
    protected final MapperRegistry mapperRegistry = new MapperRegistry(this);


    public <T> T getMapper(Class<T> type, SqlSession sqlSession) {
      return mapperRegistry.getMapper(type, sqlSession);
    }
}
  • 果然里面是封装了MapperRegistry注册表

  • Configuration#getMapper:本质是MapperRegistry#getMapper

我们进入MapperRegistry
public class MapperRegistry {


    private final Map<Class<?>, MapperProxyFactory<?>> knownMappers = new HashMap<>();


    public <T> T getMapper(Class<T> type, SqlSession sqlSession) {
      final MapperProxyFactory<T> mapperProxyFactory = (MapperProxyFactory<T>) knownMappers.get(type);
      if (mapperProxyFactory == null) {
        throw new BindingException("Type " + type + " is not known to the MapperRegistry.");
      }
      try {
        return mapperProxyFactory.newInstance(sqlSession);
      } catch (Exception e) {
        throw new BindingException("Error getting mapper instance. Cause: " + e, e);
      }
    }
}
  • 真相呼之欲出,最后暴露了一个代理工厂类MapperProxyFactory

  • 通过MapperProxyFactory,来创建Mapper代理对象

  • mapperProxyFactory.newInstance(sqlSession):本质是调用MapperProxyFactory

我们进入MapperProxyFactory
public class MapperProxyFactory<T> {


  protected T newInstance(MapperProxy<T> mapperProxy) {
    return (T) Proxy.newProxyInstance(mapperInterface.getClassLoader(), new Class[] { mapperInterface }, mapperProxy);
  }


  public T newInstance(SqlSession sqlSession) {
    final MapperProxy<T> mapperProxy = new MapperProxy<>(sqlSession, mapperInterface, methodCache);
    return newInstance(mapperProxy);
  }
}
  • 最终新建了一个MapperProxy

  • 并通过Proxy.newProxyInstance,设置了一个代理对象MapperImpl,代理对象MapperImpl实现了全部的Mapper接口,并且通过MapperProxy来实现逻辑增强

我们进入MapperProxy
public class MapperProxy<T> implements InvocationHandler, Serializable {


    private final SqlSession sqlSession;


    @Override
    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
      try {
        if (Object.class.equals(method.getDeclaringClass())) {
          return method.invoke(this, args);
        } else {
          return cachedInvoker(method).invoke(proxy, method, args, sqlSession);
        }
      } catch (Throwable t) {
        throw ExceptionUtil.unwrapThrowable(t);
      }
    }
}

代码分析:

  1. 在invoke方法中,首先判断当前代理对象是否是Object类,如果是,则直接调用当前对象的方法,否则,则使用缓存的Invoker对象来调用目标对象的方法。

  2. 增强方法invoke的主要步骤是cachedInvoker方法,是用来获取缓存的Invoker对象,如果缓存中不存在该对象,则创建一个新的Invoker对象,并将其缓存起来。

cachedInvoker

private MapperMethodInvoker cachedInvoker(Method method) throws Throwable {
  try {
    return methodCache.computeIfAbsent(method, m -> {
      if (m.isDefault()) {
       // ...这里是对接口的非公开方法(default方法、static方法等的处理)
      } else {
      // 默认走的是PlainMethodInvoker
        return new PlainMethodInvoker(
            new MapperMethod(mapperInterface, method, sqlSession.getConfiguration()));
      }
    });
  } catch (RuntimeException re) {
    Throwable cause = re.getCause();
    throw cause == null ? re : cause;
  }
}
  • 正常情况的接口都是public的,会触发PlainMethodInvoker的创建

  • PlainMethodInvoker最核心的,是对MapperMethod的封装,MapperMethod也是最核心的Mapper方法了

我们进入PlainMethodInvoker
private static class PlainMethodInvoker implements MapperMethodInvoker {
  private final MapperMethod mapperMethod;


  public PlainMethodInvoker(MapperMethod mapperMethod) {
    super();
    this.mapperMethod = mapperMethod;
  }


  @Override
  public Object invoke(Object proxy, Method method, Object[] args, SqlSession sqlSession) throws Throwable {
    return mapperMethod.execute(sqlSession, args);
  }
}
  • PlainMethodInvoker封装了一个MapperMethod,其invoke实际上也是对MapperMethod的调用

我们进入MapperMethod

构造器

public class MapperMethod {
    public MapperMethod(Class<?> mapperInterface, Method method, Configuration config) {
      this.command = new SqlCommand(config, mapperInterface, method);
      this.method = new MethodSignature(config, mapperInterface, method);
    }
}
  • MapperMethod内部会构建一个SqlCommand,以及MethodSignature

以上就是Mybatis对SqlSession的代码封装了,的确非常复杂,对象的调用链非常长,但我们用一张图来总结下也不是不行:

bd2667bb2d7e88369de0b3ab79f1fd10.png

4、sqlSessionFactory的加载

@Bean
@ConditionalOnMissingBean
public SqlSessionFactory sqlSessionFactory(DataSource dataSource) throws Exception {
  SqlSessionFactoryBean factory = new SqlSessionFactoryBean();
  //...省略属性设置
  return factory.getObject();
}

如果缺少SqlSessionFactory,则默认调用这个方法

5、sqlSessionTemplate的加载

@Bean
@ConditionalOnMissingBean
public SqlSessionTemplate sqlSessionTemplate(SqlSessionFactory sqlSessionFactory) {
  ExecutorType executorType = this.properties.getExecutorType();
  if (executorType != null) {
    return new SqlSessionTemplate(sqlSessionFactory, executorType);
  } else {
    return new SqlSessionTemplate(sqlSessionFactory);
  }
}


public SqlSessionTemplate(SqlSessionFactory sqlSessionFactory, ExecutorType executorType,
    PersistenceExceptionTranslator exceptionTranslator) {
  this.sqlSessionFactory = sqlSessionFactory;
  this.executorType = executorType;
  this.exceptionTranslator = exceptionTranslator;
  this.sqlSessionProxy = (SqlSession) newProxyInstance(
              SqlSessionFactory.class.getClassLoader(),
              new Class[] { SqlSession.class }, new SqlSessionInterceptor());
}
  • 如果缺少SqlSessionTemplate,则默认调用这个方法

  • SqlSessionTemplate的构造器,最终是调用了Proxy类的动态代理,生成对SqlSession的代理对象。

    • 在 JDK 动态代理中,生成的代理类 $Proxy 是继承 Proxy 并且实现 SqlSession 接口,当调用代理类的方法时,会进入到拦截器 SqlSessionInterceptor 的 invoke 方法中

5.1、SqlSessionInterceptor增强器
SqlSessionInterceptor


private class SqlSessionInterceptor implements InvocationHandler {
  @Override
  public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
    SqlSession sqlSession = getSqlSession(SqlSessionTemplate.this.sqlSessionFactory,
        SqlSessionTemplate.this.executorType, SqlSessionTemplate.this.exceptionTranslator);
    try {
      Object result = method.invoke(sqlSession, args);
      if (!isSqlSessionTransactional(sqlSession, SqlSessionTemplate.this.sqlSessionFactory)) {
        // force commit even on non-dirty sessions because some databases require
        // a commit/rollback before calling close()
        sqlSession.commit(true);
      }
      return result;
    } catch (Throwable t) {
      Throwable unwrapped = unwrapThrowable(t);
      if (SqlSessionTemplate.this.exceptionTranslator != null && unwrapped instanceof PersistenceException) {
        // release the connection to avoid a deadlock if the translator is no loaded. See issue #22
        closeSqlSession(sqlSession, SqlSessionTemplate.this.sqlSessionFactory);
        sqlSession = null;
        Throwable translated = SqlSessionTemplate.this.exceptionTranslator
            .translateExceptionIfPossible((PersistenceException) unwrapped);
        if (translated != null) {
          unwrapped = translated;
        }
      }
      throw unwrapped;
    } finally {
      if (sqlSession != null) {
        closeSqlSession(sqlSession, SqlSessionTemplate.this.sqlSessionFactory);
      }
    }
  }
}

SqlSessionInterceptor代理的逻辑比较复杂,下面我们用一张图来表示其过程:

d6cd07473c134400b79d2680ffb12ca7.png

5.2、SqlSession的线程安全

刚刚我们从源码得知,获取会话的操作getSqlSession源码,SqlSession的线程安全是通过TransactionSynchronizationManager事务同步管理器来实现的。

public abstract class TransactionSynchronizationManager {
    private static final ThreadLocal<Map<Object, Object>> resources =
          new NamedThreadLocal<>("Transactional resources");


    /**
     * Gets an SqlSession from Spring Transaction Manager or creates a new one if needed. Tries to get a SqlSession out of
     * current transaction. If there is not any, it creates a new one. Then, it synchronizes the SqlSession with the
     * transaction if Spring TX is active and <code>SpringManagedTransactionFactory</code> is configured as a transaction
     * manager.
     */
    public static SqlSession getSqlSession(SqlSessionFactory sessionFactory, ExecutorType executorType,
        PersistenceExceptionTranslator exceptionTranslator) {


      SqlSessionHolder holder = (SqlSessionHolder) 
              TransactionSynchronizationManager.getResource(sessionFactory);


      SqlSession session = sessionHolder(executorType, holder);
      if (session != null) {
        return session;
      }


      LOGGER.debug(() -> "Creating a new SqlSession");
      session = sessionFactory.openSession(executorType);


      registerSessionHolder(sessionFactory, executorType, exceptionTranslator, session);


      return session;
    }


    @Nullable
    public static Object getResource(Object key) {
       Object actualKey = TransactionSynchronizationUtils.unwrapResourceIfNecessary(key);
       Object value = doGetResource(actualKey);
       return value;
    }


    @Nullable
    private static Object doGetResource(Object actualKey) {
       Map<Object, Object> map = resources.get();
       if (map == null) {
          return null;
       }
       Object value = map.get(actualKey);
       // Transparently remove ResourceHolder that was marked as void...
       if (value instanceof ResourceHolder && ((ResourceHolder) value).isVoid()) {
          map.remove(actualKey);
          // Remove entire ThreadLocal if empty...
          if (map.isEmpty()) {
             resources.remove();
          }
          value = null;
       }
       return value;
    }
}

代码分析:

  • TransactionSynchronizationManager事务同步管理器的底层,维护了一个ThreadLocal

  • 通过线程封闭的方式,实现了SqlSession线程安全

  • 更多关于ThreadLocal的文章,可以参考:

    • 线程安全反思录(上):ThreadLocal到底安全不?

    • JDK源码ThreadLocal类

5.3、小结

经过上面的流程,sqlSessionTemplate已经构建完成了,那么后面就是sqlSessionTemplate发挥作用的时刻了!

但是,Mybatis作为简洁高效的ORM框架,肯定不会直接暴露sqlSessionTemplate给用户去调用的,而是做了层层封装,使得更加切合业务开发习惯。

二、框架Bean运行期

当我们要查询一个SQL时,我们是怎么做到的?是不是按照以下的步骤:

  • @Autowired注入UserMapper

  • 使用UserMapper的insert/update/delete/select接口

  • 动态映射到Mapper.xml的sql执行

  • 拿到组装结果并返回

我们上面已经知道了,@Autowired注入的Mapper,被调用时,最终会触发动态代理类的执行;上面经过分析,每个Mapper对象的方法,最终都会转化成一个MapperMethod,而这里面是一层非常复杂的对象封装链,如下图:

1506531eea2a213a9d423dd17ae54895.png

1、MapperMethod执行

上面说到的MapperMethod,就是最后SQL被执行的地方了,最核心的方法是execute方法:

org.apache.ibatis.binding.MapperMethod#execute


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

代码分析:

  • INSERT

    • convertArgsToSqlCommandParam:使用ParamNameResolver处理实参列表

    • 调用了sqlSession的insert

    • rowCountResult:返回值类型进行转换(非常人性化,给框架点赞)

  • UPDATE

    • 处理流程同INSERT类似

    • 调用了sqlSession的update

  • DELETE

    • 处理流程同INSERT类似

    • 调用了sqlSession的delete

  • SELECT

    • executeWithResultHandler:处理返回值为Void且包含ResultHandler

    • executeForMany:处理返回值为集合或数组

    • executeForMap:处理返回值为Map

    • executeForCursor:处理返回值为Cursor

    • 剩下的就返回单一对象的情况

你以为分析到此就结束?不,还有sqlSession的执行!

2、SqlSession执行

DefaultSqlSession实现了SqlSession接口定义的方法,并且为每种数据库操作提供了重载,见下图:

6e68b48a6edcc565bf675b8cd21378e3.png

2.1、insert

额,底层原来是调用的update。。

@Override
public int insert(String statement, Object parameter) {
  return update(statement, parameter);
}
2.2、update
@Override
public int update(String statement, Object parameter) {
  try {
    dirty = true;
    MappedStatement ms = configuration.getMappedStatement(statement);
    return executor.update(ms, wrapCollection(parameter));
  } catch (Exception e) {
    throw ExceptionFactory.wrapException("Error updating database.  Cause: " + e, e);
  } finally {
    ErrorContext.instance().reset();
  }
}
  • 获取MappedStatement

  • 通过Executor进行update操作

2.3、delete

额,底层原来也是调用的update。。

@Override
public int delete(String statement) {
  return update(statement, null);
}
2.4、select
@Override
public void select(String statement, Object parameter, RowBounds rowBounds, ResultHandler handler) {
  try {
    MappedStatement ms = configuration.getMappedStatement(statement);
    executor.query(ms, wrapCollection(parameter), rowBounds, handler);
  } catch (Exception e) {
    throw ExceptionFactory.wrapException("Error querying database.  Cause: " + e, e);
  } finally {
    ErrorContext.instance().reset();
  }
}
  • 获取MappedStatement

  • 通过Executor进行query操作

你以为分析到此就结束?不,还有Executor的执行!

3、Executor执行

BaseExecutor是模板类,包含了基础的数据库操作。

3.1、update
org.apache.ibatis.executor.BaseExecutor#update


@Override
public int update(MappedStatement ms, Object parameter) throws SQLException {
  ErrorContext.instance().resource(ms.getResource()).activity("executing an update").object(ms.getId());
  if (closed) {
    throw new ExecutorException("Executor was closed.");
  }
  clearLocalCache();
  return doUpdate(ms, parameter);
}

SimpleExecutor则是默认的执行器,我们主要围绕它进行展开,继续关注doUpdate方法,继续往下debug到SimpleExecutor的内容。

org.apache.ibatis.executor.SimpleExecutor#doUpdate


/**
 * 执行更新操作的实现方法
 * 
 * @param ms 映射语句对象,包含执行的SQL信息
 * @param parameter 传递给SQL的参数
 * @return 更新操作影响的行数
 * @throws SQLException 如果执行过程中发生SQL异常
 */
@Override
public int doUpdate(MappedStatement ms, Object parameter) throws SQLException {
  Statement stmt = null;
  try {
    // 获取配置对象,用于创建StatementHandler
    Configuration configuration = ms.getConfiguration();
    // 创建StatementHandler,负责执行SQL语句
    StatementHandler handler = configuration.newStatementHandler(this, ms, parameter, RowBounds.DEFAULT, null, null);
    // 准备SQL语句并返回Statement对象
    stmt = prepareStatement(handler, ms.getStatementLog());
    // 执行更新操作并返回影响的行数
    return handler.update(stmt);
  } finally {
    // 关闭Statement资源
    closeStatement(stmt);
  }
}

代码分析:

  • 获取Configuration信息

  • 新建一个StatementHandler

  • prepareStatement:预处理

  • handler.update(stmt):执行更新操作并返回影响的行数

我们发现,最终的执行,是交给了StatementHandler的实现类来操作的

3.2、select

BaseExecutor#query:提供了缓存管理和事务管理的基本功能

org.apache.ibatis.executor.BaseExecutor#query


/**
 * 查询数据并返回结果列表
 * 
 * @param ms 映射语句对象,包含SQL模板和其他配置信息
 * @param parameter 传递给SQL语句的参数
 * @param rowBounds 用于分页查询的行范围
 * @param resultHandler 结果处理器,用于处理查询结果
 * @return 返回一个包含查询结果的列表
 * @throws SQLException 如果查询过程中发生SQL异常
 * 
 * 此方法是执行查询的核心方法之一它负责根据提供的参数和配置信息执行SQL查询,
 * 并将结果传递给结果处理器进行处理或者封装成列表返回
 */
@Override
public <E> List<E> query(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler) throws SQLException {
  // 准备SQL语句并生成缓存键CacheKey
  BoundSql boundSql = ms.getBoundSql(parameter);
  CacheKey key = createCacheKey(ms, parameter, rowBounds, boundSql);
  // 执行查询并返回结果
  return query(ms, parameter, rowBounds, resultHandler, key, boundSql);
}




/**
 * 查询数据库并返回结果列表
 * 此方法是执行查询的核心部分,负责处理缓存、数据库查询以及结果处理
 * 
 * @param ms 映射语句对象,包含查询的SQL和配置信息
 * @param parameter 查询参数,可以是任何类型
 * @param rowBounds 行范围对象,用于分页查询
 * @param resultHandler 结果处理器,用于处理查询结果
 * @param key 缓存键,用于标识查询缓存
 * @param boundSql 准备好的SQL语句对象
 * @return 查询结果列表,元素类型为E
 * @throws SQLException 如果查询过程中发生数据库访问错误
 */
@Override
public <E> List<E> query(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler, CacheKey key, BoundSql boundSql) throws SQLException {  
  // 在查询堆栈为空且映射语句要求刷新缓存时,清除本地缓存
  if (queryStack == 0 && ms.isFlushCacheRequired()) {
    clearLocalCache();
  }


  List<E> list;
  try {
    // 增加查询堆栈深度,防止递归查询导致的问题
    queryStack++;
    // 尝试从本地缓存中获取结果,如果resultHandler为null且缓存中存在结果,则使用缓存结果
    list = resultHandler == null ? (List<E>) localCache.getObject(key) : null;
    // 如果从缓存中获取到结果,处理本地缓存的输出参数
    if (list != null) {
      handleLocallyCachedOutputParameters(ms, key, parameter, boundSql);
    } else {
      // 如果缓存中没有结果,从数据库查询数据
      list = queryFromDatabase(ms, parameter, rowBounds, resultHandler, key, boundSql);
    }
  } finally {
    // 减少查询堆栈深度,确保资源正确释放
    queryStack--;
  }


  // 当查询堆栈为空时,处理延迟加载和本地缓存
  if (queryStack == 0) {
    for (DeferredLoad deferredLoad : deferredLoads) {
      deferredLoad.load();
    }
    // issue #601
    deferredLoads.clear();
      // 开启了statement级别的缓存,那么要清理会话级别的缓存数据
    if (configuration.getLocalCacheScope() == LocalCacheScope.STATEMENT) {
      // issue #482
      clearLocalCache();
    }
  }
  return list;
}




/**
 * 从数据库中查询数据,并将结果存储在缓存中
 * 如果声明类型为CALLABLE,则还将输出参数存储在缓存中
 * 
 * @param ms 映射语句对象,包含SQL信息和配置
 * @param parameter 查询参数
 * @param rowBounds 行范围,用于分页
 * @param resultHandler 结果处理器,用于处理查询结果
 * @param key 缓存键,用于存储和检索缓存中的数据
 * @param boundSql 编译后的SQL语句和参数映射
 * @return 查询结果列表
 * @throws SQLException 如果查询过程中发生SQL异常
 */
private <E> List<E> queryFromDatabase(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler, CacheKey key, BoundSql boundSql) throws SQLException {
    List<E> list;
    // 先在本地缓存中放入执行占位符,表示查询正在进行
    localCache.putObject(key, EXECUTION_PLACEHOLDER);
    try {
        // 执行查询
        list = doQuery(ms, parameter, rowBounds, resultHandler, boundSql);
    } finally {
        // 查询结束后,从本地缓存中移除执行占位符
        localCache.removeObject(key);
    }
    // 将查询结果放入本地缓存
    localCache.putObject(key, list);
    // 如果声明类型为CALLABLE,则将输出参数放入本地缓存
    if (ms.getStatementType() == StatementType.CALLABLE) {
        localOutputParameterCache.putObject(key, parameter);
    }
    return list;
}


/**
 * 执行查询方法
 * 
 * @param ms 映射语句对象,包含了一条SQL语句的配置信息
 * @param parameter 传递给SQL语句的参数
 * @param rowBounds 用于分页查询的参数,定义了起始记录和结束记录
 * @param resultHandler 结果处理器,用于处理查询结果
 * @param boundSql 编译后的SQL语句对象,包含了最终执行的SQL和参数映射
 * @return 返回查询结果列表,列表中的元素类型由泛型E指定
 * @throws SQLException 如果查询过程中发生SQL异常
 * 
 * 此方法负责执行映射语句中的SQL查询,并通过StatementHandler处理查询结果
 * 它封装了查询过程,包括创建StatementHandler、准备SQL语句、执行查询和关闭资源
 */
@Override
public <E> List<E> doQuery(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler, BoundSql boundSql) throws SQLException {
  Statement stmt = null;
  try {
    // 获取配置对象,Configuration包含了全局配置信息
    Configuration configuration = ms.getConfiguration();
    // 创建StatementHandler,用于处理SQL语句执行和结果集处理
    StatementHandler handler = configuration.newStatementHandler(wrapper, ms, parameter, rowBounds, resultHandler, boundSql);
    // 准备SQL语句,包括创建Statement和设置参数等
    stmt = prepareStatement(handler, ms.getStatementLog());
    // 执行查询并返回结果
    return handler.query(stmt, resultHandler);
  } finally {
    // 关闭Statement,释放资源
    closeStatement(stmt);
  }
}

上面的代码很长,但是我们最终发现,到底也是交给statement处理最后的sql。

我们可以用一个流程图来解析

14243aefe08b41894ab965aa8d3e07af.png

一级缓存 - CacheKey

上面代码的CacheKey,实际上就是SqlSession对象开启时,默认使用的一级缓存,它的生命周期跟SqlSession一样。

@Override
public CacheKey createCacheKey(MappedStatement ms, Object parameterObject, RowBounds rowBounds, BoundSql boundSql) {
  if (closed) {
    throw new ExecutorException("Executor was closed.");
  }
  CacheKey cacheKey = new CacheKey();
  cacheKey.update(ms.getId());
  cacheKey.update(rowBounds.getOffset());
  cacheKey.update(rowBounds.getLimit());
  cacheKey.update(boundSql.getSql());


  for (ParameterMapping parameterMapping : parameterMappings) {
      if (parameterMapping.getMode() != ParameterMode.OUT) {
        Object value;
        String propertyName = parameterMapping.getProperty();
        if (boundSql.hasAdditionalParameter(propertyName)) {
          value = boundSql.getAdditionalParameter(propertyName);
        } else if (parameterObject == null) {
          value = null;
        } else if (typeHandlerRegistry.hasTypeHandler(parameterObject.getClass())) {
          value = parameterObject;
        } else {
          MetaObject metaObject = configuration.newMetaObject(parameterObject);
          value = metaObject.getValue(propertyName);
        }
        cacheKey.update(value);
      }
    }
    if (configuration.getEnvironment() != null) {
      // issue #176
      cacheKey.update(configuration.getEnvironment().getId());
    }
  return cacheKey;
}

CacheKey对象由以下几个部分构成:id、offset和limit、sql、用户提交参数、Environment的id五部分构成。

一级缓存的线程安全

mybatis中的一级缓存是存放在hashMap中的,之所以没有用线程安全的集合,主要是因为SqlSession都是线程不安全的,所以没有必须要。

而SqlSession线程安全则是交给了每次触发行为的clearCache()方法,当每次执行sql完毕后,都会清理掉缓存的内容,这样就可以避免缓存所引起的线程安全问题!

其他Executor介绍

类型

特点

缓存

SimpleExecutor

专注实现4个基本方法实现

一级缓存

ReuseExecutor

减少SQL预编译开销、创建和销毁Statement对象开销,提高性能

一级缓存

BatchExecutor

批量处理优化方式,将多条SQL发送给数据库执行,减少网络方面开销,提高性能

一级缓存

CachingExecutor

增加二级缓存-statement级别的开销,减少数据库执行次数,提高性能

一级缓存

二级缓存

下面我们着重讲解一下CachingExecutor。

CachingExecutor

CachingExecutor封装了一个用于执行数据库操作的Executor对象,还有管理二级缓存的TransactionalCacheManager对象。

二级缓存

二级缓存:应用级别的缓存,由 TransactionalCacheManager&TransactionalCache两个组件来实现:

  • TransactionalCache:继承了Cache接口,保存某个SqlSession某个事务需要加的二级缓存数据

  • TransactionalCacheManager:用于管理CachingExecutor使用的二级缓存对象,即key。

public class CachingExecutor implements Executor {
    private final Executor delegate;
    private final TransactionalCacheManager tcm = new TransactionalCacheManager();


    @Override
    public <E> List<E> query(MappedStatement ms, Object parameterObject, RowBounds rowBounds, ResultHandler resultHandler, CacheKey key, BoundSql boundSql)
        throws SQLException {
        //【1】查询所在Namespace对应的二级缓存
      Cache cache = ms.getCache();
      if (cache != null) {
        //【2】
        flushCacheIfRequired(ms);
          //【3】
        if (ms.isUseCache() && resultHandler == null) {
          //【4】
          ensureNoOutParams(ms, boundSql);
            //【5】
          List<E> list = (List<E>) tcm.getObject(cache, key);
            //【6】
          if (list == null) {
            list = delegate.query(ms, parameterObject, rowBounds, resultHandler, key, boundSql);
            tcm.putObject(cache, key, list); // issue #578 and #116
          }
          return list;
        }
      }
        //【7】
      return delegate.query(ms, parameterObject, rowBounds, resultHandler, key, boundSql);
    }
}

代码分析:

  • 【1】ms.getCache()

    • 获取缓存对象:查询所在Namespace对应的二级缓存(是否开启)

  • 【2】flushCacheIfRequired(ms) -二级缓存什么时候需要清理呢?这里就是答案了!

    • 缓存cache有数据 且  MappedStatement 的配置了需要清空缓存。

  • 【3】ms.isUseCache()

    • 检查是否使用缓存:如果 MappedStatement 配置了使用缓存且没有 ResultHandler,则继续执行以下步骤

  • 【4】ensureNoOutParams:确保没有输出参数:调用 ensureNoOutParams 方法,确保查询语句没有输出参数。

  • 【5】tcm.getObject:取出二级缓存

    • 【6】尝试从二级缓存中获取查询结果。如果缓存中存在结果,则直接返回。

    • 如果缓存中没有结果,则委托给 delegate 执行查询,并将结果存入缓存。

  • 【7】没有启动二级缓存,则直接调用底层的Executor,最终返回查询结果。

二级缓存关系图如下

6568f09b17b3786e3d5d4be626c5e0d7.png

二级缓存的数据结构

MyBatis的二级缓存构建是通过装饰器模式实现的

d8f6d566855d6ed731b36b4eb3100484.png

装饰链:SynchronizedCache -> LoggingCache -> SerializedCache -> LruCache -> PerpetualCache

SynchronizedCache:同步Cache,实现比较简单,直接使用synchronized修饰方法。
LoggingCache:日志功能,装饰类,用于记录缓存的命中率,如果开启了DEBUG模式,则会输出命中率日志。
SerializedCache:序列化功能,将值序列化后存到缓存中。该功能用于缓存返回一份实例的Copy,用于保存线程安全。
LruCache:采用了Lru算法的Cache实现,移除最近最少使用的Key/Value。
PerpetualCache: 作为为最基础的缓存类,底层实现比较简单,直接使用了HashMap
二级缓存的线程安全

每当执行 insert、update、delete,flushCache=true 时,二级缓存都会被清空。

二级缓存的数据提交

二级缓存必须要在会话事务提交之后,才能生效!在查询到结果后,会调用 SqlSession 的 commit 方法进行提交。

public class TransactionalCache implements Cache {
    private final Cache delegate;
    private boolean clearOnCommit;
    private final Map<Object, Object> entriesToAddOnCommit;
    private final Set<Object> entriesMissedInCache;


    public void commit() {
        // 【1】提交事务前,清空已有的二级缓存脏数据
      if (clearOnCommit) {
        delegate.clear();
      }
      //【2】将entriesToAddOnCommit集合,保存到二级缓存
      flushPendingEntries();
      // 【3】clearOnCommit重置为false
      reset();
    } 


    private void flushPendingEntries() {
    // 【3】遍历entriesToAddOnCommit集合,将其中记录的缓存添加到二级缓存里面
      for (Map.Entry<Object, Object> entry : entriesToAddOnCommit.entrySet()) {
        delegate.putObject(entry.getKey(), entry.getValue());
      }
      // 【4】遍历entriesMissedInCache集合,将entriesToAddOnCommit集合没有的缓存项目,追加到二级缓存里
      for (Object entry : entriesMissedInCache) {
        if (!entriesToAddOnCommit.containsKey(entry)) {
          delegate.putObject(entry, null);
        }
      }
    }
}
  • 【1】提交事务前,清空已有的二级缓存脏数据

  • 【2】将entriesToAddOnCommit集合,保存到二级缓存

  • 【3】遍历entriesToAddOnCommit集合,将其中记录的缓存添加到二级缓存里面

  • 【4】遍历entriesMissedInCache集合,将entriesToAddOnCommit集合没有的缓存项目,追加到二级缓存里

一级缓存和二级缓存对比

为了增加查询的性能,MyBatis 提供了二级缓存架构,分为一级缓存和二级缓存。

这两级缓存最大的区别就是:

  1. 一级缓存是会话级别的,只要出了这个 SqlSession,缓存就没用了

  2. 二级缓存可以跨会话,多个会话可以使用相同的缓存

  3. 一级缓存使用简单,默认就开启。二级缓存需要手动开启,相对复杂,而且要注意的事项也多,否则可能有隐患

  4. 二级缓存的 key 和一级缓存的 key 是一样的

4、Statement执行

SimpleStatementHandler#update
SimpleStatementHandler#update


/**
 * 执行更新操作
 * 
 * @param statement SQL语句的执行对象
 * @return 受更新操作影响的行数
 * @throws SQLException 如果执行更新操作时发生错误
 */
@Override
public int update(Statement statement) throws SQLException {
  // 获取SQL语句
  String sql = boundSql.getSql();
  // 获取传递给SQL语句的参数
  Object parameterObject = boundSql.getParameterObject();
  // 获取键生成器
  KeyGenerator keyGenerator = mappedStatement.getKeyGenerator();
  // 用于记录受影响的行数
  int rows;


  // 如果键生成器是Jdbc3KeyGenerator类型
  if (keyGenerator instanceof Jdbc3KeyGenerator) {
    // 执行SQL并返回生成的键
    statement.execute(sql, Statement.RETURN_GENERATED_KEYS);
    // 获取受影响的行数
    rows = statement.getUpdateCount();
    // 在执行完SQL后处理生成的键
    keyGenerator.processAfter(executor, mappedStatement, statement, parameterObject);
  } else if (keyGenerator instanceof SelectKeyGenerator) {
    // 如果键生成器是SelectKeyGenerator类型
    // 执行SQL但不返回生成的键
    statement.execute(sql);
    // 获取受影响的行数
    rows = statement.getUpdateCount();
    // 在执行完SQL后处理生成的键
    keyGenerator.processAfter(executor, mappedStatement, statement, parameterObject);
  } else {
    // 如果键生成器不是上述两种类型
    // 简单执行SQL
    statement.execute(sql);
    // 获取受影响的行数
    rows = statement.getUpdateCount();
  }
  // 返回受影响的行数
  return rows;
}
keyGenerator.processAfter

Jdbc3KeyGenerator为例子

@Override
public void processAfter(Executor executor, MappedStatement ms, Statement stmt, Object parameter) {
  processBatch(ms, stmt, parameter);
}


public void processBatch(MappedStatement ms, Statement stmt, Object parameter) {
    // 【1】
  final String[] keyProperties = ms.getKeyProperties();
  if (keyProperties == null || keyProperties.length == 0) {
    return;
  }
  // 【2】
  try (ResultSet rs = stmt.getGeneratedKeys()) {
    final ResultSetMetaData rsmd = rs.getMetaData();
    final Configuration configuration = ms.getConfiguration();
    if (rsmd.getColumnCount() < keyProperties.length) {
      // Error?
    } else {
    // 【3】
      assignKeys(configuration, rs, rsmd, keyProperties, parameter);
    }
  } catch (Exception e) {
    throw new ExecutorException("Error getting generated key or setting result to parameter object. Cause: " + e, e);
  }
}

我们可以用一个流程图来解析

07c532cc21d2653cbc84b711b1670eab.png

SimpleStatementHandler#query
/**
 * 执行查询并处理结果集
 * 
 * @param statement SQL语句的封装对象,用于执行SQL语句
 * @param resultHandler 结果集处理对象,用于处理查询结果
 * @return 返回一个包含查询结果的列表
 * @throws SQLException 如果执行SQL语句时发生错误
 * 
 */
@Override
public <E> List<E> query(Statement statement, ResultHandler resultHandler) throws SQLException {
  // 从BoundSql对象中获取SQL语句
  String sql = boundSql.getSql();
  // 执行SQL语句
  statement.execute(sql);
  // 处理并返回结果集
  return resultSetHandler.handleResultSets(statement);
}

总结

以上是SpringBoot集成了Mybatis的框架代码源码分析,我们从2个方面入手:

  • 初始化时的Mybatis对MapperMethod的层层封装

  • 运行期的MapperMethod执行

总结了以下知识点:

  • SpringBoot自动装载原理

  • 一级缓存和二级缓存的区别和线程安全

  • 主键生成器

  • SqlSessionInterceptor的代理模式

其他文章

多线程反思(中):对ThreadPoolExecutors的思考

线程安全反思录(上):ThreadLocal到底安全不?

Spring事务管理:应用实战案例和规则

Spring源码分析:bean加载流程

SpringCloud源码:客户端分析(一)- SpringBootApplication注解类加载流程

SpringCloud源码:客户端分析(二)- 客户端源码分析

Kafka消息堆积问题排查

基于SpringMVC的API灰度方案

理解到位:灾备和只读数据库

SQL治理经验谈:索引覆盖

Mybatis链路分析:JDK动态代理和责任链模式的应用

大模型安装部署、测试、接入SpringCloud应用体系

Mybatis插件-租户ID的注入&拦截应用

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/2235991.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

[IAA系列] Image Aesthetic Assessment

Preface 本文旨在记录个人结合AI工具对IAA这个领域的一些了解&#xff0c;主要是通过论文阅读的方式加深对领域的了解。有什么问题&#xff0c;欢迎在评论区提出并讨论。 什么是IAA Image Aesthetic Assessment&#xff08;图像美学评估&#xff09;是一种评估图像在视觉上的…

leetcode 2043.简易银行系统

1.题目要求: 示例: 输入&#xff1a; ["Bank", "withdraw", "transfer", "deposit", "transfer", "withdraw"] [[[10, 100, 20, 50, 30]], [3, 10], [5, 1, 20], [5, 20], [3, 4, 15], [10, 50]] 输出&#xff…

一文了解Android SELinux

在Android系统中&#xff0c;SELinux&#xff08;Security-Enhanced Linux&#xff09;是一个增强的安全机制&#xff0c;用于对系统进行强制访问控制&#xff08;Mandatory Access Control&#xff0c;MAC&#xff09;。它限制了应用程序和进程的访问权限&#xff0c;提供了更…

Java链表及源码解析

文章目录 创建一个ILindkedList接口创建方法(模拟实现链表方法)创建MyLinkedList来实现接口的方法创建链表节点addFirst方法&#xff08;新增头部属性&#xff09;addLast方法&#xff08;新增到末尾一个属性&#xff09;remove方法&#xff08;删除指定属性&#xff09;addInd…

微服务系列四:热更新措施与配置共享

目录 前言 一、基于Nacos的管理中心整体方案 二、配置共享动态维护 2.1 分析哪些配置可拆&#xff0c;需要动态提供哪些参数 2.2 在nacos 分别创建共享配置 创建jdbc相关配置文件 创建日志相关配置文件 创建接口文档配置文件 2.3 拉取本地合并配置文件 2.3.1 拉取出现…

NoETL自动化指标平台为数据分析提质增效,驱动业务决策

直觉判断往往来源于多年的经验和专业知识&#xff0c;能够在复杂和不确定的环境中快速做出决策反应。但这种方式普遍存在主观偏见&#xff0c;缺乏合理的科学依据&#xff0c;无法全面、客观、精准地评估和识别市场趋势与用户需求&#xff0c;从而造成决策失误&#xff0c;给业…

使用亚马逊 S3 连接器为 PyTorch 和 MinIO 创建地图式数据集

在深入研究 Amazon 的 PyTorch S3 连接器之前&#xff0c;有必要介绍一下它要解决的问题。许多 AI 模型需要使用无法放入内存的数据进行训练。此外&#xff0c;许多为计算机视觉和生成式 AI 构建的真正有趣的模型使用的数据甚至无法容纳在单个服务器附带的磁盘驱动器上。解决存…

基于MATLAB的实现垃圾分类Matlab源码

⼀、垃圾分类 如何通过垃圾分类管理&#xff0c;最⼤限度地实现垃圾资源利⽤&#xff0c;减少垃圾处置量&#xff0c;改善⽣存环境质量&#xff0c;是当前世界各国共同关注的迫切问题之⼀。根据国家制定的统⼀标准&#xff0c;现在⽣活垃圾被⼴泛分为四类&#xff0c;分别是可…

硬件基础10 逻辑门电路——CMOS

目录 一、门电路类型 二、CMOS逻辑门 1、CMOS基础 2、MOS管开关电路 &#xff08;1&#xff09;、基础理论分析 &#xff08;2&#xff09;、开关动态特性 3、CMOS反相器 4、与非、或非门 三、逻辑门的不同输出结构与参数 1、CMOS的保护和缓冲 2、漏极开路与三态输出…

新手散户如何避免被割?有量化策略适应暴涨暴跌行情吗?|附代码

这是邢不行第 124 期量化小讲堂的分享 作者 | 邢不行 大A今年上半年的行情较为坎坷&#xff0c;市场持续下跌&#xff0c;导致诸多投资者风格大变&#xff0c;从倾向于高风险的进攻策略转为低风险的防御策略&#xff0c;尤以高股息策略和杠铃策略最为火爆。 本文给大家介绍一…

数据链路层Mac协议与ARP协议

Mac帧 ​ ​ 如何将有效载荷和报头分离&#xff1f; 根据固定大小 ​​ 报头固定大小&#xff0c;按报头大小分离 如何分用&#xff1f; ​​​​ 类型为0800&#xff0c;代表为IP报文&#xff0c;应该交给网络层IP协议 目的地址 原地址为Mac地址 局域网通信 ​ 局…

轮椅车、医用病床等康复类器具检测设备的介绍

康复类器具检测设备是指用于检测、评估和测试康复类器具的设备。康复类器具包括轮椅、助行器、假肢、矫形器等。这些器具在使用前需要经过检测和评估以确保其满足质量、性能、安全和有效性的要求。 康复类器具的测试项目及其设备主要包括以下几种&#xff1a; 1、力学测试设…

WiFi一直获取不到IP地址是怎么回事?

在当今这个信息化时代&#xff0c;WiFi已成为我们日常生活中不可或缺的一部分。无论是家庭、办公室还是公共场所&#xff0c;WiFi都为我们提供了便捷的无线互联网接入。然而&#xff0c;有时我们可能会遇到WiFi连接后无法获取IP地址的问题&#xff0c;这不仅影响了我们的网络使…

基于SSM+VUE儿童接种疫苗预约管理系统JAVA|VUE|Springboot计算机毕业设计源代码+数据库+LW文档+开题报告+答辩稿+部署教+代码讲解

源代码数据库LW文档&#xff08;1万字以上&#xff09;开题报告答辩稿 部署教程代码讲解代码时间修改教程 一、开发工具、运行环境、开发技术 开发工具 1、操作系统&#xff1a;Window操作系统 2、开发工具&#xff1a;IntelliJ IDEA或者Eclipse 3、数据库存储&#xff1a…

城镇住房保障:SpringBoot系统架构解析

2相关技术 2.1 MYSQL数据库 MySQL是一个真正的多用户、多线程SQL数据库服务器。 是基于SQL的客户/服务器模式的关系数据库管理系统&#xff0c;它的有点有有功能强大、使用简单、管理方便、安全可靠性高、运行速度快、多线程、跨平台性、完全网络化、稳定性等&#xff0c;非常…

软件测试—功能测试详解

&#x1f345; 点击文末小卡片 &#xff0c;免费获取软件测试全套资料&#xff0c;资料在手&#xff0c;涨薪更快 一、测试项目启动与研读需求文档 &#xff08;一&#xff09; 组建测试团队 1、测试团队中的角色 2、测试团队的基本责任 尽早地发现软件程序、系统或产…

第十五届蓝桥杯C/C++B组题解——数字接龙

题目描述 小蓝最近迷上了一款名为《数字接龙》的迷宫游戏&#xff0c;游戏在一个大小为N N 的格子棋盘上展开&#xff0c;其中每一个格子处都有着一个 0 . . . K − 1 之间的整数。游戏规则如下&#xff1a; 从左上角 (0, 0) 处出发&#xff0c;目标是到达右下角 (N − 1, N …

【9695】基于springboot+vue的学生就业管理系统

作者主页&#xff1a;Java码库 主营内容&#xff1a;SpringBoot、Vue、SSM、HLMT、Jsp、PHP、Nodejs、Python、爬虫、数据可视化、小程序、安卓app等设计与开发。 收藏点赞不迷路 关注作者有好处 文末获取免费源码 项目描述 本学生就业管理系统以springboot作为框架&#xff…

Android使用scheme方式唤醒处于后台时的App场景

场景&#xff1a;甲App唤醒处于后台时的乙App的目标界面Activity&#xff0c;且乙App的目标界面Activity处于最上层&#xff0c;即已经打开状态&#xff0c;要求甲App使用scheme唤醒乙App时&#xff0c;达到跟从桌面icon拉起App效果一致&#xff0c;不能出现只拉起了乙App的目标…

centos7,yum安装mongodb

yum安装mongodb 1.配置MongoDB的yum源2.安装Mongodb3.启动Mongodb4.配置远程访问5.设置mongo密码 1.配置MongoDB的yum源 1.创建yum源文件&#xff0c;输入命令&#xff1a; vim /etc/yum.repos.d/mongodb-org-5.0.repo然后在文件中输入以下内容并保存&#xff1a; [mongodb-…