MyBatis Plus通用CRUD与条件构造器使用及SQL自动注入原理分析

本文涉及的产品
公共DNS(含HTTPDNS解析),每月1000万次HTTP解析
云解析 DNS,旗舰版 1个月
全局流量管理 GTM,标准版 1个月
简介: MyBatis Plus通用CRUD与条件构造器使用及SQL自动注入原理分析

首先根据MyBatis Plus入门实践详解 搭建好工程。然后创建数据库表与相关的类。


表结构如下:

EmployeeMapper接口继承自BaseMapper<Employee>

public interface EmployeeMapper extends BaseMapper<Employee> {
}

这个BaseMapper是com.baomidou.mybatisplus.mapper.BaseMapper。这里测试的MyBatis Plus版本是:

<dependency>
   <groupId>com.baomidou</groupId>
   <artifactId>mybatis-plus</artifactId>
   <version>2.3</version>
</dependency>   

BaseMapper定义了常用的增删改查接口,继承该接口后无需编写mapper.xml文件,即可获得通用的CRUD功能。

当然你可以在这个employeeMapper里面自定义方法,方法通过注解或者mapper.xml里面insert|update|select|delete实现。

【1】通用插入数据

① insert


测试代码如下:

@Test
public void testCommonInsert() {
  //初始化Employee对象
  Employee employee  = new Employee();
  employee.setLastName("MP");
  employee.setEmail("mp@163.com");
  employee.setSalary(20000.0);
  //插入到数据库   
  Integer result = employeeMapper.insert(employee);  
  System.out.println("result: " + result );
  //获取当前数据在数据库中的主键值
  Integer key = employee.getId();
  System.out.println("key:" + key );
}


测试结果如下:

结论:

insert方法在插入时, 会根据实体类的每个属性进行非空判断,只有非空的属性对应的字段才会出现到SQL语句中

② insertAllColumn

测试代码如下:

@Test
public void testCommonInsert() {
  //初始化Employee对象
  Employee employee  = new Employee();
  employee.setLastName("MP");
  employee.setEmail("mp@163.com");
  employee.setSalary(20000.0);
  //插入到数据库   
  Integer result = employeeMapper.insertAllColumn(employee);  
  System.out.println("result: " + result );
  //获取当前数据在数据库中的主键值
  Integer key = employee.getId();
  System.out.println("key:" + key );
}


测试结果如下:

结论

insertAllColumn方法在插入时, 不管属性是否非空, 属性所对应的字段都会出现到SQL语句中。


【2】通用更新数据

① updateById

测试代码如下:

@Test
public void testCommonUpdate() {
  //初始化修改对象
  Employee employee = new Employee();
  employee.setId(7);
  employee.setLastName("小泽老师");
  employee.setEmail("xz@sina.com");
  employee.setGender(0);
  Integer result = employeeMapper.updateById(employee);
  System.out.println("result: " + result );
}


测试结果如下:

结论

updateById方法在更新时, 会根据实体类的每个属性进行非空判断,只有非空的属性对应的字段才会出现到SQL语句中

② updateAllColumnById

测试代码如下:

@Test
public void testCommonUpdate() {
  //初始化修改对象
  Employee employee = new Employee();
  employee.setId(7);
  employee.setLastName("小泽老师");
  employee.setEmail("xz@sina.com");
  employee.setGender(0);
  Integer result = employeeMapper.updateAllColumnById(employee);
  System.out.println("result: " + result );
}


测试结果如下:

结论

updateAllColumnById方法在更新时, 不管属性是否非空, 属性所对应的字段都会出现到SQL语句中。


【3】通用查询数据

① selectById

测试代码如下:

Employee employee = employeeMapper.selectById(7);
System.out.println(employee);


测试结果如下:

② selectOne多列查询

测试代码如下:

Employee  employee = new Employee();
employee.setId(7);
employee.setLastName("小泽老师");
employee.setGender(0);
Employee result = employeeMapper.selectOne(employee);
System.out.println("result: " +result );

测试结果如下:


这里需要注意的是使用selectOne时需要保证数据库中顶多查询出一条数据,否则就会报错nested exception is org.apache.ibatis.exceptions.TooManyResultsException: Expected one result (or null) to be returned by selectOne(), but found: 2

③ selectBatchIds

测试代码如下:

List<Integer> idList = new ArrayList<>();
idList.add(7);
idList.add(8);
idList.add(9);
List<Employee> emps = employeeMapper.selectBatchIds(idList);
System.out.println(emps);

测试结果如下:

④ selectByMap

测试代码如下:

Map<String,Object> columnMap = new HashMap<>();
//这里注意是列名, 非对象属性名
columnMap.put("last_name", "MP");
columnMap.put("gender", 1);
List<Employee> emps = employeeMapper.selectByMap(columnMap);
System.out.println(emps);


测试结果如下:

⑤ 分页查询selectPage

测试代码如下:

List<Employee> emps = employeeMapper.selectPage(new Page<>(2, 2), null);
System.out.println(emps);


测试结果如下:

可以看到SQL语句上面并没有limit关键字进行分页,但是返回的数据结果确实是经过分页处理的。这说明其并非是真正的物理分页,而是借助于RowBounds实现的内存分页。如果想实现物理分页,可以考虑使用pageHelper插件。


故而上述分页实例并不推荐,推荐使用MyBatis Plus的分页插件。


【4】通用删除数据

① deleteById

测试代码如下:

Integer result = employeeMapper.deleteById(13);
System.out.println("result: " + result );

测试结果如下:

将会返回删除条数,没有删除返回0。

② deleteByMap


根据组合条件删除,返回删除条数,没有删除返回0.

测试代码如下:

Map<String,Object> columnMap = new HashMap<>();
columnMap.put("last_name", "MP");
columnMap.put("email", "mp@163.com");
Integer result = employeeMapper.deleteByMap(columnMap);
System.out.println("result: " + result );


测试结果如下:

③ deleteBatchIds

根据ID批量删除,返回删除的记录数,没有删除返回0。


测试代码如下:

vList<Integer> idList = new ArrayList<>();
idList.add(13);
idList.add(14);
idList.add(15);
Integer result = employeeMapper.deleteBatchIds(idList);
System.out.println("result: " + result );

测试结果如下:


【5】条件构造器使用

① 条件查询

如下,分页查询tbl_employee表中,年龄在18~50之间且性别为男且姓名为Tom的所有用户。

测试代码如下:

List<Employee> emps =employeeMapper.selectPage(new Page<Employee>(1, 2),
          new EntityWrapper<Employee>()
          .between("age", 18, 50)
          .eq("gender", 1)
          .eq("last_name", "Tom")
        );
System.out.println(emps);


这里使用new EntityWrapper传一个Wrapper实例对象进去,使用between、eq这些关键字来封装查询条件(在JPA里面也可以看到这种思想)

查询SQL如下:

SELECT id,last_name AS lastName,email,gender,age FROM tbl_employee 
WHERE (age BETWEEN ? AND ? AND gender = ? AND last_name = ?)

还可以使用Wrapper的另外一个子类Condition来实现上述效果,condition和EntityWrapper都wrapper的子类:

List<Employee > emps = employeeMapper.selectPage(
              new Page<Employee>(1,2),
              Condition.create()
              .between("age", 18, 50)
              .eq("gender", "1")
              .eq("last_name", "Tom")
              );
System.out.println(emps);

查询SQL如下:

SELECT id,last_name AS lastName,email,gender,age FROM tbl_employee 
WHERE (age BETWEEN ? AND ? AND gender = ? AND last_name = ?)


② 组合条件模糊查询

查询tbl_employee表中, 性别为女并且名字中带有"老师" 或者 邮箱中带有"a"。

测试代码如下:

List<Employee> emps = employeeMapper.selectList(
        new EntityWrapper<Employee>()
        .eq("gender", 0)
        .like("last_name", "老师")
        .orNew()   // SQL: (gender = ? AND last_name LIKE ?) OR (email LIKE ?)
        .like("email", "a")
        );
System.out.println(emps);

查询SQL如下:

SELECT id,last_name AS lastName,email,gender,age FROM tbl_employee 
WHERE (gender = ? AND last_name LIKE ?) OR (email LIKE ?) 
Parameters: 0(Integer), %老师%(String), %a%(String)

上面使用的是orNew,下面对比使用or的时候查询SQL:

List<Employee> emps = employeeMapper.selectList(
  new EntityWrapper<Employee>()
  .eq("gender", 0)
  .like("last_name", "老师")
  .or()    // SQL: (gender = ? AND last_name LIKE ? OR email LIKE ?)
  .like("email", "a")
  );
System.out.println(emps);
SELECT id,last_name AS lastName,email,gender,age FROM tbl_employee 
WHERE (gender = ? AND last_name LIKE ? OR email LIKE ?)
Parameters: 0(Integer), %老师%(String), %a%(String)

可以发现OR只是作为一个查询条件,而orNew则封装了两个子查询。针对本例效果是一致的,但是在某些情况下结果会不一致。


③ 条件查询排序


查询性别为女的, 根据age进行排序(asc/desc), 简单分页。

测试代码如下:

