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。

测试代码如下:

List<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属性设置完后进行一系列操作。关于该接口可以参考博文:Spring中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。

这里对类加载、对象创建过程不懂的同学可以参考博文:Java类的加载过程详解(加载验证准备解析初始化使用卸载)https://blog.csdn.net/J080624/article/details/82116500

ok,咱们继续回去看下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();
            }
        }
    }

也就是说即使你调用了MybatisMapperAnnotationBuilder而非MapperXmlBuilder的parse方法时,同样给xml了一个解析机会!当然,当你调用MapperXmlBuilder的parse方法时,在bindMapperForNamespace()也给了注解解析的机会!


【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;
    }

}

关于枚举类可以参考博文:自定义枚举类与使用enum关键字

<div class="post-text" itemprop="text"> <p>I have a problem with google maps markers. I want to use polish characters in the names of the markers. As i have in my db. This is the file which i use to create proper (afaik) XML. </p> <pre><code>function parseToXML($htmlStr) { $xmlStr=str_replace('<','&lt;',$htmlStr); $xmlStr=str_replace('>','&gt;',$xmlStr); $xmlStr=str_replace('"','&quot;',$xmlStr); $xmlStr=str_replace("'",'&#39;',$xmlStr); $xmlStr=str_replace("&",'&amp;',$xmlStr); return $xmlStr; } // Opens a connection to a MySQL server $connection=mysql_connect ('localhost', $username, $password); if (!$connection) { die('Not connected : ' . mysql_error()); } // Set the active MySQL database $db_selected = mysql_select_db($database, $connection); if (!$db_selected) { die ('Can\'t use db : ' . mysql_error()); } // Select all the rows in the markers table $query = "SELECT * FROM markers WHERE 1"; $result = mysql_query($query); if (!$result) { die('Invalid query: ' . mysql_error()); } header("Content-type: text/xml"); // Start XML file, echo parent node echo '<markers>'; // Iterate through the rows, printing XML nodes for each while ($row = @mysql_fetch_assoc($result)){ // ADD TO XML DOCUMENT NODE echo '<marker '; $test = $row['name']; //$test2 = string utf8_encode ( string $test ); echo 'name="' . $test . '" '; echo 'address="' . parseToXML($row['address']) . '" '; echo 'lat="' . $row['lat'] . '" '; echo 'lng="' . $row['lng'] . '" '; echo 'city="' . $row['city'] . '" '; echo '/>'; } // End XML file echo '</markers>'; </code></pre> <p>I get proper output from my db using this file (browser display polish characters properly) but when i create a map using: </p> <pre><code><!DOCTYPE html > <head> <meta name="viewport" content="initial-scale=1.0, user-scalable=no" /> <meta http-equiv="content-type" content="text/html; charset=UTF-8"/> <title>PHP/MySQL & Google Maps Example</title> <script src="https://maps.googleapis.com/maps/api/js?key=AIzaSyBdj-LlQrTCj6bQcAq4fxONy9MaZcXvfc8" type="text/javascript"></script> <link rel="stylesheet" type="text/css" href="style.css"> <script type="text/javascript"> //<![CDATA[ var customIcons = { restaurant: { icon: 'http://labs.google.com/ridefinder/images/mm_20_blue.png' }, bar: { icon: 'http://labs.google.com/ridefinder/images/mm_20_red.png' } }; function load() { var map = new google.maps.Map(document.getElementById("map"), { center: new google.maps.LatLng(50.046465, 19.913828), zoom: 13, mapTypeId: 'roadmap' }); var infoWindow = new google.maps.InfoWindow; // Change this depending on the name of your PHP file downloadUrl("markergen.php", function(data) { var xml = data.responseXML; var markers = xml.documentElement.getElementsByTagName("marker"); for (var i = 0; i < markers.length; i++) { var name = markers[i].getAttribute("name"); var address = markers[i].getAttribute("address"); var type = markers[i].getAttribute("city"); var point = new google.maps.LatLng( parseFloat(markers[i].getAttribute("lat")), parseFloat(markers[i].getAttribute("lng"))); var html = "<b>" + name + "</b> <br/>" + address; var icon = customIcons[type] || {}; var marker = new google.maps.Marker({ map: map, position: point, icon: icon.icon }); bindInfoWindow(marker, map, infoWindow, html); } }); } function bindInfoWindow(marker, map, infoWindow, html) { google.maps.event.addListener(marker, 'click', function() { infoWindow.setContent(html); infoWindow.open(map, marker); }); } function downloadUrl(url, callback) { var request = window.ActiveXObject ? new ActiveXObject('Microsoft.XMLHTTP') : new XMLHttpRequest; request.onreadystatechange = function() { if (request.readyState == 4) { request.onreadystatechange = doNothing; callback(request, request.status); } }; request.open('GET', url, true); request.send(null); } function doNothing() {} //]]> </script> </head> <body onload="load()"> <div id="map"></div> </body> </html> </code></pre> <p>Then i get whole data, but without correctly displayed polish characters. I know that i should probably use </p> <pre><code>string utf8_encode ( string $data ) </code></pre> <p>but i did try to put it in first file to convert data and fail. So my question is where i should put it exactly into my code? Or is there any other/better option to do this.</p> <p>EDIT: I'm now trying to use DOM objects like this:</p> <pre><code><?php require("../test1/phpsqlinfo_dbinfo.php"); // Start XML file, create parent node $dom = new DOMDocument("1.0"); $node = $dom->createElement("markers"); $parnode = $dom->appendChild($node); // Opens a connection to a MySQL server $mysqli = new mysqli("localhost", $username, $password, $database); if ($mysqli->connect_errno) { echo "Failed to connect to MySQL: (" . $mysqli->connect_errno . ") " . $mysqli->connect_error; } if (!$mysqli->set_charset("utf8")) { printf("Error loading character set utf8: %s ", $mysqli->error); exit(); } else { printf("Current character set: %s ", $mysqli->character_set_name()); } // Select all the rows in the markers table $query = "SELECT * FROM markers WHERE 1"; $result = $mysqli->query($query); header("Content-type: text/xml"); // Iterate through the rows, adding XML nodes for each while ($row = @mysqli_fetch_assoc($result)){ // ADD TO XML DOCUMENT NODE $node = $dom->createElement("marker"); $newnode = $parnode->appendChild($node); $newnode->setAttribute("name",$row['name']); $newnode->setAttribute("address", $row['address']); $newnode->setAttribute("lat", $row['lat']); $newnode->setAttribute("lng", $row['lng']); $newnode->setAttribute("city", $row['city']); } echo $dom->saveXML(); ?> </code></pre> <p>But now i have marker inside of marker in results like: <code><marker><marker>...</marker></marker></code> with data ofc. How to fix it? Still i'm not sure that data are correctly encoded but should be.</p> </div>
©️2020 CSDN 皮肤主题: 程序猿惹谁了 设计师:上身试试 返回首页