Spring MVC 利用自定义媒体类型控制使用自定义消息转换器

目录

  1. 问题描述
  2. Spring MVC 如何处理返回值
  3. 自定义消息转换器

问题描述

由于存在一些技术债,需要服务层返回的json即能支持驼峰(默认支持),又能支持下划线。

Spring MVC配置的RestController默认使用jackson将结果转成驼峰格式的json,所以需要自定义一个在符合某个条件下返回的json转成下划线格式。

Spring MVC 如何处理返回值

Spring MVC通过HttpMessageConverter来处理数据流和对象的转换,HttpMessageConverter定义了如下方法:

  • boolean canRead(Class<?> clazz, MediaType mediaType):是否支持该媒体类型将参数转成对象
  • boolean canWrite(Class<?> clazz, MediaType mediaType):是否支持该媒体类型将对象输出
  • List getSupportedMediaTypes():转换器支持的媒体类型
  • T read(Class<? extends T> clazz, HttpInputMessage inputMessage)
    throws IOException, HttpMessageNotReadableException:输入转成对象
  • void write(T t, MediaType contentType, HttpOutputMessage outputMessage)
    throws IOException, HttpMessageNotWritableException:对象输出

Spring MVC根据Content-type的媒体类型来指定流转对象的解析器,Accept的媒体类型来决定对象转成流的转换器。选择对象转成流的转换器的代码如下AbstractMessageConverterMethodProcessor.writeWithMessageConverters:

    
    protected <T> void writeWithMessageConverters(T value, MethodParameter returnType,
        ServletServerHttpRequest inputMessage, ServletServerHttpResponse outputMessage)
        throws IOException, HttpMediaTypeNotAcceptableException, HttpMessageNotWritableException {
    
      Object outputValue;
      Class<?> valueType;
      Type declaredType;
    
      if (value instanceof CharSequence) {
        outputValue = value.toString();
        valueType = String.class;
        declaredType = String.class;
      }
      else {
        outputValue = value;
        valueType = getReturnValueType(outputValue, returnType);
        declaredType = getGenericType(returnType);
      }
    
      HttpServletRequest request = inputMessage.getServletRequest();
      // Accept的媒体类型
      List<MediaType> requestedMediaTypes = getAcceptableMediaTypes(request);
      // 转换器中支持媒体类型的转换器
      List<MediaType> producibleMediaTypes = getProducibleMediaTypes(request, valueType, declaredType);
    
      if (outputValue != null && producibleMediaTypes.isEmpty()) {
        throw new IllegalArgumentException("No converter found for return value of type: " + valueType);
      }
      // 能够处理该媒体类型的转换器
      Set<MediaType> compatibleMediaTypes = new LinkedHashSet<MediaType>();
      for (MediaType requestedType : requestedMediaTypes) {
        for (MediaType producibleType : producibleMediaTypes) {
          if (requestedType.isCompatibleWith(producibleType)) {
            compatibleMediaTypes.add(getMostSpecificMediaType(requestedType, producibleType));
          }
        }
      }
      if (compatibleMediaTypes.isEmpty()) {
        if (outputValue != null) {
          throw new HttpMediaTypeNotAcceptableException(producibleMediaTypes);
        }
        return;
      }
    
      List<MediaType> mediaTypes = new ArrayList<MediaType>(compatibleMediaTypes);
      MediaType.sortBySpecificityAndQuality(mediaTypes);
      // 最终处理的媒体类型
      MediaType selectedMediaType = null;
      for (MediaType mediaType : mediaTypes) {
        if (mediaType.isConcrete()) {
          selectedMediaType = mediaType;
          break;
        }
        else if (mediaType.equals(MediaType.ALL) || mediaType.equals(MEDIA_TYPE_APPLICATION)) {
          selectedMediaType = MediaType.APPLICATION_OCTET_STREAM;
          break;
        }
      }
    
      if (selectedMediaType != null) {
        selectedMediaType = selectedMediaType.removeQualityValue();
        // 选择初始化中支持该媒体类型的转换器输出
        for (HttpMessageConverter<?> messageConverter : this.messageConverters) {
          if (messageConverter instanceof GenericHttpMessageConverter) {
            if (((GenericHttpMessageConverter) messageConverter).canWrite(
                declaredType, valueType, selectedMediaType)) {
              outputValue = (T) getAdvice().beforeBodyWrite(outputValue, returnType, selectedMediaType,
                  (Class<? extends HttpMessageConverter<?>>) messageConverter.getClass(),
                  inputMessage, outputMessage);
              if (outputValue != null) {
                addContentDispositionHeader(inputMessage, outputMessage);
                ((GenericHttpMessageConverter) messageConverter).write(
                    outputValue, declaredType, selectedMediaType, outputMessage);
                if (logger.isDebugEnabled()) {
                  logger.debug("Written [" + outputValue + "] as \"" + selectedMediaType +
                      "\" using [" + messageConverter + "]");
                }
              }
              return;
            }
          }
          else if (messageConverter.canWrite(valueType, selectedMediaType)) {
            outputValue = (T) getAdvice().beforeBodyWrite(outputValue, returnType, selectedMediaType,
                (Class<? extends HttpMessageConverter<?>>) messageConverter.getClass(),
                inputMessage, outputMessage);
            if (outputValue != null) {
              addContentDispositionHeader(inputMessage, outputMessage);
              ((HttpMessageConverter) messageConverter).write(outputValue, selectedMediaType, outputMessage);
              if (logger.isDebugEnabled()) {
                logger.debug("Written [" + outputValue + "] as \"" + selectedMediaType +
                    "\" using [" + messageConverter + "]");
              }
            }
            return;
          }
        }
      }
    
      if (outputValue != null) {
        throw new HttpMediaTypeNotAcceptableException(this.allSupportedMediaTypes);
      }
    }

