在开发Spring Boot应用时,我们经常面临着不同的控制器方法需要处理各种不同类型的响应结果,以及在代码中分散处理异常可能导致项目难以维护的问题。
一、统一结果返回
统一结果返回是一种通过定义通用的返回格式,使所有的响应结果都符合同一标准的方法。这有助于提高代码的一致性,减少重复代码的编写,以及使客户端更容易理解和处理API的响应,提高团队的协作效率、降低项目维护的难度,并使代码更易于理解和扩展。
导入依赖
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<version>1.18.20</version>
<scope>provided</scope>
</dependency>
1、 定义通用的响应对象
package cn.hxzy.common;
import lombok.Data;
import java.io.Serializable;
@Data
public class ResultResponse<T> implements Serializable {
private static final long serialVersionUID = -1133637474601003587L;
/**
* 接口响应状态码
*/
private Integer code;
/**
* 接口响应信息
*/
private String msg;
/**
* 接口响应的数据
*/
private T data;
}
2 、定义接口响应状态码
统一结果返回的关键之一是规定一套通用的状态码。这有助于客户端更容易地理解和处理 API 的响应,同时也为开发者提供了一致的标准。
通常,一些 HTTP 状态码已经被广泛接受,如:
200 OK
:表示成功处理请求。201 Created
:表示成功创建资源。204 No Content
:表示成功处理请求,但没有返回任何内容。
对于错误情况,也可以使用常见的 HTTP 状态码,如:
400 Bad Request
:客户端请求错误。401 Unauthorized
:未授权访问。404 Not Found
:请求资源不存在。500 Internal Server Error
:服务器内部错误。
除了 HTTP 状态码外,你还可以定义自己的应用程序特定状态码,以表示更具体的情况。确保文档中清晰地说明了每个状态码所代表的含义,使开发者能够正确地解释和处理它们。
package cn.hxzy.common;
import lombok.AllArgsConstructor;
import lombok.Getter;
@Getter
@AllArgsConstructor
public enum StatusEnum {
SUCCESS(200 ,"请求处理成功"),
UNAUTHORIZED(401 ,"用户认证失败"),
FORBIDDEN(403 ,"权限不足"),
SERVICE_ERROR(500, "服务器被外星人劫持了,请稍后重试"),
PARAM_INVALID(1000, "无效的参数"),
;
public final Integer code;
public final String message;
}
3、 定义统一的成功和失败的处理方法
定义统一的成功和失败的响应方法有助于保持代码一致性和规范性,简化控制器逻辑,提高代码复用性,降低维护成本,提高可读性,促进团队协作,以及更便于进行测试。
package cn.hxzy.common;
import lombok.Data;
import java.io.Serializable;
@Data
public class ResultResponse<T> implements Serializable {
/**
* 接口响应状态码
*/
private Integer code;
/**
* 接口响应信息
*/
private String msg;
/**
* 接口响应的数据
*/
private T data;
/**
* 封装成功响应的方法
* @param data 响应数据
* @return reponse
* @param <T> 响应数据类型
*/
public static <T> ResultResponse<T> success(T data) {
ResultResponse<T> response = new ResultResponse<>();
response.setData(data);
response.setCode(StatusEnum.SUCCESS.code);
return response;
}
/**
* 封装 error的响应
* @param statusEnum error响应的状态值
* @return
* @param <T>
*/
public static <T> ResultResponse<T> error(StatusEnum statusEnum) {
return error(statusEnum.code, statusEnum.message);
}
/**
* 封装error的响应 可自定义错误信息
* @param code 自定义code
* @param errorMsg 自定义提示消息
* @param <T>
* @return
*/
public static <T> ResultResponse<T> error(int code, String errorMsg) {
ResultResponse<T> response = new ResultResponse<>();
response.setCode(code);
response.setMsg(errorMsg);
return response;
}
/**
* 封装失败响应的方法 自定义异常返回
* @param statusEnum 枚举对象
* @param message 状态提示消息
* @param <T>
* @return
*/
public static <T> ResultResponse<T> error(StatusEnum statusEnum,String message){
ResultResponse<T> response=new ResultResponse();
response.setCode(statusEnum.getCode());
response.setMsg(message);
return response;
}
}
4、web层统一响应结果
在web层使用统一结果返回的目的是将业务逻辑的处理结果按照预定的通用格式进行封装,以提高代码的一致性和可读性。
定义实体类:
package cn.hxzy.domain;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
import java.io.Serializable;
@Data
@NoArgsConstructor
@AllArgsConstructor
public class User implements Serializable {
private int userId;
private String userNo;
private String userPwd;
private String userName;
}
定义控制层:
package cn.hxzy.controller;
import cn.hxzy.common.ResultResponse;
import cn.hxzy.common.StatusEnum;
import cn.hxzy.domain.User;
import org.springframework.web.bind.annotation.*;
import java.util.ArrayList;
import java.util.List;
@RestController
@RequestMapping("/user")
public class UserController {
/**
* 查询用户
*
* @param userId 用户id
* @return
*/
@GetMapping("/select/{userId}")
public ResultResponse<User> selectUser(@PathVariable String userId) {
try {
if (userId.equals("1")) {
User user = new User(10086, "admin", "123", "小王1");
return ResultResponse.success(user);
} else {
return ResultResponse.error(StatusEnum.PARAM_INVALID);
}
} catch (RuntimeException e) {
return ResultResponse.error(StatusEnum.SERVICE_ERROR);
}
}
/**
* 根据用户ID获取用户信息
*
* @return
*/
@GetMapping("/list/{userId}")
public ResultResponse<List<User>> getUserList() {
List<User> list = new ArrayList<>();
//模拟从后台查询得到用户对象
User user1 = new User(1, "admin", "123", "小王1");
User user2 = new User(2, "aaa", "123", "小王2");
User user3 = new User(3, "bbb", "123", "小王3");
list.add(user1);
list.add(user2);
list.add(user3);
return ResultResponse.success(list);
}
}
采用统一的响应格式简化了业务逻辑处理流程,使得开发者更容易处理成功和失败的情况,同时客户端也更容易理解和处理 API 的响应。
这一实践有助于降低维护成本、提高团队协作效率,并促进代码的规范化。
二、统一异常处理
package cn.hxzy.user.controller;
import cn.hxzy.common.Result;
import cn.hxzy.entity.User;
import org.springframework.web.bind.annotation.CrossOrigin;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
@RestController
@RequestMapping("/user")
public class UserController {
@PutMapping("/update")
public ResultResponse<Void> update() {
int num=10/0;
return ResultResponse.success(null);
}
}
ResponseBody响应:
{
"timestamp": "2022-11-03T02:05:49.190+00:00",
"status": 500,
"error": "Internal Server Error",
"path": "/user/update"
}
通过上面的代码,可以看出定义统一异常处理的必要性。体现在保持代码的一致性、提供更清晰的错误信息、以及更容易排查问题。通过定义统一的异常处理方式,确保在整个应用中对异常的处理保持一致,减少了重复编写相似异常处理逻辑的工作,同时提供友好的错误信息帮助开发者和维护人员更快地定位和解决问题,最终提高了应用的可维护性和可读性。
1、枚举类
package cn.hxzy.common;
import lombok.AllArgsConstructor;
import lombok.Getter;
@Getter
@AllArgsConstructor
public enum StatusEnum {
//定义自己的HTTP应用程序特定状态码
SUCCESS(200 ,"请求处理成功"),
UNAUTHORIZED(401 ,"用户认证失败"),
FORBIDDEN(403 ,"权限不足"),
SERVICE_ERROR(500, "服务器被外星人劫持了,请稍后重试"),
PARAM_INVALID(1000, "无效的参数"),
//业务相关异常状态码,可以根据自己的项目自由扩展
USERNAME_NOT_EXISTS(10001,"用户名不存在"),
USER_CREDIT_NOT_ENOUTH(1002,"用户积分不足"),
INVALID_CODE(1003,"验证码无效"),
;
public final Integer code;
public final String message;
}
可参考如下数据扩展业务枚举:
// -------------------失败状态码----------------------
// 参数错误
int PARAMS_IS_NULL = 10001;// 参数为空
int PARAMS_NOT_COMPLETE = 10002; // 参数不全
int PARAMS_TYPE_ERROR = 1003; // 参数类型错误
int PARAMS_IS_INVALID = 10004; // 参数无效
// 用户错误
int USER_NOT_EXIST = 20001; // 用户不存在
int USER_NOT_LOGGED_IN = 20002; // 用户未登陆
int USER_ACCOUNT_ERROR = 20003; // 用户名或密码错误
int USER_ACCOUNT_FORBIDDEN = 20004; // 用户账户已被禁用
int USER_HAS_EXIST = 20005;// 用户已存在
// 业务错误
int BUSINESS_ERROR = 30001;// 系统业务出现问题
// 系统错误
int SYSTEM_INNER_ERROR = 40001; // 系统内部错误
// 数据错误
int DATA_NOT_FOUND = 50001; // 数据未找到
int DATA_IS_WRONG = 50002;// 数据有误
int DATA_ALREADY_EXISTED = 50003;// 数据已存在
// 接口错误
int INTERFACE_INNER_INVOKE_ERROR = 60001; // 系统内部接口调用异常
int INTERFACE_OUTER_INVOKE_ERROR = 60002;// 系统外部接口调用异常
int INTERFACE_FORBIDDEN = 60003;// 接口禁止访问
int INTERFACE_ADDRESS_INVALID = 60004;// 接口地址无效
int INTERFACE_REQUEST_TIMEOUT = 60005;// 接口请求超时
int INTERFACE_EXCEED_LOAD = 60006;// 接口负载过高
// 权限错误
int PERMISSION_NO_ACCESS = 70001;// 没有访问权限
2、自定义异常类
package cn.hxzy.exception;
import cn.hxzy.common.StatusEnum;
import lombok.AllArgsConstructor;
import lombok.Getter;
@Getter
@AllArgsConstructor
public class AppException extends RuntimeException {
//状态码
private int code = 500;
//异常消息
private String msg = "服务器异常";
public AppException(StatusEnum statusEnum) {
super();
this.code = statusEnum.getCode();
this.msg = statusEnum.getMessage();
}
}
3、创建全局统一异常处理类
3.1 全局异常
package cn.hxzy.exception;
import cn.hxzy.common.ResultResponse;
import cn.hxzy.common.StatusEnum;
import org.springframework.web.bind.annotation.ControllerAdvice;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.ResponseBody;
@ControllerAdvice
public class GlobalExceptionHandler {
//全局异常
@ExceptionHandler(value = {Exception.class})
@ResponseBody
public <T> ResultResponse<T> exceptionHandle(Exception e) {
return ResultResponse.error(StatusEnum.SERVICE_ERROR);
}
}
3.2 特定异常
//特定异常1,其他省略
@ExceptionHandler(ArithmeticException.class)
@ResponseBody
public <T> ResultResponse<T> error(ArithmeticException e){
return ResultResponse.error(-1,"执行了ArithmeticException异常处理");
}
3.3 自定义异常
//自定义异常
@ExceptionHandler(AppException.class)
@ResponseBody
public <T> ResultResponse<T> error(AppException e){
return ResultResponse.error(e.getCode(),e.getMsg());
}
4、返回统一结果与统一异常处理测试
package cn.hxzy.controller;
import cn.hxzy.common.ResultResponse;
import cn.hxzy.common.StatusEnum;
import cn.hxzy.exception.AppException;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RestController;
@RestController
public class DemoController {
@GetMapping("/test/{name}")
public ResultResponse<String> demo(@PathVariable String name) {
//正常返回结果
if ("ok".equals(name)) {
return ResultResponse.success("查询成功");
}
if ("err".equals(name)) {
//返回业务相关异常结果 1
return ResultResponse.error(StatusEnum.INVALID_CODE);
}
if ("err2".equals(name)) {
//返回业务相关异常结果 2
return ResultResponse.error(StatusEnum.USER_CREDIT_NOT_ENOUTH);
}
if ("err3".equals(name)) {
//抛出自定义相关异常对象
throw new AppException(StatusEnum.SERVICE_ERROR);
}
if ("err4".equals(name)) {
//抛出自定义相关异常对象
throw new AppException(50001,"系统业务逻辑异常");
}
if ("err5".equals(name)) {
//出现系统异常
int num = 10 / 0;
}
return ResultResponse.success(null);
}
}
三、Hibernate数据校验简介
我们在业务中经常会遇到参数校验问题,比如前端参数校验、Kafka消息参数校验等,如果业务逻辑比较复杂,各种实体比较多的时候,我们通过代码对这些数据
一一校验,会出现大量的重复代码以及和主要业务无关的逻辑。
public String validateStudentVO(StudentVO studentVO) {
if (StringUtils.isBlank(studentVO.getName())) {
return "姓名不能为空";
}
if (studentVO.getName().length() > 20) {
return "姓名不能超过20个字符";
}
if (studentVO.getAge() == null) {
return "年龄不能为空";
}
return null;
}
SpringBoot提供了参数校验机制,但是其底层还是通过Hibernate进行数据校验,所以有必要去了解一下Hibernate数据校验和JSR数据校验规范。
1、JSR数据校验规范
Java官方发布了数据合法性校验提供的标准框架:BeanValidator。Bean Validation先后经历了1.0(JSR 303)、1.1(JSR 349)、2.0(JSR 380)这3个版本。
在BeanValidator框架中,用户通过在Bean的属性上标注类似于@NotNull、@Max等标准的注解指定校验规则,并通过标准的验证接口对Bean进行验证。
2、JSR注解列表
JSR标准中的数据校验注解如下所示:
注解名 | 注解数据类型 | 注解作用 | 示例 |
---|---|---|---|
AssertFalse | boolean/Boolean | 被注释的元素必须为False | @AssertFalse private boolean success; |
AssertTrue | boolean/Boolean | 被注释的元素必须为True | @AssertTrue private boolean success; |
DecimalMax | BigDecimal/BigInteger/CharSequence/byte/short/int/long及其包装类 | 被注释的值应该小于等于指定的最大值 | @DecimalMax("10") private BigDecimal value; |
DecimalMin | BigDecimal/BigInteger/CharSequence/byte/short/int/long及其包装类 | 被注释的值应该大于等于指定的最小值 | @DecimalMin("10") private BigDecimal value; |
Digits | BigDecimal/BigInteger/CharSequence/byte/short/int/long及其包装类 | integer指定整数部分最大位数,fraction指定小数部分最大位数 | @Digits(integer = 10,fraction = 4) private BigDecimal value; |
CharSequence | 字符串为合法的邮箱格式 | @Email private String email; | |
Future | java中的各种日期类型 | 指定日期应该在当期日期之后 | @Future private LocalDateTime future; |
FutureOrPresent | java中的各种日期类型 | 指定日期应该为当期日期或当期日期之后 | @FutureOrPresent private LocalDateTime futureOrPresent; |
Max | BigDecimal/BigInteger/byte/short/int/long及包装类 | 被注释的值应该小于等于指定的最大值 | @Max("10") private BigDecimal value; |
Min | BigDecimal/BigInteger/byte/short/int/long及包装类 | 被注释的值应该大于等于指定的最小值 | @Min("10") private BigDecimal value; |
Negative | BigDecimal/BigInteger/byte/short/int/long/float/double及包装类 | 被注释的值应该是负数 | @Negative private BigDecimal value; |
NegativeOrZero | BigDecimal/BigInteger/byte/short/int/long/float/double及包装类 | 被注释的值应该是0或者负数 | @NegativeOrZero private BigDecimal value; |
NotBlank | CharSequence | 被注释的字符串至少包含一个非空字符 | @NotBlank private String noBlankString; |
NotEmpty | CharSequence/Collection/Map/Array | 被注释的集合元素个数大于0 | @NotEmpty private List<string> values; |
NotNull | any | 被注释的值不为空 | @NotEmpty private Object value; |
Null | any | 被注释的值必须空 | @Null private Object value; |
Past | java中的各种日期类型 | 指定日期应该在当期日期之前 | @Past private LocalDateTime past; |
PastOrPresent | java中的各种日期类型 | 指定日期应该在当期日期或之前 | @PastOrPresent private LocalDateTime pastOrPresent; |
Pattern | CharSequence | 被注释的字符串应该符合给定得到正则表达式 | @Pattern(\d*) private String numbers; |
Positive | BigDecimal/BigInteger/byte/short/int/long/float/double及包装类 | 被注释的值应该是正数 | @Positive private BigDecimal value; |
PositiveOrZero | BigDecimal/BigInteger/byte/short/int/long/float/double及包装类 | 被注释的值应该是正数或0 | @PositiveOrZero private BigDecimal value; |
Size | CharSequence/Collection/Map/Array | 被注释的集合元素个数在指定范围内 | @Size(min=1,max=10) private List<string> values; |
3、Hibernate数据校验
基于JSR数据校验规范,Hibernate添加了一些新的注解校验,然后实现了JSR的Validator
接口用于数据校验。
@Null 被注释的元素必须为 null
@NotNull 被注释的元素必须不为 null
@AssertTrue 被注释的元素必须为 true
@AssertFalse 被注释的元素必须为 false
@Min(value) 被注释的元素必须是一个数字,其值必须大于等于指定的最小值
@Max(value) 被注释的元素必须是一个数字,其值必须小于等于指定的最大值
@DecimalMin(value) 被注释的元素必须是一个数字,其值必须大于等于指定的最小值
@DecimalMax(value) 被注释的元素必须是一个数字,其值必须小于等于指定的最大值
@Size(max=, min=) 被注释的元素的大小必须在指定的范围内
@Digits (integer, fraction) 被注释的元素必须是一个数字,其值必须在可接受的范围内
@Past 被注释的元素必须是一个过去的日期
@Future 被注释的元素必须是一个将来的日期
@Pattern(regex=,flag=) 被注释的元素必须符合指定的正则表达式 * Hibernate Validator 附加的 constraint
@NotBlank(message =) 验证字符串非null,且长度必须大于0
@Email 被注释的元素必须是电子邮箱地址
@Length(min=,max=) 被注释的字符串的大小必须在指定的范围内
@NotEmpty 被注释的字符串的内容必须非空
@Range(min=,max=,message=) 被注释的元素必须在合适的范围内
四、SpringBoot使用数据校验
1、整合依赖
1.1 SpringMVC的依赖(了解)
<!--hibernate-validator-后台校验-->
<dependency>
<groupId>org.hibernate.validator</groupId>
<artifactId>hibernate-validator</artifactId>
<version>6.1.5.Final</version>
</dependency>
<dependency>
<groupId>javax.validation</groupId>
<artifactId>validation-api</artifactId>
<version>2.0.1.Final</version>
</dependency>
<!--hibernate-validator-后台校验-->
1.2 SpringBoot整合依赖
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-validation</artifactId>
</dependency>
2、实体类标注
package com.example.oa.domain;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
import org.hibernate.validator.constraints.Length;
import javax.validation.constraints.*;
@Data
@NoArgsConstructor
@AllArgsConstructor
public class Person {
@NotBlank(message = "姓名不能为空")
private String name;
@NotBlank //非空的
@Length(min = 6,max = 20,message = "密码长度必须大于6位,小于20位")
private String passWord;
@Min(1)
@Max(150)
private int age;
@Pattern(regexp="^1[35678]\\d{9}$",message = "手机号格式不正确")
private String phone;
@Email
private String email;
@Pattern(regexp = "^[1-9]\\d{5}(18|19|([23]\\d))\\d{2}((0[1-9])|(10|11|12))(([0-2][1-9])|10|20|30|31)\\d{3}[0-9Xx]$",message = "身份证格式不正确")
private String cardNo;
//系统 上传身份证照片 ,OCR AI接口
}
3、Controller应用
package com.example.oa.controller;
import com.example.oa.common.ResultResponse;
import com.example.oa.common.StatusEnum;
import com.example.oa.domain.Person;
import org.springframework.validation.BindingResult;
import org.springframework.validation.ObjectError;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import javax.validation.Valid;
import java.util.ArrayList;
import java.util.List;
@RestController
@RequestMapping("/person")
public class PersonController {
@PostMapping("/save")
public ResultResponse<List<String>> save(@RequestBody @Valid Person person, BindingResult result) {
List<String> list = new ArrayList<>();
//判断是否存在异常,校验不通过
if (result.hasErrors()) {
//得到所有的返回的校验不通过的异常信息
List<ObjectError> allErrors = result.getAllErrors();
for (ObjectError allError : allErrors) {
System.out.println(allError.getDefaultMessage()); //得到注解标注的消息
list.add(allError.getDefaultMessage());
}
return ResultResponse.error(10005,"数据校验错误",list);
}
return ResultResponse.success(null);
}
}
4、捕获MethodArgumentNotValidException异常信息
创建一个全局异常处理类,用于捕获抛出的异常,这里主要是通过Stream的API获取到注解中message属性的值
//特定异常
@ExceptionHandler(MethodArgumentNotValidException.class)
@ResponseStatus(HttpStatus.BAD_REQUEST)
@ResponseBody
public <T> ResultResponse<Map<String, Object>> handleValidationExceptions(MethodArgumentNotValidException ex) {
//存放异常信息的map字典集合容器
Map<String, Object> map = new HashMap<>();
//异常结果
List<ObjectError> allErrors = ex.getBindingResult().getAllErrors();
for (ObjectError error : allErrors) {
FieldError fieldError = (FieldError) error; //向下转型成子类
String fieldName = fieldError.getField(); //名称
String errorMessage = error.getDefaultMessage(); //异常消息
map.put(fieldName, errorMessage);
}
return ResultResponse.error(400, "传入的数据校验未通过", map);
}