討論范圍 >>> 自定義注解及注解國際化的使用缠诅,源碼實現(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來定義
通過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替換成對象辐马,也是可以通過{}這種方式,是獲取目標數(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)用校驗方法
- 先在 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();
}
}