作者:京東零售 王鵬超
1.什么是引數決議器
@RequstBody、@RequstParam 這些注解是不是很熟悉?
我們在開發Controller介面時經常會用到此類引數注解,那這些注解的作用是什么?我們真的了解嗎?
簡單來說,這些注解就是幫我們將前端傳遞的引數直接決議成直接可以在代碼邏輯中使用的javaBean,例如@RequstBody接收json引數,轉換成java物件,如下所示:
前臺傳參 | 引數格式 |
---|---|
application/json |
正常代碼書寫如下:
@RequestMapping(value = "https://www.cnblogs.com/getUserInfo")
public String getUserInfo(@RequestBody UserInfo userInfo){
//***
return userInfo.getName();
}
但如果是服務接收引數的方式改變了,如下代碼,引數就不能成功接收了,這個是為什么呢?
@RequestMapping(value = "https://www.cnblogs.com/getUserInfo")
public String getUserInfo(@RequestBody String userName, @RequestBody Integer userId){
//***
return userName;
}
如果上面的代碼稍微改動一下注解的使用并且前臺更改一下傳參格式,就可以正常決議了,
前臺傳參 | 引數格式 |
---|---|
http://***?userName=Alex&userId=1 | 無 |
@RequestMapping(value = "https://www.cnblogs.com/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 = "https://www.cnblogs.com/Jcloud/archive/2023/04/14/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 = https://www.cnblogs.com/Jcloud/archive/2023/04/14/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引數決議器的介紹性解讀,希望對大家有所幫助,歡迎有這類需求或者興趣的同學溝通交流,批評指正,一起進步!
轉載請註明出處,本文鏈接:https://www.uj5u.com/houduan/550104.html
標籤:其他
下一篇:token驗證