API接口开发的校验和异常处理实践
1 <!--web依赖包,web应用必备--> 2 <dependency> 3 <groupId>org.springframework.boot</groupId> 4 <artifactId>spring-boot-starter-web</artifactId> 5 </dependency>
本文还用了swagger来生成API文档,lombok来简化类,不过这两者不是必须的,可用可不用。
1 public String addUser(User user) { 2 if (user == null || user.getId() == null || user.getAccount() == null || user.getPassword() == null || user.getEmail() == null) { 3 return "对象或者对象字段不能为空"; 4 } 5 if (StringUtils.isEmpty(user.getAccount()) || StringUtils.isEmpty(user.getPassword()) || StringUtils.isEmpty(user.getEmail())) { 6 return "不能输入空字符串"; 7 } 8 if (user.getAccount().length() < 6 || user.getAccount().length() > 11) { 9 return "账号长度必须是6-11个字符"; 10 } 11 if (user.getPassword().length() < 6 || user.getPassword().length() > 16) { 12 return "密码长度必须是6-16个字符"; 13 } 14 if (!Pattern.matches("^[a-zA-Z0-9_-]+@[a-zA-Z0-9_-]+(\\.[a-zA-Z0-9_-]+)+$", user.getEmail())) { 15 return "邮箱格式不正确"; 16 } 17 // 参数校验完毕后这里就写上业务逻辑 18 return "success"; 19 }
这样做当然是没有什么错的,而且格式排版整齐也一目了然,不过这样太繁琐了,这还没有进行业务操作呢光是一个参数校验就已经这么多行代码,实在不够优雅。我们来改进一下,使用Spring Validator和Hibernate Validator这两套Validator来进行方便的参数校验!这两套Validator依赖包已经包含在前面所说的web依赖包里了,所以可以直接使用。
1 @Data 2 public class User { 3 @NotNull(message = "用户id不能为空") 4 private Long id; 5 6 @NotNull(message = "用户账号不能为空") 7 @Size(min = 6, max = 11, message = "账号长度必须是6-11个字符") 8 private String account; 9 10 @NotNull(message = "用户密码不能为空") 11 @Size(min = 6, max = 11, message = "密码长度必须是6-16个字符") 12 private String password; 13 14 @NotNull(message = "用户邮箱不能为空") 15 @Email(message = "邮箱格式不正确") 16 private String email; 17 }
校验规则和错误提示信息配置完毕后,接下来只需要在接口需要校验的参数上加上@Valid注解,并添加BindResult参数即可方便完成验证:
1 @RestController 2 @RequestMapping("user") 3 public class UserController { 4 @Autowired 5 private UserService userService; 6 7 @PostMapping("/addUser") 8 public String addUser(@RequestBody @Valid User user, BindingResult bindingResult) { 9 // 如果有参数校验失败,会将错误信息封装成对象组装在BindingResult里 10 for (ObjectError error : bindingResult.getAllErrors()) { 11 return error.getDefaultMessage(); 12 } 13 return userService.addUser(user); 14 } 15 }
这样当请求数据传递到接口的时候Validator就自动完成校验了,校验的结果就会封装到BindingResult中去,如果有错误信息我们就直接返回给前端,业务逻辑代码也根本没有执行下去。此时,业务层里的校验代码就已经不需要了:
1 public String addUser(User user) { 2 // 直接编写业务逻辑 3 return "success"; 4 }
现在可以看一下参数校验效果。我们故意给这个接口传递一个不符合校验规则的参数,先传递一个错误数据给接口,故意将password这个字段不满足校验条件:
1 { 2 "account": "12345678", 3 "email": "123@qq.com", 4 "id": 0, 5 "password": "123" 6 }
再来看一下接口的响应数据:
-
简化代码,之前业务层那么一大段校验代码都被省略掉了。
-
使用方便,那么多校验规则可以轻而易举的实现,比如邮箱格式验证,之前自己手写正则表达式要写那么一长串,还容易出错,用Validator直接一个注解搞定。(还有更多校验规则注解,可以自行去了解哦)
-
减少耦合度,使用Validator能够让业务层只关注业务逻辑,从基本的参数校验逻辑中脱离出来。
使用Validator+ BindingResult已经是非常方便实用的参数校验方式了,在实际开发中也有很多项目就是这么做的,不过这样还是不太方便,因为你每写一个接口都要添加一个BindingResult参数,然后再提取错误信息返回给前端。这样有点麻烦,并且重复代码很多(尽管可以将这个重复代码封装成方法)。我们能否去掉BindingResult这一步呢?当然是可以的!
1 @PostMapping("/addUser") 2 public String addUser(@RequestBody @Valid User user) { 3 return userService.addUser(user); 4 }
没错,是直接将整个错误对象相关信息都响应给前端了!这样就很难受,不过解决这个问题也很简单,就是我们接下来要讲的全局异常处理!
1 @RestControllerAdvice 2 public class ExceptionControllerAdvice { 3 @ExceptionHandler(MethodArgumentNotValidException.class) 4 public String MethodArgumentNotValidExceptionHandler(MethodArgumentNotValidException e) { 5 // 从异常对象中拿到ObjectError对象 6 ObjectError objectError = e.getBindingResult().getAllErrors().get(0); 7 // 然后提取错误提示信息进行返回 8 return objectError.getDefaultMessage(); 9 } 10 }
我们再来看下这次校验失败后的响应数据:
-
是捕获异常(
try...catch
)还是抛出异常(throws
) -
是在
controller
层做处理还是在service
层处理又或是在dao
层做处理 -
处理异常的方式是啥也不做,还是返回特定数据,如果返回又返回什么数据
-
不是所有异常我们都能预先进行捕捉,如果发生了没有捕捉到的异常该怎么办?
以上这些问题都可以用全局异常处理来解决,全局异常处理也叫统一异常处理,全局和统一处理代表什么? 代表规范! 规范有了,很多问题就会迎刃而解!全局异常处理的基本使用方式大家都已经知道了,我们接下来更进一步的规范项目中的异常处理方式:自定义异常。在很多情况下,我们需要手动抛出异常,比如在业务层当有些条件并不符合业务逻辑,我这时候就可以手动抛出异常从而触发事务回滚。那手动抛出异常最简单的方式就是throw new RuntimeException("异常信息")
了,不过使用自定义会更好一些:
-
自定义异常可以携带更多的信息,不像这样只能携带一个字符串。
-
项目开发中经常是很多人负责不同的模块,使用自定义异常可以统一了对外异常展示的方式。
-
自定义异常语义更加清晰明了,一看就知道是项目中手动抛出的异常。
我们现在就来开始写一个自定义异常:
1 @Getter //只要getter方法,无需setter 2 public class APIException extends RuntimeException { 3 private int code; 4 private String msg; 5 6 public APIException() { 7 this(1001, "接口错误"); 8 } 9 public APIException(String msg) { 10 this(1001, msg); 11 } 12 public APIException(int code, String msg) { 13 super(msg); 14 this.code = code; 15 this.msg = msg; 16 } 17 }
在刚才的全局异常处理类中记得添加对我们自定义异常的处理:
1 @ExceptionHandler(APIException.class) 2 public String APIExceptionHandler(APIException e) { 3 return e.getMsg(); 4 }
1 @Getter 2 public class ResultVO<T> { 3 /** 4 * 状态码,比如1000代表响应成功 5 */ 6 private int code; 7 /** 8 * 响应信息,用来说明响应情况 9 */ 10 private String msg; 11 /** 12 * 响应的具体数据 13 */ 14 private T data; 15 16 public ResultVO(T data) { 17 this(1000, "success", data); 18 } 19 public ResultVO(int code, String msg, T data) { 20 this.code = code; 21 this.msg = msg; 22 this.data = data; 23 } 24 }
然后我们修改一下全局异常处理那的返回值:
1 @ExceptionHandler(APIException.class) 2 public ResultVO<String> APIExceptionHandler(APIException e) { 3 // 注意哦,这里返回类型是自定义响应体 4 return new ResultVO<>(e.getCode(), "响应失败", e.getMsg()); 5 } 6 @ExceptionHandler(MethodArgumentNotValidException.class) 7 public ResultVO<String> MethodArgumentNotValidExceptionHandler(MethodArgumentNotValidException e) { 8 ObjectError objectError = e.getBindingResult().getAllErrors().get(0); 9 // 注意哦,这里返回类型是自定义响应体 10 return new ResultVO<>(1001, "参数校验失败", objectError.getDefaultMessage()); 11 }
我们再来看一下此时如果发生异常了会响应什么数据给前端:
OK,这个异常信息响应就非常好了,状态码和响应说明还有错误提示数据都返给了前端,并且是所有异常都会返回相同的格式!异常这里搞定了,别忘了我们到接口那也要修改返回类型,我们新增一个接口好来看看效果:
1 @GetMapping("/getUser") 2 public ResultVO<User> getUser() { 3 User user = new User(); 4 user.setId(1L); 5 user.setAccount("12345678"); 6 user.setPassword("12345678"); 7 user.setEmail("123@qq.com"); 8 9 return new ResultVO<>(user); 10 }
看一下如果响应正确返回的是什么效果:
数据格式是规范了,不过响应码code和响应信息msg还没有规范呀!大家发现没有,无论是正确响应,还是异常响应,响应码和响应信息是想怎么设置就怎么设置,要是10个开发人员对同一个类型的响应写10个不同的响应码,那这个统一响应体的格式规范就毫无意义!所以,必须要将响应码和响应信息给规范起来。
1 @Getter 2 public enum ResultCode { 3 SUCCESS(1000, "操作成功"), 4 5 FAILED(1001, "响应失败"), 6 7 VALIDATE_FAILED(1002, "参数校验失败"), 8 9 ERROR(5000, "未知错误"); 10 11 private int code; 12 private String msg; 13 14 ResultCode(int code, String msg) { 15 this.code = code; 16 this.msg = msg; 17 } 18 }
然后修改响应体的构造方法,让其只准接受响应码枚举来设置响应码和响应信息:
1 public ResultVO(T data) { 2 this(ResultCode.SUCCESS, data); 3 } 4 5 public ResultVO(ResultCode resultCode, T data) { 6 this.code = resultCode.getCode(); 7 this.msg = resultCode.getMsg(); 8 this.data = data; 9 }
然后同时修改全局异常处理的响应码设置方式:
1 @ExceptionHandler(APIException.class) 2 public ResultVO<String> APIExceptionHandler(APIException e) { 3 // 注意哦,这里传递的响应码枚举 4 return new ResultVO<>(ResultCode.FAILED, e.getMsg()); 5 } 6 7 @ExceptionHandler(MethodArgumentNotValidException.class) 8 public ResultVO<String> MethodArgumentNotValidExceptionHandler(MethodArgumentNotValidException e) { 9 ObjectError objectError = e.getBindingResult().getAllErrors().get(0); 10 // 注意哦,这里传递的响应码枚举 11 return new ResultVO<>(ResultCode.VALIDATE_FAILED, objectError.getDefaultMessage()); 12 }
这样响应码和响应信息只能是枚举规定的那几个,就真正做到了响应数据格式、响应码和响应信息规范化、统一化!
首先,先创建一个类加上注解使其成为全局处理类。然后继承ResponseBodyAdvice
接口重写其中的方法,即可对我们的controller
进行增强操作,具体看代码和注释:
1 @RestControllerAdvice(basePackages = {"com.csl.demo.controller"}) // 注意哦,这里要加上需要扫描的包 2 public class ResponseControllerAdvice implements ResponseBodyAdvice<Object> { 3 @Override 4 public boolean supports(MethodParameter returnType, Class<? extends HttpMessageConverter<?>> aClass) { 5 // 如果接口返回的类型本身就是ResultVO那就没有必要进行额外的操作,返回false 6 return !returnType.getParameterType().equals(ResultVO.class); 7 } 8 9 @Override 10 public Object beforeBodyWrite(Object data, MethodParameter returnType, MediaType mediaType, Class<? extends HttpMessageConverter<?>> aClass, ServerHttpRequest request, ServerHttpResponse response) { 11 // String类型不能直接包装,所以要进行些特别的处理 12 if (returnType.getGenericParameterType().equals(String.class)) { 13 ObjectMapper objectMapper = new ObjectMapper(); 14 try { 15 // 将数据包装在ResultVO里后,再转换为json字符串响应给前端 16 return objectMapper.writeValueAsString(new ResultVO<>(data)); 17 } catch (JsonProcessingException e) { 18 throw new APIException("返回String类型错误"); 19 } 20 } 21 // 将原本的数据包装在ResultVO里 22 return new ResultVO<>(data); 23 } 24 }
我们可以现在去掉接口的数据包装来看下效果:
1 @GetMapping("/getUser") 2 public User getUser() { 3 User user = new User(); 4 user.setId(1L); 5 user.setAccount("12345678"); 6 user.setPassword("12345678"); 7 user.setEmail("123@qq.com"); 8 // 注意哦,这里是直接返回的User类型,并没有用ResultVO进行包装 9 return user; 10 }
然后我们来看看响应数据:
注意:
beforeBodyWrite
方法里包装数据无法对String类型的数据直接进行强转,所以要进行特殊处理,这里不讲过多的细节,有兴趣可以自行深入了解。
-
通过Validator + 自动抛出异常来完成了方便的参数校验
-
通过全局异常处理 + 自定义异常完成了异常操作的规范
-
通过数据统一响应完成了响应数据的规范
-
多个方面组装非常优雅的完成了后端接口的协调,让开发人员有更多的经历注重业务逻辑代码,轻松构建后端接口
再次强调,项目体系该怎么构建、后端接口该怎么写都没有一个绝对统一的标准,不是说一定要按照本文的来才是最好的,你怎样都可以,本文每一个环节你都可以按照自己的想法来进行编码,我只是提供了一个思路!
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 从 HTTP 原因短语缺失研究 HTTP/2 和 HTTP/3 的设计差异
· AI与.NET技术实操系列:向量存储与相似性搜索在 .NET 中的实现
· 基于Microsoft.Extensions.AI核心库实现RAG应用
· Linux系列:如何用heaptrack跟踪.NET程序的非托管内存泄露
· 开发者必知的日志记录最佳实践
· TypeScript + Deepseek 打造卜卦网站:技术与玄学的结合
· Manus的开源复刻OpenManus初探
· 写一个简单的SQL生成工具
· AI 智能体引爆开源社区「GitHub 热点速览」
· C#/.NET/.NET Core技术前沿周刊 | 第 29 期(2025年3.1-3.9)