在线不卡日本ⅴ一区v二区_精品一区二区中文字幕_天堂v在线视频_亚洲五月天婷婷中文网站

  • <menu id="lky3g"></menu>
  • <style id="lky3g"></style>
    <pre id="lky3g"><tt id="lky3g"></tt></pre>

    Controller層代碼這么寫,簡潔又優(yōu)雅

    一個(gè)優(yōu)秀的Controller層邏輯

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

    從現(xiàn)狀看問題

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

    • 接收請求并解析參數(shù)
    • 調(diào)用 Service 執(zhí)行具體的業(yè)務(wù)代碼(可能包含參數(shù)校驗(yàn))
    • 捕獲業(yè)務(wù)邏輯異常做出反饋
    • 業(yè)務(wù)邏輯執(zhí)行成功做出響應(yīng)

    //DTO@Datapublic class TestDTO { private Integer num; private String type;}//Service@Servicepublic class TestService { public Double service(TestDTO testDTO) throws Exception { if (testDTO.getNum() 1) { result = result * num; num -= 1; } return result; } throw new Exception(“未識別的算法”); }}//Controller@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; }}

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

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

    改造 Controller 層邏輯

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

    統(tǒng)一返回值類型無論項(xiàng)目前后端是否分離都是非常必要的,方便對接接口的開發(fā)人員更加清晰地知道這個(gè)接口的調(diào)用是否成功(不能僅僅簡單地看返回值是否為 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, “沒有權(quán)限訪問資源”); private Integer code; private String message; //省略get、set方法和構(gòu)造方法}//統(tǒng)一返回?cái)?shù)據(jù)結(jié)構(gòu)@Data@NoArgsConstructor@AllArgsConstructorpublic 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; }}

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

    統(tǒng)一包裝處理

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

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

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

    • supports:判斷是否要交給 beforeBodyWrite 方法執(zhí)行,ture:需要;false:不需要
    • beforeBodyWrite:對 response 進(jìn)行具體的處理

    // 如果引入了swagger或knife4j的文檔生成組件,這里需要僅掃描自己項(xiàng)目的包,否則文檔無法正常生成@RestControllerAdvice(basePackages = “com.example.demo”)public class ResponseAdvice implements ResponseBodyAdvice { @Override public boolean supports(MethodParameter returnType, Class converterType) { // 如果不需要進(jìn)行封裝的,可以添加一些校驗(yàn)手段,比如添加標(biāo)記排除的注解 return true; } @Override public Object beforeBodyWrite(Object body, MethodParameter returnType, MediaType selectedContentType, Class selectedConverterType, ServerHttpRequest request, ServerHttpResponse response) { // 提供一定的靈活度,如果body已經(jīng)被包裝了,就不進(jìn)行包裝 if (body instanceof Result) { return body; } return Result.success(body); }}

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

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

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

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

    Get 請求的參數(shù)接收一般依賴這兩個(gè)注解,但是處于 url 有長度限制和代碼的可維護(hù)性,超過 5 個(gè)參數(shù)盡量用實(shí)體來傳參 對 @PathVariable 和 @RequestParam 參數(shù)進(jìn)行校驗(yàn)需要在入?yún)⒙暶骷s束的注解

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

    @RestController(value = “prettyTestController”)@RequestMapping(“/pretty”)@Validatedpublic 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ā))

    • 用于解析 @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(); //把請求數(shù)據(jù)封裝成標(biāo)注的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) { //執(zhí)行數(shù)據(jù)校驗(yàn) validateIfApplicable(binder, parameter); //如果校驗(yàn)不通過,就拋出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) { //獲取參數(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)邏輯,最終會調(diào)用Hibernate Validator執(zhí)行真正的校驗(yàn) //所以Spring Validation是對Hibernate Validation的二次封裝 binder.validate(validationHints); break; } } }}

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

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

    對 @RequestBody 參數(shù)進(jìn)行校驗(yàn)需要在 DTO 對象中加入校驗(yàn)條件后,再搭配 @Validated 即可完成自動校驗(yàn) 如果校驗(yàn)失敗,會拋出 ConstraintViolationException 異常

    //DTO@Datapublic 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ù)上面,可以比較容易猜測到是使用了 AOP 對方法進(jìn)行增強(qiáng)

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

    public class MethodValidationPostProcessor extends AbstractBeanFactoryAwareAdvisingPostProcessor implements InitializingBean { //指定了創(chuàng)建切面的Bean的注解 private Class 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 { //無需增強(qiáng)的方法,直接跳過 if (isFactoryBeanMetadataMethod(invocation.getMethod())) { return invocation.proceed(); } Class[] groups = determineValidationGroups(invocation); ExecutableValidator execVal = this.validator.forExecutables(); Method methodToValidate = invocation.getMethod(); Set result; try { //方法入?yún)⑿r?yàn),最終還是委托給Hibernate Validator來校驗(yàn) //所以Spring Validation是對Hibernate Validation的二次封裝 result = execVal.validateParameters( invocation.getThis(), methodToValidate, invocation.getArguments(), groups); } catch (IllegalArgumentException ex) { … } //校驗(yàn)不通過拋出ConstraintViolationException異常 if (!result.isEmpty()) { throw new ConstraintViolationException(result); } //Controller方法調(diào)用 Object returnValue = invocation.proceed(); //下面是對返回值做校驗(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ī)則需要做兩件事情

    • 自定義注解類,定義錯(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)不通過返回的提示信息 */ String message() default “不是一個(gè)手機(jī)號碼格式”; /** * Constraint要求的屬性,用于分組校驗(yàn)和擴(kuò)展,留空就好 */ Class[] groups() default {}; Class[] payload() default {};}//注解校驗(yàn)器public class MobileValidator implements ConstraintValidator { private boolean required = false; private final Pattern pattern = Pattern.compile(“^1[34578][0-9]{9}#34;); // 驗(yàn)證手機(jī)號 /** * 在驗(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(); }}

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

    更多關(guān)于 Spring 參數(shù)校驗(yàn)請參考:

    https://juejin.cn/post/6856541106626363399

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

    原來的代碼中可以看到有幾個(gè)問題

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

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

    而統(tǒng)一攔截異常的目的一個(gè)是為了可以與前面定義下來的統(tǒng)一包裝返回結(jié)構(gòu)能對應(yīng)上,另一個(gè)是我們希望無論系統(tǒng)發(fā)生什么異常,Http 的狀態(tài)碼都要是 200 ,盡可能由業(yè)務(wù)來區(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)不通過時(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)不通過時(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); } /** * 頂級異常捕獲并統(tǒng)一處理,當(dāng)其他異常無法處理時(shí)候選擇使用 */ @ExceptionHandler({Exception.class}) public Result handle(Exception ex) { return Result.failed(ex.getMessage()); }}

    總結(jié)

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

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

    原文鏈接:https://mp.weixin.qq.com/s/KA1gwlup0uybaJR0PbGFyw

    鄭重聲明:本文內(nèi)容及圖片均整理自互聯(lián)網(wǎng),不代表本站立場,版權(quán)歸原作者所有,如有侵權(quán)請聯(lián)系管理員(admin#wlmqw.com)刪除。
    上一篇 2022年8月4日 15:16
    下一篇 2022年8月4日 15:16

    相關(guān)推薦

    聯(lián)系我們

    聯(lián)系郵箱:admin#wlmqw.com
    工作時(shí)間:周一至周五,10:30-18:30,節(jié)假日休息