☰
MyBatis参数获取全解析:从单参数到@Param与源码原理
2026/10/28 18:53:28 网站建设 项目流程

作为一个整天跟 MyBatis 打交道的老码农,我得说Mapper接口方法里那点参数传递的门道,看着简单,实际上坑是真不少。很多新手甚至干了两三年的开发,在dao层写接口的时候,参数一多就随便塞个Map或者干脆用@Param硬怼,结果代码维护起来想死的心都有。更别提面试的时候,面试官随便问一句“MyBatis是怎么拿到你传进去的那个参数的”,不少人就卡壳了。

这篇就把MyBatis获取参数值的各种情况一次性捋清楚。从最简单的单个参数,到多个参数、POJO、Map、@Param注解,再到源码层面看看它到底怎么解析的。保证你看完不仅能应付日常开发,面试被问到也能扯得明明白白。

1. 内容整体设计与思路拆解

1.1 搞懂参数获取的本质:它就是个“存取”游戏

先理解最核心的一点:MyBatis框架本身并不关心你的Mapper接口方法长什么样,它在运行时会通过JDK动态代理为你的接口生成一个实现类。当你调用接口方法时,真正的逻辑会转移到SqlSession上,而你的参数值需要被框架“接住”,然后封装成一个Map类型的参数对象,最终传递给SqlSource(也就是你写的那些SQL)。说白了,整个参数解析过程就是搞清楚“你给的参数叫什么名字”和“SQL里怎么引用这个名字”的过程。

理解了这一点,你就能明白为什么有时候传一个参数什么都不用做,传多个参数却直接#{param1}、#{param2}的奇怪现象。框架不知道你的参数在业务上叫什么,在没给提示的情况下,只能用位置序号兜底命名。

1.2 从使用场景反推技术选型

很多人在写接口时比较随意,这里我先给出一个基本的选择思路,后面再逐一展开:

  • 单个简单类型参数(String、Integer等):直接传,SQL里随便用#{任意名字},因为只有一个值,框架直接拿来用。
  • 多个参数(2个以上):优先考虑封装成一个POJO类,如果不想为了查询单独建类,就用Map,再不行就老老实实加@Param。
  • 参数是对象(增删改查涉及多字段):直接传POJO,SQL里用#{属性名}。
  • 集合/数组参数(foreach遍历):根据是List、Set还是数组,框架对默认名称的处理不太一样,这个坑很多。

记住这个粗略的原则,你写接口的时候就不会太纠结。接下来我一个个场景过,最后再带你到源码里看看,为什么会出现param1这种玩意儿。

2. 核心细节解析与实操要点

2.1 单个参数:最简单也最容易让人得意忘形

先看最舒服的场景。假设有个需求,要根据用户id查一条记录,接口定义如下:

User selectUserById(Integer id);

XML 这样写:

<select id="selectUserById" resultType="com.example.User"> select * from user where id = #{id} </select>

注意,你的接口参数名是id,SQL里用的#{id},能跑通。但你把这个id改成#{userId}甚至#{abc},一样能跑通。这就是单个简单类型参数的情况:MyBatis 对参数名不敏感,因为只有一个参数值,它直接把这个值取出来用,#{}里面写什么名字只起占位作用。

如果是单个 POJO 参数呢?比如:

int insertUser(User user);

SQL 里就得老老实实用属性名了:

