昨天,我开发的代码,又收获了一个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; } }
其实,扩展不须要这么麻烦,官方提供了扩展点,我也是写完后,查了下才发现的。
不过,本文只是给一个思路,和一些我用到的方法吧,但愿能抛砖引玉。