使用SpringCloud構建項目時,使用Swagger生成相應的接口文檔是推薦的選項,Swagger可以提供頁面訪問,直接在網頁上調試後端系統的接口, 很是方便。最近卻遇到了一個有點困惑的問題,演示接口示例以下(原有功能接口帶有業務實現邏輯,這裏簡化了接口):html
/** * @description: 演示類 * @author: Huang Ying **/ @Api(tags = "演示類") @RestController @Slf4j public class DemoController { @ApiOperation(value = "測試接口") @ApiImplicitParams({ @ApiImplicitParam(name = "uid", value = "用戶ID", paramType = "query", dataType = "Long") }) @RequestMapping(value = "/api/json/demo", method = RequestMethod.GET) public String auth(@RequestParam(value = "uid") Long uid) { System.out.println(uid); return "the uid: " + uid; } }
問題出在接口參數uid的必填性上,@RequestParam
註解裏require默認爲true,要求必填,但@ApiImplicitParam
註解裏require默認爲false,要求非必填,該業務接口在進行功能聯調時,uid竟然能獲得一個null值,按照通常認知習慣@ApiImplicitParam
註解的主要做用是生成接口文檔,不該該對@RequestParam
的屬性有侵入性纔對,目前反饋的bug,讓我懷疑@ApiImplicitParam
是否是會侵入@RequestParam
的require屬性?前端
SpringBoot版本:2.1.6.RELEASE
SpringCloud版本:Greenwich.SR3java
SpringCloud業務模塊使用的swagger:git
swagger bootstrap ui 1.9.6 加強swagger ui樣式
spring4all-swagger 1.9.0.RELEASE 配置化swagger參數,免去代碼開發github
SpringCloud業務網關使用的swagger:web
knife4j 2.0.1 加強swagger ui樣式(網關用gateway搭建,swagger使用knife4j-spring-boot-starter依賴,能夠聚合業務模塊的swagger文檔)spring
這次的範圍只針對SpringCloud業務模塊,暫時不涉及業務網關的Swagger文檔。apache
測試工具目前有兩個:
swagger doc:使用瀏覽器進行訪問,以下圖:json
postman:手動配置接口參數,示例:bootstrap
接口示例如開篇所示,咱們先使用以下接口,所有使用默認值,即@ApiImplicitParam的required爲false,@RequestParam的required爲true:
@ApiOperation(value = "測試接口") @ApiImplicitParams({ @ApiImplicitParam(name = "uid", value = "用戶ID", paramType = "query", dataType = "Long") }) @RequestMapping(value = "/api/json/demo", method = RequestMethod.GET) public String auth(@RequestParam(value = "uid") Long uid) { System.out.println(uid); return "the uid: " + uid; }
看swagger的結果:
看postman的結果:
咱們修改@ApiImplicitParam的required值爲true,@RequestParam不變,重啓模塊@ApiImplicitParam(name = "uid", value = "用戶ID", paramType = "query", required = true, dataType = "Long")
看swagger的結果:
經過調試瀏覽器能夠發現,爲空校驗是js完成的,js判斷爲空後,並未發起請求到後端,這樣咱們能夠認爲swagger內@ApiImplicitParam的required參數生效了。
在前面咱們使用postman測試接口時,發現參數項是空的,咱們加上參數,但不寫值測試後,結果讓人詫異:
而且不管@ApiImplicitParam的required值如何修改,結果都是同樣的,確定有一個地方是搞錯了,致使咱們誤判。
後來仔細查閱資料,發現是咱們對@RequestParam的required參數理解錯了,這個required爲true的含義是:接口參數名必定要存在,但參數後面有沒有值它管不着。拿剛剛的例子來講:
這兩個請求是經過的: localhost:8080/api/json//demo?uid localhost:8080/api/json//demo?uid= 只有這種請求是不經過的: localhost:8080/api/json//demo?
通過上述三個接口的測試場景,咱們至少能夠明確3點:
上一節當中說起swagger讀取@ApiImplicitParam註解的required參數,最終會體如今js上,經過瀏覽器F12的追蹤,定位到swaggerbootstrapui.js文件上,這裏摘抄部分源碼:
# 點擊發送按鈕時,逐行讀取參數信息,並提取required參數 paramBody.find("tr").each(function () { var paramtr=$(this); var cked=paramtr.find("td:first").find(":checked").prop("checked"); var _urlAppendflag=true; //that.log(cked) if (cked){ //若是選中,留意此行的required:paramtr.data("required")信息提取 var trdata={name:paramtr.find("td:eq(2)").find("input").val(),in:paramtr.data("in"),required:paramtr.data("required"),type:paramtr.data("type"),emflag:paramtr.data("emflag"),schemavalue:paramtr.data("schemavalue")}; //that.log("trdata....") //that.log(trdata); //獲取key //var key=paramtr.find("td:eq(1)").find("input").val(); var key=trdata["name"]; //獲取value var value=""; var reqflag=false; // 後面代碼省略 } })
js上判斷該屬性required是否爲true的處理,js源碼以下:
//判斷是否required if (trdata.hasOwnProperty("required")){ var required=trdata["required"]; if (required){ if(!reqflag){ //必須,驗證value是否爲空 if(value==null||value==""){ validateflag=true; var des=trdata["name"] //validateobj={message:des+"不能爲空"}; validateobj={message:des+i18n.message.debug.fieldNotEmpty}; return false; } } } }
swagger前端js驗證經過能夠向後臺發送請求,或者使用postman向後臺系統發送請求時,開始進入後臺的一系列過濾器、Servlet處理,東西還很多:
// 實際的業務方法部分 auth:28, DemoController (com.hy.demo.controller) invoke0:-1, NativeMethodAccessorImpl (sun.reflect) invoke:62, NativeMethodAccessorImpl (sun.reflect) invoke:43, DelegatingMethodAccessorImpl (sun.reflect) invoke:498, Method (java.lang.reflect) // 請求參數的提取、控制部分 doInvoke:190, InvocableHandlerMethod (org.springframework.web.method.support) invokeForRequest:138, InvocableHandlerMethod (org.springframework.web.method.support) invokeAndHandle:104, ServletInvocableHandlerMethod (org.springframework.web.servlet.mvc.method.annotation) invokeHandlerMethod:892, RequestMappingHandlerAdapter (org.springframework.web.servlet.mvc.method.annotation) handleInternal:797, RequestMappingHandlerAdapter (org.springframework.web.servlet.mvc.method.annotation) handle:87, AbstractHandlerMethodAdapter (org.springframework.web.servlet.mvc.method) // 下面是各類基礎Web服務組件的過濾器等,暫時不關心 doDispatch:1039, DispatcherServlet (org.springframework.web.servlet) doService:942, DispatcherServlet (org.springframework.web.servlet) proce***equest:1005, FrameworkServlet (org.springframework.web.servlet) doGet:897, FrameworkServlet (org.springframework.web.servlet) service:634, HttpServlet (javax.servlet.http) service:882, FrameworkServlet (org.springframework.web.servlet) service:741, HttpServlet (javax.servlet.http) internalDoFilter:231, ApplicationFilterChain (org.apache.catalina.core) doFilter:166, ApplicationFilterChain (org.apache.catalina.core) doFilter:53, WsFilter (org.apache.tomcat.websocket.server) internalDoFilter:193, ApplicationFilterChain (org.apache.catalina.core) doFilter:166, ApplicationFilterChain (org.apache.catalina.core) doFilter:84, SecurityBasicAuthFilter (com.github.xiaoymin.swaggerbootstrapui.filter) internalDoFilter:193, ApplicationFilterChain (org.apache.catalina.core) doFilter:166, ApplicationFilterChain (org.apache.catalina.core) doFilter:53, ProductionSecurityFilter (com.github.xiaoymin.swaggerbootstrapui.filter) internalDoFilter:193, ApplicationFilterChain (org.apache.catalina.core) doFilter:166, ApplicationFilterChain (org.apache.catalina.core) doFilter:124, WebStatFilter (com.alibaba.druid.support.http) internalDoFilter:193, ApplicationFilterChain (org.apache.catalina.core) doFilter:166, ApplicationFilterChain (org.apache.catalina.core) doFilterInternal:88, HttpTraceFilter (org.springframework.boot.actuate.web.trace.servlet) doFilter:109, OncePerRequestFilter (org.springframework.web.filter) internalDoFilter:193, ApplicationFilterChain (org.apache.catalina.core) doFilter:166, ApplicationFilterChain (org.apache.catalina.core) doFilterInternal:99, RequestContextFilter (org.springframework.web.filter) doFilter:109, OncePerRequestFilter (org.springframework.web.filter) internalDoFilter:193, ApplicationFilterChain (org.apache.catalina.core) doFilter:166, ApplicationFilterChain (org.apache.catalina.core) doFilterInternal:92, FormContentFilter (org.springframework.web.filter) doFilter:109, OncePerRequestFilter (org.springframework.web.filter) internalDoFilter:193, ApplicationFilterChain (org.apache.catalina.core) doFilter:166, ApplicationFilterChain (org.apache.catalina.core) doFilterInternal:93, HiddenHttpMethodFilter (org.springframework.web.filter) doFilter:109, OncePerRequestFilter (org.springframework.web.filter) internalDoFilter:193, ApplicationFilterChain (org.apache.catalina.core) doFilter:166, ApplicationFilterChain (org.apache.catalina.core) filterAndRecordMetrics:114, WebMvcMetricsFilter (org.springframework.boot.actuate.metrics.web.servlet) doFilterInternal:104, WebMvcMetricsFilter (org.springframework.boot.actuate.metrics.web.servlet) doFilter:109, OncePerRequestFilter (org.springframework.web.filter) internalDoFilter:193, ApplicationFilterChain (org.apache.catalina.core) doFilter:166, ApplicationFilterChain (org.apache.catalina.core) doFilterInternal:200, CharacterEncodingFilter (org.springframework.web.filter) doFilter:109, OncePerRequestFilter (org.springframework.web.filter) internalDoFilter:193, ApplicationFilterChain (org.apache.catalina.core) doFilter:166, ApplicationFilterChain (org.apache.catalina.core) invoke:202, StandardWrapperValve (org.apache.catalina.core) invoke:96, StandardContextValve (org.apache.catalina.core) invoke:490, AuthenticatorBase (org.apache.catalina.authenticator) invoke:139, StandardHostValve (org.apache.catalina.core) invoke:92, ErrorReportValve (org.apache.catalina.valves) invoke:74, StandardEngineValve (org.apache.catalina.core) service:343, CoyoteAdapter (org.apache.catalina.connector) service:408, Http11Processor (org.apache.coyote.http11) process:66, AbstractProcessorLight (org.apache.coyote) process:853, AbstractProtocol$ConnectionHandler (org.apache.coyote) doRun:1587, NioEndpoint$SocketProcessor (org.apache.tomcat.util.net) run:49, SocketProcessorBase (org.apache.tomcat.util.net) runWorker:1149, ThreadPoolExecutor (java.util.concurrent) run:624, ThreadPoolExecutor$Worker (java.util.concurrent) run:61, TaskThread$WrappingRunnable (org.apache.tomcat.util.threads) run:748, Thread (java.lang)
彙集重點在請求參數的讀取校驗方面,首先看org.springframework.web.method.annotation.AbstractNamedValueMethodArgumentResolver
類的resolveArgument方法:
@Override @Nullable public final Object resolveArgument(MethodParameter parameter, @Nullable ModelAndViewContainer mavContainer, NativeWebRequest webRequest, @Nullable WebDataBinderFactory binderFactory) throws Exception { // 留意此方法調用 NamedValueInfo namedValueInfo = getNamedValueInfo(parameter); MethodParameter nestedParameter = parameter.nestedIfOptional(); Object resolvedName = resolveStringValue(namedValueInfo.name); if (resolvedName == null) { throw new IllegalArgumentException( "Specified name must not resolve to null: [" + namedValueInfo.name + "]"); } // 後面暫時省略 }
getNamedValueInfo
方法的實現以下:
/** * Obtain the named value for the given method parameter. */ private NamedValueInfo getNamedValueInfo(MethodParameter parameter) { NamedValueInfo namedValueInfo = this.namedValueInfoCache.get(parameter); if (namedValueInfo == null) { namedValueInfo = createNamedValueInfo(parameter); namedValueInfo = updateNamedValueInfo(parameter, namedValueInfo); this.namedValueInfoCache.put(parameter, namedValueInfo); } return namedValueInfo; }
進入createNamedValueInfo(parameter)
方法時,這部分代碼以下:
@Override protected NamedValueInfo createNamedValueInfo(MethodParameter parameter) { RequestParam ann = parameter.getParameterAnnotation(RequestParam.class); return (ann != null ? new RequestParamNamedValueInfo(ann) : new RequestParamNamedValueInfo()); } /** * NamedValueInfo的定義 * Represents the information about a named value, including name, whether it's required and a default value. */ protected static class NamedValueInfo { private final String name; private final boolean required; @Nullable private final String defaultValue; public NamedValueInfo(String name, boolean required, @Nullable String defaultValue) { this.name = name; this.required = required; this.defaultValue = defaultValue; } }
這段代碼很關鍵,這裏只讀取@RequestParam註解,不會讀@ApiImplicitParam註解,因此@ApiImplicitParam註解不會影響@RequestParam的屬性,而且不管是從swagger doc過來的請求,仍是postman過來的請求,都執行這一段代碼,最終讀取註解的結果用CurrenctHashMap存儲,key的格式是method 'xxx' parameter y
,xxx爲方法名,y爲參數的順序號,如method 'auth' parameter 0
,基本上能夠保證惟一性。
源碼閱讀到這裏,基本上能夠驗證前面說起的小結論的前2條,引用一下:
前面2個問題已經從源碼中找到解釋,來看第3個問題:若是參數設置required=true,但只是要求參數名存在,若是此字段是Long類型或Integer類型,寫成uid=
或'uid',也能經過校驗,最終進入方法後,仍是得手動寫代碼進行爲空校驗,這顯然不是咱們想要的結果?該如何解決呢?
接上一節,若是這樣通用的參數,得挨個判斷是否爲空,這樣的作法就有點難受了,有沒有更好的解決辦法呢?預期的實現效果是字段加上require=true後,Long類型或其餘數值類型能夠把"",null過濾掉,要否則require還有什麼意義呢?
解決方法有兩個思路:
方案2更通用一些,適用GET、POST請求,而且原有的單個參數聲明無需封裝到POJO類裏。
官網自己提供自定義參數綁定的擴展,見https://docs.spring.io/spring/docs/5.1.8.RELEASE/spring-framework-reference/web.html#mvc-ann-initbinder
官網的例子是在指定的Controller類中使用@InitBinder註解,影響範圍僅限該Controller類,示例以下:
@InitBinder public void initBinder(WebDataBinder binder) { /* * 註冊對於String類型參數對象的屬性進行trim操做的編輯器, * 構造參數表明空串是否轉爲null,false,則將null轉爲空串。 */ binder.registerCustomEditor(String.class, new StringTrimmerEditor(false)); // 這裏我還添加了其餘類型的屬性編輯器,true表示容許使用"",而且將""處理爲空,false表示不容許使用"" binder.registerCustomEditor(Short.class, new CustomNumberEditor(Short.class, false)); binder.registerCustomEditor(Integer.class, new CustomNumberEditor(Integer.class, false)); binder.registerCustomEditor(Long.class, new CustomNumberEditor(Long.class, false)); binder.registerCustomEditor(Float.class, new CustomNumberEditor(Float.class, false)); binder.registerCustomEditor(Double.class, new CustomNumberEditor(Double.class, false)); binder.registerCustomEditor(BigDecimal.class, new CustomNumberEditor(BigDecimal.class, false)); binder.registerCustomEditor(BigInteger.class, new CustomNumberEditor(BigInteger.class, false)); }
因爲這次面臨的問題是全模塊@RequestParam的值的問題,須要作一個全局的配置,此時須要新增一個類,並使用@ControllerAdvice註解,代碼以下:
@ControllerAdvice public class CustomWebBindingInitializer implements WebBindingInitializer { @InitBinder @Override public void initBinder(WebDataBinder binder) { /* * 註冊對於String類型參數對象的屬性進行trim操做的編輯器, * 構造參數表明空串是否轉爲null,false,則將null轉爲空串。 */ binder.registerCustomEditor(String.class, new StringTrimmerEditor(false)); // 這裏我還添加了其餘類型的屬性編輯器,true表示容許使用"",而且將""處理爲空,false表示不容許使用"" binder.registerCustomEditor(Short.class, new CustomNumberEditor(Short.class, false)); binder.registerCustomEditor(Integer.class, new CustomNumberEditor(Integer.class, false)); binder.registerCustomEditor(Long.class, new CustomNumberEditor(Long.class, false)); binder.registerCustomEditor(Float.class, new CustomNumberEditor(Float.class, false)); binder.registerCustomEditor(Double.class, new CustomNumberEditor(Double.class, false)); binder.registerCustomEditor(BigDecimal.class, new CustomNumberEditor(BigDecimal.class, false)); binder.registerCustomEditor(BigInteger.class, new CustomNumberEditor(BigInteger.class, false)); } }
注意一下CustomNumberEditor實例初始化的傳的false參數。
重啓應用,看一下效果:
都已經到這兒了,再加把勁把相關的源碼看一下,仍是在org.springframework.web.method.annotation.AbstractNamedValueMethodArgumentResolver
類的resolveArgument方法的後半段:
@Override @Nullable public final Object resolveArgument(MethodParameter parameter, @Nullable ModelAndViewContainer mavContainer, NativeWebRequest webRequest, @Nullable WebDataBinderFactory binderFactory) throws Exception { // 前面省略 if (binderFactory != null) { WebDataBinder binder = binderFactory.createBinder(webRequest, null, namedValueInfo.name); try { // 在這裏對參數進行轉換 arg = binder.convertIfNecessary(arg, parameter.getParameterType(), parameter); } catch (ConversionNotSupportedException ex) { throw new MethodArgumentConversionNotSupportedException(arg, ex.getRequiredType(), namedValueInfo.name, parameter, ex.getCause()); } catch (TypeMismatchException ex) { throw new MethodArgumentTypeMismatchException(arg, ex.getRequiredType(), namedValueInfo.name, parameter, ex.getCause()); } } handleResolvedValue(arg, namedValueInfo.name, parameter, mavContainer, webRequest); return arg; }
從binder.convertIfNecessary
方法一路跟下去,中間省略一些調用,最終到達org.springframework.beans.propertyeditors.CustomNumberEditor
類的setAsText方法:
/** * Parse the Number from the given text, using the specified NumberFormat. */ @Override public void setAsText(String text) throws IllegalArgumentException { if (this.allowEmpty && !StringUtils.hasText(text)) { // Treat empty String as null value. setValue(null); } else if (this.numberFormat != null) { // Use given NumberFormat for parsing text. setValue(NumberUtils.parseNumber(text, this.numberClass, this.numberFormat)); } else { // Use default valueOf methods for parsing text. setValue(NumberUtils.parseNumber(text, this.numberClass)); } }
仔細看allowEmpty變量,針對Long類型的參數,咱們擴展數據綁定時,該變量設置的是false,表示不接受空值,試驗中咱們傳的值是空串,那麼這裏的條件分支判斷就必須對空串轉換成數值,執行Long.valueOf("")
結果報出運行時異常java.lang.NumberFormatException,告知客戶端參數不對,這是指望的結果。
本篇以實際的研發排錯過程爲出發點,剛開始本身也覺得@ApiImplicitParam對@RequestParam的required屬性的有侵入性,以爲詫異便深刻源碼論證本身的想法,經閱讀源碼後發現事實並非這樣,是剛開始咱們對required的理解有誤。既然required的做用很是有限,那麼確定能找到通用的解決方案避免手動寫代碼對全部參數進行爲空判斷,這些解決一個問題後,發現新的問題,再繼續解決,最終獲得的結果,分析如有不詳盡之處,請指正,謝謝。
專一Java高併發、分佈式架構,更多技術乾貨分享與心得,請關注公衆號:Java架構社區
能夠掃左邊二維碼添加好友,邀請你加入Java架構社區微信羣共同探討技術