【Spring】詳解Spring MVC中不同格式的POST請(qǐng)求參數(shù)的數(shù)據(jù)類型轉(zhuǎn)換過程

你也許寫過很多Controller,那你可曾和我一樣好奇最初字符串格式的HTTP請(qǐng)求參數(shù)如何轉(zhuǎn)化成類型各異的Controller方法參數(shù)蒸其?

引子:假設(shè)現(xiàn)在有一個(gè)Long型的請(qǐng)求參數(shù),需要轉(zhuǎn)化為OffsetDateTime類型的方法參數(shù)字柠,請(qǐng)問如何實(shí)現(xiàn)绑榴?

1 常見的POST請(qǐng)求格式

首先,讓我們看一下3種常見的POST請(qǐng)求格式:

  • application/x-www-form-urlencoded: 默認(rèn)的表單提交格式蹂午,不支持文件
  • multipart/form-data: 用于上傳文件栏豺,同時(shí)也支持普通類型的參數(shù)
  • application/json: 提交JSON格式的raw數(shù)據(jù),適用于AJAX請(qǐng)求和REST風(fēng)格的接口

對(duì)于不同類型的請(qǐng)求格式豆胸,Spring有著不同的轉(zhuǎn)換過程(從請(qǐng)求參數(shù)到方法參數(shù))奥洼,請(qǐng)看下圖。

2 Spring MVC中的數(shù)據(jù)類型轉(zhuǎn)換過程

從上圖可以看到晚胡,Spring在解析請(qǐng)求參數(shù)時(shí)灵奖,會(huì)根據(jù)請(qǐng)求格式進(jìn)入到不同的轉(zhuǎn)換流程:

  • 如果是非raw請(qǐng)求(即包含參數(shù)數(shù)組),則交由ModelAttributeMethodProcessor處理估盘,ModelAttributeMethodProcessor再調(diào)用Spring Converter SPI對(duì)請(qǐng)求參數(shù)逐個(gè)進(jìn)行轉(zhuǎn)換瓷患。
  • 如果是raw請(qǐng)求,則交由RequestResponseBodyMethodProcessor處理遣妥,對(duì)于JSON格式的請(qǐng)求體擅编,會(huì)再調(diào)用MappingJackson2HttpMessageConverter,最終通過ObjectMapper完成轉(zhuǎn)換箫踩。

<i>*關(guān)于Spring Converter SPI的進(jìn)一步解讀爱态,可參考這篇文章</i>

回到開頭的那個(gè)問題,答案就很簡(jiǎn)單了境钟。如果是非raw請(qǐng)求锦担,則需要實(shí)現(xiàn)一個(gè)自定義的Long->OffsetDatetime的Converter;如果是raw請(qǐng)求慨削,則確保ObjectMapper中包含一個(gè)Long->OffsetDatetime的反序列化器洞渔,注冊(cè)Jackon自帶的JavaTimeModule即可。

2.1 如何注冊(cè)自定義Converter缚态?

以Spring Boot為例磁椒,

1. 實(shí)現(xiàn)org.springframework.core.convert.converter.Converter接口生成一個(gè)自定義Converter。

public class OffsetDateTimeConverter implements Converter<String, OffsetDateTime> {

    @Override
    public OffsetDateTime convert(String source) {
        if (!NumberUtils.isNumber(source)) {
            return null;
        }

        Long milli = NumberUtils.createLong(source);
        return OffsetDateTime.ofInstant(Instant.ofEpochMilli(milli), systemDefault());
    }
}

2. 選擇一個(gè)標(biāo)注@Configuration注解的配置類猿规,繼承org.springframework.web.servlet.config.annotation.WebMvcConfigurerAdapter衷快,然后覆蓋addFormatters方法,注冊(cè)自定義Converter姨俩。

@Configuration
public class WebConfig extends WebMvcConfigurerAdapter {
    @Override
    public void addFormatters(FormatterRegistry registry) {
        registry.addConverter(new OffsetDateTimeConverter());
    }
}

2.2 如何注冊(cè)自定義Jackson Deserializer和Serializer蘸拔?

以Spring Boot為例师郑,

1. 繼承com.fasterxml.jackson.databind.JsonDeserializercom.fasterxml.jackson.databind.JsonSerializer生成自定義Jackson Deserializer和Serializer。

2. 繼承com.fasterxml.jackson.databind.module.SimpleModule生成一個(gè)自定義Jackson Module调窍,在其中添加自定義的Jackson Deserializer和Serializer宝冕。

3. 選擇一個(gè)標(biāo)注@Configuration注解的配置類,通過@Bean注解將自定義的Jackson Module注冊(cè)為Bean邓萨,Spring Boot會(huì)自動(dòng)發(fā)現(xiàn)和注冊(cè)這個(gè)Module到默認(rèn)的ObjectMapper中地梨。

示例代碼參見下一小節(jié)。

3 更多示例

3.1 演示Controller

演示3種常見的GET, POST請(qǐng)求參數(shù)的數(shù)據(jù)類型轉(zhuǎn)換缔恳。

@org.springframework.web.bind.annotation.RestController
@Validated
public class RestController implements IController {

