SpringBoot 中如何自定義參數(shù)解析器堤舒?

在一個 Web 請求中色建,參數(shù)我們無非就是放在地址欄或者請求體中,個別請求可能放在請求頭中舌缤。

放在地址欄中箕戳,我們可以通過如下方式獲取參數(shù):

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

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

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

如果是 JSON 形式陵吸,我們則通過如果如下方式獲取到輸入流玻墅,然后解析成 JSON 字符串,再通過 JSON 工具轉(zhuǎn)為對象:

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

如果參數(shù)放在請求頭中壮虫,我們可以通過如下方式獲劝南帷:

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

如果你用的是 Jsp/Servlet 那一套技術(shù)棧,那么參數(shù)獲取無外乎這幾種方式囚似。

如果用了 SpringMVC 框架剩拢,有的小伙伴們可能會覺得參數(shù)獲取方式太豐富了,各種注解如 @RequestParam饶唤、@RequestBody徐伐、@RequestHeader@PathVariable搬素,參數(shù)可以是 key/value 形式呵晨,也可以是 JSON 形式,非常豐富熬尺!但是摸屠,無論多么豐富,最底層獲取參數(shù)的方式無外乎上面幾種粱哼。

那有小伙伴要問了季二,SpringMVC 到底是怎么樣從 request 中把參數(shù)提取出來直接給我們用的呢?例如下面這個接口:

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

我們都知道 name 參數(shù)是從 HttpServletRequest 中提取出來的揭措,到底是怎么提取出來的胯舷?這就是松哥今天要和大家分享的話題。

1.自定義參數(shù)解析器

為了搞清楚這個問題绊含,我們先來自定義一個參數(shù)解析器看看桑嘶。

自定義參數(shù)解析器需要實現(xiàn) HandlerMethodArgumentResolver 接口,我們先來看看該接口:

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

}

這個接口中就兩個方法:

  • supportsParameter:該方法表示是否啟用這個參數(shù)解析器躬充,返回 true 表示啟用逃顶,返回 false 表示不啟用。
  • resolveArgument:這是具體的解析過程充甚,就是從 request 中取出參數(shù)的過程以政,方法的返回值就對應(yīng)了接口中參數(shù)的值。

自定義參數(shù)解析器只需要實現(xiàn)該接口即可伴找。

假設(shè)我現(xiàn)在有這樣一個需求(實際上在 Spring Security 中獲取當(dāng)前登錄用戶名非常方便盈蛮,這里只是為了該案例而做,勿抬杠):

假設(shè)我現(xiàn)在系統(tǒng)安全框架使用了 Spring Security(對 Spring Security 不熟悉的小伙伴技矮,可以在公眾號江南一點雨后臺回復(fù) ss抖誉,有教程)殊轴,如果我在接口的參數(shù)上添加了 @CurrentUserName 注解,那么該參數(shù)的值就是當(dāng)前登錄的用戶名寸五,像下面這樣:

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

要實現(xiàn)這個功能梳凛,非常 easy,首先我們自定義一個 @CurrentUserName 注解梳杏,如下:

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

這個注解沒啥好解釋的韧拒。

接下來我們自定義參數(shù)解析器 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:如果參數(shù)類型是 String十性,并且參數(shù)上有 @CurrentUserName 注解叛溢,則使用該參數(shù)解析器。
  • resolveArgument:該方法的返回值就是參數(shù)的具體值劲适,當(dāng)前登錄用戶名從 SecurityContextHolder 中獲取即可(具體參數(shù)松哥的 Spring Security 教程楷掉,公號后臺回復(fù) ss)。

最后霞势,我們再將自定義的參數(shù)解析器配置到 HandlerAdapter 中烹植,配置方式如下:

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

至此,就算配置完成了愕贡。

接下來啟動項目草雕,用戶登錄成功后,訪問 /hello 接口固以,就可以看到返回當(dāng)前登錄用戶數(shù)據(jù)了墩虹。

這就是我們自定義的一個參數(shù)類型解析器『┝眨可以看到诫钓,非常 Easy。

在 SpringMVC 中篙螟,默認也有很多 HandlerMethodArgumentResolver 的實現(xiàn)類菌湃,他們處理的問題也都類似,松哥再給大家舉個例子遍略。

2.PrincipalMethodArgumentResolver

如果我們在項目中使用了 Spring Security慢味,我們可以通過如下方式獲取當(dāng)前登錄用戶信息:

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

即直接在當(dāng)前接口的參數(shù)中添加 Principal 類型的參數(shù)即可,該參數(shù)描述了當(dāng)前登錄用戶信息墅冷,這個用過 Spring Security 的小伙伴應(yīng)該都知道(不熟悉 Spring Security 的小伙伴可以在公眾號【江南一點雨】后臺回復(fù) ss)。

那么這個功能是怎么實現(xiàn)的呢或油?當(dāng)然就是 PrincipalMethodArgumentResolver 在起作用了寞忿!