List<Employee> emps  = employeeMapper.selectList(
        new EntityWrapper<Employee>()
        .eq("gender", 0)
        .orderBy("age")//默认升序 可以使用orderAsc
        //.orderDesc(Arrays.asList(new String [] {"age"}))
        .last("desc limit 1,3")
        );

查询SQL如下:

SELECT id,last_name AS lastName,email,gender,age FROM tbl_employee 
WHERE (gender = ?) ORDER BY age desc limit 1,3


④ 更新数据

测试代码如下:

@Test
public void testEntityWrapperUpdate() {
  Employee employee = new Employee();
  employee.setLastName("苍老师");
  employee.setEmail("cls@sina.com");
  employee.setGender(0);
  employeeMapper.update(employee, 
        new EntityWrapper<Employee>()
        .eq("last_name", "Tom")
        .eq("age", 44)
        );
}


更新SQL如下:

Preparing: UPDATE tbl_employee SET last_name=?, email=?, gender=? WHERE (last_name = ? AND age = ?) 
Parameters: 苍老师(String), cls@sina.com(String), 0(Integer), Tom(String), 44(Integer)
Updates: 0


⑤ 删除数据

测试代码如下:

@Test
public void testEntityWrapperDelete() {
  employeeMapper.delete(
        new EntityWrapper<Employee>()
        .eq("last_name", "Tom")
        .eq("age", 22)
      );
}


测试结果如下:

为什么继承了BaseMapper后,不需要写SQL,不需要mapper.xml配置文件,即可使用大多CRUD方法?


【6】MP 启动注入 SQL原理分析

以如下代码为例跟踪一下究竟发生了什么:

@Test
public void testEntityWrapperUpdate() {
  Employee employee = new Employee();
  employee.setLastName("苍老师");
  employee.setEmail("cls@sina.com");
  employee.setGender(0);
  Integer update = employeeMapper.update(employee,
      new EntityWrapper<Employee>()
          .eq("last_name", "Tom")
          .eq("age", 44)
  );
  System.out.println(update);
}

① 此时的employeeMapper对象是个什么?

此时的employeeMapper是个代理对象org.apache.ibatis.binding.MapperProxy@7728643a,如下图所示其是一个代理对象。


这里sqlsessionTemplate是sqlsession实现类:

主要属性如下:

继续跟踪其sqlSessionFactory属性,即employeeMapper.sqlSession.sqlSessionFactory:


sqlSessionFactory只有一个属性configuration,其是MybatisConfiguration实例对象,该实例对象有MybatisMapperRegistry注册中心实例对象、 mappedStatements(Configuration$StrictMap)。那么什么是mappedStatements?我们看一下:

似乎找到了employeeMapper那么多默认功能的来源!项目启动的时候初始化employeeMapper对象时会自动注入这些方法!那么具体如何注入的呢?我们跟下代码看看。


② debug运行代码

如下图所示,在启动的时候根据type注入了employeeMapper,在AbstractAutowireCapableBeanFactory类中调用了afterPropertiesSet方法,然后将一系列mapper的方法注入。


那么有两个问题:

  • 1.调用了谁的afterPropertiesSet方法?afterPropertiesSet方法是InitializingBean接口的。
  • 2.MappedStatement究竟是什么?



1.1 AbstractAutowireCapableBeanFactory.initializeBean

如下所示我们跟踪源码从这里开始,主要跟踪其invokeInitMethods方法。

  protected Object initializeBean(final String beanName, final Object bean, RootBeanDefinition mbd) {
    if (System.getSecurityManager() != null) {
      AccessController.doPrivileged(new PrivilegedAction<Object>() {
        @Override
        public Object run() {
          invokeAwareMethods(beanName, bean);
          return null;
        }
      }, getAccessControlContext());
    }
    else {
      invokeAwareMethods(beanName, bean);
    }
    Object wrappedBean = bean;
    if (mbd == null || !mbd.isSynthetic()) {
    //循环执行那些bean的后置处理器的postProcessBeforeInitialization方法
      wrappedBean = applyBeanPostProcessorsBeforeInitialization(wrappedBean, beanName);
    }
    try {
//调用初始化方法:如InitializingBean's {@code afterPropertiesSet}
//or a custom init-method
      invokeInitMethods(beanName, wrappedBean, mbd);
    }
    catch (Throwable ex) {
      throw new BeanCreationException(
          (mbd != null ? mbd.getResourceDescription() : null),
          beanName, "Invocation of init method failed", ex);
    }
//循环执行那些bean的后置处理器的postProcessBeforeInitialization方法
    if (mbd == null || !mbd.isSynthetic()) {
      wrappedBean = applyBeanPostProcessorsAfterInitialization(wrappedBean, beanName);
    }
    return wrappedBean;
  }

1.2 AbstractAutowireCapableBeanFactory.invokeInitMethods

方法源码如下所示:

#这里的bean 是wrappedBean
protected void invokeInitMethods(String beanName, final Object bean, RootBeanDefinition mbd)
      throws Throwable {
    boolean isInitializingBean = (bean instanceof InitializingBean);
    if (isInitializingBean && (mbd == null || !mbd.isExternallyManagedInitMethod("afterPropertiesSet"))) {
      if (logger.isDebugEnabled()) {
        logger.debug("Invoking afterPropertiesSet() on bean with name '" + beanName + "'");
      }
      if (System.getSecurityManager() != null) {
        try {
          AccessController.doPrivileged(new PrivilegedExceptionAction<Object>() {
            @Override
            public Object run() throws Exception {
              ((InitializingBean) bean).afterPropertiesSet();
              return null;
            }
          }, getAccessControlContext());
        }
        catch (PrivilegedActionException pae) {
          throw pae.getException();
        }
      }
      else {
        ((InitializingBean) bean).afterPropertiesSet();
      }
    }
    if (mbd != null) {
      String initMethodName = mbd.getInitMethodName();
      if (initMethodName != null && !(isInitializingBean && "afterPropertiesSet".equals(initMethodName)) &&
          !mbd.isExternallyManagedInitMethod(initMethodName)) {
        invokeCustomInitMethod(beanName, bean, mbd);
      }
    }
  }

invokeInitMethods方法作用是什么?

在bean属性被设置后告诉bean的bean factory是什么并检测其是否实现了InitializingBean接口或者自定义了一个init method。如果是,则尝试调用其afterPropertiesSet以及invokeCustomInitMethod(调用自定义的init method方法)。


为什么要从这里开始?

这里是bean实例化过程的一步,执行栈信息如下(可以看到是spring初始化bean的过程):

这里方法参数具体值如下:



具体看下此时的包装bean:

MapperFactoryBean类继承图如下:

可以看到其实现了InitializingBean接口,afterPropertiesSet方法会在bean属性设置完后进行一系列操作。


我们跟踪进afterPropertiesSet方法,这里是DaoSupport.afterPropertiesSet方法,源码如下:

 public final void afterPropertiesSet() throws IllegalArgumentException, BeanInitializationException {
 //从这里跟进去 参考1.3
        this.checkDaoConfig();
        try {
            this.initDao();
        } catch (Exception var2) {
            throw new BeanInitializationException("Initialization of DAO failed", var2);
        }
    }


1.3 MapperFactoryBean.checkDaoConfig

  protected void checkDaoConfig() {
    super.checkDaoConfig();
    notNull(this.mapperInterface, "Property 'mapperInterface' is required");
    //获取到configuration --MyBatisConfiguration实例
    Configuration configuration = getSqlSession().getConfiguration();
      //如果该configuration没有该mapperInterface,则进行addMapper操作
    if (this.addToConfig && !configuration.hasMapper(this.mapperInterface)) {
      try {
      //这个是核心,参考1.4
        configuration.addMapper(this.mapperInterface);
      } 
      //...
      }
    }
  }


configuration.addMapper(this.mapperInterface);直接调用了mybatisMapperRegistry的addMapper方法:


public <T> void addMapper(Class<T> type) {
        this.mybatisMapperRegistry.addMapper(type);
    }

1.4 MybatisMapperRegistry.addMapper

 public <T> void addMapper(Class<T> type) {
 //判断是否接口
        if (type.isInterface()) {
        //判断是否已经存在
            if (this.hasMapper(type)) {
                return;
            }
            boolean loadCompleted = false;
            try {
            //type:new MapperProxyFactory(type) 放到knownMappers这个hashmap中
                this.knownMappers.put(type, new MapperProxyFactory(type));
                //获取一个解析器,进行对象方法注入
                MybatisMapperAnnotationBuilder parser = new MybatisMapperAnnotationBuilder(this.config, type);
                //重点来了 参考1.5
                parser.parse();
                loadCompleted = true;
            } finally {
                if (!loadCompleted) {
                    this.knownMappers.remove(type);
                }
            }
        }
    }


这里我们重点看下MybatisMapperAnnotationBuilder 是什么,其构造函数如下:

