Skip to content

wuuJiawei/ArcRoute

Repository files navigation

ArcRoute

ArcRoute Logo

API Interface Driven Dynamic Spring MVC Routing Framework

Maven Central GitHub release Java Spring Boot License


简介

ArcRoute 是一个 API 接口驱动 的动态 Spring MVC 路由框架。它的核心思想是将 HTTP 接口声明从传统的 @Controller 中剥离出来,通过 接口定义 + 实现分离 的方式实现 API 路由。

业务代码只需关注接口定义和实现,无需关心 Web 层细节。框架会自动完成路由注册、参数绑定、校验、调用分发等工作。

项目初衷

ArcRoute 的起点,是我发表在掘金上的一篇关于“Service 层是否应该直接返回 Result”的文章,以及后续和读者的讨论。

讨论里反复出现一个问题:很多项目里的 Controller 层并不承载业务,只是在做参数转发和响应包装,看起来像大量重复样板代码。

这个项目想解决的正是这件事:不是否定分层,而是抹平没有业务承载的 Controller 样板,把接口声明、路由注册、参数绑定、调用分发交给框架,把业务实现留在 API Impl / Service。

相关阅读:

特性

核心特性

  • 接口声明与实现分离 - API 定义在 Interface,业务在 Impl,完全解耦
  • 动态路由注册 - 启动时扫描注解,自动注册到 Spring MVC
  • 统一调用链 - 参数解析 → 校验 → 前置处理 → 业务调用 → 后置处理 → 响应包装
  • 可插拔处理器机制 - 支持 ApiPreProcessorApiPostProcessorApiExceptionProcessor
  • 局部调用链配置 - 支持 @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 共存

快速开始

1. 添加依赖

<!-- 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-annotations
  • pub.lighting:arcroute-core
  • pub.lighting:arcroute-servlet-javax
  • pub.lighting:arcroute-servlet-jakarta
  • pub.lighting:arcroute-spring-boot-starter

1.1 Spring Boot 兼容与依赖说明

  • arcroute-spring-boot-starter 不强制绑定 javax.validationjakarta.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>

2. 定义 API 接口

@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);
}

3. 实现 API

@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));
    }
}

4. 运行

直接启动 Spring Boot 应用,框架会自动注册路由。

GET  /users/{id}  -> UserApiImpl.getUser()
POST /users        -> UserApiImpl.createUser()

5. 错误处理扩展开关(默认开启)

# 未知异常对外返回固定文案(默认 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)返回 HTTP 400
  • 鉴权失败(UNAUTHORIZED)返回 HTTP 401
  • 未知异常返回 HTTP 500

扩展说明:

  • 你可以通过实现 ApiExceptionProcessor 覆盖默认未知异常处理逻辑
  • traceId 注入、日志落盘与日志框架集成由用户自行实现

6. SSE 与 WebSocket 边界

  • SSE:推荐使用 @RawResponse + produces = text/event-stream,直接返回 Spring 原生 SseEmitter
  • WebSocket:推荐保持 Spring Boot 原生 WebSocketConfigurer/WebSocketHandler(或 STOMP)
  • 设计原则:ArcRoute 仅处理 HTTP 请求-响应模型,实时双向通道交给 Spring 原生组件

7. Controller 风格参数注入

你可以像 @Controller 一样,在 @ApiRoute 方法参数里直接声明 Servlet 上下文对象,无需额外注解:

@ApiRoute(path = "/request-context", method = ApiHttpMethod.GET)
Map<String, Object> requestContext(HttpServletRequest request,
                                   HttpSession session,
                                   Principal principal,
                                   Locale locale);

8. 推荐校验方式

优先使用 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

@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!

  1. Fork 本仓库
  2. 创建分支 (git checkout -b feature/xxx)
  3. 提交更改 (git commit -m 'Add xxx')
  4. 推送分支 (git push origin feature/xxx)
  5. 创建 Pull Request

许可证

本项目基于 MIT 许可证开源,详见 LICENSE


Made with ❤️ by ArcRoute

About

Interface Driven Dynamic Spring MVC Routing Framework, Keeping Services Focused.

Topics

Resources

License

Stars

Watchers

Forks

Packages

 
 
 

Contributors

Languages