其中常用的@RestController将对象转json是通过MappingJackson2HttpMessageConverter实现的。不做配置情况下,返回的json格式默认是驼峰样式(只有健是如此,值不做处理)。

Spring MVC项目在初始化阶段就会初始化MappingJackson2HttpMessageConverter,具体代码位于WebMvcConfigurationSupport中:

    /**
     * Provides access to the shared {@link HttpMessageConverter}s used by the
     * {@link RequestMappingHandlerAdapter} and the
     * {@link ExceptionHandlerExceptionResolver}.
     * This method cannot be overridden.
     * Use {@link #configureMessageConverters(List)} instead.
     * Also see {@link #addDefaultHttpMessageConverters(List)} that can be
     * used to add default message converters.
     */
    protected final List<HttpMessageConverter<?>> getMessageConverters() {
      // 如果没有消息转换器则初始化消息转换器
      if (this.messageConverters == null) {
        this.messageConverters = new ArrayList<HttpMessageConverter<?>>();
        // 配置消息转换器。本身没有实现,留给定制初始化消息转换器
        configureMessageConverters(this.messageConverters);
        // 如果依然没有消息转换器,那么添加默认转换器
        if (this.messageConverters.isEmpty()) {
          addDefaultHttpMessageConverters(this.messageConverters);
        }
        // 扩展消息转换器。没有实现,留给定制使用
        extendMessageConverters(this.messageConverters);
      }
      return this.messageConverters;
    }
    
    /**
     * Adds a set of default HttpMessageConverter instances to the given list.
     * Subclasses can call this method from {@link #configureMessageConverters(List)}.
     * @param messageConverters the list to add the default message converters to
     */
    protected final void addDefaultHttpMessageConverters(List<HttpMessageConverter<?>> messageConverters) {
      // 字符串转换工具。如果媒体类型是 text/plain则使用该转换工具,默认字符集是ISO-8859-1
      StringHttpMessageConverter stringConverter = new StringHttpMessageConverter();
      stringConverter.setWriteAcceptCharset(false);
    
      // 字节数组转换工具。如果媒体类型是 application/octet-stream,那么该转换工具
      messageConverters.add(new ByteArrayHttpMessageConverter());
      messageConverters.add(stringConverter);
      messageConverters.add(new ResourceHttpMessageConverter());
      messageConverters.add(new SourceHttpMessageConverter<Source>());
      messageConverters.add(new AllEncompassingFormHttpMessageConverter());
    
      if (romePresent) {
        messageConverters.add(new AtomFeedHttpMessageConverter());
        messageConverters.add(new RssChannelHttpMessageConverter());
      }
    
      if (jackson2XmlPresent) {
        messageConverters.add(new MappingJackson2XmlHttpMessageConverter(
            Jackson2ObjectMapperBuilder.xml().applicationContext(this.applicationContext).build()));
      }
      else if (jaxb2Present) {
        messageConverters.add(new Jaxb2RootElementHttpMessageConverter());
      }
      // 如果WebMvcConfigurationSupport.class.getClassLoader()可以加载到 com.fasterxml.jackson.databind.ObjectMapper 和 com.fasterxml.jackson.core.JsonGenerator 那么启用MappingJackson2HttpMessageConverter转换器。即如果不自定义configureMessageConverters,那么MappingJackson2HttpMessageConverter是默认启动的转换器
      if (jackson2Present) {
        messageConverters.add(new MappingJackson2HttpMessageConverter(
            Jackson2ObjectMapperBuilder.json().applicationContext(this.applicationContext).build()));
      }
      else if (gsonPresent) {
        messageConverters.add(new GsonHttpMessageConverter());
      }
    }

