前言

昨天,我开发的代码,又收获了一个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;
}
}

总结

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

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

Spring Boot Validation,既有注解不满足,我是怎么暴力扩展validation注解的的更多相关文章

  1. Spring Boot中的那些生命周期和其中的可扩展点(转)

    前言可扩展点的种类Spring Boot启动过程 1.SpringApplication的启动过程 2.ApplicationContext的启动过程 3.一般的非懒加载单例Bean在Spring B ...

  2. spring boot: 一般注入说明(四) Profile配置,Environment环境配置 @Profile注解

    1.通过设定Environment的ActiveProfile来设置当前context所需要的环境配置,在开发中使用@Profile注解类或方法,达到不同情况下选择实例化不同的Bean. 2.使用jv ...

  3. spring boot常用注解使用小结

    1.@RestController和@RequestMapping注解 4.0重要的一个新的改进是@RestController注解,它继承自@Controller注解. 4.0之前的版本,Sprin ...

  4. Spring Boot入门(四):开发Web Api接口常用注解总结

    本系列博客记录自己学习Spring Boot的历程,如帮助到你,不胜荣幸,如有错误,欢迎指正! 在程序员的日常工作中,Web开发应该是占比很重的一部分,至少我工作以来,开发的系统基本都是Web端访问的 ...

  5. spring boot 如何将没有注解的类@Autowired

    等于将类交给spring管理,也就是IOC. 注解@Autowired是自动装配,也就是spring帮你创建对象,当然前提是这个@Autowired的类已经配置成Bean了,spring配置bean文 ...

  6. spring mvc注解和spring boot注解

    1 spring mvc和spring boot之间的关系 spring boot包含spring mvc.所以,spring mvc的注解在spring boot总都是可以用的吗? spring b ...

  7. Spring Boot 项目学习 (二) MySql + MyBatis 注解 + 分页控件 配置

    0 引言 本文主要在Spring Boot 基础项目的基础上,添加 Mysql .MyBatis(注解方式)与 分页控件 的配置,用于协助完成数据库操作. 1 创建数据表 这个过程就暂时省略了. 2 ...

  8. Spring Boot中自定义注解+AOP实现主备库切换

    摘要: 本篇文章的场景是做调度中心和监控中心时的需求,后端使用TDDL实现分表分库,需求:实现关键业务的查询监控,当用Mybatis查询数据时需要从主库切换到备库或者直接连到备库上查询,从而减小主库的 ...

  9. spring mvc 和spring boot 中注解的使用

    1 spring mvc和spring boot之间的关系 spring boot包含spring mvc.所以,spring mvc的注解在spring boot总都是可以用的吗? spring b ...

随机推荐

  1. day6-作业(不完整)

    # 1.用代码实现:利用下划线将列表的'每一个元素'拼接成字符串 li=['ndfj','dlfj',12434]# 注意是将元素与元素转换为字符串之间用_拼接,而不是将每个字符串进行拼接 li=[' ...

  2. 在控制器中如何对frxml的控件初始化

    如果在控制器中实现Initializable这个接口,并重iInitializable这个方法 对于一个fxml文件来说它首先执行控制器的构造函数,这个时候它是无法对@FXML修饰的方法进行访问的,然 ...

  3. Jquery EasyUI 中ValidateBox验证框使用讲解

    来源素文宅博客:http://blog.yoodb.com/ Validatebox(验证框)的设计目的是为了验证输入的表单字段是否有效.如果用户输入了无效的值,它将会更改输入框的背景颜色,并且显示警 ...

  4. kubernetes的ingress-nginx

    这是一篇学习记录.记录kubernetes集群中如何将jenkins服务通过域名接入外部.由于是测试环境,域名是自定义的,解析写在/etc/hosts和自己本地的hosts中. 部署图: 一.部署后端 ...

  5. js调用浏览器“打印”与“打印预览”

    用到html <object>标签,具体做法如下: 1.在html文档任意位置添加<object>标签: <div style="border: 1px sol ...

  6. ef+Npoi导出百万行excel之踩坑记

            最近在做一个需求是导出较大的excel,本文是记录我在做需求过程中遇到的几个问题和解题方法,给大家分享一下,一来可以帮助同样遇到问题的朋友,二呢,各位大神也许有更好的方法可以指点小弟一 ...

  7. &#128293;《手把手教你》系列练习篇之1-python+ selenium自动化测试(详细教程)

    1.简介 相信各位小伙伴或者同学们通过前面已经介绍了的Python+Selenium基础篇,通过前面几篇文章的介绍和练习,Selenium+Python的webUI自动化测试算是 一只脚已经迈入这个门 ...

  8. 2019-10-16:渗透测试,基础学习,burpsuit学习,爆破的四种方式学习

    Burp Suite 是用于攻击web 应用程序的集成平台,包含了许多工具.Burp Suite为这些工具设计了许多接口,以加快攻击应用程序的过程.所有工具都共享一个请求,并能处理对应的HTTP 消息 ...

  9. webpackd学习的意义

    高速发展的前端技术,与浏览器支持的不相匹配.导致前端必须把前端比较先进的技术进行一层编码从而使得浏览器可以加载. 比如前端框架Vue,Angular,React.Less,Sass.TypeScrip ...

  10. Scala函数式编程(四)函数式的数据结构 上

    这次来说说函数式的数据结构是什么样子的,本章会先用一个list来举例子说明,最后给出一个Tree数据结构的练习,放在公众号里面,练习里面给出了基本的结构,但代码是空缺的需要补上,此外还有预留的test ...