配置构造方法的constructor元素
简单了解constructor元素
mybatis
为我们提供了一个constructor
元素来配置PO
对象的构造方法,通常来说,mybatis
会通过无参构造方法实例化PO
对象,但是在某些特殊的场景下,基于特定的原因,PO
对象可能没有提供无参构造,或者必须通过特定的构造方法才能被实例化,这时候,我们就用到了constructor
元素.
关于constructor
元素,mybatis
官方文档是这样介绍的:
构造方法注入允许你在初始化时为类设置属性的值,而不用暴露出公有方法。
MyBatis
也支持私有属性和私有JavaBean
属性来完成注入,但有一些人更青睐于通过构造方法进行注入。
constructor
元素就是为此而生的。
constructor
元素的定义并不复杂,按照resultMap
元素的定义来看,一个resultMap
最多只能配置一个constructor
元素.
constructor
元素没有属性定义,只有两个子元素定义:
而且这两个子元素的属性定义还是完全一样的:
不仅如此,就连idArg
和arg
这两个元素的作用都十分相似,他们都用来配置构造方法的构造参数,唯一不同的是用idArg
子元素配置的构造参数会被标记为当前对象的唯一标识符.
如何根据constructor元素来获取对应构造方法
因为java
类定义中的方法是允许重载的,所以一个类定义中有可能会出现多个同名不同参的构造方法,比如:
@Data
@NoArgsConstructor
public class User {
private Integer id;
private String name;
public User(Integer id) {
this.id = id;
}
public User(String name) {
this.name = name;
}
public User(Integer id, String name) {
this.id = id;
this.name = name;
}
}
在上面这个示例中,一个小小的User
类定义,就提供了三个不同的构造方法.
那么,当我们配置了constructor
元素时,mybatis
该如何选择对应的构造方法呢?
按照mybatis
官方文档的描述,最初mybatis
只能根据用户声明idArg/arg
子元素的顺序以及该元素所对应的java
类型来获取对应的构造方法.
后来,因为JDK1.8+
的版本可以获取方法定义中的形参名称了,所以从版本3.4.3
开始,mybatis
开始支持根据参数名称匹配所对应的构造方法.
- 可以参考Resolve POJO constructor arguments by name rather than position
- 或者直接访问https://github.com/mybatis/mybatis-3/issues/721
这是官方介绍:
当你在处理一个带有多个形参的构造方法时,很容易搞乱 arg 元素的顺序。 从版本 3.4.3 开始,可以在指定参数名称的前提下,以任意顺序编写 arg 元素。 为了通过名称来引用构造方法参数,你可以添加 @Param 注解,或者使用 '-parameters' 编译选项并启用 useActualParamName 选项(默认开启)来编译项目。
抱着好奇的心态,我去对比了一下3.4.2
和3.4.3
两个版本中idArg/arg
元素的DTD
定义:
果然不出我所料,name
属性是在3.4.3
版本中新增的.
useActualParamName
全局配置和-parameters
编译选项
-parameters
编译选项
说到-parameters
编译选项,就不得不提一下JDK
增强提案:JEP 118: Access to Parameter Names at Runtime
访问地址:http://openjdk.java.net/jeps/118
118
提案提出:
提供一种机制,可在运行时通过核心反射轻松可靠地检索方法和构造函数的参数名称。
在java
中,java.lang.reflect.Parameter
对象用于描述方法参数,它的getName()
方法用于获取方法名称.
本篇文章不会对
java
源码做深入的探究,如果想了解更多关于形参名称的生成方案,可以参考java.lang.reflect.Executable
的getParameters()
方法.
getParameters()
方法的实现其实很简单,这里不做探究的原因是因为想控制学习的深度.
针对同样的测试代码:
public interface Example {
void simpleMethod(String id, String name);
}
@Slf4j
public class ParametersCompileParameterTest {
@Test
@SneakyThrows
public void simpleMethodTest() {
Method method = Example.class.getMethod("simpleMethod", String.class, String.class);
for (Parameter parameter : method.getParameters()) {
log.debug(parameter.getName());
}
}
}
如果我们在编译文件时启用了-parameters
编译选项,simpleMethod()
方法的两个形参名称就会被编译进Example.class
文件中:
这样在为simpleMethod()
方法的两个形参创建对应的Parameter
对象时,就会使用的真实的形参名称.
这时候,我们再调用Parameter
对象的getName()
方法就会获得真实的形参名称.
运行结果:
DEBUG [main] - paramName
DEBUG [main] - otherName
但是如果我们在编译文件时未启用-parameters
编译选项,在Example.class
文件中就不会包含simpleMethod()
方法的两个形参名称的描述信息:
因此,我们得到的形参所对应的Parameter
对象的name
值是一个合成的argN
,其中N
表示形参在方法参数列表中的索引位置.
这时候,再调用Parameter
对象的getName()
方法,就无法获取真实的形参名称,只能获取类似于argN
的取值.
运行结果:
DEBUG [main] - arg0
DEBUG [main] - arg1
IDEA
配置javac -parameters
方法
允许使用方法形参作为参数名称的全局配置useActualParamName
在前面的文章中,我们了解到mybatis
有一个默认值为true
的全局配置useActualParamName
,这个配置项的作用是在运行时,允许使用方法形参作为参数名称.
比如,在没有启用useActualParamName
的前提下,针对下面的语句配置:
如果我们直接定义方法selectForExample()
,不做任何特殊处理:
User selectNormalUser(Integer id, String name);
在运行时,将会得到报错信息:
Caused by: org.apache.ibatis.binding.BindingException: Parameter 'id' not found. Available parameters are [0, 1, param1, param2]
这是因为mybatis
无法识别id
和name
这两个参数名称,我们只能通过参数定义的下标来获取对参数对象的访问.
针对这种场景,我们可以选择使用@Param
注解为参数指定名称:
User selectNormalUser(@Param("id") Integer id, @Param("name")String name);
也可以选择启用useActualParamName
配置,让mybatis
自己使用形参名称作为参数名称,但是如果我们只是单纯的配置了useActualParamName
参数的值为true
,针对方法定义:
User selectNormalUser(Integer id, String name);
我们会得到另一个报错:
Caused by: org.apache.ibatis.binding.BindingException: Parameter 'id' not found. Available parameters are [arg1, arg0, param1, param2]
解决这个报错的方案就是在编译文件时启用-parameters
编译选项.
下面是不同场景下,mybatis
生成的有效参数名称数组:
场景 | 参数名称1 | 参数名称2 | 参数名称3 | 参数名称4 |
---|---|---|---|---|
禁用useActualParamName |
0 | 1 | param1 | param2 |
使用@Param 注解 |
id | name | param1 | param2 |
启用useActualParamName ,禁用-parameters 编译选项 |
arg0 | arg1 | param1 | param2 |
启用useActualParamName ,启用-parameters 编译选项 |
id | name | param1 | param2 |
mybatis
生成有效参数名称数组的逻辑在org.apache.ibatis.reflection.ParamNameResolver
中被实现,在后面的文章中会学到该类.
了解idArg/arg
元素的属性定义
idArg/arg
元素的column
,select
,resultMap
,columnPrefix
,javaType
,jdbcType
,typeHandler
这些属性和前面学习到的基本一致,这里就不做赘述了.
唯一有些陌生的是idArg/arg
元素新增的用来配置构造方法形参名字的name
属性.
实践一下
根据用户声明idArg/arg
子元素的顺序以及该元素所对应的java
类型来获取对应的构造方法
创建一个简单的示例代码:
@Data
@NoArgsConstructor
public class User {
private Integer id;
private String name;
public User(Integer id, String name) {
this.id = id;
this.name = name;
}
}
在上面的代码中,根据constructor
元素的配置,它所对应的构造方法的参数列表类型依次为Integer
和String
,刚好匹配User
提供的构造方法,所以下面的单元测试是能够正常运行的:
@Test
public void selectConstructorUser() {
sqlSessionFactory.getConfiguration().addMapper(ConstructorMapper.class);
@Cleanup
SqlSession sqlSession = sqlSessionFactory.openSession();
ConstructorMapper constructorMapper = sqlSession.getMapper(ConstructorMapper.class);
User user = constructorMapper.selectConstructorUser(1, "Panda");
assert user != null;
}
但是如果我们将两个arg
元素的位置互换:
我们将得到一条报错信息:
java.lang.NoSuchMethodException: org.apache.learning.result_map.constructor.User.(java.lang.String, java.lang.Integer)
这是因为根据constructor
元素的配置,它所对应的构造方法的参数列表类型依次为String
和Integer
,但是User
中并没有提供对应的构造方法.
虽然myabtis
没有要求必填javaType
属性,但是在没有指定构造参数名称时,最好是传入javaType
属性,除非你的构造参数类型是Object
,否则你会得到一个类似于下面的异常信息:
Caused by: java.lang.NoSuchMethodException: org.apache.learning.result_map.constructor.User.(java.lang.Object, java.lang.Object)
跟据参数名称匹配所对应的构造方法
我们继续看一下如何跟据参数名称匹配对应的构造方法.
在上面的代码中,我们通过name
属性指定了arg
元素对应的构造参数名称,同时arg
元素的配置顺序和实际声明的构造方法的参数顺序是相反的.
@Test
public void selectReversalConstructorUserWillSuccess() {
sqlSessionFactory.getConfiguration().addMapper(ConstructorMapper.class);
@Cleanup
SqlSession sqlSession = sqlSessionFactory.openSession();
ConstructorMapper constructorMapper = sqlSession.getMapper(ConstructorMapper.class);
User user = constructorMapper.selectNamedConstructorUser(1, "Panda");
assert user != null;
}
上面的单元测试能够正确运行,表示mybatis
是根据参数名称来匹配对应的构造方法的,那既然如此,如果我们有两个具有相同构造参数名称,但是不一样的构造方法会怎样呢?
说干就干,我们编写代码来测试一下这个场景:
@Data
public class Resource {
private Integer id;
private Integer pid;
private String name;
public Resource(Integer id, Integer pid, String name) {
this.id = id;
this.pid = pid;
this.name = name;
}
public Resource(Integer id, String name, Integer pid) {
this.id = id;
this.pid = pid;
this.name = name;
}
}
仔细看,Resource
拥有两个参数完全一致,但是参数位置不同的构造方法,然后我们再声明一个和上述两个构造方法参数位置都不一样的constructor
配置:
提供一个单元测试:
@Test
public void selectNamedConstructorResource() {
sqlSessionFactory.getConfiguration().addMapper(ConstructorMapper.class);
@Cleanup
SqlSession sqlSession = sqlSessionFactory.openSession();
ConstructorMapper constructorMapper = sqlSession.getMapper(ConstructorMapper.class);
Resource resource = constructorMapper.selectNamedConstructorResource(1);
assert resource != null;
}
单元测试成功运行,这是为什么呢?
这是因为,在根据参数名称来匹配对应的构造方法时,如果有多个匹配的构造方法,那么第一个被匹配的构造方法生效.
结束
上面的内容就是constructor
元素的相关知识了,涉及到的相关代码可以在gitee
仓库中找到:https://gitee.com/topanda/mybatis-3/tree/master/src/test/java/org/apache/learning/result_map/constructor
砖厂繁忙,告辞!