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

所以,我們只要想辦法,在這里面加上我們自己的一條記錄就行了,最簡單的辦法是,把代碼給它覆蓋了,但是,我還是有底線的,能擴展就擴展,實在不行了,再覆蓋。

img

分析了一下,這個地方,是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;
    }
}

總結

其實,擴展不需要這么麻煩,官方提供了擴展點,我也是寫完后,查了下才發現的。

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


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM