16.SpringBoot學習(十六)——Spring Boot MessageConverter消息轉換器


1.簡介

1.1 概述

Spring MVC uses the HttpMessageConverter interface to convert HTTP requests and responses. Sensible defaults are included out of the box. For example, objects can be automatically converted to JSON (by using the Jackson library) or XML (by using the Jackson XML extension, if available, or by using JAXB if the Jackson XML extension is not available). By default, strings are encoded in UTF-8.

Spring MVC使用HttpMessageConverter接口轉換HTTP請求和響應。開箱即用中包含明智的默認設置。例如,可以將對象自動轉換為JSON(通過使用Jackson庫)或XML(通過使用Jackson XML擴展(如果可用)或通過使用JAXB(如果Jackson XML擴展不可用))。默認情況下,字符串以UTF-8編碼。

1.2 特點

HttpMessageConverter 是一個接口,它包含以下幾個方法

image-20200729211313871

  • canRead: 判斷是否支持解析當前 MediaType
  • canWrite: 判斷是否支持輸出當前 MediaType
  • getSupportedMediaTypes: 獲取支持的 MediaTypes
  • read: 解析http消息內容
  • write: 輸出指定MediaType的消息內容

這里的MediaType即為http請求中常見的 Content-Type;例如:application/json、application/xml等

2.演示環境

  1. JDK 1.8.0_201
  2. Spring Boot 2.2.0.RELEASE
  3. 構建工具(apache maven 3.6.3)
  4. 開發工具(IntelliJ IDEA )

3.演示代碼

3.1 代碼說明

自定義HttpMessageConverter消息轉換器,實現消息的解析和輸出

3.2 代碼結構

image-20200729212037648

3.3 maven 依賴

<dependencies>
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-web</artifactId>
    </dependency>
</dependencies>

3.4 配置文件

無配置

3.5 java代碼

UserModel.java

public class UserModel {

    private Long id;
    private String name;
    private Integer age;
    private Date birthday;
    private BigDecimal salary;
    private String phone;

    public UserModel() {}

    public UserModel(Long id, String name, Integer age, Date birthday, BigDecimal salary, String phone) {
        this.id = id;
        this.name = name;
        this.age = age;
        this.birthday = birthday;
        this.salary = salary;
        this.phone = phone;
    }

    // get&set&toString
}

UserRepository.java

@Repository
public class UserRepository {

    private static final AtomicLong ID_GENERATOR = new AtomicLong(2);

    private static final Map<Long, UserModel> USER_MAP = new HashMap<>();

    @PostConstruct
    public void init() {
        UserModel user1 =
            new UserModel(1L, "zhangsan", 20, new Date(), new BigDecimal("23456.11"), "13666666666");
        UserModel user2 =
            new UserModel(2L, "lisi", 30, new Date(), new BigDecimal("12345.67"), "13888888888");
        USER_MAP.put(user1.getId(), user1);
        USER_MAP.put(user2.getId(), user2);
    }

    public List<UserModel> findAll() {
        return new ArrayList<>(USER_MAP.values());
    }

    public UserModel findById(Long id) {
        return USER_MAP.get(id);
    }

    public UserModel add(UserModel userModel) {
        long id = ID_GENERATOR.incrementAndGet();
        userModel.setId(id);
        USER_MAP.put(id, userModel);
        return userModel;
    }

    public UserModel update(UserModel userModel) {
        USER_MAP.put(userModel.getId(), userModel);
        return USER_MAP.get(userModel.getId());
    }

    public UserModel deleteById(Long id) {
        UserModel userModel = USER_MAP.get(id);
        USER_MAP.remove(id);
        return userModel;
    }
}

WebMvcConfig.java

@Configuration
public class WebMvcConfig implements WebMvcConfigurer {

    @Override
    public void extendMessageConverters(List<HttpMessageConverter<?>> converters) {
        // 擴展 MessageConverter,將 PropertiesHttpMessageConverter 放在第一位
        converters.add(0, new PropertiesHttpMessageConverter());
    }
}

PropertiesHttpMessageConverter.java

public class PropertiesHttpMessageConverter extends AbstractHttpMessageConverter<Object> {

