文章目录
- 一、目标:注解配置执行SQL
- 二、设计:注解配置执行SQL
- 三、实现:注解配置执行SQL
- 3.1 工程结构
- 3.2 注解配置执行SQL类图
- 3.3 脚本语言驱动器
- 3.3.1 脚本语言驱动器接口
- 3.3.2 XML语言驱动器
- 3.4 注解配置构建器
- 3.4.1 定义增删改查注解
- 3.4.2 注解配置解析
- 3.5 Mapper XML解析调用
- 3.5.1 修改配置项
- 3.5.2 修改映射构建器助手
- 3.5.3 XML配置构建器
- 3.5.4 映射器注册机
- 四、测试:注解配置执行SQL
- 4.1 测试环境配置
- 4.1.1 配置数据源配置
- 4.1.2 修改IUserDao用户持久层
- 4.2 单元测试
- 4.2.1 插入测试
- 4.2.2 查询测试(多条数据)
- 4.2.3 修改测试
- 4.2.4 删除测试
- 五、总结:注解配置执行SQL
一、目标:注解配置执行SQL
💡 扩展ORM框架功能,实现配置方法注解的方式处理增删改查操作
- 日常使用 Mybatis 框架,对于 XML 和注解配置也都是可以共用的,主要基于配置文件 mappers 中,引入的哪类资源。
- XML 配置:
resource="mapper/User_Mapper.xml"
- 注解配置:
class="com.lino.mybatis.test.dao.IUserDao"
- XML 配置:
- 两种使用方式的共性:都需要基于这些提供的信息,获取出:SQL语句、入参、出参等,并把这些信息包装成一个整体的映射语句,串联整个流程。
二、设计:注解配置执行SQL
💡 如何通过注解方式处理SQL的配置?
- 将注解的解析部分与 Mapper XML 进行策略处理,对于不同类型的使用方式,做到解析结果一致。
- 那么后续的处理 SQL 执行和结果封装等流程就可以正常执行了。
- 这部分代码逻辑变动,主要以 XMLConfigBuilder 配置构建器的 Mapper 解析开始。
- 因为只有从这里开始才是判断一个 Mapper 到底是使用了 XML 配置还是注解配置。
- 基于这样不同的 mapper 引入的类型信息,则需要在 XMLConfigBuilder 配置构建器,解析 Mapper 元素信息时进行判断,按照不同的获取类型,resource、class 进行不同的解析处理。
- 只要在解析处理中把这两部分的差异做适配处理,最终后续的流程就可以正常进行。
- 首先以加载解析 XML 为入口,处理不同配置 SQL 方式的解析操作。
- 就是结合原有解析 Mapper XML 配置扩展注解 SQL 配置方式。
- 具体的处理过程:主要在
XMLConfigBuilder#mapperElement
配置构建器解析 mapper 配置时,处理关于注解的解析部分。- 这些注解目前添加
@Select、@Insert、@Update、@Delete
。 - 处理解析注解则会向 Configuration 配置项中添加
ResultMap、MappedStatement
信息。 - 用于后续获取 Mapper 调用 DefaultSqlSession 对应的执行方法时,拿到映射器语句配置进行 SQL 执行和结果封装。
- 这些注解目前添加
三、实现:注解配置执行SQL
3.1 工程结构
mybatis-step-12
|-src|-main| |-java| |-com.lino.mybatis| |-annotations| | |-Delete.java| | |-Insert.java| | |-Select.java| | |-Update.java| |-binding| | |-MapperMethod.java| | |-MapperProxy.java| | |-MapperProxyFactory.java| | |-MapperRegistry.java| |-builder| | |-annotations| | | |-MapperAnnotationBuilder.java| | |-xml| | | |-XMLConfigBuilder.java| | | |-XMLMapperBuilder.java| | | |-XMLStatementBuilder.java| | |-BaseBuilder.java| | |-MapperBuilderAssistant.java| | |-ParameterExpression.java| | |-SqlSourceBuilder.java| | |-StaticSqlSource.java| |-datasource| | |-druid| | | |-DruidDataSourceFacroty.java| | |-pooled| | | |-PooledConnection.java| | | |-PooledDataSource.java| | | |-PooledDataSourceFacroty.java| | | |-PoolState.java| | |-unpooled| | | |-UnpooledDataSource.java| | | |-UnpooledDataSourceFacroty.java| | |-DataSourceFactory.java| |-executor| | |-parameter| | | |-ParameterHandler.java| | |-result| | | |-DefaultResultContext.java| | | |-DefaultResultHandler.java| | |-resultset| | | |-DefaultResultSetHandler.java| | | |-ResultSetHandler.java| | | |-ResultSetWrapper.java| | |-statement| | | |-BaseStatementHandler.java| | | |-PreparedStatementHandler.java| | | |-SimpleStatementHandler.java| | | |-StatementHandler.java| | |-BaseExecutor.java| | |-Executor.java| | |-SimpleExecutor.java| |-io| | |-Resources.java| |-mapping| | |-BoundSql.java| | |-Environment.java| | |-MappedStatement.java| | |-ParameterMapping.java| | |-ResultMap.java| | |-ResultMapping.java| | |-SqlCommandType.java| | |-SqlSource.java| |-parsing| | |-GenericTokenParser.java| | |-TokenHandler.java| |-reflection| | |-factory| | | |-DefaultObjectFactory.java| | | |-ObjectFactory.java| | |-invoker| | | |-GetFieldInvoker.java| | | |-Invoker.java| | | |-MethodInvoker.java| | | |-SetFieldInvoker.java| | |-property| | | |-PropertyNamer.java| | | |-PropertyTokenizer.java| | |-wrapper| | | |-BaseWrapper.java| | | |-BeanWrapper.java| | | |-CollectionWrapper.java| | | |-DefaultObjectWrapperFactory.java| | | |-MapWrapper.java| | | |-ObjectWrapper.java| | | |-ObjectWrapperFactory.java| | |-MetaClass.java| | |-MetaObject.java| | |-Reflector.java| | |-SystemMetaObject.java| |-scripting| | |-defaults| | | |-DefaultParameterHandler.java| | | |-RawSqlSource.java| | |-xmltags| | | |-DynamicContext.java| | | |-MixedSqlNode.java| | | |-SqlNode.java| | | |-StaticTextSqlNode.java| | | |-XMLLanguageDriver.java| | | |-XMLScriptBuilder.java| | |-LanguageDriver.java| | |-LanguageDriverRegistry.java| |-session| | |-defaults| | | |-DefaultSqlSession.java| | | |-DefaultSqlSessionFactory.java| | |-Configuration.java| | |-ResultContext.java| | |-ResultHandler.java| | |-RowBounds.java| | |-SqlSession.java| | |-SqlSessionFactory.java| | |-SqlSessionFactoryBuilder.java| | |-TransactionIsolationLevel.java| |-transaction| | |-jdbc| | | |-JdbcTransaction.java| | | |-JdbcTransactionFactory.java| | |-Transaction.java| | |-TransactionFactory.java| |-type| | |-BaseTypeHandler.java| | |-IntegerTypeHandler.java| | |-JdbcType.java| | |-LongTypeHandler.java| | |-StringTypeHandler.java| | |-TypeAliasRegistry.java| | |-TypeHandler.java| | |-TypeHandlerRegistry.java|-test|-java| |-com.lino.mybatis.test| |-dao| | |-IUserDao.java| |-po| | |-User.java| |-ApiTest.java|-resources|-mybatis-config-datasource.xml
3.2 注解配置执行SQL类图
- XMLConfigBuilder 配置构建器是解析 Mapper 的入口。
- 以这条流程线中的方法
mapperElement
开始,处理 XML 解析的基础上,扩展注解的解析处理。 - 通过从
xml
读取到的class
配置,通过 Configuration 配置项类的添加 Mapper 方法,启动解析注解类语句的操作。 - 也就是在 MapperRegistry 映射器注册机,随着 Configuration 配置项调用
addMapper
时所做的注解解析操作。
- 以这条流程线中的方法
- 新增加核心类 MapperAnnotationBuilder 注解配置构建器,就是专门解析注解为主的类。
- 注解方式的解析主要通过 Method 类,获取方法的入参、出参信息。
- 以及基于这样的信息,获取到 LanguageDriver 脚本语言驱动器,从而创建出 SqlSession 属性。
- 再往下执行,就和前面的一样。
- 获取 Mapper 调用 DefaultSqlSession 对应的方法。
- 以及从 Configuartion 配置项中获取到解析的 SQL 信息做相应的参数设置和结果包装操作。
3.3 脚本语言驱动器
3.3.1 脚本语言驱动器接口
LanguageDriver.java
package com.lino.mybatis.scripting;import com.lino.mybatis.executor.parameter.ParameterHandler;
import com.lino.mybatis.mapping.BoundSql;
import com.lino.mybatis.mapping.MappedStatement;
import com.lino.mybatis.mapping.SqlSource;
import com.lino.mybatis.session.Configuration;
import org.dom4j.Element;/*** @description: 脚本语言驱动*/
public interface LanguageDriver {/*** 创建SQL源** @param configuration 配置项* @param script 元素* @param parameterType 参数类型* @return SqlSource SQL源码*/SqlSource createSqlSource(Configuration configuration, Element script, Class<?> parameterType);/*** 创建SQL源(annotation 注解方式)** @param configuration 配置项* @param script 注解名称* @param parameterType 参数类型* @return SqlSource SQL源码*/SqlSource createSqlSource(Configuration configuration, String script, Class<?> parameterType);/*** 创建参数处理器** @param mappedStatement 映射器语句类* @param parameterObject 参数对象* @param boundSql sql语句* @return ParameterHandler 参数处理器*/ParameterHandler createParameterHandler(MappedStatement mappedStatement, Object parameterObject, BoundSql boundSql);
}
- 通过重载一个
createSqlSource
接口,把script
的入参设置为String
类型,来解析注解 SQL 的配置
3.3.2 XML语言驱动器
XMLLanguageDriver.java
package com.lino.mybatis.scripting.xmltags;import com.lino.mybatis.executor.parameter.ParameterHandler;
import com.lino.mybatis.mapping.BoundSql;
import com.lino.mybatis.mapping.MappedStatement;
import com.lino.mybatis.mapping.SqlSource;
import com.lino.mybatis.scripting.LanguageDriver;
import com.lino.mybatis.scripting.defaults.DefaultParameterHandler;
import com.lino.mybatis.scripting.defaults.RawSqlSource;
import com.lino.mybatis.session.Configuration;
import org.dom4j.Element;/*** @description: XML 语言驱动器*/
public class XMLLanguageDriver implements LanguageDriver {@Overridepublic SqlSource createSqlSource(Configuration configuration, Element script, Class<?> parameterType) {// 用XML脚本构建器解析XMLScriptBuilder builder = new XMLScriptBuilder(configuration, script, parameterType);return builder.parseScriptNode();}@Overridepublic SqlSource createSqlSource(Configuration configuration, String script, Class<?> parameterType) {// 暂时不解析动态 SQLreturn new RawSqlSource(configuration, script, parameterType);}@Overridepublic ParameterHandler createParameterHandler(MappedStatement mappedStatement, Object parameterObject, BoundSql boundSql) {return new DefaultParameterHandler(mappedStatement, parameterObject, boundSql);}
}
- 用于解析注解方式的
createSqlSource
方法,其与 XML 解析来说,更加简单。- 因为这里不需要提供专门的 XML 脚本构建器。
- 而是直接按照 SQL 的入参信息,创建 RawSqlSource 即可。
3.4 注解配置构建器
- 在 Mybatis 框架的实现中,有专门一个
annotations
注解包,来提供用于配置到 DAO 方法上的注解。 - 这些注解包括了所有的增删改查操作,同时设定一些额外的返回参数等。
3.4.1 定义增删改查注解
- 添加四个注解
@Insert、@Delete、@Update、@Select
Insert.java
package com.lino.mybatis.annotations;import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;/*** @description: insert 语句注解*/
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
public @interface Insert {String[] value();
}
Delete.java
package com.lino.mybatis.annotations;import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;/*** @description: delete 语句注解*/
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
public @interface Delete {String[] value();
}
Update.java
package com.lino.mybatis.annotations;import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;/*** @description: update 语句注解*/
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
public @interface Update {String[] value();
}
Select.java
package com.lino.mybatis.annotations;import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;/*** @description: select 语句注解*/
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
public @interface Select {String[] value();
}
- 在 Mybatis 源码中,除了上面的四个注解,还有如
@Arg、@InsertProvider、@Param、@ResultMap
等注解参数配置。
3.4.2 注解配置解析
MapperAnnotationBuilder.java
package com.lino.mybatis.builder.annotation;import com.lino.mybatis.annotations.Delete;
import com.lino.mybatis.annotations.Insert;
import com.lino.mybatis.annotations.Select;
import com.lino.mybatis.annotations.Update;
import com.lino.mybatis.binding.MapperMethod;
import com.lino.mybatis.builder.MapperBuilderAssistant;
import com.lino.mybatis.mapping.SqlCommandType;
import com.lino.mybatis.mapping.SqlSource;
import com.lino.mybatis.scripting.LanguageDriver;
import com.lino.mybatis.session.Configuration;
import com.lino.mybatis.session.ResultHandler;
import com.lino.mybatis.session.RowBounds;
import java.lang.annotation.Annotation;
import java.lang.reflect.*;
import java.util.*;/*** @description: 注解配置构建器 Mapper*/
public class MapperAnnotationBuilder {private final Set<Class<? extends Annotation>> sqlAnnotationTypes = new HashSet<>();private Configuration configuration;private MapperBuilderAssistant assistant;private Class<?> type;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;sqlAnnotationTypes.add(Select.class);sqlAnnotationTypes.add(Insert.class);sqlAnnotationTypes.add(Update.class);sqlAnnotationTypes.add(Delete.class);}public void parse() {String resource = type.toString();if (!configuration.isResourceLoaded(resource)) {assistant.setCurrentNamespace(type.getName());Method[] methods = type.getMethods();for (Method method : methods) {if (!method.isBridge()) {// 解析语句parseStatement(method);}}}}/*** 解析语句** @param method 方法*/private void parseStatement(Method method) {Class<?> parameterTypeClass = getParameterType(method);LanguageDriver languageDriver = getLanguageDriver(method);SqlSource sqlSource = getSqlSourceFromAnnotations(method, parameterTypeClass, languageDriver);if (sqlSource != null) {final String mappedStatementId = type.getName() + "." + method.getName();SqlCommandType sqlCommandType = getSqlCommandType(method);boolean isSelect = sqlCommandType == SqlCommandType.SELECT;String resultMapId = null;if (isSelect) {resultMapId = parseResultMap(method);}// 调用助手类assistant.addMappedStatement(mappedStatementId,sqlSource,sqlCommandType,parameterTypeClass,resultMapId,getReturnType(method),languageDriver);}}/*** 重点:DAO方法的返回类型,如果为 List 则需要获取集合中的对象类型** @param method 方法* @return 对象类型*/private Class<?> getReturnType(Method method) {Class<?> returnType = method.getReturnType();if (Collection.class.isAssignableFrom(returnType)) {Type returnTypeParameter = method.getGenericReturnType();if (returnTypeParameter instanceof ParameterizedType) {Type[] actualTypeArguments = ((ParameterizedType) returnTypeParameter).getActualTypeArguments();if (actualTypeArguments != null && actualTypeArguments.length == 1) {returnTypeParameter = actualTypeArguments[0];if (returnTypeParameter instanceof Class) {returnType = (Class<?>) returnTypeParameter;} else if (returnTypeParameter instanceof ParameterizedType) {returnType = (Class<?>) ((ParameterizedType) returnTypeParameter).getRawType();} else if (returnTypeParameter instanceof GenericArrayType) {Class<?> componentType = (Class<?>) ((GenericArrayType) returnTypeParameter).getGenericComponentType();// (issue #525) support List<byte[]>returnType = Array.newInstance(componentType, 0).getClass();}}}}return returnType;}private String parseResultMap(Method method) {// generateResultMapNameStringBuilder suffix = new StringBuilder();for (Class<?> c : method.getParameterTypes()) {suffix.append("-");suffix.append(c.getSimpleName());}if (suffix.length() < 1) {suffix.append("-void");}String resultMapId = type.getName() + "." + method.getName() + suffix;// 添加 ResultMapClass<?> returnType = getReturnType(method);assistant.addResultMap(resultMapId, returnType, new ArrayList<>());return resultMapId;}private SqlCommandType getSqlCommandType(Method method) {Class<? extends Annotation> type = getSqlAnnotationType(method);if (type == null) {return SqlCommandType.UNKNOWN;}return SqlCommandType.valueOf(type.getSimpleName().toUpperCase(Locale.ENGLISH));}private SqlSource getSqlSourceFromAnnotations(Method method, Class<?> parameterType, LanguageDriver languageDriver) {try {Class<? extends Annotation> sqlAnnotationType = getSqlAnnotationType(method);if (sqlAnnotationType != null) {Annotation sqlAnnotation = method.getAnnotation(sqlAnnotationType);final String[] strings = (String[]) sqlAnnotation.getClass().getMethod("value").invoke(sqlAnnotation);return buildSqlSourceFromStrings(strings, parameterType, languageDriver);}return null;} catch (Exception e) {throw new RuntimeException("Could not find value method on SQL annotation. Cause: " + e);}}private SqlSource buildSqlSourceFromStrings(String[] strings, Class<?> parameterTypeClass, LanguageDriver languageDriver) {final StringBuilder sql = new StringBuilder();for (String fragment : strings) {sql.append(fragment);sql.append(" ");}return languageDriver.createSqlSource(configuration, sql.toString(), parameterTypeClass);}private Class<? extends Annotation> getSqlAnnotationType(Method method) {for (Class<? extends Annotation> type : sqlAnnotationTypes) {Annotation annotation = method.getAnnotation(type);if (annotation != null) {return type;}}return null;}private LanguageDriver getLanguageDriver(Method method) {Class<?> langClass = configuration.getLanguageRegistry().getDefaultDriverClass();return configuration.getLanguageRegistry().getDriver(langClass);}private Class<?> getParameterType(Method method) {Class<?> parameterType = null;Class<?>[] parameterTypes = method.getParameterTypes();for (Class<?> clazz : parameterTypes) {if (!RowBounds.class.isAssignableFrom(clazz) && !ResultHandler.class.isAssignableFrom(clazz)) {if (parameterType == null) {parameterType = clazz;} else {parameterType = MapperMethod.ParamMap.class;}}}return parameterType;}
}
- 自定义注解的解析配置,主要在 MapperAnnotationBuilder 类中完成。
- 整个类在构造函数中配置需要解析的注解,并提供解析方法处理语句的解析。
- 这个类的解析操作,都是基于 Method 来获取参数类型、返回类型、注解方法等,完成解析过程。
- 整个解析的核心流程。
- 根据
Method#getParameterTypes
方法获取入参类型。 - 从 Configuration 配置项中获取默认的 LanguageDriver 脚本语言驱动。
- 以及基于注解所提供的配置信息,也就是
value
值中的 SQL 来创建 SqlSource 语句。
- 根据
- 这些基本的信息创建完成以后,则根据 SqlCommandType 的命令类型为
SELECT
时,创建出 ResultMap 信息。- 这个 ResultMap 会被写入到 Configuration 配置项的
Map<String, ResultMap> resultMaps
。
- 这个 ResultMap 会被写入到 Configuration 配置项的
- 整体准备好这些基本配置以后,则会调用 MapperBuilderAssistant 映射构建器助手,存入映射器语句。往后的流程都是一样。
- 注意:
getReturnType(Method method)
方法的使用。它有一个非常核心的问题点,在于要拿到方法的返回类型:- 如果是普通的基本类型或者对象类型,直接就可以返回。
- 如果是集合类型,则需要通过
Collection.class.isAssignableFrom
判断,再进行集合中参数类型的获取。- 例如:
List<User>
则需要根据method.getGenericReturnType()
获取返回类型,并判断是否为Class
进行返回具体的类型。
- 例如:
3.5 Mapper XML解析调用
- 在 Mybatis 的源码中,是基于 XML 配置构建器解析 Mapper 时进行判断处理,是
xml
还是注解。 - 如果是注解则会调用到
MapperRegistry#addMapper
方法,并开始执行解析注解的相关操作。
3.5.1 修改配置项
Configuration.java
package com.lino.mybatis.session;import com.lino.mybatis.binding.MapperRegistry;
import com.lino.mybatis.datasource.druid.DruidDataSourceFactory;
import com.lino.mybatis.datasource.pooled.PooledDataSourceFactory;
import com.lino.mybatis.datasource.unpooled.UnpooledDataSourceFactory;
import com.lino.mybatis.executor.Executor;
import com.lino.mybatis.executor.SimpleExecutor;
import com.lino.mybatis.executor.parameter.ParameterHandler;
import com.lino.mybatis.executor.resultset.DefaultResultSetHandler;
import com.lino.mybatis.executor.resultset.ResultSetHandler;
import com.lino.mybatis.executor.statement.PreparedStatementHandler;
import com.lino.mybatis.executor.statement.StatementHandler;
import com.lino.mybatis.mapping.BoundSql;
import com.lino.mybatis.mapping.Environment;
import com.lino.mybatis.mapping.MappedStatement;
import com.lino.mybatis.mapping.ResultMap;
import com.lino.mybatis.reflection.MetaObject;
import com.lino.mybatis.reflection.factory.DefaultObjectFactory;
import com.lino.mybatis.reflection.factory.ObjectFactory;
import com.lino.mybatis.reflection.wrapper.DefaultObjectWrapperFactory;
import com.lino.mybatis.reflection.wrapper.ObjectWrapperFactory;
import com.lino.mybatis.scripting.LanguageDriver;
import com.lino.mybatis.scripting.LanguageDriverRegistry;
import com.lino.mybatis.scripting.xmltags.XMLLanguageDriver;
import com.lino.mybatis.transaction.Transaction;
import com.lino.mybatis.transaction.jdbc.JdbcTransactionFactory;
import com.lino.mybatis.type.TypeAliasRegistry;
import com.lino.mybatis.type.TypeHandlerRegistry;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Map;
import java.util.Set;/*** @description: 配置项*/
public class Configuration {/*** 环境*/protected Environment environment;/*** 映射注册机*/protected MapperRegistry mapperRegistry = new MapperRegistry(this);/*** 映射的语句,存在Map里*/protected final Map<String, MappedStatement> mappedStatements = new HashMap<>(16);/*** 结果映射,存在Map里*/protected final Map<String, ResultMap> resultMaps = new HashMap<>(16);...public ResultMap getResultMap(String id) {return resultMaps.get(id);}public void addResultMap(ResultMap resultMap) {resultMaps.put(resultMap.getId(), resultMap);}
}
- 添加
Map<String, ResultMap> resultMaps
结果映射的列表。
3.5.2 修改映射构建器助手
MapperBuilderAssistant.java
package com.lino.mybatis.builder;import com.lino.mybatis.mapping.*;
import com.lino.mybatis.scripting.LanguageDriver;
import com.lino.mybatis.session.Configuration;
import java.util.ArrayList;
import java.util.List;/*** @description: 映射构建器助手,建造者*/
public class MapperBuilderAssistant extends BaseBuilder {private String currentNamespace;private String resource;public MapperBuilderAssistant(Configuration configuration, String resource) {super(configuration);this.resource = resource;}public String getCurrentNamespace() {return currentNamespace;}public void setCurrentNamespace(String currentNamespace) {this.currentNamespace = currentNamespace;}public String applyCurrentNamespace(String base, boolean isReference) {if (base == null) {return null;}if (isReference) {if (base.contains(".")) {return base;}} else {if (base.startsWith(currentNamespace + ".")) {return base;}if (base.contains(".")) {throw new RuntimeException("Dots are not allowed in element names, please remove it from " + base);}}return currentNamespace + "." + base;}/*** 添加映射器语句*/public MappedStatement addMappedStatement(String id, SqlSource sqlSource, SqlCommandType sqlCommandType,Class<?> parameterType, String resultMap, Class<?> resultType,LanguageDriver lang) {// 给id加上namespace前缀:com.lino.mybatis.test.dao.IUserDao.queryUserInfoByIdid = applyCurrentNamespace(id, false);MappedStatement.Builder statementBuilder = new MappedStatement.Builder(configuration, id, sqlCommandType, sqlSource, resultType);// 结果映射, 给 MappedStatement#resultMapssetStatementResultMap(resultMap, resultType, statementBuilder);MappedStatement statement = statementBuilder.build();// 映射语句信息,建造完存放到配置项中configuration.addMappedStatement(statement);return statement;}private void setStatementResultMap(String resultMap, Class<?> resultType, MappedStatement.Builder statementBuilder) {// 因为暂时还没有在 Mapper XML 中配置 Map 返回结果,所以这里返回的是 nullresultMap = applyCurrentNamespace(resultMap, true);List<ResultMap> resultMaps = new ArrayList<>();if (resultMap != null) {String[] resultMapNames = resultMap.split(",");for (String resultMapName : resultMapNames) {resultMaps.add(configuration.getResultMap(resultMapName.trim()));}}/** 通常使用 resultType 即可满足大部分场景* <select id="queryUserInfoById" resultType="com.lino.mybatis.test.po.User">* 使用 resultType 的情况下,Mybatis 会自动创建一个 ResultMap,基于属性名称映射列到 JavaBean 的属性上。*/else if (resultType != null) {ResultMap.Builder inlineResultMapBuilder = new ResultMap.Builder(configuration, statementBuilder.id() + "-Inline", resultType, new ArrayList<>());resultMaps.add(inlineResultMapBuilder.build());}statementBuilder.resultMaps(resultMaps);}public ResultMap addResultMap(String id, Class<?> type, List<ResultMapping> resultMappings) {ResultMap.Builder inlineResultMapBuilder = new ResultMap.Builder(configuration, id, type, resultMappings);ResultMap resultMap = inlineResultMapBuilder.build();configuration.addResultMap(resultMap);return resultMap;}
}
- 修改
applyCurrentNamespace
方法,添加对命名空间的全称判断。 - 修改
setStatementResultMap
方法, 完善resultMap != null
时的逻辑处理。 - 添加
addResultMap
方法,添加返回值集合。
3.5.3 XML配置构建器
XMLConfigBuilder
package com.lino.mybatis.builder.xml;import com.lino.mybatis.builder.BaseBuilder;
import com.lino.mybatis.datasource.DataSourceFactory;
import com.lino.mybatis.io.Resources;
import com.lino.mybatis.mapping.Environment;
import com.lino.mybatis.session.Configuration;
import com.lino.mybatis.transaction.TransactionFactory;
import org.dom4j.Document;
import org.dom4j.DocumentException;
import org.dom4j.Element;
import org.dom4j.io.SAXReader;
import org.xml.sax.InputSource;
import javax.sql.DataSource;
import java.io.InputStream;
import java.io.Reader;
import java.util.List;
import java.util.Properties;/*** @description: XML配置构建器,建造者模式,集成BaseBuilder*/
public class XMLConfigBuilder extends BaseBuilder {.../*** <mappers>* <mapper resource="org/mybatis/builder/AuthorMapper.xml"/>* <mapper resource="org/mybatis/builder/BlogMapper.xml"/>* <mapper resource="org/mybatis/builder/PostMapper.xml"/>* </mappers>*/private void mapperElement(Element mappers) throws Exception {List<Element> mapperList = mappers.elements("mapper");for (Element e : mapperList) {String resource = e.attributeValue("resource");String mapperClass = e.attributeValue("class");// XML解析if (resource != null && mapperClass == null) {InputStream inputStream = Resources.getResourceAsStream(resource);// 在for循环里每个mapper都重新new一个XMLMapperBuilder,来解析XMLMapperBuilder mapperParser = new XMLMapperBuilder(inputStream, configuration, resource);mapperParser.parse();}// Annontation 注解解析else if (resource == null && mapperClass != null) {Class<?> mapperInterface = Resources.classForName(mapperClass);configuration.addMapper(mapperInterface);}}}
}
- 在 XMLConfigBuilder 配置构建器的 Mapper 解析处理中,根据从 XML 配置获取到的
resource、class
分别进行判断解析。 - 如果
resource
为空,mapperClass
不为空,则进行注解的解析处理。- 在这段代码中则是根据
mapperClass
获取对应的接口,并通过Configuration#addMapper
方法,添加到配置项中。 - 而这个 Mapper 的添加会调用到 MapperRegistry 进而调用注解解析操作。
- 在这段代码中则是根据
3.5.4 映射器注册机
MapperRegistry.java
package com.lino.mybatis.binding;import cn.hutool.core.lang.ClassScanner;
import com.lino.mybatis.builder.annotation.MapperAnnotationBuilder;
import com.lino.mybatis.session.Configuration;
import com.lino.mybatis.session.SqlSession;
import java.util.HashMap;
import java.util.Map;
import java.util.Set;/*** @description: 映射器注册机*/
public class MapperRegistry {private Configuration configuration;public MapperRegistry(Configuration configuration) {this.configuration = configuration;}/*** 将已添加的映射器代理加入到HashMap*/private final Map<Class<?>, MapperProxyFactory<?>> knownMappers = new HashMap<>(16);public <T> T getMapper(Class<T> type, SqlSession sqlSession) {final MapperProxyFactory<T> mapperProxyFactory = (MapperProxyFactory<T>) knownMappers.get(type);if (mapperProxyFactory == null) {throw new RuntimeException("Type " + type + " is not known to the MapperRegistry.");}try {return mapperProxyFactory.newInstance(sqlSession);} catch (Exception e) {throw new RuntimeException("Error getting mapper instance. Cause: " + e, e);}}public <T> void addMapper(Class<T> type) {/* Mapper 必须是接口才会注册 */if (type.isInterface()) {if (hasMapper(type)) {// 如果重复添加,报错throw new RuntimeException("Type " + type + " is already known to the MapperRegistry.");}// 注册映射器代理工厂knownMappers.put(type, new MapperProxyFactory<>(type));// 解析注解类语句配置MapperAnnotationBuilder parser = new MapperAnnotationBuilder(configuration, type);parser.parse();}}public <T> boolean hasMapper(Class<T> type) {return knownMappers.containsKey(type);}public void addMappers(String packageName) {Set<Class<?>> mapperSet = ClassScanner.scanPackage(packageName);for (Class<?> mapperClass : mapperSet) {addMapper(mapperClass);}}
}
- 在
addMapper
方法中,根据Class
注册完映射器代理工厂后,则开始进行解析注解操作。
四、测试:注解配置执行SQL
4.1 测试环境配置
4.1.1 配置数据源配置
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE configuration PUBLIC "-//mybatis.org//DTD Config 3.0//EN" "http://mybatis.org/dtd/mybatis-3-config.dtd">
<configuration><environments default="development"><environment id="development"><transactionManager type="JDBC"/><dataSource type="POOLED"><property name="driver" value="com.mysql.jdbc.Driver"/><property name="url"value="jdbc:mysql://127.0.0.1:3306/mybatis?useUnicode=true&characterEncoding=utf8"/><property name="username" value="root"/><property name="password" value="123456"/></dataSource></environment></environments><mappers><!--XML配置--><!-- <mapper resource="mapper/User_Mapper.xml"/>--><!--注解配置--><mapper class="com.lino.mybatis.test.dao.IUserDao"/></mappers>
</configuration>
4.1.2 修改IUserDao用户持久层
IUserDao.java
package com.lino.mybatis.test.dao;import com.lino.mybatis.annotations.Delete;
import com.lino.mybatis.annotations.Insert;
import com.lino.mybatis.annotations.Select;
import com.lino.mybatis.annotations.Update;
import com.lino.mybatis.test.po.User;
import java.util.List;/*** @Description: 用户持久层*/
public interface IUserDao {/*** 根据ID查询用户信息** @param uId ID* @return User 用户*/@Select("SELECT id, userId, userName, userHead FROM user WHERE id = #{id}")User queryUserInfoById(Long uId);/*** 根据用户对象查询用户信息** @param user 用户* @return User 用户*/@Select("SELECT id, userId, userName, userHead FROM user WHERE id = #{id} and userId = #{userId}")User queryUserInfo(User user);/*** 查询用户对象列表** @return List<User> 用户列表*/@Select("SELECT id, userId, userName, userHead FROM user")List<User> queryUserInfoList();/*** 更新用户信息** @param user 用户对象* @return 受影响的行数*/@Update("UPDATE user SET userName = #{userName} WHERE id = #{id}")int updateUserInfo(User user);/*** 新增用户信息** @param user 用户对象* @return 受影响的行数*/@Insert("INSERT INTO user (userId, userName, userHead, createTime, updateTime) VALUES (#{userId}, #{userName}, #{userHead}, now(), now())")int insertUserInfo(User user);/*** 根据ID删除用户信息** @param uId ID* @return 受影响的行数*/@Delete("DELETE FROM user WHERE userId = #{userId}")int deleteUserInfoByUserId(String uId);
}
4.2 单元测试
4.2.1 插入测试
ApiTest.java
@Test
public void test_insertUserInfo() {// 1.获取映射器对象IUserDao userDao = sqlSession.getMapper(IUserDao.class);// 2.测试验证User user = new User();user.setUserId("10002");user.setUserName("张三");user.setUserHead("1_05");userDao.insertUserInfo(user);logger.info("测试结果:{}", "Insert OK");// 3.提交事务sqlSession.commit();
}
测试结果
16:32:32.510 [main] INFO c.l.mybatis.builder.SqlSourceBuilder - 构建参数映射 property:userId propertyType:class java.lang.String
16:32:32.510 [main] INFO c.l.mybatis.builder.SqlSourceBuilder - 构建参数映射 property:userName propertyType:class java.lang.String
16:32:32.510 [main] INFO c.l.mybatis.builder.SqlSourceBuilder - 构建参数映射 property:userHead propertyType:class java.lang.String
16:32:33.171 [main] INFO c.l.m.d.pooled.PooledDataSource - Created connention 597190999.
16:32:33.213 [main] INFO c.l.m.s.d.DefaultParameterHandler - 根据每个ParameterMapping中的TypeHandler设置对应的参数信息 value:"10002"
16:32:33.213 [main] INFO c.l.m.s.d.DefaultParameterHandler - 根据每个ParameterMapping中的TypeHandler设置对应的参数信息 value:"张三"
16:32:33.213 [main] INFO c.l.m.s.d.DefaultParameterHandler - 根据每个ParameterMapping中的TypeHandler设置对应的参数信息 value:"1_05"
16:32:33.213 [main] INFO com.lino.mybatis.test.ApiTest - 测试结果:Insert OK
- 从测试日志信息和数据库的截图,可以看到数据已经插入到数据库,验证通过
- 注意:执行完 SQL 以后,还执行一次
sqlSession.commit()
。- 这是因为在
DefaultSqlSessionFactory#openSession
开启Session
创建事务工厂的时候,传入给事务工厂构造函数的事务是否自动提交为 false。 - 所以这里就需要我们去手动提交事务,否则是不会插入到数据库中的。
- 这是因为在
4.2.2 查询测试(多条数据)
ApiTest.java
@Test
public void test_queryUserInfoList() {// 1.获取映射器对象IUserDao userDao = sqlSession.getMapper(IUserDao.class);// 2.测试验证: 对象参数List<User> users = userDao.queryUserInfoList();logger.info("测试结果:{}", JSON.toJSONString(users));
}
测试结果
16:40:47.699 [main] INFO c.l.m.s.defaults.DefaultSqlSession - 执行查询 statement:com.lino.mybatis.test.dao.IUserDao.queryUserInfoList parameter:null
16:40:48.361 [main] INFO c.l.m.d.pooled.PooledDataSource - Created connention 1192171522.
16:40:48.395 [main] INFO com.lino.mybatis.test.ApiTest - 测试结果:[{"id":1,"userHead":"1_04","userId":"10001","userName":"小零哥"},{"id":4,"userHead":"1_05","userId":"10002","userName":"张三"}]
- 现在我们再查询结果的时候,就可以查询到2条记录的集合了,这说明我们添加的
MapperMethod#execute
调用sqlSession.selectList(command.getName(), param)
是测试通过的。
4.2.3 修改测试
ApiTest.java
@Test
public void test_updateUserInfo() {// 1.获取映射器对象IUserDao userDao = sqlSession.getMapper(IUserDao.class);// 2.测试验证int count = userDao.updateUserInfo(new User(1L, "10001", "小灵哥"));logger.info("测试结果:{}", count);// 3.提交事务sqlSession.commit();
}
测试结果
16:44:11.979 [main] INFO c.l.m.d.pooled.PooledDataSource - Created connention 597190999.
16:44:12.027 [main] INFO c.l.m.s.d.DefaultParameterHandler - 根据每个ParameterMapping中的TypeHandler设置对应的参数信息 value:"小灵哥"
16:44:12.028 [main] INFO c.l.m.s.d.DefaultParameterHandler - 根据每个ParameterMapping中的TypeHandler设置对应的参数信息 value:1
16:44:12.037 [main] INFO com.lino.mybatis.test.ApiTest - 测试结果:1
- 这里测试验证把
ID=1
的用户,userName
修改为 小灵哥,通过测试日志和数据库截图,测试通过。
4.2.4 删除测试
ApiTest.java
@Test
public void test_deleteUserInfoByUserId() {// 1.获取映射器对象IUserDao userDao = sqlSession.getMapper(IUserDao.class);// 2.测试验证int count = userDao.deleteUserInfoByUserId("10002");logger.info("测试结果:{}", count == 1);// 3.提交事务sqlSession.commit();
}
测试结果
16:47:54.591 [main] INFO c.l.m.d.pooled.PooledDataSource - Created connention 597190999.
16:47:54.633 [main] INFO c.l.m.s.d.DefaultParameterHandler - 根据每个ParameterMapping中的TypeHandler设置对应的参数信息 value:"10002"
16:47:54.643 [main] INFO com.lino.mybatis.test.ApiTest - 测试结果:true
- 这里把数据表中
userId = '10002'
的用户删除掉,通过测试日志和数据库截图,测试通过。
五、总结:注解配置执行SQL
- 在原有解析 Mapper XML 的基础上扩展了使用注解方式的解析和处理,让整个框架功能更加完善。同时在扩展注解功能的结构时候,可以看到整个整合过程并不复杂,更多是类似模块式的拼装,通过开发出一个注解解析构建器,并在 Mapper 注册过程中完成调用和解析操作。
- 所以如果一个框架的整体设计是完善的,那么在功能的迭代和添加过程中也会是非常清晰容易的。
- 在整个内容的实现中,主要以串联核心流程为主,剔除掉一些分支过程。这主要是因为很多分支过程都是在处理一些各类场景的情况。
- 而我们学习源码是需要掌握主脉络且不被太多分支流程干扰的,才能把主干流程理顺。
- 本章处理注解时,只是添加了
@Insert、@Delete、@Update、@Select
四个注解。