给 SmartMVC 打个广告:Controller 终于不用兼职当包装工了

郭胜凯2026/08/17推荐SmartMVCSpring MVC

今天占用 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(...),也不用开会讨论这次到底叫 dataresult 还是 payload。命名当然很重要,但同一个问题讨论到第三次以后,它主要是在为会议室创造价值。

SmartMVC 也知道什么不该包装。ApiResponsebyte[]、Spring ResourceStreamingResponseBodyProblemDetail 会保持原样。毕竟下载文件外面再套一层 JSON,属于形式上非常统一,功能上非常打不开。

第二项业务:让异常体面地离场

业务失败不可怕。可怕的是同一种失败,在 A 接口返回 400,在 B 接口返回 200 加一句“失败”,到了 C 接口则直接把堆栈送给前端,让浏览器也参与后端建设。

SmartMVC 把常见 MVC 异常、业务异常和 Bean Validation 校验错误整理成统一结构。应用可以用明确的异常表达“参数不对”“没有登录”“没有权限”“资源不存在”,客户端也可以按稳定的错误码和字段详情处理。

对于新 API,建议让 HTTP 状态码认真履行 HTTP 状态码的岗位职责。业务失败不必永远返回 200 OK。系统已经失败了,就不要再要求状态码保持积极乐观。

第三项业务:让时间停止自由发挥

时间问题有一种特殊魅力:代码在你电脑上是对的,到了服务器上也能运行,只是日期悄悄去了另一个时区。

SmartMVC 统一处理 LocalDateLocalDateTimeInstantOffsetDateTimeZonedDateTimeDate,格式与时区都放在 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 状态有统一规则;
  • 想集中管理日期时间与请求日志;
  • 需要可替换、边界清楚的认证授权能力;
  • 不想为了获得这些能力,把业务代码托付给一套庞大的家族谱。

如果你的项目正准备新建一套 ResultGlobalExceptionHandler、时间格式化器、请求拦截器和权限注解,可以先停一下。不是不让写,是建议先看看别人已经一本正经地写完的版本。

完整文档:SmartMVC 中文文档open in new window

源码仓库:onlyGuo/smart-mvcopen in new window

欢迎试用。使用中遇到问题请提交 Issue,最好附上复现步骤、配置和异常信息。只写“有问题,速修”也不是不行,只是它会自动进入玄学分析流程。