java通過自定義注解進行參數(shù)校驗并使用國際化錯誤提示

討論范圍 >>> 自定義注解及注解國際化的使用缠诅,源碼實現(xiàn)后續(xù)探究
為什么用注解校驗,1.可以通過@Validated 和 @Valid 注解在controller層自動進行校驗 2. 也可以靈活的使用javax.validation.Validator 來手動校驗,并且這種校驗方式不同以往手動校驗拋出異常的方式,可以遇到第一個錯誤后不拋出異常,將所有錯誤信息收集到一起展辞。適用于復(fù)雜配置草稿化,也就是可以在配置錯誤的情況下先暫存万牺,在發(fā)布配置時統(tǒng)一校驗并將所有錯誤信息返回拾枣。

如何使用

1.注解提示信息國際化

首先注解的提示信息可以通過注解上的message屬性定義例如下面代碼

   @Length(min = 1, max = 45, message = "節(jié)點名稱長度1 ~ 45")
    private String name;

而message的國際化该互,可以通過配置resource中的 ValidationMessages.properties來定義


image.png

通過key value的映射婚瓜,配置國際化信息廊鸥,但是需要指定一個新的properties加載配置

    @Bean
    public Validator validator(ResourceBundleMessageSource messageSource) {
// RESOURCE_NAME 為字符串希太,為指定新的國際化配置侥祭,和原有的
        messageSource.getBasenameSet().add(RESOURCE_NAME);
        LocalValidatorFactoryBean factoryBean = new LocalValidatorFactoryBean();
        MessageInterpolatorFactory interpolatorFactory = new MessageInterpolatorFactory();
        factoryBean.setMessageInterpolator(interpolatorFactory.getObject());
        factoryBean.setValidationMessageSource(messageSource);
        return factoryBean;
    }

國際化的 映射關(guān)系為 key(配置類型) -> 語言 -> 具體的key value映射
而注解的國際化配置配置類型默認為 ValidationMessages平痰,具體原理后續(xù)源碼解析中討論
然后就可以在注解的message中使用配置code

國際化配置的演示
   /**
     * 注解中的 國際化配置必須是{} 包裹款违,因為其可以混合靜態(tài)值使用藕坯,并且可以使用被校驗?zāi)繕藢ο?     * 通過el表達式獲取值团南,或者直接獲取產(chǎn)生校驗的注解的靜態(tài)屬性,下面一一演示
     */
    public static final String CONDITION_ERROR = "{CONDITION_ERROR}";
// ----------國際化配置-------
CONDITION_ERROR=條件校驗失敗,請檢查!
CONDITION_ERROR=condition validate error,please check config!
使用靜態(tài)固定值配合 國際化配置
   @Length(min = 1, max = 45, message = "{NAME_LENGTH}1 ~ 45")
    private String name;
// 國際化配置
NAME_LENGTH=名稱的長度范圍是
NAME_LENGTH=yingwenhuozheqitayuyan
// 校驗不通過拿到的異常信息是
名稱的長度范圍是1 ~ 45
yingwenhuozheqitayuyan1 ~ 45
使用注解靜態(tài)屬性值 + 國際化配置

這里要注意的是低版本的hibernate-validator校驗實現(xiàn)jar包有bug炼彪,關(guān)于基本類型數(shù)組轉(zhuǎn)換的類型安全問題吐根,已經(jīng)在新版本解決了這個bug,具體可看上一篇文章 http://www.reibang.com/p/8d4ad5e2d735 當注解中使用基本類型數(shù)組作為屬性時如果通過下面{min}的方式會報錯噢

   @Length(min = 1, max = 45, message = "{NAME_LENGTH}{min} ~ {max}")
    private String name;
// 國際化配置
NAME_LENGTH=名稱的長度范圍是
NAME_LENGTH=yingwenhuozheqitayuyan
// 校驗不通過拿到的異常信息是
名稱的長度范圍是1 ~ 45
yingwenhuozheqitayuyan1 ~ 45
使用注解靜態(tài)屬性值 + 國際化配置 + 目標屬性el表達式
   @Length(min = 1, max = 45, message = "{NAME_LENGTH}{min} ~ {max} 傳入的值為 ${validatedValue}")
    private String name;
