SpringBoot 中如何自定義參數(shù)解析器?
在一個(gè) Web 請(qǐng)求中,參數(shù)我們無(wú)非就是放在地址欄或者請(qǐng)求體中,個(gè)別請(qǐng)求可能放在請(qǐng)求頭中。
放在地址欄中,我們可以通過(guò)如下方式獲取參數(shù):
- String javaboy = request.getParameter("name ");
 
放在請(qǐng)求體中,如果是 key/value 形式,我們可以通過(guò)如下方式獲取參數(shù):
- String javaboy = request.getParameter("name ");
 
如果是 JSON 形式,我們則通過(guò)如果如下方式獲取到輸入流,然后解析成 JSON 字符串,再通過(guò) JSON 工具轉(zhuǎn)為對(duì)象:
- BufferedReader reader = new BufferedReader(new InputStreamReader(request.getInputStream()));
 - String json = reader.readLine();
 - reader.close();
 - User user = new ObjectMapper().readValue(json, User.class);
 
如果參數(shù)放在請(qǐng)求頭中,我們可以通過(guò)如下方式獲?。?/p>
- String javaboy = request.getHeader("name");
 
如果你用的是 Jsp/Servlet 那一套技術(shù)棧,那么參數(shù)獲取無(wú)外乎這幾種方式。
如果用了 SpringMVC 框架,有的小伙伴們可能會(huì)覺(jué)得參數(shù)獲取方式太豐富了,各種注解如 @RequestParam、@RequestBody、@RequestHeader、@PathVariable,參數(shù)可以是 key/value 形式,也可以是 JSON 形式,非常豐富!但是,無(wú)論多么豐富,最底層獲取參數(shù)的方式無(wú)外乎上面幾種。
那有小伙伴要問(wèn)了,SpringMVC 到底是怎么樣從 request 中把參數(shù)提取出來(lái)直接給我們用的呢?例如下面這個(gè)接口:
- @RestController
 - public class HelloController {
 - @GetMapping("/hello")
 - public String hello(String name) {
 - return "hello "+name;
 - }
 - }
 
我們都知道 name 參數(shù)是從 HttpServletRequest 中提取出來(lái)的,到底是怎么提取出來(lái)的?這就是松哥今天要和大家分享的話題。
1.自定義參數(shù)解析器
為了搞清楚這個(gè)問(wèn)題,我們先來(lái)自定義一個(gè)參數(shù)解析器看看。
自定義參數(shù)解析器需要實(shí)現(xiàn) HandlerMethodArgumentResolver 接口,我們先來(lái)看看該接口:
- public interface HandlerMethodArgumentResolver {
 - boolean supportsParameter(MethodParameter parameter);
 - @Nullable
 - Object resolveArgument(MethodParameter parameter, @Nullable ModelAndViewContainer mavContainer,
 - NativeWebRequest webRequest, @Nullable WebDataBinderFactory binderFactory) throws Exception;
 - }
 
這個(gè)接口中就兩個(gè)方法:
- supportsParameter:該方法表示是否啟用這個(gè)參數(shù)解析器,返回 true 表示啟用,返回 false 表示不啟用。
 - resolveArgument:這是具體的解析過(guò)程,就是從 request 中取出參數(shù)的過(guò)程,方法的返回值就對(duì)應(yīng)了接口中參數(shù)的值。
 
自定義參數(shù)解析器只需要實(shí)現(xiàn)該接口即可。
假設(shè)我現(xiàn)在有這樣一個(gè)需求(實(shí)際上在 Spring Security 中獲取當(dāng)前登錄用戶名非常方便,這里只是為了該案例而做,勿抬杠):
假設(shè)我現(xiàn)在系統(tǒng)安全框架使用了 Spring Security(對(duì) Spring Security 不熟悉的小伙伴,可以在公眾號(hào)江南一點(diǎn)雨后臺(tái)回復(fù) ss,有教程),如果我在接口的參數(shù)上添加了 @CurrentUserName 注解,那么該參數(shù)的值就是當(dāng)前登錄的用戶名,像下面這樣:
- @RestController
 - public class HelloController {
 - @GetMapping("/hello")
 - public String hello(@CurrentUserName String name) {
 - return "hello "+name;
 - }
 - }
 
要實(shí)現(xiàn)這個(gè)功能,非常 easy,首先我們自定義一個(gè) @CurrentUserName 注解,如下:
- @Retention(RetentionPolicy.RUNTIME)
 - @Target(ElementType.PARAMETER)
 - public @interface CurrentUserName {
 - }
 
這個(gè)注解沒(méi)啥好解釋的。
接下來(lái)我們自定義參數(shù)解析器 CurrentUserNameHandlerMethodArgumentResolver,如下:
- public class CurrentUserNameHandlerMethodArgumentResolver implements HandlerMethodArgumentResolver {
 - @Override
 - public boolean supportsParameter(MethodParameter parameter) {
 - return parameter.getParameterType().isAssignableFrom(String.class)&¶meter.hasParameterAnnotation(CurrentUserName.class);
 - }
 - @Override
 - public Object resolveArgument(MethodParameter parameter, ModelAndViewContainer mavContainer, NativeWebRequest webRequest, WebDataBinderFactory binderFactory) throws Exception {
 - User user = (User) SecurityContextHolder.getContext().getAuthentication().getPrincipal();
 - return user.getUsername();
 - }
 - }
 