如果只是单独希望转成下划线样式,那么可以直接重写extendMessageConverters方法,获取MappingJackson2HttpMessageConverter,然后属性名称命名策略改成下划线方式。

自定义消息转换器

由于服务接口众多,并且需要同时支持驼峰和下划线,所以决定自定义一个媒体类型比如:application/underscore,如果调用方指定是Accept:application/underscore,那么由自定义模版消息将结果转成下划线格式的json。

自定义媒体消息:

    public class CustomMediaType extends MediaType {
         public CustomMediaType(String type, String subtype) {
             super(type, subtype);
         }
    
         /**
          * Public constant media type for {@code application/underscore}.
          * @see #APPLICATION_UNDERSCORE_UTF8
          */
         public final static MediaType APPLICATION_UNDERSCORE;
    
         /**
          * Public constant media type for {@code application/underscore;charset=UTF-8}.
          */
         public final static MediaType APPLICATION_UNDERSCORE_UTF8;
    
         /**
          * A String equivalent of {@link CustomMediaType#APPLICATION_UNDERSCORE}.
          * @see #APPLICATION_UNDERSCORE_UTF8_VALUE
          */
         public final static String APPLICATION_UNDERSCORE_VALUE = "application/underscore";
    
         /**
          * A String equivalent of {@link CustomMediaType#APPLICATION_UNDERSCORE_UTF8}.
          * @see #APPLICATION_UNDERSCORE_VALUE
          */
         public final static String APPLICATION_UNDERSCORE_UTF8_VALUE = "application/underscore;charset=UTF-8";
    
         static {
             APPLICATION_UNDERSCORE = valueOf(APPLICATION_UNDERSCORE_VALUE);
             APPLICATION_UNDERSCORE_UTF8 = valueOf(APPLICATION_UNDERSCORE_UTF8_VALUE);
         }
     } 

