在构建RESTful API时,保持响应格式的一致性至关重要。Spring Boot提供了ResponseBodyAdvice接口,允许我们在控制器方法返回后统一处理响应体。本文将通过一个健康检查接口案例,展示如何通过自定义注解+响应处理器实现响应格式的标准化封装。

一、核心需求场景

当我们的接口需要返回统一结构时(如包含状态码、消息、数据及分页信息),传统做法是在每个Controller方法中手动构建响应对象。这种方式存在以下问题:

  • 代码冗余:重复构建相同结构的Map
  • 维护困难:修改响应格式需要改动多处代码
  • 扩展性差:难以统一添加元数据(如链路追踪ID)

通过ResponseBodyAdvice+自定义注解的组合方案,可以优雅地解决这些问题。

二、实现方案解析

1. 自定义注解标记

import java.lang.annotation.Inherited

@Target(AnnotationTarget.FUNCTION)
@Retention(AnnotationRetention.RUNTIME)
@Inherited
annotation class ResultHandle()
  • 作用:标识需要统一处理的接口方法
  • 特性
    • 运行时保留(RUNTIME)
    • 可继承(Inherited)
    • 仅作用于方法(FUNCTION)

2. 响应处理器实现

@ControllerAdvice()
class ResponseHandler : ResponseBodyAdvice<Any> {
    // 判断是否要处理当前响应
    override fun supports(
        returnType: MethodParameter,
        converterType: Class<out HttpMessageConverter<*>>
    ): Boolean {
        return returnType.getMethodAnnotation(ResultHandle::class.java) != null
    }

    // 处理响应体的核心逻辑
    override fun beforeBodyWrite(
        body: Any?,
        returnType: MethodParameter,
        selectedContentType: MediaType,
        selectedConverterType: Class<out HttpMessageConverter<*>>,
        request: ServerHttpRequest,
        response: ServerHttpResponse
    ): Any? {
        val uri = request.uri
        return when {
            body is String || body is Boolean -> {
                mapOf(
                    "result" to body,
                    "_links" to Links(Self(uri.toString()))
                )
            }
            else -> {
                val map = BeanUtil.beanToMap(body)
                map["_links"] = Links(Self(uri.toString()))
                map
            }
        }
    }
}

关键处理逻辑:

  1. 类型判断
    • 简单类型(String/Boolean)直接包装
    • 复杂对象使用Hutool的BeanUtil转换为Map
  2. 元数据添加
    • 自动注入请求URI生成HATEOAS风格的_links
    • 可扩展添加其他元数据(如traceId、时间戳等)

3. 控制器示例

@RestController
@RequestMapping
class HealthCheckController {
    private val logger = LoggerFactory.getLogger(HealthCheckController::class.java)

    @ResultHandle
    @GetMapping("/storages/health/check")
    fun checkHealth(): Any {
        return Date().time.toString()
    }
}
  • 响应示例
{
  "result": "1716589200000",
  "_links": {
    "self": {
      "href": "/storages/health/check"
    }
  }
}
三、方案优势
  1. 非侵入式:通过注解标记需要处理的接口
  2. 统一格式:保证所有标记接口返回相同结构
  3. 扩展性强:可轻松添加以下功能:
    • 统一错误码处理
    • 分页信息封装
    • 签名验证
    • 响应压缩
  4. 性能优化:集中处理减少重复代码

四、高级扩展方向

  1. 响应过滤
override fun supports(...): Boolean {
    return returnType.getMethodAnnotation(ResultHandle::class.java) != null 
        && !returnType.getMethodAnnotation(RawResponse::class.java) != null
}

通过添加@RawResponse注解实现白名单过滤

  1. 内容协商
when (selectedContentType) {
    MediaType.APPLICATION_XML -> { /* XML格式处理 */ }
    else -> { /* JSON默认处理 */ }
}

  1. 安全增强
map["traceId"] = MDC.get("traceId")
map["timestamp"] = System.currentTimeMillis()

五、注意事项

  1. 类型转换:复杂对象转Map时注意循环引用问题
  2. 性能监控:建议添加处理耗时监控
  3. 异常处理:需配合@ExceptionHandler实现完整错误处理
  4. 版本控制:可通过请求头实现多版本响应格式支持

通过本文方案,我们可以实现:

  • 90%的接口零配置响应封装
  • 响应格式变更只需修改处理器逻辑
  • 天然支持OpenAPI规范生成
  • 为前后端协作提供统一契约

这种设计模式特别适用于中大型项目的API层标准化建设,建议结合Swagger/OpenAPI文档工具使用效果更佳。

Logo

智能硬件社区聚焦AI智能硬件技术生态,汇聚嵌入式AI、物联网硬件开发者,打造交流分享平台,同步全国赛事资讯、开展 OPC 核心人才招募,助力技术落地与开发者成长。

更多推荐