Spring Boot Validation, both notes are not met, how I extended validation annotations violence

Foreword

Yesterday, I developed code, and harvest a bug, say when a list of query interface, under normal circumstances, can keyword fuzzy query based on a keyword, the background will go to query the database% keyword% (non-Internet project, there is no use es, only this); however, when input% character can be fuzzy matching all of the records, as if, as if this condition is not the same filter.

The reason is simple, when input%, the final out of the sql, this is %%%.

We used mybatis plus, worded as follows, it seems that this is a problem (bug warning):

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

mapperLayer code is as follows (hereinafter only demo, single-table is certainly not write sql directly, ha ha):

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

When the condition is entered %, we look at sql console print:

Found the problem, take a look at how to change it.

Project source code (recommended to walk through the code, look at this article, will be easier):
https://gitee.com/ckl111/all-simple-demo-in-work/tree/master/spring-boot-validation-demo

Modification method

Without further ado, Syria, I think the approach is to determine the parameters of the request, under normal circumstances, there will not be such a request parameter in the% character. The problem is, we have a list of queries in many places have this problem, too lazy to write one by one if/else, as lazy, it must think of a way, and that is the use of java eestandardized Lane validation.

Use spring validationthe demo, you can take a look at the code cloud blogger:

https://gitee.com/ckl111/all-simple-demo-in-work/tree/master/spring-boot-validation-demo

Simple to use as follows:

So, my solution to this problem is to customize a comment, added to the support fuzzy queries field, in the process handler of the notes, it is determined whether or contain special characters%, if included, directly to the client throwing error code.

Set the direction, he went ahead, first I do not have time to search for answers, because the feeling is not hard, as if he can get the look, ha ha.

Then get started.

Straighten out the original logic, identify the extended mode

Because, I know this kind of validation annotations, mainly in the validation-api bag, maven coordinates:

        <dependency>
            <groupId>javax.validation</groupId>
            <artifactId>validation-api</artifactId>
        </dependency>

And then, this package is java ee specification only defines, do not realize, realize, then, hibernate this has been achieved, spring-boot-starter-web in default also cited this dependency.

So, we can understand, validation-apidefines the basic notes, and then hibernate-validatorcarried out to achieve, and extends the annotation part, I can find the two, such as

org.hibernate.validator.constraints.Length, the check whether the string length specified range

org.hibernate.validator.constraints.Email, calibrate the specified string is a valid email address

My local maven projects are managed, and downloaded the source code, so look for direct org.hibernate.validator.constraints.Emaillocal references, that discovered the following codes org.hibernate.validator.internal.metadata.core.ConstraintHelper:

So, as long as we think of ways in which our own inside with a record on the line, the easiest way is to put the code to cover it up, but I still have a bottom line, it can be extended to expand, it is to die, then covered.

img

Analysis of what this place is the org.hibernate.validator.internal.metadata.core.ConstraintHelperconstructor, the first new one hashmap, these notes and annotations processor after put into it, and then the following code is assigned to a class 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 );
}

So, my idea is, and so after the class constructor is called, modify this map. Well, come and see how to manipulate the constructor of the class in what is called? Through the search, found at 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();
}

Continue to track, we found in

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

总结

其实,扩展不需要这么麻烦,官方提供了扩展点,我也是写完后,查了下才发现的。

不过,本文只是给一个思路,和一些我用到的方法吧,希望能抛砖引玉。

Guess you like

Origin www.cnblogs.com/grey-wolf/p/12037311.html