给 SmartMVC 打个广告:Controller 终于不用兼职当包装工了
今天占用 Smart MyBatis 的博客,郑重介绍一下隔壁工位的新同事:SmartMVC。
按照互联网产品的标准开场,它是一款“面向未来、赋能研发、重塑体验”的框架。按照能听懂的说法,它负责把 Spring MVC 项目里那些每个人都写过、每个项目又不太一样的基础工作收拾整齐:统一响应、异常处理、参数校验、日期时间、请求日志和认证授权。
它不负责创造需求,不负责修改排期,也不能在周五下午替你回复“这个改动很简单”。但它能让 Controller 少干一点重复劳动。这已经是一个 Java 库在法律允许范围内能表达的最大诚意。
为什么还要有一个 SmartMVC
一个新项目刚开始时,Controller 通常十分清爽:
@GetMapping("/users/{id}")
public UserView get(@PathVariable Long id) {
return userService.get(id);
}
过了一阵子,它可能会逐渐承担更多社会责任:成功时套一层统一响应,失败时翻译异常,校验失败时整理字段错误,日期按团队规定输出,请求结束后记录耗时,再顺手判断一下当前用户到底有没有资格看到这个接口。
最后,原本只想返回一个用户的 Controller,成长为一名精通包装、翻译、纪检、计时和门卫工作的复合型人才。业务代码不一定增加了,岗位职责肯定丰富了。
SmartMVC 的思路很克制:
该做的,都帮你做好;不该做的,一律不碰。
它是 Spring MVC 的增强层,不要求 Controller 继承某个祖传基类,不接管业务对象,也不替应用决定 Spring Web 和 Bean Validation 的版本。熟悉的 Spring MVC 写法继续保留,重复的基础设施工作则集中到一处。框架负责把桌面收拾干净,但不会趁机替你重新装修办公室。
第一项业务:统一大家对“统一”的理解
很多项目都有统一响应,主要区别是每个项目统一得都不一样。
SmartMVC 会把普通 Controller 返回值自动包装成稳定的 ApiResponse:
{
"success": true,
"code": "OK",
"message": "success",
"data": {
"id": 1001,
"name": "张三"
},
"timestamp": "1786005000000"
}
Controller 仍然只返回业务数据:
@GetMapping("/users/{id}")
public UserView get(@PathVariable Long id) {
return userService.get(id);
}
不需要每个方法手写 ApiResponse.success(...),也不用开会讨论这次到底叫 data、result 还是 payload。命名当然很重要,但同一个问题讨论到第三次以后,它主要是在为会议室创造价值。
SmartMVC 也知道什么不该包装。ApiResponse、byte[]、Spring Resource、StreamingResponseBody 和 ProblemDetail 会保持原样。毕竟下载文件外面再套一层 JSON,属于形式上非常统一,功能上非常打不开。
第二项业务:让异常体面地离场
业务失败不可怕。可怕的是同一种失败,在 A 接口返回 400,在 B 接口返回 200 加一句“失败”,到了 C 接口则直接把堆栈送给前端,让浏览器也参与后端建设。
SmartMVC 把常见 MVC 异常、业务异常和 Bean Validation 校验错误整理成统一结构。应用可以用明确的异常表达“参数不对”“没有登录”“没有权限”“资源不存在”,客户端也可以按稳定的错误码和字段详情处理。
对于新 API,建议让 HTTP 状态码认真履行 HTTP 状态码的岗位职责。业务失败不必永远返回 200 OK。系统已经失败了,就不要再要求状态码保持积极乐观。
第三项业务:让时间停止自由发挥
时间问题有一种特殊魅力:代码在你电脑上是对的,到了服务器上也能运行,只是日期悄悄去了另一个时区。
SmartMVC 统一处理 LocalDate、LocalDateTime、Instant、OffsetDateTime、ZonedDateTime 和 Date,格式与时区都放在 spring.smart.mvc.* 配置下。部署跨时区时,建议明确配置 IANA zone-id:
spring:
smart:
mvc:
date-time:
zone-id: Asia/Shanghai
不要把服务器默认时区当成团队共识。服务器一般不参加需求评审,它有自己的想法。
第四项业务:日志负责还原现场
SmartMVC 可以记录请求摘要、状态码和处理耗时,并使用实际 Controller 作为日志类别。出了问题,你可以按接口和类快速定位,不必在一大片格式各异的日志里进行数字考古。
这不意味着日志应该收集一切。密码、令牌和个人信息仍然不该放进 URL 查询参数;请求日志不是许愿池,往里面扔多少数据都不会自动换来可观测性。
第五项业务:门可以自动,但锁得自己装
SmartMVC 提供 @Auth、@Anonymous、角色、命名权限、METHOD:PATH 请求权限,以及可注入的 CurrentAuth。认证范围可以选择:
ANNOTATED:只保护显式标注的接口,适合渐进接入;GLOBAL:默认保护全部接口,再明确放行匿名入口。
真正的用户识别逻辑由应用实现并替换。Starter 在没有自定义认证实现时使用的是默认放行实现,只适合启动、演示和测试,不是生产环境的隐形保安。
请不要因为接口上出现了 @Auth,就产生门已经锁好的错觉。贴上“闲人免进”不等于安装了门禁,这一点物业和黑客都很清楚。
另外,CurrentAuth 保存的是同步 Servlet 请求范围内的身份。进入 @Async、自定义线程池或异步请求前,应先复制真正需要的不可变身份信息。ThreadLocal 不会因为大家关系不错,就主动跨线程帮你送材料。
接入成本:一个依赖,以及对边界的基本尊重
SmartMVC 1.0 要求 Java 17+、Spring Boot 3.2+,当前构建与测试基线为 Spring Boot 3.5.7。现有应用继续显式提供 Web 与 Bean Validation,再加入 SmartMVC:
<dependency>
<groupId>ink.icoding</groupId>
<artifactId>spring-boot-starter-smart-mvc</artifactId>
<version>1.0.0</version>
</dependency>
配置统一放在 spring.smart.mvc 下,Starter 还提供配置元数据,IDE 可以补全属性、默认值和枚举选项。你依然需要理解配置的含义,但至少不用靠手速猜拼写。
它和 Smart MyBatis 是什么关系
Smart MyBatis 负责让持久层少写重复 CRUD,SmartMVC 负责让 Web 层少写重复基础设施代码。一个在数据库门口维持秩序,一个在 HTTP 门口维持秩序,中间的业务层终于可以专心处理业务——或者继续开会,这取决于贵公司的组织架构。
它们都遵循类似的原则:增强已有框架,不把成熟生态重新发明一遍;能自动处理的重复工作自动处理,需要应用做决定的地方把控制权留给应用。
如果你喜欢 Smart MyBatis 那种“少写一点,但底层仍然认识”的克制,SmartMVC 大概率也合你的口味。它不会承诺让项目从此没有 BUG,只会努力让 BUG 出现时格式统一、状态明确、日志可查。听起来不够颠覆,但通常比颠覆生产环境更受欢迎。
最后,广告时间
SmartMVC 适合这样的 Spring MVC 项目:
- 不想在每个 Controller 里手工包装响应;
- 希望异常、校验和 HTTP 状态有统一规则;
- 想集中管理日期时间与请求日志;
- 需要可替换、边界清楚的认证授权能力;
- 不想为了获得这些能力,把业务代码托付给一套庞大的家族谱。
如果你的项目正准备新建一套 Result、GlobalExceptionHandler、时间格式化器、请求拦截器和权限注解,可以先停一下。不是不让写,是建议先看看别人已经一本正经地写完的版本。
完整文档:SmartMVC 中文文档
源码仓库:onlyGuo/smart-mvc
欢迎试用。使用中遇到问题请提交 Issue,最好附上复现步骤、配置和异常信息。只写“有问题,速修”也不是不行,只是它会自动进入玄学分析流程。