public MybatisMapperAnnotationBuilder(Configuration configuration, Class<?> type) {
        super(configuration, type);
        String resource = type.getName().replace('.', '/') + ".java (best guess)";
        this.assistant = new MapperBuilderAssistant(configuration, resource);
        this.configuration = configuration;
        this.type = type;
        this.sqlAnnotationTypes.add(Select.class);
        this.sqlAnnotationTypes.add(Insert.class);
        this.sqlAnnotationTypes.add(Update.class);
        this.sqlAnnotationTypes.add(Delete.class);
        this.sqlProviderAnnotationTypes.add(SelectProvider.class);
        this.sqlProviderAnnotationTypes.add(InsertProvider.class);
        this.sqlProviderAnnotationTypes.add(UpdateProvider.class);
        this.sqlProviderAnnotationTypes.add(DeleteProvider.class);
    }

其首先调用了父类MapperAnnotationBuilder的构造函数,如下所示:

  public MapperAnnotationBuilder(Configuration configuration, Class<?> type) {
  //资源符号路径转换
        String resource = type.getName().replace('.', '/') + ".java (best guess)";
        this.assistant = new MapperBuilderAssistant(configuration, resource);
        this.configuration = configuration;
        this.type = type;
        this.sqlAnnotationTypes.add(Select.class);
        this.sqlAnnotationTypes.add(Insert.class);
        this.sqlAnnotationTypes.add(Update.class);
        this.sqlAnnotationTypes.add(Delete.class);
        this.sqlProviderAnnotationTypes.add(SelectProvider.class);
        this.sqlProviderAnnotationTypes.add(InsertProvider.class);
        this.sqlProviderAnnotationTypes.add(UpdateProvider.class);
        this.sqlProviderAnnotationTypes.add(DeleteProvider.class);
    }


调用完父类的构造函数后,当前MybatisMapperAnnotationBuilder如下:

然后继续走MybatisMapperAnnotationBuilder的构造函数,最终如下所示:


1.5 MybatisMapperAnnotationBuilder.parse

public void parse() {
//拿到接口完整路径
        String resource = this.type.toString();
        if (!this.configuration.isResourceLoaded(resource)) {
        //获取mapper.xml文件 这个很有意思,参考1.6
            this.loadXmlResource();
            this.configuration.addLoadedResource(resource);
            //设置当前的Namespace--命名空间
            this.assistant.setCurrentNamespace(this.type.getName());
            this.parseCache();
            this.parseCacheRef();
            //获取接口的方法
            Method[] methods = this.type.getMethods();
            //如果是BaseMapper的子类,则会调用inspectInject方法
            if (BaseMapper.class.isAssignableFrom(this.type)) {
            //获取注入器进行注入 参考1.7
                GlobalConfigUtils.getSqlInjector(this.configuration).inspectInject(this.assistant, this.type);
            }
            Method[] arr$ = methods;
            int len$ = methods.length;
            for(int i$ = 0; i$ < len$; ++i$) {
                Method method = arr$[i$];
                try {
                    if (!method.isBridge()) {
                        this.parseStatement(method);
                    }
                } catch (IncompleteElementException var8) {
                    this.configuration.addIncompleteMethod(new MethodResolver(this, method));
                }
            }
        }
        this.parsePendingMethods();
    }


这里可以看下获取的接口的方法:

对比下我们在employeeMapper里面看到的方法(可以猜测,将会把这些方法一个个注入进去,具体注入什么进去到哪里呢?):


1.6 MybatisMapperAnnotationBuilder.loadXmlResource

   private void loadXmlResource() {
        if (!this.configuration.isResourceLoaded("namespace:" + this.type.getName())) {
        // 这里xmlResource 是com/jane/mp/mapper/EmployeeMapper.xml
            String xmlResource = this.type.getName().replace('.', '/') + ".xml";
            InputStream inputStream = null;
            try {
            //尝试读取文件流 具体可参考
                inputStream = Resources.getResourceAsStream(this.type.getClassLoader(), xmlResource);
            } catch (IOException var4) {
                ;
            }
            if (inputStream != null) {
                XMLMapperBuilder xmlParser = new XMLMapperBuilder(inputStream, this.assistant.getConfiguration(), xmlResource, this.configuration.getSqlFragments(), this.type.getName());
                xmlParser.parse();
            }
        }
    }

1.7 AutoSqlInjector.inspectInject

   public void inspectInject(MapperBuilderAssistant builderAssistant, Class<?> mapperClass) {
   //interface com.jane.mp.mapper.EmployeeMapper
        String className = mapperClass.toString();
        Set<String> mapperRegistryCache = GlobalConfigUtils.getMapperRegistryCache(builderAssistant.getConfiguration());
        //如果缓存中没有className   则调用inject,然后放到mapperRegistryCache
        if (!mapperRegistryCache.contains(className)) {
        //参考1.8
            this.inject(builderAssistant, mapperClass);
 //将会放到GlobalConfiguration.mapperRegistryCache中(Set<String>),下次即可从其获取
            mapperRegistryCache.add(className);
        }
    }


1.8 AutoSqlInjector.inject(MapperBuilderAssistant builderAssistant, Class<?> mapperClass)

  public void inject(MapperBuilderAssistant builderAssistant, Class<?> mapperClass) {
        this.configuration = builderAssistant.getConfiguration();
        this.builderAssistant = builderAssistant;
        this.languageDriver = this.configuration.getDefaultScriptingLanguageInstance();
        Class<?> modelClass = this.extractModelClass(mapperClass);
        if (null != modelClass) {
            if (this.getGlobalConfig().isSqlParserCache()) {
                PluginUtils.initSqlParserInfoCache(mapperClass);
            }
//获取表信息
            TableInfo table = TableInfoHelper.initTableInfo(builderAssistant, modelClass);
            this.injectSql(builderAssistant, mapperClass, modelClass, table);
        }
    }


看下这里TableInfo 是个什么?


1.9 AutoSqlInjector.inject(MapperBuilderAssistant builderAssistant, Class<?> mapperClass, Class<?> modelClass, TableInfo table)

 protected void injectSql(MapperBuilderAssistant builderAssistant, Class<?> mapperClass, Class<?> modelClass, TableInfo table) {
        /**
         * 表信息包含主键,注入主键相关方法
         */
        if (StringUtils.isNotEmpty(table.getKeyProperty())) {
            /** 删除 */
            this.injectDeleteByIdSql(false, mapperClass, modelClass, table);
            this.injectDeleteByIdSql(true, mapperClass, modelClass, table);
            /** 修改 */
            this.injectUpdateByIdSql(true, mapperClass, modelClass, table);
            this.injectUpdateByIdSql(false, mapperClass, modelClass, table);
            /** 查询 */
            this.injectSelectByIdSql(false, mapperClass, modelClass, table);
            this.injectSelectByIdSql(true, mapperClass, modelClass, table);
        } else {
            // 表不包含主键时 给予警告
            logger.warn(String.format("%s ,Not found @TableId annotation, Cannot use Mybatis-Plus 'xxById' Method.",
                modelClass.toString()));
        }
        /**
         * 正常注入无需主键方法
         */
        /** 插入 */
        this.injectInsertOneSql(true, mapperClass, modelClass, table);
        this.injectInsertOneSql(false, mapperClass, modelClass, table);
        /** 删除 */
        this.injectDeleteSql(mapperClass, modelClass, table);
        this.injectDeleteByMapSql(mapperClass, table);
        /** 修改 */
        this.injectUpdateSql(mapperClass, modelClass, table);
        /** 修改 (自定义 set 属性) */
        this.injectUpdateForSetSql(mapperClass, modelClass, table);
        /** 查询 */
        this.injectSelectByMapSql(mapperClass, modelClass, table);
        this.injectSelectOneSql(mapperClass, modelClass, table);
        this.injectSelectCountSql(mapperClass, modelClass, table);
        this.injectSelectListSql(SqlMethod.SELECT_LIST, mapperClass, modelClass, table);
        this.injectSelectListSql(SqlMethod.SELECT_PAGE, mapperClass, modelClass, table);
        this.injectSelectMapsSql(SqlMethod.SELECT_MAPS, mapperClass, modelClass, table);
        this.injectSelectMapsSql(SqlMethod.SELECT_MAPS_PAGE, mapperClass, modelClass, table);
        this.injectSelectObjsSql(SqlMethod.SELECT_OBJS, mapperClass, modelClass, table);
        /** 自定义方法 */
        this.inject(configuration, builderAssistant, mapperClass, modelClass, table);
    }


③ 以 injectDeleteByIdSql为例查看具体如何注入了进去

① 调用AutoSqlInjector.injectDeleteByIdSql方法:

 protected void injectDeleteByIdSql(boolean batch, Class<?> mapperClass, Class<?> modelClass, TableInfo table) {
        SqlMethod sqlMethod = SqlMethod.DELETE_BY_ID;
        String idStr = table.getKeyProperty();
        //判断是否批量删除,若是则封装foreach 语句
        if (batch) {
            sqlMethod = SqlMethod.DELETE_BATCH_BY_IDS;
            StringBuilder ids = new StringBuilder();
            ids.append("\n<foreach item=\"item\" index=\"index\" collection=\"coll\" separator=\",\">");
            ids.append("#{item}");
            ids.append("\n</foreach>");
            idStr = ids.toString();
        }
//SQL格式化 <script>DELETE FROM tbl_employee WHERE id=#{id}</script>
        String sql = String.format(sqlMethod.getSql(), table.getTableName(), table.getKeyColumn(), idStr);
      //根据SQL创建sqlSource 
        SqlSource sqlSource = this.languageDriver.createSqlSource(this.configuration, sql, modelClass);
    //封装一个个MappedStatement放到
        this.addDeleteMappedStatement(mapperClass, sqlMethod.getMethod(), sqlSource);
    }