<insert id="insertUser"> insert into user(name, age, email) values(#{name}, #{age}, #{email}) </insert>

因为框架知道你这个参数是User类型,它会拿#{}里的名字去调用user.getName()、user.getAge()。如果写了个User类里不存在的属性,运行期直接报ReflectionException: There is no setter for property named 'xxx'。

这块儿给个小经验:单个 POJO 传参时,#{}里的名字必须和属性名严格一致(大小写不敏感,但建议保持一致),比如name对应getName()。很多同学用Lombok生成getter/setter后容易忽略这一点,其实大多数情况下不报错,是因为MyBatis底层走的是反射,对属性命名有自己的一套宽松规则,但你别去挑战它,规规矩矩最省事。

2.2 多个参数:为什么会出现 param1 这种鬼名字?

多个参数是很多人第一次懵掉的地方。来看代码:

User selectUserByNameAndAge(String name, Integer age);

XML 你可能会很自然地写:

<select id="selectUserByNameAndAge" resultType="com.example.User"> select * from user where name = #{name} and age = #{age} </select>

跑一下,报错:

Parameter 'name' not found. Available parameters are [arg1, arg0, param1, param2]

看到没,这个报错信息已经把答案告诉你了:可用参数是 [arg1, arg0, param1, param2]。你没给参数起名字,框架就默认用位置来命名。这里注意,JDK 8之前编译后拿不到方法参数名,所以MyBatis干脆不认你原来写的name、age,统一以param1、param2命名(arg0、arg1是另一种形式的别名)。

所以多个参数的写法有两种:

第一种:用默认位置名(能用但不推荐)

<select id="selectUserByNameAndAge" resultType="com.example.User"> select * from user where name = #{param1} and age = #{param2} </select>

第二种:加 @Param 注解(推荐)

User selectUserByNameAndAge(@Param("name") String name, @Param("age") Integer age);
<select id="selectUserByNameAndAge" resultType="com.example.User"> select * from user where name = #{name} and age = #{age} </select>

这一点真的很重要:只要超过一个参数,而且你没加任何注解,那就别在#{}里写你方法定义时的参数名了,大概率会报错。从代码可读性角度来看,param1、param2这种写法简直是灾难,你过两周回来看代码,鬼知道param1是name还是age。

2.3 Map 参数:万金油式的传参,但别滥用

当参数实在太多,又不想为某个查询单独建一个POJO时,Map是一个快速方案。接口定义:

List<User> selectUserByCondition(Map<String, Object> params);

调用方式:

Map<String, Object> params = new HashMap<>(); params.put("name", "张三"); params.put("age", 20); List<User> users = userMapper.selectUserByCondition(params);

XML:

<select id="selectUserByCondition" resultType="com.example.User"> select * from user where name = #{name} and age = #{age} </select>

这种方式的原理不难理解:你传一个Map进去,MyBatis拿到的是整个Map,它会根据key去找对应的value。只要 key 和你#{}里的名字对上就行,顺序无所谓。

优点很明显——灵活、不用建类。缺点同样明显——类型不安全(编译期不检查 key 是否存在、value 类型是否匹配),key拼错了只能运行期报错。我给的实践建议是:临时用一下可以,但如果一个参数集合在多个地方复用,或者涉及核心业务逻辑,还是老老实实建POJO。另外提醒一下,Java 新一点的版本里Map.of()能少写不少代码,但千万别传一个不可变Map进去后又在MyBatis拦截器里想改它的值,会抛UnsupportedOperationException。

2.4 POJO 参数:业务开发中最推荐的姿势

如果业务上已经有一个对象(比如查询条件UserQuery),直接把它作为参数是最优雅的:

List<User> selectUserList(UserQuery query);
<select id="selectUserList" resultType="com.example.User"> select * from user <where> <if test="name != null and name != ''"> and name like concat('%', #{name}, '%') </if> <if test="age != null"> and age = #{age} </if> </where> </select>

这时候#{}里就是POJO的属性名。这里扩展一个容易被忽略的小点:在<if test="">里写的也是属性名,不是param1,而且test里的写法走的是OGNL表达式,规则跟#{}有点不同(比如字符串比较写'y'别漏引号)。

用一个专门用于查询条件的POJO来接收参数,从架构上看是最干净的。你可以组合页码、排序字段、过滤条件等等,还能在里面做参数校验和默认值处理。通用一些的做法是在查询条件类里加上分组条件之类:

public class UserQuery { private String name; private Integer age; private String email; private String orderByColumn; private String orderByType; // getter/setter 省略 }

XML里还可以在<script>中动态拼接排序字段(注意排序字段不能用#{},得用${},前提是你做了白名单校验,否则会有SQL注入风险)。

2.5 @Param 注解:多参数的“正规军”姿势

@Param的使用很简单,在方法参数前加上注解,给参数起个名字即可。它解决的问题是多个参数时框架无法获取参数名的问题。

List<User> selectUserByIdsAndStatus( @Param("ids") List<Long> ids, @Param("status") Integer status );

XML:

<select id="selectUserByIdsAndStatus" resultType="com.example.User"> select * from user where status = #{status} and id in <foreach collection="ids" item="id" open="(" separator="," close=")"> #{id} </foreach> </select>

这里注意一个很隐蔽的坑:当只有一个集合或数组参数时,不加@Param也能在foreach里用collection="list"或collection="array"访问;但如果你这个集合混在其他参数里(像上面这个例子),那就必须给集合参数加@Param,并且foreach的collection属性必须跟注解名一致,否则你只能用param1这种名字,非常容易想不起来。

那到底什么时候该用@Param?

  • 方法有多个参数且不便合并成一个对象。
  • 需要在SQL中多次引用同一个参数。
  • 参数包含集合(如List、Set)需要配合foreach遍历。
  • 需要在动态SQL中判断参数是否为空(if test里要用名字)。

当然也有不少人习惯了在单参数 POJO 上也加@Param,这纯粹是个人风格问题,不影响功能,有时为了避免将来加参数引起连锁改动也是一种防御性写法。

2.6 集合与数组参数的特殊规则:List、Set、Array 的区别

如果把一个List直接作为唯一参数传给Mapper方法:

List<User> selectUserByIds(List<Long> ids);

XML里的foreach这么写是可以的:

<select id="selectUserByIds" resultType="com.example.User"> select * from user where id in <foreach collection="list" item="id" open="(" separator="," close=")"> #{id} </foreach> </select>

注意!这里用的是collection="list",因为MyBatis对于List类型默认会额外存一个 key 叫list(实际上ParamNameResolver源码里会把它包装为collection和list,但这里是List直接传入的情况)。

如果参数是Set,默认 key 是collection。如果是一个数组Long[] ids,那collection得写成array:

<foreach collection="array" item="id" ...>

还有更细的:如果你希望SQL可读性更好,最稳的还是注解指定名字:

List<User> selectUserByIds(@Param("ids") Long[] ids);
<foreach collection="ids" item="id" ...>

只要加了@Param,数组也会以ids这个名字存储,collection里写ids就行,不用关心array还是list了。这块儿的坑非常经典,我记得以前有个同事问为什么换成数组后就报There is no getter for property named 'list',就是这个原因。

3. 实操过程与核心环节实现

3.1 Mapper 接口与 XML 映射的最佳实践示例

空谈理论没用,下面直接给一个接近真实业务的完整例子,把所有情况串起来。

实体类(常规操作,字段不多但足够覆盖场景):

public class User { private Long id; private String name; private Integer age; private String email; private Integer status; // 1-正常 0-禁用 // 省略 getter/setter }

Mapper 接口:

public interface UserMapper { // 场景一:单个简单类型参数 User selectById(Long id); // 场景二:多个参数,使用 @Param List<User> selectByNameAndStatus(@Param("name") String name, @Param("status") Integer status); // 场景三:Map 参数(演示用,不推荐用于核心场景) List<User> selectByMap(Map<String, Object> params); // 场景四:POJO 参数,支持动态 SQL List<User> selectByCondition(UserQuery query); // 场景五:集合参数,使用 foreach(加了 @Param 的情况) List<User> selectByIds(@Param("ids") List<Long> ids); // 场景六:数组参数(不加 @Param 演示) List<User> selectByIdArray(Long[] ids); }

UserQuery 查询对象:

public class UserQuery { private String name; private Integer age; private Integer status; private String orderByCol; private String orderByDir; // asc/desc // getter/setter 省略 }

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="com.example.mapper.UserMapper"> <sql id="baseColumns"> id, name, age, email, status </sql> <select id="selectById" resultType="com.example.User"> select <include refid="baseColumns"/> from user where id = #{id} </select> <select id="selectByNameAndStatus" resultType="com.example.User"> select <include refid="baseColumns"/> from user where name = #{name} and status = #{status} </select> <select id="selectByMap" resultType="com.example.User"> select <include refid="baseColumns"/> from user where name = #{name} and age = #{age} </select> <select id="selectByCondition" resultType="com.example.User"> select <include refid="baseColumns"/> from user <where> <if test="name != null and name != ''"> and name like concat('%', #{name}, '%') </if> <if test="age != null"> and age = #{age} </if> <if test="status != null"> and status = #{status} </if> </where> </select> <select id="selectByIds" resultType="com.example.User"> select <include refid="baseColumns"/> from user where id in <foreach collection="ids" item="id" open="(" separator="," close=")"> #{id} </foreach> </select> <select id="selectByIdArray" resultType="com.example.User"> select <include refid="baseColumns"/> from user where id in <foreach collection="array" item="id" open="(" separator="," close=")"> #{id} </foreach> </select> </mapper>

这个例子里的selectByCondition是日常开发中出现频率最高的形式。<where>标签会自动去掉第一个多余的and,配合<if>实现按需拼条件,非常实用。比如前端只传了name=张,那SQL就是where name like concat('%', '张', '%'),不会把你age的条件拼进去。

3.2 源码角度验证参数获取机制(面试加分项)

与其背网上的结论,不如自己翻一遍源码。从MapperProxy开始,核心链路是这样的:

  • MapperMethod内部有个ParamNameResolver类,getNamedParams(Object[] args)方法负责把方法入参转成一个Map。
  • 如果参数列表只有一个且没有@Param,直接返回那个参数本身(所以单参数时#{}名字无所谓)。
  • 如果有多个参数或者存在@Param,会返回一个ParamMap。ParamMap里会放两类键值:
    • @Param指定的名字(如果有的话)。
    • 位置参数:param1、param2……(注意是 1 开始,不是 0)。
    • 另外如果编译时开启了-parameters参数(JDK 8+),可能还能拿到真实参数名,但实践中不常用,别依赖它。
  • foreach的collection属性解析时有个额外的逻辑:如果参数是List,多存一个list;如果是数组,多存一个array;如果是Collection,多存一个collection。

看一下源码关键流程(简化版):

public Object getNamedParams(Object[] args) { final int paramCount = names.size(); if (args == null || paramCount == 0) { return null; } else if (!hasParamAnnotation && paramCount == 1) { // 只有一个参数且没加 @Param,直接返回参数本身 return args[names.firstKey()]; } else { // 多个参数 or 有 @Param,包装成 Map final Map<String, Object> param = new ParamMap<>(); int i = 0; for (Map.Entry<Integer, String> entry : names.entrySet()) { param.put(entry.getValue(), args[entry.getKey()]); // 额外添加 param1, param2 ... final String genericParamName = "param" + (i + 1); if (!names.containsValue(genericParamName)) { param.put(genericParamName, args[entry.getKey()]); } i++; } return param; } }

这里names是从@Param注解或者-parameters编译参数里解析出来的。如果啥都没有,多参数场景下names里就只有0和1这种位置索引(具体取决于实现),拿到后会自动生成param1、param2给 SQL 用。这就是为什么报错信息里你会看到Available parameters are [arg1, arg0, param1, param2]——arg0、arg1是MyBatis3.4.x 之后加的一种特殊别名,但平常写SQL不太建议用它们,歧义太大。

另外值得一提的坑:某个参数明明加了@Param("id"),但 XML 里用了#{param1}引用,也是能跑通的,因为代码里会把param1也加进去。但如果你后面调整了参数顺序,这种混用方式非常容易出问题。我的建议是统一用@Param的名字,绝对不碰paramX,防止将来代码变成车轱辘话来回折腾。

3.3 实战编码规范建议(少踩坑的套路)

基于上面这些原理,我整理了一套自己写Mapper层时的铁律,分享给你:

  1. 接口方法参数超过三个,建议用 POJO 封装(比如查询条件类、更新DTO),既清晰又方便以后加字段。

  2. 参数数量在 2~3 个且不太可能再变时,可以用 @Param,但所有参数名必须语义化,禁止用param1这种。比如@Param("name")、@Param("age")。

  3. 有集合参数要遍历时,必须给集合参数加 @Param 并起一个复数名字(如ids),不要在 XML 里依赖list、array这种默认 key。

  4. #{}和${}的区别要刻在脑子里:#{}是预编译占位符,自动加引号防注入;${}是字符串拼接,只能用于表名、列名、排序字段等结构位置,并且务必做白名单校验。参数获取本质上都能拿到值,但安全性天差地别。

  5. 在 XML 里做动态 SQL 时,test里的参数名规则和#{}一致,如果长时间没跑通,优先看是不是多个参数但没加@Param导致的。

4. 常见问题与排查技巧实录

4.1 高频报错整理(含原因分析和解决方案)

下面列的都是我见过的、或者在网上高频出现的问题,我把场景、报错信息和处理办法整理到一起,方便你直接对号入座。

场景报错信息根本原因解决方案
多参数但 XML 里写参数名Parameter 'name' not found. Available parameters are [arg1, arg0, param1, param2]多个参数时框架没拿到真实参数名加@Param("name"),或者用param1(不推荐)。
单个 POJO 参数但#{}属性名写错There is no getter for property named 'xxx' in 'class com.example.User'反射找不到对应属性检查 POJO 类是否存在该字段(含 getter)。
foreach里 collection 写错foreach item 'id' not found. Available parameters are [list, param1]等类似提示集合参数默认 key 是list/collection/array之一给参数加@Param并用注解名;或根据集合类型写list/array。
传了Map但 key 拼错Parameter 'name' not found. Available parameters are [param1]Map里没有对应 key检查Map的 key 和 SQL 中#{}是否一致。
一个参数是String,但用了#{}拼接like查询正常,但结果不符like拼接时把引号弄错了用concat('%', #{name}, '%'),而不是'%${name}%'(防注入)。
test里参数名不在可用参数中执行时报There is no getter for property named 'xxx'动态 SQL 判断时同样受参数封装规则限制多参数务必@Param,并在 XML 使用注解名。
排序字段传进来没生效或报错SQL 拼接异常 / 注入提示试图用#{}传排序字段,被当成字符串值加了引号改用${}并通过白名单校验(如 switch 映射)。

4.2 几个容易忽视但非常实用的细节

第一个细节:在 Spring Boot + MyBatis 中,长时间只增删改查不报错,不代表参数写法就对。有些错误只在特定分支才会触发。比如selectByCondition里某个<if>可能只有在传入status时才会走到那条 SQL,如果之前所有调用都没传这个字段,那拼 SQL 时根本不会引用到那个参数,也就不会暴露问题。所以写完 SQL 后,尽量每个分支都测一遍。

第二个细节:MyBatis-Plus的逻辑删除和参数获取机制并不冲突,但如果你用@Select注解直接写 SQL,想“禁用逻辑删除”就比较麻烦。MyBatis-Plus的逻辑删除是靠它自己的 SQL 注入器实现的,对自定义 SQL 一般不生效(需要加@InterceptorIgnore之类)。这块和参数获取的关系在于:一旦你启用逻辑删除,查出来的SQL会自动追加where deleted = 0,如果这时你自己又在SQL里手动加了deleted = ?参数,容易参数顺序错乱导致结果不对。所以在自定义 SQL 里尽量少掺和逻辑删除的字段,让插件统一处理。

第三个细节:XML 里的#{}不仅可以取普通值,还能取对象属性嵌套属性,比如#{user.address.city}。这种写法在插入订单时很常用(订单里有user对象引用)。它的底层其实就是OGNL式的属性导航,只要每一层都有对应的 getter 就能取到。

第四个细节:接口方法没有参数时,XML 里不要写#{},否则直接报错。一般做统计或列出全部数据时不带参数,SQL 里就不要出现任何#{}占位符。

第五个细节:如果你用注解方式写 SQL(@Select、@Insert等),多参数的处理规则和 XML 是完全一致的,也得用@Param或param1。不过注解方式写复杂 SQL 很难维护,一般都推荐 XML,注解只用来写极简单的 SQL。

4.3 排查思路:遇到参数问题先看可用参数列表

遇到参数相关报错,我的排查顺序几乎是固定的,分享出来给你参考:

第一步,看异常信息。MyBatis非常友好,几乎把所有可用参数都列在报错信息里了,比如Available parameters are [arg1, arg0, param1, param2]。你第一时间就能知道当前框架认为有哪些 key 可用。

第二步,确认方法签名。去Mapper接口里数一下参数个数,看有没有加@Param,加了几个。

第三步,对照 XML 里的#{}名称。如果是多参数,把#{}里的名字改成你用@Param指定的名字;如果没加@Param,继续改成param1、param2来快速验证问题是不是出在命名上。

第四步,如果是foreach的collection报错,先确认参数是List、Set、数组还是普通对象。不同类型可用 key 不同。

第五步,如果是<if test>报错,检查test中的名字是否可用,并且是否存在/等误写,比如string != ''中string不是参数名就错了(有些人是想判断参数非空,结果写成判断一个不存在的字符串对象)。

这个排查链基本能解决九成以上问题,而且速度很快。别在没看报错的情况下瞎试,那是浪费生命。

5. 前后端联调中的参数传递场景(扩展实践)

5.1 接收前端 query/body 参数后传入 Mapper 的方式

实际业务中,Mapper的入参往往不是直接从前端来的,而是经过Controller->Service->Mapper这条链路。很多人会困惑:到底应该一层层传对象,还是组装Map?我给的实践是:

  • Controller层面用独立的VO/DTO(视图对象/数据传输对象)接收前端参数,不要直接拿实体类怼到接口上。
  • Service层把DTO转换成Mapper需要的查询条件对象(比如UserQuery),然后再调用Mapper。
  • Mapper层参数如果不是查询条件对象,最多再加一个分页参数,不要说一个字段就加一个参数,最后方法签名长到没法看。

比如一个典型的列表分页查询:

@RestController public class UserController { @Autowired private UserService userService; @GetMapping("/users") public Result list(UserQueryDTO queryDTO) { UserQuery query = new UserQuery(); query.setName(queryDTO.getName()); query.setAge(queryDTO.getAge()); query.setStatus(queryDTO.getStatus()); Page<User> page = userService.pageUsers(query, queryDTO.getPageNum(), queryDTO.getPageSize()); return Result.success(page); } }

这样的好处是各层解耦,前端传参变动时,只需要改DTO和query对象,Mapper的 SQL 几乎不用动。MyBatis参数获取的问题自然就只集中在Mapper方法签名 + XML 这一小块范围里了。

5.2 分页插件与多参数组合时的正确姿势

熟悉PageHelper的老铁应该遇到过这种问题:方法参数里既有分页对象又有查询条件,怎么传才能让插件正确分页,同时 SQL 还能正确取参?

典型写法:

IPage<User> selectUserPage(Page<?> page, @Param("query") UserQuery query);

这里用MyBatis-Plus的IPage做分页(Page是它的实现类),Page参数会被分页插件识别,不参与业务参数的命名。query对象则通过@Param("query")指定名称。XML 里这样取:

<select id="selectUserPage" resultType="com.example.User"> select <include refid="baseColumns"/> from user <where> <if test="query.name != null and query.name != ''"> and name like concat('%', #{query.name}, '%') </if> <if test="query.status != null"> and status = #{query.status} </if> </where> </select>

注意这里#{}都写了query.name,因为@Param("query")已经把对象挂在query这个 key 下面了。如果你忘了写@Param还可能因为多参数问题直接抓瞎。另外分页参数(Page)和普通业务参数混合时,普通参数最好总是带@Param,这样你才能保证 XML 里的名字稳定、可维护。

6. 终极避坑:从参数名到 SQL 注入的边界

关于${}和#{},我单独拿出来强调一下。

#{}在参数获取层面可以说人人都会,但很多人对${}完全没有安全意识。需要明确一点:无论你用什么方式传参,只要你在 SQL 里用${}进行字符串拼接,它本质上就是拿你的参数值直接拼 SQL 片段。如果这个值来自前端且经过了用户输入,那SQL注入风险就是 100% 的。

举个例子,如果你按我上面selectByCondition的思路加一个排序功能,结果写成:

<select id="selectByCondition" resultType="com.example.User"> select * from user <where> ... </where> order by ${orderByCol} ${orderByDir} </select>

此时如果orderByCol的值是name; drop table user; --,那整个 SQL 就变成了灾难。要防这种问题又不想写复杂校验,一个万能解法是白名单映射:

<select id="selectByCondition" resultType="com.example.User"> select * from user <where> ... </where> order by <choose> <when test="orderByCol == 'name'">name</when> <when test="orderByCol == 'age'">age</when> <when test="orderByCol == 'createTime'">create_time</when> <otherwise>id</otherwise> </choose> <choose> <when test="orderByDir == 'desc'">desc</when> <otherwise>asc</otherwise> </choose> </select>

这样即使底层用的是${}(白名单内所以必然是安全值),也能保证注入不了。这种模式才是正确姿势。

还有人问,如果前端传来的值是1 OR 1=1,用#{}查询会怎样?因为你用了预编译占位符,数据库收到的 SQL 是where id = ?,参数值1 OR 1=1被当成一个完整的字符串(或根据类型处理)绑定进去,不会破坏 SQL 结构。这就是为什么只要能用#{}的地方,绝对不去碰${}。

最后再补充一下刚才热词里提到的mybatis log easyplus和“打印可执行 SQL”。在排查参数获取问题时,能直接看到实际执行的 SQL 是最省力的。你如果用了MyBatis-Plus,它的SQL日志默认只打印带?的预编译语句,参数值是单独一行的。有时候你以为参数名或值传错了,看了日志才知道其实是类型转换的问题(比如字符串传进去没加引号、日期格式不对导致查不到数据)。用一些插件可以直接把参数值拼进 SQL 打印出来,方便是方便,但生产环境记得关掉,否则敏感 SQL 会全量打到日志里。

参数获取这个问题,说小了就是一个#{}语法的事,说大了就是理解 MyBatis 运行机制的一把钥匙。把上面这些场景全部吃透,再配合源码阅读,我相信你至少能在 MyBatis 这块少掉不少头发。根据我个人经验,写 Mapper 层时“先想清楚参数结构,再动手写 SQL”是最省钱省力的方式,千万别图一时方便——不然等代码上线出了诡异问题,一个一个排查才是最痛苦的。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询