<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>

          Spring Validation最佳實(shí)踐及其實(shí)現(xiàn)原理,參數(shù)校驗(yàn)沒那么簡(jiǎn)單!

          共 26103字,需瀏覽 53分鐘

           ·

          2021-08-10 20:35

          不點(diǎn)藍(lán)字,我們哪來(lái)故事?

          每天 11 點(diǎn)更新文章,餓了點(diǎn)外賣,點(diǎn)擊 ??《無(wú)門檻外賣優(yōu)惠券,每天免費(fèi)領(lǐng)!》

          作者 | 伍陸七

          來(lái)源 | https://juejin.cn/post/6856541106626363399

          之前也寫過(guò)一篇關(guān)于Spring Validation使用的文章,不過(guò)自我感覺還是浮于表面,本次打算徹底搞懂Spring Validation。本文會(huì)詳細(xì)介紹Spring Validation各種場(chǎng)景下的最佳實(shí)踐及其實(shí)現(xiàn)原理,死磕到底!項(xiàng)目源碼:spring-validation(https://github.com/chentianming11/spring-validation)

          簡(jiǎn)單使用

          Java API規(guī)范(JSR303)定義了Bean校驗(yàn)的標(biāo)準(zhǔn)validation-api,但沒有提供實(shí)現(xiàn)。hibernate validation是對(duì)這個(gè)規(guī)范的實(shí)現(xiàn),并增加了校驗(yàn)注解如@Email@Length等。Spring Validation是對(duì)hibernate validation的二次封裝,用于支持spring mvc參數(shù)自動(dòng)校驗(yàn)。接下來(lái),我們以spring-boot項(xiàng)目為例,介紹Spring Validation的使用。

          引入依賴

          如果spring-boot版本小于2.3.xspring-boot-starter-web會(huì)自動(dòng)傳入hibernate-validator依賴。如果spring-boot版本大于2.3.x,則需要手動(dòng)引入依賴:

          <dependency>
              <groupId>org.hibernate</groupId>
              <artifactId>hibernate-validator</artifactId>
              <version>6.0.1.Final</version>
          </dependency>

          對(duì)于web服務(wù)來(lái)說(shuō),為防止非法參數(shù)對(duì)業(yè)務(wù)造成影響,在Controller層一定要做參數(shù)校驗(yàn)的!大部分情況下,請(qǐng)求參數(shù)分為如下兩種形式:

          1. POSTPUT請(qǐng)求,使用requestBody傳遞參數(shù);
          2. GET請(qǐng)求,使用requestParam/PathVariable傳遞參數(shù)。

          下面我們簡(jiǎn)單介紹下requestBodyrequestParam/PathVariable的參數(shù)校驗(yàn)實(shí)戰(zhàn)!

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

          POSTPUT請(qǐng)求一般會(huì)使用requestBody傳遞參數(shù),這種情況下,后端使用DTO對(duì)象進(jìn)行接收。只要給DTO對(duì)象加上@Validated注解就能實(shí)現(xiàn)自動(dòng)參數(shù)校驗(yàn)。比如,有一個(gè)保存User的接口,要求userName長(zhǎng)度是2-10accountpassword字段長(zhǎng)度是6-20。如果校驗(yàn)失敗,會(huì)拋出MethodArgumentNotValidException異常,Spring默認(rèn)會(huì)將其轉(zhuǎn)為400(Bad Request)請(qǐng)求。

          DTO表示數(shù)據(jù)傳輸對(duì)象(Data Transfer Object),用于服務(wù)器和客戶端之間交互傳輸使用的。在spring-web項(xiàng)目中可以表示用于接收請(qǐng)求參數(shù)的Bean對(duì)象。

          • DTO字段上聲明約束注解
          @Data
          public class UserDTO {

              private Long userId;

              @NotNull
              @Length(min = 2, max = 10)
              private String userName;

              @NotNull
              @Length(min = 6, max = 20)
              private String account;

              @NotNull
              @Length(min = 6, max = 20)
              private String password;
          }

          • 在方法參數(shù)上聲明校驗(yàn)注解
          @PostMapping("/save")
          public Result saveUser(@RequestBody @Validated UserDTO userDTO) {
              // 校驗(yàn)通過(guò),才會(huì)執(zhí)行業(yè)務(wù)邏輯處理
              return Result.ok();
          }

          這種情況下,使用@Valid@Validated都可以

          requestParam/PathVariable參數(shù)校驗(yàn)

          GET請(qǐng)求一般會(huì)使用requestParam/PathVariable傳參。如果參數(shù)比較多(比如超過(guò)6個(gè)),還是推薦使用DTO對(duì)象接收。否則,推薦將一個(gè)個(gè)參數(shù)平鋪到方法入?yún)⒅小T谶@種情況下,必須在Controller類上標(biāo)注@Validated注解,并在入?yún)⑸下暶骷s束注解(如@Min等)。如果校驗(yàn)失敗,會(huì)拋出ConstraintViolationException異常。代碼示例如下:

          @RequestMapping("/api/user")
          @RestController
          @Validated
          public class UserController {
              // 路徑變量
              @GetMapping("{userId}")
              public Result detail(@PathVariable("userId") @Min(10000000000000000L) Long userId) {
                  // 校驗(yàn)通過(guò),才會(huì)執(zhí)行業(yè)務(wù)邏輯處理
                  UserDTO userDTO = new UserDTO();
                  userDTO.setUserId(userId);
                  userDTO.setAccount("11111111111111111");
                  userDTO.setUserName("xixi");
                  userDTO.setAccount("11111111111111111");
                  return Result.ok(userDTO);
              }

              // 查詢參數(shù)
              @GetMapping("getByAccount")
              public Result getByAccount(@Length(min = 6, max = 20) @NotNull String  account) {
                  // 校驗(yàn)通過(guò),才會(huì)執(zhí)行業(yè)務(wù)邏輯處理
                  UserDTO userDTO = new UserDTO();
                  userDTO.setUserId(10000000000000003L);
                  userDTO.setAccount(account);
                  userDTO.setUserName("xixi");
                  userDTO.setAccount("11111111111111111");
                  return Result.ok(userDTO);
              }
          }

          統(tǒng)一異常處理

          前面說(shuō)過(guò),如果校驗(yàn)失敗,會(huì)拋出MethodArgumentNotValidException或者ConstraintViolationException異常。在實(shí)際項(xiàng)目開發(fā)中,通常會(huì)用統(tǒng)一異常處理來(lái)返回一個(gè)更友好的提示。比如我們系統(tǒng)要求無(wú)論發(fā)送什么異常,http的狀態(tài)碼必須返回200,由業(yè)務(wù)碼去區(qū)分系統(tǒng)的異常情況。

          @RestControllerAdvice
          public class CommonExceptionHandler {

              @ExceptionHandler({MethodArgumentNotValidException.class})
              @ResponseStatus(HttpStatus.OK)
              @ResponseBody
              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();
                 return Result.fail(BusinessCode.參數(shù)校驗(yàn)失敗, msg);
              }

              @ExceptionHandler({ConstraintViolationException.class})
              @ResponseStatus(HttpStatus.OK)
              @ResponseBody
              public Result handleConstraintViolationException(ConstraintViolationException ex
          {
                  return Result.fail(BusinessCode.參數(shù)校驗(yàn)失敗, ex.getMessage());
              }
          }

          進(jìn)階使用

          分組校驗(yàn)

          在實(shí)際項(xiàng)目中,可能多個(gè)方法需要使用同一個(gè)DTO類來(lái)接收參數(shù),而不同方法的校驗(yàn)規(guī)則很可能是不一樣的。這個(gè)時(shí)候,簡(jiǎn)單地在DTO類的字段上加約束注解無(wú)法解決這個(gè)問題。因此,spring-validation支持了分組校驗(yàn)的功能,專門用來(lái)解決這類問題。還是上面的例子,比如保存User的時(shí)候,UserId是可空的,但是更新User的時(shí)候,UserId的值必須>=10000000000000000L;其它字段的校驗(yàn)規(guī)則在兩種情況下一樣。這個(gè)時(shí)候使用分組校驗(yàn)的代碼示例如下:

          • 約束注解上聲明適用的分組信息groups
          @Data
          public class UserDTO {

              @Min(value = 10000000000000000L, groups = Update.class)
              private Long userId
          ;

              @NotNull(groups = {Save.classUpdate.class})
              @Length(min 
          2, max = 10, groups = {Save.classUpdate.class})
              private String userName
          ;

              @NotNull(groups = {Save.classUpdate.class})
              @Length(min 
          6, max = 20, groups = {Save.classUpdate.class})
              private String account
          ;

              @NotNull(groups = {Save.classUpdate.class})
              @Length(min 
          6, max = 20, groups = {Save.classUpdate.class})
              private String password
          ;

              /**
               * 保存的時(shí)候校驗(yàn)分組
               */

              public interface Save {
              }

              /**
               * 更新的時(shí)候校驗(yàn)分組
               */

              public interface Update {
              }
          }

          • @Validated注解上指定校驗(yàn)分組
          @PostMapping("/save")
          public Result saveUser(@RequestBody @Validated(UserDTO.Save.class) UserDTO userDTO) {
              // 校驗(yàn)通過(guò),才會(huì)執(zhí)行業(yè)務(wù)邏輯處理
              return Result.ok();
          }

          @PostMapping("/update")
          public Result updateUser(@RequestBody @Validated(UserDTO.Update.class) UserDTO userDTO) {
              // 校驗(yàn)通過(guò),才會(huì)執(zhí)行業(yè)務(wù)邏輯處理
              return Result.ok();
          }

          嵌套校驗(yàn)

          前面的示例中,DTO類里面的字段都是基本數(shù)據(jù)類型String類型。但是實(shí)際場(chǎng)景中,有可能某個(gè)字段也是一個(gè)對(duì)象,這種情況先,可以使用嵌套校驗(yàn)。比如,上面保存User信息的時(shí)候同時(shí)還帶有Job信息。需要注意的是,此時(shí)DTO類的對(duì)應(yīng)字段必須標(biāo)記@Valid注解

          @Data
          public class UserDTO {

              @Min(value = 10000000000000000L, groups = Update.class)
              private Long userId
          ;

              @NotNull(groups = {Save.classUpdate.class})
              @Length(min 
          2, max = 10, groups = {Save.classUpdate.class})
              private String userName
          ;

              @NotNull(groups = {Save.classUpdate.class})
              @Length(min 
          6, max = 20, groups = {Save.classUpdate.class})
              private String account
          ;

              @NotNull(groups = {Save.classUpdate.class})
              @Length(min 
          6, max = 20, groups = {Save.classUpdate.class})
              private String password
          ;

              @NotNull(groups = {Save.classUpdate.class})
              @Valid
              private Job job
          ;

              @Data
              public static class Job {

                  @Min(value = 1, groups = Update.class)
                  private Long jobId
          ;

                  @NotNull(groups = {Save.classUpdate.class})
                  @Length(min 
          2, max = 10, groups = {Save.classUpdate.class})
                  private String jobName
          ;

                  @NotNull(groups = {Save.classUpdate.class})
                  @Length(min 
          2, max = 10, groups = {Save.classUpdate.class})
                  private String position
          ;
              }

              /**
               * 保存的時(shí)候校驗(yàn)分組
               */

              public interface Save {
              }

              /**
               * 更新的時(shí)候校驗(yàn)分組
               */

              public interface Update {
              }
          }

          嵌套校驗(yàn)可以結(jié)合分組校驗(yàn)一起使用。還有就是嵌套集合校驗(yàn)會(huì)對(duì)集合里面的每一項(xiàng)都進(jìn)行校驗(yàn),例如List<Job>字段會(huì)對(duì)這個(gè)list里面的每一個(gè)Job對(duì)象都進(jìn)行校驗(yàn)。

          集合校驗(yàn)

          如果請(qǐng)求體直接傳遞了json數(shù)組給后臺(tái),并希望對(duì)數(shù)組中的每一項(xiàng)都進(jìn)行參數(shù)校驗(yàn)。此時(shí),如果我們直接使用java.util.Collection下的list或者set來(lái)接收數(shù)據(jù),參數(shù)校驗(yàn)并不會(huì)生效!我們可以使用自定義list集合來(lái)接收參數(shù):

          • 包裝List類型,并聲明@Valid注解
          public class ValidationList<Eimplements List<E{

              @Delegate // @Delegate是lombok注解
              @Valid // 一定要加@Valid注解
              public List<E> list = new ArrayList<>();

              // 一定要記得重寫toString方法
              @Override
              public String toString() {
                  return list.toString();
              }
          }

          @Delegate注解受lombok版本限制,1.18.6以上版本可支持。如果校驗(yàn)不通過(guò),會(huì)拋出NotReadablePropertyException,同樣可以使用統(tǒng)一異常進(jìn)行處理。

          比如,我們需要一次性保存多個(gè)User對(duì)象,Controller層的方法可以這么寫:

          @PostMapping("/saveList")
          public Result saveList(@RequestBody @Validated(UserDTO.Save.class) ValidationList<UserDTO> userList) {
              // 校驗(yàn)通過(guò),才會(huì)執(zhí)行業(yè)務(wù)邏輯處理
              return Result.ok();
          }

          自定義校驗(yàn)

          業(yè)務(wù)需求總是比框架提供的這些簡(jiǎn)單校驗(yàn)要復(fù)雜的多,我們可以自定義校驗(yàn)來(lái)滿足我們的需求。自定義spring validation非常簡(jiǎn)單,假設(shè)我們自定義加密id(由數(shù)字或者a-f的字母組成,32-256長(zhǎng)度)校驗(yàn),主要分為兩步:

          • 自定義約束注解
          @Target({METHOD, FIELD, ANNOTATION_TYPE, CONSTRUCTOR, PARAMETER})
          @Retention(RUNTIME)
          @Documented
          @Constraint(validatedBy = {EncryptIdValidator.class})
          public @interface EncryptId 
          {

              // 默認(rèn)錯(cuò)誤消息
              String message() default "加密id格式錯(cuò)誤";

              // 分組
              Class<?>[] groups() default {};

              // 負(fù)載
              Class<? extends Payload>[] payload() default {};
          }

          • 實(shí)現(xiàn)ConstraintValidator接口編寫約束校驗(yàn)器
          public class EncryptIdValidator implements ConstraintValidator<EncryptIdString{

              private static final Pattern PATTERN = Pattern.compile("^[a-f\\d]{32,256}$");

              @Override
              public boolean isValid(String value, ConstraintValidatorContext context) {
                  // 不為null才進(jìn)行校驗(yàn)
                  if (value != null) {
                      Matcher matcher = PATTERN.matcher(value);
                      return matcher.find();
                  }
                  return true;
              }
          }

          這樣我們就可以使用@EncryptId進(jìn)行參數(shù)校驗(yàn)了!

          編程式校驗(yàn)

          上面的示例都是基于注解來(lái)實(shí)現(xiàn)自動(dòng)校驗(yàn)的,在某些情況下,我們可能希望以編程方式調(diào)用驗(yàn)證。這個(gè)時(shí)候可以注入javax.validation.Validator對(duì)象,然后再調(diào)用其api

          @Autowired
          private javax.validation.Validator globalValidator;

          // 編程式校驗(yàn)
          @PostMapping("/saveWithCodingValidate")
          public Result saveWithCodingValidate(@RequestBody UserDTO userDTO) {
              Set<ConstraintViolation<UserDTO>> validate = globalValidator.validate(userDTO, UserDTO.Save.class);
              // 如果校驗(yàn)通過(guò),validate為空;否則,validate包含未校驗(yàn)通過(guò)項(xiàng)
              if (validate.isEmpty()) {
                  // 校驗(yàn)通過(guò),才會(huì)執(zhí)行業(yè)務(wù)邏輯處理

              } else {
                  for (ConstraintViolation<UserDTO> userDTOConstraintViolation : validate) {
                      // 校驗(yàn)失敗,做其它邏輯
                      System.out.println(userDTOConstraintViolation);
                  }
              }
              return Result.ok();
          }

          快速失敗(Fail Fast)

          Spring Validation默認(rèn)會(huì)校驗(yàn)完所有字段,然后才拋出異常。可以通過(guò)一些簡(jiǎn)單的配置,開啟Fali Fast模式,一旦校驗(yàn)失敗就立即返回。

          @Bean
          public Validator validator() {
              ValidatorFactory validatorFactory = Validation.byProvider(HibernateValidator.class)
                      .configure()
                      // 快速失敗模式
                      .failFast(true)
                      .buildValidatorFactory()
          ;
              return validatorFactory.getValidator();
          }

          @Valid@Validated區(qū)別

          區(qū)別@Valid@Validated
          提供者JSR-303規(guī)范Spring
          是否支持分組不支持支持
          標(biāo)注位置METHOD, FIELD, CONSTRUCTOR, PARAMETER, TYPE_USETYPE, METHOD, PARAMETER
          嵌套校驗(yàn)支持不支持

          實(shí)現(xiàn)原理

          requestBody參數(shù)校驗(yàn)實(shí)現(xiàn)原理

          spring-mvc中,RequestResponseBodyMethodProcessor是用于解析@RequestBody標(biāo)注的參數(shù)以及處理@ResponseBody標(biāo)注方法的返回值的。顯然,執(zhí)行參數(shù)校驗(yàn)的邏輯肯定就在解析參數(shù)的方法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();
                  //將請(qǐng)求數(shù)據(jù)封裝到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);
                          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);
              }
          }

          可以看到,resolveArgument()調(diào)用了validateIfApplicable()進(jìn)行參數(shù)校驗(yàn)。

          protected void validateIfApplicable(WebDataBinder binder, MethodParameter parameter) {
              // 獲取參數(shù)注解,比如@RequestBody、@Valid、@Validated
              Annotation[] annotations = parameter.getParameterAnnotations();
              for (Annotation ann : annotations) {
                  // 先嘗試獲取@Validated注解
                  Validated validatedAnn = AnnotationUtils.getAnnotation(ann, Validated.class);
                  //如果直接標(biāo)注了@Validated,那么直接開啟校驗(yàn)。
                  //如果沒有,那么判斷參數(shù)前是否有Valid起頭的注解。
                  if (validatedAnn != null || ann.annotationType().getSimpleName().startsWith("Valid")) {
                      Object hints = (validatedAnn != null ? validatedAnn.value() : AnnotationUtils.getValue(ann));
                      Object[] validationHints = (hints instanceof Object[] ? (Object[]) hints : new Object[] {hints});
                      //執(zhí)行校驗(yàn)
                      binder.validate(validationHints);
                      break;
                  }
              }
          }

          看到這里,大家應(yīng)該能明白為什么這種場(chǎng)景下@Validated@Valid兩個(gè)注解可以混用。我們接下來(lái)繼續(xù)看WebDataBinder.validate()實(shí)現(xiàn)。

          @Override
          public void validate(Object target, Errors errors, Object... validationHints) {
              if (this.targetValidator != null) {
                  processConstraintViolations(
                      //此處調(diào)用Hibernate Validator執(zhí)行真正的校驗(yàn)
                      this.targetValidator.validate(target, asValidationGroups(validationHints)), errors);
              }
          }

          最終發(fā)現(xiàn)底層最終還是調(diào)用了Hibernate Validator進(jìn)行真正的校驗(yàn)處理。

          方法級(jí)別的參數(shù)校驗(yàn)實(shí)現(xiàn)原理

          上面提到的將參數(shù)一個(gè)個(gè)平鋪到方法參數(shù)中,然后在每個(gè)參數(shù)前面聲明約束注解的校驗(yàn)方式,就是方法級(jí)別的參數(shù)校驗(yàn)。實(shí)際上,這種方式可用于任何Spring Bean的方法上,比如Controller/Service等。其底層實(shí)現(xiàn)原理就是AOP,具體來(lái)說(shuō)是通過(guò)MethodValidationPostProcessor動(dòng)態(tài)注冊(cè)AOP切面,然后使用MethodValidationInterceptor對(duì)切點(diǎn)方法織入增強(qiáng)

          public class MethodValidationPostProcessor extends AbstractBeanFactoryAwareAdvisingPostProcessorimplements InitializingBean {
              @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());
              }
          }

          接著看一下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)
                      result = execVal.validateParameters(
                          invocation.getThis(), methodToValidate, invocation.getArguments(), groups);
                  }
                  catch (IllegalArgumentException ex) {
                      ...
                  }
                  //有異常直接拋出
                  if (!result.isEmpty()) {
                      throw new ConstraintViolationException(result);
                  }
                  //真正的方法調(diào)用
                  Object returnValue = invocation.proceed();
                  //對(duì)返回值做校驗(yàn),最終還是委托給Hibernate Validator來(lái)校驗(yàn)
                  result = execVal.validateReturnValue(invocation.getThis(), methodToValidate, returnValue, groups);
                  //有異常直接拋出
                  if (!result.isEmpty()) {
                      throw new ConstraintViolationException(result);
                  }
                  return returnValue;
              }
          }

          實(shí)際上,不管是requestBody參數(shù)校驗(yàn)還是方法級(jí)別的校驗(yàn),最終都是調(diào)用Hibernate Validator執(zhí)行校驗(yàn),Spring Validation只是做了一層封裝

          往期推薦

          我對(duì)軟件分層設(shè)計(jì)的思考

          聚合支付如何設(shè)計(jì)

          Lombok!代碼簡(jiǎn)潔神器還是代碼“亞健康”元兇?

          Spring為什么建議使用構(gòu)造器來(lái)注入?

          下方二維碼關(guān)注我

          技術(shù)草根堅(jiān)持分享 編程,算法,架構(gòu)

          看完文章,餓了點(diǎn)外賣,點(diǎn)擊 ??《無(wú)門檻外賣優(yōu)惠券,每天免費(fèi)領(lǐng)!》

          朋友,助攻一把!點(diǎn)個(gè)在看
          瀏覽 61
          點(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>
                  无码一区二区黑人猛烈视频网站 | 欧美成人在线支援 | 免费观看一级一片 | 老妇人一区二区三区 | 琪琪色影音先锋 |