sqlMethod.getSql如下:

<script>DELETE FROM %s WHERE %s=#{%s}</script>


创建的SqlSource为:


② this.addDeleteMappedStatement

AutoSqlInjector.addDeleteMappedStatement方法源码如下:

public MappedStatement addDeleteMappedStatement(Class<?> mapperClass, String id, SqlSource sqlSource) {
        return this.addMappedStatement(mapperClass, id, sqlSource, SqlCommandType.DELETE, (Class)null, (String)null, Integer.class, new NoKeyGenerator(), (String)null, (String)null);
    }

其调用了addMappedStatement方法:

public MappedStatement addMappedStatement(Class<?> mapperClass, String id, SqlSource sqlSource, SqlCommandType sqlCommandType, Class<?> parameterClass, String resultMap, Class<?> resultType, KeyGenerator keyGenerator, String keyProperty, String keyColumn) {
        String statementName = mapperClass.getName() + "." + id;
        //判断是否已经存在
        if (this.hasMappedStatement(statementName)) {
            System.err.println("{" + statementName + "} Has been loaded by XML or SqlProvider, ignoring the injection of the SQL.");
            return null;
        } else {
            boolean isSelect = false;
            //判断是否查询语句
            if (sqlCommandType == SqlCommandType.SELECT) {
                isSelect = true;
            }
//从这里继续跟 参考③
            return this.builderAssistant.addMappedStatement(id, sqlSource, StatementType.PREPARED, sqlCommandType, (Integer)null, (Integer)null, (String)null, parameterClass, resultMap, resultType, (ResultSetType)null, !isSelect, isSelect, false, keyGenerator, keyProperty, keyColumn, this.configuration.getDatabaseId(), this.languageDriver, (String)null);
        }
    }

③ MapperBuilderAssistant.addMappedStatement(String id, SqlSource sqlSource, StatementType statementType, SqlCommandType sqlCommandType, Integer fetchSize, Integer timeout, String parameterMap, Class<?> parameterType, String resultMap, Class<?> resultType, ResultSetType resultSetType, boolean flushCache, boolean useCache, boolean resultOrdered, KeyGenerator keyGenerator, String keyProperty, String keyColumn, String databaseId, LanguageDriver lang, String resultSets)

是的,这个方法的参数多到了难以想象(xml中标签属性都有)。主要参数如下所示:

方法源码如下:

public MappedStatement addMappedStatement(String id, SqlSource sqlSource, StatementType statementType, SqlCommandType sqlCommandType, Integer fetchSize, Integer timeout, String parameterMap, Class<?> parameterType, String resultMap, Class<?> resultType, ResultSetType resultSetType, boolean flushCache, boolean useCache, boolean resultOrdered, KeyGenerator keyGenerator, String keyProperty, String keyColumn, String databaseId, LanguageDriver lang, String resultSets) {
        if (this.unresolvedCacheRef) {
            throw new IncompleteElementException("Cache-ref not yet resolved");
        } else {
        //给方法名拼接上name space: com.jane.mp.mapper.EmployeeMapper.deleteById
            id = this.applyCurrentNamespace(id, false);
            boolean isSelect = sqlCommandType == SqlCommandType.SELECT;
//获取statementBuilder 
            org.apache.ibatis.mapping.MappedStatement.Builder statementBuilder = (new org.apache.ibatis.mapping.MappedStatement.Builder(this.configuration, id, sqlSource, sqlCommandType)).resource(this.resource).fetchSize(fetchSize).timeout(timeout).statementType(statementType).keyGenerator(keyGenerator).keyProperty(keyProperty).keyColumn(keyColumn).databaseId(databaseId).lang(lang).resultOrdered(resultOrdered).resultSets(resultSets).resultMaps(this.getStatementResultMaps(resultMap, resultType, id)).resultSetType(resultSetType).flushCacheRequired((Boolean)this.valueOrDefault(flushCache, !isSelect)).useCache((Boolean)this.valueOrDefault(useCache, isSelect)).cache(this.currentCache);
//获取参数对象
            ParameterMap statementParameterMap = this.getStatementParameterMap(parameterMap, parameterType, id);
            if (statementParameterMap != null) {
                statementBuilder.parameterMap(statementParameterMap);
            }
            MappedStatement statement = statementBuilder.build();
            //从这里跟进去,参考④
            this.configuration.addMappedStatement(statement);
            return statement;
        }
    }


看一下创建的MappedStatement对象:


④ MybatisConfiguration.addMappedStatement(MappedStatement ms)

 public void addMappedStatement(MappedStatement ms) {
        logger.debug("addMappedStatement: " + ms.getId());
        //判断是否刷新,如果是就移除掉
        if (GlobalConfigUtils.isRefresh(ms.getConfiguration())) {
            this.mappedStatements.remove(ms.getId());
        } else if (this.mappedStatements.containsKey(ms.getId())) {
            logger.error("mapper[" + ms.getId() + "] is ignored, because it's exists, maybe from xml file");
            return;
        }
//从这里继续跟进去
        super.addMappedStatement(ms);
    }

留意日志发现其打印了addMappedStatement: com.jane.mp.mapper.EmployeeMapper.deleteById

父类Configuration的addMappedStatement方法很简单:

 public void addMappedStatement(MappedStatement ms) {
 //以 ms.getId():ms  放入MybatisConfiguration的mappedStatements属性中
        this.mappedStatements.put(ms.getId(), ms);
    }


如下可以看到刚添加的deleteById :

同理注入其他:


在上面的图示中我们还看到了许多SqlRunner.XXX 方法,那么这些是什么时候注入的呢?是在实例化MybatisSqlSessionFactoryBean过程中注入的。这又是一个漫长的过程,参考【7】


【7】sqlSessionFactoryBean的实例化过程

如下图所示,我们这里仍旧从AbstractAutowireCapableBeanFactory.invokeInitMethods方法中的afterPropertiesSet方法开始开始:


① MybatisSqlSessionFactoryBean.afterPropertiesSet

方法源码如下:

 public void afterPropertiesSet() throws Exception {
 //几个断言
        notNull(dataSource, "Property 'dataSource' is required");
        notNull(sqlSessionFactoryBuilder, "Property 'sqlSessionFactoryBuilder' is required");
        state((configuration == null && configLocation == null) || !(configuration != null && configLocation != null),
            "Property 'configuration' and 'configLocation' can not specified with together");
//创建一个sqlsessionfactory,从这里跟进去② 
        this.sqlSessionFactory = buildSqlSessionFactory();
    }

② MybatisSqlSessionFactoryBean.buildSqlSessionFactory

