精品欧美一区二区三区在线观看 _久久久久国色av免费观看性色_国产精品久久在线观看_亚洲第一综合网站_91精品又粗又猛又爽_小泽玛利亚一区二区免费_91亚洲精品国偷拍自产在线观看 _久久精品视频在线播放_美女精品久久久_欧美日韩国产成人在线

不服不行,這才是后端API接口應(yīng)該有的樣子!

開發(fā) 后端
說(shuō)到 Controller,相信大家都不陌生,它可以很方便地對(duì)外提供數(shù)據(jù)接口。它的定位,我認(rèn)為是「不可或缺的配角」。

說(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)開發(fā) Controller 代碼會(huì)有幾個(gè)問(wèn)題:

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

二、改造 Controller 層邏輯

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

統(tǒng)一返回值類型無(wú)論項(xiàng)目前后端是否分離都是非常必要的,方便對(duì)接接口的開發(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 都寫這么一段最終封裝的邏輯,這些都是很重復(fù)的工作,所以還要繼續(xù)想辦法進(jìn)一步處理統(tǒng)一返回結(jié)構(gòu)。

2.統(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
    T 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)。

3.處理 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)題有兩種方式:

  • 在 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";
}
  • 修改 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(0, new 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;
            }
        }
    }
}

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

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

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

(2) 校驗(yàn)原理

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

  • 用于解析 @RequestBody 標(biāo)注的參數(shù)
  • 處理 @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開頭的注解就進(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;
         }
      }
   }
}

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

