Spring Converter和HttpMessageConverter
Converter系列
Converter
系列是用于实现类型转换的策略组件。
Converter
Converter
接口很简单,没什么好说的,用于把类型S
转换成类型T
source
永远不会为Null- 你可以抛出任何异常以表示转换失败,可以抛出
IllegalArgumentException
代表source
参数不合法 - 你需要保证你的
Converter
实现线程安全
package org.springframework.core.convert.converter;
public interface Converter<S, T> {
T convert(S source);
}
ConverterFactory
它是一个Converter
的工厂类,用于生成一个Converter
。
看起来没什么必要,实际上,注意getConveter
方法的返回值,该方法的返回值是一个Converter<S, T>
类型,而<T extends R>
。也就是说,这个工厂用于生产任何以类型R
的子类为目标类型的Converter
:
package org.springframework.core.convert.converter;
public interface ConverterFactory<S, R> {
<T extends R> Converter<S, T> getConverter(Class<T> targetType);
}
一个典型的用例就是转换枚举类型的时候:
public class StringEnumConverterFactory implements ConverterFactory<String, Enum> {
@Override
public <T extends Enum> Converter<String, T> getConverter(Class<T> targetType) {
return new Converter<String, T>() {
@Override
public T convert(String source) {
return (T) Enum.valueOf(targetType, source);
}
};
}
}
因为任意一个枚举值都是Enum
的子类,这个StringEnumConverter
用于获取任何枚举类型的Converter
。
public static void main(String[] args) {
StringEnumConverterFactory converterFactory = new StringEnumConverterFactory();
Converter<String, Gender> converter = converterFactory.getConverter(Gender.class);
Gender gender = converter.convert("MALE");
System.out.println(gender == Gender.MALE);
}
GenericConverter
一种使用起来更加复杂但扩展性更好的转换器接口:
public interface GenericConverter {
@Nullable
Set<ConvertiblePair> getConvertibleTypes();
@Nullable
Object convert(@Nullable Object source, TypeDescriptor sourceType, TypeDescriptor targetType);
}
getConvertibleTypes
方法返回一个ConvertiblePair
的集合,一个ConvertiblePair
中包含了一个源类型和一个目标类型,代表着转换器支持在这两种类型之间进行转换:
convert
方法是实际执行转换的方法,source
是要转换的对象,它是所有支持的ConvertiblePair
中的源类型中的一个,TypeDescriptor
类型的sourceType
可以用来方便的获取source
的类型、注解等信息,targetType
同理。
一个典型用例就是在数组和Collection
之间提供转换。
ConditionalGenericConverter
GenericConverter
的子接口,它在GenericConverter
的基础上添加了一个matches
方法。
因为有时即使是一对匹配的ConvertiblePair
,还是可能会希望在某些特定条件下不进行转换,比如在目标转换类上有某些注解时,这时可以使用ConditionalGenericConverter
。
ConversionService
实际项目中可能有许多许多的Converter
,一个一个的手动来根据类型调用不太现实,如果你正在编写一个库,对调用者来说更是非常不友好。
ConversionService
是一个门面接口(Facade Interface),它向外部隐藏了内部实际调用什么Converter完成工作,外部只需要调用canConvert
判断给定的类型是否能转换,再调用convert
实际完成转换即可。
package org.springframework.core.convert;
public interface ConversionService {
boolean canConvert(Class<?> sourceType, Class<?> targetType);
<T> T convert(Object source, Class<T> targetType);
boolean canConvert(TypeDescriptor sourceType, TypeDescriptor targetType);
Object convert(Object source, TypeDescriptor sourceType, TypeDescriptor targetType);
}
大部分的ConversionService
实现类也同时实现了ConversionRegistry
接口,以让外部可以向它注册Converter。
下面的示例中使用GenericConversionService
,向其中添加了将字符串转换成Person的PersonConverter
以及刚刚的StringEnumConverterFactory
,同时,ConversionRegistry
也支持添加GenericConverter
:
public static void main(String[] args) {
GenericConversionService conversionService = new GenericConversionService();
conversionService.addConverter(new PersonConverter());
conversionService.addConverterFactory(new StringEnumConverterFactory());
if (conversionService.canConvert(String.class, Person.class)) {
Person person = conversionService.convert("Yudoge:21:200.0:200.0:翻斗大街:2001-04-10", Person.class);
System.out.println(person);
}
if (conversionService.canConvert(String.class, Gender.class)) {
Gender gender = conversionService.convert("MALE", Gender.class);
System.out.println(gender);
}
}
结果:
在Spring中应用ConversionService
在每一个Spring容器中(或ApplicationContext)都可以注册一个ConversionService
类型的Bean,BeanName为conversionService
,Spring会在需要类型转换时使用它。
ConversionService
使用FactoryBean
注册:
<bean id="conversionService" class="org.springframework.context.support.ConversionServiceFactoryBean" />
A default ConversionService can convert between strings, numbers, enums, collections, maps, and other common types. To suppliment or override the default converters with your own custom converter(s), set the
converters
property. Property values may implement either of the Converter, ConverterFactory, or GenericConverter interfaces.
<bean id="conversionService" class="org.springframework.context.support.ConversionServiceFactoryBean">
<property name="converters">
<list>
<bean class="example.MyCustomConverter" />
</list>
</property>
</bean>
HttpMessageConverter
Converter
是Spring需要进行类型转换时使用的,比如在Controller数据绑定时需要把Http参数或PathVariable中的字符串转换成其它类型,如数字、Date、Pojos时使用的。
而HttpMessageConverter
则更加底层,它用于将一个Http请求的body转换成一个对象,或者将一个对象转换成Http响应的body。
能否将请求转换成一个对象依赖于请求的Content-Type
头,比如一个将JSON格式请求体转换成Java对象的HttpMessageConverter
依赖Content-Type
为application/json
。
能否将一个Java对象转换成一个Http响应依赖于请求的Accept
头,比如一个Accept
字段为application/xml
的请求不可能由一个将Java对象转换为JSON字符串的转换器来执行转换。
所以HttpMessageConverter
中有这两个方法:
// 能否将指定mediatype的请求读取到class类型的对象中
boolean canRead(Class<?> clazz, @Nullable MediaType mediaType);
// 能否将class类型的对象写入到指定mediatype的请求的响应中
boolean canWrite(Class<?> clazz, @Nullable MediaType mediaType);
由于Http请求响应就是byte数组嘛,不管是从什么Java对象转换过来的。所以,HttpMessageConverter
提供的实际转换方法是这样的:
T read(Class<? extends T> clazz, HttpInputMessage inputMessage)
throws IOException, HttpMessageNotReadableException;
void write(T t, @Nullable MediaType contentType, HttpOutputMessage outputMessage)
throws IOException, HttpMessageNotWritableException;
HttpInputMessage.getBody
获得一个该请求body的InputStream
,而HttpOutputMessage.getBody
获得一个该请求的响应的body的OutputStream
。
下面我们写一个HttpMessageConverter
的小示例:
public class JsonHttpMessageConverter implements HttpMessageConverter<Object> {
@Override
public boolean canRead(Class<?> clazz, MediaType mediaType) {
return mediaType.isPresentIn(getSupportedMediaTypes());
}
@Override
public boolean canWrite(Class<?> clazz, MediaType mediaType) {
// 如果前端没指定它允许接受的mediatype,那么无条件写入
if (mediaType == null) return true;
return mediaType.isPresentIn(getSupportedMediaTypes());
}
@Override
public List<MediaType> getSupportedMediaTypes() {
return Arrays.asList(MediaType.APPLICATION_JSON);
}
@Override
public Object read(Class<?> clazz, HttpInputMessage inputMessage) throws IOException, HttpMessageNotReadableException {
ObjectMapper objectMapper = new ObjectMapper();
System.out.println("JsonHttpMessageConverter is reading!");
return objectMapper.readValue(inputMessage.getBody(), clazz);
}
@Override
public void write(Object o, MediaType contentType, HttpOutputMessage outputMessage) throws IOException, HttpMessageNotWritableException {
// 设置响应的content-type
outputMessage.getHeaders().set("Content-Type", "application/json");
ObjectMapper objectMapper = new ObjectMapper();
objectMapper.writeValue(outputMessage.getBody(), o);
System.out.println("JsonHttpMessageConverter is writting!");
}
}
注册:
@Configuration
@ComponentScan(basePackages = {"top.yudoge.controller"})
@EnableWebMvc
public class AppConfig implements WebMvcConfigurer {
// 该方法如果没有注册任何converter,那么将有一批默认的converter被应用
@Override
public void configureMessageConverters(List<HttpMessageConverter<?>> converters) {
converters.add(new JsonHttpMessageConverter());
}
}
准备一个Controller方法,从请求体中接收对象,并返回对象到响应体中,这样read和write都测试到了:
@PostMapping("/postObject")
public Person postObject(@RequestBody Person person) {
return person;
}
结果:
何时使用
- 在一个Controller方法上,有
@ResponseBody
时,表示该方法的返回值应该直接被编码到Http响应体中,而不是走什么视图解析器。SpringMVC应该根据请求的Accept
来选择一个HttpMessageConverter来对返回对象进行write
操作 - 在一个Controller方法的参数中,有
@RequestBody
注解时,表示SpringMVC应该根据请求的Content-Type
来选择一个HttpMessageConverter来进行read
操作
实际上,如果
HttpMessageConverter
接管了请求的响应,返回给DispatcherServlet
的ModelAndView
为null,此时DispatcherServlet
不会做任何视图渲染工作。