用这个框架彻底摆脱Controller,从此专注业务——ArcRoute

前言

前阵子,我写了一篇文章:

《为什么 Java 里面,Service 层不直接返回 Result 对象?》

没想到那篇文章讨论度很高。

很多人赞同,也有不少人持反对意见意见:

"既然 Controller 只是转发参数、包装一下 Result,那它存在的意义到底是什么?"

这个问题问得特别好。

因为它刚好触及了很多 Java Web 项目的痛点:

我们一边强调分层,一边又在大量 Controller 里重复写几乎没有业务价值的样板代码。

于是我顺着这个问题,尝试着往前走了一步,做了一个开源框架:

ArcRoute

当然不是要推翻 Spring MVC,也从来没想过要否定分层结构,只是想试着解决这个问题:

当 Controller 只剩「声明路由 + 绑定参数 + 调用实现 + 包装响应」这些机械劳动时,能不能把这些事情交给框架,让业务代码真正回到业务本身?

先说结论:真正反对的是职责错位

上一篇文章的核心观点很简单:

  • Service 层应该面向业务语义,而不是面向 HTTP 响应结构
  • Result 更适合作为接口层、表现层的响应封装
  • 业务失败应该优先通过异常和统一机制处理,而不是在 Service 里到处 Result.fail(...)
  • 业务之间互调时,直接传递领域对象,比一层层拆 Result 更自然、更可复用

这背后反映出一个很实际的工程问题:

一旦 Service 和 HTTP 响应格式绑死,业务层就很容易被表现层污染。

可问题来了。

如果我们坚持"Service 不直接返回 Result",那很多 Controller 最后就会变成这样:

less 复制代码
@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)上。

比如:

java 复制代码
@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);
}

然后业务实现写在实现类里:

kotlin 复制代码
@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 等参数绑定方式,还支持直接注入 HttpServletRequestHttpSessionPrincipalLocale 这类原生参数。

这让接口定义本身更像一个清晰的契约,而不是一堆 Controller 方法里的杂糅细节。

2. 调用链全程可插拔

它支持 ApiPreProcessorApiPostProcessorApiExceptionProcessor,还支持用 @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 大量重复、几乎纯转发的项目

这类项目最典型的代码就是:

typescript 复制代码
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 依赖:

xml 复制代码
<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、提建议、提反例。

一个项目想要真正成长起来,还越来越多人在真实项目里去使用、去反馈、去批评。

相关推荐
SunnyDays10111 小时前
Java 如何根据模板高效生成Word文档
java·根据模板生成word文档·生成word文档
攀岩巨峰的程序猿1 小时前
代码开发过程中涉及到bean的copy方法梳理
java
golang学习记1 小时前
IDEA 2026.1 EAP 5 发布:K2模式更强了!
java·ide·intellij-idea
xuansec1 小时前
【JavaEE安全】Java反序列化深度剖析:核心原理、利用链构造与安全风险管控
java·安全·java-ee
艾莉丝努力练剑1 小时前
静态地址重定位与动态地址重定位:Linux操作系统的视角
java·linux·运维·服务器·c语言·开发语言·c++
菜鸟小九1 小时前
hot100(31-40)
java·算法
xu_ws2 小时前
Spring-ai项目-deepseek-会话日志
java·人工智能·spring
咸蛋超超人2 小时前
下订单重复提交问题递进式解决方案案例
java·后端