启用 -parameters 编译选项简化 mybatis @Param 注解重复问题

介绍了启用 -parameters 编译选项简化 mybatis @Param 注解重复的问题, 包括 maven 的配置及相应 IDE 中的设置(Eclipse 及 Intellij IDEA)

目录
[隐藏]

在使用 mybatis 查询的时候, 只需要定义一个查询接口, mybatis 会为我们注入注解实现或是 xml 实现. 但当我们需要传递参数时, 通常需要 @Param 来定义一个名称, 但经常的, 我们也不难发现, 这个名称与参数名称通常是一样的:

User findUser(@Param("username") String username, @Param("password") String password);

如上, username 和 password 都重复了.

而之所以要这么使用, 是因为 xml 中 ${xxx} 所引用的名称就来自于 @Param 里定义的值:

<select id="findUser" resultType="net.xiaogd.demo.mybatis.entity.User">
    select * from user where username = #{username} and password = #{password}
</select>

这就带来一个重复的问题, 可否简化这个定义, 使得无需重复录入名称, 甚至完全地去掉呢? 比如像下面这样:

User findUser(String username, String password);

答案是可以的, 下面就来说下怎么去做到这一点.

前置条件

  1. 首先项目需要使用 jdk8 或以上;
  2. 其次, 需要增加一个编译时的选项 -parameters.

    也即是这样去编译: javac -parameters

通常, 如果没有加上这个选项, 编译后的方法参数签名会变成这样:

User findUser(String arg0, String arg1);

实际的名称会变成如上所示的 arg0, arg1 这样没有太多含义的, 毕竟解析器并不关心实际的名称, 有含义的名称只是给人阅读的而已.

下面就说说怎么去引入这个编译选项以使得可以保留有意义的参数名, 包括 maven 中的设置及 IDE 中的设置(包括 Intellij IDEA 和 Eclipse)

maven 编译时的选项

对于 maven, 可以在编译插件 maven-compiler-plugin 中使用 compilerArgs 增加参数来实现:

<plugin>
    <groupId>org.apache.maven.plugins</groupId>
    <artifactId>maven-compiler-plugin</artifactId>
    <configuration>
        <compilerArgs>
            <arg>-parameters</arg>
        </compilerArgs>
    </configuration>
</plugin>

注: 对于较新的 maven 版本(>= 3.6.2), 也可以直接使用 parameters 配置项:

<plugin>
    <groupId>org.apache.maven.plugins</groupId>
    <artifactId>maven-compiler-plugin</artifactId>
    <configuration>
        <parameters>true</parameters>
    </configuration>
</plugin>

参见: https://maven.apache.org/plugins/maven-compiler-plugin/compile-mojo.html#parameters

另注: 如果你使用 spring boot 2.0 及以后的版本并依赖了 spring-boot-starter-parent, 默认情况下已经启用了这一选项:

参见这里的说明: https://stackoverflow.com/questions/31845676/how-to-compile-spring-boot-applications-with-java-8-parameter-flag/49316086#49316086

你可以通过查看最终生效的 Effective POM 来确认这一点, 对于 Eclipse, 在打开的 pom.xml 文件下方选项卡中选择"Effective POM", 然后搜索 maven-compiler-plugin 关键字可以找到相关配置:
maven-effective-pom-compiler-parameters-true-eclipse

对于 Intellij IDEA, 在 maven 窗口中 右键--Show Effective POM:
maven-effective-pom-compiler-parameters-true-intellij-idea

或者通过实际是否正常运行来确认这一点, 如果不是很确定, 当然你可以如上所述在自己的 pom.xml 文件中显式地配置上它.

IDE 编译时保留参数名称

说完了 maven 中的配置, 下面再说说在 IDE 中的类似设置.

注: 通常, 如果 maven 中设置了相应选项, 在项目作为 maven 项目导入并构建时, 这些额外的设置也会生效, 无需额外再作设置. 但考虑到 IDE 的版本及可能存在 bug 等各类原因, 如果在 IDE 中运行不正常, 那么则需要额外检查及配置.

Intellij IDEA 中的设置 -parameters

对于 IDEA, 在下述位置 Settings > Build, Execution, Deployment > Compiler > Java Compiler > Additional command line parameters(额外的命令行参数) 的输入框中, 输入-parameters:

Intellij-idea-compiler-additional-command-line-parameters-javac-parameters

参考: https://stackoverflow.com/questions/39217830/how-to-use-parameters-javac-option-in-intellij

如前所述, 如果没有设置也运行正常, 则不必去设置.

Eclipse 中的设置 -parameters

对于 Eclipse, 则是检查 Store information about method parameters 选项, 看看是否已经是勾选上, 如果没有, 则把它勾上:

eclipse-java-compiler-store-information-about-method-parameters

如前所述, 如果没有勾选也运行正常或者默认已经勾选上了, 则不必再去勾选.

另注: 因为以上设置涉及编译, 所以通常需要重新编译项目(如果设置后没有自动触发 rebuild), 如果还不生效, 甚至可能需要重启 IDE.

没有配置成功时的异常

如果没有加入 -parameters 选项或因其它原因没有启用成功, 则去掉 @Param 注解后可能会遇到下述异常:

exception:
org.mybatis.spring.MyBatisSystemException: nested exception is org.apache.ibatis.binding.BindingException: Parameter 'username' not found. Available parameters are [arg1, arg0, param1, param2]

完整的异常如下:

exception: 
org.mybatis.spring.MyBatisSystemException: nested exception is org.apache.ibatis.binding.BindingException: Parameter 'username' 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.$Proxy78.selectList(Unknown Source)
    at org.mybatis.spring.SqlSessionTemplate.selectList(SqlSessionTemplate.java:230)
    at org.apache.ibatis.binding.MapperMethod.executeForMany(MapperMethod.java:137)
    at org.apache.ibatis.binding.MapperMethod.execute(MapperMethod.java:75)
    at org.apache.ibatis.binding.MapperProxy.invoke(MapperProxy.java:59)
    at com.sun.proxy.$Proxy86.findUserByUsernameAndPassword(Unknown Source)
    at net.xiaogd.demo.mybatis.dao.user.UserDaoTest.testXmlDao(UserDaoTest.java:76)
    at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
    at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62)
    at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
    at java.lang.reflect.Method.invoke(Method.java:498)
    at org.junit.runners.model.FrameworkMethod$1.runReflectiveCall(FrameworkMethod.java:50)
    at org.junit.internal.runners.model.ReflectiveCallable.run(ReflectiveCallable.java:12)
    at org.junit.runners.model.FrameworkMethod.invokeExplosively(FrameworkMethod.java:47)
    at org.junit.internal.runners.statements.InvokeMethod.evaluate(InvokeMethod.java:17)
    at org.springframework.test.context.junit4.statements.RunBeforeTestExecutionCallbacks.evaluate(RunBeforeTestExecutionCallbacks.java:74)
    at org.springframework.test.context.junit4.statements.RunAfterTestExecutionCallbacks.evaluate(RunAfterTestExecutionCallbacks.java:84)
    at org.springframework.test.context.junit4.statements.RunBeforeTestMethodCallbacks.evaluate(RunBeforeTestMethodCallbacks.java:75)
    at org.springframework.test.context.junit4.statements.RunAfterTestMethodCallbacks.evaluate(RunAfterTestMethodCallbacks.java:86)
    at org.springframework.test.context.junit4.statements.SpringRepeat.evaluate(SpringRepeat.java:84)
    at org.junit.runners.ParentRunner.runLeaf(ParentRunner.java:325)
    at org.springframework.test.context.junit4.SpringJUnit4ClassRunner.runChild(SpringJUnit4ClassRunner.java:251)
    at org.springframework.test.context.junit4.SpringJUnit4ClassRunner.runChild(SpringJUnit4ClassRunner.java:97)
    at org.junit.runners.ParentRunner$3.run(ParentRunner.java:290)
    at org.junit.runners.ParentRunner$1.schedule(ParentRunner.java:71)
    at org.junit.runners.ParentRunner.runChildren(ParentRunner.java:288)
    at org.junit.runners.ParentRunner.access$000(ParentRunner.java:58)
    at org.junit.runners.ParentRunner$2.evaluate(ParentRunner.java:268)
    at org.springframework.test.context.junit4.statements.RunBeforeTestClassCallbacks.evaluate(RunBeforeTestClassCallbacks.java:61)
    at org.springframework.test.context.junit4.statements.RunAfterTestClassCallbacks.evaluate(RunAfterTestClassCallbacks.java:70)
    at org.junit.runners.ParentRunner.run(ParentRunner.java:363)
    at org.springframework.test.context.junit4.SpringJUnit4ClassRunner.run(SpringJUnit4ClassRunner.java:190)
    at org.eclipse.jdt.internal.junit4.runner.JUnit4TestReference.run(JUnit4TestReference.java:89)
    at org.eclipse.jdt.internal.junit.runner.TestExecution.run(TestExecution.java:41)
    at org.eclipse.jdt.internal.junit.runner.RemoteTestRunner.runTests(RemoteTestRunner.java:542)
    at org.eclipse.jdt.internal.junit.runner.RemoteTestRunner.runTests(RemoteTestRunner.java:770)
    at org.eclipse.jdt.internal.junit.runner.RemoteTestRunner.run(RemoteTestRunner.java:464)
    at org.eclipse.jdt.internal.junit.runner.RemoteTestRunner.main(RemoteTestRunner.java:210)
