前言
一个完整的后端请求由 4 部分组成:
- 接口地址(也就是 URL 地址)
- 请求方式(一般就是 get、set,当然还有 put、delete)
- 请求数据(request,有 head 跟 body)
- 响应数据(response)
Controller 主要的工作有以下几项:
- 接收请求并解析参数
- 调用 Service 执行具体的业务代码(可能包含参数校验)
- 捕获业务逻辑异常做出反馈
- 业务逻辑执行成功做出响应
解决以下 3 个问题:
- 返回响应数据该如何统一的进行处理
- 接收到请求,处理业务逻辑时抛出了异常又该如何处理
- 当接收到请求时,如何优雅的校验参数
统一返回结构
封装Result
package com.wanqi.result;
public interface IResult {
Integer getCode();
String getMessage();
}
定义状态码
package com.wanqi.result;
public enum ResultEnum implements IResult {
SUCCESS(2001, "接口调用成功"),
VALIDATE_FAILED(2002, "参数校验失败"),
COMMON_FAILED(2003, "接口调用失败"),
FORBIDDEN(2004, "没有权限访问资源"),
DATA_CONVERSION(2005, "数据转换异常");
private Integer code;
private String message;
ResultEnum(int code, String message) {
this.code=code;
this.message=message;
}
@Override
public Integer getCode() {
return code;
}
@Override
public String getMessage() {
return message;
}
}
Result包装类
package com.wanqi.result;
public class Result<T> {
private Integer code;
private String message;
private T data;
public Integer getCode() {
return code;
}
public void setCode(Integer code) {
this.code = code;
}
public String getMessage() {
return message;
}
public void setMessage(String message) {
this.message = message;
}
public T getData() {
return data;
}
public void setData(T data) {
this.data = data;
}
public Result() {
}
public Result(Integer code, String message, T data) {
this.code = code;
this.message = message;
this.data = data;
}
public static <T> Result<T> success(T data) {
return new Result<>(ResultEnum.SUCCESS.getCode(), ResultEnum.SUCCESS.getMessage(), data);
}
public static <T> Result<T> success(String message, T data) {
return new Result<>(ResultEnum.SUCCESS.getCode(), message, data);
}
public static Result<?> failed() {
return new Result<>(ResultEnum.COMMON_FAILED.getCode(), ResultEnum.COMMON_FAILED.getMessage(), null);
}
public static Result<?> failed(String message) {
return new Result<>(ResultEnum.COMMON_FAILED.getCode(), message, null);
}
public static Result<?> failed(Integer code,String message) {
return new Result<>(code, message, null);
}
public static Result<?> failed(IResult errorResult) {
return new Result<>(errorResult.getCode(), errorResult.getMessage(), null);
}
public static <T> Result<T> instance(Integer code, String message, T data) {
Result<T> result = new Result<>();
result.setCode(code);
result.setMessage(message);
result.setData(data);
return result;
}
}
统一包装处理
package com.wanqi.advice;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.wanqi.exception.APIException;
import com.wanqi.result.Result;
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;
// 如果引入了swagger或knife4j的文档生成组件,这里需要仅扫描自己项目的包,否则文档无法正常生成
@RestControllerAdvice(basePackages = "com.wanqi.controller")
public class ResponseAdvice implements ResponseBodyAdvice<Object> {
@Override
public boolean supports(MethodParameter returnType, Class<? extends HttpMessageConverter<?>> converterType) {
// response是Result类型不进行包装
return !returnType.getParameterType().isAssignableFrom(Result.class);
}
@Override
public Object beforeBodyWrite(Object body, MethodParameter returnType, MediaType selectedContentType, Class<? extends HttpMessageConverter<?>> selectedConverterType, ServerHttpRequest request, ServerHttpResponse response) {
if(returnType.getGenericParameterType().equals(String.class)){
ObjectMapper objectMapper = new ObjectMapper();
try {
return objectMapper.writeValueAsString(Result.success(body));
} catch (JsonProcessingException e) {
throw new APIException(e.getMessage());
}
}
return Result.success(body);
}
}
Controller层
package com.wanqi.controller;
import com.wanqi.pojo.TestDTO;
import com.wanqi.pojo.UserDTO;
import org.hibernate.validator.constraints.*;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.*;
import javax.validation.constraints.Max;
import javax.validation.constraints.Min;
@RestController
public class TestController {
@PostMapping("/test")
public TestDTO test(@RequestBody TestDTO testDTO) {
return testDTO;
}
@PostMapping("/test1")
public String test1(@RequestBody TestDTO testDTO) {
return testDTO.getName();
}
}
不统一处理返回
自定义注解
package com.wanqi.advice;
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 NotControllerResponseAdvice {
}
在增强过滤方法上过滤包含这个注解的方法
@RestControllerAdvice(basePackages = "com.wanqi.controller")
public class ResponseAdvice implements ResponseBodyAdvice<Object> {
@Override
public boolean supports(MethodParameter returnType, Class<? extends HttpMessageConverter<?>> converterType) {
// response是Result类型,或者注释了NotControllerResponseAdvice都不进行包装
return !(returnType.getParameterType().isAssignableFrom(Result.class)
|| returnType.hasMethodAnnotation(NotControllerResponseAdvice.class));
}
.....
controller
- 在不需要统一处理的方法上使用@NotControllerResponseAdvice注解即可
package com.wanqi.controller;
import com.wanqi.advice.NotControllerResponseAdvice;
import com.wanqi.pojo.TestDTO;
import com.wanqi.pojo.UserDTO;
import com.wanqi.result.Result;
import org.hibernate.validator.constraints.*;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.*;
import javax.validation.Valid;
import javax.validation.constraints.Max;
import javax.validation.constraints.Min;
@Validated
@RestController
public class TestController {
@GetMapping("/health")
@NotControllerResponseAdvice
public String health() {
return "success";
}
@GetMapping("/health1")
public Result health1() {
return Result.success("dddd");
}
}
自定义异常与统一拦截异常
自定义异常
package com.wanqi.exception;
import com.wanqi.result.ResultEnum;
//自定义异常
public class APIException extends RuntimeException {
private int code;
private String msg;
public int getCode() {
return code;
}
public String getMsg() {
return msg;
}
public APIException(String message) {
super(message);
this.code = ResultEnum.DATA_CONVERSION.getCode();
this.msg = ResultEnum.DATA_CONVERSION.getMessage();
}
}
统一拦截异常
package com.wanqi.advice;
import com.wanqi.exception.APIException;
import com.wanqi.result.IResult;
import com.wanqi.result.Result;
import com.wanqi.result.ResultEnum;
import org.springframework.util.StringUtils;
import org.springframework.validation.BindingResult;
import org.springframework.validation.FieldError;
import org.springframework.web.bind.MethodArgumentNotValidException;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.RestControllerAdvice;
import javax.validation.ConstraintViolationException;
//统一拦截异常
@RestControllerAdvice(basePackages = "com.wanqi")
public class ControllerExceptionAdvice {
/**
* 捕获 {@code APIException} 异常
*/
@ExceptionHandler({APIException.class})
public Result<?> handleBusinessException(APIException ex) {
return Result.failed(ex.getMessage());
}
/**
* {@code @RequestBody} 参数校验不通过时抛出的异常处理
*/
@ExceptionHandler({MethodArgumentNotValidException.class})
public Result<?> handleMethodArgumentNotValidException(MethodArgumentNotValidException ex) {
BindingResult bindingResult = ex.getBindingResult();
StringBuilder sb = new StringBuilder("校验失败:");
for (FieldError fieldError : bindingResult.getFieldErrors()) {
sb.append(fieldError.getField()).append(":").append(fieldError.getDefaultMessage()).append(", ");
}
String msg = sb.toString();
if (StringUtils.hasText(msg)) {
return Result.failed(new IResult() {
@Override
public Integer getCode() {
return ResultEnum.VALIDATE_FAILED.getCode();
}
@Override
public String getMessage() {
return msg;
}
});
}
return Result.failed(ResultEnum.VALIDATE_FAILED);
}
/**
* {@code @PathVariable} 和 {@code @RequestParam} 参数校验不通过时抛出的异常处理
*/
@ExceptionHandler({ConstraintViolationException.class})
public Result<?> handleConstraintViolationException(ConstraintViolationException ex) {
if (StringUtils.hasText(ex.getMessage())) {
return Result.failed(ResultEnum.VALIDATE_FAILED.getCode(), ex.getMessage());
}
return Result.failed(ResultEnum.VALIDATE_FAILED);
}
/**
* 顶级异常捕获并统一处理,当其他异常无法处理时候选择使用
*/
@ExceptionHandler({Exception.class})
public Result<?> handle(Exception ex) {
return Result.failed(ex.getMessage());
}
}
参数校验
- 使用hibernate-validator,@Validated注解参数校验
<!-- https://mvnrepository.com/artifact/org.hibernate/hibernate-validator -->
<dependency>
<groupId>org.hibernate</groupId>
<artifactId>hibernate-validator</artifactId>
<version>6.2.5.Final</version>
</dependency>
@RequestBody 参数校验
- Post、Put 请求的参数推荐使用 @RequestBody 请求体参数。
- 对 @RequestBody 参数进行校验需要在对象中加入校验条件后,再搭配 @Validated 即可完成自动校验。
UserDTO
package com.wanqi.pojo;
import org.hibernate.validator.constraints.Length;
import javax.validation.constraints.NotNull;
public class UserDTO {
private Long userId;
@NotNull
@Length(min = 2,max = 10)
private String userName;
@NotNull
@Length(min = 6,max = 20)
private String account;
@NotNull
@Length(min = 8,max = 20)
private String password;
public Long getUserId() {
return userId;
}
public void setUserId(Long userId) {
this.userId = userId;
}
public String getUserName() {
return userName;
}
public void setUserName(String userName) {
this.userName = userName;
}
public String getAccount() {
return account;
}
public void setAccount(String account) {
this.account = account;
}
public String getPassword() {
return password;
}
public void setPassword(String password) {
this.password = password;
}
@Override
public String toString() {
return "UserDTO{" +
"userId=" + userId +
", userName='" + userName + '\'' +
", account='" + account + '\'' +
", password='" + password + '\'' +
'}';
}
}
controller
package com.wanqi.controller;
import com.wanqi.pojo.TestDTO;
import com.wanqi.pojo.UserDTO;
import org.hibernate.validator.constraints.*;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.*;
import javax.validation.constraints.Max;
import javax.validation.constraints.Min;
@Validated
@RestController
public class TestController {
// 使用@Valid或者@Validated
@PostMapping("/user")
public UserDTO user(@RequestBody @Validated UserDTO userDTO) {
return userDTO;
}
}
@PathVariable 和 @RequestParam 参数校验
- Get 请求的参数接收一般依赖这两个注解,但是处于 url 有长度限制和代码的可维护性,超过 5 个参数尽量用实体来传参。
- 对 @PathVariable 和 @RequestParam 参数进行校验需要在入参声明约束的注解。
controller
package com.wanqi.controller;
import com.wanqi.pojo.TestDTO;
import com.wanqi.pojo.UserDTO;
import org.hibernate.validator.constraints.*;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.*;
import javax.validation.Valid;
import javax.validation.constraints.Max;
import javax.validation.constraints.Min;
@Validated
@RestController
public class TestController {
@GetMapping("/{num}")
public Integer detail(@PathVariable("num") @Min(value = 1 ,message = "不能小于1") @Max(20) Integer num) {
return num * num;
}
@GetMapping("/getByEmail")
public String getByAccount(@RequestParam @NotBlank @Email String email) {
return email;
}
}
自定义校验规则
自定义校验规则需要做两件事情:
- 自定义注解类,定义错误信息和一些其他需要的内容
- 注解校验器,定义判定规则
自定义注解类
package com.wanqi.validator;
import javax.validation.Constraint;
import javax.validation.Payload;
import java.lang.annotation.*;
@Target({ElementType.METHOD, ElementType.FIELD, ElementType.ANNOTATION_TYPE, ElementType.CONSTRUCTOR, ElementType.PARAMETER})
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Constraint(validatedBy = MobileValidator.class)
public @interface Mobile {
/**
* 是否允许为空
*/
boolean required() default true;
/**
* 校验不通过返回的提示信息
*/
String message() default "不是一个手机号码格式";
/**
* Constraint要求的属性,用于分组校验和扩展,留空就好
*/
Class<?>[] groups() default {};
Class<? extends Payload>[] payload() default {};
}
注解校验器,实现ConstraintValidator接口
package com.wanqi.validator;
import org.springframework.util.StringUtils;
import javax.validation.ConstraintValidator;
import javax.validation.ConstraintValidatorContext;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
public class MobileValidator implements ConstraintValidator<Mobile, CharSequence> {
private boolean required = false;
private final Pattern pattern = Pattern.compile("^1[34578][0-9]{9}$"); // 验证手机号
/**
* 在验证开始前调用注解里的方法,从而获取到一些注解里的参数
*
* @param constraintAnnotation annotation instance for a given constraint declaration
*/
@Override
public void initialize(Mobile constraintAnnotation) {
this.required = constraintAnnotation.required();
}
/**
* 判断参数是否合法
*
* @param value object to validate
* @param context context in which the constraint is evaluated
*/
@Override
public boolean isValid(CharSequence value, ConstraintValidatorContext context) {
if (this.required) {
// 验证
return isMobile(value);
}
if (StringUtils.hasText(value)) {
// 验证
return isMobile(value);
}
return true;
}
private boolean isMobile(final CharSequence str) {
Matcher m = pattern.matcher(str);
return m.matches();
}
}
使用
package com.wanqi.controller;
import com.wanqi.validator.Mobile;
import org.hibernate.validator.constraints.*;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.*;
@Validated
@RestController
public class TestController {
@GetMapping("/phone")
public String phone(@RequestParam @Mobile String phone) {
return phone;
}
}