这个是核心方法,用来创建Configuration 实例并根据configuration实例获取sqlsessionFactory实例。

    protected SqlSessionFactory buildSqlSessionFactory() throws Exception {
        Configuration configuration;
        // TODO 加载自定义 MybatisXmlConfigBuilder
        MybatisXMLConfigBuilder xmlConfigBuilder = null;
        if (this.configuration != null) {
            configuration = this.configuration;
            if (configuration.getVariables() == null) {
                configuration.setVariables(this.configurationProperties);
            } else if (this.configurationProperties != null) {
                configuration.getVariables().putAll(this.configurationProperties);
            }
            //这里,如果configLocation 不为null,则获取MybatisXMLConfigBuilder并得到Configuration
        } else if (this.configLocation != null) {
            xmlConfigBuilder = new MybatisXMLConfigBuilder(this.configLocation.getInputStream(), null, this.configurationProperties);
            configuration = xmlConfigBuilder.getConfiguration();
        } else {
            if (LOGGER.isDebugEnabled()) {
                LOGGER.debug("Property 'configuration' or 'configLocation' not specified, using default MyBatis Configuration");
            }
            // TODO 使用自定义配置,注意这里直接new了一个MybatisConfiguration对象
            configuration = new MybatisConfiguration();
            if (this.configurationProperties != null) {
                configuration.setVariables(this.configurationProperties);
            }
        }
        if (this.objectFactory != null) {
            configuration.setObjectFactory(this.objectFactory);
        }
        if (this.objectWrapperFactory != null) {
            configuration.setObjectWrapperFactory(this.objectWrapperFactory);
        }
        if (this.vfs != null) {
            configuration.setVfsImpl(this.vfs);
        }
        if (hasLength(this.typeAliasesPackage)) {
            // TODO 支持自定义通配符
            String[] typeAliasPackageArray;
            if (typeAliasesPackage.contains("*") && !typeAliasesPackage.contains(",")
                && !typeAliasesPackage.contains(";")) {
                typeAliasPackageArray = PackageHelper.convertTypeAliasesPackage(typeAliasesPackage);
            } else {
                typeAliasPackageArray = tokenizeToStringArray(this.typeAliasesPackage,
                    ConfigurableApplicationContext.CONFIG_LOCATION_DELIMITERS);
            }
            if (typeAliasPackageArray == null) {
                throw new MybatisPlusException("not find typeAliasesPackage:" + typeAliasesPackage);
            }
            for (String packageToScan : typeAliasPackageArray) {
                configuration.getTypeAliasRegistry().registerAliases(packageToScan,
                    typeAliasesSuperType == null ? Object.class : typeAliasesSuperType);
                if (LOGGER.isDebugEnabled()) {
                    LOGGER.debug("Scanned package: '" + packageToScan + "' for aliases");
                }
            }
        }
        // TODO 自定义枚举类扫描处理
        if (hasLength(this.typeEnumsPackage)) {
            Set<Class> classes = null;
            if (typeEnumsPackage.contains("*") && !typeEnumsPackage.contains(",")
                && !typeEnumsPackage.contains(";")) {
                classes = PackageHelper.scanTypePackage(typeEnumsPackage);
            } else {
                String[] typeEnumsPackageArray = tokenizeToStringArray(this.typeEnumsPackage,
                    ConfigurableApplicationContext.CONFIG_LOCATION_DELIMITERS);
                if (typeEnumsPackageArray == null) {
                    throw new MybatisPlusException("not find typeEnumsPackage:" + typeEnumsPackage);
                }
                classes = new HashSet<Class>();
                for (String typePackage : typeEnumsPackageArray) {
                    classes.addAll(PackageHelper.scanTypePackage(typePackage));
                }
            }
            // 取得类型转换注册器
            TypeHandlerRegistry typeHandlerRegistry = configuration.getTypeHandlerRegistry();
            for (Class cls : classes) {
                if (cls.isEnum()) {
                    if (IEnum.class.isAssignableFrom(cls)) {
                        typeHandlerRegistry.register(cls.getName(), com.baomidou.mybatisplus.handlers.EnumTypeHandler.class.getCanonicalName());
                    } else {
                        // 使用原生 EnumOrdinalTypeHandler
                        typeHandlerRegistry.register(cls.getName(), org.apache.ibatis.type.EnumOrdinalTypeHandler.class.getCanonicalName());
                    }
                }
            }
        }
        if (!isEmpty(this.typeAliases)) {
            for (Class<?> typeAlias : this.typeAliases) {
                configuration.getTypeAliasRegistry().registerAlias(typeAlias);
                if (LOGGER.isDebugEnabled()) {
                    LOGGER.debug("Registered type alias: '" + typeAlias + "'");
                }
            }
        }
        if (!isEmpty(this.plugins)) {
            for (Interceptor plugin : this.plugins) {
                configuration.addInterceptor(plugin);
                if (LOGGER.isDebugEnabled()) {
                    LOGGER.debug("Registered plugin: '" + plugin + "'");
                }
            }
        }
        if (hasLength(this.typeHandlersPackage)) {
            String[] typeHandlersPackageArray = tokenizeToStringArray(this.typeHandlersPackage,
                ConfigurableApplicationContext.CONFIG_LOCATION_DELIMITERS);
            for (String packageToScan : typeHandlersPackageArray) {
                configuration.getTypeHandlerRegistry().register(packageToScan);
                if (LOGGER.isDebugEnabled()) {
                    LOGGER.debug("Scanned package: '" + packageToScan + "' for type handlers");
                }
            }
        }
        if (!isEmpty(this.typeHandlers)) {
            for (TypeHandler<?> typeHandler : this.typeHandlers) {
                configuration.getTypeHandlerRegistry().register(typeHandler);
                if (LOGGER.isDebugEnabled()) {
                    LOGGER.debug("Registered type handler: '" + typeHandler + "'");
                }
            }
        }
        if (this.databaseIdProvider != null) {//fix #64 set databaseId before parse mapper xmls
            try {
                configuration.setDatabaseId(this.databaseIdProvider.getDatabaseId(this.dataSource));
            } catch (SQLException e) {
                throw new NestedIOException("Failed getting a databaseId", e);
            }
        }
        if (this.cache != null) {
            configuration.addCache(this.cache);
        }
        if (xmlConfigBuilder != null) {
            try {
                xmlConfigBuilder.parse();
                if (LOGGER.isDebugEnabled()) {
                    LOGGER.debug("Parsed configuration file: '" + this.configLocation + "'");
                }
            } catch (Exception ex) {
                throw new NestedIOException("Failed to parse config resource: " + this.configLocation, ex);
            } finally {
                ErrorContext.instance().reset();
            }
        }
        if (this.transactionFactory == null) {
            this.transactionFactory = new SpringManagedTransactionFactory();
        }
        configuration.setEnvironment(new Environment(this.environment, this.transactionFactory, this.dataSource));
        // 设置元数据相关
        GlobalConfigUtils.setMetaData(dataSource, globalConfig);
        SqlSessionFactory sqlSessionFactory = this.sqlSessionFactoryBuilder.build(configuration);
        // TODO SqlRunner
        SqlRunner.FACTORY = sqlSessionFactory;
        // TODO 缓存 sqlSessionFactory
        globalConfig.setSqlSessionFactory(sqlSessionFactory);
        // TODO 设置全局参数属性
        globalConfig.signGlobalConfig(sqlSessionFactory);
        if (!isEmpty(this.mapperLocations)) {
            if (globalConfig.isRefresh()) {
                //TODO 设置自动刷新配置 减少配置
                new MybatisMapperRefresh(this.mapperLocations, sqlSessionFactory, 2,
                    2, true);
            }
            for (Resource mapperLocation : this.mapperLocations) {
                if (mapperLocation == null) {
                    continue;
                }
                try {
 // 解析mapper.xml中的配置,并将CRUD等创建为一个个mappedstatement对象放到configuration中
                    XMLMapperBuilder xmlMapperBuilder = new XMLMapperBuilder(mapperLocation.getInputStream(),
                        configuration, mapperLocation.toString(), configuration.getSqlFragments());
                    xmlMapperBuilder.parse();
                } catch (Exception e) {
                    throw new NestedIOException("Failed to parse mapping resource: '" + mapperLocation + "'", e);
                } finally {
                    ErrorContext.instance().reset();
                }
                if (LOGGER.isDebugEnabled()) {
                    LOGGER.debug("Parsed mapper file: '" + mapperLocation + "'");
                }
            }
        } else {
            if (LOGGER.isDebugEnabled()) {
                LOGGER.debug("Property 'mapperLocations' was not specified or no matching resources found");
            }
        }
        return sqlSessionFactory;
    }

其他咱们先不管,由于本文测试的时候使用了mybatis-config.xml,所以这里会走到xmlConfigBuilder = new MybatisXMLConfigBuilder(this.configLocation.getInputStream(), null, this.configurationProperties);,我们继续跟踪其构造函数:

public MybatisXMLConfigBuilder(InputStream inputStream, String environment, Properties props) {
        this(new XPathParser(inputStream, true, props, new XMLMapperEntityResolver()), environment, props);
    }
#从这里继续跟踪进去
    private MybatisXMLConfigBuilder(XPathParser parser, String environment, Properties props) {
        //TODO 自定义 Configuration
        super(new MybatisConfiguration());
        ErrorContext.instance().resource("SQL Mapper Configuration");
        this.configuration.setVariables(props);
        this.parsed = false;
        this.environment = environment;
        this.parser = parser;
    }
#可以看到首先调用了   super(new MybatisConfiguration());
#还是走到了new MybatisConfiguration()

③ new MybatisConfiguration()如此重要

那么我们看下MybatisConfiguration这个类,其实MyBatis 或者 MP全局配置对象,其属性如下:

public class MybatisConfiguration extends Configuration {
    private static final Log logger = LogFactory.getLog(MybatisConfiguration.class);
    /**
     * Mapper 注册
     */
    public final MybatisMapperRegistry mybatisMapperRegistry = new MybatisMapperRegistry(this);
    /**
     * 初始化调用
     */
    public MybatisConfiguration() {
        this.setDefaultScriptingLanguage(MybatisXMLLanguageDriver.class);
        logger.debug("Mybatis-plus init success.");
    }
    //...
 }   


方法列表如下:


可以看到该类主要做三件事情:获取一个MybatisMapperRegistry注册中心、往mappedStatements中添加MappedStatement对象以及往mybatisMapperRegistry注册中心添加mapper: mybatisMapperRegistry.addMappers(packageName, superType);


需要擦亮眼睛的是,public final MybatisMapperRegistry mybatisMapperRegistry = new MybatisMapperRegistry(this);这是一个用final修饰的变量,也就是常量。那么常量是在何时被赋值的呢?


静态变量和常量(如果有初始值)在类加载过程中就已经初始化了,在编译的时候存储在class的常量池中,在加载后又被放进方法区的运行时常量池中。


这里常量mybatisMapperRegistry被赋予了初始值,那么在类MybatisConfiguration加载过程中的编译阶段就会将初始值存入constantValue属性(class文件的常量池)中,在准备阶段就将constantValue的值赋给mybatisMapperRegistry。


④ MybatisMapperRegistry

类源码如下:

public class MybatisMapperRegistry extends MapperRegistry {
//如果一个mapper已经被加载过,将会被放到knownMappers 中
    private final Map<Class<?>, MapperProxyFactory<?>> knownMappers = new HashMap<>();
    //MybatisConfiguration实例对象的引用
    private final Configuration config;
    public MybatisMapperRegistry(Configuration config) {
        super(config);
        this.config = config;
        // TODO注入SqlRunner
        GlobalConfigUtils.getSqlInjector(config).injectSqlRunner(config);
    }
    @Override
    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 MybatisPlusMapperRegistry.");
        }
        try {
            return mapperProxyFactory.newInstance(sqlSession);
        } catch (Exception e) {
            throw new BindingException("Error getting mapper instance. Cause: " + e, e);
        }
    }
    @Override
    public <T> boolean hasMapper(Class<T> type) {
        return knownMappers.containsKey(type);
    }
    @Override
    public <T> void addMapper(Class<T> type) {
        if (type.isInterface()) {
            if (hasMapper(type)) {
                // TODO 如果之前注入 直接返回
                return;
                // throw new BindingException("Type " + type +
                // " is already known to the MybatisPlusMapperRegistry.");
            }
            boolean loadCompleted = false;
            try {
                knownMappers.put(type, new MapperProxyFactory<>(type));
                // It's important that the type is added before the parser is run
                // otherwise the binding may automatically be attempted by the
                // mapper parser. If the type is already known, it won't try.
                // TODO 自定义无 XML 注入---解析mapper中方法上面使用了注解SQL
                MybatisMapperAnnotationBuilder parser = new MybatisMapperAnnotationBuilder(config, type);
                parser.parse();
                loadCompleted = true;
            } finally {
                if (!loadCompleted) {
                    knownMappers.remove(type);
                }
            }
        }
    }
    /**
     * @since 3.2.2
     */
    @Override
    public Collection<Class<?>> getMappers() {
        return Collections.unmodifiableCollection(knownMappers.keySet());
    }
}


核心在这里:

    GlobalConfigUtils.getSqlInjector(config).injectSqlRunner(config);

这里将会调用AutoSqlInjector.injectSqlRunner(Configuration configuration)注入sqlrunner相关的mappedStatement对象。

   @Override
    public void injectSqlRunner(Configuration configuration) {
        this.configuration = configuration;
        this.languageDriver = configuration.getDefaultScriptingLanguageInstance();
        initSelectList();
        initSelectObjs();
        initInsert();
        initUpdate();
        initDelete();
        initCount();
    }

最后看下日志:


【8】mapperLocation设置的那些mapper.xml配置文件何时被解析?

同样是在MybatisSqlSessionFactoryBean实例化过程中。代码如下实例:

if (!isEmpty(this.mapperLocations)) {
         if (globalConfig.isRefresh()) {
             //TODO 设置自动刷新配置 减少配置
             new MybatisMapperRefresh(this.mapperLocations, sqlSessionFactory, 2,
                 2, true);
         }
         for (Resource mapperLocation : this.mapperLocations) {
             if (mapperLocation == null) {
                 continue;
             }
             try {
                 // 获取一个XMLMapperBuilder 然后解析那些mapper.xml配置文件
                 XMLMapperBuilder xmlMapperBuilder = new XMLMapperBuilder(mapperLocation.getInputStream(),
                     configuration, mapperLocation.toString(), configuration.getSqlFragments());
                 xmlMapperBuilder.parse();
             }
             //...
         }
     }
     //...

① XMLMapperBuilder.parse

方法源码如下所示:

  public void parse() {
  //如果当前mapper.xml配置文件没有加载过,就会解析并加载
    if (!configuration.isResourceLoaded(resource)) {
    //从这里跟进去,参考2
      configurationElement(parser.evalNode("/mapper"));
      //解析完xml文件后,将该String resource放到loadedResources这个hashset中
      configuration.addLoadedResource(resource);
      bindMapperForNamespace();
    }
    parsePendingResultMaps();
    parsePendingCacheRefs();
    parsePendingStatements();
  }

② XMLMapperBuilder.configurationElement

方法源码如下:

  private void configurationElement(XNode context) {
    try {
    //获取namespace 
      String namespace = context.getStringAttribute("namespace");
      if (namespace == null || namespace.equals("")) {
        throw new BuilderException("Mapper's namespace cannot be empty");
      }
      builderAssistant.setCurrentNamespace(namespace);
      cacheRefElement(context.evalNode("cache-ref"));
      cacheElement(context.evalNode("cache"));
      parameterMapElement(context.evalNodes("/mapper/parameterMap"));
      resultMapElements(context.evalNodes("/mapper/resultMap"));
      sqlElement(context.evalNodes("/mapper/sql"));
      //解析那些CRUD标签,并创建一个个MappedStatement,然后放到MappedStatements这个map中 参考3
      buildStatementFromContext(context.evalNodes("select|insert|update|delete"));
    } catch (Exception e) {
      throw new BuilderException("Error parsing Mapper XML. The XML location is '" + resource + "'. Cause: " + e, e);
    }
  }

③ XMLMapperBuilder.buildStatementFromContext(List<XNode> list)

如下图所示,这里每一个NODE就对应了了一条SQL。

其有继续调用了如下方法:

 private void buildStatementFromContext(List<XNode> list, String requiredDatabaseId) {
   for (XNode context : list) {
   //获取一个XMLStatementBuilder ,然后解析结点
     final XMLStatementBuilder statementParser = new XMLStatementBuilder(configuration, builderAssistant, context, requiredDatabaseId);
     try {
     //这里参考4
       statementParser.parseStatementNode();
     } catch (IncompleteElementException e) {
       configuration.addIncompleteStatement(statementParser);
     }
   }
 }

④ XMLStatementBuilder.parseStatementNode()

这个方法获取了标签(insert|update|delete|select)的相关属性,并在最后调用了builderAssistant这个助手将其添加MybatisConfiguration的 protected final Map<String, MappedStatement> mappedStatements = new StrictMap<MappedStatement>("Mapped Statements collection"):

  public void parseStatementNode() {
    String id = context.getStringAttribute("id");
    String databaseId = context.getStringAttribute("databaseId");
    if (!databaseIdMatchesCurrent(id, databaseId, this.requiredDatabaseId)) {
      return;
    }
    Integer fetchSize = context.getIntAttribute("fetchSize");
    Integer timeout = context.getIntAttribute("timeout");
    String parameterMap = context.getStringAttribute("parameterMap");
    String parameterType = context.getStringAttribute("parameterType");
    Class<?> parameterTypeClass = resolveClass(parameterType);
    String resultMap = context.getStringAttribute("resultMap");
    String resultType = context.getStringAttribute("resultType");
    String lang = context.getStringAttribute("lang");
    LanguageDriver langDriver = getLanguageDriver(lang);
    Class<?> resultTypeClass = resolveClass(resultType);
    String resultSetType = context.getStringAttribute("resultSetType");
    StatementType statementType = StatementType.valueOf(context.getStringAttribute("statementType", StatementType.PREPARED.toString()));
    ResultSetType resultSetTypeEnum = resolveResultSetType(resultSetType);
    String nodeName = context.getNode().getNodeName();
    SqlCommandType sqlCommandType = SqlCommandType.valueOf(nodeName.toUpperCase(Locale.ENGLISH));
    boolean isSelect = sqlCommandType == SqlCommandType.SELECT;
    boolean flushCache = context.getBooleanAttribute("flushCache", !isSelect);
    boolean useCache = context.getBooleanAttribute("useCache", isSelect);
    boolean resultOrdered = context.getBooleanAttribute("resultOrdered", false);
    // Include Fragments before parsing
    XMLIncludeTransformer includeParser = new XMLIncludeTransformer(configuration, builderAssistant);
    includeParser.applyIncludes(context.getNode());
    // Parse selectKey after includes and remove them.
    processSelectKeyNodes(id, parameterTypeClass, langDriver);
    // Parse the SQL (pre: <selectKey> and <include> were parsed and removed)
    SqlSource sqlSource = langDriver.createSqlSource(configuration, context, parameterTypeClass);
    String resultSets = context.getStringAttribute("resultSets");
    String keyProperty = context.getStringAttribute("keyProperty");
    String keyColumn = context.getStringAttribute("keyColumn");
    KeyGenerator keyGenerator;
    String keyStatementId = id + SelectKeyGenerator.SELECT_KEY_SUFFIX;
    keyStatementId = builderAssistant.applyCurrentNamespace(keyStatementId, true);
    if (configuration.hasKeyGenerator(keyStatementId)) {
      keyGenerator = configuration.getKeyGenerator(keyStatementId);
    } else {
      keyGenerator = context.getBooleanAttribute("useGeneratedKeys",
          configuration.isUseGeneratedKeys() && SqlCommandType.INSERT.equals(sqlCommandType))
          ? Jdbc3KeyGenerator.INSTANCE : NoKeyGenerator.INSTANCE;
    }
//看到这个是不是很熟悉?ok下面就不再重复过程了
    builderAssistant.addMappedStatement(id, sqlSource, statementType, sqlCommandType,
        fetchSize, timeout, parameterMap, parameterTypeClass, resultMap, resultTypeClass,
        resultSetTypeEnum, flushCache, useCache, resultOrdered, 
        keyGenerator, keyProperty, keyColumn, databaseId, langDriver, resultSets);
  }