// 國際化配置
NAME_LENGTH=名稱的長度范圍是 
NAME_LENGTH=yingwenhuozheqitayuyan 
// 校驗不通過拿到的異常信息是
名稱的長度范圍是1 ~ 45 ${傳入的值}
yingwenhuozheqitayuyan1 ~ 45 ${傳入的值}

當然如果name替換成對象辐马,也是可以通過{validatedValue.name} 等方式獲取拷橘,為什么使用{}這種方式,是獲取目標數(shù)據(jù)就通過${},而validatedValue 則是固定的值冗疮,只被校驗的目標對象萄唇,后續(xù)源碼解析可以看到源碼中寫死的這個值。

最佳實現(xiàn)
// 只使用code  映射到國際化配置中术幔,國際化配置中可以使用 {} 和 ${validatedValue}
   @Length(min = 1, max = 45, message = "{NAME_LENGTH}")
    private String name;
// 國際化配置
NAME_LENGTH=名稱的長度最長范圍為{min} ~ {max} 傳入的值為 ${validatedValue}
NAME_LENGTH=yingwen 為{min} ~ {max} yingwen  ${validatedValue}
// 校驗不通過拿到的異常信息是
名稱的長度范圍是1 ~ 45 ${傳入的值}
yingwen 為1 ~ 45 yingwen  ${傳入的值}

自定義注解實現(xiàn)校驗

實現(xiàn) ConstraintValidator<A, T> 接口

public interface ConstraintValidator<A extends Annotation, T> {

// 初始化當前校驗實例時回調(diào)的方法
    default void initialize(A constraintAnnotation) {
    }
// 校驗方法另萤,返回false則會拋出異常,如果使用手動校驗的方式诅挑,會收集每個返回false的message信息和被校驗的目標對象
    boolean isValid(T value, ConstraintValidatorContext context);
}

定義注解

@Target({ElementType.FIELD})
@Retention(RetentionPolicy.RUNTIME)
// 指定自定義校驗實現(xiàn)類
@Constraint(validatedBy = TestValidateImpl.class)
public @interface TestValidate {

    String message() default "";

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

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

}

自定義實現(xiàn)類 需要指定注解和被校驗的 對象類型,如果不是自定義對象可以直接指定,被指定的對象類型如果為了通用性可以使用接口或者抽象類

public class TestValidateImpl implements ConstraintValidator<TestValidate, TestValidateModel> {
    @Override
    public boolean isValid(TestValidateModel value, ConstraintValidatorContext context) {
        return false;
    }
}

被校驗的對象

@Data
public class TestModel {

    @TestValidate(message = "{TEST} 嗷嗚 ${validatedValue.name}")
    private TestValidateModel bo;
    @Data
    public static class TestValidateModel{
        private String name;
    }
}

然后在實現(xiàn)類里寫邏輯即可四敞,通過返回true 和false 實現(xiàn)校驗是否通過

還有一種情況在一個實現(xiàn)類中對復(fù)雜對象進行多項校驗,或者多個屬性聯(lián)動校驗

@Data
public class TestModel {

    @TestValidate(message = "{TEST} 嗷嗚 ${validatedValue.name}")

    private TestValidateModel bo;
    @Data
    public static class TestValidateModel{
        private String name;
        private LocalDateTime startTime;
        private LocalDateTime endTime;
    }
}
接下來有兩種方式
1. 如果是給 controller層用@Validated 注解進行接口層的校驗可以直接拋出異常
2. 手動調(diào)用校驗方法
  1. 先在 controller層異常攔截做好國際化邏輯
  /**
     * 要在自定義部分處理好國際化
     * @param constraintDeclarationException 在controller validation階段拋出的異常 自定義校驗注解使用
     * @param locale locale
     * @return RestErrorResponse
     */
    @ExceptionHandler(value = CustomConstraintDeclarationException.class)
    @ResponseBody
    public final RestErrorResponse handConstraintDeclarationException(CustomConstraintDeclarationException constraintDeclarationException, Locale locale) {
        String message = constraintDeclarationException.getMessage();
        // 應(yīng)該在內(nèi)部拼接時已經(jīng) 處理好國際化
        log.error(message, constraintDeclarationException);
        return new RestErrorResponse(HttpStatus.INTERNAL_SERVER_ERROR.value(), message);
    }

