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,返回值的轉換器就能指定爲自定義的轉換器了。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章