SpringBoot参数非空校验的非最优实现历程

Posted peripateticism

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了SpringBoot参数非空校验的非最优实现历程相关的知识,希望对你有一定的参考价值。

  SpringBoot参数非空校验在网上已经有很多资料了,自己最近要实现这一个功能,大概看了下觉得没什么难度,不想在过程中还是遇到了一些问题,在此记录,希望有遇到和我一样问题的人和过路大神不吝指教。

  需求是做一个全局请求参数非空校验和异常拦截,spring提供的@Validated和Hibernate提供的@Valid目前不支持请求参数为基本类型的非空判断,只能是请求参数封装为对象时,判断对象属性非空,所以要自己实现一个对基本类型的非空判断。

  首先说下网上原创转载最多的一个思路:实现一个指向方法的注解,注解中创建一个String[]属性,用来存放方法中需要非空判断的参数的名称 -----> 创建AOP,切点为注解的方法,增强方法中拿到注解中的String[],然后遍历判断是否为空,如果为空则抛出一个自定义异常 ----->  实现一个全局异常处理类,捕获抛出的自定义异常,进行后续处理。

  首先说下根据这个思路的实现非常简单,也很实用,只是有两个吹毛求疵的问题。第一,注解需要写成@CheckParam(param1,param2)这样的形式加在方法上,还需要手动写param1,param2这样的要进行非空判断的参数的名称,而不是像@RequestParam注解直接加在参数上就OK了。第二,@RequestParam注解本身会判断非空,一起使用时,自己的注解无效。

  下面先说第一个问题,这个问题首先想到拦截器实现。

代码1:继承HandlerInterceptorAdapter ,实现拦截器。代码说明:(代码中的CheckParamNull是自定义注解,ResponseBo是自定义的json返回类)

 1 public class ParameterNotBlankInterceptor extends HandlerInterceptorAdapter 
 2     //在请求处理之前进行调用(Controller方法调用之前
 3     @Override
 4     public boolean preHandle(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, Object o) throws Exception 
 5 
 6         //如果不是映射到方法直接通过
 7         if (!(o instanceof HandlerMethod)) 
 8             return true;
 9         
10         HandlerMethod handlerMethod = (HandlerMethod) o;
11         Parameter[] methodParameters = handlerMethod.getMethod().getParameters();
12         for (int i = 0; i< methodParameters.length; i++)
13             if(methodParameters[i].getAnnotation(ParamNotBlank.class) != null)
14                 CheckParamNull  noblank =methodParameters[i].getAnnotation(CheckParamNull.class);
15                 Object obj = httpServletRequest.getParameter(methodParameters[i].getName());
16                 httpServletResponse.setCharacterEncoding("UTF-8");
17                 if (obj == null)
18                 httpServletResponse.getWriter().write(JSON.toJSONString(ResponseBo.error(noblank.message())));
19                     return false;
20                 else if(obj instanceof String && StringUtils.isBlank((String)obj))
21                 httpServletResponse.getWriter().write(JSON.toJSONString(ResponseBo.error(noblank.message())));
22                     return false;
23                 
24                 return true;
25             
26         
27         return true;
28     
29 
30     //请求处理之后进行调用,但是在视图被渲染之前(Controller方法调用之后)
31     @Override
32     public void postHandle(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, Object o, ModelAndView modelAndView) throws Exception 
33 
34     
35 
36     //在整个请求结束之后被调用,也就是在DispatcherServlet 渲染了对应的视图之后执行(主要是用于进行资源清理工作)
37     @Override
38     public void afterCompletion(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, Object o, Exception e) throws Exception 
39 
40     
41 

 

代码2:加入拦截器链

1 @Configuration
2 public class InterceptorConfig extends WebMvcConfigurerAdapter 
3     public void addInterceptors(InterceptorRegistry registry)
4         registry.addInterceptor(new ParameterNotBlankInterceptor())
5                 .addPathPatterns("/**");
6         super.addInterceptors(registry);
7     
8 

  使用这个方法的问题来自代码1的第15行,当我们同时使用@RequestParam的时候,如果在@RequestParam(value="重新定义的请求参数名称")的value属性重新定义了请求名称,那么代码1的第15行