校驗邏輯 代碼

public class TestValidateImpl implements ConstraintValidator<TestValidate, TestValidateModel> {

    @Autowired
    MessageSource messageSource;

    @Override
    public boolean isValid(TestValidateModel value, ConstraintValidatorContext context) {
        if (value == null){
            setMessage(new BaseException(這里寫對應(yīng)code));
            return false;
        }
        if (value.getName().length() > 15){
            // 這樣操作是直接中斷校驗揍障,拋出異常目养,CustomConstraintDeclarationException是在進行注解校驗過程中唯一可以不catch拋出的
            // 為了可以直接中斷拋出,結(jié)合 @Validated 注解在controller層使用
            String message = messageSource.getMessage(配置好的國際化code, null, LocaleContextHolder.getLocale());
            throw new CustomConstraintDeclarationException(message, 隨便定義一個異常類型);
            return false;
        }
        if (value.getEndTime().compareTo(value.getStartTime()) < 0){
            // 如果不直接拋出異常中斷毒嫡,則配置 注解上的 message 拿到信息癌蚁。但是這樣錯誤信息的獲取復(fù)雜度太高,好處是可以兼容@Validated注解
            //和手動調(diào)用校驗方法兜畸,又可以controller層校驗努释,又可以手動校驗一次拿到所有校驗不通過的屬性
            return false;
        }
        return true;
    }

    // 這里是使用 自身框架 throw 自定義 exception邏輯,通過 MessageSource直接獲取國際化信息即可,這里的邏輯會直接中斷咬摇,不會一直收集后續(xù)的校驗信息
    @SneakyThrows
    protected void setMessage(BaseException baseException) {
        // MessageSourceUtils 方法為我們目前自己定義的邏輯伐蒂,可忽略,主要是 MessageSource#getMessage()方法
        String message = messageSource.getMessage(MessageSourceUtils.getMessageCode(baseException.getMessageCode()),
            baseException.getArgs(), LocaleContextHolder.getLocale());
        throw new CustomConstraintDeclarationException(message, baseException);
    }
}
使用方式和集中情況如上。但是我們這里有多個校驗項肛鹏,又想每個校驗項讓客戶端(無論是@Validated注解還是手動調(diào)用校驗方法逸邦,都可以拿到所有校驗信息和對應(yīng)的錯誤提示)

先看手動如何調(diào)用校驗方法

    @Autowired
    protected Validator globalValidator;
    @Override
    public <T> void verify(T target, Function<T, String> targetCategoryMapper, Function<T, Long> idMapper) {
// 手動調(diào)用校驗,可以獲取所有 返回 false不同過的校驗項在扰,然后取注解上的message作為提示信息
        Set<ConstraintViolation<T>> constraintViolations = globalValidator.validate(target);
        if (CollectionUtils.isNotEmpty(constraintViolations)) {
            constraintViolations.forEach(constraint -> {
                String nodeName = targetCategoryMapper != null ? targetCategoryMapper.apply(target) : null;
                Long id = idMapper != null ? idMapper.apply(target) : null;
                setFailInfo(nodeName, constraint.getMessage(), id);
            });
        }
    }

####### 如何使用

public class TestValidateImpl implements ConstraintValidator<TestValidate, TestValidateModel> {
    @Autowired
    MessageSource messageSource;
    @Autowired

    VerfiyServiceverfiyService;

