昨天,我開發的代碼,又收穫了一個bug,說是界面上列表查詢時,正常狀況下,能夠根據某個關鍵字keyword模糊查詢,後臺會去數據庫 %keyword%查詢(非互聯網項目,沒有使用es,只能這樣了);可是,當輸入%字符時,能夠模糊匹配出全部的記錄,就好像,好像這個條件沒進行過濾同樣。java
緣由很簡單,當輸入%時,最終出來的sql,就是%%%這樣的。git
咱們用的mybatis plus
,寫法以下,看來這樣是有問題的(bug警告):web
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了,哈哈):spring
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:sql
問題找到了,看看怎麼改吧。數據庫
項目源碼在(建議先看代碼,再看本文,會容易一些):
https://gitee.com/ckl111/all-simple-demo-in-work/tree/master/spring-boot-validation-demoapi
閒言少敘,我想的辦法是,判斷請求參數,正常狀況下,請求參數裏都不會有這種%字符。問題是,咱們有不少地方的列表查詢有這個問題,懶得一個一個寫if/else
,做爲懶人,確定要想一想辦法了,那就是使用java ee
規範裏的validation
。mybatis
使用spring validation
的demo,能夠看看博主的碼雲:app
https://gitee.com/ckl111/all-simple-demo-in-work/tree/master/spring-boot-validation-demomaven
簡單的使用方法以下:
因此,我解決這個問題的辦法就是,自定義一個註解,加在支持模糊查詢的字段上,在該註解的處理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,好比像下面這樣:
這裏看看咱們是怎麼自定義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; } }
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; } }
其實,擴展不須要這麼麻煩,官方提供了擴展點,我也是寫完後,查了下才發現的。
不過,本文只是給一個思路,和一些我用到的方法吧,但願能拋磚引玉。