你也許寫過不少Controller,那你可曾和我同樣好奇最初字符串格式的HTTP請求參數如何轉化成類型各異的Controller方法參數?html
引子:假設如今有一個Long型的請求參數,須要轉化爲OffsetDateTime類型的方法參數,請問如何實現?git
首先,讓咱們看一下3種常見的POST請求格式:github
application/x-www-form-urlencoded
: 默認的表單提交格式,不支持文件web
multipart/form-data
: 用於上傳文件,同時也支持普通類型的參數spring
application/json
: 提交JSON格式的raw數據,適用於AJAX請求和REST風格的接口json
對於不一樣類型的請求格式,Spring有着不一樣的轉換過程(從請求參數到方法參數),請看下圖。數組
從上圖能夠看到,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便可。
以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()); } }
以Spring Boot爲例,
1. 繼承com.fasterxml.jackson.databind.JsonDeserializer
和com.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種常見的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)); } }
基於特定屬性的枚舉數據類型轉換器,若是沒法找到,再嘗試用枚舉名進行轉換。
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) ); } }
用於註冊自定義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)); } }
自定義枚舉序列化器,查找特定屬性並進行序列化,若是沒法找到,則序列化爲枚舉名。
@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); } } }
自定義枚舉反序列化器,根據特定屬性進行反序列化,若是沒法找到,再嘗試用枚舉名進行反序列化。
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上的示例工程。