目錄
問題描述
由於存在一些技術債,需要服務層返回的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,返回值的轉換器就能指定爲自定義的轉換器了。