You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

Spring控制器中Kotlin异常处理:如何展示错误信息?

Kotlin Spring控制器中明确成功/失败响应的最佳实践

1. 构建带@ResponseStatus的自定义异常体系

Kotlin虽然没有强制声明抛出异常的语法,但可以通过自定义异常+Spring的@ResponseStatus注解,让异常与HTTP状态码绑定,同时配合全局@ExceptionHandler统一处理。这种方式能让开发者清晰感知控制器可能抛出的异常类型,也能让前端明确错误响应的格式。

示例代码:

// 自定义异常,绑定HTTP状态码
@ResponseStatus(HttpStatus.BAD_REQUEST)
class InvalidParamException(message: String) : RuntimeException(message)

@ResponseStatus(HttpStatus.INTERNAL_SERVER_ERROR)
class ServiceException(message: String) : RuntimeException(message)

// 全局异常处理器
@RestControllerAdvice
class GlobalExceptionHandler {
    @ExceptionHandler(InvalidParamException::class)
    fun handleInvalidParam(e: InvalidParamException): ErrorResponse {
        return ErrorResponse(HttpStatus.BAD_REQUEST.value(), e.message ?: "参数错误")
    }

    @ExceptionHandler(ServiceException::class)
    fun handleServiceError(e: ServiceException): ErrorResponse {
        return ErrorResponse(HttpStatus.INTERNAL_SERVER_ERROR.value(), e.message ?: "服务异常")
    }
}

// 控制器方法
@GetMapping("/names")
fun getMyObj(
    @RequestParam page: Int,
    @RequestParam countPerPage: Int
): List<MyObj> {
    if (page < 1 || countPerPage > 100) {
        throw InvalidParamException("页码或每页数量不符合要求")
    }
    return service.getInfo(page, countPerPage) // 内部可能抛出ServiceException
}

这种方式下,虽然控制器方法没有声明抛出异常,但通过统一的异常体系,开发者能直观识别可能的错误场景,Spring也会自动将异常映射为对应HTTP响应。

2. 用Spring REST Docs自动生成带错误响应的接口文档

如果不想手动维护文档,可以用Spring REST Docs,通过编写集成测试自动生成接口文档,其中可明确标记控制器可能抛出的异常及对应的响应格式,完全替代手动文档且准确性更高。

示例测试代码片段:

@Test
fun `get names with invalid param returns bad request`() {
    mockMvc.get("/names?page=0&countPerPage=200")
        .andExpect(status().isBadRequest)
        .andDo(document("get-names",
            responseFields(
                fieldWithPath("code").description("错误码"),
                fieldWithPath("message").description("错误信息")
            ),
            throws(
                exceptionWithName("InvalidParamException")
                    .description("当页码小于1或每页数量超过100时抛出")
            )
        ))
}

运行测试后,生成的文档会包含成功响应和错误响应的详细信息,无需手动编写。

3. 封装统一响应体+Kotlin Result处理

定义通用的响应体类,控制器方法统一返回这个类,结合Kotlin的Result类型在服务层处理业务逻辑,在控制器中解析Result,将成功或失败结果转为统一响应。这种方式能让接口返回类型明确包含所有可能的响应情况。

示例代码:

// 统一响应体
data class ApiResponse<T>(
    val code: Int,
    val message: String,
    val data: T? = null
) {
    companion object {
        fun <T> success(data: T): ApiResponse<T> {
            return ApiResponse(HttpStatus.OK.value(), "success", data)
        }

        fun fail(code: Int, message: String): ApiResponse<Nothing> {
            return ApiResponse(code, message)
        }
    }
}

// 服务层返回Result
@Service
class MyService {
    fun getInfo(page: Int, countPerPage: Int): Result<List<MyObj>> {
        return if (page < 1 || countPerPage > 100) {
            Result.failure(InvalidParamException("参数错误"))
        } else {
            // 业务逻辑
            Result.success(listOf(MyObj("name1"), MyObj("name2")))
        }
    }
}

// 控制器方法
@GetMapping("/names")
fun getMyObj(
    @RequestParam page: Int,
    @RequestParam countPerPage: Int
): ApiResponse<List<MyObj>> {
    return runCatching {
        service.getInfo(page, countPerPage).getOrThrow()
    }.fold(
        onSuccess = { ApiResponse.success(it) },
        onFailure = {
            when (it) {
                is InvalidParamException -> ApiResponse.fail(HttpStatus.BAD_REQUEST.value(), it.message!!)
                is ServiceException -> ApiResponse.fail(HttpStatus.INTERNAL_SERVER_ERROR.value(), it.message!!)
                else -> ApiResponse.fail(HttpStatus.INTERNAL_SERVER_ERROR.value(), "未知错误")
            }
        }
    )
}

这种方式下,接口返回类型始终是ApiResponse,开发者能直接看到响应结构,同时通过fold处理Result的成功和失败分支,明确所有可能的错误情况。

4. 用密封类定义明确的结果类型

Kotlin的密封类(Sealed Class) 可以限定所有可能的结果类型,控制器返回密封类实例,配合Swagger等文档工具,能让接口文档清晰展示所有成功/失败的响应格式。

示例代码:

// 密封类定义结果类型
sealed class ApiResult<out T> {
    data class Success<out T>(val data: T) : ApiResult<T>()
    data class Error(val code: Int, val message: String) : ApiResult<Nothing>()
}

// 控制器方法
@GetMapping("/names")
fun getMyObj(
    @RequestParam page: Int,
    @RequestParam countPerPage: Int
): ApiResult<List<MyObj>> {
    return if (page < 1 || countPerPage > 100) {
        ApiResult.Error(HttpStatus.BAD_REQUEST.value(), "参数错误")
    } else {
        try {
            ApiResult.Success(service.getInfo(page, countPerPage))
        } catch (e: ServiceException) {
            ApiResult.Error(HttpStatus.INTERNAL_SERVER_ERROR.value(), e.message!!)
        }
    }
}

配合Swagger的@ApiResponses注解,可在文档中明确标记每个分支的响应:

@ApiResponses(
    ApiResponse(code = 200, message = "请求成功", response = Success::class),
    ApiResponse(code = 400, message = "参数错误", response = Error::class),
    ApiResponse(code = 500, message = "服务异常", response = Error::class)
)

这种方式完全避免依赖@Throws注解,同时让结果类型的所有可能性都明确可见。

内容的提问来源于stack exchange,提问作者Moko Doko

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.06.26 05:55:17