詳解如何在SpringBoot中自定義參數解析器

前言

在一個 Web 請求中,參數我們無非就是放在地址欄或者請求體中,個別請求可能放在請求頭中。

放在地址欄中,我們可以通過如下方式獲取參數:

String javaboy = request.getParameter("name ");

放在請求體中,如果是 key/value 形式,我們可以通過如下方式獲取參數:

String javaboy = request.getParameter("name ");

如果是 JSON 形式,我們則通過如果如下方式獲取到輸入流,然後解析成 JSON 字符串,再通過 JSON 工具轉為對象:

BufferedReader reader = new BufferedReader(new InputStreamReader(request.getInputStream()));
String json = reader.readLine();
reader.close();
User user = new ObjectMapper().readValue(json, User.class);

如果參數放在請求頭中,我們可以通過如下方式獲取:

String javaboy = request.getHeader("name");

如果你用的是 Jsp/Servlet 那一套技術棧,那麼參數獲取無外乎這幾種方式。

如果用瞭 SpringMVC 框架,有的小夥伴們可能會覺得參數獲取方式太豐富瞭,各種註解如 @RequestParam@RequestBody@RequestHeader@PathVariable,參數可以是 key/value 形式,也可以是 JSON 形式,非常豐富!但是,無論多麼豐富,最底層獲取參數的方式無外乎上面幾種。

那有小夥伴要問瞭,SpringMVC 到底是怎麼樣從 request 中把參數提取出來直接給我們用的呢?例如下面這個接口:

@RestController
public class HelloController {
    @GetMapping("/hello")
    public String hello(String name) {
        return "hello "+name;
    }
}

我們都知道 name 參數是從 HttpServletRequest 中提取出來的,到底是怎麼提取出來的?這就是松哥今天要和大傢分享的話題。

1.自定義參數解析器

為瞭搞清楚這個問題,我們先來自定義一個參數解析器看看。

自定義參數解析器需要實現 HandlerMethodArgumentResolver 接口,我們先來看看該接口:

public interface HandlerMethodArgumentResolver {
 boolean supportsParameter(MethodParameter parameter);
 @Nullable
 Object resolveArgument(MethodParameter parameter, @Nullable ModelAndViewContainer mavContainer,
   NativeWebRequest webRequest, @Nullable WebDataBinderFactory binderFactory) throws Exception;

}

這個接口中就兩個方法:

  • supportsParameter:該方法表示是否啟用這個參數解析器,返回 true 表示啟用,返回 false 表示不啟用。
  • resolveArgument:這是具體的解析過程,就是從 request 中取出參數的過程,方法的返回值就對應瞭接口中參數的值。

自定義參數解析器隻需要實現該接口即可。

假設我現在有這樣一個需求(實際上在 Spring Security 中獲取當前登錄用戶名非常方便,這裡隻是為瞭該案例而做,勿抬杠):

假設我現在系統安全框架使用瞭 Spring Security,如果我在接口的參數上添加瞭 @CurrentUserName 註解,那麼該參數的值就是當前登錄的用戶名,像下面這樣:

@RestController
public class HelloController {
    @GetMapping("/hello")
    public String hello(@CurrentUserName String name) {
        return "hello "+name;
    }
}

要實現這個功能,非常 easy,首先我們自定義一個 @CurrentUserName 註解,如下:

@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.PARAMETER)
public @interface CurrentUserName {
}

這個註解沒啥好解釋的。

接下來我們自定義參數解析器 CurrentUserNameHandlerMethodArgumentResolver,如下:

public class CurrentUserNameHandlerMethodArgumentResolver implements HandlerMethodArgumentResolver {
    @Override
    public boolean supportsParameter(MethodParameter parameter) {
        return parameter.getParameterType().isAssignableFrom(String.class)&&parameter.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:如果參數類型是 String,並且參數上有 @CurrentUserName 註解,則使用該參數解析器。
  • resolveArgument:該方法的返回值就是參數的具體值,當前登錄用戶名從 SecurityContextHolder 中獲取即可

最後,我們再將自定義的參數解析器配置到 HandlerAdapter 中,配置方式如下:

@Configuration
public class WebConfig implements WebMvcConfigurer {
    @Override
    public void addArgumentResolvers(List<HandlerMethodArgumentResolver> resolvers) {
        resolvers.add(new CurrentUserNameHandlerMethodArgumentResolver());
    }
}

至此,就算配置完成瞭。

接下來啟動項目,用戶登錄成功後,訪問 /hello 接口,就可以看到返回當前登錄用戶數據瞭。

這就是我們自定義的一個參數類型解析器。可以看到,非常 Easy。

在 SpringMVC 中,默認也有很多 HandlerMethodArgumentResolver 的實現類,他們處理的問題也都類似,松哥再給大傢舉個例子。

2.PrincipalMethodArgumentResolver

如果我們在項目中使用瞭 Spring Security,我們可以通過如下方式獲取當前登錄用戶信息:

@GetMapping("/hello2")
public String hello2(Principal principal) {
    return "hello " + principal.getName();
}

即直接在當前接口的參數中添加 Principal 類型的參數即可,該參數描述瞭當前登錄用戶信息,這個用過 Spring Security 的小夥伴應該都知道

那麼這個功能是怎麼實現的呢?當然就是 PrincipalMethodArgumentResolver 在起作用瞭!

我們一起來看下這個參數解析器:

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:這個方法主要是判斷參數類型是不是 Principal,如果參數類型是 Principal,就支持。
  • resolveArgument:這個方法的邏輯很簡單,首先獲取原生的請求,再從請求中獲取 Principal 對象返回即可。

是不是很簡單,有瞭這個,我們就可以隨時加載到當前登錄用戶信息瞭。

3.RequestParamMapMethodArgumentResolver

松哥再給大傢舉個例子:

@RestController
public class HelloController {
    @PostMapping("/hello")
    public void hello(@RequestParam MultiValueMap map) throws IOException {
        //省略...
    }
}

這個接口很多小夥伴可能都寫過,使用 Map 去接收前端傳來的參數,那麼這裡用到的參數解析器就是 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:參數類型是 Map,並且使用瞭 @RequestParam 註解,並且 @RequestParam 註解中沒有配置 name 屬性,就可以使用該參數解析器。
  • resolveArgument:具體解析分為兩種情況:MultiValueMap 和其他 Map,前者中又分三種情況:MultipartFile、Part 或者其他普通請求,前兩者可以處理文件上傳,第三個就是普通參數。如果是普通 Map,則直接獲取到原始請求參數放到一個 Map 集合中返回即可。

4.小結

前面和大傢聊的都是幾種簡單的情況,還有復雜的如 PathVariableMethodArgumentResolver 和 RequestParamMethodArgumentResolver 松哥以後再和大傢詳細聊。

以上就是詳解如何在SpringBoot中自定義參數解析器的詳細內容,更多關於SpringBoot自定義參數解析器的資料請關註LevelAH其它相關文章!

推薦閱讀: