天天看點

HandlerMethodArgumentResolver(二):Map參數類型和固定參數類型【享學Spring MVC】關注A哥

每篇一句

黃金的導電性最好,為什麼電腦主機闆還是要用銅?

飛機最快,為什麼還有人做火車?

清華大學最好,為什麼還有人去普通學校?

因為資源都是有限的,我們現實生活中必須兼顧成本與産出的平衡

前言

上文 介紹了

Spring MVC

用于處理入參的處理器:

HandlerMethodReturnValueHandler

它的作用,以及介紹了最為常用的兩個參數處理器子類:

PathVariableMethodArgumentResolver

RequestParamMethodArgumentResolver

。由于該體系的重要以及龐大,本文将接着繼續講解~

第一類:基于

Name

(續)

RequestHeaderMethodArgumentResolver

@RequestHeader

注解,可以把Request請求header部分的值綁定到方法的參數上。

public class RequestHeaderMethodArgumentResolver extends AbstractNamedValueMethodArgumentResolver {

	// 必須标注@RequestHeader注解,并且不能,不能,不能是Map類型
	// 有的小夥伴會說:`@RequestHeader Map headers`這樣可以接收到所有的請求頭啊
	// 其實不是本類的功勞,是`RequestHeaderMapMethodArgumentResolver`的作用
	@Override
	public boolean supportsParameter(MethodParameter parameter) {
		return (parameter.hasParameterAnnotation(RequestHeader.class) &&
				!Map.class.isAssignableFrom(parameter.nestedIfOptional().getNestedParameterType()));
	}

	// 了解起來很簡單:可以單值,也可以List/數組
	@Override
	@Nullable
	protected Object resolveName(String name, MethodParameter parameter, NativeWebRequest request) throws Exception {
		String[] headerValues = request.getHeaderValues(name);
		if (headerValues != null) {
			return (headerValues.length == 1 ? headerValues[0] : headerValues);
		} else {
			return null;
		}
	}
}
           

此處理器能處理的是我們這麼來使用:

@ResponseBody
    @GetMapping("/test")
    public Object test(@RequestHeader("Accept-Encoding") String encoding,
                       @RequestHeader("Accept-Encoding") List<String> encodingList) {
        System.out.println(encoding);
        System.out.println(encodingList);
        return encoding;
    }
           

請求頭截圖:

HandlerMethodArgumentResolver(二):Map參數類型和固定參數類型【享學Spring MVC】關注A哥

結果列印(集合封裝成功了,證明逗号分隔是可以被封裝成集合/數組的):

gzip, deflate, br
[gzip, deflate, br]
           
Tip:注解指定的value值(key值)是

區分大小寫的

RequestAttributeMethodArgumentResolver

處理必須标注有

@RequestAttribute

注解的參數,原理說這一句話就夠了。

SessionAttributeMethodArgumentResolver

同上(注解不一樣,scope不一樣而已)

AbstractCookieValueMethodArgumentResolver(抽象類)

對解析标注有

@CookieValue

的做了一層抽象,子類負責從request裡拿值(該抽象類不合請求域綁定)。

public abstract class AbstractCookieValueMethodArgumentResolver extends AbstractNamedValueMethodArgumentResolver {
	...
	@Override
	public boolean supportsParameter(MethodParameter parameter) {
		return parameter.hasParameterAnnotation(CookieValue.class);
	}	
	@Override
	protected void handleMissingValue(String name, MethodParameter parameter) throws ServletRequestBindingException {
		throw new MissingRequestCookieException(name, parameter);
	}
	... // 并木有實作核心resolveName方法
}
           
ServletCookieValueMethodArgumentResolver

指定了從

HttpServletRequest

去拿cookie值。

public class ServletCookieValueMethodArgumentResolver extends AbstractCookieValueMethodArgumentResolver {
	private UrlPathHelper urlPathHelper = new UrlPathHelper();
	...
	public void setUrlPathHelper(UrlPathHelper urlPathHelper) {
		this.urlPathHelper = urlPathHelper;
	}

