Spring Boot應用使用Validation校驗入參,現有註解不滿足,我是怎麼暴力擴充套件validation註解的
前言
昨天,我開發的程式碼,又收穫了一個bug,說是介面上列表查詢時,正常情況下,可以根據某個關鍵字keyword模糊查詢,後臺會去資料庫 %keyword%查詢(非網際網路專案,沒有使用es,只能這樣了);但是,當輸入%字元時,可以模糊匹配出所有的記錄,就好像,好像這個條件沒進行過濾一樣。
原因很簡單,當輸入%時,最終出來的sql,就是%%%這樣的。
我們用的mybatis plus
,寫法如下,看來這樣是有問題的(bug警告):
QueryWrapper<QueryUserListReqVO> wrapper = new QueryWrapper<>(); if (StringUtils.isNotBlank(reqVO.getIncidentNumber())) { // 如果傳入的條件不為空,需要模糊查詢 wrapper.and(i -> i.like("i.incident_number", reqVO.getIncidentNumber())); } //根據wrapper去查詢 return this.baseMapper.getAppealedNormalIncidentList( wrapper);
mapper
層程式碼如下(以下僅為演示,單表肯定不直接寫sql了,哈哈):
public interface IncidentAppealInformationMapper extends BaseMapper<IncidentAppealInformation> { @Select("SELECT \n" + " * \n" " FROM\n" + " incident_appeal_information a ${ew.customSqlSegment}") List<GetAppealedNormalIncidentListRespVO> getAppealedNormalIncidentList(@Param(Constants.WRAPPER)QueryWrapper wrapper);
當輸入的條件為%
時,我們看看console列印的sql:
問題找到了,看看怎麼改吧。
專案原始碼在(建議先看程式碼,再看本文,會容易一些):
https://gitee.com/ckl111/all-simple-demo-in-work/tree/master/spring-boot-validation-demo
修改方法
閒言少敘,我想的辦法是,判斷請求引數,正常情況下,請求引數裡都不會有這種%字元。問題是,我們有很多地方的列表查詢有這個問題,懶得一個一個寫if/else
,作為懶人,肯定要想想辦法了,那就是使用java ee
規範裡的validation
。
使用spring validation
的demo,可以看看博主的碼雲:
https://gitee.com/ckl111/all-simple-demo-in-work/tree/master/spring-boot-validation-demo
簡單的使用方法如下:
所以,我解決這個問題的辦法就是,自定義一個註解,加在支援模糊查詢的欄位上,在該註解的處理handler中,判斷是否包含了特殊字元%,如果包含了,直接給客戶端拋錯誤碼。
定了方向,說幹就幹,我這裡沒有第一時間去搜索答案,因為感覺也不是很難,好像自己可以搞定的樣子,哈哈。
那就開始吧。
理順原有邏輯,找準擴充套件方式
因為,我知道這類validation註解,主要是在validation-api的包裡,maven座標:
<dependency>
<groupId>javax.validation</groupId>
<artifactId>validation-api</artifactId>
</dependency>
然後呢,這個包是java ee 規範的,只定義,不實現,實現的話,hibernate對這個進行了實現,spring-boot-starter-web裡預設也引了這個依賴。
所以,大家可以這麼理解,validation-api
定義了基本的註解,然後hibernate-validator
進行了實現,並且,擴充套件了一部分註解,我隨便找了兩個,比如
org.hibernate.validator.constraints.Length,校驗字串長度是否在指定的範圍內
org.hibernate.validator.constraints.Email,校驗指定字串為一個有效的email地址
我本地工程都是maven管理,且下載了原始碼的,所以直接查詢 org.hibernate.validator.constraints.Email
的引用的地方,即發現了下面這個程式碼org.hibernate.validator.internal.metadata.core.ConstraintHelper
:
所以,我們只要想辦法,在這裡面加上我們自己的一條記錄就行了,最簡單的辦法是,把程式碼給它覆蓋了,但是,我還是有底線的,能擴充套件就擴充套件,實在不行了,再覆蓋。
分析了一下,這個地方,是org.hibernate.validator.internal.metadata.core.ConstraintHelper
的建構函式裡,先是new了一個hashmap,把這些註解和註解處理器put進去後,再用下面的程式碼賦給了類中的field:
// 一個map,key:註解class,value:能夠處理該註解class的handler的描述符
@Immutable
private final Map<Class<? extends Annotation>, List<? extends ConstraintValidatorDescriptor<?>>> builtinConstraints;
public ConstraintHelper() {
Map<Class<? extends Annotation>, List<ConstraintValidatorDescriptor<?>>> tmpConstraints = new HashMap<>();
// Bean Validation constraints
putConstraint( tmpConstraints, Email.class, EmailValidator.class );
this.builtinConstraints = Collections.unmodifiableMap( tmpConstraints );
}
所以,我的思路是,等這個類的建構函式被呼叫後,修改下這個map。那,先得看看怎麼操縱這個類的建構函式在哪被呼叫的?經過查詢,發現是在org.hibernate.validator.internal.engine.ValidatorFactoryImpl#ValidatorFactoryImpl
:
public ValidatorFactoryImpl(ConfigurationState configurationState) {
ClassLoader externalClassLoader = getExternalClassLoader( configurationState );
this.valueExtractorManager = new ValueExtractorManager( configurationState.getValueExtractors() );
this.beanMetaDataManagers = new ConcurrentHashMap<>();
// 這裡new了一個上面類的例項
this.constraintHelper = new ConstraintHelper();
}
繼續追蹤,發現在
## org.hibernate.validator.HibernateValidator
public class HibernateValidator implements ValidationProvider<HibernateValidatorConfiguration> {
...
@Override
public ValidatorFactory buildValidatorFactory(ConfigurationState configurationState) {
// 這裡new了該類的例項
return new ValidatorFactoryImpl( configurationState );
}
}
到這裡,我們可以在上面這裡,打個斷點,看看什麼場景下,會走到這裡來了:
走到上圖的最後一步時,會進入到單獨的執行緒來做以上動作:
org.springframework.boot.autoconfigure.BackgroundPreinitializer.ValidationInitializer
/**
* Early initializer for javax.validation.
*/
private static class ValidationInitializer implements Runnable {
@Override
public void run() {
Configuration<?> configuration = Validation.byDefaultProvider().configure();
configuration.buildValidatorFactory().getValidator();
}
}
我們接著看,看什麼情況會走到我們之前的
## org.hibernate.validator.HibernateValidator
public class HibernateValidator implements ValidationProvider<HibernateValidatorConfiguration> {
...
@Override
public ValidatorFactory buildValidatorFactory(ConfigurationState configurationState) {
// 這裡new了該類的例項
return new ValidatorFactoryImpl( configurationState );
}
}
經過跟蹤,發現在以下地方進入的:
@Override
public final ValidatorFactory buildValidatorFactory() {
loadValueExtractorsFromServiceLoader();
parseValidationXml();
for ( ValueExtractorDescriptor valueExtractorDescriptor : valueExtractorDescriptors.values() ) {
validationBootstrapParameters.addValueExtractorDescriptor( valueExtractorDescriptor );
}
ValidatorFactory factory = null;
if ( isSpecificProvider() ) {
factory = validationBootstrapParameters.getProvider().buildValidatorFactory( this );
}
else {
//如果沒有指定validator,則會進入該分支,一般預設都進入該分支了
final Class<? extends ValidationProvider<?>> providerClass = validationBootstrapParameters.getProviderClass();
if ( providerClass != null ) {
for ( ValidationProvider<?> provider : providerResolver.getValidationProviders() ) {
if ( providerClass.isAssignableFrom( provider.getClass() ) ) {
factory = provider.buildValidatorFactory( this );
break;
}
}
if ( factory == null ) {
throw LOG.getUnableToFindProviderException( providerClass );
}
}
else {
//進入這裡,是因為,引數裡沒指定provider class,provider class可以在classpath下的META- INF/validation.xml中指定
// 這裡,providerResolver會去根據自己的規則,獲取validationProvider class集合
List<ValidationProvider<?>> providers = providerResolver.getValidationProviders(); // 取第一個集合中的provider,這裡的providers.get(0)一般就會取到前面我們說的 // HibernateValidator
factory = providers.get( 0 ).buildValidatorFactory( this );
}
}
return factory;
}
這段邏輯,還是有點繞的,先說說,頻繁出現的provider是啥意思?
我先來,其實,這就是個工廠。
然後,讓api來話事,這個類,javax.validation.spi.ValidationProvider
出現在validation-api
包裡。我們說了,這個包,只管定介面,不管實現。
public interface ValidationProvider<T extends Configuration<T>> {
...
/**
* 構造一個ValidatorFactory並返回
*
* Build a {@link ValidatorFactory} using the current provider implementation.
* <p>
* The {@code ValidatorFactory} is assembled and follows the configuration passed
* via {@link ConfigurationState}.
* <p>
* The returned {@code ValidatorFactory} is properly initialized and ready for use.
*
* @param configurationState the configuration descriptor
* @return the instantiated {@code ValidatorFactory}
* @throws ValidationException if the {@code ValidatorFactory} cannot be built
*/
ValidatorFactory buildValidatorFactory(ConfigurationState configurationState);
}
既然說了,這個介面,只管介面,不管實現;那麼實現在哪指定呢?
這個是利用了SPI機制,javax.validation.spi.ValidationProvider的實現在下面這個地方指定:
然後,我再畫個圖來說,前面查詢provider的簡易流程:
所以,大家如果對SPI機制有了解的話,那麼我們可以在classpath下,自定義一個ValidationProvider,比如像下面這樣:
通過SPI機制擴充套件ValidationProvider
這裡看看我們是怎麼自定義com.example.webdemo.config.CustomHibernateValidator
的:
package com.example.webdemo.config;
import lombok.extern.slf4j.Slf4j;
import org.hibernate.validator.HibernateValidator;
import org.hibernate.validator.internal.engine.ValidatorFactoryImpl;
import javax.validation.ValidatorFactory;
import javax.validation.spi.ConfigurationState;
import java.lang.reflect.Field;
@Slf4j
public class CustomHibernateValidator extends HibernateValidator{
@Override
public ValidatorFactory buildValidatorFactory(ConfigurationState configurationState) {
ValidatorFactoryImpl validatorFactory = new ValidatorFactoryImpl(configurationState);
// 修改validatorFactory中原有的ConstraintHelper
CustomConstraintHelper customConstraintHelper = new CustomConstraintHelper();
try {
Field field = validatorFactory.getClass().getDeclaredField("constraintHelper");
field.setAccessible(true);
field.set(validatorFactory,customConstraintHelper);
} catch (IllegalAccessException | NoSuchFieldException e) {
log.error("{}",e);
}
// 我們自定義的CustomConstraintHelper,繼承了原有的
// org.hibernate.validator.internal.metadata.core.ConstraintHelper,這裡對
// 原有類中的註解--》註解處理器map進行修改,放進我們自定義的註解和註解處理器
customConstraintHelper.moidfy();
return validatorFactory;
}
}
自定義的CustomConstraintHelper
package com.example.webdemo.config;
import com.example.webdemo.annotation.SpecialCharNotAllowed;
import com.example.webdemo.annotation.SpecialCharValidator;
import lombok.extern.slf4j.Slf4j;
import org.hibernate.validator.internal.engine.constraintvalidation.ConstraintValidatorDescriptor;
import org.hibernate.validator.internal.metadata.core.ConstraintHelper;
import javax.validation.ConstraintValidator;
import java.lang.annotation.Annotation;
import java.lang.reflect.Field;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
@Slf4j
public class CustomConstraintHelper extends ConstraintHelper {
public CustomConstraintHelper() {
super();
}
void moidfy(){
Field field = null;
try {
field = this.getClass().getSuperclass().getDeclaredField("builtinConstraints");
field.setAccessible(true);
Object o = field.get(this);
// 因為field被定義為了private final,且實際型別為
// this.builtinConstraints = Collections.unmodifiableMap( tmpConstraints );
// 因為不能修改,所以我這裡只能拷貝到一個新的hashmap,再反射設定回去
Map<Class<? extends Annotation>, List<? extends ConstraintValidatorDescriptor<?>>> modifiedMap = new HashMap<>();
modifiedMap.putAll((Map<? extends Class<? extends Annotation>, ? extends List<? extends ConstraintValidatorDescriptor<?>>>) o);
// 在這裡註冊我們自定義的註解和註解處理器
modifiedMap.put( SpecialCharNotAllowed.class,
Collections.singletonList( ConstraintValidatorDescriptor.forClass( SpecialCharValidator.class, SpecialCharNotAllowed.class ) ) );
/**
* 設定回field
*/
field.set(this,modifiedMap);
} catch (NoSuchFieldException | IllegalAccessException e) {
log.error("{}",e);
}
}
private static <A extends Annotation> void putConstraint(Map<Class<? extends Annotation>, List<ConstraintValidatorDescriptor<?>>> validators,
Class<A> constraintType, Class<? extends ConstraintValidator<A, ?>> validatorType) {
validators.put( constraintType, Collections.singletonList( ConstraintValidatorDescriptor.forClass( validatorType, constraintType ) ) );
}
}
自定義的註解和處理器
package com.example.webdemo.annotation;
import javax.validation.Payload;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
/**
* 註解,主要驗證是否有特殊字元
*/
@Target({ElementType.FIELD})
@Retention(RetentionPolicy.RUNTIME)
public @interface SpecialCharNotAllowed {
// String message() default "{javax.validation.constraints.Min.message}";
String message() default "special char like '%' is illegal";
Class<?>[] groups() default { };
Class<? extends Payload>[] payload() default { };
}
package com.example.webdemo.annotation;
import javax.validation.ConstraintValidator;
import javax.validation.ConstraintValidatorContext;
public class SpecialCharValidator implements ConstraintValidator<SpecialCharNotAllowed, Object> {
@Override
public boolean isValid(Object object, ConstraintValidatorContext constraintValidatorContext) {
if (object == null) {
return true;
}
if (object instanceof String) {
String str = (String) object;
if (str.contains("%")) {
return false;
}
}
return true;
}
}
總結
其實,擴充套件不需要這麼麻煩,官方提供了擴充套件點,我也是寫完後,查了下才發現的。
不過,本文只是給一個思路,和一些我用到的方法吧,希望能拋磚引玉