(4) 校驗(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;
    }
}

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

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

  • 自定義注解類,定義錯(cuò)誤信息和一些其他需要的內(nèi)容
  • 注解校驗(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<Mobile, CharSequence> {

    private boolean required = false;

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

    /**
     * 在驗(yàn)證開始前調(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ù)邏輯解耦開,代碼更加簡(jiǎn)潔,符合單一職責(zé)原則。

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

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

  • 拋出的異常不夠具體,只是簡(jiǎn)單地把錯(cuò)誤信息放到了 Exception 中
  • 拋出異常后,Controller 不能具體地根據(jù)異常做出反饋
  • 雖然做了參數(shù)自動(dòng)校驗(yàn),但是異常返回結(jié)構(gòu)和正常返回結(jié)構(gòu)不一致

自定義異常是為了后面統(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ù)邏輯的開發(fā),代碼簡(jiǎn)潔、功能完善,何樂(lè)而不為呢?

責(zé)任編輯:趙寧寧 來(lái)源: 碼猿技術(shù)專欄
相關(guān)推薦

2025-11-03 04:00:00

ControllerDTO校驗(yàn)

2024-12-06 12:17:31

2019-10-24 15:11:10

數(shù)據(jù)分析PythonFineBI

2021-11-16 10:13:37

Facebook元宇宙VR

2025-06-10 08:10:00

VLANIP網(wǎng)絡(luò)

2011-01-04 09:54:41

Windows 8

2015-03-20 10:18:10

AndroidAndroid bug

2015-03-20 08:53:47

AndroidBUG修復(fù)

2020-09-03 07:21:15

數(shù)據(jù)庫(kù)數(shù)據(jù)SQL

2020-03-02 18:32:51

Windows 10Windows微軟

2024-09-25 15:00:25

2011-06-21 19:30:26

云計(jì)算私有云Gartner

2020-08-11 23:33:20

Kubernetes集群開發(fā)

2011-06-20 08:47:05

云計(jì)算私有云虛擬化

2022-03-14 10:41:39

電腦計(jì)算機(jī)迷你

2021-12-15 07:24:56

SocketTCPUDP

2020-07-22 14:45:20

2023-01-02 11:58:44

Excel

2022-01-26 00:05:00

AOPRPC遠(yuǎn)程調(diào)用
點(diǎn)贊
收藏

51CTO技術(shù)棧公眾號(hào)

亚洲成人网av| ...xxx性欧美| 国产不卡视频在线| 久久久免费看片| 国产乱码精品一区二区三区亚洲人| 亚洲人成伊人成综合网小说| 国产伦精品一区二区三区四区免费 | 日韩亚洲电影在线| 777精品久无码人妻蜜桃| 国产高清免费av在线| 蜜桃久久久久久| 久久久中精品2020中文| 亚洲av无码一区二区三区人| 国产精品一区二区三区www| 亚洲福中文字幕伊人影院| 日本日本精品二区免费| av一级黄色片| 欧美资源在线| 久久97精品久久久久久久不卡| av网页在线观看| 黄页网站在线| 亚洲国产成人一区二区三区| 国产美女精品久久久| 曰批又黄又爽免费视频| 极品少妇一区二区三区| 色噜噜国产精品视频一区二区| 久久国产劲爆∧v内射| 快播电影网址老女人久久| 亚洲曰韩产成在线| 久久精品日韩| 国产又爽又黄免费软件| 亚洲免费综合| 欧美黑人一级爽快片淫片高清| 成人黄色a级片| 偷拍亚洲色图| 亚洲成人aaa| 国产又粗又猛又爽又黄| 美女视频一区| 色成年激情久久综合| 免费看国产一级片| 宅男网站在线免费观看| 国产欧美日韩亚州综合| 久久精品日韩精品| 日韩在线观看视频网站| 国产成人午夜精品影院观看视频 | 另类天堂视频在线观看| 少妇av片在线观看| 国产精品一线天粉嫩av| 日韩av在线电影网| 国产精品一区二区无码对白| 99国内精品久久久久| 欧美三级韩国三级日本一级| 精品国产成人av在线免| 午夜av不卡| 精品久久久久久中文字幕大豆网| 久久久久久久久久伊人| 超碰caoporn久久| 中文一区二区完整视频在线观看 | 91亚洲自偷观看高清| 国产亚洲精品一区二区| 亚洲精品国产91| 国产一区不卡| 亚洲最新中文字幕| 成人黄色a级片| 久久综合av| 精品国产美女在线| 午夜激情福利网| 女人色偷偷aa久久天堂| 久久99国产精品自在自在app | 成人午夜黄色影院| 国产乱人乱偷精品视频a人人澡| 精品综合久久久久久8888| 国产一区二区丝袜| 国产精品视频在线观看免费| 国产裸体歌舞团一区二区| 91最新国产视频| 亚洲精品喷潮一区二区三区| 成人av在线一区二区三区| 国产精品视频福利| 性猛交xxxx| 久久久久久久电影| 亚洲图片都市激情| 在线中文免费视频| 亚洲成av人**亚洲成av**| 黄色一级在线视频| 日韩美女在线看免费观看| 欧美群妇大交群的观看方式| 在线观看中文av| 国产精品chinese在线观看| 日韩av在线电影网| 国产精品理论在线| 欧美fxxxxxx另类| 97国产在线视频| 亚洲色成人www永久网站| 乱一区二区av| 国产精品一区二区三区不卡| 国产在线一二| 伊人性伊人情综合网| 国产乱子伦农村叉叉叉| 黄色欧美视频| 亚洲第一精品自拍| 天堂网av2018| 国产日韩欧美一区二区三区在线观看| 国产精品久久99久久| 精品国产乱码一区二区三 | 日韩极品在线观看| 91探花福利精品国产自产在线| 国产刺激高潮av| 欧美激情一区在线观看| 日b视频免费观看| 欧美日韩精品免费观看视完整| 制服丝袜av成人在线看| 久久午夜夜伦鲁鲁片| 国产精品久久久久久| 欧美专区在线观看| 精品人妻午夜一区二区三区四区| 久久女同精品一区二区| 人妻无码一区二区三区四区| 香蕉成人av| 精品成人私密视频| 你懂得视频在线观看| 国产欧美69| 亚洲一区亚洲二区亚洲三区| 粉嫩av一区| 欧美日韩色婷婷| 国产乱淫av麻豆国产免费| 日韩中文在线电影| 欧美在线性爱视频| 天天色综合av| 一区二区三区精品久久久| 五月婷婷之婷婷| 国产99亚洲| 91av视频在线免费观看| www.日韩在线观看| 一区在线播放视频| 99热手机在线| 亚瑟一区二区三区四区| 国内精品模特av私拍在线观看| 国产精品无码久久av| 国产精品美女久久久久久| 日本黄网站免费| 亚洲+变态+欧美+另类+精品| 久久久久国产精品免费| 精品国产999久久久免费| 中文字幕日本不卡| 性欧美在线视频| 四季av一区二区三区免费观看| 国产成人欧美在线观看| 麻豆导航在线观看| 色综合久久88色综合天天| 最近中文字幕无免费| 亚洲国产专区校园欧美| 国产乱码精品一区二区三区日韩精品| 丝袜中文在线| 日韩久久精品一区| 免费一级肉体全黄毛片| 国产激情一区二区三区四区 | 怡红院在线播放| 日韩一区二区三区高清免费看看| 爱爱视频免费在线观看| 国产综合色产在线精品| 特级西西444| 亚洲国产精品免费视频| 欧美另类在线观看| 高清乱码毛片入口| 欧美日韩色婷婷| www色com| 麻豆成人久久精品二区三区红 | 欧美午夜片在线观看| 日本不卡一区视频| 国产乱码精品一区二区三区五月婷 | 日韩免费特黄一二三区| 国产欧美欧洲在线观看| 国产丝袜在线| 亚洲国产成人精品久久| 成人免费视频毛片| 国产午夜亚洲精品羞羞网站| 国产原创精品在线| 888久久久| 国产欧美日韩综合精品二区| 全亚洲第一av番号网站| 在线日韩日本国产亚洲| 99久久精品国产色欲| 亚洲成在人线免费| 亚洲做受高潮无遮挡| 奇米888四色在线精品| ijzzijzzij亚洲大全| 中文久久电影小说| 欧美在线免费观看| 在线免费观看黄色av| 日韩精品专区在线| 国产又大又黄视频| 中文字幕一区二区三区视频| youjizz.com日本| 日本成人中文字幕| av久久久久久| 国产99久久精品一区二区300| 91精品视频在线| 两个人看的在线视频www| 在线成人激情视频| 国产 日韩 欧美 综合| 欧洲精品中文字幕| 久久久久久久中文字幕| 久久久久久一二三区| 成人免费黄色av| 性8sex亚洲区入口| 国产人妻互换一区二区| 蜜桃一区二区| 成人一区二区在线| 国产精品久久久久77777丨| 欧美第一黄网免费网站| 成年人在线观看| 亚洲国产欧美一区| 999久久久久| 在线一区二区三区四区五区| 国产一级一片免费播放| 国产精品久久免费看| 亚洲国产精品无码久久久久高潮| 久国产精品韩国三级视频| 久章草在线视频| 亚洲二区在线| 日韩一二区视频| 成人激情诱惑| 看高清中日韩色视频| 深夜福利一区| 成人亚洲激情网| 电影久久久久久| 51精品在线观看| 国产经典三级在线| 欧美成人在线免费| 男人天堂手机在线| 最近2019中文字幕mv免费看| 九色网友自拍视频手机在线| 欧美精品一区二区久久婷婷| 国内精品久久久久久久久久| 欧美久久久久久久久中文字幕| 中文人妻av久久人妻18| 欧美性猛xxx| 日韩精品手机在线| 亚洲午夜免费福利视频| 久草视频在线免费看| 成人免费在线播放视频| 中文国语毛片高清视频| 国产精品少妇自拍| 国产三级在线观看完整版| 国产清纯在线一区二区www| 亚洲精品成人无码熟妇在线| 91丨porny丨在线| 蜜桃精品成人影片| 91美女蜜桃在线| 日本高清www| 国产日韩欧美在线一区| 国产调教在线观看| 国产精品美女久久久久aⅴ| 奇米网一区二区| 国产精品欧美极品| 成人高潮免费视频| 亚洲男人的天堂在线观看| 看片网站在线观看| 亚洲午夜久久久久中文字幕久| 日本特黄特色aaa大片免费| 午夜欧美2019年伦理| 国产精久久久久久| 天涯成人国产亚洲精品一区av| 天堂网av手机版| 一本高清dvd不卡在线观看| 亚洲精品国产无码| 欧美日本视频在线| 亚洲AV无码乱码国产精品牛牛 | 精品视频一二三| 亚洲人精品午夜在线观看| 成年人视频在线观看免费| 久久久精品日本| 成人三级小说| 日本亚洲欧洲色α| 欧美综合社区国产| 亚洲已满18点击进入在线看片 | 日韩一区不卡| 亚洲人metart人体| 国产伦精品一区二区三区四区视频_| 亚洲欧美日韩国产一区| 蜜臀av免费观看| 丰满白嫩尤物一区二区| 亚洲第一香蕉网| 国产精品人成在线观看免费| 久草网在线观看| 一本到不卡免费一区二区| 亚洲综合视频在线播放| 精品日韩欧美一区二区| 九色蝌蚪在线| 色综合久久久久久中文网| 伊人久久综合一区二区| 成人国产精品久久久久久亚洲| 波多野结衣欧美| 五月天色一区| 亚洲五月婷婷| 久久婷婷综合色| 成+人+亚洲+综合天堂| 中文天堂资源在线| 亚洲第一av色| 一女二男一黄一片| 日韩精品免费综合视频在线播放| 欧美一区二区三区在线观看免费| 久久久欧美一区二区| 久久人体av| 精品在线不卡| 一级毛片免费高清中文字幕久久网| 亚洲熟妇国产熟妇肥婆| 精品制服美女丁香| 免费看污黄网站在线观看| 亚洲专区一二三| 国产又粗又长又大视频| 精品偷拍一区二区三区在线看 | 欧美最猛性xxxx| 国产一区二区三区亚洲综合| 日本在线观看一区二区| 黄色成人91| 在线免费黄色网| 日本一区二区三区高清不卡| 精品在线视频免费| 91麻豆精品国产91久久久久| 国产区av在线| 97在线观看免费| 亚洲一区二区电影| 9999在线观看| 日本美女视频一区二区| 日韩精品卡通动漫网站| 亚洲成人精品影院| 成 人 黄 色 片 在线播放| 日日摸夜夜添一区| 日韩成人高清| 欧美区高清在线| 国产精品视频| 黄色污在线观看| 一二三区精品视频| 精品人妻一区二区三区含羞草 | 亚洲国产日韩欧美综合久久| 国产视频在线播放| 国产精品主播视频| 国产成人一区| 欧美黄色免费影院| 99久久国产综合色|国产精品| 久久久久亚洲av片无码下载蜜桃| 91精品国产综合久久福利软件| 亚洲xxxxxx| 国产热re99久久6国产精品| 日韩欧美一区二区三区在线视频| www日韩视频| 久久久99精品久久| 无码视频一区二区三区| 亚洲欧美中文日韩在线v日本| 热三久草你在线| 久久精品日产第一区二区三区乱码| 日韩午夜高潮| 日本一卡二卡在线| 五月婷婷久久丁香| 日韩a在线看| 国产91亚洲精品| 成人婷婷网色偷偷亚洲男人的天堂| 亚洲成人福利在线观看| 中文无字幕一区二区三区| 中文字幕一区二区三区人妻四季| 夜夜嗨av色综合久久久综合网 | 91久久综合| 成人h动漫精品一区| 色婷婷久久久久swag精品| 国产香蕉在线| 91色在线视频| 影音先锋久久久| 醉酒壮男gay强迫野外xx| 色婷婷综合久久久| 生活片a∨在线观看| av观看久久| 老鸭窝91久久精品色噜噜导演| 午夜影院黄色片| 日韩亚洲欧美一区二区三区| 国产www视频在线观看| 免费中文日韩| 激情久久五月天| 日本中文字幕网| 亚洲天堂成人在线视频| 亚洲久草在线| 麻豆tv在线播放| 中文字幕第一页久久| 国产黄色一区二区| 欧美中文在线观看| 99久久99久久精品国产片果冰| caopor在线| 色av综合在线| 在线看一级片| 日本成人黄色| 岛国精品一区二区| 啪啪小视频网站| 欧美国产一区二区三区| 国产精品美女久久久久久不卡 | 久青草视频在线播放| 久久精品人人做人人爽人人| 99草在线视频| 奇米四色中文综合久久| 欧美成人综合|