<kbd id="afajh"><form id="afajh"></form></kbd>
<strong id="afajh"><dl id="afajh"></dl></strong>
    <del id="afajh"><form id="afajh"></form></del>
        1. <th id="afajh"><progress id="afajh"></progress></th>
          <b id="afajh"><abbr id="afajh"></abbr></b>
          <th id="afajh"><progress id="afajh"></progress></th>

          Controller 層代碼就該這么寫(xiě),簡(jiǎn)潔又優(yōu)雅!

          共 38823字,需瀏覽 78分鐘

           ·

          2023-10-06 22:47

          說(shuō)到 Controller,相信大家都不陌生,它可以很方便地對(duì)外提供數(shù)據(jù)接口。它的定位,我認(rèn)為是「不可或缺的配角」,說(shuō)它不可或缺是因?yàn)闊o(wú)論是傳統(tǒng)的三層架構(gòu)還是現(xiàn)在的COLA架構(gòu),Controller 層依舊有一席之地,說(shuō)明他的必要性;說(shuō)它是配角是因?yàn)?Controller 層的代碼一般是不負(fù)責(zé)具體的邏輯業(yè)務(wù)邏輯實(shí)現(xiàn),但是它負(fù)責(zé)接收和響應(yīng)請(qǐng)求

          從現(xiàn)狀看問(wèn)題

          Controller 主要的工作有以下幾項(xiàng)

          • 接收請(qǐng)求并解析參數(shù)
          • 調(diào)用 Service 執(zhí)行具體的業(yè)務(wù)代碼(可能包含參數(shù)校驗(yàn))
          • 捕獲業(yè)務(wù)邏輯異常做出反饋
          • 業(yè)務(wù)邏輯執(zhí)行成功做出響應(yīng)
          //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() <= 0) {
                      throw new Exception("輸入的數(shù)字需要大于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("未識(shí)別的算法");
              }
          }


          //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;
              }
          }

          如果真的按照上面所列的工作項(xiàng)來(lái)開(kāi)發(fā) Controller 代碼會(huì)有幾個(gè)問(wèn)題

          1. 參數(shù)校驗(yàn)過(guò)多地耦合了業(yè)務(wù)代碼,違背單一職責(zé)原則
          2. 可能在多個(gè)業(yè)務(wù)中都拋出同一個(gè)異常,導(dǎo)致代碼重復(fù)
          3. 各種異常反饋和成功響應(yīng)格式不統(tǒng)一,接口對(duì)接不友好

          改造 Controller 層邏輯

          統(tǒng)一返回結(jié)構(gòu)

          統(tǒng)一返回值類型無(wú)論項(xiàng)目前后端是否分離都是非常必要的,方便對(duì)接接口的開(kāi)發(fā)人員更加清晰地知道這個(gè)接口的調(diào)用是否成功(不能僅僅簡(jiǎn)單地看返回值是否為 null 就判斷成功與否,因?yàn)橛行┙涌诘脑O(shè)計(jì)就是如此),使用一個(gè)狀態(tài)碼、狀態(tài)信息就能清楚地了解接口調(diào)用情況

          //定義返回?cái)?shù)據(jù)結(jié)構(gòu)
          public interface IResult {
              Integer getCode();
              String getMessage();
          }

          //常用結(jié)果的枚舉
          public enum ResultEnum implements IResult {
              SUCCESS(2001"接口調(diào)用成功"),
              VALIDATE_FAILED(2002"參數(shù)校驗(yàn)失敗"),
              COMMON_FAILED(2003"接口調(diào)用失敗"),
              FORBIDDEN(2004"沒(méi)有權(quán)限訪問(wèn)資源");

              private Integer code;
              private String message;

              //省略get、set方法和構(gòu)造方法
          }

          //統(tǒng)一返回?cái)?shù)據(jù)結(jié)構(gòu)
          @Data
          @NoArgsConstructor
          @AllArgsConstructor
          public 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;
              }
          }

          統(tǒng)一返回結(jié)構(gòu)后,在 Controller 中就可以使用了,但是每一個(gè) Controller 都寫(xiě)這么一段最終封裝的邏輯,這些都是很重復(fù)的工作,所以還要繼續(xù)想辦法進(jìn)一步處理統(tǒng)一返回結(jié)構(gòu)

          統(tǒng)一包裝處理

          Spring 中提供了一個(gè)類 ResponseBodyAdvice ,能幫助我們實(shí)現(xiàn)上述需求

          ResponseBodyAdvice 是對(duì) Controller 返回的內(nèi)容在 HttpMessageConverter 進(jìn)行類型轉(zhuǎn)換之前攔截,進(jìn)行相應(yīng)的處理操作后,再將結(jié)果返回給客戶端。那這樣就可以把統(tǒng)一包裝的工作放到這個(gè)類里面。

          public interface ResponseBodyAdvice<T{
              boolean supports(MethodParameter returnType, Class<? extends HttpMessageConverter<?>> converterType);

              @Nullable
              beforeBodyWrite(@Nullable T body, MethodParameter returnType, MediaType selectedContentType, Class<? extends HttpMessageConverter<?>> selectedConverterType, ServerHttpRequest request, ServerHttpResponse response);
          }
          • supports:判斷是否要交給 beforeBodyWrite 方法執(zhí)行,ture:需要;false:不需要
          • beforeBodyWrite:對(duì) response 進(jìn)行具體的處理
          // 如果引入了swagger或knife4j的文檔生成組件,這里需要僅掃描自己項(xiàng)目的包,否則文檔無(wú)法正常生成
          @RestControllerAdvice(basePackages = "com.example.demo")
          public class ResponseAdvice implements ResponseBodyAdvice<Object{
              @Override
              public boolean supports(MethodParameter returnType, Class<? extends HttpMessageConverter<?>> converterType) {
                  // 如果不需要進(jìn)行封裝的,可以添加一些校驗(yàn)手段,比如添加標(biāo)記排除的注解
                  return true;
              }
            

              @Override
              public Object beforeBodyWrite(Object body, MethodParameter returnType, MediaType selectedContentType, Class<? extends HttpMessageConverter<?>> selectedConverterType, ServerHttpRequest request, ServerHttpResponse response) {
                  // 提供一定的靈活度,如果body已經(jīng)被包裝了,就不進(jìn)行包裝
                  if (body instanceof Result) {
                      return body;
                  }
                  return Result.success(body);
              }
          }

          經(jīng)過(guò)這樣改造,既能實(shí)現(xiàn)對(duì) Controller 返回的數(shù)據(jù)進(jìn)行統(tǒng)一包裝,又不需要對(duì)原有代碼進(jìn)行大量的改動(dòng)

          處理 cannot be cast to java.lang.String 問(wèn)題

          如果直接使用 ResponseBodyAdvice,對(duì)于一般的類型都沒(méi)有問(wèn)題,當(dāng)處理字符串類型時(shí),會(huì)拋出 xxx.包裝類 cannot be cast to java.lang.String 的類型轉(zhuǎn)換的異常

          ResponseBodyAdvice 實(shí)現(xiàn)類中 debug 發(fā)現(xiàn),只有 String 類型的 selectedConverterType 參數(shù)值是 org.springframework.http.converter.StringHttpMessageConverter,而其他數(shù)據(jù)類型的值是 org.springframework.http.converter.json.MappingJackson2HttpMessageConverter

          • String 類型

          • 其他類型 (如 Integer 類型)

          現(xiàn)在問(wèn)題已經(jīng)較為清晰了,因?yàn)槲覀冃枰祷匾粋€(gè) Result 對(duì)象

          所以使用 MappingJackson2HttpMessageConverter 是可以正常轉(zhuǎn)換的

          而使用 StringHttpMessageConverter 字符串轉(zhuǎn)換器會(huì)導(dǎo)致類型轉(zhuǎn)換失敗

          現(xiàn)在處理這個(gè)問(wèn)題有兩種方式

          1. beforeBodyWrite 方法處進(jìn)行判斷,如果返回值是 String 類型就對(duì) Result 對(duì)象手動(dòng)進(jìn)行轉(zhuǎn)換成 JSON 字符串,另外方便前端使用,最好在 @RequestMapping 中指定 ContentType

            @RestControllerAdvice(basePackages = "com.example.demo")
            public class ResponseAdvice implements ResponseBodyAdvice<Object{
                ...
                @Override
                public Object beforeBodyWrite(Object body, MethodParameter returnType, MediaType selectedContentType, Class<? extends HttpMessageConverter<?>> selectedConverterType, ServerHttpRequest request, ServerHttpResponse response) {
                    // 提供一定的靈活度,如果body已經(jīng)被包裝了,就不進(jìn)行包裝
                    if (body instanceof Result) {
                        return body;
                    }
                    // 如果返回值是String類型,那就手動(dòng)把Result對(duì)象轉(zhuǎn)換成JSON字符串
                    if (body instanceof String) {
                        try {
                            return this.objectMapper.writeValueAsString(Result.success(body));
                        } catch (JsonProcessingException e) {
                            throw new RuntimeException(e);
                        }
                    }
                    return Result.success(body);
                }
                ...
            }

            @GetMapping(value = "/returnString", produces = "application/json; charset=UTF-8")
            public String returnString() {
                return "success";
            }
          2. 修改 HttpMessageConverter 實(shí)例集合中 MappingJackson2HttpMessageConverter 的順序。因?yàn)榘l(fā)生上述問(wèn)題的根源所在是集合中 StringHttpMessageConverter 的順序先于 MappingJackson2HttpMessageConverter 的,調(diào)整順序后即可從根源上解決這個(gè)問(wèn)題

            • 網(wǎng)上有不少做法是直接在集合中第一位添加 MappingJackson2HttpMessageConverter

              @Configuration
              public class WebConfiguration implements WebMvcConfigurer {
                  
                  @Override
                  public void configureMessageConverters(List<HttpMessageConverter<?>> converters) {
                      converters.add(0new MappingJackson2HttpMessageConverter());
                  }
              }
            • 誠(chéng)然,這種方式可以解決問(wèn)題,但其實(shí)問(wèn)題的根源不是集合中缺少這一個(gè)轉(zhuǎn)換器,而是轉(zhuǎn)換器的順序?qū)е碌模宰詈侠淼淖龇☉?yīng)該是調(diào)整 MappingJackson2HttpMessageConverter 在集合中的順序

              @Configuration
              public class WebMvcConfiguration implements WebMvcConfigurer {

                  /**
                   * 交換MappingJackson2HttpMessageConverter與第一位元素
                   * 讓返回值類型為String的接口能正常返回包裝結(jié)果
                   *
                   * @param converters initially an empty list of converters
                   */

                  @Override
                  public void configureMessageConverters(List<HttpMessageConverter<?>> converters) {
                      for (int i = 0; i < converters.size(); i++) {
                          if (converters.get(i) instanceof MappingJackson2HttpMessageConverter) {
                              MappingJackson2HttpMessageConverter mappingJackson2HttpMessageConverter = (MappingJackson2HttpMessageConverter) converters.get(i);
                              converters.set(i, converters.get(0));
                              converters.set(0, mappingJackson2HttpMessageConverter);
                              break;
                          }
                      }
                  }
              }

          參數(shù)校驗(yàn)

          Java API 的規(guī)范 JSR303 定義了校驗(yàn)的標(biāo)準(zhǔn) validation-api ,其中一個(gè)比較出名的實(shí)現(xiàn)是 hibernate validationspring validation 是對(duì)其的二次封裝,常用于 SpringMVC 的參數(shù)自動(dòng)校驗(yàn),參數(shù)校驗(yàn)的代碼就不需要再與業(yè)務(wù)邏輯代碼進(jìn)行耦合了

          @PathVariable 和 @RequestParam 參數(shù)校驗(yàn)

          Get 請(qǐng)求的參數(shù)接收一般依賴這兩個(gè)注解,但是處于 url 有長(zhǎng)度限制和代碼的可維護(hù)性,超過(guò) 5 個(gè)參數(shù)盡量用實(shí)體來(lái)傳參

          對(duì) @PathVariable 和 @RequestParam 參數(shù)進(jìn)行校驗(yàn)需要在入?yún)⒙暶骷s束的注解

          如果校驗(yàn)失敗,會(huì)拋出 MethodArgumentNotValidException 異常

          @RestController(value = "prettyTestController")
          @RequestMapping("/pretty")
          @Validated
          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;
              }
          }

          校驗(yàn)原理

          在 SpringMVC 中,有一個(gè)類是 RequestResponseBodyMethodProcessor ,這個(gè)類有兩個(gè)作用(實(shí)際上可以從名字上得到一點(diǎn)啟發(fā))

          1. 用于解析 @RequestBody 標(biāo)注的參數(shù)
          2. 處理 @ResponseBody 標(biāo)注方法的返回值

          解析 @RequestBoyd 標(biāo)注參數(shù)的方法是 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();
                //把請(qǐng)求數(shù)據(jù)封裝成標(biāo)注的DTO對(duì)象
                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) {
                    //執(zhí)行數(shù)據(jù)校驗(yàn)
                    validateIfApplicable(binder, parameter);
                    //如果校驗(yàn)不通過(guò),就拋出MethodArgumentNotValidException異常
                    //如果我們不自己捕獲,那么最終會(huì)由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.
              * <p>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) {
              //獲取參數(shù)上的所有注解
                Annotation[] annotations = parameter.getParameterAnnotations();
                for (Annotation ann : annotations) {
                //如果注解中包含了@Valid、@Validated或者是名字以Valid開(kāi)頭的注解就進(jìn)行參數(shù)校驗(yàn)
                   Object[] validationHints = ValidationAnnotationUtils.determineValidationHints(ann);
                   if (validationHints != null) {
                  //實(shí)際校驗(yàn)邏輯,最終會(huì)調(diào)用Hibernate Validator執(zhí)行真正的校驗(yàn)
                  //所以Spring Validation是對(duì)Hibernate Validation的二次封裝
                      binder.validate(validationHints);
                      break;
                   }
                }
             }
          }

          @RequestBody 參數(shù)校驗(yàn)

          Post、Put 請(qǐng)求的參數(shù)推薦使用 @RequestBody 請(qǐng)求體參數(shù)

          對(duì) @RequestBody 參數(shù)進(jìn)行校驗(yàn)需要在 DTO 對(duì)象中加入校驗(yàn)條件后,再搭配 @Validated 即可完成自動(dòng)校驗(yàn)

          如果校驗(yàn)失敗,會(huì)拋出 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;
              }
          }

          校驗(yàn)原理

          聲明約束的方式,注解加到了參數(shù)上面,可以比較容易猜測(cè)到是使用了 AOP 對(duì)方法進(jìn)行增強(qiáng)

          而實(shí)際上 Spring 也是通過(guò) MethodValidationPostProcessor 動(dòng)態(tài)注冊(cè) AOP 切面,然后使用 MethodValidationInterceptor 對(duì)切點(diǎn)方法進(jìn)行織入增強(qiáng)

          public class MethodValidationPostProcessor extends AbstractBeanFactoryAwareAdvisingPostProcessor implements InitializingBean {
            
              //指定了創(chuàng)建切面的Bean的注解
             private Class<? extends Annotation> validatedAnnotationType = Validated.class;
            
              @Override
              public void afterPropertiesSet() {
                  //為所有@Validated標(biāo)注的Bean創(chuàng)建切面
                  Pointcut pointcut = new AnnotationMatchingPointcut(this.validatedAnnotationType, true);
                  //創(chuàng)建Advisor進(jìn)行增強(qiáng)
                  this.advisor = new DefaultPointcutAdvisor(pointcut, createMethodValidationAdvice(this.validator));
              }

              //創(chuàng)建Advice,本質(zhì)就是一個(gè)方法攔截器
              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 {
                  //無(wú)需增強(qiáng)的方法,直接跳過(guò)
                  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 {
                      //方法入?yún)⑿r?yàn),最終還是委托給Hibernate Validator來(lái)校驗(yàn)
                       //所以Spring Validation是對(duì)Hibernate Validation的二次封裝
                      result = execVal.validateParameters(
                          invocation.getThis(), methodToValidate, invocation.getArguments(), groups);
                  }
                  catch (IllegalArgumentException ex) {
                      ...
                  }
                  //校驗(yàn)不通過(guò)拋出ConstraintViolationException異常
                  if (!result.isEmpty()) {
                      throw new ConstraintViolationException(result);
                  }
                  //Controller方法調(diào)用
                  Object returnValue = invocation.proceed();
                  //下面是對(duì)返回值做校驗(yàn),流程和上面大概一樣
                  result = execVal.validateReturnValue(invocation.getThis(), methodToValidate, returnValue, groups);
                  if (!result.isEmpty()) {
                      throw new ConstraintViolationException(result);
                  }
                  return returnValue;
              }
          }

          自定義校驗(yàn)規(guī)則

          有些時(shí)候 JSR303 標(biāo)準(zhǔn)中提供的校驗(yàn)規(guī)則不滿足復(fù)雜的業(yè)務(wù)需求,也可以自定義校驗(yàn)規(guī)則

          自定義校驗(yàn)規(guī)則需要做兩件事情

          1. 自定義注解類,定義錯(cuò)誤信息和一些其他需要的內(nèi)容
          2. 注解校驗(yàn)器,定義判定規(guī)則
          //自定義注解類
          @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;

              /**
               * 校驗(yàn)不通過(guò)返回的提示信息
               */

              String message() default "不是一個(gè)手機(jī)號(hào)碼格式";

              /**
               * Constraint要求的屬性,用于分組校驗(yàn)和擴(kuò)展,留空就好
               */

              Class<?>[] groups() default {};
              Class<? extends Payload>[] payload() default {};
          }

          //注解校驗(yàn)器
          public class MobileValidator implements ConstraintValidator<MobileCharSequence{

              private boolean required = false;

              private final Pattern pattern = Pattern.compile("^1[34578][0-9]{9}$"); // 驗(yàn)證手機(jī)號(hào)

              /**
               * 在驗(yàn)證開(kāi)始前調(diào)用注解里的方法,從而獲取到一些注解里的參數(shù)
               *
               * @param constraintAnnotation annotation instance for a given constraint declaration
               */

              @Override
              public void initialize(Mobile constraintAnnotation) {
                  this.required = constraintAnnotation.required();
              }

              /**
               * 判斷參數(shù)是否合法
               *
               * @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) {
                      // 驗(yàn)證
                      return isMobile(value);
                  }
                  if (StringUtils.hasText(value)) {
                      // 驗(yàn)證
                      return isMobile(value);
                  }
                  return true;
              }

              private boolean isMobile(final CharSequence str) {
                  Matcher m = pattern.matcher(str);
                  return m.matches();
              }
          }

          自動(dòng)校驗(yàn)參數(shù)真的是一項(xiàng)非常必要、非常有意義的工作。JSR303 提供了豐富的參數(shù)校驗(yàn)規(guī)則,再加上復(fù)雜業(yè)務(wù)的自定義校驗(yàn)規(guī)則,完全把參數(shù)校驗(yàn)和業(yè)務(wù)邏輯解耦開(kāi),代碼更加簡(jiǎn)潔,符合單一職責(zé)原則。

          自定義異常與統(tǒng)一攔截異常

          原來(lái)的代碼中可以看到有幾個(gè)問(wèn)題

          1. 拋出的異常不夠具體,只是簡(jiǎn)單地把錯(cuò)誤信息放到了 Exception 中
          2. 拋出異常后,Controller 不能具體地根據(jù)異常做出反饋
          3. 雖然做了參數(shù)自動(dòng)校驗(yàn),但是異常返回結(jié)構(gòu)和正常返回結(jié)構(gòu)不一致
          4. 微信搜索公眾號(hào):架構(gòu)師指南,回復(fù):架構(gòu)師 領(lǐng)取資料 。

          自定義異常是為了后面統(tǒng)一攔截異常時(shí),對(duì)業(yè)務(wù)中的異常有更加細(xì)顆粒度的區(qū)分,攔截時(shí)針對(duì)不同的異常作出不同的響應(yīng)

          而統(tǒng)一攔截異常的目的一個(gè)是為了可以與前面定義下來(lái)的統(tǒng)一包裝返回結(jié)構(gòu)能對(duì)應(yīng)上,另一個(gè)是我們希望無(wú)論系統(tǒng)發(fā)生什么異常,Http 的狀態(tài)碼都要是 200 ,盡可能由業(yè)務(wù)來(lái)區(qū)分系統(tǒng)的異常

          //自定義異常
          public class ForbiddenException extends RuntimeException {
              public ForbiddenException(String message) {
                  super(message);
              }
          }

          //自定義異常
          public class BusinessException extends RuntimeException {
              public BusinessException(String message) {
                  super(message);
              }
          }

          //統(tǒng)一攔截異常
          @RestControllerAdvice(basePackages = "com.example.demo")
          public class ExceptionAdvice {

              /**
               * 捕獲 {@code BusinessException} 異常
               */

              @ExceptionHandler({BusinessException.class})
              public Result<?> 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} 參數(shù)校驗(yàn)不通過(guò)時(shí)拋出的異常處理
               */

              @ExceptionHandler({MethodArgumentNotValidException.class})
              public Result<?> handleMethodArgumentNotValidException(MethodArgumentNotValidException ex
          {
                  BindingResult bindingResult = ex.getBindingResult();
                  StringBuilder sb = new StringBuilder("校驗(yàn)失敗:");
                  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} 參數(shù)校驗(yàn)不通過(guò)時(shí)拋出的異常處理
               */

              @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);
              }

              /**
               * 頂級(jí)異常捕獲并統(tǒng)一處理,當(dāng)其他異常無(wú)法處理時(shí)候選擇使用
               */

              @ExceptionHandler({Exception.class})
              public Result<?> handle(Exception ex
          {
                  return Result.failed(ex.getMessage());
              }

          }

          總結(jié)

          做好了這一切改動(dòng)后,可以發(fā)現(xiàn) Controller 的代碼變得非常簡(jiǎn)潔,可以很清楚地知道每一個(gè)參數(shù)、每一個(gè) DTO 的校驗(yàn)規(guī)則,可以很明確地看到每一個(gè) Controller 方法返回的是什么數(shù)據(jù),也可以方便每一個(gè)異常應(yīng)該如何進(jìn)行反饋

          這一套操作下來(lái)后,我們能更加專注于業(yè)務(wù)邏輯的開(kāi)發(fā),代碼簡(jiǎn)潔、功能完善,何樂(lè)而不為呢?

          來(lái)源:juejin.cn/post/7123091045071454238

          PS:如果覺(jué)得我的分享不錯(cuò),歡迎大家隨手點(diǎn)贊、在看。

           關(guān)注公眾號(hào):Java后端編程,回復(fù)下面關(guān)鍵字 

               
               

          要Java學(xué)習(xí)完整路線,回復(fù)  路線 

          缺Java入門(mén)視頻,回復(fù) 視頻 

          要Java面試經(jīng)驗(yàn),回復(fù)  面試 

          缺Java項(xiàng)目,回復(fù): 項(xiàng)目 

          進(jìn)Java粉絲群: 加群 


          PS:如果覺(jué)得我的分享不錯(cuò),歡迎大家隨手點(diǎn)贊、在看。

                 
                 
          (完)




          加我"微信獲取一份 最新Java面試題資料

          請(qǐng)備注:666不然不通過(guò)~


          最近好文


          1、再見(jiàn)了 Shiro!

          2、無(wú)意中發(fā)現(xiàn)了一位清華妹子的資料庫(kù)!

          3、Spring Boot + Gzip 壓縮超大 JSON 對(duì)象,傳輸大小減少一半!

          4、牢記這16個(gè)SpringBoot 擴(kuò)展接口,寫(xiě)出更加漂亮的代碼

          5、聊聊大廠都怎么防止重復(fù)下單?



                          
                          
                           
                           
                             
                             
          最近面試BAT,整理一份面試資料Java面試BAT通關(guān)手冊(cè),覆蓋了Java核心技術(shù)、JVM、Java并發(fā)、SSM、微服務(wù)、數(shù)據(jù)庫(kù)、數(shù)據(jù)結(jié)構(gòu)等等。
          獲取方式:關(guān)注公眾號(hào)并回復(fù) java 領(lǐng)取,更多內(nèi)容陸續(xù)奉上。
          明天見(jiàn)(??ω??)??

          瀏覽 1017
          點(diǎn)贊
          評(píng)論
          收藏
          分享

          手機(jī)掃一掃分享

          分享
          舉報(bào)
          評(píng)論
          圖片
          表情
          推薦
          點(diǎn)贊
          評(píng)論
          收藏
          分享

          手機(jī)掃一掃分享

          分享
          舉報(bào)
          <kbd id="afajh"><form id="afajh"></form></kbd>
          <strong id="afajh"><dl id="afajh"></dl></strong>
            <del id="afajh"><form id="afajh"></form></del>
                1. <th id="afajh"><progress id="afajh"></progress></th>
                  <b id="afajh"><abbr id="afajh"></abbr></b>
                  <th id="afajh"><progress id="afajh"></progress></th>
                  青青草精品在线视频 | 成人大香蕉网 | 91麻豆精品成一区二区 | 免费清高视频一黄色情 | 欧美理论三级 |