前言
本篇主要是为之后 SpringBoot 结合 DDD 领域驱动设计落地实践 的相关章节而服务,对于前面章节讲过的内容,后面不在赘述.
统一返回数据类型
ApiStatus
我们可以先定一个 ApiStatus,作用如下:
- 统一管理所有接口的状态码,避免一些魔法数字(例如有人会直接在代码里写出
if (code == 8) { ... }这种没有明确意义的常量) - 让前端可以直接判断出这个响应到底是成功还是失败了,已经失败的原因,进一步的前端就可以根据不同的 code(状态码),来展示对应不同的界面.
代码如下:
/**
* 用来表明 api 的不同状态
*/
enum class ApiStatus(val code: Int, val msg: String) {
//请求成功
SUCCESS(0, "ok"),
//请求失败
FAIL(1, "request failed"),
//非法请求
INVALID_REQUEST(2, "invalid request"),
//非法参数
INVALID_PARAM(3, "invalid param"),
//未绑定
NOT_BINDED(4, "not binded"),
//服务器错误
SERVER_ERROR(5, "server error"),
//没有注册
NOT_REGISTERED(6, "not registered"),
//token 过期
TOKEN_EXPIRE(7, "token expire"),
//访问频率被限制
FREQUENCY_LIMITED(8, "frequency limited"),
//没有权限
NO_PERMISSION(9, "no permission"),
//用户未登陆
NOT_LOGIN(40001, "require login"),
//用户被封
USER_LOCKED(40002, "user is locked"),
;
}
ApiResp
1)定义一个 ApiResp 类,前端接收后端的响应结构统一如下:
import com.fasterxml.jackson.annotation.JsonInclude
import java.io.Serializable
/**
* 用来处理作为 api http 的返回接口
*
* @author cyk
*/
data class ApiResp<T> (
var code: Int,
var msg: String,
@field:JsonInclude(JsonInclude.Include.NON_NULL)
var data: T? = null
): Serializable {
companion object {
private const val serialVersionUID: Long = 1L
/**
* 成功响应
*/
fun <T> ok(data: T): ApiResp<T> {
return ApiResp(
ApiStatus.SUCCESS.code,
ApiStatus.SUCCESS.msg,
data
)
}
fun <T> ok(): ApiResp<T> {
return ApiResp(
ApiStatus.SUCCESS.code,
ApiStatus.SUCCESS.msg,
)
}
/**
* 约定: 错误响应,一定没有 data 数据
*/
fun no(code: ApiStatus): ApiResp<ApiStatus> {
return ApiResp(
code.code,
code.msg
)
}
/**
* 考虑灵活性,支持自定义 code 的信息
*/
fun no(code: Int, msg: String): ApiResp<Unit> {
return ApiResp(
code,
msg
)
}
}
}
2)解释:
- code:状态码,前端可以直接根据约定好状态码,直接判断出响应是成功还是失败了.
- msg:描述信息,一般成功的响应,这里都会直接写一个 “ok”(当然你自定义其他的也无所谓),而对于错误的响应,这里则是对错误信息的具体描述,例如参数错误(如果对后台系统,甚至你还可以直接给更具体的错误原因).
- data:具体响应数据.
3)@JsonInclude(JsonInclude.Include.NON_NULL) 这个注解有什么用?
表示被序列化成 json 的时候,这个字段如果为 null,就不输出;
你对前端的返回数据一般都是一个 json 结构,但是有些例如错误响应,是没有 data 数据,那么响应可能就是这样的.

既然这里是 null,其实 data 这个字段也可以不序列化,一定程度上减少网络开销~ 那么加上此注解后,效果如下:

3)为什么要实现 Serializable 接口?serialVersionUID 有啥用?
Note:
序列化:将你定义的对象(例如ApiResp)转化成一种约定的传输格式(例如 JSON 格式)
反序列化:将约定的传输格式(例如 JSON 格式)转化成你定义的对象(例如 ApiResp)
- 实现 Serializable 接口,是为了让你的对象可以序列化成 字节流(例如保存到文件、Redis 或者网络传输等),之后能在通过反序列化还原成原对象.
- serialVersionUID 是一个版本号,当你序列化一个对象时,JVM 就会把
类名、字段结构、serialVersionUID一起写入文件,反序列化时,会检查当前类定义的 serialVersionUID 和文件中的时候否一致,如果不一致就会报错.
如果我们自己不手动定义 serialVersionUID,JVM 就会自动生成一个 UID,这就会导致一个问题:你将 类A 序列化成字节流保存到文件后,然后你修改了 类A(例如 加一个字段、改名字、改字段顺序等),自动生成的 UID 就会变化,将来你把文件中的数据再反序列化成 类A 的时候,JVM 就会对比 serialVersionUID 发现不一致,最后报错
4)我自定义的 serialVersionUID 值有什么要求么?
没有任何要求,我一般常见用的做法就是:
- 简单起见,直接写成固定的 serialVersionUID = 1L 就行(不同类的 serialVersionUID 相同也无所谓)
- 或者让 IDEA 直接给你生成一串数字.
统一异常处理
1)统一异常处理可以通过以下两个注解实现:
- @RestControllerAdvice:等价于
@ControllerAdvice + @ResponseBody,@ResponseBody 就是说把你给前端返回的数据序列化成 JSON,而不是一个页面;@ControllerAdvice 就是 AOP 切面处理了(控制器通知类),他不会拦截请求本身,而是拦截 Controller 层方法执行过程中的某些特殊环节(例如 异常处理、数据绑定、模型属性初始化). - @ExceptionHandler:用来标注某个方法,他能处理的异常类型.
2)工作原理:简单来讲,就是 Controller 抛出异常后,就会去 @ControllerAdvice 类中找有没有被 @ExceptionHandler 标注的对应异常的处理方法.
3)最佳实践:
- 项目中所有参数相关的错误,统一抛出 IllegalArgumentException 异常.(例如
throw IllegalArgumentException("username 不能为空")) - 项目中其他所有意料之内的错误,统一抛出 IllegalStateException 异常. (例如
throw IllegalStateException("user 信息保存失败"))
统一异常处理代码如下
import org.cyk.tpl.infra.model.ApiResp
import org.cyk.tpl.infra.model.ApiStatus
import org.slf4j.LoggerFactory
import org.springframework.web.bind.annotation.ExceptionHandler
import org.springframework.web.bind.annotation.RestControllerAdvice
/**
* 统一异常处理
*/
@RestControllerAdvice
class ApplicationExceptionHandler {
companion object {
private val log = LoggerFactory.getLogger(ApplicationExceptionHandler::class.java)
}
/**
* 处理非法参数异常,使用 warn 日志级别
*/
@ExceptionHandler(IllegalArgumentException::class)
fun illegalArgumentExceptionHandle(e: IllegalArgumentException): ApiResp<Unit> {
log.warn("非法参数,原因: {}", e.message)
return ApiResp.no(ApiStatus.INVALID_PARAM.code, e.message ?: "")
}
/**
* 处理其他所有意料之内的异常,使用 error 日志级别
*/
@ExceptionHandler(IllegalStateException::class)
fun illegalStateExceptionHandle(e: IllegalStateException): ApiResp<Unit> {
log.error("非法状态", e)
return ApiResp.no(ApiStatus.SERVER_ERROR.code, e.message ?: "")
}
/**
* 处理其他所有意料之外的异常(兜底)
*/
@ExceptionHandler(Exception::class)
fun exceptionHandle(e: Exception): ApiResp<Unit> {
log.error("服务器异常", e)
return ApiResp.no(ApiStatus.SERVER_ERROR.code, e.message ?: "")
}
}
注意:
对于异常信息描述(例如 "user 信息保存失败"),不可直接暴露具体参数(例如 "user 信息保存失败 ${user}"),因为这里的错误信息是用户可见的,对于我们开发人员排查错误需要记录具体参数时,在抛异常前面手动打上日志即可,例如如下:
val user = Userinfo(15)
// 保存逻辑 ...
val saveSuccess = false
if (saveSuccess.not()) {
log.error("user 信息保存失败. user: {}", user)
throw IllegalStateException("user 信息保存失败")
}
4)三种不同异常处理方式的效果如下:
import org.cyk.tpl.infra.model.ApiResp
import org.springframework.web.bind.annotation.PostMapping
import org.springframework.web.bind.annotation.RequestMapping
import org.springframework.web.bind.annotation.RestController
@RestController
@RequestMapping("test/")
class TestApi {
@PostMapping("arg/")
fun arg(): ApiResp<Unit> {
throw IllegalArgumentException("username 不能为空")
}
@PostMapping("state/")
fun state(): ApiResp<Unit> {
throw IllegalStateException("user 信息保存失败")
}
@PostMapping("other")
fun other(): ApiResp<Exception> {
throw RuntimeException("运行失败")
}
}
运行触发后,日志效果如下:
2025-11-02T16:05:32.966+08:00 WARN 28305 --- [tpl] [nio-8080-exec-1] o.c.t.i.aop.ApplicationExceptionHandler : 非法参数,原因: username 不能为空
2025-11-02T16:05:37.978+08:00 ERROR 28305 --- [tpl] [nio-8080-exec-3] o.c.t.i.aop.ApplicationExceptionHandler : 非法状态
java.lang.IllegalStateException: user 信息保存失败
at org.cyk.tpl.api.TestApi.state(TestApi.kt:20) ~[classes/:na]
at java.base/jdk.internal.reflect.DirectMethodHandleAccessor.invoke(DirectMethodHandleAccessor.java:104) ~[na:na]
at java.base/java.lang.reflect.Method.invoke(Method.java:565) ~[na:na]
at kotlin.reflect.jvm.internal.calls.CallerImpl$Method.callMethod(CallerImpl.kt:97) ~[kotlin-reflect-2.2.20.jar:2.2.20-release-333]
at kotlin.reflect.jvm.internal.calls.CallerImpl$Method$Instance.call(CallerImpl.kt:113) ~[kotlin-reflect-2.2.20.jar:2.2.20-release-333]
at kotlin.reflect.jvm.internal.KCallableImpl.callDefaultMethod$kotlin_reflection(KCallableImpl.kt:250) ~[kotlin-reflect-2.2.20.jar:2.2.20-release-333]
at kotlin.reflect.jvm.internal.KCallableImpl.callBy(KCallableImpl.kt:155) ~[kotlin-reflect-2.2.20.jar:2.2.20-release-333]
at org.springframework.web.method.support.InvocableHandlerMethod$KotlinDelegate.invokeFunction(InvocableHandlerMethod.java:335) ~[spring-web-6.2.12.jar:6.2.12]
at org.springframework.web.method.support.InvocableHandlerMethod.doInvoke(InvocableHandlerMethod.java:255) ~[spring-web-6.2.12.jar:6.2.12]
at org.springframework.web.method.support.InvocableHandlerMethod.invokeForRequest(InvocableHandlerMethod.java:191) ~[spring-web-6.2.12.jar:6.2.12]
at
......
2025-11-02T16:05:41.824+08:00 ERROR 28305 --- [tpl] [nio-8080-exec-4] o.c.t.i.aop.ApplicationExceptionHandler : 服务器异常
java.lang.RuntimeException: 运行失败
at org.cyk.tpl.api.TestApi.other(TestApi.kt:25) ~[classes/:na]
at java.base/jdk.internal.reflect.DirectMethodHandleAccessor.invoke(DirectMethodHandleAccessor.java:104) ~[na:na]
at java.base/java.lang.reflect.Method.invoke(Method.java:565) ~[na:na]
at kotlin.reflect.jvm.internal.calls.CallerImpl$Method.callMethod(CallerImpl.kt:97) ~[kotlin-reflect-2.2.20.jar:2.2.20-release-333]
at kotlin.reflect.jvm.internal.calls.CallerImpl$Method$Instance.call(CallerImpl.kt:113) ~[kotlin-reflect-2.2.20.jar:2.2.20-release-333]
at kotlin.reflect.jvm.internal.KCallableImpl.callDefaultMethod$kotlin_reflection(KCallableImpl.kt:250) ~[kotlin-reflect-2.2.20.jar:2.2.20-release-333]
at kotlin.reflect.jvm.internal.KCallableImpl.callBy(KCallableImpl.kt:155) ~[kotlin-reflect-2.2.20.jar:2.2.20-release-333]
at org.springframework.web.method.support.InvocableHandlerMethod$KotlinDelegate.invokeFunction(InvocableHandlerMethod.java:335) ~[spring-web-6.2.12.jar:6.2.12]
at org.springframework.web.method.support.InvocableHandlerMethod.doInvoke(InvocableHandlerMethod.java:255) ~[spring-web-6.2.12.jar:6.2.12]
at org.springframework.web.method.support.InvocableHandlerMethod.invokeForRequest(InvocableHandlerMethod.java:191) ~[spring-web-6.2.12.jar:6.2.12]
at
......
SpringBoot统一响应与异常处理
1664

被折叠的 条评论
为什么被折叠?