    @Override
    public boolean isValid(TestValidateModel value, ConstraintValidatorContext context) {
        boolean result = true;
        if (value == null) {
            verfiyService.set錯誤信息(value, "{NOT_NULL}");
            // 為了所有校驗項都要交驗到缕减,可以不立即返回false
            result = false;
        }
        if (value.getName().length() > 15) {
context.buildConstraintViolationWithTemplate("{NAME_LENGTH}").addConstraintViolation();
            result = false;
        }
        if (value.getEndTime().compareTo(value.getStartTime()) < 0) {
context.buildConstraintViolationWithTemplate("{TIME_START_END}").addConstraintViolation();
            result = false;
        }
        return true;
    }
}
那么問題來了,這里的方法不會走spring 和 hibernate-validator內(nèi)置方法來做國際化轉(zhuǎn)換芒珠,我們需要自己實現(xiàn)這部分邏輯桥狡。下面是仿照hibernate-validator源碼實現(xiàn),拿到
/**
 * @description: 自定義的注解的 message國際化
 * @author: yhr
 * @modified By: yhr
 * @date: Created in 2021/12/3 10:11
 * @version:v1.0
 * 1.通過自定義注解或者原有注解 在注解的message上用 {}  包裹國際化code
 * 2. 在自定義注解 使用手動填入信息時{@link ProcessVerifyService#verify(Object, Function, Function)}
 * 在自定義注解例如 {@link TriggerConfigValidateImpl} 通過 {@link ProcessSetFailByTargetConsumer#setFailInfo(Object, String)}
 * 來手動放入錯誤信息皱卓。手動放入的信息也可以 用 {} 包裹國際化code
 * 3. 國際化code 需要在resource下的 ValidatedMessages 對應(yīng)的properties配置國際化信息裹芝,同時國際化的信息可以使用sp el表達式
 * 使用方式為  ${} 包裹 被添加注解的對象為 validatedValue 固定值例如
 * {err_code_1}
 * err_code_1=嗷嗷嗚~${validatedValue.name} 會獲取被注解對象的name字段,獲取不道則替換為null
 * -------------如果是直接獲取注解中的配置項娜汁,在properties中就不需要用${},使用{}即可嫂易,例如{@link org.hibernate.validator.constraints.Length}
 * 例如{@link Length#max()}直接在properties配置err_code_1=嗷嗷嗚~名字長度不能超過{max}輸入名稱為:${validatedValue.name}
 * 但是這樣只適用于不是數(shù)組,如果取注解中的數(shù)組會報錯 , 目前spring-boot-starter-validator 2.3.2依賴 hibernate-validator 6.1.5
 * 有bug存炮,后續(xù)hibernate-validator 6.2以上已經(jīng)把數(shù)組類型安全bug修復(fù)目前咱不可以使用獲取注解中的數(shù)組變量
 * 特別注意的是不支持方法和計算炬搭,當前的表達式實使用的是 {@link ValueExpression} 只是屬性取值表達式
 * 并不支持 {@link javax.el.MethodExpression} 方法表達式蜈漓,也就是不能支持像正常sp el表達式類似 obj != null ? a : b
 * T(org.apache.commons.collections4.isNotEmpty(list)) ? a : b 之類的靜態(tài)方法使用及當前實例方法使用及計算都不被支持
 * 4.
 * 本類實現(xiàn)仿照javax.validation的標準 {@link MessageInterpolator} 的 hibernate包的實現(xiàn)
 * @see AbstractMessageInterpolator
 * ---------------------快速查看案例-------------------
 * @see TestController#test1()
 * @see TestController#test2()
 */
public interface ValidationCustomResourceBundle {

    /**
     * 自定義注解校驗 實現(xiàn)國際化 及el表達式邏輯
     * @param messageTemplate 消息模板,注解中的message 或者 自定義手動放入的 字符串
     *                        目前統(tǒng)一放在{@link FlowValidateMessages}
     * @see ProcessVerifyService#verify(Object, Function, Function)
     * @see ProcessSetFailByTargetConsumer#setFailInfo(Object, String)
     * @param locale  國際化
     * @param target 目標對象
     * @return 處理好的返回信息
     */
    String parseMessageTemplate(String messageTemplate, Locale locale, Object target);

}

具體實現(xiàn)宫盔,代碼邏輯并不復(fù)雜融虽,先處理{} 靜態(tài)屬性獲取,然后處理${} 被校驗對象的獲取灼芭,然后
通過 MessageSourceResourceBundleLocator 處理國際化邏輯

@Configuration
@Slf4j
public class ValidationCustomResourceBundleHibernateImpl implements ValidationCustomResourceBundle {

    @Autowired
    MessageSource messageSource;

    private final ExpressionFactory expressionFactory = new ExpressionFactoryImpl();

    private static final String VALIDATED_VALUE_NAME = "validatedValue";

