API Interface Driven Dynamic Spring MVC Routing Framework
ArcRoute 是一个 API 接口驱动 的动态 Spring MVC 路由框架。它的核心思想是将 HTTP 接口声明从传统的 @Controller 中剥离出来,通过 接口定义 + 实现分离 的方式实现 API 路由。
业务代码只需关注接口定义和实现,无需关心 Web 层细节。框架会自动完成路由注册、参数绑定、校验、调用分发等工作。
ArcRoute 的起点,是我发表在掘金上的一篇关于“Service 层是否应该直接返回 Result”的文章,以及后续和读者的讨论。
讨论里反复出现一个问题:很多项目里的 Controller 层并不承载业务,只是在做参数转发和响应包装,看起来像大量重复样板代码。
这个项目想解决的正是这件事:不是否定分层,而是抹平没有业务承载的 Controller 样板,把接口声明、路由注册、参数绑定、调用分发交给框架,把业务实现留在 API Impl / Service。
相关阅读:
- 接口声明与实现分离 - API 定义在 Interface,业务在 Impl,完全解耦
- 动态路由注册 - 启动时扫描注解,自动注册到 Spring MVC
- 统一调用链 - 参数解析 → 校验 → 前置处理 → 业务调用 → 后置处理 → 响应包装
- 可插拔处理器机制 - 支持
ApiPreProcessor、ApiPostProcessor、ApiExceptionProcessor - 局部调用链配置 - 支持
@ApiPipeline在接口级/方法级挂载处理器与校验器 - 参数绑定 - 支持
@Path、@Query、@Header、@Body、@Part、@Cookie、@Ctx - 原生参数注入 - 支持在方法参数中直接声明
HttpServletRequest/HttpSession/Principal/Locale - 校验支持 - 内置 Bean Validation(JSR-303)+ 可扩展
ApiValidator - 统一响应包装 -
@WrapResult自动包装返回结果,支持@RawResponse跳过包装 - 流式响应 - 支持
@RawResponse返回ResponseEntity<Resource>与SseEmitter
- Java 8+ - 兼容 Java 8 到 Java 21
- Spring Boot 2.7 / 3.x - 自动配置,开箱即用
- 双重 Servlet 支持 - Javax 与 Jakarta 按模块自动适配,不强制绑定单一命名空间
- 无侵入 - 不修改 Spring 原有机制,与
@RestController共存
<!-- Maven Central -->
<dependency>
<groupId>pub.lighting</groupId>
<artifactId>arcroute-spring-boot-starter</artifactId>
<version>0.1.2</version>
</dependency>// Gradle (Maven Central)
implementation("pub.lighting:arcroute-spring-boot-starter:0.1.2")可选模块坐标(同版本 0.1.2):
pub.lighting:arcroute-annotationspub.lighting:arcroute-corepub.lighting:arcroute-servlet-javaxpub.lighting:arcroute-servlet-jakartapub.lighting:arcroute-spring-boot-starter
arcroute-spring-boot-starter不强制绑定javax.validation或jakarta.validation。- 框架会根据运行时 Servlet 命名空间自动启用对应适配模块(
arcroute-servlet-javax/arcroute-servlet-jakarta)。 - 若你使用 Bean Validation 注解(如
@Valid/@NotBlank),请在业务应用显式引入 Spring Validation Starter:
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-validation</artifactId>
</dependency>@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) {
// 纯业务逻辑,无 Web 细节
return userService.findById(id);
}
@Override
public Result<Long> createUser(CreateUserCmd cmd) {
// 业务逻辑
return Result.ok(userService.create(cmd));
}
}直接启动 Spring Boot 应用,框架会自动注册路由。
GET /users/{id} -> UserApiImpl.getUser()
POST /users -> UserApiImpl.createUser()
# 未知异常对外返回固定文案(默认 true)
arcroute.runtime.safe-error-message-enabled=true
# 是否启用隐式全量扫描(默认 false)。
# false: 仅扫描 @ApiService 标注 Bean(推荐)
# true : 回退到历史行为,可能在启动阶段实例化非 API Bean
arcroute.runtime.allow-implicit-api-scan=false关闭后效果:
safe-error-message-enabled=false:未知异常会把原始 message 返回给调用方
默认错误状态码语义:
- 参数错误(
INVALID_PARAM)返回 HTTP400 - 鉴权失败(
UNAUTHORIZED)返回 HTTP401 - 未知异常返回 HTTP
500
扩展说明:
- 你可以通过实现
ApiExceptionProcessor覆盖默认未知异常处理逻辑 traceId注入、日志落盘与日志框架集成由用户自行实现
- SSE:推荐使用
@RawResponse + produces = text/event-stream,直接返回 Spring 原生SseEmitter - WebSocket:推荐保持 Spring Boot 原生
WebSocketConfigurer/WebSocketHandler(或 STOMP) - 设计原则:ArcRoute 仅处理 HTTP 请求-响应模型,实时双向通道交给 Spring 原生组件
你可以像 @Controller 一样,在 @ApiRoute 方法参数里直接声明 Servlet 上下文对象,无需额外注解:
@ApiRoute(path = "/request-context", method = ApiHttpMethod.GET)
Map<String, Object> requestContext(HttpServletRequest request,
HttpSession session,
Principal principal,
Locale locale);优先使用 Bean Validation 注解声明规则,避免在 ApiValidator 中按 routeName 做硬编码分支:
public class CreateUserCmd {
@NotBlank(message = "name is required")
private String name;
@NotBlank(message = "email is required")
@Email(message = "email is invalid")
private String email;
}UserSearchResult search(@Query("q") String keyword,
@Query("page") @Min(1) Integer page,
@Query("size") @Min(1) @Max(50) Integer size);ApiValidator 建议只处理跨对象或依赖外部系统的复杂业务校验。
完整的示例项目请参考 arcroute-demo。
@Api(basePath = "/users", produces = MediaType.APPLICATION_JSON, version = "v1", authRequired = true)
@WrapResult
public interface UserApi {
// GET /users/{id}
@ApiRoute(path = "/{id}", method = ApiHttpMethod.GET, name = "user.get")
UserDTO get(
@Path("id") Long id,
@Header(value = "X-Trace-Id", defaultValue = "") String traceId
);
// POST /users
@ApiRoute(path = "", method = ApiHttpMethod.POST, consumes = MediaType.APPLICATION_JSON, name = "user.create")
Long create(@Body @Valid CreateUserCmd cmd);
// PUT /users/{id}
@ApiRoute(path = "/{id}", method = ApiHttpMethod.PUT, consumes = MediaType.APPLICATION_JSON, name = "user.replace")
UserDTO replace(@Path("id") Long id, @Body ReplaceUserCmd cmd);
// DELETE /users/{id}
@ApiRoute(path = "/{id}", method = ApiHttpMethod.DELETE, name = "user.delete")
DeleteResult delete(@Path("id") Long id);
// multipart 上传
@ApiRoute(path = "/upload", method = ApiHttpMethod.POST, consumes = MediaType.MULTIPART_FORM_DATA)
UploadResult upload(@Part("file") MultipartFile file);
// 文件下载示例(方案A:Spring 原生 ResponseEntity<Resource>)
@RawResponse
@ApiRoute(path = "/export", method = ApiHttpMethod.GET)
ResponseEntity<Resource> exportUsers();
}@Api(authRequired = true) 是认证元标记,本身不会触发内置鉴权。建议在 ApiPreProcessor 中读取该标记并执行鉴权逻辑。
@Component
public class AuthPreProcessor implements ApiPreProcessor {
@Override
public void pre(ApiContext ctx, ApiInvocation inv) {
String token = ctx.getRequest().getHeader("X-Auth-Token");
if (token == null || token.isEmpty()) {
throw new BizException(401, "未授权访问");
}
// 验证 token ...
ctx.setAttribute("operator", getOperator(token));
}
}@Component
public class GlobalExceptionProcessor implements ApiExceptionProcessor {
@Override
public Object onException(ApiContext ctx, ApiInvocation inv, Throwable ex) {
if (ex instanceof BizException) {
return Result.fail(((BizException) ex).getCode(), ex.getMessage());
}
return Result.fail("500", "系统异常");
}
}| 模块 | 说明 |
|---|---|
arcroute-annotations |
核心注解定义 |
arcroute-core |
核心处理逻辑 |
arcroute-servlet-javax |
Javax Servlet 适配器 |
arcroute-servlet-jakarta |
Jakarta Servlet 适配器 |
arcroute-spring-boot-starter |
Spring Boot 自动配置 |
arcroute-demo |
完整示例项目 |
欢迎提交 Issue 和 Pull Request!
- Fork 本仓库
- 创建分支 (
git checkout -b feature/xxx) - 提交更改 (
git commit -m 'Add xxx') - 推送分支 (
git push origin feature/xxx) - 创建 Pull Request
本项目基于 MIT 许可证开源,详见 LICENSE。
Made with ❤️ by ArcRoute
