疯狂餐厅
86.88M · 2026-03-21
前阵子,我写了一篇文章:
《为什么 Java 里面,Service 层不直接返回 Result 对象?》
没想到那篇文章讨论度很高。
很多人赞同,也有不少人持反对意见意见:
“既然 Controller 只是转发参数、包装一下 Result,那它存在的意义到底是什么?”
这个问题问得特别好。
因为它刚好触及了很多 Java Web 项目的痛点:
我们一边强调分层,一边又在大量 Controller 里重复写几乎没有业务价值的样板代码。
于是我顺着这个问题,尝试着往前走了一步,做了一个开源框架:
ArcRoute
当然不是要推翻 Spring MVC,也从来没想过要否定分层结构,只是想试着解决这个问题:
当 Controller 只剩「声明路由 + 绑定参数 + 调用实现 + 包装响应」这些机械劳动时,能不能把这些事情交给框架,让业务代码真正回到业务本身?
上一篇文章的核心观点很简单:
Result 更适合作为接口层、表现层的响应封装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 做的是两件事:
传统写法里,路由声明、参数注解、实现逻辑都堆在一个类里。
ArcRoute 把这两部分拆开了:
这意味着接口可以更清晰地作为“契约”存在,而不是埋在实现细节里。README 也把这一点列为核心特性之一:接口声明与实现分离,API 定义在 Interface,业务在 Impl。
ArcRoute 内置了一条统一调用链:
参数解析 → 校验 → 前置处理 → 业务调用 → 后置处理 → 响应包装。
这意味着,很多原本散落在各个 Controller / Advice / Interceptor 里的重复逻辑,可以被整合成一条清晰、可插拔的管道。
分层还是存在的,仍然严格遵守,但是能显著减少重复劳动。
我觉得 ArcRoute 最适合宣传的一点,不是“能少写 Controller”,而是:
它让业务归业务,接口归接口
因为在很多项目里,之所以 Service 最后开始返回 Result,是因为开发者在漫长的代码后,开始嫌麻烦后妥协:
而 ArcRoute 做的事情,本质上就是把妥协的诱因拿掉。
你不需要为了维持分层,额外写一堆机械代码;框架已经把路由、绑定、校验、调用分发这些动作做掉了。业务实现就安安心心写业务逻辑。
所以我会这么概括它:
上一篇文章是在回答“为什么不该这么写”;ArcRoute 是在回答“那怎样写,才不痛苦”。
ArcRoute 不只是一个路由扫描器,而是围绕接口层做了一套完整的能力编排。
目前的核心能力包括:动态路由注册、可插拔处理器、局部调用链配置、参数绑定、Bean Validation 支持、统一响应包装,以及原生 Servlet/Spring Web 参数注入。
我觉得这里面有几个点特别适合拿出来讲。
它支持 @Path、@Query、@Header、@Body、@Part、@Cookie、@Ctx 等参数绑定方式,还支持直接注入 HttpServletRequest、HttpSession、Principal、Locale 这类原生参数。
这让接口定义本身更像一个清晰的契约,而不是一堆 Controller 方法里的杂糅细节。
它支持 ApiPreProcessor、ApiPostProcessor、ApiExceptionProcessor,还支持用 @ApiPipeline 在接口级或方法级挂载处理器和校验器。
这意味着:
都可以从业务代码里抽出去。
ArcRoute 提供 @WrapResult 自动包装返回结果,也支持通过 @RawResponse 跳过包装。
当然还支持用 @RawResponse 返回 ResponseEntity<Resource> 和 SseEmitter 这样的原生响应。
考虑到不同团队的 Result 结构都不同, ArcRoute 也支持自定义返回的响应体结构。
不吹牛的说,既保持了灵活性,又有统一归口。
ArcRoute 至少从设计上给了两个出口:
这就比较符合真实项目。
我觉得这个框架还有个优点很值得写一写:
没有另起炉灶。
就像 README 里面写的:
@RestController 共存。所以老项目不必推倒重来。
完全可以:
@RestController我想,对团队来说,渐进式接入,才更容易真正落地。
非要说一些适用场景,那我推荐几个:
这类项目最典型的代码就是:
public Result<?> xxx(...) {
return Result.success(service.xxx(...));
}
几十个、几百个接口,结构都一样。
这种情况下,用 ArcRoute 把重复代码提走,收益会非常明显。
指那些不想再写几百个一模一样的 Controller 的团队。
ArcRoute 本质上就是给这类团队一个工程化解法。
比如统一鉴权、参数校验、审计、异常转换、接口级扩展,这类需求一多,传统 Controller 写法很容易散。
ArcRoute 的处理器机制和调用链模型,会更有组织性。
强调一下:ArcRoute 不是银弹,任何技术方案都不是银弹。
它不一定适合这些情况:
如果就十来个接口,手写 Controller 完全不是问题,没必要为了优雅再加一层抽象。
ArcRoute 的思想不复杂,但它毕竟不是大家最熟悉的 Spring MVC 默认写法,团队需要一点接受成本。
如果你的接口层本身就有大量自定义流程,Controller 不是空心层,那 ArcRoute 的优势会变小。
说到底,ArcRoute 不是为了整活,也不是为了重新发明 Spring MVC。
只是想把一件我很在意的事,做得再顺畅一点:
业务代码应该专注业务,接口代码应该专注接口,重复劳动应该交给框架。
上一篇文章里,我是在讲一个设计判断:
而这一次,我更想把它推进到工程层面:
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、提建议、提反例。
一个项目想要真正成长起来,还越来越多人在真实项目里去使用、去反馈、去批评。