来源:https://juejin.cn/post/7123091045071454238说到 Controller,相信大家都不陌生,它可以很方便地对外提供数据接口。它的定位,我认为是「不可或缺的配角」。说它不可或缺是因为无论是传统的三层架构还是现在的 COLA 架构,Controller 层依旧有一席之地,说明他的必要性。说它是配角是因为 Controller 层的代码一般是不负责具体的逻辑业务逻辑实现,但是它负责接收和响应请求。
- 调用 Service 执行具体的业务代码(可能包含参数校验)
@Datapublic class TestDTO { private Integer num; private String type;}@Servicepublic class TestService {
public Double service(TestDTO testDTO) throws Exception { if (testDTO.getNum() <= 0) { throw new Exception("输入的数字需要大于0"); } if (testDTO.getType().equals("square")) { return Math.pow(testDTO.getNum(), 2); } if (testDTO.getType().equals("factorial")) { double result = 1; int num = testDTO.getNum(); while (num > 1) { result = result * num; num -= 1; } return result; } throw new Exception("未识别的算法"); }}
@RestControllerpublic class TestController {
private TestService testService;
@PostMapping("/test") public Double test(@RequestBody TestDTO testDTO) { try { Double result = this.testService.service(testDTO); return result; } catch (Exception e) { throw new RuntimeException(e); } }
@Autowired public DTOid setTestService(TestService testService) { this.testService = testService; }}
如果真的按照上面所列的工作项来开发 Controller 代码会有几个问题:统一返回值类型无论项目前后端是否分离都是非常必要的,方便对接接口的开发人员更加清晰地知道这个接口的调用是否成功(不能仅仅简单地看返回值是否为 null 就判断成功与否,因为有些接口的设计就是如此)。使用一个状态码、状态信息就能清楚地了解接口调用情况:public interface IResult { Integer getCode(); String getMessage();}
public enum ResultEnum implements IResult { SUCCESS(2001, "接口调用成功"), VALIDATE_FAILED(2002, "参数校验失败"), COMMON_FAILED(2003, "接口调用失败"), FORBIDDEN(2004, "没有权限访问资源");
private Integer code; private String message; }
@Data@NoArgsConstructor@AllArgsConstructorpublic class Result<T> { private Integer code; private String message; private T 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(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; }}
统一返回结构后,在 Controller 中就可以使用了,但是每一个 Controller 都写这么一段最终封装的逻辑,这些都是很重复的工作,所以还要继续想办法进一步处理统一返回结构。Spring 中提供了一个类 ResponseBodyAdvice ,能帮助我们实现上述需求:public interface ResponseBodyAdvice<T> { boolean supports(MethodParameter returnType, Class<? extends HttpMessageConverter<?>> converterType);
@Nullable T beforeBodyWrite(@Nullable T body, MethodParameter returnType, MediaType selectedContentType, Class<? extends HttpMessageConverter<?>> selectedConverterType, ServerHttpRequest request, ServerHttpResponse response);}
ResponseBodyAdvice 是对 Controller 返回的内容在 HttpMessageConverter 进行类型转换之前拦截,进行相应的处理操作后,再将结果返回给客户端。- supports:判断是否要交给 beforeBodyWrite 方法执行,ture:需要;false:不需要
- beforeBodyWrite:对 response 进行具体的处理
@RestControllerAdvice(basePackages = "com.example.demo")public class ResponseAdvice implements ResponseBodyAdvice<Object> { @Override public boolean supports(MethodParameter returnType, Class<? extends HttpMessageConverter<?>> converterType) { return true; }
@Override public Object beforeBodyWrite(Object body, MethodParameter returnType, MediaType selectedContentType, Class<? extends HttpMessageConverter<?>> selectedConverterType, ServerHttpRequest request, ServerHttpResponse response) { if (body instanceof Result) { return body; } return Result.success(body); }}
经过这样改造,既能实现对 Controller 返回的数据进行统一包装,又不需要对原有代码进行大量的改动。Java API 的规范 JSR303 定义了校验的标准 validation-api ,其中一个比较出名的实现是 hibernate validation。spring validation 是对其的二次封装,常用于 SpringMVC 的参数自动校验,参数校验的代码就不需要再与业务逻辑代码进行耦合了。①@PathVariable 和 @RequestParam 参数校验Get 请求的参数接收一般依赖这两个注解,但是处于 url 有长度限制和代码的可维护性,超过 5 个参数尽量用实体来传参。对 @PathVariable 和 @RequestParam 参数进行校验需要在入参声明约束的注解。如果校验失败,会抛出 MethodArgumentNotValidException 异常。@RestController(value = "prettyTestController")@RequestMapping("/pretty")public class TestController {
private TestService testService;
@GetMapping("/{num}") public Integer detail(@PathVariable("num") @Min(1) @Max(20) Integer num) { return num * num; }
@GetMapping("/getByEmail") public TestDTO getByAccount(@RequestParam @NotBlank @Email String email) { TestDTO testDTO = new TestDTO(); testDTO.setEmail(email); return testDTO; }
@Autowired public void setTestService(TestService prettyTestService) { this.testService = prettyTestService; }}
在 SpringMVC 中,有一个类是 RequestResponseBodyMethodProcessor,这个类有两个作用(实际上可以从名字上得到一点启发)- 处理 @ResponseBody 标注方法的返回值
解析 @RequestBoyd 标注参数的方法是 resolveArgument。public class RequestResponseBodyMethodProcessor extends AbstractMessageConverterMethodProcessor { @Override public Object resolveArgument(MethodParameter parameter, @Nullable ModelAndViewContainer mavContainer, NativeWebRequest webRequest, @Nullable WebDataBinderFactory binderFactory) throws Exception {
parameter = parameter.nestedIfOptional(); Object arg = readWithMessageConverters(webRequest, parameter, parameter.getNestedGenericParameterType()); String name = Conventions.getVariableNameForParameter(parameter);
if (binderFactory != null) { WebDataBinder binder = binderFactory.createBinder(webRequest, arg, name); if (arg != null) { validateIfApplicable(binder, parameter); if (binder.getBindingResult().hasErrors() && isBindExceptionRequired(binder, parameter)) { throw new MethodArgumentNotValidException(parameter, binder.getBindingResult()); } } if (mavContainer != null) { mavContainer.addAttribute(BindingResult.MODEL_KEY_PREFIX + name, binder.getBindingResult()); } }
return adaptArgumentIfNecessary(arg, parameter); }}
public abstract class AbstractMessageConverterMethodArgumentResolver implements HandlerMethodArgumentResolver { protected void validateIfApplicable(WebDataBinder binder, MethodParameter parameter) { Annotation[] annotations = parameter.getParameterAnnotations(); for (Annotation ann : annotations) { Object[] validationHints = ValidationAnnotationUtils.determineValidationHints(ann); if (validationHints != null) { binder.validate(validationHints); break; } } }}
Post、Put 请求的参数推荐使用 @RequestBody 请求体参数。对 @RequestBody 参数进行校验需要在 DTO 对象中加入校验条件后,再搭配 @Validated 即可完成自动校验。如果校验失败,会抛出 ConstraintViolationException 异常。@Datapublic class TestDTO { @NotBlank private String userName;
@NotBlank @Length(min = 6, max = 20) private String password;
@NotNull @Email private String email;}
@RestController(value = "prettyTestController")@RequestMapping("/pretty")public class TestController {
private TestService testService;
@PostMapping("/test-validation") public void testValidation(@RequestBody @Validated TestDTO testDTO) { this.testService.save(testDTO); } @Autowired public void setTestService(TestService testService) { this.testService = testService; }}
声明约束的方式,注解加到了参数上面,可以比较容易猜测到是使用了 AOP 对方法进行增强。而实际上 Spring 也是通过 MethodValidationPostProcessor 动态注册 AOP 切面,然后使用 MethodValidationInterceptor 对切点方法进行织入增强。public class MethodValidationPostProcessor extends AbstractBeanFactoryAwareAdvisingPostProcessor implements InitializingBean {
private Class<? extends Annotation> validatedAnnotationType = Validated.class;
@Override public void afterPropertiesSet() { Pointcut pointcut = new AnnotationMatchingPointcut(this.validatedAnnotationType, true); this.advisor = new DefaultPointcutAdvisor(pointcut, createMethodValidationAdvice(this.validator)); }
protected Advice createMethodValidationAdvice(@Nullable Validator validator) { return (validator != null ? new MethodValidationInterceptor(validator) : new MethodValidationInterceptor()); }}
public class MethodValidationInterceptor implements MethodInterceptor { @Override public Object invoke(MethodInvocation invocation) throws Throwable { if (isFactoryBeanMetadataMethod(invocation.getMethod())) { return invocation.proceed(); }
Class<?>[] groups = determineValidationGroups(invocation); ExecutableValidator execVal = this.validator.forExecutables(); Method methodToValidate = invocation.getMethod(); Set<ConstraintViolation<Object>> result; try { result = execVal.validateParameters( invocation.getThis(), methodToValidate, invocation.getArguments(), groups); } catch (IllegalArgumentException ex) { ... } if (!result.isEmpty()) { throw new ConstraintViolationException(result); } Object returnValue = invocation.proceed(); result = execVal.validateReturnValue(invocation.getThis(), methodToValidate, returnValue, groups); if (!result.isEmpty()) { throw new ConstraintViolationException(result); } return returnValue; }}
有些时候 JSR303 标准中提供的校验规则不满足复杂的业务需求,也可以自定义校验规则。@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 "不是一个手机号码格式"; Class<?>[] groups() default {}; Class<? extends Payload>[] payload() default {};}
public class MobileValidator implements ConstraintValidator<Mobile, CharSequence> {
private boolean required = false;
private final Pattern pattern = Pattern.compile("^1[34578][0-9]{9}$");
@Override public void initialize(Mobile constraintAnnotation) { this.required = constraintAnnotation.required(); }
@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(); }}
自动校验参数真的是一项非常必要、非常有意义的工作。JSR303 提供了丰富的参数校验规则,再加上复杂业务的自定义校验规则,完全把参数校验和业务逻辑解耦开,代码更加简洁,符合单一职责原则。- 抛出的异常不够具体,只是简单地把错误信息放到了 Exception 中
- 抛出异常后,Controller 不能具体地根据异常做出反馈
- 虽然做了参数自动校验,但是异常返回结构和正常返回结构不一致
自定义异常是为了后面统一拦截异常时,对业务中的异常有更加细颗粒度的区分,拦截时针对不同的异常作出不同的响应。而统一拦截异常的目的一个是为了可以与前面定义下来的统一包装返回结构能对应上,另一个是我们希望无论系统发生什么异常,Http 的状态码都要是 200 ,尽可能由业务来区分系统的异常。public class ForbiddenException extends RuntimeException { public ForbiddenException(String message) { super(message); }}
public class BusinessException extends RuntimeException { public BusinessException(String message) { super(message); }}
@RestControllerAdvice(basePackages = "com.example.demo")public class ExceptionAdvice {
@ExceptionHandler({BusinessException.class}) public Result<?> handleBusinessException(BusinessException ex) { return Result.failed(ex.getMessage()); }
@ExceptionHandler({ForbiddenException.class}) public Result<?> handleForbiddenException(ForbiddenException ex) { return Result.failed(ResultEnum.FORBIDDEN); }
@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(ResultEnum.VALIDATE_FAILED.getCode(), msg); } return Result.failed(ResultEnum.VALIDATE_FAILED); }
@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()); }}
做好了这一切改动后,可以发现 Controller 的代码变得非常简洁,可以很清楚地知道每一个参数、每一个 DTO 的校验规则,可以很明确地看到每一个 Controller 方法返回的是什么数据,也可以方便每一个异常应该如何进行反馈。这一套操作下来后,我们能更加专注于业务逻辑的开发,代码简介、功能完善,何乐而不为呢?
程序员技术交流群
扫码添加我的微信,记得备注:城市、昵称和技术方向,我拉你进群。