    private static final List<DayOfWeek> WEEKENDS = Lists.newArrayList(DayOfWeek.SATURDAY, DayOfWeek.SUNDAY);

    /**
     * 轉(zhuǎn)換GET請(qǐng)求參數(shù)
     */
    @RequestMapping(value = "/isWeekend", method = RequestMethod.GET)
    public JsonResult<Boolean> isWeekend(@Valid VacationRequest request) {
        return JsonResult.ok(WEEKENDS.contains(request.getStart().getDayOfWeek()));
    }

    /**
     * 轉(zhuǎn)換POST請(qǐng)求體
     */
    @RequestMapping(value = "/approve", method = RequestMethod.POST)
    public VacationApproval vacate(@RequestBody @Valid VacationRequest request) {
        return VacationApproval.approve(request);
    }

    /**
     * 轉(zhuǎn)換POST請(qǐng)求參數(shù)
     */
    @RequestMapping(value = "/deny", method = RequestMethod.POST)
    public VacationApproval deny(@Valid VacationRequest request) {
        return VacationApproval.deny(request);
    }
}

3.2 自定義Enum Converter(用于非raw格式的請(qǐng)求)

基于特定屬性的枚舉數(shù)據(jù)類型轉(zhuǎn)換器宝剖,如果無(wú)法找到,再嘗試用枚舉名進(jìn)行轉(zhuǎn)換歉甚。

public static class CustomEnumConverter<T extends Enum<T>> implements Converter<String, T> {

    private Class<T> enumCls;
    private String prop;

    /**
     * @param enumCls 枚舉類型
     * @param prop 屬性名
     */
    public CustomEnumConverter(Class<T> enumCls, String prop) {
        this.enumCls = enumCls;
        this.prop = prop;
    }

    @Override
    public T convert(String source) {
        if (StringUtils.isEmpty(source)) {
            return null;
        }
        return Enums.getEnum(enumCls, prop, source).orElseGet(() ->
                Stream.of(enumCls.getEnumConstants())
                        .filter(e -> e.name().equals(source))
                        .findFirst().orElse(null)
        );
    }
}

3.3 自定義Module(用于raw格式的請(qǐng)求)

用于注冊(cè)自定義Enum Serializer和Enum Deserializer万细。

public class CustomEnumModule extends SimpleModule {

    /**
     * @param prop 屬性名
     */
    public CustomEnumModule(@NotNull String prop){
        Asserts.notBlank(prop);

        addDeserializer(Enum.class, new CustomEnumDeserializer(prop));
        addSerializer(Enum.class, new CustomEnumSerializer(prop));
    }
}

3.3.1 自定義Enum Serializer

自定義枚舉序列化器,查找特定屬性并進(jìn)行序列化纸泄,如果無(wú)法找到赖钞,則序列化為枚舉名。

@Slf4j
public class CustomEnumSerializer extends JsonSerializer<Enum> {

    private String prop;

    /**
     * @param prop 屬性名
     */
    public CustomEnumSerializer(@NotNull String prop) {
        Asserts.notBlank(prop);

        this.prop = prop;
    }

    @Override
    public void serialize(Enum value, JsonGenerator gen, SerializerProvider serializers) throws IOException {
        if (value == null) {
            gen.writeNull();
            return;
        }
        try {
            PropertyDescriptor pd = getPropertyDescriptor(value, prop);
            if (pd == null || pd.getReadMethod() == null) {
                gen.writeString(value.name());
                return;
            }
            Method m = pd.getReadMethod();
            m.setAccessible(true);
            gen.writeObject(m.invoke(value));
        } catch (IllegalAccessException | InvocationTargetException | NoSuchMethodException e) {
            throw new CommonException(e);
        }
    }
}

3.3.2 自定義Enum Deserializer

自定義枚舉反序列化器聘裁,根據(jù)特定屬性進(jìn)行反序列化雪营,如果無(wú)法找到,再嘗試用枚舉名進(jìn)行反序列化衡便。

public class CustomEnumDeserializer extends JsonDeserializer<Enum> implements ContextualDeserializer {

    @Setter
    private Class<Enum> enumCls;

    private String prop;

    /**
     * @param prop 屬性名
     */
    public CustomEnumDeserializer(@NotNull String prop) {
        Asserts.notBlank(prop);

        this.prop = prop;
    }

    @Override
    public Enum deserialize(JsonParser parser, DeserializationContext ctx) throws IOException {
        String text = parser.getText();
        return Enums.getEnum(enumCls, prop, text).orElseGet(() ->
                Stream.of(enumCls.getEnumConstants())
                        .filter(e -> e.name().equals(text))
                        .findFirst().orElse(null)
        );
    }

    @Override
    public JsonDeserializer createContextual(DeserializationContext ctx, BeanProperty property) throws JsonMappingException {
        Class rawCls = ctx.getContextualType().getRawClass();
        Asserts.isTrue(rawCls.isEnum());

        Class<Enum> enumCls = (Class<Enum>) rawCls;
        CustomEnumDeserializer clone = new CustomEnumDeserializer(prop);
        clone.setEnumCls(enumCls);
        return clone;
    }
}

