1. 实现目标
- 优雅校验接口入参
- 响应体格式统一处理
- 异常统一处理
2. 统一状态码
- 创建状态码接口,所有状态码必须实现这个接口,统一标准
package com.example.mavendemo.enums;
public interface StatusCode {
int getCode();
String getMsg();
}
package com.example.mavendemo.enums;
import lombok.AllArgsConstructor;
import lombok.Getter;
@Getter
@AllArgsConstructor
public enum ResponseCode implements StatusCode {
SUCCESS(0, "请求成功"),
FAILED(1, "请求失败"),
VALIDATE_ERROR(2, "参数校验失败"),
RESPONSE_ERROR(3, "返回失败"),
APP_ERROR(40000,"服务内部异常"),
BUSINESS_ERROR(40001,"业务异常");
private int code;
private String msg;
}
3. 统一响应体
- 响应结果统一使用ResultResponse进行包装,返回给前端
package com.example.mavendemo.dto;
import com.example.mavendemo.enums.ResponseCode;
import com.example.mavendemo.enums.StatusCode;
import lombok.AllArgsConstructor;
import lombok.Data;
@Data
@AllArgsConstructor
public class ResultResponse {
private int code;
private String msg;
private Object data;
public static ResultResponse ok() {
return new ResultResponse(ResponseCode.SUCCESS.getCode(), ResponseCode.SUCCESS.getMsg(), null);
}
public static ResultResponse ok(Object data) {
return new ResultResponse(ResponseCode.SUCCESS.getCode(), ResponseCode.SUCCESS.getMsg(), data);
}
public static ResultResponse error(StatusCode statusCode) {
return new ResultResponse(statusCode.getCode(), statusCode.getMsg(), null);
}
public static ResultResponse error(StatusCode statusCode, Object data) {
return new ResultResponse(statusCode.getCode(), statusCode.getMsg(), data);
}
}
4. 统一异常
package com.example.mavendemo.exception;
import com.example.mavendemo.enums.ResponseCode;
import com.example.mavendemo.enums.StatusCode;
import lombok.Getter;
@Getter
public class ApiException extends RuntimeException {
private StatusCode statusCode;
public ApiException(String message) {
super(message);
this.statusCode = ResponseCode.APP_ERROR;
}
public ApiException(StatusCode statusCode, String message) {
super(message);
this.statusCode = statusCode;
}
}
5. 统一入参校验
- 需要引入spring-boot-starter-validation
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.6.6</version>
<relativePath/>
</parent>
<groupId>com.example</groupId>
<artifactId>maven-demo</artifactId>
<version>0.0.1-SNAPSHOT</version>
<name>maven-demo</name>
<description>maven-demo</description>
<properties>
<java.version>8</java.version>
</properties>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-validation</artifactId>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
<executions>
<execution>
<goals>
<goal>repackage</goal>
</goals>
</execution>
</executions>
<configuration>
<includeSystemScope>true</includeSystemScope>
<mainClass>com.example.mavendemo.MavenDemoApplication</mainClass>
</configuration>
</plugin>
</plugins>
</build>
</project>
- 定义实体类,对于需要校验的字段添加注解,例如@NotBlank,@Max
package com.example.mavendemo.model;
import javax.validation.constraints.Max;
import javax.validation.constraints.NotBlank;
import lombok.Data;
@Data
public class User {
@NotBlank(message = "姓名不能为空")
private String name;
@Max(value = 18, message = "年龄不能大于18")
private Integer age;
private String address;
}
- 在传参时,使用@Valid,例如
@RequestBody @Valid User user
6. 统一返回结果
- 如果不想统一响应体,可以定义一个注解,标记具体的接口进行排除
package com.example.mavendemo.annotation;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
@Target({ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
public @interface NotResponseAdvice {
}
- 实现ResponseBodyAdvice接口,重写supports和beforeBodyWrite方法,完成对response body的增强
package com.example.mavendemo.advice;
import com.example.mavendemo.annotation.NotResponseAdvice;
import org.springframework.core.MethodParameter;
import org.springframework.http.MediaType;
import org.springframework.http.converter.HttpMessageConverter;
import org.springframework.http.server.ServerHttpRequest;
import org.springframework.http.server.ServerHttpResponse;
import org.springframework.web.bind.annotation.RestControllerAdvice;
import org.springframework.web.servlet.mvc.method.annotation.ResponseBodyAdvice;
import com.example.mavendemo.dto.ResultResponse;
@RestControllerAdvice(basePackages = {"com.example.mavendemo.controller"})
public class ControllerResponseAdvice implements ResponseBodyAdvice<Object> {
@Override
public boolean supports(MethodParameter returnType, Class<? extends HttpMessageConverter<?>> converterType) {
return !(returnType.getParameterType().isAssignableFrom(ResultResponse.class)
|| returnType.hasMethodAnnotation(NotResponseAdvice.class));
}
@Override
public Object beforeBodyWrite(Object body, MethodParameter returnType, MediaType selectedContentType,
Class<? extends HttpMessageConverter<?>> selectedConverterType, ServerHttpRequest request,
ServerHttpResponse response) {
return ResultResponse.ok(body);
}
}
- 这里需要注意String类型的返回值,使用上面的代码会报错java.lang.ClassCastException: cannot be cast to java.lang.String
- 需要介绍一下HttpMessageConverter接口,负责将请求信息转换为一个java对象,将java对象输出为响应信息;所以在触发@ResponseBody注解时,Spring都会遍历这个HttpMessageConverter列表,然后选择第一个符合返回值类型的转换器然后进行转换。
- HttpMessageConverter如下
- String类型会优先使用StringHttpMessageConverter转换器。实际上String类型既可以使用MappingJackson2HttpMessageConverter,也可以使用StringHttpMessageConverter来解析。所以可以将HttpMessageConverter列表反转,调换MappingJackson2HttpMessageConverter和StringHttpMessageConverter的顺序来解决。
- 实现WebMvcConfigurer,重写configureMessageConverters方法
package com.example.mavendemo.advice;
import java.util.Collections;
import java.util.List;
import org.springframework.context.annotation.Configuration;
import org.springframework.http.converter.HttpMessageConverter;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
@Configuration
public class WebResponseConfig implements WebMvcConfigurer {
@Override
public void configureMessageConverters(List<HttpMessageConverter<?>> converters) {
Collections.reverse(converters);
}
}
7. 统一异常处理
- 使用@RestControllerAdvice注解对Controller进行增强,配合@ExceptionHandler使用,统一处理异常
package com.example.mavendemo.advice;
import com.example.mavendemo.exception.ApiException;
import org.springframework.web.bind.MethodArgumentNotValidException;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.RestControllerAdvice;
import com.example.mavendemo.dto.ResultResponse;
import com.example.mavendemo.enums.ResponseCode;
@RestControllerAdvice
public class ControllerExceptionAdvice {
@ExceptionHandler(MethodArgumentNotValidException.class)
public ResultResponse methodArgumentNotValidExceptionHandler(MethodArgumentNotValidException e) {
return ResultResponse.error(ResponseCode.VALIDATE_ERROR,
e.getBindingResult().getAllErrors().get(0).getDefaultMessage());
}
@ExceptionHandler(ApiException.class)
public ResultResponse apiExceptionHandler(ApiException e) {
return ResultResponse.error(e.getStatusCode(), e.getMessage());
}
@ExceptionHandler(Exception.class)
public ResultResponse exceptionHandler(Exception e) {
return ResultResponse.error(ResponseCode.FAILED, e.getMessage());
}
}
8. 验证
package com.example.mavendemo.controller;
import javax.validation.Valid;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RestController;
import com.example.mavendemo.annotation.NotResponseAdvice;
import com.example.mavendemo.enums.ResponseCode;
import com.example.mavendemo.exception.ApiException;
import com.example.mavendemo.model.User;
@RestController
public class HelloController {
@PostMapping("/hello")
public User hello(@RequestBody @Valid User user) {
return user;
}
@GetMapping("/name")
public String getName(String name) {
return name;
}
@NotResponseAdvice
@GetMapping("/findUser")
public User getAddress(String name, Integer age) {
if (age > 18) {
throw new ApiException(ResponseCode.BUSINESS_ERROR, "查无此人");
}
User user = new User();
user.setName(name);
user.setAge(age);
user.setAddress("北京市王府井大街1号");
return user;
}
}
- 返回值是java对象
- 返回值是String类型
9. 入参解密
- 假如有些接口的请求参数需要加密传输,后端接收时需要进行统一解密
- 新建解密注解,这里限制只能在参数上使用,不能在方法上使用
package com.example.mavendemo.annotation;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.PARAMETER})
public @interface DecryptRequestBody {
}
- 新建请求拦截,继承RequestBodyAdviceAdapter抽象类,重写supports和beforeBodyRead方法,这里加解密AesUtil工具类换成你自己的
package com.example.mavendemo.advice;
import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.lang.reflect.Type;
import java.nio.charset.StandardCharsets;
import org.apache.commons.io.IOUtils;
import org.springframework.core.MethodParameter;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpInputMessage;
import org.springframework.http.converter.HttpMessageConverter;
import org.springframework.web.bind.annotation.RestControllerAdvice;
import org.springframework.web.servlet.mvc.method.annotation.RequestBodyAdviceAdapter;
import com.example.mavendemo.annotation.DecryptRequestBody;
import com.example.mavendemo.controller.HelloController;
import com.example.mavendemo.util.AesUtil;
@RestControllerAdvice(assignableTypes = {HelloController.class})
public class ControllerRequestAdvice extends RequestBodyAdviceAdapter {
@Override
public boolean supports(MethodParameter methodParameter, Type targetType,
Class<? extends HttpMessageConverter<?>> converterType) {
return methodParameter.hasParameterAnnotation(DecryptRequestBody.class);
}
@Override
public HttpInputMessage beforeBodyRead(HttpInputMessage inputMessage, MethodParameter parameter, Type targetType,
Class<? extends HttpMessageConverter<?>> converterType) throws IOException {
HttpHeaders headers = inputMessage.getHeaders();
String inputParam = IOUtils.toString(inputMessage.getBody(), StandardCharsets.UTF_8);
String decrypt = AesUtil.decrypt(inputParam);
ByteArrayInputStream decryptBody = new ByteArrayInputStream(decrypt.getBytes(StandardCharsets.UTF_8));
return new HttpInputMessage() {
@Override
public InputStream getBody() {
return decryptBody;
}
@Override
public HttpHeaders getHeaders() {
return headers;
}
};
}
}
- 使用方法,在接口入参处使用@DecryptRequestBody注解即可
@PostMapping("/decrypt")
public User decrypt(@DecryptRequestBody @RequestBody User user) {
return user;
}