前言
前阵子,我写了一篇文章:
《为什么 Java 里面,Service 层不直接返回 Result 对象?》
没想到那篇文章讨论度很高。
很多人赞同,也有不少人持反对意见意见:
“既然 Controller 只是转发参数、包装一下 Result,那它存在的意义到底是什么?”
这个问题问得特别好。
因为它刚好触及了很多 Java Web 项目的痛点:
我们一边强调分层,一边又在大量 Controller 里重复写几乎没有业务价值的样板代码。
于是我顺着这个问题,尝试着往前走了一步,做了一个开源框架:
当然不是要推翻 Spring MVC,也从来没想过要否定分层结构,只是想试着解决这个问题:
当 Controller 只剩「声明路由 + 绑定参数 + 调用实现 + 包装响应」这些机械劳动时,能不能把这些事情交给框架,让业务代码真正回到业务本身?
先说结论:真正反对的是职责错位
上一篇文章的核心观点很简单:
- Service 层应该面向业务语义,而不是面向 HTTP 响应结构
Result更适合作为接口层、表现层的响应封装- 业务失败应该优先通过异常和统一机制处理,而不是在 Service 里到处
Result.fail(...) - 业务之间互调时,直接传递领域对象,比一层层拆
Result更自然、更可复用
这背后反映出一个很实际的工程问题:
一旦 Service 和 HTTP 响应格式绑死,业务层就很容易被表现层污染。
可问题来了。
如果我们坚持“Service 不直接返回 Result”,那很多 Controller 最后就会变成这样:
@GetMapping("/user/{id}")
public Result<UserDTO> getUser(@PathVariable Long id) {
UserDTO dto = userService.getUser(id);
return Result.success(dto);
}
很多项目里基本都是这种 Controller,这些代码没有任何问题,但是看久了会觉得有些无聊。
也正是因为这个矛盾,我才做了 ArcRoute。
我在项目初衷里就明确写了:这个框架的起点,正是那篇“Service 层是否应该直接返回 Result”的文章,以及后续围绕 Controller 样板代码的讨论。(传送门跳转)
试图解决的核心问题
ArcRoute 的核心思路,可以概括成一句话:
把没有业务承载价值的 Controller 样板,统统收敛到框架里。
在 ArcRoute 里,HTTP 接口不再必须写在 @Controller / @RestController 里,而是可以定义在接口(interface)上。
比如:
@Api(basePath = "/users", produces = MediaType.APPLICATION_JSON)
public interface UserApi {
@ApiRoute(path = "/{id}", method = ApiHttpMethod.GET)
UserDTO getUser(@Path("id") Long id);
@ApiRoute(path = "", method = ApiHttpMethod.POST, consumes = MediaType.APPLICATION_JSON)
Result<Long> createUser(@Body CreateUserCmd cmd);
}
然后业务实现写在实现类里:
@ApiService(UserApi.class)
@Service
public class UserApiImpl implements UserApi {
@Override
public UserDTO getUser(Long id) {
return userService.findById(id);
}
@Override
public Result<Long> createUser(CreateUserCmd cmd) {
return Result.ok(userService.create(cmd));
}
}
框架会在启动时扫描注解,自动完成路由注册、参数绑定、校验、调用分发等工作。
还是保留了分层思想,但不需要再手写胶水代码。
把接口定义从实现里拆出来
很多人第一次看这种设计,会产生误解:
“这不就是把 Controller 换了个写法吗?”
表面上看,好像是。
但本质上,ArcRoute 做的是两件事:
1. 把接口定义和实现解耦
传统写法里,路由声明、参数注解、实现逻辑都堆在一个类里。
ArcRoute 把这两部分拆开了:
- Interface 负责描述 API 长什么样
- Impl 负责写业务实现
这意味着接口可以更清晰地作为“契约”存在,而不是埋在实现细节里。README 也把这一点列为核心特性之一:接口声明与实现分离,API 定义在 Interface,业务在 Impl。
2. 把机械流程抽成统一调用链
ArcRoute 内置了一条统一调用链:
参数解析 → 校验 → 前置处理 → 业务调用 → 后置处理 → 响应包装。
这意味着,很多原本散落在各个 Controller / Advice / Interceptor 里的重复逻辑,可以被整合成一条清晰、可插拔的管道。
分层还是存在的,仍然严格遵守,但是能显著减少重复劳动。
天然契合
我觉得 ArcRoute 最适合宣传的一点,不是“能少写 Controller”,而是:
它让业务归业务,接口归接口
因为在很多项目里,之所以 Service 最后开始返回 Result,是因为开发者在漫长的代码后,开始嫌麻烦后妥协:
- Controller 只是转发
- 还要额外包装一次
- 还要处理异常
- 还要写参数绑定
- 久而久之,大家就会想:要不干脆 Service 直接把 Result 返回了算了
而 ArcRoute 做的事情,本质上就是把妥协的诱因拿掉。
你不需要为了维持分层,额外写一堆机械代码;框架已经把路由、绑定、校验、调用分发这些动作做掉了。业务实现就安安心心写业务逻辑。
所以我会这么概括它:
上一篇文章是在回答“为什么不该这么写”;ArcRoute 是在回答“那怎样写,才不痛苦”。
不是只做路由
ArcRoute 不只是一个路由扫描器,而是围绕接口层做了一套完整的能力编排。
目前的核心能力包括:动态路由注册、可插拔处理器、局部调用链配置、参数绑定、Bean Validation 支持、统一响应包装,以及原生 Servlet/Spring Web 参数注入。
我觉得这里面有几个点特别适合拿出来讲。
1. 参数绑定更像声明式接口
它支持 @Path、@Query、@Header、@Body、@Part、@Cookie、@Ctx 等参数绑定方式,还支持直接注入 HttpServletRequest、HttpSession、Principal、Locale 这类原生参数。
这让接口定义本身更像一个清晰的契约,而不是一堆 Controller 方法里的杂糅细节。
2. 调用链全程可插拔
它支持 ApiPreProcessor、ApiPostProcessor、ApiExceptionProcessor,还支持用 @ApiPipeline 在接口级或方法级挂载处理器和校验器。
这意味着:
- 登录态校验
- 审计日志
- 统一异常转换
- 通用埋点
- 特定接口的前后置处理
都可以从业务代码里抽出去。
3. 统一包装,但又允许跳过包装
ArcRoute 提供 @WrapResult 自动包装返回结果,也支持通过 @RawResponse 跳过包装。
当然还支持用 @RawResponse 返回 ResponseEntity<Resource> 和 SseEmitter 这样的原生响应。
考虑到不同团队的 Result 结构都不同, ArcRoute 也支持自定义返回的响应体结构。
不吹牛的说,既保持了灵活性,又有统一归口。
ArcRoute 至少从设计上给了两个出口:
- 正常业务接口,走统一包装
- 文件下载、流式响应等特殊场景,走原生响应
这就比较符合真实项目。
低侵入改造
我觉得这个框架还有个优点很值得写一写:
没有另起炉灶。
就像 README 里面写的:
- 支持 Spring Boot 2.7 / 3.x
- 兼容 Java 8 到 Java 21
- 支持 Javax 和 Jakarta 两套 Servlet 命名空间
- 不修改 Spring 原有机制,可与
@RestController共存。
所以老项目不必推倒重来。
完全可以:
- 老接口继续保留原来的
@RestController - 新模块、新子系统、新 API 逐步迁到 ArcRoute
- 先把最烦人的样板区改掉,再慢慢演进
我想,对团队来说,渐进式接入,才更容易真正落地。
它适合什么场景
非要说一些适用场景,那我推荐几个:
第一类:Controller 大量重复、几乎纯转发的项目
这类项目最典型的代码就是:
public Result<?> xxx(...) {
return Result.success(service.xxx(...));
}
几十个、几百个接口,结构都一样。
这种情况下,用 ArcRoute 把重复代码提走,收益会非常明显。
第二类:想坚持分层,但又讨厌样板代码的团队
指那些不想再写几百个一模一样的 Controller 的团队。
ArcRoute 本质上就是给这类团队一个工程化解法。
第三类:需要统一前后置处理能力的项目
比如统一鉴权、参数校验、审计、异常转换、接口级扩展,这类需求一多,传统 Controller 写法很容易散。
ArcRoute 的处理器机制和调用链模型,会更有组织性。
它不适合什么场景
强调一下:ArcRoute 不是银弹,任何技术方案都不是银弹。
它不一定适合这些情况:
1. 项目很小,接口也不多
如果就十来个接口,手写 Controller 完全不是问题,没必要为了优雅再加一层抽象。
2. 团队对接口驱动写法不熟
ArcRoute 的思想不复杂,但它毕竟不是大家最熟悉的 Spring MVC 默认写法,团队需要一点接受成本。
3. 业务接口非常强依赖个性化控制器逻辑
如果你的接口层本身就有大量自定义流程,Controller 不是空心层,那 ArcRoute 的优势会变小。
我为什么做这个框架
说到底,ArcRoute 不是为了整活,也不是为了重新发明 Spring MVC。
只是想把一件我很在意的事,做得再顺畅一点:
业务代码应该专注业务,接口代码应该专注接口,重复劳动应该交给框架。
上一篇文章里,我是在讲一个设计判断:
Service 层不该直接返回 Result。
而这一次,我更想把它推进到工程层面:
如果你真的认同职责分离,那就不该只停留在嘴上,还应该有一套让开发者愿意坚持这件事的工具。
ArcRoute,就是我给出的一个答案。它把 “Keeping Services Focused” 直接写进了仓库描述里,这个定位其实和上一篇文章是一脉相承的。
快速上手
如果你想尝试一下,可以直接用 Starter 依赖:
<dependency>
<groupId>pub.lighting</groupId>
<artifactId>arcroute-spring-boot-starter</artifactId>
<version>0.1.2</version>
</dependency>
如果你正在做 Spring Boot 2.7 / 3.x 项目,又对“Controller 样板太多、Service 职责容易漂移”这件事感到烦,那这个项目也许值得你驻足。
GitHub 仓库:wuuJiawei/ArcRoute
写在最后
既然提出了问题,那就继续推进,尝试解决问题。
这是我一贯的工作方式。
如果你也认同,欢迎去看看 ArcRoute。
也欢迎来提 Issue、提建议、提反例。
一个项目想要真正成长起来,还越来越多人在真实项目里去使用、去反馈、去批评。