0%

聊聊 java8 Parameter 反射类

零、前言

java8 推出来的时候,新上了一个反射类Parameter,可以通过这个类拿到方法的具体参数名称。但是,这个功能默认是关闭的,在使用javac 编译的时候,使用-parameters参数,才能在getName()的时候返回具体参数的名称,而不是类似arg0…之类的。很多java框架都使用了该类的新特性,让组件使用起来更便利。但是,如果我们不了解这些,没有配置对应开关参数,或者不小心关闭了开关,可能会影响到框架正常使用,从而导致奇奇怪怪的未遇到过的问题。

这篇文章,主要是在一次重构过程中,对于原始代码的调整,导致mybatis框架使用时候的异常case分析,而产出的。

一、背景

最近在历史系统代码上做重构的时候,单元测试部分,使用junit5和jacoco来做单测覆盖。由于历史配置的问题,导致junit5的单测用例,在maven test 后都没有执行,后来发现是jacoco-maven-plugin插件配置问题,然后stackoverflow copy 了一份,单测运行ok了。
如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
<plugin>  
<artifactId>maven-compiler-plugin</artifactId>
<version>3.1</version>
<configuration>
<source>${java.version}</source>
<target>${java.version}</target>
</configuration>
</plugin>

<plugin>
<groupId>org.jacoco</groupId>
<artifactId>jacoco-maven-plugin</artifactId>
<version>${jacoco.version}</version>
<executions>
<execution>
<id>prepare-agent</id>
<goals>
<goal>prepare-agent</goal>
</goals>
</execution>
<execution>
<id>report</id>
<phase>prepare-package</phase>
<goals>
<goal>report</goal>
</goals>
</execution>
<execution>
<id>post-unit-test</id>
<phase>test</phase>
<goals>
<goal>report</goal>
</goals>
<configuration>
<!-- Sets the path to the file which contains the execution data. -->
<dataFile>target/jacoco.exec</dataFile>
<!-- Sets the output directory for the code coverage report. -->
<outputDirectory>target/jacoco-ut</outputDirectory>
</configuration>
</execution>
</executions>
</plugin>

然后部署,测试功能,发现之前基于mybatisDao interface 执行报错。如下:

1
2
3
4
5
6
7
8
9
Caused by: org.mybatis.spring.MyBatisSystemException: nested exception is org.apache.ibatis.binding.BindingException: Parameter 'namesCode' not found. Available parameters are [arg1, arg0, param1, param2]
at org.mybatis.spring.MyBatisExceptionTranslator.translateExceptionIfPossible(MyBatisExceptionTranslator.java:77)
at org.mybatis.spring.SqlSessionTemplate$SqlSessionInterceptor.invoke(SqlSessionTemplate.java:446)
at com.sun.proxy.$Proxy129.selectOne(Unknown Source)
at org.mybatis.spring.SqlSessionTemplate.selectOne(SqlSessionTemplate.java:166)
at org.apache.ibatis.binding.MapperMethod.execute(MapperMethod.java:82)
at org.apache.ibatis.binding.MapperProxy.invoke(MapperProxy.java:59)
at com.sun.proxy.$Proxy155.queryByCode(Unknown Source)
at

从上面的报错信息,可以明确知道,binding过程想从上下文拿到对应的namesCode参数值,但是上下文的参数列表中,只有[arg1, arg0, param1, param2],这种没有语义含义的参数名。

回想整个过程中,除了增加各种单测用例之外,本次只是对pom文件中插件对了调整。为了先不影响提测,先人工加了mybatis @param注解来解决,然后再来查问题。

二、发现&解决问题

先从 mybatis @param注解入手,一般这种,应该要么是maven依赖版本降了不支持,要么就是其他pom配置改了。
然后,网上查了资料,mybatis 发现不使用 @param的方式,就是用maven-compiler-plugin ,参考:https://blog.csdn.net/tangyaya8/article/details/90300554 。发现版本3.1比较低,然后改成3.8.0,发现问题解决了。

因此,总结一下,就是对于java8而言,新增了Parameter反射类,可以拿到方法参数名。但是,获取参数名默认是不打开的,需要设置打开下,例如下javac时,新增 -parameters 参数,对于maven项目,可以通过maven-compiler-plugin 配置来完成。spring boot 的parent pom 中,已经把这个配置打开了,所以一般我们使用spring boot时,没有注意这个。如下:

1
2
3
4
5
6
<plugin>  
<artifactId>maven-compiler-plugin</artifactId>
<configuration>
<parameters>true</parameters>
</configuration>
</plugin>

