用这个框架彻底摆脱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、提建议、提反例。

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

相关推荐
小bo波41 分钟前
枚举实战
java·设计模式·枚举·后端开发·代码重构
夜微凉41 小时前
三、Spring
java·后端·spring
橘右今1 小时前
2026 Java后端高频面试宝典
java·开发语言·面试
xyzzklk2 小时前
解决Salesforce无法向外发送邮件
android·java·开发语言·网络·crm·salesforce·客户关系管理
biubiubiu07062 小时前
SpringBoot关于外部化配置
java·spring boot·spring
Full Stack Developme3 小时前
Spring Bean 依赖注入
python·spring·log4j
zzz_23683 小时前
【Spring】面试突击系列(二):SpringBoot 入门与自动配置原理
java·spring boot·spring
Full Stack Developme3 小时前
Spring AOP 与 AspectJ
java·后端·spring
快乐的木子李3 小时前
最新版Maven免安装配置教程
java·maven
世人万千丶4 小时前
鸿蒙PC问题解决:窗口拖动与拉伸时页面布局瞬间错乱、回弹后恢复
学习·华为·开源·harmonyos·鸿蒙·鸿蒙系统