Object obj = httpServletRequest.getParameter(methodParameters[i].getName());拿到的就一定是null,因为methodParameters[i].getName()拿到的名称是请求方法中参数列表中的参数名,这样即便request中有值,但是由于名称不同,也就无法取到值。虽然说@RequestParam本身就会判断非空,没有必要再用自定义注解,但是保不准别人会拿来一起用,如果能保证使用自定义注解时@RequestParam不会一起出现,那么这个方法也是可行的。或者还有一种方式就是在判断自定义注代码解这里同时判断是否存在@RequestParam注解,如果有就用@RequestParam注解中的value值来request中取值,但是springweb绑定注解那么多,也不能肯定别人就会用@RequestParam,如果要判断使用那得一大串代码,果断放弃。

  出现这个问题后,就在想有没有在springweb绑定注解之后工作的方法,当时想到看能不能新加入一个解析器实现org.springframework.web.method.support.HandlerMethodArgumentResolver接口,以求在springweb绑定注解的解析器工作之后执行,写完之后发现无法执行到自己的解析器。追源码看到如下内容:

  1 /*
  2  * Copyright 2002-2017 the original author or authors.
  3  *
  4  * Licensed under the Apache License, Version 2.0 (the "License");
  5  * you may not use this file except in compliance with the License.
  6  * You may obtain a copy of the License at
  7  *
  8  *      http://www.apache.org/licenses/LICENSE-2.0
  9  *
 10  * Unless required by applicable law or agreed to in writing, software
 11  * distributed under the License is distributed on an "AS IS" BASIS,
 12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 13  * See the License for the specific language governing permissions and
 14  * limitations under the License.
 15  */
 16 
 17 package org.springframework.web.method.support;
 18 
 19 import java.util.Collections;
 20 import java.util.LinkedList;
 21 import java.util.List;
 22 import java.util.Map;
 23 import java.util.concurrent.ConcurrentHashMap;
 24 
 25 import org.apache.commons.logging.Log;
 26 import org.apache.commons.logging.LogFactory;
 27 
 28 import org.springframework.core.MethodParameter;
 29 import org.springframework.lang.Nullable;
 30 import org.springframework.web.bind.support.WebDataBinderFactory;
 31 import org.springframework.web.context.request.NativeWebRequest;
 32 
 33 /**
 34  * Resolves method parameters by delegating to a list of registered @link HandlerMethodArgumentResolvers.
 35  * Previously resolved method parameters are cached for faster lookups.
 36  *
 37  * @author Rossen Stoyanchev
 38  * @author Juergen Hoeller
 39  * @since 3.1
 40  */
 41 public class HandlerMethodArgumentResolverComposite implements HandlerMethodArgumentResolver 
 42 
 43     protected final Log logger = LogFactory.getLog(getClass());
 44 
 45     private final List<HandlerMethodArgumentResolver> argumentResolvers = new LinkedList<>();
 46 
 47     private final Map<MethodParameter, HandlerMethodArgumentResolver> argumentResolverCache =
 48             new ConcurrentHashMap<>(256);
 49 
 50 
 51     /**
 52      * Add the given @link HandlerMethodArgumentResolver.
 53      */
 54     public HandlerMethodArgumentResolverComposite addResolver(HandlerMethodArgumentResolver resolver) 
 55         this.argumentResolvers.add(resolver);
 56         return this;
 57     
 58 
 59     /**
 60      * Add the given @link HandlerMethodArgumentResolvers.
 61      * @since 4.3
 62      */
 63     public HandlerMethodArgumentResolverComposite addResolvers(@Nullable HandlerMethodArgumentResolver... resolvers) 
 64         if (resolvers != null) 
 65             for (HandlerMethodArgumentResolver resolver : resolvers) 
 66                 this.argumentResolvers.add(resolver);
 67             
 68         
 69         return this;
 70     
 71 
 72     /**
 73      * Add the given @link HandlerMethodArgumentResolvers.
 74      */
 75     public HandlerMethodArgumentResolverComposite addResolvers(
 76             @Nullable List<? extends HandlerMethodArgumentResolver> resolvers) 
 77 
 78         if (resolvers != null) 
 79             for (HandlerMethodArgumentResolver resolver : resolvers) 
 80                 this.argumentResolvers.add(resolver);
 81             
 82         
 83         return this;
 84     
 85 
 86     /**
 87      * Return a read-only list with the contained resolvers, or an empty list.
 88      */
 89     public List<HandlerMethodArgumentResolver> getResolvers() 
 90         return Collections.unmodifiableList(this.argumentResolvers);
 91     
 92 
 93     /**
 94      * Clear the list of configured resolvers.
 95      * @since 4.3
 96      */
 97     public void clear() 
 98         this.argumentResolvers.clear();
 99     
