Spring控制器中Kotlin异常处理:如何展示错误信息?
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