自定义消息转换工具:

    public class UnderScoreConverter extends AbstractJackson2HttpMessageConverter {
    
    
         public UnderScoreConverter(ObjectMapper objectMapper) {
             // 初始化消息转换器,该转换器支持自定义的媒体类型
             super(objectMapper, CustomMediaType.APPLICATION_UNDERSCORE, CustomMediaType.APPLICATION_UNDERSCORE_UTF8);
    
             // 自定义解析方式
             objectMapper.setSerializerFactory(objectMapper.getSerializerFactory()
                     .withSerializerModifier(new MyBeanSerializerModifier()));
             // 自定义日期解析方式(日期建议还是使用时间戳,再由调用方控制展示样式)
             objectMapper.setDateFormat(new SimpleDateFormat("yyyy-MM-dd HH:mm:ss")).setTimeZone(TimeZone.getTimeZone("GMT+8"));
             // 属性命名策略选用下划线方式
             objectMapper.setPropertyNamingStrategy(PropertyNamingStrategy.SNAKE_CASE);
             // 允许包含控制字符串结束等特殊字符
             objectMapper.configure(JsonParser.Feature.ALLOW_UNQUOTED_CONTROL_CHARS, true);
         }
    
         class MyBeanSerializerModifier extends BeanSerializerModifier {
    
             @Override
             public List<BeanPropertyWriter> changeProperties(SerializationConfig config,
                                                              BeanDescription beanDesc,
                                                              List<BeanPropertyWriter> beanProperties) {
                 // 循环所有的beanPropertyWriter
                 for (int i = 0; i < beanProperties.size(); i++) {
                     BeanPropertyWriter writer = beanProperties.get(i);
                     // 判断字段的类型,如果是数组或集合则注册nullSerializer
    
                     // 注册null转空数组
                     if (isArrayType(writer)) {
                         writer.assignNullSerializer(new NullArrayJsonSerializer());
                     } else if (isMap(writer)) {
                         writer.assignNullSerializer(new NullObjectJsonSerializer());
                     } else if (isObjectType(writer)) {
                         writer.assignNullSerializer(new NullObjectJsonSerializer());
                     }
                 }
                 return beanProperties;
             }
    
             /**
              * 是否是数组
              */
             private boolean isArrayType(BeanPropertyWriter writer) {
                 Class<?> clazz = writer.getType().getRawClass();
                 return clazz.isArray() || Collection.class.isAssignableFrom(clazz);
             }
    
             /**
              * 是否是map
              *
              * @param writer 属性编辑器
              * @return
              */
             private boolean isMap(BeanPropertyWriter writer) {
                 Class<?> clazz = writer.getType().getRawClass();
                 return Map.class.isAssignableFrom(clazz);
             }
    
             /**
              * 是否是String
              */
             private boolean isStringType(BeanPropertyWriter writer) {
                 Class<?> clazz = writer.getType().getRawClass();
                 return CharSequence.class.isAssignableFrom(clazz) || Character.class.isAssignableFrom(clazz);
             }
    
             /**
              * 是否是数值类型
              */
             private boolean isNumberType(BeanPropertyWriter writer) {
                 Class<?> clazz = writer.getType().getRawClass();
                 return Number.class.isAssignableFrom(clazz);
             }
    
             /**
              * 是否是boolean
              */
             private boolean isBooleanType(BeanPropertyWriter writer) {
                 Class<?> clazz = writer.getType().getRawClass();
                 return clazz.equals(Boolean.class);
             }
    
             /**
              * 是否是对象
              *
              * @param writer 属性编辑器
              * @return
              */
             private boolean isObjectType(BeanPropertyWriter writer) {
                 Class<?> clazz = writer.getType().getRawClass();
                 if (isBooleanType(writer) || isNumberType(writer) || isStringType(writer) || clazz.isArray() || isMap(writer)) {
                     return false;
                 } else {
                     return true;
                 }
             }
         }
    
         /**
          * 处理数组集合类型的null值
          */
         public static class NullArrayJsonSerializer extends JsonSerializer<Object> {
             @Override
             public void serialize(Object value, JsonGenerator jsonGenerator,
                                   SerializerProvider serializerProvider) throws IOException {
                 jsonGenerator.writeStartArray();
                 jsonGenerator.writeEndArray();
             }
         }
    
         /**
          * 处理实体对象类型的null值
          */
         public static class NullObjectJsonSerializer extends JsonSerializer<Object> {
             @Override
             public void serialize(Object value, JsonGenerator jsonGenerator,
                                   SerializerProvider serializerProvider) throws IOException {
                 jsonGenerator.writeStartObject();
                 jsonGenerator.writeEndObject();
             }
         }
     }

Spring MVC初始化时让其加载自定义转换器:

    @Configuration
    public class CustomWebMvcConfigurationSupport extends WebMvcConfigurationSupport {
    
        // 自定义扩展消息转换器
        @Override
        protected void extendMessageConverters(List<HttpMessageConverter<?>> converters) {
    
    
            super.extendMessageConverters(converters);
            // 自定义仅是对默认转换器做个补充
            if (converters != null) {
                // 初始化默认转换器并加入到默认转换器集合中
                UnderScoreConverter underLineConverter = new UnderScoreConverter(
                        Jackson2ObjectMapperBuilder.json().applicationContext(getApplicationContext()).build());
                converters.add(underLineConverter);
            }
        }
    
        // 由于自定义WebMvcConfigurationSupport导致swagger初始化配置被覆盖,以下用于恢复swagger配置。
        @Override
        protected void addResourceHandlers(ResourceHandlerRegistry registry) {
            registry.addResourceHandler("swagger-ui.html")
                    .addResourceLocations("classpath:/META-INF/resources/");
            registry.addResourceHandler("/webjars/**")
                    .addResourceLocations("classpath:/META-INF/resources/webjars/");
        }
    }

以上配置启动项目后,调用方在请求头(Headers)中增加Accept:Application/underscore,返回值的转换器就能指定为自定义的转换器了。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值