100 
101 
102     /**
103      * Whether the given @linkplain MethodParameter method parameter is supported by any registered
104      * @link HandlerMethodArgumentResolver.
105      */
106     @Override
107     public boolean supportsParameter(MethodParameter parameter) 
108         return (getArgumentResolver(parameter) != null);
109     
110 
111     /**
112      * Iterate over registered @link HandlerMethodArgumentResolvers and invoke the one that supports it.
113      * @throws IllegalStateException if no suitable @link HandlerMethodArgumentResolver is found.
114      */
115     @Override
116     @Nullable
117     public Object resolveArgument(MethodParameter parameter, @Nullable ModelAndViewContainer mavContainer,
118             NativeWebRequest webRequest, @Nullable WebDataBinderFactory binderFactory) throws Exception 
119 
120         HandlerMethodArgumentResolver resolver = getArgumentResolver(parameter);
121         if (resolver == null) 
122             throw new IllegalArgumentException("Unknown parameter type [" + parameter.getParameterType().getName() + "]");
123         
124         return resolver.resolveArgument(parameter, mavContainer, webRequest, binderFactory);
125     
126 
127     /**
128      * Find a registered @link HandlerMethodArgumentResolver that supports the given method parameter.
129      */
130     @Nullable
131     private HandlerMethodArgumentResolver getArgumentResolver(MethodParameter parameter) 
132         HandlerMethodArgumentResolver result = this.argumentResolverCache.get(parameter);
133         if (result == null) 
134             for (HandlerMethodArgumentResolver methodArgumentResolver : this.argumentResolvers) 
135                 if (logger.isTraceEnabled()) 
136                     logger.trace("Testing if argument resolver [" + methodArgumentResolver + "] supports [" +
137                             parameter.getGenericParameterType() + "]");
138                 
139                 if (methodArgumentResolver.supportsParameter(parameter)) 
140                     result = methodArgumentResolver;
141                     this.argumentResolverCache.put(parameter, result);
142                     break;
143                 
144             
145         
146         return result;
147     
148 
149 

  代码131行开始的该类最后一方法。134行遍历所有的Resolvers解析器。139行,当拿到第一个支持解析parameter这个参数的methodArgumentResolver时,就会break出循环,不会再找其他的解析器,而@RequestParam的解析器RequestParamMethodArgumentResolver排名第一,我写了半天的解析器没啥关系,郁闷···。这样的话只能重写@RequestParam的解析器RequestParamMethodArgumentResolver,工作量巨大且麻烦,像我这种菜直接忽略该方式···

  来到了最后一个招数,既然不能对@RequestParam这种级别的注解做个啥工作,反正人家也有非空判断了,直接用就好了,但是不能使用它的异常直接返回,因为要统一嘛,拦截下异常就OK了,写到这里自己都快崩溃了,前面长篇大论的半天原来都是废话!【手动加个表情吧···】

  拦截异常的代码:

  说明:ServletRequestBindingException是RequestParamMethodArgumentResolver抛出的参数为空异常的父类

/**
 * GlobalExceptionHandler
 */
@RestControllerAdvice
@Order(value = Ordered.HIGHEST_PRECEDENCE)
public class GlobalExceptionHandler 

    @ExceptionHandler(value = ValidationException.class)
    @ResponseStatus(HttpStatus.BAD_REQUEST)
    public ResponseBo handleValidationException(ValidationException exception) 
        return ResponseBo.error(exception.getMessage(),400);
    


     @ExceptionHandler(value = ServletRequestBindingException.class)
     public ResponseBo handleServletRequestBindingException(ServletRequestBindingException exception) 
         return ResponseBo.error(exception.getMessage(),400);
     

  然后要做的就是没有springweb绑定注解时的非空判断,而且如果同时使用了springweb绑定注解例如@RequestParam和自定义注解,还不能影响@RequestParam的使用。由于拦截器会在请求到达controller之前动作,也就是在@RequestParam的解析器RequestParamMethodArgumentResolver之前执行,所以不能用,这样绕了一大圈又回到了AOP的怀抱···

  首先注解代码:

 1 @Target(ElementType.PARAMETER,ElementType.METHOD)
 2 @Retention(RetentionPolicy.RUNTIME)
 3 public @interface CheckParamNull 
 4 
 5     String message() default "参数为空";
 6     /**
 7      * 是否为null,默认不能为null
 8      */
 9     boolean notNull() default true;
