@RequstBody、@RequstParam 這些註解是不是很熟悉?我們在開發Controller介面時經常會用到此類參數註解,那這些註解的作用是什麼?我們真的瞭解嗎? ...
作者:京東零售 王鵬超
1.什麼是參數解析器
@RequstBody、@RequstParam 這些註解是不是很熟悉?
我們在開發Controller介面時經常會用到此類參數註解,那這些註解的作用是什麼?我們真的瞭解嗎?
簡單來說,這些註解就是幫我們將前端傳遞的參數直接解析成直接可以在代碼邏輯中使用的javaBean,例如@RequstBody接收json參數,轉換成java對象,如下所示:
前臺傳參 | 參數格式 |
---|---|
application/json |
正常代碼書寫如下:
@RequestMapping(value = "/getUserInfo")
public String getUserInfo(@RequestBody UserInfo userInfo){
//***
return userInfo.getName();
}
但如果是服務接收參數的方式改變了,如下代碼,參數就不能成功接收了,這個是為什麼呢?
@RequestMapping(value = "/getUserInfo")
public String getUserInfo(@RequestBody String userName, @RequestBody Integer userId){
//***
return userName;
}
如果上面的代碼稍微改動一下註解的使用並且前臺更改一下傳參格式,就可以正常解析了。
前臺傳參 | 參數格式 |
---|---|
http://***?userName=Alex&userId=1 | 無 |
@RequestMapping(value = "/getUserInfo")
public String getUserInfo(@RequestParam String userName, @RequestParam Integer userId){
//***
return userName;
}
這些這裡就不得不引出這些註解背後都對應的內容—Spring提供的參數解析器,這些參數解析器幫助我們解析前臺傳遞過來的參數,綁定到我們定義的Controller入參上,不通類型格式的傳遞參數,需要不同的參數解析器,有時候一些特殊的參數格式,甚至需要我們自定義一個參數解析器。
不論是在SpringBoot還是在Spring MVC中,一個HTTP請求會被DispatcherServlet類接收(本質是一個Servlet,繼承自HttpServlet)。Spring負責從HttpServlet中獲取並解析請求,將請求uri匹配到Controller類方法,並解析參數並執行方法,最後處理返回值並渲染視圖。
參數解析器的作用就是將http請求提交的參數轉化為我們controller處理單元的入參。原始的Servlet獲取參數的方式如下,需要手動從HttpServletRequest中獲取所需信息。
@WebServlet(urlPatterns="/getResource")
public class resourceServlet extends HttpServlet {
@Override
protected void doGet(HttpServletRequest req, HttpServletResponse resp) {
/**獲取參數開始*/
String resourceId = req.getParameter("resourceId");
String resourceType = req.getHeader("resourceType");
/**獲取參數結束*/
resp.setContentType("text/html;charset=utf-8");
PrintWriter out = resp.getWriter();
out.println("resourceId " + resourceId + " resourceType " + resourceType);
}
}
Spring為了幫助開發者解放生產力,提供了一些特定格式(header中content-type對應的類型)入參的參數解析器,我們在介面參數上只要加上特定的註解(當然不加註解也有預設解析器),就可以直接獲取到想要的參數,不需要我們自己去HttpServletRequest中手動獲取原始入參,如下所示:
@RestController
public class resourceController {
@RequestMapping("/resource")
public String getResource(@RequestParam("resourceId") String resourceId,
@RequestParam("resourceType") String resourceType,
@RequestHeader("token") String token) {
return "resourceId" + resourceId + " token " + token;
}
}
常用的註解類參數解析器使用方式以及與註解的對應關係對應關係如下:
註解命名 | 放置位置 | 用途 |
---|---|---|
@PathVariable | 放置在參數前 | 允許request的參數在url路徑中 |
@RequestParam | 放置在參數前 | 允許request的參數直接連接在url地址後面,也是Spring預設的參數解析器 |
@RequestHeader | 放置在參數前 | 從請求header中獲取參數 |
@RequestBody | 放置在參數前 | 允許request的參數在參數體中,而不是直接連接在地址後面 |
註解命名 | 對應的解析器 | content-type |
---|---|---|
@PathVariable | PathVariableMethodArgumentResolver | 無 |
@RequestParam | RequestParamMethodArgumentResolver | 無(get請求)和multipart/form-data |
@RequestBody | RequestResponseBodyMethodProcessor | application/json |
@RequestPart | RequestPartMethodArgumentResolver | multipart/form-data |
2.參數解析器原理
要瞭解參數解析器,首先要瞭解一下最原始的Spring MVC的執行過程。客戶端用戶發起一個Http請求後,請求會被提交到前端控制器(Dispatcher Servlet),由前端控制器請求處理器映射器(步驟1),處理器映射器會返回一個執行鏈(Handler Execution 步驟2),我們通常定義的攔截器就是在這個階段執行的,之後前端控制器會將映射器返回的執行鏈中的Handler信息發送給適配器(Handler Adapter 步驟3),適配器會根據Handler找到並執行相應的Handler邏輯,也就是我們所定義的Controller控制單元(步驟4),Handler執行完畢會返回一個ModelAndView對象,後續再經過視圖解析器解析和視圖渲染就可以返回給客戶端請求響應信息了。
在容器初始化的時候,RequestMappingHandlerMapping 映射器會將 @RequestMapping 註解註釋的方法存儲到緩存,其中key是 RequestMappingInfo,value是HandlerMethod。HandlerMethod 是如何進行方法的參數解析和綁定,就要瞭解請求參數適配器**RequestMappingHandlerAdapter,**該適配器對應接下來的參數解析及綁定過程。源碼路徑如下:
org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerAdapter
RequestMappingHandlerAdapter大致的解析和綁定流程如下圖所示,
RequestMappingHandlerAdapter實現了介面InitializingBean,在Spring容器初始化Bean後,調用方法afterPropertiesSet( ),將預設參數解析器綁定HandlerMethodArgumentResolverComposite 適配器的參數 argumentResolvers上,其中HandlerMethodArgumentResolverComposite是介面HandlerMethodArgumentResolver的實現類。源碼路徑如下:
org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerAdapter#afterPropertiesSet
@Override
public void afterPropertiesSet() {
// Do this first, it may add ResponseBody advice beans
initControllerAdviceCache();
if (this.argumentResolvers == null) {
/** */
List<HandlerMethodArgumentResolver> resolvers = getDefaultArgumentResolvers();
this.argumentResolvers = new HandlerMethodArgumentResolverComposite().addResolvers(resolvers);
}
if (this.initBinderArgumentResolvers == null) {
List<HandlerMethodArgumentResolver> resolvers = getDefaultInitBinderArgumentResolvers();
this.initBinderArgumentResolvers = new HandlerMethodArgumentResolverComposite().addResolvers(resolvers);
}
if (this.returnValueHandlers == null) {
List<HandlerMethodReturnValueHandler> handlers = getDefaultReturnValueHandlers();
this.returnValueHandlers = new HandlerMethodReturnValueHandlerComposite().addHandlers(handlers);
}
}
通過getDefaultArgumentResolvers( )方法,可以看到Spring為我們提供了哪些預設的參數解析器,這些解析器都是HandlerMethodArgumentResolver介面的實現類。
針對不同的參數類型,Spring提供了一些基礎的參數解析器,其中有基於註解的解析器,也有基於特定類型的解析器,當然也有兜底預設的解析器,如果已有的解析器不能滿足解析要求,Spring也提供了支持用戶自定義解析器的擴展點,源碼如下:
org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerAdapter#getDefaultArgumentResolvers
private List<HandlerMethodArgumentResolver> getDefaultArgumentResolvers() {
List<HandlerMethodArgumentResolver> resolvers = new ArrayList<HandlerMethodArgumentResolver>();
// Annotation-based argument resolution 基於註解
/** @RequestPart 文件註入 */
resolvers.add(new RequestParamMethodArgumentResolver(getBeanFactory(), false));
/** @RequestParam 名稱解析參數 */
resolvers.add(new RequestParamMapMethodArgumentResolver());
/** @PathVariable url路徑參數 */
resolvers.add(new PathVariableMethodArgumentResolver());
/** @PathVariable url路徑參數,返回一個map */
resolvers.add(new PathVariableMapMethodArgumentResolver());
/** @MatrixVariable url矩陣變數參數 */
resolvers.add(new MatrixVariableMethodArgumentResolver());
/** @MatrixVariable url矩陣變數參數 返回一個map*/
resolvers.add(new Matrix VariableMapMethodArgumentResolver());
/** 兜底處理@ModelAttribute註解和無註解 */
resolvers.add(new ServletModelAttributeMethodProcessor(false));
/** @RequestBody body體解析參數 */
resolvers.add(new RequestResponseBodyMethodProcessor(getMessageConverters(), this.requestResponseBodyAdvice));
/** @RequestPart 使用類似RequestParam */
resolvers.add(new RequestPartMethodArgumentResolver(getMessageConverters(), this.requestResponseBodyAdvice));
/** @RequestHeader 解析請求header */
resolvers.add(new RequestHeaderMethodArgumentResolver(getBeanFactory()));
/** @RequestHeader 解析請求header,返回map */
resolvers.add(new RequestHeaderMapMethodArgumentResolver());
/** Cookie中取值註入 */
resolvers.add(new ServletCookieValueMethodArgumentResolver(getBeanFactory()));
/** @Value */
resolvers.add(new ExpressionValueMethodArgumentResolver(getBeanFactory()));
/** @SessionAttribute */
resolvers.add(new SessionAttributeMethodArgumentResolver());
/** @RequestAttribute */
resolvers.add(new RequestAttributeMethodArgumentResolver());
// Type-based argument resolution 基於類型
/** Servlet api 對象 HttpServletRequest 對象綁定值 */
resolvers.add(new ServletRequestMethodArgumentResolver());
/** Servlet api 對象 HttpServletResponse 對象綁定值 */
resolvers.add(new ServletResponseMethodArgumentResolver());
/** http請求中 HttpEntity RequestEntity數據綁定 */
resolvers.add(new HttpEntityMethodProcessor(getMessageConverters(), this.requestResponseBodyAdvice));
/** 請求重定向 */
resolvers.add(new RedirectAttributesMethodArgumentResolver());
/** 返回Model對象 */
resolvers.add(new ModelMethodProcessor());
/** 處理入參,返回一個map */
resolvers.add(new MapMethodProcessor());
/** 處理錯誤方法參數,返回最後一個對象 */
resolvers.add(new ErrorsMethodArgumentResolver());
/** SessionStatus */
resolvers.add(new SessionStatusMethodArgumentResolver());
/** */
resolvers.add(new UriComponentsBuilderMethodArgumentResolver());
// Custom arguments 用戶自定義
if (getCustomArgumentResolvers() != null) {
resolvers.addAll(getCustomArgumentResolvers());
}
// Catch-all 兜底預設
resolvers.add(new RequestParamMethodArgumentResolver(getBeanFactory(), true));
resolvers.add(new ServletModelAttributeMethodProcessor(true));
return resolvers;
}
HandlerMethodArgumentResolver介面中只定義了兩個方法,分別是解析器適用範圍確定方法supportsParameter( )和參數解析方法resolveArgument(),不同用途的參數解析器的使用差異就體現在這兩個方法上,這裡就不具體展開參數的解析和綁定過程。
3.自定義參數解析器的設計
Spring的設計很好踐行了開閉原則,不僅在封裝整合了很多非常強大的能力,也為用戶留好了自定義拓展的能力,參數解析器也是這樣,Spring提供的參數解析器基本能滿足常用的參數解析能力,但很多系統的參數傳遞並不規範,比如京東color網關傳業務參數都是封裝在body中,需要先從body中取出業務參數,然後再針對性解析,這時候Spring提供的解析器就幫不了我們了,需要我們擴展自定義適配參數解析器了。
Spring提供兩種自定義參數解析器的方式,一種是實現適配器介面HandlerMethodArgumentResolver,另一種是繼承已有的參數解析器(HandlerMethodArgumentResolver介面的現有實現類)例如AbstractNamedValueMethodArgumentResolver進行增強優化。如果是深度定製化的自定義參數解析器,建議實現自己實現介面進行開發,以實現介面適配器介面自定義開發解析器為例,介紹如何自定義一個參數解析器。
通過查看源碼發現,參數解析適配器介面留給我擴展的方法有兩個,分別是supportsParameter( )和resolveArgument( ),第一個方法是自定義參數解析器適用的場景,也就是如何命中參數解析器,第二個是具體解析參數的實現。
public interface HandlerMethodArgumentResolver {
/**
* 識別到哪些參數特征,才使用當前自定義解析器
*/
boolean supportsParameter(MethodParameter parameter);
/**
* 具體參數解析方法
*/
Object resolveArgument(MethodParameter parameter, ModelAndViewContainer mavContainer,
NativeWebRequest webRequest, WebDataBinderFactory binderFactory) throws Exception;
}
現在開始具體實現一個基於註解的自定義參數解析器,這個是代碼實際使用過程中用到的參數解析器,獲取color網關的body業務參數,然後解析後給Controller方法直接使用。
public class ActMethodArgumentResolver implements HandlerMethodArgumentResolver {
private static final String DEFAULT_VALUE = "body";
@Override
public boolean supportsParameter(MethodParameter parameter) {
/** 只有指定註解註釋的參數才會走當前自定義參數解析器 */
return parameter.hasParameterAnnotation(RequestJsonParam.class);
}
@Override
public Object resolveArgument(MethodParameter parameter, ModelAndViewContainer mavContainer, NativeWebRequest webRequest, WebDataBinderFactory binderFactory) throws Exception {
/** 獲取參數註解 */
RequestJsonParam attribute = parameter.getParameterAnnotation(RequestJsonParam.class);
/** 獲取參數名 */
String name = attribute.value();
/** 獲取指定名字參數的值 */
String value = webRequest.getParameter(StringUtils.isEmpty(name) ? DEFAULT_VALUE : name);
/** 獲取註解設定參數類型 */
Class<?> targetParamType = attribute.recordClass();
/** 獲取實際參數類型 */
Class<?> webParamType = parameter.getParameterType()
/** 以自定義參數類型為準 */
Class<?> paramType = targetParamType != null ? targetParamType : parameter.getParameterType();
if (ObjectUtils.equals(paramType, String.class)
|| ObjectUtils.equals(paramType, Integer.class)
|| ObjectUtils.equals(paramType, Long.class)
|| ObjectUtils.equals(paramType, Boolean.class)) {
JSONObject object = JSON.parseObject(value);
log.error("ActMethodArgumentResolver resolveArgument,paramName:{}, object:{}", paramName, JSON.toJSONString(object));
if (object.get(paramName) instanceof Integer && ObjectUtils.equals(paramType, Long.class)) {
//入參:Integer 目標類型:Long
result = paramType.cast(((Integer) object.get(paramName)).longValue());
}else if (object.get(paramName) instanceof Integer && ObjectUtils.equals(paramType, String.class)) {
//入參:Integer 目標類型:String
result = String.valueOf(object.get(paramName));
}else if (object.get(paramName) instanceof Long && ObjectUtils.equals(paramType, Integer.class)) {
//入參:Long 目標類型:Integer(精度丟失)
result = paramType.cast(((Long) object.get(paramName)).intValue());
}else if (object.get(paramName) instanceof Long && ObjectUtils.equals(paramType, String.class)) {
//入參:Long 目標類型:String
result = String.valueOf(object.get(paramName));
}else if (object.get(paramName) instanceof String && ObjectUtils.equals(paramType, Long.class)) {
//入參:String 目標類型:Long
result = Long.valueOf((String) object.get(paramName));
} else if (object.get(paramName) instanceof String && ObjectUtils.equals(paramType, Integer.class)) {
//入參:String 目標類型:Integer
result = Integer.valueOf((String) object.get(paramName));
} else {
result = paramType.cast(object.get(paramName));
}
}else if (paramType.isArray()) {
/** 入參是數組 */
result = JsonHelper.fromJson(value, paramType);
if (result != null) {
Object[] targets = (Object[]) result;
for (int i = 0; i < targets.length; i++) {
WebDataBinder binder = binderFactory.createBinder(webRequest, targets[i], name + "[" + i + "]");
validateIfApplicable(binder, parameter, annotations);
}
}
} else if (Collection.class.isAssignableFrom(paramType)) {
/** 這裡要特別註意!!!,集合參數由於範型獲取不到集合元素類型,所以指定類型就非常關鍵了 */
Class recordClass = attribute.recordClass() == null ? LinkedHashMap.class : attribute.recordClass();
result = JsonHelper.fromJsonArrayBy(value, recordClass, paramType);
if (result != null) {
Collection<Object> targets = (Collection<Object>) result;
int index = 0;
for (Object targetObj : targets) {
WebDataBinder binder = binderFactory.createBinder(webRequest, targetObj, name + "[" + (index++) + "]");
validateIfApplicable(binder, parameter, annotations);
}
}
} else{
result = JSON.parseObject(value, paramType);
}
if (result != null) {
/** 參數綁定 */
WebDataBinder binder = binderFactory.createBinder(webRequest, result, name);
result = binder.convertIfNecessary(result, paramType, parameter);
validateIfApplicable(binder, parameter, annotations);
mavContainer.addAttribute(name, result);
}
}
自定義參數解析器註解的定義如下,這裡定義了一個比較特殊的屬性recordClass,後續會講到是解決什麼問題。
/**
* 請求json參數處理註解
* @author wangpengchao01
* @date 2022-11-07 14:18
*/
@Target(ElementType.PARAMETER)
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface RequestJsonParam {
/**
* 綁定的請求參數名
*/
String value() default "body";
/**
* 參數是否必須
*/
boolean required() default false;
/**
* 預設值
*/
String defaultValue() default ValueConstants.DEFAULT_NONE;
/**
* 集合json反序列化後記錄的類型
*/
Class recordClass() default null;
}
通過配置類將自定義解析器註冊到Spring容器中
@Configuration
public class WebConfig extends WebMvcConfigurerAdapter {
@Bean
public static ActMethodArgumentResolver actMethodArgumentResolverConfigurer() {
return new ActMethodArgumentResolver();
}
@Override
public void addArgumentResolvers(List<HandlerMethodArgumentResolver> argumentResolvers) {
argumentResolvers.add(actMethodArgumentResolverConfigurer());
}
}
到此,一個完整的基於註解的自定義參數解析器就完成了。
4.總結
瞭解Spring的參數解析器原理有助於正確使用Spring的參數解析器,也讓我們可以設計適用於自身系統的參數解析器,對於一些通用參數類型的解析減少重覆代碼的書寫,但是這裡有個前提是我們項目中複雜類型的入參要統一,前端傳遞參數的格式也要統一,不然設計自定義參數解析器就是個災難,需要做各種複雜的相容工作。參數解析器的設計儘量要放在項目開發開始階段,歷史複雜的系統如果介面開發沒有統一規範也不建議自定義參數解析器設計。
該文章僅作為Spring參數解析器的介紹性解讀,希望對大家有所幫助,歡迎有這類需求或者興趣的同學溝通交流,批評指正,一起進步!