我們一起來看下這個參數(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:這個方法主要是判斷參數(shù)類型是不是 Principal,如果參數(shù)類型是 Principal顶岸,就支持腔彰。
  • resolveArgument:這個方法的邏輯很簡單叫编,首先獲取原生的請求,再從請求中獲取 Principal 對象返回即可霹抛。

是不是很簡單搓逾,有了這個,我們就可以隨時加載到當(dāng)前登錄用戶信息了杯拐。

3.RequestParamMapMethodArgumentResolver

松哥再給大家舉個例子:

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

這個接口很多小伙伴可能都寫過霞篡,使用 Map 去接收前端傳來的參數(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 注解中沒有配置 name 屬性顶滩,就可以使用該參數(shù)解析器余掖。
  • resolveArgument:具體解析分為兩種情況:MultiValueMap 和其他 Map,前者中又分三種情況:MultipartFile礁鲁、Part 或者其他普通請求盐欺,前兩者可以處理文件上傳,第三個就是普通參數(shù)仅醇。如果是普通 Map冗美,則直接獲取到原始請求參數(shù)放到一個 Map 集合中返回即可。

4.小結(jié)

前面和大家聊的都是幾種簡單的情況着憨,還有復(fù)雜的如 PathVariableMethodArgumentResolver 和 RequestParamMethodArgumentResolver 松哥以后再和大家詳細聊饭宾。同時還有一個問題就是這些參數(shù)解析器具體是在哪里調(diào)用的,這個也會在松哥近期的 SpringMVC 源碼解析系列文章中和大家分享货抄,好啦沾瓦,今天周末,就這點簡單的小知識祝大家周末愉快~

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末准谚,一起剝皮案震驚了整個濱河市挫剑,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌柱衔,老刑警劉巖樊破,帶你破解...
    沈念sama閱讀 217,542評論 6 504
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異唆铐,居然都是意外死亡哲戚,警方通過查閱死者的電腦和手機,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 92,822評論 3 394
  • 文/潘曉璐 我一進店門艾岂,熙熙樓的掌柜王于貴愁眉苦臉地迎上來顺少,“玉大人,你說我怎么就攤上這事〈嘌祝” “怎么了梅猿?”我有些...
    開封第一講書人閱讀 163,912評論 0 354
  • 文/不壞的土叔 我叫張陵,是天一觀的道長秒裕。 經(jīng)常有香客問我袱蚓,道長,這世上最難降的妖魔是什么几蜻? 我笑而不...
    開封第一講書人閱讀 58,449評論 1 293
  • 正文 為了忘掉前任喇潘,我火速辦了婚禮,結(jié)果婚禮上入蛆,老公的妹妹穿的比我還像新娘响蓉。我一直安慰自己,他們只是感情好哨毁,可當(dāng)我...
    茶點故事閱讀 67,500評論 6 392
  • 文/花漫 我一把揭開白布枫甲。 她就那樣靜靜地躺著,像睡著了一般扼褪。 火紅的嫁衣襯著肌膚如雪想幻。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 51,370評論 1 302
  • 那天话浇,我揣著相機與錄音脏毯,去河邊找鬼。 笑死幔崖,一個胖子當(dāng)著我的面吹牛食店,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播赏寇,決...
    沈念sama閱讀 40,193評論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼吉嫩,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了嗅定?” 一聲冷哼從身側(cè)響起自娩,我...
    開封第一講書人閱讀 39,074評論 0 276
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎渠退,沒想到半個月后忙迁,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 45,505評論 1 314
  • 正文 獨居荒郊野嶺守林人離奇死亡碎乃,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 37,722評論 3 335
  • 正文 我和宋清朗相戀三年姊扔,在試婚紗的時候發(fā)現(xiàn)自己被綠了。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片梅誓。...
    茶點故事閱讀 39,841評論 1 348
  • 序言:一個原本活蹦亂跳的男人離奇死亡旱眯,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情删豺,我是刑警寧澤,帶...
    沈念sama閱讀 35,569評論 5 345
  • 正文 年R本政府宣布愧怜,位于F島的核電站呀页,受9級特大地震影響,放射性物質(zhì)發(fā)生泄漏拥坛。R本人自食惡果不足惜蓬蝶,卻給世界環(huán)境...
    茶點故事閱讀 41,168評論 3 328
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望猜惋。 院中可真熱鬧丸氛,春花似錦、人聲如沸著摔。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,783評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽谍咆。三九已至禾锤,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間摹察,已是汗流浹背恩掷。 一陣腳步聲響...
    開封第一講書人閱讀 32,918評論 1 269
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留供嚎,地道東北人黄娘。 一個月前我還...
    沈念sama閱讀 47,962評論 2 370
  • 正文 我出身青樓,卻偏偏與公主長得像克滴,于是被迫代替她去往敵國和親逼争。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點故事閱讀 44,781評論 2 354

推薦閱讀更多精彩內(nèi)容