Controller 层代码就该这么写,简洁又优雅! |
您所在的位置:网站首页 › 三菱欧蓝德用车感受 › Controller 层代码就该这么写,简洁又优雅! |
点击关注公众号,Java干货及时送达 推荐阅读: Spring 旗下最牛逼的国产项目! Nacos 2.2 正式发布,这次更新太炸了! 来源:https://juejin.cn/post/7123091045071454238 一个优秀的 Controller 层逻辑说到 Controller,相信大家都不陌生,它可以很方便地对外提供数据接口。它的定位,我认为是「不可或缺的配角」。 说它不可或缺是因为无论是传统的三层架构还是现在的 COLA 架构,Controller 层依旧有一席之地,说明他的必要性。 说它是配角是因为 Controller 层的代码一般是不负责具体的逻辑业务逻辑实现,但是它负责接收和响应请求。 从现状看问题Controller 主要的工作有以下几项: 接收请求并解析参数 调用 Service 执行具体的业务代码(可能包含参数校验) 捕获业务逻辑异常做出反馈 业务逻辑执行成功做出响应 //DTO @Data public class TestDTO { private Integer num; private String type; } //Service @Service public class TestService { public Double service(TestDTO testDTO) throws Exception { if (testDTO.getNum() 1) { result = result * num; num -= 1; } return result; } throw new Exception("未识别的算法"); } } //Controller @RestController public 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 代码会有几个问题: 参数校验过多地耦合了业务代码,违背单一职责原则 可能在多个业务中都抛出同一个异常,导致代码重复 各种异常反馈和成功响应格式不统一,接口对接不友好 改造 Controller 层逻辑 统一返回结构统一返回值类型无论项目前后端是否分离都是非常必要的,方便对接接口的开发人员更加清晰地知道这个接口的调用是否成功(不能仅仅简单地看返回值是否为 null 就判断成功与否,因为有些接口的设计就是如此)。 推荐一个开源免费的 Spring Boot 最全教程: https://github.com/javastacks/spring-boot-best-practice 使用一个状态码、状态信息就能清楚地了解接口调用情况: //定义返回数据结构 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; //省略get、set方法和构造方法 } //统一返回数据结构 @Data @NoArgsConstructor @AllArgsConstructor public class Result { private Integer code; private String message; private T data; public static Result success(T data) { return new Result(ResultEnum.SUCCESS.getCode(), ResultEnum.SUCCESS.getMessage(), data); } public static Result 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 Result instance(Integer code, String message, T data) { Result result = new Result(); result.setCode(code); result.setMessage(message); result.setData(data); return result; } }统一返回结构后,在 Controller 中就可以使用了,但是每一个 Controller 都写这么一段最终封装的逻辑,这些都是很重复的工作,所以还要继续想办法进一步处理统一返回结构。Spring Boot 学习笔记,分享给你,学习太好了。 统一包装处理Spring 中提供了一个类 ResponseBodyAdvice ,能帮助我们实现上述需求: public interface ResponseBodyAdvice { boolean supports(MethodParameter returnType, Class> converterType); @Nullable T beforeBodyWrite(@Nullable T body, MethodParameter returnType, MediaType selectedContentType, Class> selectedConverterType, ServerHttpRequest request, ServerHttpResponse response); }ResponseBodyAdvice 是对 Controller 返回的内容在 HttpMessageConverter 进行类型转换之前拦截,进行相应的处理操作后,再将结果返回给客户端。 那这样就可以把统一包装的工作放到这个类里面: supports: 判断是否要交给 beforeBodyWrite 方法执行,ture:需要;false:不需要 beforeBodyWrite: 对 response 进行具体的处理 // 如果引入了swagger或knife4j的文档生成组件,这里需要仅扫描自己项目的包,否则文档无法正常生成 @RestControllerAdvice(basePackages = "com.example.demo") public class ResponseAdvice implements ResponseBodyAdvice { @Override public boolean supports(MethodParameter returnType, Class> converterType) { // 如果不需要进行封装的,可以添加一些校验手段,比如添加标记排除的注解 return true; } @Override public Object beforeBodyWrite(Object body, MethodParameter returnType, MediaType selectedContentType, Class> selectedConverterType, ServerHttpRequest request, ServerHttpResponse response) { // 提供一定的灵活度,如果body已经被包装了,就不进行包装 if (body instanceof Result) { return body; } return Result.success(body); } }经过这样改造,既能实现对 Controller 返回的数据进行统一包装,又不需要对原有代码进行大量的改动。 另外,如果你近期准备面试跳槽,建议在Java面试库小程序在线刷题,涵盖 2000+ 道 Java 面试题,几乎覆盖了所有主流技术面试题。 参数校验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,这个类有两个作用(实际上可以从名字上得到一点启发) 用于解析 @RequestBody 标注的参数 处理 @ResponseBody 标注方法的返回值 解析 @RequestBoyd 标注参数的方法是 resolveArgument。 public class RequestResponseBodyMethodProcessor extends AbstractMessageConverterMethodProcessor { /** * Throws MethodArgumentNotValidException if validation fails. * @throws HttpMessageNotReadableException if {@link RequestBody#required()} * is {@code true} and there is no body content or if there is no suitable * converter to read the content with. */ @Override public Object resolveArgument(MethodParameter parameter, @Nullable ModelAndViewContainer mavContainer, NativeWebRequest webRequest, @Nullable WebDataBinderFactory binderFactory) throws Exception { parameter = parameter.nestedIfOptional(); //把请求数据封装成标注的DTO对象 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); //如果校验不通过,就抛出MethodArgumentNotValidException异常 //如果我们不自己捕获,那么最终会由DefaultHandlerExceptionResolver捕获处理 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 { /** * Validate the binding target if applicable. *The default implementation checks for {@code @javax.validation.Valid}, * Spring's {@link org.springframework.validation.annotation.Validated}, * and custom annotations whose name starts with "Valid". * @param binder the DataBinder to be used * @param parameter the method parameter descriptor * @since 4.1.5 * @see #isBindExceptionRequired */ protected void validateIfApplicable(WebDataBinder binder, MethodParameter parameter) { //获取参数上的所有注解 Annotation[] annotations = parameter.getParameterAnnotations(); for (Annotation ann : annotations) { //如果注解中包含了@Valid、@Validated或者是名字以Valid开头的注解就进行参数校验 Object[] validationHints = ValidationAnnotationUtils.determineValidationHints(ann); if (validationHints != null) { //实际校验逻辑,最终会调用Hibernate Validator执行真正的校验 //所以Spring Validation是对Hibernate Validation的二次封装 binder.validate(validationHints); break; } } } } ②@RequestBody 参数校验 Post、Put 请求的参数推荐使用 @RequestBody 请求体参数。 对 @RequestBody 参数进行校验需要在 DTO 对象中加入校验条件后,再搭配 @Validated 即可完成自动校验。 如果校验失败,会抛出 ConstraintViolationException 异常。 //DTO @Data public class TestDTO { @NotBlank private String userName; @NotBlank @Length(min = 6, max = 20) private String password; @NotNull @Email private String email; } //Controller @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 { //指定了创建切面的Bean的注解 private Class[] groups = determineValidationGroups(invocation); ExecutableValidator execVal = this.validator.forExecutables(); Method methodToValidate = invocation.getMethod(); Set result; try { //方法入参校验,最终还是委托给Hibernate Validator来校验 //所以Spring Validation是对Hibernate Validation的二次封装 result = execVal.validateParameters( invocation.getThis(), methodToValidate, invocation.getArguments(), groups); } catch (IllegalArgumentException ex) { ... } //校验不通过抛出ConstraintViolationException异常 if (!result.isEmpty()) { throw new ConstraintViolationException(result); } //Controller方法调用 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 "不是一个手机号码格式"; /** * Constraint要求的属性,用于分组校验和扩展,留空就好 */ Class[] groups() default {}; Class handleBusinessException(BusinessException ex) { return Result.failed(ex.getMessage()); } /** * 捕获 {@code ForbiddenException} 异常 */ @ExceptionHandler({ForbiddenException.class}) public Result handleForbiddenException(ForbiddenException ex) { return Result.failed(ResultEnum.FORBIDDEN); } /** * {@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(ResultEnum.VALIDATE_FAILED.getCode(), 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()); } } 总结做好了这一切改动后,可以发现 Controller 的代码变得非常简洁,可以很清楚地知道每一个参数、每一个 DTO 的校验规则,可以很明确地看到每一个 Controller 方法返回的是什么数据,也可以方便每一个异常应该如何进行反馈。 这一套操作下来后,我们能更加专注于业务逻辑的开发,代码简介、功能完善,何乐而不为呢? End Spring 旗下最牛逼的国产项目! 23 种设计模式实战(很全) Spring Boot 3.0 正式发布,王炸!! Spring Cloud Alibaba 最新重磅发布! Nacos 2.2 正式发布,这次更新太炸了! Spring Cloud 微服务最新课程! |
今日新闻 |
点击排行 |
|
推荐新闻 |
图片新闻 |
|
专题文章 |
CopyRight 2018-2019 实验室设备网 版权所有 win10的实时保护怎么永久关闭 |