supportsParameter:如果參數(shù)類型是 String,并且參數(shù)上有 @CurrentUserName 注解,則使用該參數(shù)解析器。
resolveArgument:該方法的返回值就是參數(shù)的具體值,當(dāng)前登錄用戶名從 SecurityContextHolder 中獲取即可(具體參數(shù)松哥的 Spring Security 教程,公號(hào)后臺(tái)回復(fù) ss)。
最后,我們?cè)賹⒆远x的參數(shù)解析器配置到 HandlerAdapter 中,配置方式如下:
- @Configuration
 - public class WebConfig implements WebMvcConfigurer {
 - @Override
 - public void addArgumentResolvers(List<HandlerMethodArgumentResolver> resolvers) {
 - resolvers.add(new CurrentUserNameHandlerMethodArgumentResolver());
 - }
 - }
 
至此,就算配置完成了。
接下來(lái)啟動(dòng)項(xiàng)目,用戶登錄成功后,訪問(wèn) /hello 接口,就可以看到返回當(dāng)前登錄用戶數(shù)據(jù)了。
這就是我們自定義的一個(gè)參數(shù)類型解析器??梢钥吹剑浅?Easy。
在 SpringMVC 中,默認(rèn)也有很多 HandlerMethodArgumentResolver 的實(shí)現(xiàn)類,他們處理的問(wèn)題也都類似,松哥再給大家舉個(gè)例子。
2.PrincipalMethodArgumentResolver
如果我們?cè)陧?xiàng)目中使用了 Spring Security,我們可以通過(guò)如下方式獲取當(dāng)前登錄用戶信息:
- @GetMapping("/hello2")
 - public String hello2(Principal principal) {
 - return "hello " + principal.getName();
 - }
 
即直接在當(dāng)前接口的參數(shù)中添加 Principal 類型的參數(shù)即可,該參數(shù)描述了當(dāng)前登錄用戶信息,這個(gè)用過(guò) Spring Security 的小伙伴應(yīng)該都知道(不熟悉 Spring Security 的小伙伴可以在公眾號(hào)【江南一點(diǎn)雨】后臺(tái)回復(fù) ss)。
那么這個(gè)功能是怎么實(shí)現(xiàn)的呢?當(dāng)然就是 PrincipalMethodArgumentResolver 在起作用了!
我們一起來(lái)看下這個(gè)參數(shù)解析器:
- public class PrincipalMethodArgumentResolver implements HandlerMethodArgumentResolver {
 - @Override
 - public boolean supportsParameter(MethodParameter parameter) {
 - return Principal.class.isAssignableFrom(parameter.getParameterType());
 - }
 - @Override
 - public Object resolveArgument(MethodParameter parameter, @Nullable ModelAndViewContainer mavContainer,
 - NativeWebRequest webRequest, @Nullable WebDataBinderFactory binderFactory) throws Exception {
 - HttpServletRequest request = webRequest.getNativeRequest(HttpServletRequest.class);
 - if (request == null) {
 - throw new IllegalStateException("Current request is not of type HttpServletRequest: " + webRequest);
 - }
 - Principal principal = request.getUserPrincipal();
 - if (principal != null && !parameter.getParameterType().isInstance(principal)) {
 - throw new IllegalStateException("Current user principal is not of type [" +
 - parameter.getParameterType().getName() + "]: " + principal);
 - }
 - return principal;
 - }
 - }
 
supportsParameter:這個(gè)方法主要是判斷參數(shù)類型是不是 Principal,如果參數(shù)類型是 Principal,就支持。
resolveArgument:這個(gè)方法的邏輯很簡(jiǎn)單,首先獲取原生的請(qǐng)求,再?gòu)恼?qǐng)求中獲取 Principal 對(duì)象返回即可。
是不是很簡(jiǎn)單,有了這個(gè),我們就可以隨時(shí)加載到當(dāng)前登錄用戶信息了。
3.RequestParamMapMethodArgumentResolver
松哥再給大家舉個(gè)例子:
- @RestController
 - public class HelloController {
 - @PostMapping("/hello")
 - public void hello(@RequestParam MultiValueMap map) throws IOException {
 - //省略...
 - }
 - }
 