10 
11     /**
12      * 是否非空,默认可以为空
13      */
14     boolean notBlank() default false;
15 

  Aspect代码:

 1 @Aspect
 2 @Component
 3 public class CheckParamNullAspect 
 4 
 5     /**
 6      * pointcut
 7      * 定义切点:被@Log注解的方法
 8      */
 9     @Pointcut("execution( * * (..,@com.scaffold.common.annotation.CheckParamNull (*),..))")
10     public void pointcut() 
11         // do nothing
12     
13 
14     /**
15      * around
16      * 环绕增强方法,可以控制切点前后的代码执行。
17      */
18     @Around("pointcut()")
19     public Object around(ProceedingJoinPoint point) throws Throwable 
20         MethodSignature signature = ((MethodSignature) point.getSignature());
21 
22         //得到拦截的方法
23         Method method = signature.getMethod();
24         //获取方法参数注解,返回二维数组是因为某些参数可能存在多个注解
25         Annotation[][] parameterAnnotations = method.getParameterAnnotations();
26         if (parameterAnnotations == null || parameterAnnotations.length == 0) 
27             return point.proceed();
28         
29 
30         //获取方法参数名
31         String[] paramNames = signature.getParameterNames();
32         //获取参数值
33         Object[] paranValues = point.getArgs();
34         //获取方法参数类型
35 //        Class<?>[] parameterTypes = method.getParameterTypes();
36         for (int i = 0; i < parameterAnnotations.length; i++) 
37             for (int j = 0; j < parameterAnnotations[i].length; j++) 
38                 //如果该参数前面的注解是CheckParamNull的实例,并且notNull()=true,则进行非空校验
39                 if (parameterAnnotations[i][j] != null && parameterAnnotations[i][j] instanceof CheckParamNull) 
40                     paramIsNull(paramNames[i], paranValues[i], ((CheckParamNull) parameterAnnotations[i][j]));
41                     break;
42                 
43             
44         
45         return point.proceed();
46     
47 
48     /**
49      * 参数非空校验,如果参数为空,则抛出ServletRequestBindingException异常
50      * @param paramName
51      * @param value
52      * @param checkParamNull
53      */
54     private void paramIsNull(String paramName, Object value, CheckParamNull checkParamNull) throws ServletRequestBindingException 
55         if (checkParamNull.notNull() && value == null) 
56             throw new ServletRequestBindingException("Required String parameter ‘"+paramName+"‘ is not present");
57         else if(checkParamNull.notBlank() && StringUtils.isBlank(value.toString().trim()))
58             throw new ServletRequestBindingException("Required String parameter ‘"+paramName+"‘ is must not blank");
59         
60 
61     
62 
63 

  附上一个讲Spring 之AOP AspectJ切入点语法详解的链接,感觉到头来写AOP切入点语法是实现吹毛求疵问题1的关键【手动哭一会···】

异常拦截代码同上。

  关于该问题的总结:

    执行顺序:拦截器 》》解析器》》AOP

    解决问题的思路:吹毛求疵问题1绕了一大圈最终由AOP切入点语法解决。吹毛求疵问题2本质没有解决,当使用@RequestParam注解时,非空判断是springweb的解析器判断并抛出异常的,无论加不加自定义注解都根本不会走到AOP这一步,只是换了个方式(使用拦截器拦截springweb抛出的异常),使前端看起来都是一个实现。

  写完感觉自己的思路都是凌乱的,还不如人家一开始的实现,啊哈哈哈···求大神带路指教,各位看官有什么意见看法,请指教!

 

以上是关于SpringBoot参数非空校验的非最优实现历程的主要内容,如果未能解决你的问题,请参考以下文章

springboot使用hibernate validator校验

springboot使用hibernate validator校验

springboot使用hibernate validator校验

springboot使用hibernate validator校验方式

Vue使用ElementUI对表单元素进行自定义校验

为啥具有可接受的非一致启发式的 A* 会找到非最优解?