具体插件信息:https://blog.csdn.net/ystyaoshengting/article/details/104038230 。从这份资料可以看到,插件版本在3.6.2以上,才支持。

所以,将版本升级到3.8.0之后,就支持了 spring-boot-starter-parent中设置的默认打开parameters 开关。

三、java Parameter 反射类

接下来,我们看看Parameter类。
先看看下面的使用示例:

1
2
3
4
5
6
7
8
9
10
11
public class Test {  

public static void main(String[] args) throws Exception{

Method method = String.class.getMethod("indexOf",Integer.TYPE,Integer.TYPE);
Parameter[] parameters = method.getParameters();
for (Parameter parameter:parameters){
System.out.println(parameter.getName());
}
}
}

javac 如果使用-parameters,则打印的就是'ch' 'fromIndex',否则,就是'arg0' 'arg1'
针对maven项目,则是可以参考上面的maven插件设置。java 8 提供的Parameter反射类,拿到方法入参名称,让很多工具的使用变得非常简单,不需要再通过注解指定名称,来映射了。

例如,前面说的 mybatis,此外,spring mvc中也有使用,例如我们用的PathVariableRequestParam等。

四、mybatis 使用源码示例

从上面的mybatis报错信息,可以看到,主要的sql处理逻辑在MapperMethod这个类中,找到对应的代码段,如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
public Object execute(SqlSession sqlSession, Object[] args) {  
Object result;
switch (command.getType()) {
//代码省略......
case SELECT:
if (method.returnsVoid() && method.hasResultHandler()) {
executeWithResultHandler(sqlSession, args);
result = null;
} else if (method.returnsMany()) {
result = executeForMany(sqlSession, args);
} else if (method.returnsMap()) {
result = executeForMap(sqlSession, args);
} else if (method.returnsCursor()) {
result = executeForCursor(sqlSession, args);
} else {
Object param = method.convertArgsToSqlCommandParam(args);
result = sqlSession.selectOne(command.getName(), param);
}
break;
// 代码省略........
}
}

具体逻辑在convertArgsToSqlCommandParam方法中,主要是ParamNameResolver类负责实现。看,该类的构造函数,负责解析:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
public ParamNameResolver(Configuration config, Method method) {  
final Class<?>[] paramTypes = method.getParameterTypes();
final Annotation[][] paramAnnotations = method.getParameterAnnotations();
final SortedMap<Integer, String> map = new TreeMap<Integer, String>();
int paramCount = paramAnnotations.length;
// 优先从 @Param 注解中拿到入参name
for (int paramIndex = 0; paramIndex < paramCount; paramIndex++) {
//省略一些代码....
String name = null;
// 看注解
for (Annotation annotation : paramAnnotations[paramIndex]) {
// 注解处理,省略代码.....
}
if (name == null) {
// @Param没有使用注解的话
if (config.isUseActualParamName()) {
// +++这里就是我们看他自己根据反射拿到的参数name地方+++
name = getActualParamName(method, paramIndex);
}
if (name == null) {
// use the parameter index as the name ("0", "1", ...)
// gcode issue #71
name = String.valueOf(map.size());
}
}
map.put(paramIndex, name);
}
names = Collections.unmodifiableSortedMap(map);
}

// 反射拿到真实参数名称
private String getActualParamName(Method method, int paramIndex) {
if (Jdk.parameterExists) { //支持java.lang.reflect.Parameter判断
return ParamNameUtil.getParamNames(method).get(paramIndex);
}
return null;
}

下面就是最终的实现工具类,如果我们要使用的话,其实也可以通过这个工具类拿到名称列表,注意,只支持java8及以上:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29

// 最终的实现
package org.apache.ibatis.reflection;

@UsesJava8
public class ParamNameUtil {
public static List<String> getParamNames(Method method) {
return getParameterNames(method);
}

public static List<String> getParamNames(Constructor<?> constructor) {
return getParameterNames(constructor);
}

// 这边就是遍历方法的参数列表,拿到入参名称name列表
private static List<String> getParameterNames(Executable executable) {
final List<String> names = new ArrayList<String>();
final Parameter[] params = executable.getParameters();
for (Parameter param : params) {
names.add(param.getName());
}
return names;
}

private ParamNameUtil() {
super();
}
}

五、总结

一个由于无意将maven-compiler-plugin版本降低,导致mybatis 无法使用 Parameter的反射特性,最终功能受影响的问题,这里就分析结束了。

在我们使用spring boot 框架之后,很多的细节,尤其是配置、依赖等,都被框架给隐藏了,因此,在使用的时候,还是需要不断的去倒腾,发现问题后,最终需要找到原因解决它。