你也许写过很多 Controller,那你可曾和我一样好奇最初字符串格式的 HTTP 请求参数如何转化成类型各异的 Controller 方法参数?
引子:假设现在有一个 Long 型的请求参数,需要转化为 OffsetDateTime 类型的方法参数,请问如何实现?
首先,让我们看一下 3 种常见的 POST 请求格式:
: 默认的表单提交格式,不支持文件
- application/x-www-form-urlencoded
: 用于上传文件,同时也支持普通类型的参数
- multipart/form-data
: 提交 JSON 格式的 raw 数据,适用于 AJAX 请求和 REST 风格的接口
- application/json
对于不同类型的请求格式,Spring 有着不同的转换过程(从请求参数到方法参数),请看下图。
从上图可以看到,Spring 在解析请求参数时,会根据请求格式进入到不同的转换流程:
* 关于 Spring Converter SPI 的进一步解读,可参考 这篇文章
回到开头的那个问题,答案就很简单了。如果是非 raw 请求,则需要实现一个自定义的 Long->OffsetDatetime 的 Converter;如果是 raw 请求,则确保 ObjectMapper 中包含一个 Long->OffsetDatetime 的反序列化器,注册 Jackon 自带的 JavaTimeModule 即可。
以 Spring Boot 为例,
1. 实现
接口生成一个自定义 Converter。
- org.springframework.core.convert.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 注解的配置类,继承
,然后覆盖 addFormatters 方法,注册自定义 Converter。
- org.springframework.web.servlet.config.annotation.WebMvcConfigurerAdapter
- @Configuration
- public class WebConfig extends WebMvcConfigurerAdapter {
- @Override
- public void addFormatters(FormatterRegistry registry) {
- registry.addConverter(new OffsetDateTimeConverter());
- }
- }
以 Spring Boot 为例,
1. 继承
和
- com.fasterxml.jackson.databind.JsonDeserializer
生成自定义 Jackson Deserializer 和 Serializer。
- com.fasterxml.jackson.databind.JsonSerializer
2. 继承
生成一个自定义 Jackson Module,在其中添加自定义的 Jackson Deserializer 和 Serializer。
- com.fasterxml.jackson.databind.module.SimpleModule
3. 选择一个标注 @Configuration 注解的配置类,通过 @Bean 注解将自定义的 Jackson Module 注册为 Bean,Spring Boot 会自动发现和注册这个 Module 到默认的 ObjectMapper 中。
示例代码参见下一小节。
演示 3 种常见的 GET, POST 请求参数的数据类型转换。
- @org.springframework.web.bind.annotation.RestController
- @Validated
- public class RestController 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 VacationApproval vacate(@RequestBody @Valid VacationRequest request) {
- return VacationApproval.approve(request);
- }
- /**
- * 转换POST请求参数
- */
- @RequestMapping(value = "/deny", method = RequestMethod.POST)
- public VacationApproval deny(@Valid VacationRequest request) {
- return 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 上的 示例工程 。
来源: http://www.tuicool.com/articles/QFzuqe2