原始碼剖析@ApiImplicitParam對@RequestParam的required屬性侵入性
問題起源
使用SpringCloud構建專案時,使用Swagger生成相應的介面文件是推薦的選項,Swagger能夠提供頁面訪問,直接在網頁上除錯後端系統的介面, 非常方便。最近卻遇到了一個有點困惑的問題,演示介面示例如下(原有功能介面帶有業務實現邏輯,這裡簡化了介面):
/**
*
@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.RELEASESpringCloud版本:Greenwich.SR3
業務模組
SpringCloud業務模組使用的swagger:
swagger bootstrap ui 1.9.6 增強swagger ui樣式spring4all-swagger 1.9.0.RELEASE 配置化swagger引數,免去程式碼開發
業務閘道器
SpringCloud業務閘道器使用的swagger:
knife4j 2.0.1 增強swagger ui樣式(閘道器用gateway搭建,swagger使用knife4j-spring-boot-starter依賴,可以聚合業務模組的swagger文件)
此次的範圍只針對SpringCloud業務模組,暫時不涉及業務閘道器的Swagger文件。
測試工具
測試工具目前有兩個:swagger doc:使用瀏覽器進行訪問,如下圖:
postman:手動配置介面引數,示例:
案例實戰
介面測試1
介面示例如開篇所示,我們先使用如下介面,全部使用預設值,即@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的結果:
介面測試2
我們修改@ApiImplicitParam的required值為true,@RequestParam不變,重啟模組@ApiImplicitParam(name = "uid", value = "使用者ID", paramType = "query", required = true, dataType = "Long")
看swagger的結果:
透過除錯瀏覽器可以發現,為空校驗是js完成的,js判斷為空後,並未發起請求到後端,這樣我們可以認為swagger內@ApiImplicitParam的required引數生效了。
介面測試3
在前面我們使用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點:
- @ApiImplicitParam的required引數不會對@RequestParam的required值造成侵入,它們倆不相關。
- @ApiImplicitParam的required引數會影響swagger doc的js邏輯判斷,為空校驗是在js層面上完成的。
- @RequestParam的required引數預設情況下只會校驗是否有該引數名,不校驗它是否有值。
原始碼剖析
swagger部分
上一節當中提及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;
}
}
}
}
SpringCloud業務模組部分
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)
processRequest: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條,引用一下:
- @ApiImplicitParam的required引數不會對@RequestParam的required值造成侵入,它們倆不相關。
- @ApiImplicitParam的required引數會影響swagger doc的js邏輯判斷,為空校驗是在js層面上完成的。
- @RequestParam的required引數預設情況下只會校驗是否有該引數名,不校驗它是否有值。
前面2個問題已經從原始碼中找到解釋,來看第3個問題:如果引數設定required=true,但只是要求引數名存在,如果此欄位是Long型別或Integer型別,寫成uid=或’uid’,也能透過校驗,最終進入方法後,還是得手動寫程式碼進行為空校驗,這顯然不是我們想要的結果?該如何解決呢?
請求引數data bind的問題
接上一節,如果這樣通用的引數,得挨個判斷是否為空,這樣的做法就有點難受了,有沒有更好的解決辦法呢?預期的實現效果是欄位加上require=true後,Long型別或其他數值型別可以把"",null過濾掉,要不然require還有什麼意義呢?
解決方法有兩個思路:
- POST請求方法中將多個引數封裝到一個POJO類裡,用@RequestBody宣告,POJO類中可以使用@Validator框架的@NotNull等註解,並在引數前宣告@Valid。
- 自定義引數繫結規則擴充套件。
方案2更通用一些,適用GET、POST請求,並且原有的單個引數宣告無需封裝到POJO類裡。
官網本身提供自定義引數繫結的擴充套件,見
官網的例子是在指定的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引數。
重啟應用,看一下效果:
擴充套件DataBinder後相關原始碼閱讀
都已經到這兒了,再加把勁把相關的原始碼看一下,還是在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的作用非常有限,那麼肯定能找到通用的解決方案避免手動寫程式碼對所有引數進行為空判斷,這些解決一個問題後,發現新的問題,再繼續解決,最終得到的結果,分析若有不詳盡之處,歡迎大家請指正。
來自 “ ITPUB部落格 ” ,連結:http://blog.itpub.net/69923331/viewspace-2703538/,如需轉載,請註明出處,否則將追究法律責任。
相關文章
- 原始碼剖析@ApiImplicitParam對@RequestParam的required屬性的侵入性原始碼APIUI
- 屬性填充底層原始碼深入剖析前戲原始碼
- HTML5 required屬性HTMLUI
- Java替換RequstBody和RequestParam引數的屬性Java
- [Vue.js進階]從原始碼角度剖析計算屬性的原理Vue.js原始碼
- petite-vue原始碼剖析-屬性繫結`v-bind`的工作原理Vue原始碼
- JavaScript判斷元素是否具有required屬性JavaScriptUI
- 物件屬性讀取(核心原始碼)物件原始碼
- 讀Zepto原始碼之屬性操作原始碼
- css屬性與js中style物件的屬性對應表CSSJS物件
- SAP UI5 Simple Form 屬性 columnsL,columnsM,columnsXL 的屬性深入剖析試讀版UIORM
- 【spring原始碼系列】之【Bean的屬性賦值】Spring原始碼Bean賦值
- CMake 屬性之全域性屬性
- MapStruct屬性對映Struct
- spring原始碼深度解析— IOC 之 屬性填充Spring原始碼
- knockout原始碼分析之computed(依賴屬性)原始碼
- 深入剖析 Java 類屬性與類方法的應用Java
- iOS動畫 屬性屬性解析iOS動畫
- defer 屬性和 async 屬性
- jquery對標籤屬性操作jQuery
- Cardboard對像的公共方法與屬性
- spring原始碼解析之IOC容器(四)——屬性注入Spring原始碼
- Spring原始碼系列:依賴注入(三)-屬性注入Spring原始碼依賴注入
- CSS 屬性篇(七):Display屬性CSS
- CMake 屬性之目標屬性
- CMake 屬性之目錄屬性
- Python 類的屬性與例項屬性Python
- css的屬性CSS
- Vue3原始碼解析(computed-計算屬性)Vue原始碼
- vue原始碼學習:Object.defineProperty 物件屬性監聽Vue原始碼Object物件
- css屬性的可繼承性CSS繼承
- 以太坊中的全域性屬性
- JavaScript私有屬性和靜態屬性JavaScript
- js如何獲取給定屬性的屬性值JS
- javascript的cssText屬性程式碼例項JavaScriptCSS
- 框架(frameset),全域性屬性框架
- CSS字型屬性和文字屬性詳解CSS
- js的屬性物件的specified屬性用法簡單介紹JS物件