
作为一个整天跟 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。参数是对象增删改查涉及多字段直接传POJOSQL里用#{属性名}。集合/数组参数foreach遍历根据是List、Set还是数组框架对默认名称的处理不太一样这个坑很多。记住这个粗略的原则你写接口的时候就不会太纠结。接下来我一个个场景过最后再带你到源码里看看为什么会出现param1这种玩意儿。2. 核心细节解析与实操要点2.1 单个参数最简单也最容易让人得意忘形先看最舒服的场景。假设有个需求要根据用户id查一条记录接口定义如下User selectUserById(Integer id);XML 这样写select idselectUserById resultTypecom.example.User select * from user where id #{id} /select注意你的接口参数名是idSQL里用的#{id}能跑通。但你把这个id改成#{userId}甚至#{abc}一样能跑通。这就是单个简单类型参数的情况MyBatis 对参数名不敏感因为只有一个参数值它直接把这个值取出来用#{}里面写什么名字只起占位作用。如果是单个 POJO 参数呢比如int insertUser(User user);SQL 里就得老老实实用属性名了insert idinsertUser 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 idselectUserByNameAndAge resultTypecom.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 idselectUserByNameAndAge resultTypecom.example.User select * from user where name #{param1} and age #{param2} /select第二种加 Param 注解推荐User selectUserByNameAndAge(Param(name) String name, Param(age) Integer age);select idselectUserByNameAndAge resultTypecom.example.User select * from user where name #{name} and age #{age} /select这一点真的很重要只要超过一个参数而且你没加任何注解那就别在#{}里写你方法定义时的参数名了大概率会报错。从代码可读性角度来看param1、param2这种写法简直是灾难你过两周回来看代码鬼知道param1是name还是age。2.3 Map 参数万金油式的传参但别滥用当参数实在太多又不想为某个查询单独建一个POJO时Map是一个快速方案。接口定义ListUser selectUserByCondition(MapString, Object params);调用方式MapString, Object params new HashMap(); params.put(name, 张三); params.put(age, 20); ListUser users userMapper.selectUserByCondition(params);XMLselect idselectUserByCondition resultTypecom.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直接把它作为参数是最优雅的ListUser selectUserList(UserQuery query);select idselectUserList resultTypecom.example.User select * from user where if testname ! null and name ! and name like concat(%, #{name}, %) /if if testage ! 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的使用很简单在方法参数前加上注解给参数起个名字即可。它解决的问题是多个参数时框架无法获取参数名的问题。ListUser selectUserByIdsAndStatus( Param(ids) ListLong ids, Param(status) Integer status );XMLselect idselectUserByIdsAndStatus resultTypecom.example.User select * from user where status #{status} and id in foreach collectionids itemid open( separator, close) #{id} /foreach /select这里注意一个很隐蔽的坑当只有一个集合或数组参数时不加Param也能在foreach里用collectionlist或collectionarray访问但如果你这个集合混在其他参数里像上面这个例子那就必须给集合参数加Param并且foreach的collection属性必须跟注解名一致否则你只能用param1这种名字非常容易想不起来。那到底什么时候该用Param方法有多个参数且不便合并成一个对象。需要在SQL中多次引用同一个参数。参数包含集合如List、Set需要配合foreach遍历。需要在动态SQL中判断参数是否为空if test里要用名字。当然也有不少人习惯了在单参数 POJO 上也加Param这纯粹是个人风格问题不影响功能有时为了避免将来加参数引起连锁改动也是一种防御性写法。2.6 集合与数组参数的特殊规则List、Set、Array 的区别如果把一个List直接作为唯一参数传给Mapper方法ListUser selectUserByIds(ListLong ids);XML里的foreach这么写是可以的select idselectUserByIds resultTypecom.example.User select * from user where id in foreach collectionlist itemid open( separator, close) #{id} /foreach /select注意这里用的是collectionlist因为MyBatis对于List类型默认会额外存一个 key 叫list实际上ParamNameResolver源码里会把它包装为collection和list但这里是List直接传入的情况。如果参数是Set默认 key 是collection。如果是一个数组Long[] ids那collection得写成arrayforeach collectionarray itemid ...还有更细的如果你希望SQL可读性更好最稳的还是注解指定名字ListUser selectUserByIds(Param(ids) Long[] ids);foreach collectionids itemid ...只要加了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 ListUser selectByNameAndStatus(Param(name) String name, Param(status) Integer status); // 场景三Map 参数演示用不推荐用于核心场景 ListUser selectByMap(MapString, Object params); // 场景四POJO 参数支持动态 SQL ListUser selectByCondition(UserQuery query); // 场景五集合参数使用 foreach加了 Param 的情况 ListUser selectByIds(Param(ids) ListLong ids); // 场景六数组参数不加 Param 演示 ListUser 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 version1.0 encodingUTF-8? !DOCTYPE mapper PUBLIC -//mybatis.org//DTD Mapper 3.0//EN http://mybatis.org/dtd/mybatis-3-mapper.dtd mapper namespacecom.example.mapper.UserMapper sql idbaseColumns id, name, age, email, status /sql select idselectById resultTypecom.example.User select include refidbaseColumns/ from user where id #{id} /select select idselectByNameAndStatus resultTypecom.example.User select include refidbaseColumns/ from user where name #{name} and status #{status} /select select idselectByMap resultTypecom.example.User select include refidbaseColumns/ from user where name #{name} and age #{age} /select select idselectByCondition resultTypecom.example.User select include refidbaseColumns/ from user where if testname ! null and name ! and name like concat(%, #{name}, %) /if if testage ! null and age #{age} /if if teststatus ! null and status #{status} /if /where /select select idselectByIds resultTypecom.example.User select include refidbaseColumns/ from user where id in foreach collectionids itemid open( separator, close) #{id} /foreach /select select idselectByIdArray resultTypecom.example.User select include refidbaseColumns/ from user where id in foreach collectionarray itemid 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 MapString, Object param new ParamMap(); int i 0; for (Map.EntryInteger, 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层时的铁律分享给你接口方法参数超过三个建议用 POJO 封装比如查询条件类、更新DTO既清晰又方便以后加字段。参数数量在 2~3 个且不太可能再变时可以用 Param但所有参数名必须语义化禁止用param1这种。比如Param(name)、Param(age)。有集合参数要遍历时必须给集合参数加 Param 并起一个复数名字如ids不要在 XML 里依赖list、array这种默认 key。#{}和${}的区别要刻在脑子里#{}是预编译占位符自动加引号防注入${}是字符串拼接只能用于表名、列名、排序字段等结构位置并且务必做白名单校验。参数获取本质上都能拿到值但安全性天差地别。在 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 里就不要出现任何#{}占位符。第五个细节如果你用注解方式写 SQLSelect、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()); PageUser page userService.pageUsers(query, queryDTO.getPageNum(), queryDTO.getPageSize()); return Result.success(page); } }这样的好处是各层解耦前端传参变动时只需要改DTO和query对象Mapper的 SQL 几乎不用动。MyBatis参数获取的问题自然就只集中在Mapper方法签名 XML 这一小块范围里了。5.2 分页插件与多参数组合时的正确姿势熟悉PageHelper的老铁应该遇到过这种问题方法参数里既有分页对象又有查询条件怎么传才能让插件正确分页同时 SQL 还能正确取参典型写法IPageUser selectUserPage(Page? page, Param(query) UserQuery query);这里用MyBatis-Plus的IPage做分页Page是它的实现类Page参数会被分页插件识别不参与业务参数的命名。query对象则通过Param(query)指定名称。XML 里这样取select idselectUserPage resultTypecom.example.User select include refidbaseColumns/ from user where if testquery.name ! null and query.name ! and name like concat(%, #{query.name}, %) /if if testquery.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 idselectByCondition resultTypecom.example.User select * from user where ... /where order by ${orderByCol} ${orderByDir} /select此时如果orderByCol的值是name; drop table user; --那整个 SQL 就变成了灾难。要防这种问题又不想写复杂校验一个万能解法是白名单映射select idselectByCondition resultTypecom.example.User select * from user where ... /where order by choose when testorderByCol namename/when when testorderByCol ageage/when when testorderByCol createTimecreate_time/when otherwiseid/otherwise /choose choose when testorderByDir descdesc/when otherwiseasc/otherwise /choose /select这样即使底层用的是${}白名单内所以必然是安全值也能保证注入不了。这种模式才是正确姿势。还有人问如果前端传来的值是1 OR 11用#{}查询会怎样因为你用了预编译占位符数据库收到的 SQL 是where id ?参数值1 OR 11被当成一个完整的字符串或根据类型处理绑定进去不会破坏 SQL 结构。这就是为什么只要能用#{}的地方绝对不去碰${}。最后再补充一下刚才热词里提到的mybatis log easyplus和“打印可执行 SQL”。在排查参数获取问题时能直接看到实际执行的 SQL 是最省力的。你如果用了MyBatis-Plus它的SQL日志默认只打印带?的预编译语句参数值是单独一行的。有时候你以为参数名或值传错了看了日志才知道其实是类型转换的问题比如字符串传进去没加引号、日期格式不对导致查不到数据。用一些插件可以直接把参数值拼进 SQL 打印出来方便是方便但生产环境记得关掉否则敏感 SQL 会全量打到日志里。参数获取这个问题说小了就是一个#{}语法的事说大了就是理解 MyBatis 运行机制的一把钥匙。把上面这些场景全部吃透再配合源码阅读我相信你至少能在 MyBatis 这块少掉不少头发。根据我个人经验写 Mapper 层时“先想清楚参数结构再动手写 SQL”是最省钱省力的方式千万别图一时方便——不然等代码上线出了诡异问题一个一个排查才是最痛苦的。