完整代碼可以參見我在GitHub上的示例工程献起。

4 參考

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個(gè)濱河市砰诵,隨后出現(xiàn)的幾起案子征唬,更是在濱河造成了極大的恐慌,老刑警劉巖茁彭,帶你破解...
    沈念sama閱讀 211,123評(píng)論 6 490
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場(chǎng)離奇詭異扶歪,居然都是意外死亡理肺,警方通過查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 90,031評(píng)論 2 384
  • 文/潘曉璐 我一進(jìn)店門善镰,熙熙樓的掌柜王于貴愁眉苦臉地迎上來妹萨,“玉大人,你說我怎么就攤上這事炫欺『跬辏” “怎么了?”我有些...
    開封第一講書人閱讀 156,723評(píng)論 0 345
  • 文/不壞的土叔 我叫張陵品洛,是天一觀的道長(zhǎng)树姨。 經(jīng)常有香客問我摩桶,道長(zhǎng),這世上最難降的妖魔是什么帽揪? 我笑而不...
    開封第一講書人閱讀 56,357評(píng)論 1 283
  • 正文 為了忘掉前任硝清,我火速辦了婚禮,結(jié)果婚禮上转晰,老公的妹妹穿的比我還像新娘芦拿。我一直安慰自己,他們只是感情好查邢,可當(dāng)我...
    茶點(diǎn)故事閱讀 65,412評(píng)論 5 384
  • 文/花漫 我一把揭開白布蔗崎。 她就那樣靜靜地躺著,像睡著了一般扰藕。 火紅的嫁衣襯著肌膚如雪蚁趁。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 49,760評(píng)論 1 289
  • 那天实胸,我揣著相機(jī)與錄音他嫡,去河邊找鬼。 笑死庐完,一個(gè)胖子當(dāng)著我的面吹牛钢属,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播门躯,決...
    沈念sama閱讀 38,904評(píng)論 3 405
  • 文/蒼蘭香墨 我猛地睜開眼淆党,長(zhǎng)吁一口氣:“原來是場(chǎng)噩夢(mèng)啊……” “哼!你這毒婦竟也來了讶凉?” 一聲冷哼從身側(cè)響起染乌,我...
    開封第一講書人閱讀 37,672評(píng)論 0 266
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤,失蹤者是張志新(化名)和其女友劉穎懂讯,沒想到半個(gè)月后荷憋,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 44,118評(píng)論 1 303
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡褐望,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 36,456評(píng)論 2 325
  • 正文 我和宋清朗相戀三年勒庄,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片瘫里。...
    茶點(diǎn)故事閱讀 38,599評(píng)論 1 340
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡实蔽,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出谨读,到底是詐尸還是另有隱情局装,我是刑警寧澤,帶...
    沈念sama閱讀 34,264評(píng)論 4 328
  • 正文 年R本政府宣布,位于F島的核電站铐尚,受9級(jí)特大地震影響拨脉,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜塑径,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 39,857評(píng)論 3 312
  • 文/蒙蒙 一女坑、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧统舀,春花似錦匆骗、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,731評(píng)論 0 21
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)。三九已至闷串,卻和暖如春瓮钥,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背烹吵。 一陣腳步聲響...
    開封第一講書人閱讀 31,956評(píng)論 1 264
  • 我被黑心中介騙來泰國(guó)打工碉熄, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留,地道東北人肋拔。 一個(gè)月前我還...
    沈念sama閱讀 46,286評(píng)論 2 360
  • 正文 我出身青樓锈津,卻偏偏與公主長(zhǎng)得像,于是被迫代替她去往敵國(guó)和親凉蜂。 傳聞我的和親對(duì)象是個(gè)殘疾皇子琼梆,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 43,465評(píng)論 2 348

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

  • Spring Cloud為開發(fā)人員提供了快速構(gòu)建分布式系統(tǒng)中一些常見模式的工具(例如配置管理,服務(wù)發(fā)現(xiàn)窿吩,斷路器茎杂,智...
    卡卡羅2017閱讀 134,628評(píng)論 18 139
  • Spring Boot 參考指南 介紹 轉(zhuǎn)載自:https://www.gitbook.com/book/qbgb...
    毛宇鵬閱讀 46,773評(píng)論 6 342
  • Spring MVC會(huì)根據(jù)請(qǐng)求方法簽名不同,將請(qǐng)求消息中信息以一定方式轉(zhuǎn)換并綁定到請(qǐng)求方法的參數(shù)中纫雁。 1.數(shù)據(jù)綁定...
    落葉飛逝的戀閱讀 2,384評(píng)論 0 4
  • application的配置屬性煌往。 這些屬性是否生效取決于對(duì)應(yīng)的組件是否聲明為Spring應(yīng)用程序上下文里的Bea...
    新簽名閱讀 5,358評(píng)論 1 27
  • 這些屬性是否生效取決于對(duì)應(yīng)的組件是否聲明為 Spring 應(yīng)用程序上下文里的 Bean(基本是自動(dòng)配置的),為一個(gè)...
    發(fā)光的魚閱讀 1,421評(píng)論 0 14