    public PropertiesHttpMessageConverter() {
        super(MediaType.valueOf("text/properties"));
        setDefaultCharset(StandardCharsets.UTF_8);
    }

    @Override
    protected boolean supports(Class<?> clazz) {
        return clazz.isAssignableFrom(UserModel.class);
    }

    @Override
    protected Object readInternal(Class<?> clazz, HttpInputMessage inputMessage)
        throws IOException, HttpMessageNotReadableException {

        Properties props = new Properties();
        props.load(new InputStreamReader(inputMessage.getBody(), getDefaultCharset()));

        // 要求對象必須有無參構造函數
        Object instance = ReflectUtils.newInstance(UserModel.class);
        Field[] fields = clazz.getDeclaredFields();

        Stream.of(fields).filter(field -> props.containsKey(field.getName())).forEach(field -> {
            String property = props.getProperty(field.getName());
            Class<?> fieldType = field.getType();
            field.setAccessible(true);
            ReflectionUtils.setField(field, instance, resolveFieldValue(property, fieldType));
        });

        return instance;
    }

    @Override
    protected void writeInternal(Object user, HttpOutputMessage outputMessage)
        throws IOException, HttpMessageNotReadableException {

        Properties props = new Properties();
        Field[] fields = user.getClass().getDeclaredFields();

        Stream.of(fields).forEach(field -> {
            String fieldName = field.getName();
            field.setAccessible(true);
            Object fieldValue = ReflectionUtils.getField(field, user);
            props.put(fieldName, String.valueOf(fieldValue));
        });

        props.store(new OutputStreamWriter(outputMessage.getBody(), getDefaultCharset()),
            "written by properties message converter");
    }

    private Object resolveFieldValue(String property, Class<?> fieldType) {

        if (Integer.class == fieldType) {
            return Integer.valueOf(property);
        } else if (Long.class == fieldType) {
            return Long.valueOf(property);
        } else if (Short.class == fieldType) {
            return Short.valueOf(property);
        } else if (Byte.class == fieldType) {
            return Byte.valueOf(property);
        } else if (String.class == fieldType) {
            return property;
        } else if (Float.class == fieldType) {
            return Float.valueOf(property);
        } else if (Double.class == fieldType) {
            return Double.valueOf(property);
        } else if (BigDecimal.class == fieldType) {
            return new BigDecimal(property);
        } else if (Date.class == fieldType) {
            try {
                SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd");
                return sdf.parse(property);
            } catch (ParseException e) {
                e.printStackTrace();
                return null;
            }
        }

        return property;
    }
}

UserController.java

@RestController
@RequestMapping(value = "/user")
public class UserController {

    @Autowired
    private UserRepository userRepository;

    @PostMapping(value = "/add1", consumes = "text/properties", produces = "application/json;charset=UTF-8")
    public UserModel add1(@RequestBody UserModel userModel) {
        return userRepository.add(userModel);
    }

    @PostMapping(value = "/add2", consumes = "application/json;charset=UTF-8", produces = "text/properties")
    public UserModel add2(@RequestBody UserModel userModel) {
        return userRepository.add(userModel);
    }
}

3.6 git 地址

spring-boot/spring-boot-10-message-converter

4.效果展示

啟動 SpringBoot10MessageConverterApplication.main 方法,在 spring-boot-message-converter.http 訪問下列地址,觀察輸出信息是否符合預期。

接收 text/properties 類型的參數,輸出 application/json 格式內容

### POST /user/add1
POST http://localhost:8080/user/add1
Accept: application/json;charset=utf-8
Content-Type: text/properties;charset=utf-8

name=wangwu
age=22
birthday=1996-05-05
salary=6666.66
phone=13555555555

image-20200729212747549

接收 application/json 類型的參數,輸出 text/properties 格式內容

### POST /user/add2
POST http://localhost:8080/user/add2
Accept: text/properties;charset=utf-8
Content-Type: application/json;charset=utf-8

{
"name": "wangwu",
"age": "22",
"birthday": "1996-05-05",
"salary": "6666.66",
"phone": "13555555555"
}

image-20200729212844773

5.源碼分析