    private static final String LIFT = "{";
    private static final String RIGHT = "}";
    private static final String RESOURCE_NAME = "ValidationMessages";
    private static final String DOLLAR_SIGN = "$";
    private static final String SIGN = "\\";

    private static final Pattern LEFT_BRACE = Pattern.compile("\\{", Pattern.LITERAL);
    private static final Pattern RIGHT_BRACE = Pattern.compile("\\}", Pattern.LITERAL);
    private static final Pattern SLASH = Pattern.compile("\\\\", Pattern.LITERAL);
    private static final Pattern DOLLAR = Pattern.compile("\\$", Pattern.LITERAL);

    private static final int DEFAULT_INITIAL_CAPACITY = 100;
    private static final float DEFAULT_LOAD_FACTOR = 0.75f;
    private static final int DEFAULT_CONCURRENCY_LEVEL = 16;
    private final ConcurrentReferenceHashMap<LocalizedMessage, String> resolvedMessages;
    private final ConcurrentReferenceHashMap<String, List<Token>> tokenizedParameterMessages;
    private final ConcurrentReferenceHashMap<String, List<Token>> tokenizedELMessages;

    private MessageSourceResourceBundleLocator messageSourceResourceBundleLocator;

    public ValidationCustomResourceBundleHibernateImpl() {
        this.resolvedMessages = new ConcurrentReferenceHashMap<>(
            DEFAULT_INITIAL_CAPACITY,
            DEFAULT_LOAD_FACTOR,
            DEFAULT_CONCURRENCY_LEVEL,
            SOFT,
            SOFT,
            EnumSet.noneOf(ConcurrentReferenceHashMap.Option.class)
        );
        this.tokenizedParameterMessages = new ConcurrentReferenceHashMap<>(
            DEFAULT_INITIAL_CAPACITY,
            DEFAULT_LOAD_FACTOR,
            DEFAULT_CONCURRENCY_LEVEL,
            SOFT,
            SOFT,
            EnumSet.noneOf(ConcurrentReferenceHashMap.Option.class)
        );
        this.tokenizedELMessages = new ConcurrentReferenceHashMap<>(
            DEFAULT_INITIAL_CAPACITY,
            DEFAULT_LOAD_FACTOR,
            DEFAULT_CONCURRENCY_LEVEL,
            SOFT,
            SOFT,
            EnumSet.noneOf(ConcurrentReferenceHashMap.Option.class)
        );
    }

    @Bean
    public Validator validator(ResourceBundleMessageSource messageSource) {
        messageSource.getBasenameSet().add(RESOURCE_NAME);
        LocalValidatorFactoryBean factoryBean = new LocalValidatorFactoryBean();
        MessageInterpolatorFactory interpolatorFactory = new MessageInterpolatorFactory();
        factoryBean.setMessageInterpolator(interpolatorFactory.getObject());
        factoryBean.setValidationMessageSource(messageSource);
        return factoryBean;
    }


    @PostConstruct
    protected void init() {
        messageSourceResourceBundleLocator = new MessageSourceResourceBundleLocator(messageSource);
    }

    @Override
    public String parseMessageTemplate(String messageTemplate, Locale locale, Object target) {
        if (!messageTemplate.contains(LIFT)) {
            return replaceEscapedLiterals(messageTemplate);
        }
        ResourceBundle resourceBundle = messageSourceResourceBundleLocator.getResourceBundle(locale);

        String resolvedMessage = null;

        resolvedMessage = resolvedMessages.computeIfAbsent(new LocalizedMessage(messageTemplate, locale), lm ->
            interpolateBundleMessage(messageTemplate, resourceBundle, locale, target));

        if (resolvedMessage.contains(LIFT)) {
            // 參數(shù)解析  {} 部分 獲取注解中的參數(shù)及 message中配置的{}
            resolvedMessage = interpolateBundleMessage(new TokenIterator(getParameterTokens(resolvedMessage, tokenizedParameterMessages, InterpolationTermType.PARAMETER))
                , locale, target, resourceBundle);
            // el 通過屬性el表達式獲取被校驗對象的 屬性值
            resolvedMessage = interpolateBundleMessage(new TokenIterator(getParameterTokens(resolvedMessage, tokenizedELMessages, InterpolationTermType.EL))
                , target, locale);
        }
        // last but not least we have to take care of escaped literals
        resolvedMessage = replaceEscapedLiterals(resolvedMessage);
        return resolvedMessage;
    }