【9】如果我在mapper接口中放上自定义方法并使用注解呢?

如下所示:

public interface EmployeeMapper extends BaseMapper<Employee> {
    @Select("select * from tbl_employee")
    List<Employee> selectEmployee();
}


注解在哪里被解析然后注入?

① 当你没有xml配置文件时

① MapperFactoryBean.checkDaoConfig

  protected void checkDaoConfig() {
    super.checkDaoConfig();
    notNull(this.mapperInterface, "Property 'mapperInterface' is required");
    Configuration configuration = getSqlSession().getConfiguration();
    if (this.addToConfig && !configuration.hasMapper(this.mapperInterface)) {
      try {
        configuration.addMapper(this.mapperInterface);
      } catch (Exception e) {
        logger.error("Error while adding the mapper '" + this.mapperInterface + "' to configuration.", e);
        throw new IllegalArgumentException(e);
      } finally {
        ErrorContext.instance().reset();
      }
    }
  }


这个核心入口configuration.addMapper(this.mapperInterface);,会调用如下方法:


@Override
    public <T> void addMapper(Class<T> type) {
        mybatisMapperRegistry.addMapper(type);
    }

② MybatisMapperRegistry.addMapper(Class<T> type)

    @Override
    public <T> void addMapper(Class<T> type) {
        if (type.isInterface()) {
            if (hasMapper(type)) {
                // TODO 如果之前注入 直接返回
                return;
                // throw new BindingException("Type " + type +
                // " is already known to the MybatisPlusMapperRegistry.");
            }
            boolean loadCompleted = false;
            try {
                knownMappers.put(type, new MapperProxyFactory<>(type));
               //这里会对方法上面注解进行解析
                MybatisMapperAnnotationBuilder parser = new MybatisMapperAnnotationBuilder(config, type);
                parser.parse();
                loadCompleted = true;
            } finally {
                if (!loadCompleted) {
                    knownMappers.remove(type);
                }
            }
        }
    }


③ MybatisMapperAnnotationBuilder.parse

 public void parse() {
        String resource = type.toString();
        if (!configuration.isResourceLoaded(resource)) {
            loadXmlResource();
            configuration.addLoadedResource(resource);
            assistant.setCurrentNamespace(type.getName());
            parseCache();
            parseCacheRef();
            Method[] methods = type.getMethods();
            // TODO 注入 CURD 动态 SQL (应该在注解之前注入)
            if (BaseMapper.class.isAssignableFrom(type)) {
                GlobalConfigUtils.getSqlInjector(configuration).inspectInject(assistant, type);
            }
            for (Method method : methods) {
                try {
                    // issue #237
                    if (!method.isBridge()) {
                        parseStatement(method);
                    }
                } catch (IncompleteElementException e) {
                    configuration.addIncompleteMethod(new MethodResolver(this, method));
                }
            }
        }
        parsePendingMethods();
    }

② 如果有mapper.xml配置文件时(!isEmpty(this.mapperLocations))

在sqlSessionFactoryBean实例化过程中,会调用XMLMapperBuilder的parse方法。

  public void parse() {
    if (!configuration.isResourceLoaded(resource)) {
    //解析那些mapper.xml配置文件
      configurationElement(parser.evalNode("/mapper"));
      //标记资源已经被解析
      configuration.addLoadedResource(resource);
      //这里这里这里!
      bindMapperForNamespace();
    }
    parsePendingResultMaps();
    parsePendingCacheRefs();
    parsePendingStatements();
  }


这里我们重点看下bindMapperForNamespace方法:

  private void bindMapperForNamespace() {
  //获取命名空间
    String namespace = builderAssistant.getCurrentNamespace();
    if (namespace != null) {
      Class<?> boundType = null;
      try {
        boundType = Resources.classForName(namespace);
      } catch (ClassNotFoundException e) {
        //ignore, bound type is not required
      }
      if (boundType != null) {
        if (!configuration.hasMapper(boundType)) {
       //如果没有被解析过就执行如下方法
          configuration.addLoadedResource("namespace:" + namespace);
          //这里是核心入口
          configuration.addMapper(boundType);
        }
      }
    }
  }


configuration.addMapper(boundType);会调用MybatisMapperRegistry.addMapper(Class<T> type):

@Override
public <T> void addMapper(Class<T> type) {
     if (type.isInterface()) {
         if (hasMapper(type)) {
             // TODO 如果之前注入 直接返回
             return;
         }
         boolean loadCompleted = false;
         try {
             knownMappers.put(type, new MapperProxyFactory<>(type));
             // TODO 自定义无 XML 注入---解析方法上面的注解!!!
             MybatisMapperAnnotationBuilder parser = new MybatisMapperAnnotationBuilder(config, type);
             parser.parse();
             loadCompleted = true;
         } finally {
             if (!loadCompleted) {
                 knownMappers.remove(type);
             }
         }
     }
 }

当然,因为首先会做判断是否已经解析。所以当你配置了mapperLocations且其不为空的时候,在实例化sqlSessionFactoryBean的过程中就会完成mappedStatement对象的注入。当你实例化employeeMapper时调用其包装类的afterPropertiesSet方法以及其后一系列过程最终走到上面方法的时候,就不会再次注入!


③ MybatisMapperAnnotationBuilder.parse


为什么要看这个方法呢?上面我们分析了没有mapper.xml、有mapper.xml(配置了mapperLocations属性)的情况,但是如果有mapper.xml但是没有配置mapperLocations属性呢?

public void parse() {
     String resource = type.toString();
     if (!configuration.isResourceLoaded(resource)) {
     //在这里,会尝试加载接口下面的同名xml文件进行解析!!!
         loadXmlResource();
         configuration.addLoadedResource(resource);
         assistant.setCurrentNamespace(type.getName());
         parseCache();
         parseCacheRef();
         Method[] methods = type.getMethods();
         // TODO 注入 CURD 动态 SQL (应该在注解之前注入)
         if (BaseMapper.class.isAssignableFrom(type)) {
             GlobalConfigUtils.getSqlInjector(configuration).inspectInject(assistant, type);
         }
         for (Method method : methods) {
             try {
                 // issue #237
                 if (!method.isBridge()) {
                     parseStatement(method);
                 }
             } catch (IncompleteElementException e) {
                 configuration.addIncompleteMethod(new MethodResolver(this, method));
             }
         }
     }
     parsePendingMethods();
 }


MybatisMapperAnnotationBuilder.loadXmlResource

 private void loadXmlResource() {
//判断xml是否已经加载过
        if (!configuration.isResourceLoaded("namespace:" + type.getName())) {
        //接口同名xml文件
            String xmlResource = type.getName().replace('.', '/') + ".xml";
            InputStream inputStream = null;
            try {
                inputStream = Resources.getResourceAsStream(type.getClassLoader(), xmlResource);
            } catch (IOException e) {
                // ignore, resource is not required
            }
            if (inputStream != null) {
//获取XMLMapperBuilder进行解析
                XMLMapperBuilder xmlParser = new XMLMapperBuilder(inputStream, assistant.getConfiguration(), xmlResource, configuration.getSqlFragments(), type.getName());
                xmlParser.parse();
            }
        }
    }

【10】什么是mappedStatements?

其是Configuration的一个属性,类型为StrictMap:

protected final Map<String, MappedStatement> mappedStatements =
 new StrictMap<MappedStatement>("Mapped Statements collection");

一 个 MappedStatement对象对应 Mapper配置文件中的一个select/update/insert/delete节点,主要描述的是一条 SQL语句。

其主要属性如下:




看到这些属性是否很熟悉?我们对照下mapper.xml中一个select标签的属性:

也就是说,一个MappedStatement代表一个增删改查标签的详细信息。


【11】SqlMethod是什么?

这是一个SQL模板的枚举类,根据这些模板解析出一个个具体的SQL

