SpringBoot引數非空校驗的非最優實現歷程

逍遙遊007發表於2019-06-16

  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 HandlerMethodArgumentResolver}s.
 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 HandlerMethodArgumentResolver}s.
 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 HandlerMethodArgumentResolver}s.
 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 HandlerMethodArgumentResolver}s 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丟擲的異常),使前端看起來都是一個實現。

  寫完感覺自己的思路都是凌亂的,還不如人家一開始的實現,啊哈哈哈···求大神帶路指教,各位看官有什麼意見看法,請指教!

 

相關文章