1. 程式人生 > >Spring Boot Validation,既有註解不滿足,我是怎麼暴力擴充套件validation註解的

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

總結

其實,擴充套件不需要這麼麻煩,官方提供了擴充套件點,我也是寫完後,查了下才發現的。

不過,本文只是給一個思路,和一些我用到的方法吧,希望能拋磚引玉