這個(gè)接口很多小伙伴可能都寫過(guò),使用 Map 去接收前端傳來(lái)的參數(shù),那么這里用到的參數(shù)解析器就是 RequestParamMapMethodArgumentResolver。
- public class RequestParamMapMethodArgumentResolver implements HandlerMethodArgumentResolver {
 - @Override
 - public boolean supportsParameter(MethodParameter parameter) {
 - RequestParam requestParam = parameter.getParameterAnnotation(RequestParam.class);
 - return (requestParam != null && Map.class.isAssignableFrom(parameter.getParameterType()) &&
 - !StringUtils.hasText(requestParam.name()));
 - }
 - @Override
 - public Object resolveArgument(MethodParameter parameter, @Nullable ModelAndViewContainer mavContainer,
 - NativeWebRequest webRequest, @Nullable WebDataBinderFactory binderFactory) throws Exception {
 - ResolvableType resolvableType = ResolvableType.forMethodParameter(parameter);
 - if (MultiValueMap.class.isAssignableFrom(parameter.getParameterType())) {
 - // MultiValueMap
 - Class<?> valueType = resolvableType.as(MultiValueMap.class).getGeneric(1).resolve();
 - if (valueType == MultipartFile.class) {
 - MultipartRequest multipartRequest = MultipartResolutionDelegate.resolveMultipartRequest(webRequest);
 - return (multipartRequest != null ? multipartRequest.getMultiFileMap() : new LinkedMultiValueMap<>(0));
 - }
 - else if (valueType == Part.class) {
 - HttpServletRequest servletRequest = webRequest.getNativeRequest(HttpServletRequest.class);
 - if (servletRequest != null && MultipartResolutionDelegate.isMultipartRequest(servletRequest)) {
 - Collection<Part> parts = servletRequest.getParts();
 - LinkedMultiValueMap<String, Part> result = new LinkedMultiValueMap<>(parts.size());
 - for (Part part : parts) {
 - result.add(part.getName(), part);
 - }
 - return result;
 - }
 - return new LinkedMultiValueMap<>(0);
 - }
 - else {
 - Map<String, String[]> parameterMap = webRequest.getParameterMap();
 - MultiValueMap<String, String> result = new LinkedMultiValueMap<>(parameterMap.size());
 - parameterMap.forEach((key, values) -> {
 - for (String value : values) {
 - result.add(key, value);
 - }
 - });
 - return result;
 - }
 - }
 - else {
 - // Regular Map
 - Class<?> valueType = resolvableType.asMap().getGeneric(1).resolve();
 - if (valueType == MultipartFile.class) {
 - MultipartRequest multipartRequest = MultipartResolutionDelegate.resolveMultipartRequest(webRequest);
 - return (multipartRequest != null ? multipartRequest.getFileMap() : new LinkedHashMap<>(0));
 - }
 - else if (valueType == Part.class) {
 - HttpServletRequest servletRequest = webRequest.getNativeRequest(HttpServletRequest.class);
 - if (servletRequest != null && MultipartResolutionDelegate.isMultipartRequest(servletRequest)) {
 - Collection<Part> parts = servletRequest.getParts();
 - LinkedHashMap<String, Part> result = CollectionUtils.newLinkedHashMap(parts.size());
 - for (Part part : parts) {
 - if (!result.containsKey(part.getName())) {
 - result.put(part.getName(), part);
 - }
 - }
 - return result;
 - }
 - return new LinkedHashMap<>(0);
 - }
 - else {
 - Map<String, String[]> parameterMap = webRequest.getParameterMap();
 - Map<String, String> result = CollectionUtils.newLinkedHashMap(parameterMap.size());
 - parameterMap.forEach((key, values) -> {
 - if (values.length > 0) {
 - result.put(key, values[0]);
 - }
 - });
 - return result;
 - }
 - }
 - }
 - }
 
supportsParameter:參數(shù)類型是 Map,并且使用了 @RequestParam 注解,并且 @RequestParam 注解中沒(méi)有配置 name 屬性,就可以使用該參數(shù)解析器。
resolveArgument:具體解析分為兩種情況:MultiValueMap 和其他 Map,前者中又分三種情況:MultipartFile、Part 或者其他普通請(qǐng)求,前兩者可以處理文件上傳,第三個(gè)就是普通參數(shù)。如果是普通 Map,則直接獲取到原始請(qǐng)求參數(shù)放到一個(gè) Map 集合中返回即可。
4.小結(jié)
前面和大家聊的都是幾種簡(jiǎn)單的情況,還有復(fù)雜的如 PathVariableMethodArgumentResolver 和 RequestParamMethodArgumentResolver 松哥以后再和大家詳細(xì)聊。同時(shí)還有一個(gè)問(wèn)題就是這些參數(shù)解析器具體是在哪里調(diào)用的,這個(gè)也會(huì)在松哥近期的 SpringMVC 源碼解析系列文章中和大家分享,好啦,今天周末,就這點(diǎn)簡(jiǎn)單的小知識(shí)祝大家周末愉快~
本文轉(zhuǎn)載自微信公眾號(hào)「江南一點(diǎn)雨」,可以通過(guò)以下二維碼關(guān)注。轉(zhuǎn)載本文請(qǐng)聯(lián)系江南一點(diǎn)雨公眾號(hào)。
















 
 
 










 
 
 
 