5.1 自定義MessageConverter執行流程

以 UserController#add1 為例,簡單分析一下源碼

image-20200729214822124.png

在 AbstractMessageConverterMethodArgumentResolver#readWithMessageConverters 中有這樣一段

protected <T> Object readWithMessageConverters(HttpInputMessage inputMessage, MethodParameter parameter, Type targetType) throws IOException, HttpMediaTypeNotSupportedException, HttpMessageNotReadableException {

    //...

    try {
        message = new EmptyBodyCheckingHttpInputMessage(inputMessage);
		// 遍歷所有的 messageConverters
        for (HttpMessageConverter<?> converter : this.messageConverters) {
            Class<HttpMessageConverter<?>> converterType = (Class<HttpMessageConverter<?>>) converter.getClass();
            GenericHttpMessageConverter<?> genericConverter =
                (converter instanceof GenericHttpMessageConverter ? (GenericHttpMessageConverter<?>) converter : null);
            // 執行 canRead 方法,判斷是否可以支持當前的 Content-Type
            if (genericConverter != null ? genericConverter.canRead(targetType, contextClass, contentType) :
                (targetClass != null && converter.canRead(targetClass, contentType))) {					
                // 如果可以支持,判斷是否有消息體
                if (message.hasBody()) {
                    // 解析前處理邏輯
                    HttpInputMessage msgToUse =
                        getAdvice().beforeBodyRead(message, parameter, targetType, converterType);
                    // 調用 read 方法解析消息內容
                    body = (genericConverter != null ? genericConverter.read(targetType, contextClass, msgToUse) :
                            ((HttpMessageConverter<T>) converter).read(targetClass, msgToUse));
                    // 解析后處理邏輯
                    body = getAdvice().afterBodyRead(body, msgToUse, parameter, targetType, converterType);
                }
                else {
                    // 消息體為空時處理邏輯
                    body = getAdvice().handleEmptyBody(null, message, parameter, targetType, converterType);
                }
                break;
            }
        }
    }
    catch (IOException ex) {
        throw new HttpMessageNotReadableException("I/O error while reading input message", ex, inputMessage);
    }

    // ...

    return body;
}

這里需要遍歷 messageConverters 來尋找一個合適的處理器,那么這里的 messageConverters 如何獲取到自定義的 HTTPMessageConverter 呢?

其實,在項目啟動的時候,自定義 HTTPMessageConverter 被加載到 applicationContext 中。RequestMappingHandlerAdapter 在初始化完成后,調用其 afterPropertiesSet

public void afterPropertiesSet() {
    // Do this first, it may add ResponseBody advice beans
    initControllerAdviceCache();

    if (this.argumentResolvers == null) {
        // 獲取參數處理的 resolvers
        List<HandlerMethodArgumentResolver> resolvers = getDefaultArgumentResolvers();
        this.argumentResolvers = new HandlerMethodArgumentResolverComposite().addResolvers(resolvers);
    }
    if (this.initBinderArgumentResolvers == null) {
        List<HandlerMethodArgumentResolver> resolvers = getDefaultInitBinderArgumentResolvers();
        this.initBinderArgumentResolvers = new HandlerMethodArgumentResolverComposite().addResolvers(resolvers);
    }
    if (this.returnValueHandlers == null) {
        List<HandlerMethodReturnValueHandler> handlers = getDefaultReturnValueHandlers();
        this.returnValueHandlers = new HandlerMethodReturnValueHandlerComposite().addHandlers(handlers);
    }
}

在 getDefaultArgumentResolvers 中有聲明 RequestResponseBodyMethodProcessor 和 RequestPartMethodArgumentResolver,二者都需要調用 getMessageConverters 方法

