资讯中心

Java异常学习[特殊字符]

📅 2026/8/19 11:12:30
Java异常学习[特殊字符]
三板斧我先不给你讲代码我先给你讲场景。假设你现在是一个外卖平台的后端开发你写了一个“根据用户ID查订单”的接口。正常情况返回一个订单JSON。异常情况1用户传的ID是负数你忘了校验程序抛出IllegalArgumentException。异常情况2数据库连接突然断了抛出SQLException。如果没有统一响应和全局异常处理会发生什么前端小哥调用你的接口收到的不再是JSON而是一大串带着时间戳、包名、行号的Tomcat红字报错HTML页面500状态码。前端无法解析HTMLAPP直接闪退或白屏。这就是生产环境的“必崩点”。今天这一课就是教你如何用三把钥匙把这扇“地狱之门”锁死。第一把钥匙统一响应对象 ResultT定义契约无论成功还是失败给前端返回的格式必须长一模一样。我们定义一个泛型类ResultTjavaimport com.fasterxml.jackson.annotation.JsonInclude; // 泛型T代表你要返回的具体数据比如订单列表、用户信息 JsonInclude(JsonInclude.Include.NON_NULL) // 如果data为空就不序列化这个字段 public class ResultT { // 1. 状态码200表示成功其他表示各种失败 private Integer code; // 2. 提示消息给前端或者用户看的文字 private String message; // 3. 具体的数据泛型可以是任何对象 private T data; // 私有构造器不允许外面new必须通过静态方法创建 private Result(Integer code, String message, T data) { this.code code; this.message message; this.data data; } // 静态方法成功时调用只需要传数据 public static T ResultT success(T data) { return new Result(200, 操作成功, data); } // 静态方法成功但没有数据返回比如删除接口 public static T ResultT success() { return new Result(200, 操作成功, null); } // 静态方法失败时调用需要传错误码和消息 public static T ResultT error(Integer code, String message) { return new Result(code, message, null); } // 下面省略 getter / setter / toString ... }重点理解code不是HTTP状态码那是网络传输用的这是我们业务状态码。我们约定200即一切正常1001参数错误1002未登录1003库存不足等。第二把钥匙自定义业务异常弹药库你不能把所有异常比如空指针都直接抛给用户看太Low了。我们需要自定义一个异常专门代表“业务上出了岔子”。java// 继承 RuntimeException因为Spring事务回滚默认只认RuntimeException public class BusinessException extends RuntimeException { private Integer code; // 业务错误码 public BusinessException(Integer code, String message) { super(message); // 把消息交给父类 this.code code; } public Integer getCode() { return code; } // 为了方便加一个快速抛出“参数错误”的静态方法 public static BusinessException paramError(String msg) { return new BusinessException(1001, msg); } }为什么一定要继承RuntimeException因为受检异常throws Exception会污染你的接口声明而运行时异常可以“无声无息”地往上冒配合我们第三把钥匙直接截停。第三把钥匙全局异常处理器终极拦截网这是最关键的核心——RestControllerAdvice。它就像一个AOP切面面向切面编程包裹在所有Controller的外层。所有Controller抛出的异常都会先经过这里。javaimport org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.web.bind.annotation.ExceptionHandler; import org.springframework.web.bind.annotation.RestControllerAdvice; RestControllerAdvice // 等同于 ControllerAdvice ResponseBody public class GlobalExceptionHandler { private static final Logger log LoggerFactory.getLogger(GlobalExceptionHandler.class); // 1. 拦截我们自定义的业务异常 ExceptionHandler(BusinessException.class) public ResultVoid handleBusinessException(BusinessException e) { // 打印一下日志方便排查问题 log.warn(业务异常发生code{}, msg{}, e.getCode(), e.getMessage()); // 取出我们自定义异常里的code和message封装成统一的Result返回 return Result.error(e.getCode(), e.getMessage()); } // 2. 拦截系统内置异常比如参数校验失败、数字格式错误等 ExceptionHandler(IllegalArgumentException.class) public ResultVoid handleIllegalArg(IllegalArgumentException e) { log.warn(参数非法{}, e.getMessage()); // 统一返回 1001 参数错误 return Result.error(1001, 请求参数不合法 e.getMessage()); } // 3. 终极兜底拦截所有未知异常Exception.class // 注意这里异常范围最大必须写在最下面否则会覆盖上面的细粒度拦截 ExceptionHandler(Exception.class) public ResultVoid handleException(Exception e) { // 这种异常是程序员没预料到的必须打印完整的错误堆栈方便我们修复Bug log.error(系统未知异常, e); // 绝对不能把堆栈信息返回给前端只给一个友好的模糊提示 return Result.error(500, 系统繁忙请稍后重试); } }完整运行流程推演极其重要我们写一个Controller来实际跑一遍逻辑javaRestController RequestMapping(/user) public class UserController { GetMapping(/get) public ResultUser getUser(Integer age) { // 假设业务要求年龄必须大于0 if (age null || age 0) { // 1. 手动抛出我们的自定义业务异常 throw BusinessException.paramError(年龄必须大于0); } // 2. 假如你忘了判断age传了个字符串abcSpring自动转int时会抛出 IllegalArgumentException // 3. 假如数据库挂了会抛出 SQLException被Exception兜底捕获 User user new User(张三, age); return Result.success(user); } }前端调用/user/get?age-5Controller 走到if抛出BusinessException(1001, 年龄必须大于0)。Spring 检测到异常寻找ExceptionHandler。找到handleBusinessException拦截。返回 JSON{code:1001, message:年龄必须大于0, data:null}。前端拿到规范的JSON弹窗提示“年龄必须大于0”完美。前端调用/user/get?ageabcSpring 尝试将 abc 转为 Integer失败抛出IllegalArgumentException。找到handleIllegalArg。返回 JSON{code:1001, message:请求参数不合法...}。前端依然拿到规范JSON。假如代码里有个隐藏的空指针user.getName().length()但 user 是 null抛出NullPointerException。前两个ExceptionHandler不匹配走最后的handleException。后台打印完整的红字堆栈方便运维看返回 JSON{code:500, message:系统繁忙请稍后重试, data:null}。前端拿到JSON显示“服务器开了个小差”页面不白屏。关于你的假期安排回应图片里的建议图片里说“比Transactional还优先”这里我要给你掰开揉碎讲清楚并不是说“启动顺序”优先Spring加载时事务和Advice各有各的初始化逻辑。它的意思是学习优先级和代码底线优先级。你搭新项目第一件事就是建Result和GlobalExceptionHandler。因为这决定了你整个项目的“交流语言”。Transactional是业务逻辑层面的技术如果连异常都拦不住、返回都不统一事务写得再好前端拿到的也是乱码项目根本没法联调。所以利用假期第一天把这三把钥匙敲一遍跑通一个带异常拦截的HelloWorld这叫“打地基”。进阶小贴士面试加分项不要捕获Throwable它包含Error比如OutOfMemoryError这种错误程序无法恢复不应该由业务层拦截。参数校验框架ValidSpring Boot 结合Valid校验 Bean 时会抛出MethodArgumentNotValidException你需要在全局异常处理器里单独加一个拦截方法把校验失败的具体字段信息取出来拼到message里。日志一定要分级别自定义业务异常用log.warn运维不需要看堆栈未知异常用log.error必须打印堆栈否则出Bug你不知道哪里错了。好了今天的课就到这里。课后作业把你之前写的所有Controller的try-catch全部删掉用这三把钥匙重构一遍。你会发现你的Controller里只剩下纯粹的“业务逻辑”再也不用写恶心的try-catch了——这就是“全局异常处理”优雅的地方。下课有不懂的随时举手提问。根据你提到的“统一响应”和“全局异常处理”问题我以课堂形式为你完整拆解了这三把核心钥匙ResultT统一响应对象、自定义BusinessException、以及RestControllerAdvice全局拦截器。这三者配合后你的所有接口异常都会被拦截成规范的JSONTomcat的错误页再也不会抛给前端。缺一不可我把它称为“异常处理铁三角”缺了任何一个你的接口防线都会出现致命漏洞。为了让你心服口服我们来做一次“缺一不可”的破坏性实验你看看少了某个组件会发生什么1. 如果只写ResultT不写GlobalExceptionHandler缺了拦截网你确实定义了统一的返回格式但没人用它。Controller里你写了return Result.success(data);正常时没问题。一旦抛出异常比如空指针代码直接跳过后面的returnTomcat拿到异常后找不到任何Result对象直接把默认的HTML错误页抛给前端。结果前端依然收到白屏或500 HTML页面你的Result成了摆设。2. 如果写了ResultT和GlobalExceptionHandler但不写BusinessException缺了弹药库你的拦截器确实能把所有异常拦下来返回{code:500, message:系统繁忙}。问题来了如果只是“用户年龄传了负数”这种业务小错误前端收到的也是code:500系统错误。前端无法区分“这个弹窗是警告用户输入错误”还是“服务器真的挂了”。因为你只能抛NullPointerException或IllegalArgumentException全局拦截器只能根据异常类型粗略处理无法携带业务自定义的错误码如1001和业务提示语。结果前端只能对所有异常统一弹“系统错误”用户体验极差而且后端排查日志时满屏都是ERROR日志无法区分业务警告和系统Bug。3. 如果写了BusinessException和GlobalExceptionHandler但不写ResultT缺了统一格式你在拦截器里可以返回数据了比如return Map.of(code, e.getCode(), msg, e.getMessage());问题成功时 Controller 返回的是User对象{name:张三,age:18}失败时拦截器返回的是Map{code:1001,msg:年龄错误}。前端小哥得写两套解析逻辑如果是Map就取msg如果是User就直接渲染。而且字段名一会儿叫msg一会儿叫message维护起来想打人。结果响应格式不统一接口文档无法标准化联调效率极低。正确的“铁三角”分工图帮你刻在脑子里组件角色定位核心职责能不能省ResultT通讯契约定义成功/失败返回的统一外壳code/message/data绝对不能否则格式不统一BusinessException弹药/信使在业务层发现错误时携带专属错误码和提示语向上抛出绝对不能否则无法区分业务错误和系统BugGlobalExceptionHandler拦网/交警在系统最外层统一拦截所有异常决定用什么Result壳子包裹返回绝对不能否则异常直接逃逸到Tomcat编写的顺序建议假期实操步骤虽然是“铁三角”但你敲键盘得有个先后顺序我建议你按这个步骤来逻辑最顺先写ResultT定义好标准外壳长什么样。再写BusinessException定义好弹药怎么生产。最后写GlobalExceptionHandler这是组装车间把前面两个组合起来并把Spring自带的异常也映射成我们的Result。写GlobalExceptionHandler时一定要把兜底的ExceptionHandler(Exception.class)放在最后因为Spring匹配拦截器是按子类优先原则越具体越先匹配如果你把Exception.class写在最上面那所有异常都被它拦了下面的BusinessException就永远无法触发。敲黑板总结只有三个类同时存在且正确配置你的项目才算真正穿上了“防弹衣”。这是一个标准化的工业级方案不是可选项是必选项。在任何一个成熟的开源项目如若依、Spring Cloud Alibaba里你都能看到这三者的身影。假期第一天别急着搞Transactional事务先把这三个类敲出来跑通一个带BusinessException的测试接口看到前端稳稳收到{code:1001, message:...}时你的地基就打牢了。代码书写第一类Java语言自带的“硬骨头”固定必须这么写这些是JDKJava开发工具包里祖宗定好的类名你改一个字母IDE集成开发环境就会报红没得商量。Exception所有异常的根类必须这么写。IllegalArgumentExceptionJDK自带的“非法参数异常”类必须这么写。RuntimeException运行时异常的根类继承它才能保证事务回滚必须这么写。你的心态把它们当成汉字里的“你、我、他”不是你自己发明的是语言本身就有的直接拿来用就行。第二类框架Spring规定的“固定格式”固定必须这么写这些是Spring框架规定好的注解和类名你改了Spring就不认识你了。RestControllerAdvice注解名一个字都不能错。ExceptionHandler注解名一个字都不能错。org.slf4j.Logger和LoggerFactory这是日志门面SLF4J的固定API应用程序接口。注意虽然类名固定但获取它的写法套路是固定的你直接复制粘贴下面这行不需要理解为什么背下来当万能公式private static final Logger log LoggerFactory.getLogger(你的类名.class);第三类日志方法的名字半固定但你只用常用的那几个log.warn()、log.error()、log.info()这是Logger对象自带的方法名你不能改成log.warning()或log.err()否则报错。但是你不需要全部记住你只要记住业务异常自己抛的用warn警告。系统未知异常Bug用error错误。调试信息用info信息。怎么偷懒敲log.之后IDE会弹出一长串列表warn, error, info, debug...你用鼠标点或者上下键选就行根本不用背全名第四类你完全可以自己起名的“活名字”不固定随便改这是你最担心的部分但恰恰是最自由的部分你原来代码里的handleException完全可以改成任何你喜欢的名字。handleBusinessException你可以改成dealBuzEx、catchMyError、helloWorld都行唯一绑定关系只要ExceptionHandler(异常类名.class)括号里的异常类写对了方法名随便你怎么起。举个例子下面两种写法完全等价功能一模一样java// 写法1我起的名字 ExceptionHandler(BusinessException.class) public ResultVoid myLittlePony(BusinessException e) { ... } // 写法2你起的名字 ExceptionHandler(BusinessException.class) public ResultVoid 随便叫啥都行(BusinessException e) { ... }唯一不能改的是括号里的BusinessException.class因为那是告诉Spring“当出现这种异常时来找我这个方法。”攻克“自己写就不会”的终极心法你觉得自己不会写是因为你在凭记忆敲代码。真正的开发高手从来不背API他们的秘诀是搭好骨架模板复制粘贴把GlobalExceptionHandler这个类建好里面的ExceptionHandler空方法体全部敲一遍或者从我给你的答案里复制过来。这个骨架一辈子只用敲一次以后所有项目直接复制这个类过去微调。写业务逻辑时只关注throw new你在Service层只需要写throw new BusinessException(1001, 年龄不对)。至于谁去捕获、谁去返回JSON根本不用管那是全局异常处理器自动干的脏活累活。利用IDE的补全快捷键你敲ExceptionHan按一下Tab或EnterIDEA自动帮你补全完整的ExceptionHandler(Exception.class)。你只需要把括号里的Exception改成BusinessException就行。给你的“傻瓜式”开工指令照抄就行下次新建项目直接把这坨代码原封不动粘进去只改类名其他一个字都别动javaRestControllerAdvice public class GlobalExceptionHandler { private static final Logger log LoggerFactory.getLogger(GlobalExceptionHandler.class); // 这一行固定死 // 这里的方法名你随便改但括号里的 BusinessException.class 不能改 ExceptionHandler(BusinessException.class) public ResultVoid handleBiz(BusinessException e) { log.warn(业务报错{}, e.getMessage()); // warn是固定的但你可以不背敲log.选它 return Result.error(e.getCode(), e.getMessage()); } // 这里的方法名你也可以随便改但括号里的 Exception.class 不能改 ExceptionHandler(Exception.class) public ResultVoid handleUnknown(Exception e) { log.error(系统炸了, e); // error是固定的敲log.选它 return Result.error(500, 系统繁忙); } }记住一句话在Java里只有class、interface、enum、interface关键字、以及注解的名字和括号里的异常类型是死命令其他方法名、变量名全是你的自由发挥空间。疑问如何结合Valid参数校验处理MethodArgumentNotValidException把校验错误细节也统一返回。或者聊一聊ControllerAdvice与RestControllerAdvice的区别以及多个异常处理器之间的执行顺序Valid参数校验这个问题问到点子上了Valid参数校验 MethodArgumentNotValidException处理是实际开发中最高频的异常拦截场景没有之一。我现在就带你一步步把这块硬骨头啃下来。学完这一课你会彻底告别在Controller里写几十行if (xxx null)的“体力活”。第一步先检查你的“弹药库”依赖如果你用的是Spring Boot 2.x / 3.xValid注解不在spring-boot-starter-web里它在一个专门的依赖里。千万注意如果你导入Valid时发现只能导入javax.validation或jakarta.validation但代码不生效大概率是缺依赖。Maven 用户在pom.xml里加上xmldependency groupIdorg.springframework.boot/groupId artifactIdspring-boot-starter-validation/artifactId /dependencyGradle 用户implementation org.springframework.boot:spring-boot-starter-validation为什么强调这个很多新手在这卡了半天以为是代码写错了结果是依赖没引进来。这是第一道门槛。第二步定义接收参数的实体类DTO我们不再写String name, Integer age这种散装参数了而是封装成一个实体类并在字段上贴上校验注解。javaimport jakarta.validation.constraints.*; // 注意Spring Boot 3 用 jakartaBoot 2 用 javax public class UserRegisterDTO { NotNull(message 用户ID不能为空) // 不能为 null Min(value 1, message 用户ID必须大于0) private Integer userId; NotBlank(message 用户名不能为空) // 不能是 null / 空字符串 / 纯空格 Size(min 2, max 10, message 用户名长度必须在2-10之间) private String name; Email(message 邮箱格式不正确) // 校验邮箱格式 NotBlank(message 邮箱不能为空) private String email; // 省略 getter / setter / toString }这是固定的“语法糖”NotNull、NotBlank、Size这些注解的名字是固定死的但括号里的message属性你可以随便改文字。第三步在 Controller 里标记Valid触发校验的“开关”这是最关键的一步如果你的 Controller 参数忘了写Valid那么你上面写的那些NotNull注解完全不会生效形同虚设。javaRestController RequestMapping(/user) public class UserController { // 注意Valid 必须写在 RequestBody 旁边 PostMapping(/register) public ResultString register(Valid RequestBody UserRegisterDTO dto) { // 如果校验通过代码才会走到这里 System.out.println(校验通过 dto.getName()); return Result.success(注册成功); } }公式ValidRequestBody 自动触发校验。第四步在全局异常处理器中精准拦截敲黑板核心代码当校验失败时Spring 不会抛BusinessException而是抛出一个专门的异常MethodArgumentNotValidException。我们需要在GlobalExceptionHandler里新增一个“专案组”来拦截它并把校验失败的具体字段细节扒出来塞进我们的Result里。工业级标准写法把错误细节放在data里返回javaimport org.springframework.web.bind.MethodArgumentNotValidException; import org.springframework.validation.FieldError; RestControllerAdvice public class GlobalExceptionHandler { private static final Logger log LoggerFactory.getLogger(GlobalExceptionHandler.class); // ... 拦截 BusinessException 和 Exception 的代码保持不变 ... /** * 专门拦截 Valid 校验失败的异常 */ ExceptionHandler(MethodArgumentNotValidException.class) public ResultMapString, String handleValidationException(MethodArgumentNotValidException e) { log.warn(参数校验失败{}, e.getMessage()); // 1. 获取所有的字段错误FieldError // FieldError 包含了哪个字段field、错误消息defaultMessage MapString, String errorMap new HashMap(); for (FieldError fieldError : e.getBindingResult().getFieldErrors()) { // 把 字段名 和 错误提示 放进 Map errorMap.put(fieldError.getField(), fieldError.getDefaultMessage()); } // 2. 返回统一的 Resultcode 可以定为 400客户端参数错误 // 把错误细节放在 data 字段里前端拿到后可以高亮显示每个输入框的错误 return Result.error(400, 请求参数不合法, errorMap); } }如果你不想返回 Map只想返回一句完整的提示语给前端弹窗偷懒写法javaExceptionHandler(MethodArgumentNotValidException.class) public ResultVoid handleValidationException(MethodArgumentNotValidException e) { // 取出第一个错误提示拼成一句话 String message e.getBindingResult().getFieldErrors().stream() .map(FieldError::getDefaultMessage) .collect(Collectors.joining()); return Result.error(400, message); }我强烈推荐第一种返回Map的方式因为前端可以精准知道“哪个输入框红了”用户体验更好这才是企业级做法。第五步完整的“铁三角 Valid”串联演示假设前端传了一个错误的 JSONjson{ userId: -5, name: 张, email: abc }运行流程推演Spring 接收到请求将 JSON 转为UserRegisterDTO对象。发现参数上有Valid开始逐个校验字段。userId-5违反Min(1)→ 生成 FieldError字段userId消息用户ID必须大于0name张长度为1违反Size(min2)→ 生成 FieldErroremailabc不满足邮箱格式 → 生成 FieldError校验失败Spring 立即抛出MethodArgumentNotValidException不会进入 Controller 的业务代码。全局异常处理器中的handleValidationException捕获它。遍历e.getBindingResult().getFieldErrors()组装成 Map。返回 JSON 给前端json{ code: 400, message: 请求参数不合法, data: { userId: 用户ID必须大于0, name: 用户名长度必须在2-10之间, email: 邮箱格式不正确 } }前端拿到这个 JSON直接遍历data对象在每个输入框底下显示对应的红色提示文字。完美特别补充Valid和Validated的区别面试/笔试必问注解来源作用范围能不能用分组校验ValidJava标准JSR-303方法参数、属性嵌套校验❌ 不支持ValidatedSpring 提供类、方法、参数✅ 支持Groups对于 Controller 层的单层参数校验用Valid就够了。如果遇到嵌套对象校验比如UserDTO里有个AddressDTO需要校验AddressDTO的内部字段需要在嵌套对象上也加上Valid。给你的“傻瓜式”终极整合模板直接复制到你的项目里javaRestControllerAdvice public class GlobalExceptionHandler { private static final Logger log LoggerFactory.getLogger(GlobalExceptionHandler.class); // 1. 业务异常 ExceptionHandler(BusinessException.class) public ResultVoid handleBusiness(BusinessException e) { log.warn(业务异常{}, e.getMessage()); return Result.error(e.getCode(), e.getMessage()); } // 2. Valid 校验异常重点 ExceptionHandler(MethodArgumentNotValidException.class) public ResultMapString, String handleValid(MethodArgumentNotValidException e) { log.warn(参数校验失败{}, e.getMessage()); MapString, String errors e.getBindingResult().getFieldErrors().stream() .collect(Collectors.toMap( FieldError::getField, // key 字段名 FieldError::getDefaultMessage, // value 错误消息 (old, newVal) - newVal // 如果有重复key取新的 )); return Result.error(400, 参数校验失败, errors); } // 3. 系统内置异常兜底 ExceptionHandler(Exception.class) public ResultVoid handleUnknown(Exception e) { log.error(系统未知异常, e); return Result.error(500, 系统繁忙请稍后重试); } }注意如果你用的 Spring Boot 2import路径是javax.validation和javax.validation.constraints如果是 Spring Boot 3则是jakarta.validation。复制代码时留意红线的导包提示IDEA 会帮你自动修正。分组校验这个问题问得很好它触及了Valid和Validated一个核心的区别分组校验Validation Groups。简单来说Valid注解本身不支持分组。要实现“按需校验”我们需要把Valid换成 Spring 提供的Validated注解。分组校验的核心思想是给校验规则打上“标签”分组然后在需要的时候只触发带有特定“标签”的规则。一个字段上的注解可以属于一个或多个组当 Controller 中的Validated指定了某个组时只有属于该组的校验注解才会生效。下面我们通过三个步骤来解决你的“进阶挑战”步骤一定义分组接口创建“标签”首先我们需要创建两个空的 Java 接口作为我们的“标签”。你可以把它们放在UserRegisterDTO类的同一个文件里或者单独创建。java// 1. 定义一个“只要UserId”的分组作为我们的“标签” public interface OnlyUserIdValidation { } // 2. 可选为了更好的可读性你也可以定义一个包含所有校验的“默认”分组[reference:6] // 通常不显式定义也没问题不指定分组就默认是 Default.class步骤二给校验注解贴上“标签”然后修改你的UserRegisterDTO类。在NotNull和Min注解中通过groups属性指定它们属于哪个组。javaimport jakarta.validation.constraints.Min; import jakarta.validation.constraints.NotBlank; import jakarta.validation.constraints.NotNull; import jakarta.validation.groups.Default; // 引入默认分组 public class UserRegisterDTO { // 1. 给 userId 的校验注解贴上 OnlyUserIdValidation 标签 // 同时也让它属于默认组 (Default.class)这样不指定分组时它也会校验 NotNull(message 用户ID不能为空, groups {OnlyUserIdValidation.class, Default.class}) Min(value 1, message 用户ID必须大于0, groups {OnlyUserIdValidation.class, Default.class}) private Integer userId; // 2. name 和 email 的校验注解只保留默认组或者不写 groups (默认就是 Default.class) // 这样它们就不会属于 OnlyUserIdValidation 组 NotBlank(message 用户名不能为空) // 默认属于 Default.class Size(min 2, max 10, message 用户名长度在2-10之间) // 默认属于 Default.class private String name; NotBlank(message 邮箱不能为空) Email(message 邮箱格式不正确) private String email; // ... 省略 getter/setter ... }步骤三在 Controller 中指定要使用的“标签”最后在 Controller 的方法参数上把Valid替换成Validated并传入我们刚定义的OnlyUserIdValidation.class作为参数。javaimport org.springframework.validation.annotation.Validated; // 注意引入的包 // ... 其他 import RestController RequestMapping(/user) public class UserController { // 把 Valid 换成 Validated并指定分组 PostMapping(/register) public ResultString register(Validated(OnlyUserIdValidation.class) RequestBody UserRegisterDTO dto) { // 你的业务逻辑... if (dto.getName().contains(admin)) { throw new BusinessException(1001, 用户名不能包含敏感词 admin); } // ... return Result.success(注册成功用户ID dto.getUserId()); } }最终效果经过以上三步你的register接口现在的行为是校验userId因为它上面的NotNull和Min注解属于OnlyUserIdValidation组与 Controller 中指定的分组匹配所以会生效。跳过name和email它们上面的校验注解如NotBlank没有指定groups默认属于Default.class组。由于 Controller 指定的分组是OnlyUserIdValidation.class并不包含Default.class因此这些校验会被跳过。这样你就成功实现了“只校验 userId不改动 DTO 代码”的目标。需要注意的几点ValidvsValidated记住Valid是 Java 标准注解不支持分组Validated是 Spring 提供的支持分组。在需要分组的场景下必须使用Validated。Default 分组所有没有显式指定groups的校验注解都默认属于Default.class分组。这是一个非常重要的概念理解它才能准确预测校验行为。分组继承你的自定义分组可以继承Default.class这样当指定该自定义分组时Default组的校验规则也会生效。这在某些场景下能简化配置。