进学阁

业精于勤荒于嬉,行成于思毁于随

0%

构建统一返回以及全局异常处理

统一返回格式

为什么需要统一返回格式

接口的返回值类型众多,有的直接返回数据传输对象(DTO),甚至直接返回数据对象(DO),还有的返回Result对象。这样对外交互时对方处理起来特别的复杂,所以需要统一的返回格式。

构建统一返回格式

构建Result对象

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
public class ResultHelper {

// 成功的响应结果
public static <T> Result<T> success(T data) {
return new Result<T>()
.setCode(Result.SUCCESS_CODE)
.setData(data)
.setMessage(Result.SUCCESS_MESSAGE)
.setTimestamp(System.currentTimeMillis());
}

// 失败的响应结果
public static <T> Result<T> failure(String code, String message) {
return new Result<T>()
.setCode(code)
.setMessage(message)
.setTimestamp(System.currentTimeMillis());
}

// 失败的响应结果,使用默认的错误码
public static <T> Result<T> failure(String message) {
return new Result<T>()
.setCode(ErrorCode.SERVICE_ERROR.getCode())
.setMessage(message)
.setTimestamp(System.currentTimeMillis());
}

public static <T> Result<T> failure(ErrorCode errorCode) {
return new Result<T>()
.setCode(errorCode.getCode())
.setMessage(errorCode.getMessage())
.setTimestamp(System.currentTimeMillis());
}
}

访问下接口

1
2
3
4
5
6
7
8
@RestController
@RequestMapping("/user")
public class UserController {
@PostMapping("/test")
public Result<String> test() {
return ResultHelper.success("test");
}
}

结果为:

统一的模板方法

为什么需要统一的模板方法

按照上面的示例一切都很美好,代码也很简洁,内容也如预期做了输出,但是我们考虑一个问题,如果有上百个接口的时候,需要对接口进行一些公共代码的调整,比如讲参数和输出打印出来,对参数进行验证,统计代码执行的时间。当遇到这种需求可以使用AOP做切面编程,但是切面编程的效率并不是很好,所以可以选择使用统一的模板方法来处理这些问题。既可以实现统一的管理公共代码,又可以更规范的书写代码

构建统一的模板方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
@Slf4j
public abstract class ServerTemplate<T, R> {

public R Process(T request) {
log.info("开始执行,参数:{}", request);
StopWatch stopWatch = new StopWatch();
stopWatch.start();

try {
// 参数校验
validParam(request);
// 执行业务代码
R response = doProcess(request);
// 记录时间信息
stopWatch.stop();
log.info("执行结束,耗时:{}ms", stopWatch.getTotalTimeMillis());
return response;
} catch (Exception e) {
log.error("执行异常,异常信息:{}", Arrays.toString(e.getStackTrace()));
// 抛出异常 统一处理
throw e;
}
}

protected abstract void validParam(T request);

protected abstract R doProcess(T request);

使用统一格式创建一个接口

改造一下之前的代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
@PostMapping("/test")
public Result<String> test(@RequestBody String request) {
return ResultHelper.success((new ServerTemplate<String, String>(){

@Override
protected void validParam(String request) {

}

@Override
protected String doProcess(String request) {
return request;
}

}).process(request));
}

执行代码

再次优化:自动包装类

每次都ResultHelper.success()还要在参数中增加Result来维护一致性总是觉着有点太浪费时间,而且如果忘记了就会出错。所以需要再进一步优化一下,springboot的ResponseBodyAdvice可以实现自动包装类

:::info
提示: ResponseBodyAdvice 可以拦截控制器(Controller)方法的返回值,允许我们统一处理返回值或响应体。这对于统一返回格式、加密、签名等场景非常有用。

:::

集成ResponseBodyAdvice接口

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
@RestControllerAdvice
public class GlobalResponseBodyAdvice implements ResponseBodyAdvice<Object> {

@Override
public boolean supports(MethodParameter returnType, Class<? extends HttpMessageConverter<?>> converterType) {
// boolean supports = returnType.getContainingClass().getPackage().getName().startsWith("com.jianzh5.dailymart");
return true;

}

@Override
public Object beforeBodyWrite(Object body, MethodParameter returnType, MediaType selectedContentType, Class<? extends HttpMessageConverter<?>> selectedConverterType, ServerHttpRequest request,
ServerHttpResponse response) {
if (body == null) {
return JsonUtils.obj2String(ResultHelper.success(""));
}
if (body instanceof String) {
// 当响应体是String类型时,使用ObjectMapper转换,因为Spring默认使用StringHttpMessageConverter处理字符串,不会将字符串识别为JSON
// return objectMapper.writeValueAsString(ResultFactory.success(body));
return JsonUtils.obj2String(ResultHelper.success(body));
}
if (body instanceof Result<?>) {
// 已经包装过的结果无需再次包装
return body;
}
// 对响应体进行包装
return ResultHelper.success(body);
}
}

再修改一下之前的接口

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
@RestController
@RequestMapping("/user")
public class UserController {

@PostMapping("/test")
public String test(@RequestBody String request) {
return (new ServerTemplate<String, String>() {

@Override
protected void validParam(String request) {

}

@Override
protected String doProcess(String request) {
return request;
}

}).process(request);
}
}

执行结果

全局异常处理

在构建系统的可靠性时容错处理是重要的考虑方便之一,想要系统更加的健壮全局就需要添加全局异常处理,springboot其实已经做了前期的工作,@RestControllerAdvice注解+@ExceptionHandler可以解决这方面的顾虑

自定义异常的创建和使用

定义自定义异常基类

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
@Getter
public abstract class AbstractException extends RuntimeException {

@Serial
private static final long serialVersionUID = 1L;

private final String code;
private final String message;

public AbstractException(ErrorCode errorCode, String message, Throwable throwable) {
super(message, throwable);
this.code = errorCode.getCode();
this.message = Optional.ofNullable(message).orElse(errorCode.getMessage());
}

public AbstractException(String code, String message, Throwable throwable) {
super(message, throwable);
this.code = code;
this.message = message;
}
}

定义自定义异常类

1
2
3
4
5
6
7
8
9
10
public class BusinessException extends AbstractException {

public BusinessException(ErrorCode errorCode, String message, Throwable throwable) {
super(errorCode, message, throwable);
}

public BusinessException(String code, String message, Throwable throwable) {
super(code, message, throwable);
}
}

全局异常的定义

在服务端在接收到参数,通常是不受信任的,这时我们需要对参数进行验证,因为参数验证并不是业务错误所以需要进行额外的处理。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
@Slf4j
@RestControllerAdvice
public class GlobalExceptionHandler {

@ExceptionHandler(value = {MethodArgumentNotValidException.class, BindException.class, ValidationException.class})
public Result<Void> handleValidException(HttpServletRequest request, Exception e) {
String exceptionStr = "参数校验异常";
if (e instanceof MethodArgumentNotValidException ex) {
BindingResult bindingResult = ex.getBindingResult();
FieldError firstFieldError = CollectionUtil.getFirst(bindingResult.getFieldErrors());

exceptionStr = Optional.ofNullable(firstFieldError)
.map(FieldError::getDefaultMessage)
.orElse(StrUtil.EMPTY);

} else if (e instanceof ConstraintViolationException) {
ConstraintViolationException ex = (ConstraintViolationException) e;

ConstraintViolation<?> firstConstraintViolation = CollectionUtil.getFirst(ex.getConstraintViolations());

exceptionStr = Optional.ofNullable(firstConstraintViolation)
.map(ConstraintViolation::getMessage)
.orElse(StrUtil.EMPTY);

} else if (e instanceof BindException) {
BindException ex = (BindException) e;
ObjectError firstObjectError = CollectionUtil.getFirst(ex.getAllErrors());

exceptionStr = Optional.ofNullable(firstObjectError)
.map(ObjectError::getDefaultMessage)
.orElse(StrUtil.EMPTY);
}

log.error("[{}] {} [ex] {}", request.getMethod(), getUrl(request), exceptionStr);
return ResultHelper.failure(ErrorCode.PARAMETER_VALIDATION_FAILED.getCode(), exceptionStr);
}
// 处理自定义异常
@ExceptionHandler(value = {AbstractException.class})
public Result<Void> handleAbstractException(HttpServletRequest request, AbstractException ex) {
String requestURL = getUrl(request);
log.error("[{}] {} [ex] {}", request.getMethod(), requestURL, ex.toString());
return ResultHelper.failure(ex.getMessage());
}

// 兜底处理
@ExceptionHandler(value = Throwable.class)
public Result<Void> handleThrowable(HttpServletRequest request, Throwable throwable) {
log.error("[{}] {} ", request.getMethod(), getUrl(request), throwable);
return ResultHelper.failure(ErrorCode.SERVICE_ERROR);
}
// 获取浏览地址信息
private String getUrl(HttpServletRequest request) {
if (StrUtil.isEmpty(request.getQueryString())) {
return request.getRequestURL().toString();
}
return request.getRequestURL().toString() + "?" + request.getQueryString();
}
}

测试全局异常的定义

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
    @PostMapping("/test")
public String test(@RequestBody String request) {
return (new ServerTemplate<String, String>() {

@Override
protected void validParam(String request) {
if (request == null){
throw new BusinessException(ErrorCode.CLIENT_ERROR.getCode(), "请求参数不能为空");
}
}

@Override
protected String doProcess(String request) {
return request;
}

}).process(request);
}
}

小结

上文中实现了统一返回格式以及统一的模板方法,以及为了简化写法的自动包装类和全局异常的处理,在日常开发中每个人的写法各异,统一的写法规定可以避免错误的代码实现以及提升代码的可读性,可以提升整个系统的可靠性,我们可以借助springboot的注解来实现这些在