SpringBoot參數非空校驗在網上已經有不少資料了,本身最近要實現這一個功能,大概看了下以爲沒什麼難度,不想在過程當中仍是遇到了一些問題,在此記錄,但願有遇到和我同樣問題的人和過路大神不吝指教。前端
需求是作一個全局請求參數非空校驗和異常攔截,spring提供的@Validated和Hibernate提供的@Valid目前不支持請求參數爲基本類型的非空判斷,只能是請求參數封裝爲對象時,判斷對象屬性非空,因此要本身實現一個對基本類型的非空判斷。java
首先說下網上原創轉載最多的一個思路:實現一個指向方法的註解,註解中建立一個String[]屬性,用來存放方法中須要非空判斷的參數的名稱 -----> 建立AOP,切點爲註解的方法,加強方法中拿到註解中的String[],而後遍歷判斷是否爲空,若是爲空則拋出一個自定義異常 -----> 實現一個全局異常處理類,捕獲拋出的自定義異常,進行後續處理。web
首先說下根據這個思路的實現很是簡單,也很實用,只是有兩個吹毛求疵的問題。第一,註解須要寫成@CheckParam({param1,param2})這樣的形式加在方法上,還須要手動寫param1,param2這樣的要進行非空判斷的參數的名稱,而不是像@RequestParam註解直接加在參數上就OK了。第二,@RequestParam註解自己會判斷非空,一塊兒使用時,本身的註解無效。spring
下面先說第一個問題,這個問題首先想到攔截器實現。express
代碼1:繼承HandlerInterceptorAdapter ,實現攔截器。代碼說明:(代碼中的CheckParamNull是自定義註解,ResponseBo是自定義的json返回類)apache
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:加入攔截器鏈json
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,若是要判斷使用那得一大串代碼,果斷放棄。app
出現這個問題後,就在想有沒有在springweb綁定註解以後工做的方法,當時想到看能不能新加入一個解析器實現org.springframework.web.method.support.HandlerMethodArgumentResolver接口,以求在springweb綁定註解的解析器工做以後執行,寫完以後發現沒法執行到本身的解析器。追源碼看到以下內容:less
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拋出的異常),使前端看起來都是一個實現。
寫完感受本身的思路都是凌亂的,還不如人家一開始的實現,啊哈哈哈···求大神帶路指教,各位看官有什麼意見見解,請指教!