Mybatis 构造方法注入
最近阅读 Mybatis 3.5.8-SNAPSHOT 版本源码,在调试过程中遇到如下异常:
org.apache.ibatis.builder.BuilderException:
Error in result map 'MyEmployee.empResultMap'.
Failed to find a constructor in 'MyEmployee' by arg names [id, name, company].
堆栈信息如下:
Caused by: org.apache.ibatis.builder.BuilderException: Error in result map 'MyEmployee.empResultMap'. Failed to find a constructor in 'MyEmployee' by arg names [id, name, company]. There might be more info in debug log.
at org.apache.ibatis.mapping.ResultMap$Builder.build(ResultMap.java:134)
at org.apache.ibatis.builder.MapperBuilderAssistant.addResultMap(MapperBuilderAssistant.java:208)
at org.apache.ibatis.builder.ResultMapResolver.resolve(ResultMapResolver.java:47)
at org.apache.ibatis.builder.xml.XMLMapperBuilder.resultMapElement(XMLMapperBuilder.java:348)
at org.apache.ibatis.builder.xml.XMLMapperBuilder.resultMapElement(XMLMapperBuilder.java:262)
at org.apache.ibatis.builder.xml.XMLMapperBuilder.resultMapElements(XMLMapperBuilder.java:254)
at org.apache.ibatis.builder.xml.XMLMapperBuilder.configurationElement(XMLMapperBuilder.java:127)
... 2 more
分析异常原因,在 ResultMap.Builder 构建 ResultMap 对象时,会调用 argNamesOfMatchingConstructor() 方法检验实体类中是否存在 constructorResultMappings 集合对应的构造方法,即检验实体类构造参数的个数、参数名、参数类型是否和 constructorResultMappings 集合的长度和集合中 resultMapping 元素的 property 属性和 javaType 属性(即 mapper.xml 文件中 constructor 元素的子元素个数、子元素的 name 属性、javaType 属性)一致,如果不一致就会抛出 BuilderException。
调试发现上图 157 行代码根据实体类构造方法获得的参数名集合为 [arg0, arg1, arg2],和 constructorResultMappings 参数提供的参数名集合 [id, name, company] 不一致,因此抛出异常。
MyEmployee、MyCompany 实体类如下:
注意:MyEmployee 构造方法并没有使用 @Param 注解。
public class MyEmployee {
private Integer id;
private String name;
private MyCompany company;
// 构造方法参数并没有使用@Param注解
public MyEmployee(Integer id, String name, MyCompany company) {
this.id = id;
this.name = name;
this.company = company;
}
}
public class MyCompany {
private Integer id;
private String name;
}
MyEmployee.xml 文件如下:
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper
PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
"http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="MyEmployee">
<resultMap id="empResultMap" type="MyEmployee">
<constructor>
<!--如果是构造属性,根据 type 和 name 属性获取 javaType-->
<!--如果没有配置 typeHandler 属性,在解析出 javaType 之后,
org.apache.ibatis.mapping.ResultMapping.Builder.build() 方法
会调用 resolveTypeHandler() 方法,
resolveTypeHandler() 方法根据 javaType 和 jdbcType
从 TypeHandlerRegistry 中查找对应的 TypeHandler-->
<idArg column="emp_id" name="id"/>
<arg column="emp_name" name="name"/>
<arg name="company" resultMap="comResultMap"/>
</constructor>
</resultMap>
<resultMap id="comResultMap" type="MyCompany">
<!--如果不是构造属性,根据 type 和 property 属性获取 javaType-->
<id column="com_id" property="id"/>
<result column="com_name" property="name"/>
</resultMap>
</mapper>
阅读 Mybatis 官方文档,在构造方法一节,内容如下:
构造方法注入允许你在初始化时为类设置属性的值,而不用暴露出公有方法。MyBatis 也支持私有属性和私有 JavaBean 属性来完成注入,但有一些人更青睐于通过构造方法进行注入。constructor 元素就是为此而生的。
当你在处理一个带有多个形参的构造方法时,很容易搞乱 arg 元素的顺序。从版本 3.4.3 开始,可以在指定参数名称的前提下,以任意顺序编写 arg 元素。
为了通过名称来引用构造方法参数,你可以:
- 添加 @Param 注解
- 使用 ‘-parameters’ 编译选项并启用 useActualParamName 选项(默认开启)来编译项目。
private List<String> getArgNames(Constructor<?> constructor) {
// ...
for (int paramIndex = 0; paramIndex < paramCount; paramIndex++) {
String name = null;
for (Annotation annotation : paramAnnotations[paramIndex]) {
if (annotation instanceof Param) {
// 获取构造方法参数的@Param注解的value值
name = ((Param) annotation).value();
break;
}
}
if (name == null && resultMap.configuration.isUseActualParamName()) {
if (actualParamNames == null) {
// 调用java.lang.reflect.Executable#getParameters()方法
actualParamNames = ParamNameUtil.getParamNames(constructor);
}
if (actualParamNames.size() > paramIndex) {
name = actualParamNames.get(paramIndex);
}
}
paramNames.add(name != null ? name : "arg" + paramIndex);
}
return paramNames;
}
在构造方法参数没有使用 @Param 注解的情况下,ResultMap.Builder#getArgNames(Constructor) 方法会调用 java.lang.reflect.Executable#getParameters() 方法,代码如下:
public Parameter[] getParameters() {
// Need to copy the cached array to prevent users from messing
// with it. Since parameters are immutable, we can
// shallow-copy.
return privateGetParameters().clone();
}
private Parameter[] privateGetParameters() {
// Use tmp to avoid multiple writes to a volatile.
Parameter[] tmp = parameters;
if (tmp == null) {
// Otherwise, go to the JVM to get them
try {
tmp = getParameters0();
} catch(IllegalArgumentException e) {
}
// If we get back nothing, then synthesize parameters
if (tmp == null) {
tmp = synthesizeAllParams();
}
}
return tmp;
}
private Parameter[] synthesizeAllParams() {
final int realparams = getParameterCount();
final Parameter[] out = new Parameter[realparams];
for (int i = 0; i < realparams; i++)
out[i] = new Parameter("arg" + i, 0, this, i);
return out;
}
上图 getParameters0() 方法并没有从 JVM 中获取到构造方法的参数列表,于是调用 synthesizeAllParams() 方法生成参数名为 [arg0, arg1, arg2] 的参数列表,也就导致和集合 [id, name, company] 不一致,因此抛出异常。
正如官方文档所说,需要使用 “-parameters” 选项来编译项目。
javac -parameters MyEmployee.java MyCompany.java
javap -v MyEmployee.class
使用 “-parameters” 选项编译生成的 class 文件如下:
public class MyEmployee
minor version: 0
major version: 54
flags: (0x0021) ACC_PUBLIC, ACC_SUPER
this_class: #5 // MyEmployee
super_class: #6 // java/lang/Object
interfaces: 0, fields: 3, methods: 1, attributes: 1
Constant pool:
#1 = Methodref #6.#20 // java/lang/Object."<init>":()V
#2 = Fieldref #5.#21 // MyEmployee.id:Ljava/lang/Integer;
#3 = Fieldref #5.#22 // MyEmployee.name:Ljava/lang/String;
#4 = Fieldref #5.#23 // MyEmployee.company:LMyCompany;
#5 = Class #24 // MyEmployee
#6 = Class #25 // java/lang/Object
#7 = Utf8 id
#8 = Utf8 Ljava/lang/Integer;
#9 = Utf8 name
#10 = Utf8 Ljava/lang/String;
#11 = Utf8 company
#12 = Utf8 LMyCompany;
#13 = Utf8 <init>
#14 = Utf8 (Ljava/lang/Integer;Ljava/lang/String;LMyCompany;)V
#15 = Utf8 Code
#16 = Utf8 LineNumberTable
#17 = Utf8 MethodParameters
#18 = Utf8 SourceFile
#19 = Utf8 MyEmployee.java
#20 = NameAndType #13:#26 // "<init>":()V
#21 = NameAndType #7:#8 // id:Ljava/lang/Integer;
#22 = NameAndType #9:#10 // name:Ljava/lang/String;
#23 = NameAndType #11:#12 // company:LMyCompany;
#24 = Utf8 MyEmployee
#25 = Utf8 java/lang/Object
#26 = Utf8 ()V
{
public MyEmployee(java.lang.Integer, java.lang.String, MyCompany);
descriptor: (Ljava/lang/Integer;Ljava/lang/String;LMyCompany;)V
flags: (0x0001) ACC_PUBLIC
Code:
stack=2, locals=4, args_size=4
0: aload_0
1: invokespecial #1 // Method java/lang/Object."<init>":()V
4: aload_0
5: aload_1
6: putfield #2 // Field id:Ljava/lang/Integer;
9: aload_0
10: aload_2
11: putfield #3 // Field name:Ljava/lang/String;
14: aload_0
15: aload_3
16: putfield #4 // Field company:LMyCompany;
19: return
LineNumberTable:
line 24: 0
line 25: 4
line 26: 9
line 27: 14
line 28: 19
MethodParameters:
Name Flags
id
name
company
}
SourceFile: "MyEmployee.java"
相比于不使用 “-parameters” 选项编译生成的 class 文件,
- 常量池多出了一项 CONSTANT_Utf8_info #17:
#17 = Utf8 MethodParameters
- 构造方法多出了一个属性 MethodParameters:
MethodParameters: Name Flags id name company
com.sun.tools.javac.jvm.ClassWriter#writeMethod(MethodSymbol) 方法代码如下:
void writeMethod(MethodSymbol m) {
// ...
// 如果设置了“-parameters”选项,调用writeMethodParametersAttr()方法
if (options.isSet(PARAMETERS)) {
if (!m.isLambdaMethod()) // Per JDK-8138729, do not emit parameters table for lambda bodies.
acount += writeMethodParametersAttr(m);
}
// ...
}
int writeMethodParametersAttr(MethodSymbol m) {
// ...
if (m.params != null && allparams != 0) {
// 将“MethodParameters”字符串写入常量池并返回常量池索引
final int attrIndex = writeAttr(names.MethodParameters);
databuf.appendByte(allparams);
// ...
// Now write the real parameters
for (VarSymbol s : m.params) {
final int flags =
((int) s.flags() & (FINAL | SYNTHETIC | MANDATED)) |
((int) m.flags() & SYNTHETIC);
// 将方法参数的名称写入常量池,并将常量池索引写入class文件字节流
databuf.appendChar(pool.put(s.name));
// 将方法参数的flag写入class文件字节流
databuf.appendChar(flags);
}
// ...
endAttr(attrIndex);
return 1;
} else
return 0;
}
Java 前端编译
com.sun.tools.javac.main.Arguments
表示 Java 前端编译(javac 命令行和 Compiler API)的选项和参数
。
public class Arguments {
private Set<String> classNames;// classNames集合
private Set<Path> files;// java源文件路径集合
private final Options options;// javac选项集合
}
枚举类 com.sun.tools.javac.main.Option
定义了编译的选项
。
// 枚举值 PARAMETERS
PARAMETERS("-parameters","opt.parameters", STANDARD, BASIC)
// 构造方法
Option(String text, String descrKey, OptionKind kind, OptionGroup group) {
this(text, null, descrKey, kind, group, null, null, ArgKind.NONE);
}
// 构造方法
private Option(String text,// 选项名称
String argsNameKey,
String descrKey,
// 0.OptionKind.STANDARD
// 表示该选项是标准选项,由“javac -help”注释
// 1.OptionKind.EXTENDED
// 表示该选项是扩展选项,由“javac -X”注释
// 2.OptionKind.HIDDEN
// 表示该选项是隐藏选项,没有注释
OptionKind kind,
// 0.OptionGroup.BASIC
// 表示该选项是基本选项,javac命令行和Compiler API均支持
// 1.OptionGroup.FILEMANAGER
// 表示该选项由JavaFileManager支持,其他FileManager可能不支持
// 2.OptionGroup.INFO
// 表示该选项用于请求信息,例如“-help”、“-version”
// 3.OptionGroup.OPERAND
// 表示该选项用于指定java源文件路径或className
OptionGroup group,
// 0.ChoiceKind.ONEOF
// 表示该选项的值是choices中的某一个
// 1.ChoiceKind.ANYOF
// 表示该选项的值是choices中的一个或多个
ChoiceKind choiceKind,
Set<String> choices,
// 0.ArgKind.NONE
// 表示该选项没有值
// 1.ArgKind.REQUIRED
// 表示该选项和值之间使用“:”或“=”连接
// 2.ArgKind.ADJACENT
// 表示该选项和值相邻(使用空格连接)
ArgKind argKind) {
this.names = text.trim().split("\\s+");
this.primaryName = names[0];
this.argsNameKey = argsNameKey;
this.descrKey = descrKey;
this.kind = kind;
this.group = group;
this.choiceKind = choiceKind;
this.choices = choices;
this.argKind = argKind;
}
com.sun.tools.javac.util.Context
表示编译的上下文环境
。
未完待续…