	@Override
	@Nullable
	protected Object resolveName(String cookieName, MethodParameter parameter, NativeWebRequest webRequest) throws Exception {
		HttpServletRequest servletRequest = webRequest.getNativeRequest(HttpServletRequest.class);
		Assert.state(servletRequest != null, "No HttpServletRequest");

		// 工具方法,底層是:request.getCookies()
		Cookie cookieValue = WebUtils.getCookie(servletRequest, cookieName);
		// 如果用javax.servlet.http.Cookie接受值,就直接傳回了
		if (Cookie.class.isAssignableFrom(parameter.getNestedParameterType())) {
			return cookieValue;
		} else if (cookieValue != null) { // 否則傳回cookieValue
			return this.urlPathHelper.decodeRequestString(servletRequest, cookieValue.getValue());
		} else {
			return null;
		}
	}
}
           

一般我們這麼來用:

@ResponseBody
    @GetMapping("/test")
    public Object test(@CookieValue("JSESSIONID") Cookie cookie,
                       @CookieValue("JSESSIONID") String cookieValue) {
        System.out.println(cookie);
        System.out.println(cookieValue);
        return cookieValue;
    }
           

手動設定一個cookie值,然後請求

HandlerMethodArgumentResolver(二):Map參數類型和固定參數類型【享學Spring MVC】關注A哥

控制台列印如下:

javax.servlet.http.Cookie@401ef395
123456
           
Tips:在現在restful風格下,cookie使用得是很少的了。一般用于提升使用者體驗方面~

MatrixVariableMethodArgumentResolver

标注有

@MatrixVariable

注解的參數的處理器。

Matrix:矩陣

,這個注解是Spring3.2新提出來的,增強Restful的處理能力(配合

@PathVariable

使用),比如這類URL的解析就得靠它:

/owners/42;q=11/pets/21;s=23;q=22

關于

@MatrixVariable

它的使用案例,我找了兩篇靠譜文章給你參考:

參考一

參考二

// @since 3.2
public class MatrixVariableMethodArgumentResolver extends AbstractNamedValueMethodArgumentResolver {
	// @MatrixVariable注解是必須的。然後技能處理普通類型,也能處理Map
	@Override
	public boolean supportsParameter(MethodParameter parameter) {
		if (!parameter.hasParameterAnnotation(MatrixVariable.class)) {
			return false;
		}
		if (Map.class.isAssignableFrom(parameter.nestedIfOptional().getNestedParameterType())) {
			MatrixVariable matrixVariable = parameter.getParameterAnnotation(MatrixVariable.class);
			return (matrixVariable != null && StringUtils.hasText(matrixVariable.name()));
		}
		return true;
	}
	...
}
           

ExpressionValueMethodArgumentResolver

它用于處理标注有

@Value

注解的參數。對于這個注解我們太熟悉不過了,沒想到在web層依舊能發揮作用。本文就重點來會會它~

通過

@Value

讓我們在配置檔案裡給參數指派,在某些特殊場合(比如前端不用傳,但你想給個預設值,這個時候用它也是一種方案)

說明:這就相當于在Controller層使用了@Value注解,其實我是不太建議的。因為@Value建議還是隻使用在業務層~
// @since 3.1
public class ExpressionValueMethodArgumentResolver extends AbstractNamedValueMethodArgumentResolver {
	// 唯一構造函數  支援占位符、SpEL
	public ExpressionValueMethodArgumentResolver(@Nullable ConfigurableBeanFactory beanFactory) {
		super(beanFactory);
	}

	//必須标注有@Value注解
	@Override
	public boolean supportsParameter(MethodParameter parameter) {
		return parameter.hasParameterAnnotation(Value.class);
	}

	@Override
	protected NamedValueInfo createNamedValueInfo(MethodParameter parameter) {
		Value ann = parameter.getParameterAnnotation(Value.class);
		return new ExpressionValueNamedValueInfo(ann);
	}
	private static final class ExpressionValueNamedValueInfo extends NamedValueInfo {
		// 這裡name傳值為固定值  因為隻要你的key不是這個就木有問題
		// required傳固定值false
		// defaultValue:取值為annotation.value() --> 它天然支援占位符和SpEL嘛
		private ExpressionValueNamedValueInfo(Value annotation) {
			super("@Value", false, annotation.value());
		}
	}

