前几天在帮一个朋友排查他刚上线的JavaWeb项目,页面多了一个筛选条件,结果从前端到后端一路改下来,愣是把Servlet、Service、DAO三层各改了七八处,最后还在一个参数名拼写上卡了两个小时。这种场景我太熟悉了——很多JavaWeb项目,尤其是跟着视频教程或者网上笔记一步步敲出来的项目,往往在“DAO层和Servlet层参数传递”这件事上,从第一个接口开始就埋下了雷。
这篇文章我不讲空泛的架构理论,只讲怎么传参。严格来说,Servlet和DAO之间还隔着一层Service,但实际开发中我们讨论的“参数传递”,指的是请求参数从Servlet进来之后,怎么安全、清晰、可维护地一路传到DAO层去执行SQL。我会把入参、出参、分页、MyBatis映射这些高频场景全部拆开讲,配合代码示例和踩坑经验。适合刚学完JavaWeb基础、准备做完整项目的新人,也适合被祖传代码里无穷无尽Map传参折磨的开发者。
1. 从一次真实的传参翻车现场说起
1.1 灾难现场:一个需求改了六处文件
那个项目的原始写法是这样的:Servlet里直接把自己的请求参数一股脑塞进一个HashMap,然后调用Service方法,Service再把这个Map原封不动传给DAO,MyBatis里通过#{map.key}取值。
// Servlet层 Map<String, Object> params = new HashMap<>(); params.put("title", req.getParameter("title")); params.put("categoryId", Integer.parseInt(req.getParameter("categoryId"))); params.put("status", req.getParameter("status")); List<Map<String, Object>> list = articleService.pageQuery(params);需求只是“增加一个时间范围筛选”,我那个朋友的做法是往上加两个put,然后去改SQL。听着不费劲,问题出在第二周——页面的表头要改字段名,前端传过来的参数叫createTimeBegin,Map里存的key写成了createBeginTime,MyBatis里用的是createTimeBegin。三个地方三个写法,SQL跑起来不报错,查出来的却是null值的比对,数据直接空了。
这种问题在Java这种强类型语言里其实特别讽刺:你IDE里编译不会报任何错,它是运行到SQL那一层才安静地返回一个错误结果。你没法用“编译期检查”帮你兜底,因为Map的key和value全是String/Object。
1.2 为什么传参姿势会成为项目的“隐形技术债”
我不止一次见过团队里因为传参风格不统一,最后谁也看不懂谁的代码。有人用Map,有人直接传实体,有人一个方法怼上九个参数,还有人图省事把HttpServletRequest往下传。
HttpServletRequest往下传这个事必须重点说。Servlet规范里它本质上是容器给的一个HTTP请求上下文对象,里面带着请求头、请求参数、session、cookie,甚至还有你上传的文件流。把它当作普通参数传到Service再传到DAO,三层全部跟Web容器耦合了。你写单元测试的时候必须Mock一个复杂的request对象,而你压根没法在纯逻辑层去验证自己的业务代码。更危险的是session和请求作用域的数据在分层里被任意读取,一旦后续并发上来,这种“隐式依赖”会放大成非常诡异的问题。
判断标准很简单:改一个筛选条件、一个查询字段,你需要动几层?如果每次都要从Servlet改到SQL,那传参姿势大概率是错了。
2. 先把分层职责钉死:谁该接收什么参数
2.1 Servlet、Service、DAO各自的边界
很多新人把Servlet当“起点”和“终点”,觉得它就是把页面参数拿过来,调一下DAO,然后把返回数据塞到request里转发给JSP。这样写确实能跑,但三层边界就全糊了。
- Servlet层:只做三件事——解析HTTP参数、组装调用Service需要的参数对象、处理Service抛出的异常并决定响应方式。它不应该负责计算、不应该直接拼接SQL参数、不应该写JDBC。
- Service层:业务规则的编排者。它接收Servlet传过来的参数对象,做业务校验,控制事务边界,然后调用DAO。它关心的是“这个业务动作在什么条件下合法、涉及哪些数据”。
- DAO层:数据访问的最小单元。它只接收明确的数据查询/写入条件,只负责执行SQL、把结果映射成Java对象。它完全不关心参数是哪个页面传来的。
我习惯用一个比喻:Servlet是前台接待,Service是业务经理,DAO是仓库管理员。前台把客人填好的单子交给经理,经理审核后把具体的取货备货指令下达给仓库。如果前台直接冲到仓库里报一串口头描述,仓库可能听懂了,但整个流程的规则、校验、事务全乱了。
2.2 参数对象应该在哪里定义
这是最常见的一个选择困难。我的做法是专门建一个param或者query包,里面按业务模块放参数对象。PageQuery这一类的通用分页基类放在公共模块,具体的ArticleQuery放在对应业务模块的param包里。
com.example.project ├── controller # 近似于Servlet层,如果用了SpringMVC则在此 ├── service ├── dao ├── entity # 数据库实体,跟表结构一一对应 ├── vo # 给前端展示用的聚合对象 └── param # 跨层传递的查询/操作参数对象为什么不放在entity里?因为entity是数据库表的映射,你往entity里塞“pageNum”“pageSize”“排序字段”这类非业务属性,实体就越来越脏。以前见过有人直接在User实体内加一个String keyword用于模糊搜索,加一个int start用于分页,表结构里根本没有这些字段,每次查出来一个带了两个空字段的User,不伦不类。
参数对象独立出来还有一个好处:可以给不同的方法设计不同的参数形状。查询列表用ArticleQuery,新增/编辑用ArticleDTO,批量操作可以直接用List<Long>。每层的每个方法签名都清晰明确,编译期类型检查就能拦掉一大半低级错误。
2.3 依赖方向的铁律:只允许上层依赖下层的接口
Servlet只依赖Service接口,Service只依赖DAO接口。参数传递也遵循这个方向:Servlet产生的参数对象传给Service,Service可能补充、转换后再传给DAO。
反例就是Servlet直接new一个DAO对象,然后在Servlet里写事务控制。这样确实“能跑”,但一旦Service里要同时操作两张表、要做一个更新一个插入,你会发现事务根本不知道该在哪开。很多人后来被迫在Servlet里写Connection管理,越写越痛苦,根源就是一开始把分层的传参路径破坏了。
3. 入参设计的正确姿势:查询用Query对象,写操作用实体/命令对象
3.1 查询场景:设计一个带分页能力的Query基类
列表查询是所有JavaWeb项目里最烦人的传参场景。我见过一个最夸张的方法签名长这样:
List<Article> findArticles(String title, Integer categoryId, Integer status, Integer pageNum, Integer pageSize, String orderBy, String orderType)调用的人根本记不清第几个参数是status,第几个是pageSize。Java没有默认参数,你为了复用这个查询还得传一堆null,看着就难受。
我的方案是把查询条件和分页、排序统一封装成一个Query对象。基类先写好分页和排序相关字段:
public class PageQuery { private Integer pageNum = 1; private Integer pageSize = 10; private String orderBy; // 排序字段,仅允许白名单内的值 private String orderType = "asc"; // 仅允许 asc/desc // getter/setter 省略 }具体业务的查询子类再继承它:
public class ArticleQuery extends PageQuery { private String title; // 标题模糊搜索 private Long categoryId; // 分类精确匹配 private Integer status; // 状态精确匹配 private String createTimeBegin; // 时间范围起始 private String createTimeEnd; // 时间范围截止 // getter/setter 省略 }Servlet层从request里逐个取值,塞进ArticleQuery。Service层接收ArticleQuery,DAO层也接收ArticleQuery。增加筛选条件时,只需在Query类里加一个字段,在SQL里加一个<if>判断,不需要改任何方法签名。
三个层面对这个Query对象的处理方式还各不一样:
- Servlet层负责把字符串参数转成合适类型,做基础格式校验(日期格式不对直接报参数错误)
- Service层负责业务性校验(比如结束时间不能早于开始时间、license权限是否允许查看该状态)
- DAO层只用Query里的属性拼动态SQL,不关心它是哪个页面传的
3.2 单项参数和集合参数:该用Object就用Object,但别滥用
不是所有接口都要封装Query对象。比如通过ID查详情、按ID删除、批量删除,这种参数简单明确。
// 单个主键查询 Article selectById(Long id); // 批量删除 int deleteByIds(List<Long> ids); // 更新状态:明确传参,不用对象包装 int updateStatus(Long id, Integer status);这里有一个原则:参数少(两三个以内)、语义明确时,直接传单个参数即可;参数多、语义复杂、需要频繁变化时,才封装对象。但反方向要提醒一句——哪怕只有一个ID,也别用Map。selectById(Map<String, Object> params)这种写法等于告诉未来维护的人:你猜猜我要用哪个key。
我在代码里见过有人这样写:
Map<String, Object> param = new HashMap<>(); param.put("articleId", articleId); articleDao.selectByMap(param);然后DAO那边用的key是id。又是个运行时才爆炸的暗雷。明确类型、明确参数名,才是Java这门语言给你的最大保护。
3.3 写操作场景:用DTO还是用实体类?
插入和更新传参也值得单说。最简单的CRUD项目里,新增文章时前端传来的字段和表字段可能完全一样,很多人就直接用Article实体接收。这个在极简单的表结构下没问题,但表结构总会长大的。从第6个字段开始,前端需要的“提交数据结构”和“数据库表结构”就会产生差异。
我的建议是:简单的单表CRUD可以直接用实体类接收,方便省事;一旦出现以下信号,立刻拆出独立的DTO类:
- 表里有
createdAt、updatedAt这类前端不传的字段 - 前端提交的数据需要经过组合、转换才能成为实体(比如前端传了密码和确认密码,实体里只需要加密后的密码)
- 表是主子表结构,一次提交要同时插入多张表
// 前端提交数据的接收对象 public class ArticleDTO { private String title; private Long categoryId; private String summary; private String content; private List<Long> tagIds; // 文章和标签是多对多,实体里装不下 }Service层拿到DTO后,才负责把它转换成DAO需要的实体对象或参数对象。DTO和Entity的转换逻辑固定放在Service层,谁也不要越界。
4. 返回值设计:那个“直接返回实体给前端”的习惯要改改
4.1 Entity、DTO、VO到底怎么分
传参不是只有“传进去”,还有“传出来”。返回值的混乱程度往往比入参还严重。
按照我从实际项目里总结的经验,这三类的定位是互不重叠的:
- Entity(实体类):和数据库表字段一一对应,只存在于DAO层和Service层内部。它的使命是完整表达一次数据库查询出来的原始数据。
- DTO(数据传输对象):跨层传输业务数据的载体,比如Service层返回给Controller的“业务结果”。
- VO(视图对象):给前端页面渲染使用的对象,字段名、层级、展示格式完全按照前端的需要来设计。
一个典型的反例是把Entity直接序列化成JSON返回给前端,然后前端发现用户列表里多了个password字段。即使你把password字段置成null,它仍然会在JSON里出现,前端开发看到这个字段心里都会咯噔一下。更实际的问题是——多表联查时你怎么把文章表的字段、分类表的名称、作者的昵称揉到一个Entity里?往Article里硬塞一个categoryName字段、一个authorName字段?表里没有,看着就别扭。
我建议所有返回前端的对象一律用VO。哪怕现在VO和Entity长得一样,也要拆个空壳子出来。等表结构一调整,或者前端要新增一个聚合字段,你就能体会到这个空壳子的价值了。
4.2 分页结果的统一封装
分页查询的返回其实是个固定套路:数据列表 + 总条数。我见过有人用两个返回值传,见过有人塞到request attribute里,见过有人塞进Map。最省心的写法是定义一个泛型的PageResult<T>:
public class PageResult<T> { private Long total; // 总条数 private List<T> rows; // 当前页数据列表 public PageResult(Long total, List<T> rows) { this.total = total; this.rows = rows; } // getter/setter 省略 }Service层的方法就统一返回PageResult<ArticleVO>,调用方只关心total和rows,不用管分页计算细节。页面拿到这个固定结构后,前端不管是渲染分页器还是table,逻辑都一样。
只有数据列表没有总条数,这是另一个高频翻车点。很多人列表查询只返回List<Article>,前端要算“共多少页”就没法算了。要么再写一个count方法单独查,要么查一次全表list然后内存里size取一下——前者多一次查询,后者数据量大时直接内存溢出。PageResult顺手把这个问题也解决了:DAO层用同一个Query查一次list、查一次count,组装成PageResult返回。
4.3 到底是返回VO还是返回Map?
偷懒的时候用Map返回确实快:Map<String, Object>里放几个字段,直接转JSON。这在接口联调阶段很爽,但到了维护阶段你要给每个字段写注释才能说清楚这个Map里有什么。
我自己是这么规定的:Map只作为临时返回结构,比如给下拉框渲染用的一次性接口,或者聚合统计结果。业务数据的列表、详情、分页,全部强制使用VO类。类型安全的价值,在于任何一个字典key拼错了,IDE都能在编译期给你红线。这个红线是Map永远给不了你的。
5. MyBatis映射与动态SQL中的传参细节
5.1 单个参数、@Param、Query对象:三种写法的适用范围
既然标题里提到DAO层,那MyBatis怎么接收这些参数就不可避免。就算你不打算把项目搭得那么重,只用一个基于JDBC的工具类,传参的核心思路也是一样的。
用MyBatis时,有一个最常见的报错长这样:
org.apache.ibatis.binding.BindingException: Parameter 'title' not found. Available parameters are [arg0, param1]原因就是Mapper接口里的方法传了两个参数,XML里写的#{title},但MyBatis压根不知道title这个名字从哪来。
解决办法很简单:方法签名上有多个参数时,必须用@Param给每个参数起名:
List<Article> search(@Param("title") String title, @Param("categoryId") Long categoryId, @Param("status") Integer status);<select id="search" resultType="com.example.entity.Article"> SELECT * FROM article WHERE 1=1 <if test="title != null and title != ''"> AND title LIKE CONCAT('%', #{title}, '%') </if> <if test="categoryId != null"> AND category_id = #{categoryId} </if> <if test="status != null"> AND status = #{status} </if> </select>如果是用Query对象传参,XML里的写法是直接取属性名:
List<Article> pageQuery(ArticleQuery query);<select id="pageQuery" parameterType="com.example.param.ArticleQuery" resultType="com.example.entity.Article"> SELECT * FROM article <where> <if test="title != null and title != ''"> AND title LIKE CONCAT('%', #{title}, '%') </if> <if test="categoryId != null"> AND category_id = #{categoryId} </if> <if test="status != null"> AND status = #{status} </if> <if test="createTimeBegin != null and createTimeBegin != ''"> AND create_time >= #{createTimeBegin} </if> <if test="createTimeEnd != null and createTimeEnd != ''"> AND create_time <= #{createTimeEnd} </if> </where> ORDER BY ${query.orderBy} ${query.orderType} LIMIT #{query.offset}, #{query.pageSize} </select>注意XML里取Query对象时我写的是#{offset}而不是#{pageSize}——因为PageQuery基类里我定义了一个getOffset()方法用来计算MySQL里LIMIT的起始位置。这个细节很多人会忘:前端传的是pageNum=2、pageSize=10,SQL里要的是offset=10。你可以直接在XML里写LIMIT #{pageSize} * (#{pageNum} - 1),但可读性差一些。我更倾向于在PageQuery里提供offset的getter:
public Integer getOffset() { return (pageNum - 1) * pageSize; }5.2 用#{}不要用${},除非你明确知道后果
传参进SQL有两条路:#{}是预编译占位符,提交给数据库的是参数化的问号占位,能有效防止SQL注入;${}是字符串拼接,直接把值拼进SQL。新手写完${title}发现确实能查出来数据,就把所有地方都写成${},风险非常大。
必须用${}的场景只有两个:一是排序字段名,二是表名(比如分库分表时动态拼接表名)。这两个场景因为没法预编译,所以要做白名单校验。
// Service层校验排序字段 private static final Set<String> ORDER_COLUMNS = new HashSet<>(Arrays.asList( "create_time", "update_time", "view_count", "title" )); if (!ORDER_COLUMNS.contains(query.getOrderBy())) { throw new IllegalArgumentException("非法排序字段"); }就算你校验过了,XML里也别写得太野,orderBy和orderType分开传,orderType只允许asc或desc,然后在Service里兜底一下:
if (!"asc".equals(query.getOrderType()) && !"desc".equals(query.getOrderType())) { query.setOrderType("asc"); }5.3 时间范围、模糊搜索、in集合:三个高频动态SQL写法
写过滤条件时,新人最容易栽的坑有三个。
时间范围:数据库里存的是datetime,前端传的是字符串。如果用的MySQL,直接字符串比较create_time >= #{createTimeBegin}通常没问题,因为ISO格式的日期字符串可以字典序比较。但要小心前后端传的是2025-03-01 00:00:00还是2025-03-01,格式不一致时会漏数据。稳妥做法是在Servlet或Service层把时间字符串统一格式化成yyyy-MM-dd HH:mm:ss,再传给Query对象。
模糊搜索:LIKE CONCAT('%', #{title}, '%')是推荐写法。直接写LIKE '%#{title}%'会报错,因为#{}会被当成占位符,但字符串里又不能有问号;写LIKE %${title}%又有注入风险。用CONCAT函数把百分号和参数拼起来,是最安全的。
in集合:批量删除、按多个ID查询时,MyBatis的foreach是必须掌握的:
<delete id="deleteByIds"> DELETE FROM article WHERE id IN <foreach collection="ids" item="id" open="(" separator="," close=")"> #{id} </foreach> </delete>Mapper接口直接写int deleteByIds(List<Long> ids),MyBatis会把这些元素展开成逗号分隔的占位符。注意collection名称在没加@Param时,单集合参数默认叫list或collection,如果XML里写了ids就又要开始排查了。
5.4 结果映射resultMap:多表联查时字段名对不上的解决办法
项目走到联表查询这一步,返回的字段往往来自多张表。比如查文章列表时要同时显示分类名、作者名、标签。用一个扁平VO接收时:
public class ArticleVO { private Long id; private String title; private String categoryName; // 来自category表 private String authorName; // 来自user表 private String tagNames; // 多个标签名拼接 }SQL里用别名对齐:
SELECT a.id, a.title, c.name AS categoryName, u.username AS authorName, GROUP_CONCAT(t.name) AS tagNames FROM article a LEFT JOIN category c ON a.category_id = c.id LEFT JOIN user u ON a.author_id = u.id LEFT JOIN article_tag at ON a.id = at.article_id LEFT JOIN tag t ON at.tag_id = t.id WHERE a.status = #{status} GROUP BY a.id, a.title, c.name, u.username这种情况下,resultType直接写成com.example.vo.ArticleVO,MyBatis会自动把categoryName这个别名对应到VO的categoryName字段上。前提是SQL别名和VO字段名一致。
如果你更习惯用resultMap,可以显式映射:
<resultMap id="ArticleVOMap" type="com.example.vo.ArticleVO"> <id property="id" column="id"/> <result property="title" column="title"/> <result property="categoryName" column="category_name"/> <result property="authorName" column="author_name"/> </resultMap>我个人的习惯是:查询简单、别名和VO字段名能对上时,直接用resultType;一旦字段特别多,或者遇到下划线转驼峰映射不全的情况,用resultMap更稳。项目全局打开mapUnderscoreToCamelCase配置能让下划线自动映射驼峰,但新人有隐式认知门槛,一旦忘了配置又会出现大量字段为null的诡异情况。
6. 排查清单:参数传着传着就“丢了”,多半是这几件事
6.1 null值翻车:Integer和int选错就出大问题
实体里用Integer还是int,不是个人喜好问题,是null语义问题。SQL查询条件里如果传一个null和一个空字符串,含义完全不同:status = ''查不到任何数据,而status IS NULL是另一套语义。
很多时候页面上“全部状态”的下拉框,预期是不传status,直接查所有状态。结果前端传了个空字符串过来,Servlet里一转换变成0,恰好数据库里不存在status=0的数据,页面直接空白。排查方法是在Servlet层打印日志:
log.info("查询参数:[title = {}, categoryId = {}, status = {}, pageNum = {}]", query.getTitle(), query.getCategoryId(), query.getStatus(), query.getPageNum());一条日志打出来,是null、是空串、还是0,一目了然。别小看这个习惯,很多“数据莫名其妙少了几条”的问题,最后都是null或者0的锅。
还有个经典的坑:实体类的int类型字段,从数据库查出null时会转成0。如果你前端要判断这个字段“是否设置过”,0和null在语义上可能完全不一样。所以所有可能为null的数据库字段,Java端一定用Integer而不是int。
6.2 参数名对不上:MyBatis报错往往很晚
#{}里的属性名和Query类里的字段名不一致,MyBatis在启动时不报错,要等到实际调用这个方法、进入SQL解析时才报错。这里我分享一个排查思路:不管报错信息显示Available parameters是arg0还是param1,先去看Mapper接口的方法签名是否和XML的id严格对应,再去看XML里取参数的名字是否和Query里getter对应的属性名一致。
IDEA里可以这么干:在Mapper接口方法上按Ctrl/Command加鼠标左键跳转到XML,能跳过去就说明Mapper绑定没问题;然后点进Query类看看属性名,字段名千万别有“多一个字母”或者“大小写对不上”的情况。比如Query里有createTimeBegin,XML里写createTimeBegin,看着没错,但如果Query类实际写的是create_time_begin(被某些工具自动转成了下划线风格)就挂了。
6.3 IDEA运行配置的小坑:改了代码,页面还是老样子
排查传参问题的时候,还有一个环境级别的坑,特别容易把人带偏。原来我用IDEA跑JavaWeb项目的时候,经常遇到这种情况:改了Service层的某个参数处理逻辑,重启了Tomcat,但浏览器刷新完全没反应,断点也进不去,看起来就像代码没改一样。
这多半是IDEA没有触发编译或者部署的out目录没更新。我的固定配置习惯是:
- 使用war exploded方式部署,不要用war包,这样IDEA可以直接把修改后的class文件同步到Tomcat的部署目录,省去整个重新打包的过程
- 在IDEA的Build菜单下勾选Build automatically,配合JRebel或者DevTools(看框架,如果是纯Servlet就用JRebel),实现热更新
- 如果改了Mapper的XML文件,纯Servlet项目通常需要重启才能重新加载XML,因为XML不在classpath的改动范围内
排查顺序我建议是:先看日志有没有最新的启动记录或者异常堆栈,再看请求有没有到达Servlet(打个断点或者加日志),再看参数有没有正确封装,最后才去看SQL拼接和结果映射。很多人在前两步都没确认的情况下就去翻SQL,一翻就是一下午。
| 常见问题 | 典型现象 | 解决方向 |
|---|---|---|
| 参数名不一致 | BindingException或字段为null | 对照Mapper接口、XML、Query三处属性名 |
| Integer/int混用 | 查不到数据、空字符串转0 | 数据库可空字段一律用Integer |
| SQL注入风险 | ${}拼接用户输入 | 能用#{}就用#{},排序字段白名单校验 |
| 分页参数丢失 | 前端翻页没反应 | PageQuery基类统一封装pageNum/pageSize |
| IDEA热部署不生效 | 改代码页面不变 | 检查war exploded部署和build自动编译 |
| 返回Mpa给前端 | 字段缺失或拼错key | 改用VO类,让编译期兜底 |
6.4 事务边界和传参的关系:对象在Service层被修改要警惕
最后一个容易被忽略的点:如果要开事务,那Service层方法内部对传入参数对象的修改,会影响后续DAO的执行结果。比如你在Service层里把Query对象的时间格式化了一次,你的DAO层看到的已经是被修改过的对象。这本身不是坏事,但如果这个Query对象被多个DAO方法共享——查了一次list、又查一次count,count的SQL里也用了这个Query,时间格式化就执行了两次,万一格式化的实现不是幂等的,直接会拿到两个不同的查询条件,数据对不上。
我的建议是:Query对象在Service层如果有必要做二次加工,拷贝一份再加工,别在原对象上动手脚。详见下面这个小习惯:
// 不建议:直接改query if (query.getCreateTimeBegin() != null) { query.setCreateTimeBegin(formatTime(query.getCreateTimeBegin())); } // 建议:拷一份再处理 ArticleQuery finalQuery = copyQuery(query); finalQuery.setCreateTimeBegin(formatTime(query.getCreateTimeBegin()));这种细节看起来不起眼,但正是这些“不起眼”让传参链路上少了很多灵异事件。
7. 我自己长期坚持的传参规矩与一个实用小技巧
带过几个项目之后,我把传参这件事总结成了几条硬性规矩,团队新人照着写基本不会跑偏:
- 跨层传参一律使用明确类型的对象,禁止用Map作为常规传参载体
- 查询场景的入参统一继承PageQuery,分页参数不许散落在方法签名里
- 出参给前端的对象一律是VO,禁止Entity直接序列化返回
- Mapper接口的方法参数超过一个时必须加@Param,不许靠arg0、param1去猜
- 动态SQL里能用
#{}绝不用${},排序字段和表名单独做白名单校验 - 每写一个接口,随手在Servlet入口打一行参数日志,排查问题的时间能省一半
这些小规矩本质上不是架构理论,而是让你在项目越滚越大的时候,尽量少被“传参传丢”这种事情打断节奏。我见过太多项目死在写代码的人自己都记不清参数含义的阶段。
最后分享一个小技巧。写完一个接口后,花三十秒从Servlet层开始,一步步在逻辑里把参数往下追:request里来的是什么类型,Query里封的是什么类型,DAO里的方法签名是什么,XML里取的又是什么。任何一个环节的类型对不上、命名不一致,当场就能揪出来。等你把这条链路上所有的参数都追通畅了,再用测试页面测几组边界值(空串、null、超长字符串、特殊字符),这个接口才算真正稳了。
传参这件事,说难不难,说简单也不简单。难的是它贯穿了JavaWeb项目的每一层,任何一层图省事,后面都是连锁反应;简单的是,只要把入参出参的类型和边界卡死,九成以上的“奇奇怪怪问题”根本不会出现。