Caused by: org.apache.ibatis.binding.BindingException: Parameter 'username' not found. Available parameters are [arg1, arg0, param1, param2]
    at org.apache.ibatis.binding.MapperMethod$ParamMap.get(MapperMethod.java:202)
    at org.apache.ibatis.reflection.wrapper.MapWrapper.get(MapWrapper.java:45)
    at org.apache.ibatis.reflection.MetaObject.getValue(MetaObject.java:122)
    at org.apache.ibatis.executor.BaseExecutor.createCacheKey(BaseExecutor.java:219)
    at org.apache.ibatis.executor.CachingExecutor.createCacheKey(CachingExecutor.java:146)
    at org.apache.ibatis.executor.CachingExecutor.query(CachingExecutor.java:82)
    at org.apache.ibatis.session.defaults.DefaultSqlSession.selectList(DefaultSqlSession.java:148)
    at org.apache.ibatis.session.defaults.DefaultSqlSession.selectList(DefaultSqlSession.java:141)
    at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
    at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62)
    at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
    at java.lang.reflect.Method.invoke(Method.java:498)
    at org.mybatis.spring.SqlSessionTemplate$SqlSessionInterceptor.invoke(SqlSessionTemplate.java:433)
    ... 38 more

那么, 按照其提示, 可以将相应的 xml 语句调整为:

<select id="findUser" resultType="net.xiaogd.demo.mybatis.entity.User">
    select * from user where username = #{arg0} and password = #{arg1}
</select>

或者是使用 param1, param2 这样的名称(注意, 与 arg 从 0 编号不同, 这里是从 1 开始编号)

<select id="findUser" resultType="net.xiaogd.demo.mybatis.entity.User">
    select * from user where username = #{param1} and password = #{param2}
</select>

自然, 使用这些没有太多含义的编号参数名, 代码的可读性就差了很多, 参数是否正确对上了也不容易看出来.

mybatis 版本及 useActualParamName(use-actual-param-name) 的问题

最后, 还有一个配置 useActualParamName(使用实际的参数名称) 可能导致一些异常, 这点与 mybatis 不同版本的缺省配置不同有关, 也与项目本身是否显式配置了这一参数值有关.

在没有启用 -parameters 以保留方法参数名并且没有用 @Param 设置一个有效的名称时, 有时你可能会发现使用 arg0 也还是提示找不到参数:

org.mybatis.spring.MyBatisSystemException: nested exception is org.apache.ibatis.binding.BindingException: Parameter 'arg0' not found. Available parameters are [0, 1, param1, param2]

完整的异常如下:

