一、枚举类
一、 枚举实例的本质与使用
-
枚举实例的创建
- 枚举类的实例(如
SUCCESS、FAILED)由 JVM 自动创建 ,无需手动new。 - 枚举构造方法默认是
private,外部无法通过new新建实例,保证实例唯一性。 - 写法示例:
SUCCESS(0, "成功")是调用枚举构造方法初始化实例,SUCCESS是该实例的名称。
- 枚举类的实例(如
-
枚举实例的引用
- 外部通过
ResultCode.SUCCESS直接引用实例,本质是获取 JVM 提前创建好的单例对象。 - 示例:
ResultCode success = ResultCode.SUCCESS;是定义引用变量指向枚举实例,而非 "接收变量值"。
- 外部通过
二、 code 与 message 的设计要点
1. 类型选择:long 而非 int
| 选择 | 原因 |
|---|---|
| long | 1. 预留更大编码空间,避免与外部系统对接或业务扩展时数值溢出; 2. 符合 Dubbo、Spring Cloud 等微服务框架的行业使用习惯; 3. 支持负数状态码(如异常码、扩展码)的扩展场景。 |
| int | 最大值仅 21 亿,业务量级增长或跨系统对接时扩展能力有限,易出现数值溢出风险。 |
2. 修饰符:private final 的双重作用
| 修饰符 | 核心作用 | 解决的问题 |
|---|---|---|
| private | 控制访问权限,仅枚举类内部可直接访问 | 防止外部类直接读写变量,符合面向对象的封装性原则 |
| final | 强制变量不可修改,构造方法赋值后终身不变 | 防止枚举类内部方法误修改状态码/枚举值,保证运行时数据稳定 |
3. 常见误区澄清
-
误区 1 :不加
final,只要不写修改方法就没事。→ 风险:团队协作中新人可能误写修改方法,或通过反射强行修改,导致线上故障。→final是语法级强制锁,加了之后任何修改操作都会编译报错,从根源杜绝风险。 -
误区 2 :
ResultCode.SUCCESS.code可以直接修改或访问。→ 规范写法中code是private,外部无法直接访问,必须通过getCode()方法读取;→ 若code没加private,外部可直接.code访问,但会破坏封装性,不推荐。
4. Getter 方法的必要性
getCode()和getMessage()不是多余的,作用如下:- 提供外部访问变量的唯一安全入口;
- 便于框架进行序列化、JSON 转换等操作;
- 后续可在方法内添加逻辑(如多语言适配),不影响外部调用。
三、 兜底设计:解决未定义状态码的问题
1. 核心问题
直接使用枚举定义的实例,遇到未覆盖的 code 会导致程序异常。
2. 优化方案
-
步骤 1 :在枚举类中添加兜底实例
UNKNOWN。 -
步骤 2 :新增
getByCode()方法,根据code匹配枚举,匹配失败返回UNKNOWN。 -
示例代码:
public enum ResultCode {
SUCCESS(0, "成功"),
FAILED(1000, "操作失败"),
// ... 其他业务枚举项
UNKNOWN(-1, "未知错误"); // 兜底项private final long code; private final String message; ResultCode(long code, String message) { this.code = code; this.message = message; } public long getCode() { return code; } public String getMessage() { return message; } // 核心兜底方法 public static ResultCode getByCode(long code) { for (ResultCode rc : ResultCode.values()) { if (rc.code == code) { return rc; } } return UNKNOWN; }}
-
状态码区间规划(预防扩展混乱)
0-99:通用状态码(成功、失败、参数错误)
100-199:用户模块(登录、注册、用户状态)
200-299:订单模块(创建、支付、取消)
...
900-999:预留/兜底区间
实践总结
- 枚举实例属性必须加
private final,保证封装性 和不可变性; - 必须提供
getter方法,禁止外部直接访问属性; - 必须添加兜底项和
getByCode()方法,处理未定义状态码; - 提前规划状态码区间,便于业务扩展;
- 团队维护状态码文档,同步每个
code的含义和适用场景。
二、AppResult 统一返回结果类
一、核心定位与设计逻辑
- 本质 :前后端交互的统一数据容器,封装
code(状态码)、message(描述)、data(泛型业务数据),与ResultCode枚举配合(枚举存标准化状态码,AppResult 做数据封装)。 - 核心价值:保证接口返回格式统一,减少前后端沟通成本,适配 "成功 / 失败、有无数据、通用 / 个性化描述" 所有业务场景。
二、关键技术细节与疑问解答
1. Lombok 注解相关(@Data 核心细节)
| 修饰符/技术点 | 核心作用 | 解决的问题 |
|---|---|---|
| @Data | 自动生成getter/setter、equals()/hashCode()、toString()、无参构造 | 减少重复代码,提升开发效率 |
| 手动toString() | 仅输出code + message,精简日志输出 | 避免默认toString()打印冗余/敏感的data字段,提升日志可读性与数据安全性 |
| @JsonInclude(ALWAYS) | 保证null值字段在JSON中显示 | 让前端解析更统一,避免"字段不存在"的报错 |
| 无参构造 | 供Jackson序列化工具创建空对象 | 解决JSON序列化失败的问题 |
| 多构造方法 | 适配"有无data"的不同场景,复用代码 | 避免重复写赋值逻辑,简化不同场景下的对象创建 |
| this(code, message, null) | 构造方法复用,缺省data时显式传null | 让代码更简洁,语义更清晰 |
| 静态方法泛型 | 使静态方法能返回泛型类型的AppResult | 解决静态方法无法使用类级泛型的问题,让返回类型更灵活 |
| 语义化表示"无业务数据返回" | 比Object更清晰,避免调用方强转 | |
| 方法重载(success()/failed()多版本) | 适配"通用/指定枚举、默认/自定义描述、有无数据"所有场景 | 让调用更灵活,覆盖100%的业务返回需求 |
| JSON序列化 | SpringBoot自动将AppResult转成JSON返回前端 | 无需手动写序列化代码,保证前后端数据交互格式统一 |
| @Contract("-> new")/@NotNull | 提升代码严谨性,让IDE更好识别方法行为 | 告知IDE方法返回新对象且永不为null,优化静态检查体验 |
| 手动方法vs Lombok生成 | 手动写的toString()/构造方法优先级更高 | 无需修改@Data注解,直接覆盖默认生成的方法即可满足自定义需求 |
| @Data生成的equals()/hashCode() | 编译后生成,源码不可见 | 不主动调用则永远不会执行,对AppResult类无实际影响,属于"沉睡代码" |
2. 序列化与 toString () 核心区别(关键易混点)
| 修饰符/技术点 | 核心作用 | 解决的问题 |
|---|---|---|
| toString() 方法 | 后端打印日志、调试查看对象时触发,仅作用于 Java 后端(日志/调试) | ① 手动重写后只输出 code+message,精简日志、保护敏感数据;② 完全不影响前端/Postman 接收数据; |
| JSON 序列化 | SpringBoot 把 AppResult 对象转 JSON 返回前端时触发,作用于前后端数据交互(Postman/前端接收到的 JSON) | ① 全程自动:由 SpringBoot 的 Jackson 工具完成,无需手动写代码;② 独立于 toString():Jackson 直接读取 getter 方法获取 code/message/data,Postman 能完整看到 data 字段;③ @JsonInclude(ALWAYS):保证 data=null 时也会显示该字段,前端解析更统一。 |
3. 构造方法与无参构造
- 多构造方法的意义:适配 "有无 data" 场景(无参构造供 Jackson 序列化用,双参构造适配无数据场景,三参构造适配有数据场景);
this(code, message, null)逻辑:构造方法复用,缺省 data 时显式传 null,避免重复写赋值代码,语义更清晰。
4. IDE 提示注解(@Contract/@NotNull)
- 本质:IDEA 的 "优化建议",非必须改的错误;
- 作用 :
@Contract("-> new")告知 IDE 方法每次返回新对象,@NotNull声明返回值永不为 null; - 使用建议:不影响代码运行,可加(提升严谨性)也可忽略(省事儿)。
5. 泛型与方法重载
- 静态方法泛型 :静态方法无法使用类级泛型
<T>,需单独声明<T>才能返回AppResult<T>; <Void>作用 :语义化表示 "无业务数据返回",比Object更清晰;- 方法重载 :
success()/failed()多版本重载,根据参数个数 / 类型自动匹配,适配 "通用 / 指定枚举、默认 / 自定义描述、有无数据" 所有场景。
6. 序列化的执行过程
- 透明性:SpringBoot 自动完成 AppResult → JSON 的转换,无需手动写序列化代码;
- 核心依赖:无参构造 + getter 方法(Jackson 依赖这两个完成序列化,缺一不可);
- 最终结果 :前端 / Postman 收到的 JSON 是序列化产物,包含
code/message/data所有字段。
三、自定义异常处理体系(ApplicationException + GlobalExceptionHandler)
1. 核心组件分工
两个核心类是异常载体 和异常处理器的关系,缺一不可:
- ApplicationException(自定义异常类) :继承
RuntimeException,作为业务异常的载体,封装AppResult对象(包含状态码、提示信息、数据),提供多构造方法适配不同场景(传完整AppResult、传提示字符串、包装底层异常)。 - GlobalExceptionHandler(全局异常处理器) :通过
@ControllerAdvice+@ExceptionHandler实现全局异常捕获,是真正处理异常并返回标准化结果的类。
2. 核心执行流程
- 业务层抛异常 :在 Service 层(如
BoardService.getBoardById)执行业务校验(如板块不存在),构造AppResult并抛出new ApplicationException(errorResult)。异常抛出后,当前方法立即终止,后续代码(如return board)不再执行。 - 全局处理器捕获异常 :Spring 自动拦截异常,匹配对应
@ExceptionHandler方法:- 若为
ApplicationException:优先返回异常中封装的AppResult(精准业务提示);无封装则返回默认 500 结果。 - 若为其他
Exception(如空指针、SQL 异常):返回预定义的 "系统繁忙" 提示(ResultCode.ERROR_SERVICES),隐藏技术细节,保护系统安全。
- 若为
- 前端接收结果 :所有异常最终都转为
AppResult格式的 JSON 响应,前端无需适配多种异常格式。
3. 疑问解答
super(errorResult.getMessage())与this.errorResult顺序问题 :super()必须是构造方法第一行(Java 语法规则),作用是给父类RuntimeException传提示信息;this.errorResult是存储完整业务结果,两者分工独立,互不影响。- 为什么系统异常要返回
ResultCode.ERROR_SERVICES:这是兜底逻辑 ,仅针对未主动抛出ApplicationException的系统异常(如空指针),和业务异常的errorResult是两条完全独立的处理路径,互不干扰。
二、 登录拦截器实现(LoginInterceptor + AppInterceptorConfigurer)
1. 核心实现思路(先定义、再注册)
拦截器的实现遵循 "定义拦截逻辑 → 注册拦截范围" 的标准流程,是 SpringMVC 拦截请求的核心方式。
(1)拦截器逻辑类(LoginInterceptor):定义 "如何拦截"
实现HandlerInterceptor接口,包含 3 个核心方法(完整生命周期):
| 方法名 | 执行时机 | 返回值 / 核心作用 | 登录拦截器中的应用 |
|---|---|---|---|
| preHandle | Controller 执行前 | 返回boolean:true放行、false拦截 | ✅ 核心方法:校验 Session 中是否有用户登录信息(USER_SESSION),未登录则跳转登录页,直接终止请求流程;已登录则放行 |
| postHandle | Controller 执行后、视图渲染前 | 无返回值,可修改ModelAndView | ❌ 几乎不用:登录拦截只需 "前置校验",此时 Controller 已执行,拦截无意义;仅在非前后端分离项目中可补充视图数据(如添加登录用户信息) |
| afterCompletion | 视图渲染完成后(请求结束) | 无返回值,可捕获异常、释放资源 | ❌ 登录拦截无需使用;可扩展用于请求耗时统计、异常日志记录(如通过preHandle记录开始时间,此处计算耗时) |
拦截器postHandle/afterCompletion的价值:登录拦截无需使用,但可用于接口监控(耗时统计)、资源释放,是拦截器生命周期的补充,非核心但不可或缺。
2. LoginInterceptor(拦截器逻辑类)
作用:定义单个请求的拦截规则(判断是否登录、未登录跳转逻辑)。
| 代码内容 | 类型 | 作用说明 |
|---|---|---|
| implements HandlerInterceptor | 固定写法 | SpringMVC 拦截器的核心接口,必须实现 |
| @Value("${forum.login.url}") | 自定义 | 从配置文件注入登录页 URL,适配项目配置 |
| preHandle(...) 方法签名 | 固定写法 | 请求到达 Controller 前执行的核心方法,参数和返回值不可改 |
| request.getSession(false) | 固定写法 | 只获取已有 Session,不新建(避免给未登录用户创建无用 Session) |
| session != null && session.getAttribute(AppConfig.USER_SESSION) != null | 自定义逻辑 | 双重 null 检查:先判断 Session 是否存在(避免空指针),再判断是否有用户信息(判断登录状态) |
| response.sendRedirect(defaultURL) | 固定 + 自定义 | 未登录强制跳转登录页,跳转 URL 为自定义配置 |
| return true/false | 固定写法 | true放行请求,false拦截请求 |
3. AppInterceptorConfigurer(拦截器注册类)
作用:配置拦截器的生效范围(拦截哪些请求、放行哪些请求)。
| 代码内容 | 类型 | 作用说明 |
|---|---|---|
| implements WebMvcConfigurer | 固定写法 | SpringMVC 配置拦截器的核心接口 |
| @Resource private LoginInterceptor loginInterceptor | 固定写法 | 注入拦截器实例(不可手动 new,需 Spring 管理) |
| addInterceptors(InterceptorRegistry registry) | 固定写法 | 注册拦截器的核心方法 |
| registry.addInterceptor(loginInterceptor) | 固定写法 | 注册自定义拦截器 |
| .addPathPatterns("/**") | 固定策略 | 先拦截所有请求,是最安全的配置方式(避免漏拦截) |
| .excludePathPatterns(...) | 自定义配置 | 放行必须公开的路径,理由如下: 1. 登录 / 注册页 + 接口(如/sign-in.html、/user/login):不放行会导致死循环(未登录→拦截→跳登录页→又拦截); 2. 静态资源(/js/、/image/ ):不放行会导致页面样式、图片加载失败; 3. Swagger 相关路径:开发阶段放行,方便调试接口 |
4. 关键配置补充(易踩坑点)
缺少两个关键注解,会导致拦截器不生效:
- 给
LoginInterceptor加**@Component**:让 Spring 管理该类,否则无法注入。 - 给
AppInterceptorConfigurer加**@Configuration** :让 Spring 识别为配置类,否则addInterceptors方法不会执行。
5. 关键疑问解答
- 为什么先全拦截再排除:相比 "指定拦截路径",该策略更安全、易维护。新增业务接口时无需修改拦截规则,默认拦截;只需维护 "放行列表",避免漏拦截未登录可访问的接口。
session双重 null 检查的原因 :session != null检查 Session 对象是否存在(避免空指针);session.getAttribute(...) != null检查 Session 中是否有用户信息(判断是否登录),两者缺一不可。- Session 创建时机 :只有调用
request.getSession()或request.getSession(true)且无有效 Session 时才会创建,最常见场景是用户登录成功后主动创建 ,拦截器中用request.getSession(false)避免给未登录用户创建 Session。
三、 其他细节补充
AppConfig包路径问题 :AppConfig是全局常量类(如定义USER_SESSION),建议放common包(存放通用常量、工具类),而非config包(存放 Spring 配置类),符合包结构规范。@Data注解的使用建议 :AppResult作为全局返回结果类,不建议用@Data(会生成多余的equals()/hashCode()方法),推荐用@Getter + @Setter + @NoArgsConstructor + @AllArgsConstructor,更轻量规范。
四、 整体设计理念
本次实现的异常处理和登录拦截器,遵循 "统一规范、安全兜底、易于维护" 的后端开发原则:
- 异常处理:统一响应格式,区分业务异常和系统异常,既给前端精准提示,又保护后端技术细节。
- 登录拦截:采用 "全拦截 + 排除放行" 策略,确保所有业务请求都经过登录校验,避免权限漏洞。
四、接口文档配置全维度总结
一、 SpringDoc 接口文档配置(适配 Spring Boot 3.x + JDK17)
1. 核心定位与价值
替代停止维护的 Springfox Swagger,原生支持 Jakarta EE(Spring Boot 3.x 核心依赖),解决 "接口文档自动生成、在线调试" 问题,提升前后端协作效率。
2. 核心配置项(application.yml):可选但建议配置
| 配置项 | 核心作用 | 是否必配 | 关键说明 |
|---|---|---|---|
| packages-to-scan: com.example.forum.controller | 指定扫描的 Controller 包 | 否 | 过滤第三方依赖的 Controller(如 Swagger 内部接口),仅显示业务接口,文档更纯净 |
| swagger-ui.path: /swagger-ui.html | 自定义文档访问路径 | 否 | 简化默认路径(/swagger-ui/index.html),符合使用习惯 |
| persistAuthorization: true | 开启认证信息持久化 | 否 | 文档页面输入的 token 存入浏览器 localStorage,刷新页面不丢失(仅优化调试体验,与业务登录状态无关) |
| group-configs | 按模块拆分接口文档 | 否 | 核心是按 Controller 接口路径前缀匹配(Ant 通配符),示例: - group: "用户模块" paths-to-match: "/user/**" |
3. 分组配置核心规则
- 分组依据:仅基于 Controller 层(接口路径 / 包名 / 注解),与 DAO 层完全无关(DAO 是内部数据操作层,不对外暴露);
- 匹配规则:
/user/**匹配所有以/user为前缀的接口(如/user/login、/user/info/123),前缀完全一致才会匹配; - 扩展方式:支持多路径匹配(如
paths-to-match: "/article/**", "/comment/**"),也可按 Controller 包名 /@Tag注解分组(复杂场景)。
4. 兼容与适配要点
(1)注解适配(Springfox → SpringDoc)
- 兼容注解:
@ApiOperation/@ApiParam/@ApiModel等可直接复用; - 推荐替换:
@Api(tags = "xxx")→@Tag(name = "xxx"),@ApiIgnore→@Operation(hidden = true); - 核心类适配:
io.swagger.v3.oas.models.info.Contact无多参数构造方法,需用无参构造 +setXxx():
(2)Knife4j 增强 UI(可选)
- 定位:SpringDoc 原生 UI 的升级版,兼容所有 OpenAPI 规范,优化交互体验(如支持接口导出、暗黑模式);
- 核心区别:原生 UI 简洁但简陋,Knife4j 更符合国内开发习惯,新增访问路径
/doc.html,原生路径仍可用; - 建议:开发 / 测试阶段必加,生产环境可按需移除。
5. 配置验证步骤
- 启动项目,访问
http://127.0.0.1:58080/swagger-ui.html(或/doc.html); - 分组验证:顶部下拉框可切换 "用户模块""文章模块",仅显示对应接口;
- 认证持久化验证:输入 token 后刷新页面,token 仍存在,调试接口无需重新输入。
二、 设计理念与实践
1. 接口文档:简洁实用,适配性优先
- 配置原则:核心配置(
OpenAPI Bean + title/version)必配,扩展配置(分组、认证持久化)按需加; - 体验优化:
persistAuthorization: true是调试带 token 接口的 "刚需优化",packages-to-scan是 "防御性配置"; - 兼容性:优先用 yaml 配置(简单需求),复杂分组(按包 / 注解)再补 Java 配置,yaml 优先级高于 Java 配置。
三、疑问解答
persistAuthorization: true与业务登录状态的区别:该配置仅针对 "文档工具的 token 持久化"(如 Postman 记住 token),与业务层面的 Session 登录状态无关,刷新业务页面不会重新登录是 Session 的作用,而非该配置。packages-to-scan加与不加的区别:即使所有 Controller 都在一个包下,配置后可过滤第三方接口,文档更纯净,是 "规范大于功能" 的防御性配置。