public enum SqlMethod {
    /**
     * 插入
     */
    INSERT_ONE("insert", "插入一条数据(选择字段插入)", "<script>INSERT INTO %s %s VALUES %s</script>"),
    INSERT_ONE_ALL_COLUMN("insertAllColumn", "插入一条数据(全部字段插入)", "<script>INSERT INTO %s %s VALUES %s</script>"),
    /**
     * 删除
     */
    DELETE_BY_ID("deleteById", "根据ID 删除一条数据", "<script>DELETE FROM %s WHERE %s=#{%s}</script>"),
    DELETE_BY_MAP("deleteByMap", "根据columnMap 条件删除记录", "<script>DELETE FROM %s %s</script>"),
    DELETE("delete", "根据 entity 条件删除记录", "<script>DELETE FROM %s %s</script>"),
    DELETE_BATCH_BY_IDS("deleteBatchIds", "根据ID集合,批量删除数据", "<script>DELETE FROM %s WHERE %s IN (%s)</script>"),
    /**
     * 逻辑删除
     */
    LOGIC_DELETE_BY_ID("deleteById", "根据ID 逻辑删除一条数据", "<script>UPDATE %s %s WHERE %s=#{%s}</script>"),
    LOGIC_DELETE_BY_MAP("deleteByMap", "根据columnMap 条件逻辑删除记录", "<script>UPDATE %s %s %s</script>"),
    LOGIC_DELETE("delete", "根据 entity 条件逻辑删除记录", "<script>UPDATE %s %s %s</script>"),
    LOGIC_DELETE_BATCH_BY_IDS("deleteBatchIds", "根据ID集合,批量逻辑删除数据", "<script>UPDATE %s %s WHERE %s IN (%s)</script>"),
    /**
     * 修改
     */
    UPDATE_BY_ID("updateById", "根据ID 选择修改数据", "<script>UPDATE %s %s WHERE %s=#{%s} %s</script>"),
    UPDATE_ALL_COLUMN_BY_ID("updateAllColumnById", "根据ID 修改全部数据", "<script>UPDATE %s %s WHERE %s=#{%s} %s</script>"),
    UPDATE("update", "根据 whereEntity 条件,更新记录", "<script>UPDATE %s %s %s</script>"),
    UPDATE_FOR_SET("updateForSet", "根据 whereEntity 条件,自定义Set值更新记录", "<script>UPDATE %s %s %s</script>"),
    /**
     * 逻辑删除 -> 修改
     */
    LOGIC_UPDATE_BY_ID("updateById", "根据ID 修改数据", "<script>UPDATE %s %s WHERE %s=#{%s} %s</script>"),
    LOGIC_UPDATE_ALL_COLUMN_BY_ID("updateAllColumnById", "根据ID 选择修改数据", "<script>UPDATE %s %s WHERE %s=#{%s} %s</script>"),
    /**
     * 查询
     */
    SELECT_BY_ID("selectById", "根据ID 查询一条数据", "SELECT %s FROM %s WHERE %s=#{%s}"),
    SELECT_BY_MAP("selectByMap", "根据columnMap 查询一条数据", "<script>SELECT %s FROM %s %s</script>"),
    SELECT_BATCH_BY_IDS("selectBatchIds", "根据ID集合,批量查询数据", "<script>SELECT %s FROM %s WHERE %s IN (%s)</script>"),
    SELECT_ONE("selectOne", "查询满足条件一条数据", "<script>SELECT %s FROM %s %s</script>"),
    SELECT_COUNT("selectCount", "查询满足条件总记录数", "<script>SELECT COUNT(1) FROM %s %s</script>"),
    SELECT_LIST("selectList", "查询满足条件所有数据", "<script>SELECT %s FROM %s %s</script>"),
    SELECT_PAGE("selectPage", "查询满足条件所有数据(并翻页)", "<script>SELECT %s FROM %s %s</script>"),
    SELECT_MAPS("selectMaps", "查询满足条件所有数据", "<script>SELECT %s FROM %s %s</script>"),
    SELECT_MAPS_PAGE("selectMapsPage", "查询满足条件所有数据(并翻页)", "<script>SELECT %s FROM %s %s</script>"),
    SELECT_OBJS("selectObjs", "查询满足条件所有数据", "<script>SELECT %s FROM %s %s</script>"),
    /**
     * 逻辑删除 -> 查询
     */
    LOGIC_SELECT_BY_ID("selectById", "根据ID 查询一条数据", "SELECT %s FROM %s WHERE %s=#{%s} %s"),
    LOGIC_SELECT_BATCH_BY_IDS("selectBatchIds", "根据ID集合,批量查询数据", "<script>SELECT %s FROM %s WHERE %s IN (%s) %s</script>");
    private final String method;
    private final String desc;
    private final String sql;
    SqlMethod(final String method, final String desc, final String sql) {
        this.method = method;
        this.desc = desc;
        this.sql = sql;
    }
    public String getMethod() {
        return this.method;
    }
    public String getDesc() {
        return this.desc;
    }
    public String getSql() {
        return this.sql;
    }
}
目录
相关文章
|
3天前
|
SQL Java 数据库连接
【MyBatisPlus·最新教程】包含多个改造案例,常用注解、条件构造器、代码生成、静态工具、类型处理器、分页插件、自动填充字段
MyBatis-Plus是一个MyBatis的增强工具,在 MyBatis 的基础上只做增强不做改变,为简化开发、提高效率而生。本文讲解了最新版MP的使用教程,包含多个改造案例,常用注解、条件构造器、代码生成、静态工具、类型处理器、分页插件、自动填充字段等核心功能。
【MyBatisPlus·最新教程】包含多个改造案例,常用注解、条件构造器、代码生成、静态工具、类型处理器、分页插件、自动填充字段
|
12天前
|
SQL 缓存 Java
【详细实用のMyBatis教程】获取参数值和结果的各种情况、自定义映射、动态SQL、多级缓存、逆向工程、分页插件
本文详细介绍了MyBatis的各种常见用法MyBatis多级缓存、逆向工程、分页插件 包括获取参数值和结果的各种情况、自定义映射resultMap、动态SQL
【详细实用のMyBatis教程】获取参数值和结果的各种情况、自定义映射、动态SQL、多级缓存、逆向工程、分页插件
|
17天前
|
SQL 安全 Java
MyBatis-Plus条件构造器:构建安全、高效的数据库查询
MyBatis-Plus 提供了一套强大的条件构造器(Wrapper),用于构建复杂的数据库查询条件。Wrapper 类允许开发者以链式调用的方式构造查询条件,无需编写繁琐的 SQL 语句,从而提高开发效率并减少 SQL 注入的风险。
13 1
MyBatis-Plus条件构造器:构建安全、高效的数据库查询
|
1月前
|
SQL Java 数据库连接
mybatis使用四:dao接口参数与mapper 接口中SQL的对应和对应方式的总结,MyBatis的parameterType传入参数类型
这篇文章是关于MyBatis中DAO接口参数与Mapper接口中SQL的对应关系,以及如何使用parameterType传入参数类型的详细总结。
30 10
|
2月前
|
SQL XML Java
mybatis复习03,动态SQL,if,choose,where,set,trim标签及foreach标签的用法
文章介绍了MyBatis中动态SQL的用法,包括if、choose、where、set和trim标签,以及foreach标签的详细使用。通过实际代码示例,展示了如何根据条件动态构建查询、更新和批量插入操作的SQL语句。
mybatis复习03,动态SQL,if,choose,where,set,trim标签及foreach标签的用法
|
2月前
|
SQL XML Java
mybatis :sqlmapconfig.xml配置 ++++Mapper XML 文件(sql/insert/delete/update/select)(增删改查)用法
当然,这些仅是MyBatis功能的初步介绍。MyBatis还提供了高级特性,如动态SQL、类型处理器、插件等,可以进一步提供对数据库交互的强大支持和灵活性。希望上述内容对您理解MyBatis的基本操作有所帮助。在实际使用中,您可能还需要根据具体的业务要求调整和优化SQL语句和配置。
44 1
|
3月前
|
SQL Java 关系型数据库
SpringBoot 系列之 MyBatis输出SQL日志
这篇文章介绍了如何在SpringBoot项目中通过MyBatis配置输出SQL日志,具体方法是在`application.yml`或`application.properties`中设置MyBatis的日志实现为`org.apache.ibatis.logging.stdout.StdOutImpl`来直接在控制台打印SQL日志。
SpringBoot 系列之 MyBatis输出SQL日志
|
1月前
|
Java 数据库连接 Maven
mybatis使用一:springboot整合mybatis、mybatis generator,使用逆向工程生成java代码。
这篇文章介绍了如何在Spring Boot项目中整合MyBatis和MyBatis Generator,使用逆向工程来自动生成Java代码,包括实体类、Mapper文件和Example文件,以提高开发效率。
105 2
mybatis使用一:springboot整合mybatis、mybatis generator,使用逆向工程生成java代码。
|
1月前
|
SQL JSON Java
mybatis使用三:springboot整合mybatis,使用PageHelper 进行分页操作,并整合swagger2。使用正规的开发模式:定义统一的数据返回格式和请求模块
这篇文章介绍了如何在Spring Boot项目中整合MyBatis和PageHelper进行分页操作,并且集成Swagger2来生成API文档,同时定义了统一的数据返回格式和请求模块。
52 1
mybatis使用三:springboot整合mybatis,使用PageHelper 进行分页操作,并整合swagger2。使用正规的开发模式:定义统一的数据返回格式和请求模块
|
1月前
|
前端开发 Java Apache
Springboot整合shiro,带你学会shiro,入门级别教程,由浅入深,完整代码案例,各位项目想加这个模块的人也可以看这个,又或者不会mybatis-plus的也可以看这个
本文详细讲解了如何整合Apache Shiro与Spring Boot项目,包括数据库准备、项目配置、实体类、Mapper、Service、Controller的创建和配置,以及Shiro的配置和使用。
288 1
Springboot整合shiro,带你学会shiro,入门级别教程,由浅入深,完整代码案例,各位项目想加这个模块的人也可以看这个,又或者不会mybatis-plus的也可以看这个