private List<HandlerMethodArgumentResolver> getDefaultArgumentResolvers() {
    List<HandlerMethodArgumentResolver> resolvers = new ArrayList<>();

    // Annotation-based argument resolution
    resolvers.add(new RequestParamMethodArgumentResolver(getBeanFactory(), false));
    resolvers.add(new RequestParamMapMethodArgumentResolver());
    resolvers.add(new PathVariableMethodArgumentResolver());
    resolvers.add(new PathVariableMapMethodArgumentResolver());
    resolvers.add(new MatrixVariableMethodArgumentResolver());
    resolvers.add(new MatrixVariableMapMethodArgumentResolver());
    resolvers.add(new ServletModelAttributeMethodProcessor(false));
    resolvers.add(new RequestResponseBodyMethodProcessor(getMessageConverters(), this.requestResponseBodyAdvice));
    resolvers.add(new RequestPartMethodArgumentResolver(getMessageConverters(), this.requestResponseBodyAdvice));
    resolvers.add(new RequestHeaderMethodArgumentResolver(getBeanFactory()));
    resolvers.add(new RequestHeaderMapMethodArgumentResolver());
    resolvers.add(new ServletCookieValueMethodArgumentResolver(getBeanFactory()));
    resolvers.add(new ExpressionValueMethodArgumentResolver(getBeanFactory()));
    resolvers.add(new SessionAttributeMethodArgumentResolver());
    resolvers.add(new RequestAttributeMethodArgumentResolver());

    // Type-based argument resolution
    resolvers.add(new ServletRequestMethodArgumentResolver());
    resolvers.add(new ServletResponseMethodArgumentResolver());
    resolvers.add(new HttpEntityMethodProcessor(getMessageConverters(), this.requestResponseBodyAdvice));
    resolvers.add(new RedirectAttributesMethodArgumentResolver());
    resolvers.add(new ModelMethodProcessor());
    resolvers.add(new MapMethodProcessor());
    resolvers.add(new ErrorsMethodArgumentResolver());
    resolvers.add(new SessionStatusMethodArgumentResolver());
    resolvers.add(new UriComponentsBuilderMethodArgumentResolver());

    // Custom arguments
    if (getCustomArgumentResolvers() != null) {
        resolvers.addAll(getCustomArgumentResolvers());
    }

    // Catch-all
    resolvers.add(new RequestParamMethodArgumentResolver(getBeanFactory(), true));
    resolvers.add(new ServletModelAttributeMethodProcessor(true));

    return resolvers;
}

它的實現如下

public List<HttpMessageConverter<?>> getMessageConverters() {
    return this.messageConverters;
}

這里的 messageConverters 通過構造函數加入了一部分,也在 WebMvcAutoConfiguration 中進行了擴展。

5.2 自定義MessageConverter加載

image-20200729222813002.png

在 spring boot 啟動的時候,會加載到 WebMvcAutoConfiguration.EnableWebMvcConfiguration 中 requestMappingHandlerAdapter 方法,這個方法用來聲明一個 RequestMappingHandlerAdapter 的 bean,它又通過調用 super.requestMappingHandlerAdapter 來進行實例化。

在 super.requestMappingHandlerAdapter 通過adapter.setMessageConverters(getMessageConverters()); 將 messageConverters 設置到 adapter 上,這里的 getMessageConverters 實現如下

protected final List<HttpMessageConverter<?>> getMessageConverters() {
    if (this.messageConverters == null) {
        this.messageConverters = new ArrayList<>();
        // 配置 messageConverters
        configureMessageConverters(this.messageConverters);
        if (this.messageConverters.isEmpty()) {
            // 如果messageConverters為空,加載默認的配置
            addDefaultHttpMessageConverters(this.messageConverters);
        }
        // 加載擴展的 messageConverter
        extendMessageConverters(this.messageConverters);
    }
    return this.messageConverters;
}

在 extendMessageConverters 中通過委派,調用 DelegatingWebMvcConfiguration 的 extendMessageConverters 來擴展 messageConverters,最終調用到 WebMvcConfigurer 的 extendMessageConverters 方法

public void extendMessageConverters(List<HttpMessageConverter<?>> converters) {
    for (WebMvcConfigurer delegate : this.delegates) {
        delegate.extendMessageConverters(converters);
    }
}

而 WebMvcConfig 恰好是 WebMvcConfigurer 的實現類,重寫了它的 extendMessageConverters 方法,所以自定義的 HTTPMessageConverter 被加載到 RequestMappingHandlerAdapter 中。

6.參考

  1. Spring Boot Features/message-converters


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM