Mybatis 学习笔记1:构造方法注入

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。

Mybatis 学习笔记1:构造方法注入_第1张图片
调试发现上图 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 文件如下:


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>
            
            
            <idArg column="emp_id" name="id"/>
            <arg column="emp_name" name="name"/>
            <arg name="company" resultMap="comResultMap"/>
        constructor>
    resultMap>

    <resultMap id="comResultMap" type="MyCompany">
        
        <id column="com_id" property="id"/>
        <result column="com_name" property="name"/>
    resultMap>
mapper>

阅读 Mybatis 官方文档,在构造方法一节,内容如下:

构造方法注入允许你在初始化时为类设置属性的值,而不用暴露出公有方法。MyBatis 也支持私有属性和私有 JavaBean 属性来完成注入,但有一些人更青睐于通过构造方法进行注入。constructor 元素就是为此而生的。

当你在处理一个带有多个形参的构造方法时,很容易搞乱 arg 元素的顺序。版本 3.4.3 开始,可以在指定参数名称的前提下,以任意顺序编写 arg 元素。
为了通过名称来引用构造方法参数,你可以:

  1. 添加 @Param 注解
  2. 使用 ‘-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;
}

Mybatis 学习笔记1:构造方法注入_第2张图片
上图 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."":()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        // "":()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."":()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 文件,

  1. 常量池多出了一项 CONSTANT_Utf8_info #17:
    #17 = Utf8               MethodParameters
    
  2. 构造方法多出了一个属性 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 表示编译的上下文环境

未完待续…

你可能感兴趣的:(mybatis,mybatis,java,javac,javap,源码)