项目实训5
1. 背景
由于项目需要制定比较统一的返回接口,所以项目中设置了统一的异常返回处理以及统一的返回结果处理。
在实际开发中,如果不处理统一处理异常,那么前端在调用后端提供的接口,就会处理各种的异常结构,对于前端来说那可谓是一场灾难,这对前后端的协作也不友好。比如后端路径:/api/v1/index/user?id=222
,如果前端未传入ID,那么SpringBoot就会报如下异常:
{"timestamp":"2021-03-30T15:22:46.139+00:00","status":400,"error":"Bad Request","message":"","path":"/api/v1/index/user"}
对于前端来说,不好定位处理。所以如下代码设置定义:
2. 统一结果处理
首先定义结果枚举类:
package com.example.guke.result;
import lombok.Getter;
@Getter
public enum ResultCodeEnum {
SUCCESS(true,0,"成功"),
UNKNOWN_REASON(false,-1,"未知错误"),
BAD_SQL_GRAMMAR(false,-2,"sql语法错误"),
JSON_PARSE_ERROR(false,-3,"json解析异常"),
PARAM_ERROR(false,-4,"参数不正确"),
ERROR_LOGIN(false,-5,"用户名或密码错误"),
ERROR_REGISTER(false,-7,"注册失败!"),
SUCCESS_LOGIN(true,0,"登录成功!"),
SUCCESS_REGISTER(true,0,"注册成功!"),
UNKNOWN_USER(false,-8,"用户不存在"),
NO_TOKEN(false,-10,"无token,请重新登录");
private final Boolean status; //响应是否成功
private final Integer code; //返回码
private final String message; //返回消息
ResultCodeEnum(Boolean status, Integer code, String message) {
this.status = status;
this.code = code;
this.message = message;
}
}
枚举类中定义了状态,状态码和相应的信息,同时定义了一些常见的结果信息。
接下来定义Result类
package com.example.guke.result;
import lombok.Data;
import java.util.HashMap;
import java.util.Map;
/**
* @author niu
* @Description:
* @date 2021/5/5 14:34
*/
@Data
public class Result {
private Boolean success;
private Integer code;
private String message;
private Map<String,Object> data = new HashMap<String ,Object>();
public Result(){}
public Result data(Map<String,Object> map){ //给返回值传入data信息,参数为map
this.setData(map);
return this;
}
public Result data(String key,Object value){ //给返回值传入data信息,参数为键值
this.data.put(key,value);
return this;
}
public Result message(String message){ //动态设置message
this.setMessage(message);
return this;
}
public Result code(Integer code){
this.setCode(code);
return this;
}
public Result success(Boolean success){
this.setSuccess(success);
return this;
}
}
设置了各种类型的构造函数,便于在返回的时候能够便捷地构造。
同时为了符合设计模式中工厂模式的思想,项目中还定义了工厂类:
package com.example.guke.result;
import lombok.Data;
import java.util.Map;
@Data
public class ResultFactory {
public static Result success(){ //默认的成功返回值
Result r = new Result();
r.setCode(ResultCodeEnum.SUCCESS.getCode());
r.setSuccess(ResultCodeEnum.SUCCESS.getStatus());
r.setMessage(ResultCodeEnum.SUCCESS.getMessage());
return r;
}
public static Result error(){ //未知错误返回值
Result r = new Result();
r.setCode(ResultCodeEnum.UNKNOWN_REASON.getCode());
r.setSuccess(ResultCodeEnum.UNKNOWN_REASON.getStatus());
r.setMessage(ResultCodeEnum.UNKNOWN_REASON.getMessage());
return r;
}
public static Result data(Map<String,Object> map){ //给返回值传入data信息,参数为map
Result r = new Result();
r.setCode(ResultCodeEnum.SUCCESS.getCode());
r.setSuccess(ResultCodeEnum.SUCCESS.getStatus());
r.setData(map);
return r;
}
public static Result message(String message){ //动态设置message
Result r = new Result();
r.setCode(ResultCodeEnum.SUCCESS.getCode());
r.setSuccess(ResultCodeEnum.SUCCESS.getStatus());
r.setMessage(message);
return r;
}
@Deprecated
public static Result success(ResultCodeEnum codeEnum){ //传入一个枚举值的成功返回值
Result r = new Result();
r.setCode(codeEnum.getCode());
r.setSuccess(codeEnum.getStatus());
r.setMessage(codeEnum.getMessage());
return r;
}
@Deprecated
public static Result error(ResultCodeEnum codeEnum){ //传入一个枚举值的失败返回值
Result r = new Result();
r.setCode(codeEnum.getCode());
r.setSuccess(codeEnum.getStatus());
r.setMessage(codeEnum.getMessage());
return r;
}
@Deprecated
public static Result data(String key,Object value){ //给返回值传入data信息,参数为键值
Result r = new Result();
r.setCode(ResultCodeEnum.SUCCESS.getCode());
r.setSuccess(ResultCodeEnum.SUCCESS.getStatus());
// r.setData(map);
return r;
}
}
通过工厂类的静态方法能够快速地构造响应的Result
3. 统一异常处理
其实使用到了@ControllerAdvice注解
是Spring3.2中新增的注解,学名是Controller增强器,作用是给Controller控制器添加统一的操作或处理。
对于@ControllerAdvice,我们比较熟知的用法是结合@ExceptionHandler用于全局异常的处理,但其作用不止于此。ControllerAdvice拆开来就是Controller Advice,关于Advice,在Spring的AOP中,是用来封装一个切面所有属性的,包括切入点和需要织入的切面逻辑。这里ControllerAdvice也可以这么理解,其抽象级别应该是用于对Controller进行切面环绕的,而具体的业务织入方式则是通过结合其他的注解来实现的。@ControllerAdvice是在类上声明的注解,其用法主要有三点:
1.结合方法型注解@ExceptionHandler,用于捕获Controller中抛出的指定类型的异常,从而达到不同类型的异常区别处理的目的。
2.结合方法型注解@InitBinder,用于request中自定义参数解析方式进行注册,从而达到自定义指定格式参数的目的。
3.结合方法型注解@ModelAttribute,表示其注解的方法将会在目标Controller方法执行之前执行。
从上面的讲解可以看出,@ControllerAdvice的用法基本是将其声明在某个bean上,然后在该bean的方法上使用其他的注解来指定不同的织入逻辑。不过这里@ControllerAdvice并不是使用AOP的方式来织入业务逻辑的,而是Spring内置对其各个逻辑的织入方式进行了内置支持。
package com.example.guke.exception;
import com.example.guke.result.Result;
import com.example.guke.result.ResultFactory;
import lombok.extern.slf4j.Slf4j;
import org.springframework.context.support.DefaultMessageSourceResolvable;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.http.converter.HttpMessageNotReadableException;
import org.springframework.validation.BindException;
import org.springframework.validation.BindingResult;
import org.springframework.web.bind.MethodArgumentNotValidException;
import org.springframework.web.bind.annotation.*;
import javax.validation.ConstraintViolation;
import javax.validation.ConstraintViolationException;
import java.io.PrintWriter;
import java.io.StringWriter;
import java.util.Map;
import java.util.stream.Collectors;
@Slf4j
@ControllerAdvice
public class GlobalExceptionHandler {
/**
* @name: getErrorInfoFromException
* @description: TODO 用来输出具体的报错信息,比e.getMessage()的输出更详细
* @param e 异常类
* @return: java.lang.String
* @date: 2022/5/10 15:13
* @author: NiuYiq
*
*/
public String getErrorInfoFromException(Exception e) {
try {
StringWriter sw = new StringWriter();
PrintWriter pw = new PrintWriter(sw);
e.printStackTrace(pw);
return "\r\n" + sw.toString() + "\r\n";
// sw.close();
// pw.close();
} catch (Exception e2) {
return "ErrorInfoFromException";
}
}
/**
* @name: customException
* @description: TODO 处理自定义异常,程序中主动抛出的异常
* @param e
* @return: com.example.guke.result.Result
* @date: 2022/5/10 21:29
* @author: NiuYiq
*
*/
@ExceptionHandler(value = CustomException.class)
@ResponseBody
public Result customException(CustomException e){
log.error(e.getMessage());
return ResultFactory.error().message(e.getMessage());
}
/**
* 捕获数据校验异常
* @param e 错误信息集合
* @return 错误信息
*/
/**
* 处理请求参数格式错误 @RequestBody上使用@Valid 实体上使用@NotNull等,验证失败后抛出的异常是MethodArgumentNotValidException异常
*/
@ExceptionHandler(MethodArgumentNotValidException.class)
@ResponseBody
public Result MethodArgumentNotValidExceptionHandler(MethodArgumentNotValidException e) {
String message = e.getBindingResult().getAllErrors().stream().map(DefaultMessageSourceResolvable::getDefaultMessage).collect(Collectors.joining());
System.out.println(message);
return ResultFactory.error().message(message);
}
/**
* 处理请求中 使用@Valid 验证路径中请求实体校验失败后抛出的异常
*/
@ExceptionHandler(BindException.class)
@ResponseBody
public Result BindExceptionHandler(BindException e) {
String message = e.getBindingResult().getAllErrors().stream().map(DefaultMessageSourceResolvable::getDefaultMessage).collect(Collectors.joining());
return ResultFactory.error().message(message);
}
/**
* 处理请求参数格式错误 @RequestParam上validate失败后抛出的异常是ConstraintViolationException
*/
@ExceptionHandler(ConstraintViolationException.class)
@ResponseBody
public Result ConstraintViolationExceptionHandler(ConstraintViolationException e) {
String message = e.getConstraintViolations().stream().map(ConstraintViolation::getMessage).collect(Collectors.joining());
return ResultFactory.error().message(message);
}
/**
* 参数格式异常
*/
@ExceptionHandler(HttpMessageNotReadableException.class)
@ResponseBody
public Result HttpMessageNotReadableExceptionHandler(HttpMessageNotReadableException e) {
return ResultFactory.error().message("参数格式异常");
}
/**
* @name: exceptionHandler
* @description: TODO 程序运行中自动抛出的异常
* @param e
* @return: com.example.guke.result.Result
* @date: 2022/5/10 21:30
* @author: NiuYiq
*
*/
@ExceptionHandler(value = Exception.class)
@ResponseBody
@ResponseStatus(value = HttpStatus.CREATED)
public Result exceptionHandler(Exception e){
log.error(getErrorInfoFromException(e) + "\n" + e.getMessage());
return ResultFactory.error().message(getErrorInfoFromException(e)+ "\n" + e.getMessage());
}
}
本类的代码中写了一个getErrorInfoFromException方法。
定义了不同类型的异常的不同处理方式。
接着是自定义的异常类,继承自Exception
package com.example.guke.exception;
import com.example.guke.result.ResultCodeEnum;
import lombok.AllArgsConstructor;
import lombok.Getter;
import lombok.Setter;
@Getter
@Setter
@AllArgsConstructor
public class CustomException extends RuntimeException{
private int code;
private String message;
public CustomException(ResultCodeEnum resultCodeEnum){
this.code = resultCodeEnum.getCode();
this.message = resultCodeEnum.getMessage();
}
public CustomException(String message){
this.code = -1;
this.message = message;
}
}
这边通过构造方法和前面的ResultCodeEnum结合起来使用了。
于是便完成了全局的统一异常处理和统一结果处理。