本文源代码来源于mybatis-spring-boot-starter的2.1.2版本
1.XML还是Annotation
前面我们介绍了MappedStatement
的创建流程,在SqlSessionFacotry
bulid的时候,SqlSessionFacotryBean
会加载配置文件中mapper-locations
,对该路径下的 *mapper.xml
文件进行解析,并最终生成MappedStatement
放在Configuration
中供后面执行sql方法时使用。
不过不少同学可能会觉得创建xml文件写sql过于繁琐,更偏爱使用@select
等注解,直接把sql写在Mapper接口里,岂不美哉?虽然笔者更偏向xml,易于维护。仁者见仁,我们还是来探究下通过Annotation是怎么玩的?
2.Annotation的使用
设计初期的 MyBatis 是一个 XML 驱动的框架。配置信息是基于 XML 的,映射语句也是定义在 XML 中的。而在 MyBatis 3 中,我们提供了其它的配置方式。MyBatis 3 构建在全面且强大的基于 Java 语言的配置 API 之上。它是 XML 和注解配置的基础。注解提供了一种简单且低成本的方式来实现简单的映射语句。
提示
不幸的是,Java 注解的表达能力和灵活性十分有限。尽管我们花了很多时间在调查、设计和试验上,但最强大的 MyBatis 映射并不能用注解来构建——我们真没开玩笑。而 C# 属性就没有这些限制,因此 MyBatis.NET 的配置会比 XML 有更大的选择余地。虽说如此,基于 Java 注解的配置还是有它的好处的。
public interface RoleMapper {
@Select("select * from role")
@Results({
@Result(property = "roleId",column = "role_id"),
@Result(property = "roleName",column = "role_name")
})
List<Role> selectALl();
@Select("select * from role where id = #{id}")
@Results({
@Result(property = "roleId",column = "role_id"),
@Result(property = "roleName",column = "role_name")
})
Role selectById (Long id);
}
像这样通过@Select
等注解标记SQL,@Result
对应<result>
更多用法参考这里 本文不做过多讲解。
3.Annotation的扫描
前面我们聊bindMapperForNamespace()
的时候会调用MapperRegistry.addMapper()
,addmapper()
主要做了两件事情:
- 构建MapperProxyFactory和Mapper接口映射关系的map
- 处理Mapper接口的注解
public <T> void addMapper(Class<T> type) {
if (type.isInterface()) {
if (hasMapper(type)) {
throw new BindingException("Type " + type + " is already known to the MapperRegistry.");
}
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.
MapperAnnotationBuilder parser = new MapperAnnotationBuilder(config, type);
parser.parse();
loadCompleted = true;
} finally {
if (!loadCompleted) {
knownMappers.remove(type);
}
}
}
}
3.1 parse
还是来看parse
方法:
public void parse() {
String resource = type.toString();
if (!configuration.isResourceLoaded(resource)) {
loadXmlResource();
configuration.addLoadedResource(resource);
assistant.setCurrentNamespace(type.getName());
parseCache();
parseCacheRef();
//获得mapper接口的所有方法不y
Method[] methods = type.getMethods();
for (Method method : methods) {
try {
// issue #237
if (!method.isBridge()) {
parseStatement(method);
}
} catch (IncompleteElementException e) {
configuration.addIncompleteMethod(new MethodResolver(this, method));
}
}
}
parsePendingMethods();
}
3.2 parseStatement
继续来看parseStatement()
void parseStatement(Method method) {
Class<?> parameterTypeClass = getParameterType(method);
LanguageDriver languageDriver = getLanguageDriver(method);
//判断方法上是否包含那几种注解
//如果有,就根据注解的内容创建SqlSource对象。创建过程与XML创建过程一样
SqlSource sqlSource = getSqlSourceFromAnnotations(method, parameterTypeClass, languageDriver);
...//省略
String resultMapId = null;
ResultMap resultMapAnnotation = method.getAnnotation(ResultMap.class);
if (resultMapAnnotation != null) {
resultMapId = String.join(",", resultMapAnnotation.value());
} else if (isSelect) {
resultMapId = parseResultMap(method);
}
assistant.addMappedStatement(
mappedStatementId,
sqlSource,
statementType,
sqlCommandType,
fetchSize,
timeout,
// ParameterMapID
null,
parameterTypeClass,
resultMapId,
getReturnType(method),
resultSetType,
flushCache,
useCache,
// TODO gcode issue #577
false,
keyGenerator,
keyProperty,
keyColumn,
// DatabaseID
null,
languageDriver,
// ResultSets
options != null ? nullOrEmpty(options.resultSets()) : null);
}
}
实际上parseStatement()
显示创建sqlSource
然后通过addMappedStatement
创建MappedStatement
,这里和xml的构造大同小异了。之前我们讲xml的时候也提到了了关注下sqlSource
,因为实际上它就是要执行的sql。所以对于注解解析的玄关一定存在getSqlSourceFromAnnotations
中。
4.构建SqlSource
4.1 getSqlSourceFromAnnotations
来看下getSqlSourceFromAnnotations
:
private SqlSource getSqlSourceFromAnnotations(Method method, Class<?> parameterType, LanguageDriver languageDriver) {
try {
//注解就分为两大类,sqlAnnotation和sqlProviderAnnotation
//循环注解列表,判断Method包含哪一种,就返回哪种类型注解的实例
Class<? extends Annotation> sqlAnnotationType = getSqlAnnotationType(method);
Class<? extends Annotation> sqlProviderAnnotationType = getSqlProviderAnnotationType(method);
if (sqlAnnotationType != null) {
//他们是互相排斥的
if (sqlProviderAnnotationType != null) {
throw new BindingException("You cannot supply both a static SQL and SqlProvider to method named " + method.getName());
}
Annotation sqlAnnotation = method.getAnnotation(sqlAnnotationType);
//获取注解中的sql语句主体
final String[] strings = (String[]) sqlAnnotation.getClass().getMethod("value").invoke(sqlAnnotation);
return buildSqlSourceFromStrings(strings, parameterType, languageDriver);
} else if (sqlProviderAnnotationType != null) {
Annotation sqlProviderAnnotation = method.getAnnotation(sqlProviderAnnotationType);
return new ProviderSqlSource(assistant.getConfiguration(), sqlProviderAnnotation, type, method);
}
return null;
} catch (Exception e) {
throw new BuilderException("Could not find value method on SQL annotation. Cause: " + e, e);
}
}
getSqlSourceFromAnnotations
一上来就分别用 getSqlAnnotationType
和getSqlProviderAnnotationType
分别去扫描了方法上的注解,很明显他们是两种类型不同的注解,所以处理的方式不一样。
4.2 sqlAnnotation与sqlProviderAnnotation
我们在MapperAnnotationBuilder这个类下面可以看到:
public class MapperAnnotationBuilder {
private static final Set<Class<? extends Annotation>> SQL_ANNOTATION_TYPES = new HashSet<>();
private static final Set<Class<? extends Annotation>> SQL_PROVIDER_ANNOTATION_TYPES = new HashSet<>();
//...
static {
SQL_ANNOTATION_TYPES.add(Select.class);
SQL_ANNOTATION_TYPES.add(Insert.class);
SQL_ANNOTATION_TYPES.add(Update.class);
SQL_ANNOTATION_TYPES.add(Delete.class);
SQL_PROVIDER_ANNOTATION_TYPES.add(SelectProvider.class);
SQL_PROVIDER_ANNOTATION_TYPES.add(InsertProvider.class);
SQL_PROVIDER_ANNOTATION_TYPES.add(UpdateProvider.class);
SQL_PROVIDER_ANNOTATION_TYPES.add(DeleteProvider.class);
}
@Select、@Insert、@Update、@Delete
属于sqlAnnotation@SelectProvider、@InsertProvider、@UpdateProvider、@DeleteProvider
属于sqlProviderAnnotation
事实上从名字上我们也很好去分辨。
4.3 创建SqlSource对象
- sqlAnnotation
buildSqlSourceFromStrings会拼接注解里面的value
,然后调用createSqlSource
进行对parameter和value进行进一步拼接。这里和xml解析一样。
@Override
public SqlSource createSqlSource(Configuration configuration, String script, Class<?> parameterType) {
// issue #3
if (script.startsWith("<script>")) {
XPathParser parser = new XPathParser(script, false, configuration.getVariables(), new XMLMapperEntityResolver());
return createSqlSource(configuration, parser.evalNode("/script"), parameterType);
} else {
// issue #127
script = PropertyParser.parse(script, configuration.getVariables());
TextSqlNode textSqlNode = new TextSqlNode(script);
if (textSqlNode.isDynamic()) {
return new DynamicSqlSource(configuration, textSqlNode);
} else {
return new RawSqlSource(configuration, script, parameterType);
}
}
}
- sqlProviderAnnotatio
调用到ProviderSqlSource类的构造器,过程比较简单,就是拿到SqlProvider类上的方法,将方法名、方法参数和参数类型设置一下。
public ProviderSqlSource(Configuration configuration, Object provider, Class<?> mapperType, Method mapperMethod) {
String providerMethodName;
try {
// 初始化Mybatis全局配置
this.configuration = configuration;
// SqlSource对象的构建器
this.sqlSourceParser = new SqlSourceBuilder(configuration);
// 通过注解获取提供Sql内容的对象类型
this.providerType = (Class<?>) provider.getClass().getMethod("type").invoke(provider);
// 通过注解获取提供Sql内容的方法名称
providerMethodName = (String) provider.getClass().getMethod("method").invoke(provider);
for (Method m : this.providerType.getMethods()) {
if (providerMethodName.equals(m.getName()) && CharSequence.class.isAssignableFrom(m.getReturnType())) {
// 方法名称匹配,同时返回内容是可读序列的子类,其实简单来讲就是看看方法的返回对象是不是能转成字符串
if (providerMethod != null) {
throw new BuilderException("Error creating SqlSource for SqlProvider. Method '"
+ providerMethodName + "' is found multiple in SqlProvider '" + this.providerType.getName()
+ "'. Sql provider method can not overload.");
}
// 配置提供Sql的方法
this.providerMethod = m;
// 配置提供Sql方法的入参名称集合
this.providerMethodArgumentNames = new ParamNameResolver(configuration, m).getNames();
// 配置提供Sql方法的入参类型集合
this.providerMethodParameterTypes = m.getParameterTypes();
}
}
} catch (BuilderException e) {
throw e;
} catch (Exception e) {
throw new BuilderException("Error creating SqlSource for SqlProvider. Cause: " + e, e);
}
if (this.providerMethod == null) {
throw new BuilderException("Error creating SqlSource for SqlProvider. Method '"
+ providerMethodName + "' not found in SqlProvider '" + this.providerType.getName() + "'.");
}
// 解析参数类型
for (int i = 0; i < this.providerMethodParameterTypes.length; i++) {
// 获取方法入参类型
Class<?> parameterType = this.providerMethodParameterTypes[i];
if (parameterType == ProviderContext.class) {
// 查找ProviderContext类型的参数
if (this.providerContext != null) {
throw new BuilderException("Error creating SqlSource for SqlProvider. ProviderContext found multiple in SqlProvider method ("
+ this.providerType.getName() + "." + providerMethod.getName()
+ "). ProviderContext can not define multiple in SqlProvider method argument.");
}
// 构建用于Provider方法的上下文对象
this.providerContext = new ProviderContext(mapperType, mapperMethod);
// 配置使用Provider方法对应的上下文对象对应的参数位置索引
this.providerContextIndex = i;
}
}
}
代码虽然比较长,但是没什么难点,小伙伴可以自行debug一下。
总结
- Annotation的扫描发生在
bindMapperForNamespace()
的时候调用MapperRegistry.addMapper()
MapperAnnotationBuilder.parse()
会对注解解析sqlAnnotation
(@Selcet等)和sqlProviderAnnotation
(@SelectProvider等)是两类不同的注解他们是分开处理的- 根据解析结果创建
SqlSource
对象 - 调用
addMappedStatement()
完成注册