	// 這裡恒傳回null,是以即使你的key是@Value,也是不會采納你的傳值的喲~
	@Override
	@Nullable
	protected Object resolveName(String name, MethodParameter parameter, NativeWebRequest webRequest) throws Exception {
		// No name to resolve
		return null;
	}
}
           

根本原理其實隻是利用了

defaultValue

支援占位符和

SpEL

的特性而已。給個使用示例:

// 在MVC子容器中導入外部化配置
@Configuration
@PropertySource("classpath:my.properties") // 此處有鍵值對:test.myage = 18
@EnableWebMvc
public class WebMvcConfig extends WebMvcConfigurerAdapter { ... }

    @ResponseBody
    @GetMapping("/test")
    public Object test(@Value("#{T(Integer).parseInt('${test.myage:10}') + 10}") Integer myAge) {
        System.out.println(myAge);
        return myAge;
    }
           

請求:

/test

,列印:

28

注意:若你寫成

@Value("#{'${test.myage:10}' + 10}

,那你得到的答案是:

1810

(成字元串拼接了)。

另外,我看到網上有不少人說如果把這個

@PropertySource("classpath:my.properties")

放在根容器的config檔案裡導入,controller層就使用

@Value

/占位符擷取不到值了,其實這是**

不正确

**的。理由如下:

Spring MVC

子容器在建立時:

initWebApplicationContext()

if (cwac.getParent() == null) {
	cwac.setParent(rootContext); // 設定上父容器(根容器)
}

AbstractApplicationContext:如下代碼
	// 相當于子容器的環境會把父容器的Enviroment合并進來
	@Override
	public void setParent(@Nullable ApplicationContext parent) {
		this.parent = parent;
		if (parent != null) {
			Environment parentEnvironment = parent.getEnvironment();
			if (parentEnvironment instanceof ConfigurableEnvironment) {
				getEnvironment().merge((ConfigurableEnvironment) parentEnvironment);
			}
		}
	}
	
AbstractEnvironment:merge()方法如下
	@Override
	public void merge(ConfigurableEnvironment parent) {
		// 完全的從parent裡所有的PropertySources裡拷貝一份進來
		for (PropertySource<?> ps : parent.getPropertySources()) {
			if (!this.propertySources.contains(ps.getName())) {
				this.propertySources.addLast(ps);
			}
		}
		...	
	}
           

這就是為什麼說即使你是在根容器裡使用的

@PropertySource

導入的外部資源,子容器也可以使用的原因(因為子容器會把父環境給

merge

一份過來)。

但是,但是,但是:如果你是使用形如

PropertyPlaceholderConfigurer

這種方式導進來的,那是會有容器隔離效應的~

第二類:參數類型是

Map

資料來源同上,隻是參數類型是Map

這類解析器我認為是對第一類的有些處理器的一種補充,它依賴上面的相關注解。

你是否想過通過

@RequestParam

一次性全給封裝進一個

Map

裡,然後再自己分析?同樣的本類處理器給

@RequestHeader

@PathVariable

@MatrixVariable

都賦予了這種能力~

PathVariableMapMethodArgumentResolver

// @since 3.2 晚一個版本号
public class PathVariableMapMethodArgumentResolver implements HandlerMethodArgumentResolver {

	// 必須标注@PathVariable注解  并且類型是Map,并且注解不能有value值
	// 處理情況和PathVariableMethodArgumentResolver形成了互補
	@Override
	public boolean supportsParameter(MethodParameter parameter) {
		PathVariable ann = parameter.getParameterAnnotation(PathVariable.class);
		return (ann != null && Map.class.isAssignableFrom(parameter.getParameterType()) &&
				!StringUtils.hasText(ann.value()));
	}

	@Override
	public Object resolveArgument(MethodParameter parameter, @Nullable ModelAndViewContainer mavContainer,
			NativeWebRequest webRequest, @Nullable WebDataBinderFactory binderFactory) throws Exception {
		... // 處理上極其簡單,把所有的路徑參數使用Map裝着傳回即可
	}
}
           

RequestParamMapMethodArgumentResolver

它依賴的方法是:

HttpServletRequest#getParameterMap()

MultipartRequest#getMultiFileMap()

MultipartRequest#getFileMap()

等,出現于

Spring 3.1

示範一把:

@ResponseBody
    @GetMapping("/test")
    public Object test(@RequestParam Map<String,Object> params) {
        System.out.println(params);
        return params;
    }
           

請求:

/test?name=fsx&age=18&age=28

。列印

從結果看出:

  1. 它不能傳一key多值情況
  2. 若出現相同的key,以在最前面的key的值為準。
  3. Map執行個體是一個

    LinkedHashMap<String,String>

    執行個體

RequestHeaderMapMethodArgumentResolver

一次性把請求頭資訊都拿到:資料類型支出寫

MultiValueMap(LinkedMultiValueMap)/HttpHeaders/Map

。執行個體如下:

@ResponseBody
    @GetMapping("/test")
    public Object test(@RequestHeader Map<String, Object> headers) {
        headers.forEach((k, v) -> System.out.println(k + "-->" + v));
        return headers;
    }
           

請求列印:

host-->localhost:8080
connection-->keep-alive
cache-control-->max-age=0
upgrade-insecure-requests-->1
user-agent-->Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/76.0.3809.100 Safari/537.36
sec-fetch-mode-->navigate
sec-fetch-user-->?1
accept-->text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3
sec-fetch-site-->none
accept-encoding-->gzip, deflate, br
accept-language-->zh-CN,zh;q=0.9
cookie-->JSESSIONID=123456789

           

不過強烈不建議直接使用

Map

,而是使用

HttpHeaders

類型。這麼寫

@RequestHeader HttpHeaders headers

,擷取的時候更為便捷。

MatrixVariableMapMethodArgumentResolver

略。

MapMethodProcessor

它處理Map類型,但沒有标注任何注解的情況,它的執行順序是很靠後的,是以有點兜底的意思。

// @since 3.1
public class MapMethodProcessor implements HandlerMethodArgumentResolver, HandlerMethodReturnValueHandler {
	@Override
	public boolean supportsParameter(MethodParameter parameter) {
		return Map.class.isAssignableFrom(parameter.getParameterType());
	}

	// 處理邏輯非常簡單粗暴:把Model直接傳回~~~~
	@Override
	@Nullable
	public Object resolveArgument(MethodParameter parameter, @Nullable ModelAndViewContainer mavContainer,
			NativeWebRequest webRequest, @Nullable WebDataBinderFactory binderFactory) throws Exception {
		return mavContainer.getModel();
	}
}
           

使用案例:略。

這個處理器同時也解釋了:為何你方法入參上寫個

Map、HashMap、ModelMap

等等就可以非常便捷的擷取到模型的值的原因~

第三類:固定參數類型

參數比如是

SessionStatus, ServletResponse, OutputStream, Writer, WebRequest, MultipartRequest, HttpSession, Principal, InputStream

這種方式使用得其實還比較多的。比如平時我們需要用Servlet源生的API:

HttpServletRequest, HttpServletResponse

腫麼辦? 在

Spring MVC

内就特别特别簡單,隻需要在入參上聲明:就可以直接使用啦~

ServletRequestMethodArgumentResolver

// 它支援到的可不僅僅是ServletRequest,多到令人發指
public class ServletRequestMethodArgumentResolver implements HandlerMethodArgumentResolver {

	// 連Servlet 4.0的PushBuilder都支援了(Spring5.0以上版本支援的)
	@Nullable
	private static Class<?> pushBuilder;
	static {
		try {
			pushBuilder = ClassUtils.forName("javax.servlet.http.PushBuilder",
					ServletRequestMethodArgumentResolver.class.getClassLoader());
		} catch (ClassNotFoundException ex) {
			// Servlet 4.0 PushBuilder not found - not supported for injection
			pushBuilder = null;
		}
	}

	// 支援"注入"的類型,可謂多多益善
	@Override
	public boolean supportsParameter(MethodParameter parameter) {
		Class<?> paramType = parameter.getParameterType();
		return (WebRequest.class.isAssignableFrom(paramType) ||
				ServletRequest.class.isAssignableFrom(paramType) || // webRequest.getNativeRequest(requiredType)
				MultipartRequest.class.isAssignableFrom(paramType) ||
				HttpSession.class.isAssignableFrom(paramType) || //request.getSession()
				(pushBuilder != null && pushBuilder.isAssignableFrom(paramType)) || //PushBuilderDelegate.resolvePushBuilder(request, paramType);
				Principal.class.isAssignableFrom(paramType) || //request.getUserPrincipal()
				InputStream.class.isAssignableFrom(paramType) || // request.getInputStream()
				Reader.class.isAssignableFrom(paramType) || //request.getReader()
				HttpMethod.class == paramType || //HttpMethod.resolve(request.getMethod());
				Locale.class == paramType || //RequestContextUtils.getLocale(request)
				TimeZone.class == paramType || //RequestContextUtils.getTimeZone(request)
				ZoneId.class == paramType); //RequestContextUtils.getTimeZone(request);
	}
}
           

看到這你應該明白,以後你需要使用這些參數的話,直接在方法上申明即可,不需要自己再去get了,又是一種依賴注入的效果展現有木有~

ServletResponseMethodArgumentResolver

// @since 3.1
public class ServletResponseMethodArgumentResolver implements HandlerMethodArgumentResolver {
	// 它相對來說很比較簡單
	@Override
	public boolean supportsParameter(MethodParameter parameter) {
		Class<?> paramType = parameter.getParameterType();
		return (ServletResponse.class.isAssignableFrom(paramType) || // webRequest.getNativeResponse(requiredType)
				OutputStream.class.isAssignableFrom(paramType) || //response.getOutputStream()
				Writer.class.isAssignableFrom(paramType)); //response.getWriter()
	}

	@Override
	public Object resolveArgument(MethodParameter parameter, @Nullable ModelAndViewContainer mavContainer,
			NativeWebRequest webRequest, @Nullable WebDataBinderFactory binderFactory) throws Exception {
		// 這個判斷放在這。。。
		if (mavContainer != null) {
			mavContainer.setRequestHandled(true);
		}
		... 
	}
}
           

SessionStatusMethodArgumentResolver

支援

SessionStatus

。值為:

mavContainer.getSessionStatus();

UriComponentsBuilderMethodArgumentResolver

// @since 3.1
public class UriComponentsBuilderMethodArgumentResolver implements HandlerMethodArgumentResolver {
	// UriComponentsBuilder/ ServletUriComponentsBuilder
	@Override
	public boolean supportsParameter(MethodParameter parameter) {
		Class<?> type = parameter.getParameterType();
		return (UriComponentsBuilder.class == type || ServletUriComponentsBuilder.class == type);
	}

	@Override
	public Object resolveArgument(MethodParameter parameter, @Nullable ModelAndViewContainer mavContainer,
			NativeWebRequest webRequest, @Nullable WebDataBinderFactory binderFactory) throws Exception {

		HttpServletRequest request = webRequest.getNativeRequest(HttpServletRequest.class);
		return ServletUriComponentsBuilder.fromServletMapping(request);
	}
}
           

通過

UriComponentsBuilder

來得到URL的各個部分,以及建構URL都是非常的友善的。

RedirectAttributesMethodArgumentResolver

和重定向屬性

RedirectAttributes

相關。

// @since 3.1
public class RedirectAttributesMethodArgumentResolver implements HandlerMethodArgumentResolver {
	@Override
	public boolean supportsParameter(MethodParameter parameter) {
		return RedirectAttributes.class.isAssignableFrom(parameter.getParameterType());
	}

	@Override
	public Object resolveArgument(MethodParameter parameter, @Nullable ModelAndViewContainer mavContainer,
			NativeWebRequest webRequest, @Nullable WebDataBinderFactory binderFactory) throws Exception {
		ModelMap redirectAttributes;

		// 把DataBinder傳入到RedirectAttributesModelMap裡面去~~~~
		if (binderFactory != null) {
			DataBinder dataBinder = binderFactory.createBinder(webRequest, null, DataBinder.DEFAULT_OBJECT_NAME);
			redirectAttributes = new RedirectAttributesModelMap(dataBinder);
		} else {
			redirectAttributes  = new RedirectAttributesModelMap();
		}
		mavContainer.setRedirectModel(redirectAttributes);
		return redirectAttributes;
	}
}
           

如果涉及到重定向:多個視圖見傳值,使用它還是比較友善的。

ModelMethodProcessor

允許你入參裡寫:

org.springframework.ui.Model

RedirectAttributes

RedirectAttributesModelMap

ConcurrentModel

ExtendedModelMap

等等

在本文末尾,說一個特殊的處理器:

ModelAttributeMethodProcessor

:主要是針對 被

@ModelAttribute

注解修飾且不是普通類型(通過

!BeanUtils.isSimpleProperty

來判斷)的參數。

// @since 3.1
public class ModelAttributeMethodProcessor implements HandlerMethodArgumentResolver, HandlerMethodReturnValueHandler {

	// 标注有@ModelAttribute它會處理
	// 若沒有标注(隻要不是“簡單類型”),它也會兜底處理
	@Override
	public boolean supportsParameter(MethodParameter parameter) {
		return (parameter.hasParameterAnnotation(ModelAttribute.class) ||
				(this.annotationNotRequired && !BeanUtils.isSimpleProperty(parameter.getParameterType())));
	}
}
           

關于

@ModelAttribute

這塊的使用,參見這裡

總結

本文介紹完了四大類的前面三種類型,其中最為常用的是前兩種類型的使用,希望大家可以掌握,和好好發揮~

相關閱讀

HandlerMethodArgumentResolver:Controller入參自動封裝器(将方法參數parameter解析為參數值)【享學Spring MVC】

從原理層面掌握@ModelAttribute的使用(核心原理篇)【享學Spring MVC】

從原理層面掌握@ModelAttribute的使用(使用篇)【享學Spring MVC】

HandlerMethodArgumentResolver(一):Controller方法入參自動封裝器(将參數parameter解析為值)【享學Spring MVC】

HandlerMethodArgumentResolver(二):Map參數類型和固定參數類型【享學Spring MVC】

HandlerMethodArgumentResolver(三):基于HttpMessageConverter消息轉換器的參數處理器【享學Spring MVC】

關注A哥

Author A哥(YourBatman)
個人站點 www.yourbatman.cn
E-mail [email protected]
微 信 fsx641385712

活躍平台

HandlerMethodArgumentResolver(二):Map參數類型和固定參數類型【享學Spring MVC】關注A哥
HandlerMethodArgumentResolver(二):Map參數類型和固定參數類型【享學Spring MVC】關注A哥
HandlerMethodArgumentResolver(二):Map參數類型和固定參數類型【享學Spring MVC】關注A哥
HandlerMethodArgumentResolver(二):Map參數類型和固定參數類型【享學Spring MVC】關注A哥
HandlerMethodArgumentResolver(二):Map參數類型和固定參數類型【享學Spring MVC】關注A哥
HandlerMethodArgumentResolver(二):Map參數類型和固定參數類型【享學Spring MVC】關注A哥
HandlerMethodArgumentResolver(二):Map參數類型和固定參數類型【享學Spring MVC】關注A哥
HandlerMethodArgumentResolver(二):Map參數類型和固定參數類型【享學Spring MVC】關注A哥
公衆号 BAT的烏托邦(ID:BAT-utopia)
知識星球 BAT的烏托邦
每日文章推薦 每日文章推薦
HandlerMethodArgumentResolver(二):Map參數類型和固定參數類型【享學Spring MVC】關注A哥

繼續閱讀