【Spring】詳解Spring MVC中不一樣格式的POST請求參數的數據類型轉換過程

你也許寫過不少Controller,那你可曾和我同樣好奇最初字符串格式的HTTP請求參數如何轉化成類型各異的Controller方法參數?html

引子:假設如今有一個Long型的請求參數,須要轉化爲OffsetDateTime類型的方法參數,請問如何實現?git

1 常見的POST請求格式

首先,讓咱們看一下3種常見的POST請求格式:github

  • application/x-www-form-urlencoded: 默認的表單提交格式,不支持文件web

  • multipart/form-data: 用於上傳文件,同時也支持普通類型的參數spring

  • application/json: 提交JSON格式的raw數據,適用於AJAX請求和REST風格的接口json

對於不一樣類型的請求格式,Spring有着不一樣的轉換過程(從請求參數到方法參數),請看下圖。數組

2 Spring MVC中的數據類型轉換過程

從上圖能夠看到,Spring在解析請求參數時,會根據請求格式進入到不一樣的轉換流程:app

  • 若是是非raw請求(即包含參數數組),則交由ModelAttributeMethodProcessor處理,ModelAttributeMethodProcessor再調用Spring Converter SPI對請求參數逐個進行轉換。ide

  • 若是是raw請求,則交由RequestResponseBodyMethodProcessor處理,對於JSON格式的請求體,會再調用MappingJackson2HttpMessageConverter,最終經過ObjectMapper完成轉換。spring-boot

*關於Spring Converter SPI的進一步解讀,可參考這篇文章

回到開頭的那個問題,答案就很簡單了。若是是非raw請求,則須要實現一個自定義的Long->OffsetDatetime的Converter;若是是raw請求,則確保ObjectMapper中包含一個Long->OffsetDatetime的反序列化器,註冊Jackon自帶的JavaTimeModule便可。

2.1 如何註冊自定義Converter?

以Spring Boot爲例,

1. 實現org.springframework.core.convert.converter.Converter接口生成一個自定義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. 選擇一個標註@Configuration註解的配置類,繼承org.springframework.web.servlet.config.annotation.WebMvcConfigurerAdapter,而後覆蓋addFormatters方法,註冊自定義Converter。

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

2.2 如何註冊自定義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生成一個自定義Jackson Module,在其中添加自定義的Jackson Deserializer和Serializer。

3. 選擇一個標註@Configuration註解的配置類,經過@Bean註解將自定義的Jackson Module註冊爲Bean,Spring Boot會自動發現和註冊這個Module到默認的ObjectMapper中。

示例代碼參見下一小節。

3 更多示例

3.1 演示Controller

演示3種常見的GET, POST請求參數的數據類型轉換。

@RestController
@Validated
public class VacationController implements IController {

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

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

    /**
     * 轉換POST請求體
     */
    @RequestMapping(value = "/approve", method = RequestMethod.POST)
    public JsonResult<VacationApproval> vacate(@RequestBody @Valid VacationRequest request) {
        return JsonResult.ok(VacationApproval.approve(request));
    }

    /**
     * 轉換POST請求參數
     */
    @RequestMapping(value = "/deny", method = RequestMethod.POST)
    public JsonResult<VacationApproval> deny(@Valid VacationRequest request) {
        return JsonResult.ok(VacationApproval.deny(request));
    }
}

3.2 自定義Enum Converter(用於非raw格式的請求)

基於特定屬性的枚舉數據類型轉換器,若是沒法找到,再嘗試用枚舉名進行轉換。

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格式的請求)

用於註冊自定義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

自定義枚舉序列化器,查找特定屬性並進行序列化,若是沒法找到,則序列化爲枚舉名。

@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

自定義枚舉反序列化器,根據特定屬性進行反序列化,若是沒法找到,再嘗試用枚舉名進行反序列化。

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 參考

相關文章
相關標籤/搜索