    private String interpolateBundleMessage(TokenIterator tokenIterator, Locale locale, Object target, ResourceBundle resourceBundle)
        throws MessageDescriptorFormatException {
        while (tokenIterator.hasMoreInterpolationTerms()) {
            String term = tokenIterator.nextInterpolationTerm();
            String resolvedParameterValue = resolveParameter(
                term, resourceBundle, locale, target
            );
            tokenIterator.replaceCurrentInterpolationTerm(resolvedParameterValue);
        }
        return tokenIterator.getInterpolatedMessage();
    }


    private String interpolateBundleMessage(TokenIterator tokenIterator, Object target, Locale locale) {
        while (tokenIterator.hasMoreInterpolationTerms()) {
            String term = tokenIterator.nextInterpolationTerm();
            SimpleELContext elContext = new SimpleELContext(expressionFactory);
            String resolvedExpression = null;
            try {
                ValueExpression valueExpression = bindContextValues(term, elContext, locale, target);
                resolvedExpression = (String) valueExpression.getValue(elContext);
            } catch (RuntimeException e) {
                log.warn("ValidationMessages >>> 表達式錯誤 value:{} ", term, e);
            }
            tokenIterator.replaceCurrentInterpolationTerm(resolvedExpression);
        }
        return tokenIterator.getInterpolatedMessage();
    }


    private List<Token> getParameterTokens(String resolvedMessage, ConcurrentReferenceHashMap<String, List<Token>> cache, InterpolationTermType termType) {
        return cache.computeIfAbsent(
            resolvedMessage,
            rm -> new TokenCollector(resolvedMessage, termType).getTokenList()
        );
    }




    private String resolveParameter(String parameterName, ResourceBundle bundle, Locale locale, Object target)
        throws MessageDescriptorFormatException {
        String parameterValue;
        try {
            if (bundle != null) {
                parameterValue = bundle.getString(removeCurlyBraces(parameterName));
                parameterValue = interpolateBundleMessage(parameterValue, bundle, locale, target);
            } else {
                parameterValue = parameterName;
            }
        } catch (MissingResourceException e) {
            // return parameter itself
            parameterValue = parameterName;
        }
        return parameterValue;
    }

    private ValueExpression bindContextValues(String messageTemplate, SimpleELContext elContext, Locale locale, Object targetValue) {
        // bind the validated value
        ValueExpression valueExpression = expressionFactory.createValueExpression(
            targetValue,
            Object.class
        );
        elContext.getVariableMapper().setVariable(VALIDATED_VALUE_NAME, valueExpression);

        // bind a formatter instantiated with proper locale
        valueExpression = expressionFactory.createValueExpression(
            new FormatterWrapper(locale),
            FormatterWrapper.class
        );
        elContext.getVariableMapper().setVariable(RootResolver.FORMATTER, valueExpression);
        return expressionFactory.createValueExpression(elContext, messageTemplate, String.class);
    }

    private String removeCurlyBraces(String parameter) {
        return parameter.substring(1, parameter.length() - 1);
    }

    private String replaceEscapedLiterals(String resolvedMessage) {
        if (resolvedMessage.contains(SIGN)) {
            resolvedMessage = LEFT_BRACE.matcher(resolvedMessage).replaceAll(LIFT);
            resolvedMessage = RIGHT_BRACE.matcher(resolvedMessage).replaceAll(RIGHT);
            resolvedMessage = SLASH.matcher(resolvedMessage).replaceAll(Matcher.quoteReplacement(SIGN));
            resolvedMessage = DOLLAR.matcher(resolvedMessage).replaceAll(Matcher.quoteReplacement(DOLLAR_SIGN));
        }
        return resolvedMessage;
    }

