技术知识点:
@ControllerAdvice全局异常处理、异常分类设计、责任链模式应用、统一错误码枚举、异常处理优先级
一、控制台的「红色狂欢」
周四下午,暴雨再次席卷城市。小彬盯着控制台里此起彼伏的红色异常,感觉自己像在观看一场失控的烟花秀。用户反馈任务删除功能报错,但每个接口的异常返回格式都不一样:有的返回JSON字符串,有的抛出500错误,最离谱的是某个接口直接返回HTML页面——那是Spring Boot默认的错误页面。
"这怎么前端怎么对接?"小浩的消息带着三个问号砸来,"我这边接口返回一会儿是{code: 500, msg: '异常'}, 一会儿是{error: 'Internal Server Error'}, 能不能统一成一种格式?"
小彬羞愧地翻开代码,只见每个Controller里都散落着try-catch块,异常处理逻辑像意大利面一样纠缠不清。更糟糕的是,他为了快速解决问题,在不同地方手动返回了不同格式的ResponseEntity,完全没考虑统一性。
"新人通病。"身后传来老王的声音。架构师穿着格子衬衫,保温杯里飘出枸杞香,"当年我带的实习生,在Controller里写了20个try-catch,最后代码变成了'异常迷宫',连他自己都找不到哪里抛的异常。"
二、老王的「设计模式手术刀」
老王拖过椅子,在白板上画起架构图:"Spring Boot有个神器叫@ControllerAdvice,能把全局异常处理抽离出来,就像给项目装了个'异常防火墙'。"他挥笔写下基础结构:
@RestControllerAdvice
public class GlobalExceptionHandler {
@ExceptionHandler(IllegalArgumentException.class)
public ResponseEntity<ErrorResponse> handleIllegalArgumentException(IllegalArgumentException ex) {
return buildResponse(400, ex.getMessage());
}
@ExceptionHandler(Exception.class)
public ResponseEntity<ErrorResponse> handleGlobalException(Exception ex) {
log.error("全局异常:", ex);
return buildResponse(500, "服务器内部错误");
}
private ResponseEntity<ErrorResponse> buildResponse(int status, String message) {
return ResponseEntity.status(status).body(new ErrorResponse(status, message));
}
}
class ErrorResponse {
private int code;
private String message;
// 构造器、Getter省略
}
"这叫'责任链模式'的变种,"老王敲了敲白板,"不同的异常处理器负责不同类型的异常,最后统一返回标准化的ErrorResponse。就像微服务里的网关,所有请求都经过统一处理再转发。"
小彬跟着修改代码,突然发现一个问题:"自定义异常怎么处理?比如我新增的TaskNotFoundException。"老王嘴角扬起微笑:"问得好!这时候就需要'异常工厂'来统一管理。"他演示着创建异常基类:
@Getter
public class BaseException extends RuntimeException {
private final int code;
public BaseException(int code, String message) {
super(message);
this.code = code;
}
}
// 具体异常继承基类
public class TaskNotFoundException extends BaseException {
public TaskNotFoundException(String message) {
super(404, message);
}
}
然后在全局处理器里添加专属处理方法:
@ExceptionHandler(TaskNotFoundException.class)
public ResponseEntity<ErrorResponse> handleTaskNotFoundException(TaskNotFoundException ex) {
return buildResponse(ex.getCode(), ex.getMessage());
}
"设计模式是代码的瑞士军刀,"老王拍了拍小彬的肩膀,"现在你的异常处理像军队一样整齐了。"
三、贝贝哥的「洁癖式重构」
代码改完后,小彬正想测试,贝贝哥突然出现在工位旁,手里的马克杯冒着"代码即文档"的热气:"听说你在搞异常处理?让我看看有没有冗余代码。"
他扫了眼GlobalExceptionHandler,皱起眉头:"每个异常处理方法都调用buildResponse,为什么不做成注解或抽象方法?"说着,他将代码重构为:
@RestControllerAdvice
public class GlobalExceptionHandler {
@ExceptionHandler({IllegalArgumentException.class, TaskNotFoundException.class})
public ResponseEntity<ErrorResponse> handleBusinessException(BaseException ex) {
return ResponseEntity.status(ex.getCode()).body(new ErrorResponse(ex.getCode(), ex.getMessage()));
}
@ExceptionHandler(Exception.class)
public ResponseEntity<ErrorResponse> handleGlobalException(Exception ex) {
log.error("全局异常:", ex);
return handleBaseException(500, "服务器内部错误");
}
private ResponseEntity<ErrorResponse> handleBaseException(int code, String message) {
return ResponseEntity.status(code).body(new ErrorResponse(code, message));
}
}
"代码洁癖第六条:消灭重复代码,让每个方法只做一件事。"贝贝哥摸出酒精湿巾擦了擦小彬的键盘,"另外,记得给ErrorResponse加@JsonInclude(Include.NON_NULL),避免返回多余的null字段——前端最讨厌这个。"
小彬照做后,发现接口返回变得更加清爽。贝贝哥满意地点头,从口袋里掏出张键盘贴纸:"这是老王设计的'异常防火墙'贴纸,贴上它,提醒自己异常处理要'早拦截、早处理、早统一'。"
四、茶水间的「异常美学」
午休时,小彬在茶水间遇到小浩。潮男正在用Swagger调试接口,卫衣帽子上的耳机线晃着《异常战歌》的节奏:"哟,异常格式统一了!现在前端能根据code自动弹提示了——建议你们把错误码做成枚举,别让我对着数字猜含义。"
小浩掏出手机展示前端代码:"你看,我现在可以这样写:
switch (res.data.code) {
case ErrorCode.TASK_NOT_FOUND:
Toast.error('任务未找到');
break;
// ...
}
"所以后端得定义统一的错误码枚举,"小浩咬了口三明治,"比如40001=参数错误,40002=任务不存在,这样前后端协作才高效。对了,红姐让我提醒你,异常测试用例必须覆盖所有自定义异常,她的'120%覆盖率'里,每个异常类型算10%。"
五、暴雨夜的「异常军演」
夜幕降临时,暴雨升级为雷暴。小彬留在办公室编写异常测试用例,按照红姐的要求,每个异常类型都要测试三种场景:正常抛出、日志记录、返回格式。当他写完最后一个测试时,发现老王居然还在工位上画架构图。
"在看异常处理?"老王披上雨衣,"给你个实战经验:生产环境要避免暴露敏感信息,所以全局异常里不要返回ex.printStackTrace(),用自定义消息代替。"他展示了一段代码:
@ExceptionHandler(Exception.class)
public ResponseEntity<ErrorResponse> handleGlobalException(Exception ex) {
log.error("异常堆栈:", ex); // 日志记录完整堆栈
return handleBaseException(500, "服务繁忙,请稍后再试"); // 给前端友好提示
}
"这叫'内外有别'原则,"老王拍了拍小彬的肩膀,"就像项目管理,对内要暴露细节找问题,对外要保持体面给客户。"
六、第二天的「红姐阅兵」
周五上午,红姐带着测试报告来到工位,短发在阳光下闪着金属光泽:"异常处理测试全部通过,而且错误码枚举的设计让测试用例覆盖率提升了30%。"她在报告上画了个红色的勾,"不过..."
小彬心里一紧。红姐翻到最后一页:"当同时抛出多个异常时,处理顺序是否正确?比如业务异常和系统异常冲突的情况。"
老王不知何时出现,替小彬解围:"这涉及到Spring的异常处理优先级,@ExceptionHandler标注的具体异常类优先于通用Exception。"他在IDEA里演示调试,异常链清晰地按优先级被捕获。
红姐满意地点头:"很好,看来你们理解了异常处理的核心逻辑。"她从文件夹里抽出一张卡片,"这是团队新推出的'异常终结者'电子勋章,图案是@ControllerAdvice的注解图标,配文'用设计模式驯服异常野兽'。"
尾声:键盘上的「防火墙」
下班前,小彬将"异常防火墙"贴纸贴在键盘ESC键上,每次按下都像按下一个安全开关。贝贝哥路过时,往他的工位上放了本《重构:改善既有代码的设计》,扉页写着:"异常处理的优雅程度,决定了代码的健壮性——代码洁癖留"。
手机震动,小浩发来消息:"你的异常处理方案被收录进《前后端协作白皮书》啦!"附带的截图里,错误码枚举和统一响应格式被列为最佳实践,作者栏有老王、贝贝哥、小浩的签名。文档结尾写着:"优秀的异常处理不是事后补救,而是事前设计,就像优秀的团队协作不是临时救火,而是流程先行。"
小彬望着窗外渐晴的天空,想起老王说的"设计模式是瑞士军刀",突然觉得每个技术点都是一块拼图,只有按正确的模式组合,才能拼出完整的系统架构。他在笔记本上写下:"异常不是敌人,而是系统健康的晴雨表。学会用全局思维处理异常,就像学会用团队视角解决问题——每个漏洞都是成长的入口。"
【下章预告】
《Java 程序员成长记(七):业务攻坚之Redis 锁「误删惨案」:老王的「UUID+Lua 防坑术」》