本节摘要:控制器是 Model2 的交通枢纽:接请求、验参数、调服务、选视图。本节给出一个生产可用的控制器完整样貌,以及青梧书肆改造时沉淀的四包结构与红线清单——分包不是美观问题,而是让"代码放哪"不再需要讨论的团队契约。上一站立了骨架,这一站管施工现场,下一站处理最要害的事务边界。
一个合乎纪律的控制器长什么样,看完整样例最直观:
// 订单控制器:只做四件事——接参、校验、调服务、选视图 public class OrderController extends HttpServlet { private OrderService orderService = new OrderService(); @Override protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { req.setCharacterEncoding("UTF-8"); String bookId = req.getParameter("bookId"); // 参数校验不过:带着错误信息回表单页 if (bookId == null || bookId.isBlank()) { req.setAttribute("error", "请选择图书"); req.getRequestDispatcher("/order/form.jsp").forward(req, resp); return; } try { Order order = orderService.place(bookId, req.getRemoteUser()); req.setAttribute("order", order); // 写操作完成:重定向到只读结果页,防刷新重复提交 resp.sendRedirect(req.getContextPath() + "/order/success?id=" + order.getId()); } catch (BizException e) { req.setAttribute("error", e.getMessage()); req.getRequestDispatcher("/order/fail.jsp").forward(req, resp); } } }
四个动作各有纪律。接参前先定编码,否则中文参数第一步就乱码。校验只做"格式与非空"这类入口级检查,业务规则(比如"同一用户同一本书一天限购三本")归服务层——控制器要瘦,判断越少越好。调服务时用业务异常往外抛失败,控制器捕获后决定去哪,不让异常栈直接糊到用户脸上。选视图则严格执行 2.3 的口诀:展示走转发、写后走重定向。
URL 映射按资源规划,一个资源一个控制器:订单的增删查都进订单控制器,用路径参数区分动作。工程大了之后可以引入"一个前缀一个总控、内部分发"的集中式写法,但对青梧书肆这个规模,分散映射最简单清晰。
控制器多了、服务多了,分包就浮出水面。青梧书肆改造后固定为四个包,职责与红线见表:
| 包 | 住什么 | 红线 |
|---|---|---|
| 控制器包 | 各资源的 Servlet | 不许出现一条查询语句、一个业务规则判断 |
| 服务包 | 业务逻辑、事务边界 | 不许 import 容器接口,保持脱离容器可测 |
| 数据访问包 | 连接管理、增删改查 | 不许出现业务判断,只认参数不认场景 |
| 领域对象包 | 订单、图书等纯数据类 | 不许持有连接,不许含流程逻辑 |
四个包的依赖方向是单向的:控制器调用服务,服务调用数据访问,领域对象被所有层使用但不依赖任何层。画出来就是一张层次分明的剖面图。

背景:改造进行到第三周,评审同事的订单列表控制器时发现:为了拼一个"待发货订单数"的角标,控制器里内联了一条聚合查询。当事人理由很充分——"就一行,单独建服务类太隆重"。
操作:评审会上的处理不是简单打回,而是当场做三个推演:这条 SQL 要改表结构时谁受影响(控制器与数据访问层同时改,破坏单向依赖);手机端将来要同一个角标怎么办(再抄一份);单元测试怎么写(无从下手)。推演完当事人自己把查询搬进了订单仓储类,控制器改调服务方法。
结果:控制器回到四件事的纯净形态;角标查询获得第一个单元测试用例。
解读:"就一行"是分层溃烂的标准起点。红线的意义不在惩罚这一行,而在守住"代码放哪不需要讨论"的状态——一旦例外被接受,第二行、第三行会带着更多理由跟上来。把推演成本讲清楚,比引用规范条文更能说服人。
变式:确实存在不值得建服务类的极小逻辑,比如把状态码翻译成文案。这类纯展示辅助可以放进视图辅助类(本质是给页面用的工具方法),前提是它不碰数据访问。判断标准始终是那条单向依赖:只要不破坏包的依赖方向,灵活度是允许的。
💡 关键直觉:分包规范的最高目标是让新人不需要问"这段代码放哪"。凡是还需要讨论 placement 的团队,说明契约没立住——与其加文档,不如把红线写进评审清单让机器帮忙盯。
四包结构不是第一天就是这个形状,接手者了解演进节奏能少走弯路。第一阶段"单包混住":所有类挤在默认包里,靠命名前缀区分职责——很多老站的历史起点,能不动就先不动,混住本身不产生 bug。第二阶段"按层分包":本章的四包结构,规模到十几个控制器时值得完成,收益是代码位置可预测。第三阶段"按域切分":业务域(订单、会员、库存)优先于技术层,每个域内部再分层——那是规模再上一档后的事,青梧书肆停在第二阶段已经够用。判断何时升级分包结构有个朴素信号:新人问"这段代码放哪"的频率突然升高,说明当前结构已经装不下团队的心智模型了。结构跟着规模走,别为了架构图好看提前付复杂度的账。
瘦控制器里唯一允许"胖"的位置是参数校验,因为它就是入口职责。青梧书肆的控制器校验清单值得抄走:必传项检查(缺参直接带错误回表单,别让空值往下走)、格式检查(数字、日期、枚举值,非法即拒)、长度上限(超长输入在入口截断或拒绝,别等数据库报错)、编码设置(进业务前先定编码)。四项之外的一切判断都往后送。还有一个容易忽略的点:校验失败的用户反馈要指路——"参数错误"是坏提示,"请选择图书后再提交"是好提示。控制器写返回信息时多花十秒想想用户看到什么,客服工单就少一沓。清单贴在控制器包的包注释里,评审时逐条对,新写控制器的人不需要读规范文档就学会了边界。
分层骨架与施工纪律都齐了,还差最要害的一块:下单要同时扣库存、写订单,两件事必须同生共死。下一站把事务边界稳稳落进服务层。