    private String interpolateBundleMessage(String message, ResourceBundle bundle, Locale locale, Object target)
        throws MessageDescriptorFormatException {
        TokenCollector tokenCollector = new TokenCollector(message, InterpolationTermType.PARAMETER);
        TokenIterator tokenIterator = new TokenIterator(tokenCollector.getTokenList());
        while (tokenIterator.hasMoreInterpolationTerms()) {
            String term = tokenIterator.nextInterpolationTerm();
            String resolvedParameterValue = resolveParameter(
                term, bundle, locale, target
            );
            tokenIterator.replaceCurrentInterpolationTerm(resolvedParameterValue);
        }
        return tokenIterator.getInterpolatedMessage();
    }

}

總結(jié)

上述所有邏輯包含了使用注解message國際化有额,自定義注解校驗,可中斷式的自定義注解校驗實現(xiàn)彼绷,自定義注解多項聯(lián)動校驗時的支持{} 靜態(tài)屬性獲取${} 被校驗對象獲取及國際化
最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末巍佑,一起剝皮案震驚了整個濱河市,隨后出現(xiàn)的幾起案子寄悯,更是在濱河造成了極大的恐慌萤衰,老刑警劉巖,帶你破解...
    沈念sama閱讀 211,042評論 6 490
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件猜旬,死亡現(xiàn)場離奇詭異脆栋,居然都是意外死亡,警方通過查閱死者的電腦和手機洒擦,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 89,996評論 2 384
  • 文/潘曉璐 我一進店門椿争,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人熟嫩,你說我怎么就攤上這事秦踪。” “怎么了掸茅?”我有些...
    開封第一講書人閱讀 156,674評論 0 345
  • 文/不壞的土叔 我叫張陵椅邓,是天一觀的道長。 經(jīng)常有香客問我昧狮,道長希坚,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 56,340評論 1 283
  • 正文 為了忘掉前任陵且,我火速辦了婚禮,結(jié)果婚禮上个束,老公的妹妹穿的比我還像新娘慕购。我一直安慰自己,他們只是感情好茬底,可當我...
    茶點故事閱讀 65,404評論 5 384
  • 文/花漫 我一把揭開白布沪悲。 她就那樣靜靜地躺著,像睡著了一般阱表。 火紅的嫁衣襯著肌膚如雪殿如。 梳的紋絲不亂的頭發(fā)上贡珊,一...
    開封第一講書人閱讀 49,749評論 1 289
  • 那天,我揣著相機與錄音涉馁,去河邊找鬼门岔。 笑死,一個胖子當著我的面吹牛烤送,可吹牛的內(nèi)容都是我干的寒随。 我是一名探鬼主播,決...
    沈念sama閱讀 38,902評論 3 405
  • 文/蒼蘭香墨 我猛地睜開眼帮坚,長吁一口氣:“原來是場噩夢啊……” “哼妻往!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起试和,我...
    開封第一講書人閱讀 37,662評論 0 266
  • 序言:老撾萬榮一對情侶失蹤讯泣,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后阅悍,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體好渠,經(jīng)...
    沈念sama閱讀 44,110評論 1 303
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 36,451評論 2 325
  • 正文 我和宋清朗相戀三年溉箕,在試婚紗的時候發(fā)現(xiàn)自己被綠了晦墙。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 38,577評論 1 340
  • 序言:一個原本活蹦亂跳的男人離奇死亡肴茄,死狀恐怖晌畅,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情寡痰,我是刑警寧澤抗楔,帶...
    沈念sama閱讀 34,258評論 4 328
  • 正文 年R本政府宣布,位于F島的核電站拦坠,受9級特大地震影響连躏,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜贞滨,卻給世界環(huán)境...
    茶點故事閱讀 39,848評論 3 312
  • 文/蒙蒙 一入热、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧晓铆,春花似錦勺良、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,726評論 0 21
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至链蕊,卻和暖如春事甜,著一層夾襖步出監(jiān)牢的瞬間谬泌,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 31,952評論 1 264
  • 我被黑心中介騙來泰國打工逻谦, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留掌实,地道東北人。 一個月前我還...
    沈念sama閱讀 46,271評論 2 360
  • 正文 我出身青樓跨跨,卻偏偏與公主長得像潮峦,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子勇婴,可洞房花燭夜當晚...
    茶點故事閱讀 43,452評論 2 348

推薦閱讀更多精彩內(nèi)容