org.mybatis.spring.MyBatisSystemException: nested exception is org.apache.ibatis.binding.BindingException: Parameter 'arg0' not found. Available parameters are [0, 1, 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.$Proxy78.selectList(Unknown Source)
    at org.mybatis.spring.SqlSessionTemplate.selectList(SqlSessionTemplate.java:230)
    at org.apache.ibatis.binding.MapperMethod.executeForMany(MapperMethod.java:137)
    at org.apache.ibatis.binding.MapperMethod.execute(MapperMethod.java:75)
    at org.apache.ibatis.binding.MapperProxy.invoke(MapperProxy.java:59)
    at com.sun.proxy.$Proxy86.findUserByUsernameAndPassword(Unknown Source)
    at net.xiaogd.demo.mybatis.dao.user.UserDaoTest.testXmlDao(UserDaoTest.java:76)
    at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
    at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62)
    at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
    at java.lang.reflect.Method.invoke(Method.java:498)
    at org.junit.runners.model.FrameworkMethod$1.runReflectiveCall(FrameworkMethod.java:50)
    at org.junit.internal.runners.model.ReflectiveCallable.run(ReflectiveCallable.java:12)
    at org.junit.runners.model.FrameworkMethod.invokeExplosively(FrameworkMethod.java:47)
    at org.junit.internal.runners.statements.InvokeMethod.evaluate(InvokeMethod.java:17)
    at org.springframework.test.context.junit4.statements.RunBeforeTestExecutionCallbacks.evaluate(RunBeforeTestExecutionCallbacks.java:74)
    at org.springframework.test.context.junit4.statements.RunAfterTestExecutionCallbacks.evaluate(RunAfterTestExecutionCallbacks.java:84)
    at org.springframework.test.context.junit4.statements.RunBeforeTestMethodCallbacks.evaluate(RunBeforeTestMethodCallbacks.java:75)
    at org.springframework.test.context.junit4.statements.RunAfterTestMethodCallbacks.evaluate(RunAfterTestMethodCallbacks.java:86)
    at org.springframework.test.context.junit4.statements.SpringRepeat.evaluate(SpringRepeat.java:84)
    at org.junit.runners.ParentRunner.runLeaf(ParentRunner.java:325)
    at org.springframework.test.context.junit4.SpringJUnit4ClassRunner.runChild(SpringJUnit4ClassRunner.java:251)
    at org.springframework.test.context.junit4.SpringJUnit4ClassRunner.runChild(SpringJUnit4ClassRunner.java:97)
    at org.junit.runners.ParentRunner$3.run(ParentRunner.java:290)
    at org.junit.runners.ParentRunner$1.schedule(ParentRunner.java:71)
    at org.junit.runners.ParentRunner.runChildren(ParentRunner.java:288)
    at org.junit.runners.ParentRunner.access$000(ParentRunner.java:58)
    at org.junit.runners.ParentRunner$2.evaluate(ParentRunner.java:268)
    at org.springframework.test.context.junit4.statements.RunBeforeTestClassCallbacks.evaluate(RunBeforeTestClassCallbacks.java:61)
    at org.springframework.test.context.junit4.statements.RunAfterTestClassCallbacks.evaluate(RunAfterTestClassCallbacks.java:70)
    at org.junit.runners.ParentRunner.run(ParentRunner.java:363)
    at org.springframework.test.context.junit4.SpringJUnit4ClassRunner.run(SpringJUnit4ClassRunner.java:190)
    at org.eclipse.jdt.internal.junit4.runner.JUnit4TestReference.run(JUnit4TestReference.java:89)
    at org.eclipse.jdt.internal.junit.runner.TestExecution.run(TestExecution.java:41)
    at org.eclipse.jdt.internal.junit.runner.RemoteTestRunner.runTests(RemoteTestRunner.java:542)
    at org.eclipse.jdt.internal.junit.runner.RemoteTestRunner.runTests(RemoteTestRunner.java:770)
    at org.eclipse.jdt.internal.junit.runner.RemoteTestRunner.run(RemoteTestRunner.java:464)
    at org.eclipse.jdt.internal.junit.runner.RemoteTestRunner.main(RemoteTestRunner.java:210)
Caused by: org.apache.ibatis.binding.BindingException: Parameter 'arg0' not found. Available parameters are [0, 1, param1, param2]
    at org.apache.ibatis.binding.MapperMethod$ParamMap.get(MapperMethod.java:202)
    at org.apache.ibatis.reflection.wrapper.MapWrapper.get(MapWrapper.java:45)
    at org.apache.ibatis.reflection.MetaObject.getValue(MetaObject.java:122)
    at org.apache.ibatis.executor.BaseExecutor.createCacheKey(BaseExecutor.java:219)
    at org.apache.ibatis.executor.CachingExecutor.createCacheKey(CachingExecutor.java:146)
    at org.apache.ibatis.executor.CachingExecutor.query(CachingExecutor.java:82)
    at org.apache.ibatis.session.defaults.DefaultSqlSession.selectList(DefaultSqlSession.java:148)
    at org.apache.ibatis.session.defaults.DefaultSqlSession.selectList(DefaultSqlSession.java:141)
    at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
    at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62)
    at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
    at java.lang.reflect.Method.invoke(Method.java:498)
    at org.mybatis.spring.SqlSessionTemplate$SqlSessionInterceptor.invoke(SqlSessionTemplate.java:433)
    ... 38 more

而这是由于 mybatis 版本及 useActualParamName(use-actual-param-name) 选项设置的原因.

在早期的版本中(< 3.4.1), useActualParamName 选项的默认值是 false, 而之后的版本(>= 3.4.1), 这个选项的默认值则是 true.

只有设置为 true, 才能利用 -parameters 配置带来的好处.

或者就是项目本身配置了其它不同于缺省的值, 如果使用了 spring boot 项目, 可以检查如下选项:

mybatis.configuration.use-actual-param-name=false

或是在 xml 配置文件中设置:

<setting name="useActualParamName" value="false" />

如果设置了 false, 那么就要写成 #{0}, #{1} 这样:

<select id="findUser" resultType="net.xiaogd.demo.mybatis.entity.User">
    select * from user where username = #{0} and password = #{1}
</select>

如果以前有大量这样的写法, 而你为了兼容它们不想去调整, 那你就无法利用 -parameters 配置带来的好处.

无论是使用 {0}, {1}, 还是使用 {arg0}, {arg1}, 可读性都不是很好, 而且在后续如果需要增加参数, 还容易引入错误, 因此并不推荐这样的写法.

关于启用 -parameters 编译选项简化 mybatis @Param 注解重复问题就介绍到这里.

发表评论

电子邮件地址不会被公开。