Loading

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中包含了一个源类型和一个目标类型,代表着转换器支持在这两种类型之间进行转换:

img

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);
    }
}

结果:

img

在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-Typeapplication/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;
}

结果:

img

img

何时使用

  1. 在一个Controller方法上,有@ResponseBody时,表示该方法的返回值应该直接被编码到Http响应体中,而不是走什么视图解析器。SpringMVC应该根据请求的Accept来选择一个HttpMessageConverter来对返回对象进行write操作
  2. 在一个Controller方法的参数中,有@RequestBody注解时,表示SpringMVC应该根据请求的Content-Type来选择一个HttpMessageConverter来进行read操作

实际上,如果HttpMessageConverter接管了请求的响应,返回给DispatcherServletModelAndView为null,此时DispatcherServlet不会做任何视图渲染工作。

posted @ 2022-07-24 19:02  yudoge  阅读(298)  评论(0编辑  收藏  举报