admin管理员组

文章数量:1538079

在本专题之前介绍HTTP请求传参的时候,提到了关于参数值转换的主要控制接口HttpMessageConverter,并承诺在本专题的后续内容中对HttpMessageConverter接口和其实现进行详细讲解,本文就负责完成这个工作(后续本专题的内容还会讲解工作原理)。

1、HttpMessageConverter基本使用方式

HttpMessageConverter接口和其实现类,在SpringMVC框架中负责完成controller层方法的参数读取以及方法返回值的写入(到HTTP响应中)。为了让HttpMessageConverter接口适应不同的操作场景,也为了简化HttpMessageConverter接口的实现,还为了抽象化该接口的多个具体实现的公共逻辑,HttpMessageConverter接口下层又有一些重要的抽象类。本小节首先介绍HttpMessageConverter接口的使用方式,这里先列出其中重要方法的详细说明:

  • List< MediaType > getSupportedMediaTypes()
    该方法被实现后可以告知转换器,哪些HTTP格式类型(列表)可以被转换器支持,例如APPLICATION_XML_VALUE(application/xml)、IMAGE_GIF_VALUE(image/gif)。

  • boolean canRead(Class<?> clazz, @Nullable MediaType mediaType)
    当发生了HTTP请求时该方法将被触发。系统会根据该方法验证controller层指定方法中的指定类,是否能被本次转换支持。mediaType参数表示本次HTTP请求的信息格式类型(实际上就是HTTP请求信息中,Head部分的Content-Type属性)。clazz参数表示本次转换将要试图转换成的目标类型。如果该方法返回true,则表示可以进行转换,这样系统才可能会执行后续的read方法(注意是可能会);其它情况返回false,表示本转换器不支持本次请求信息的转换。换句话说,该方法的意义就是“判定该方法是否可以在指定的消息类型基础上,将HTTP请求信息转换为指定的clazz?”

  • T read(Class<? extends T> clazz, HttpInputMessage inputMessage)
    如果该转换器的canRead方法返回true,该方法才会被触发。在这个方法中,开发人员可以完成实际的HTTP信息到指定clazz类的实例对象的转换。请注意inputMessage参数,正式的HTTP请求在Body部分传递的参数,可以由该对象进行获取。另外注意该方法执行出错可以抛出IOException异常或者HttpMessageNotReadableException异常。

  • boolean canWrite(Class<?> clazz, @Nullable MediaType mediaType)
    当需要进行HTTP响应信息转换时,该方法会被触发。系统会根据该方法验证controller层指定方法中的指定返回类,是否能够被本次转换支持。mediaType参数表示本次HTTP响应的信息格式类型(实际上就是HTTP响应信息中,Head部分的Content-Type属性)。clazz参数表示本次转换将要试图转换成的目标类型。如果该方法返回true,则表示可以进行转换,这样系统才会执行后续的write方法;其它情况返回false,表示不可以进行转换。换句话说,该方法的意义就是“判定该方法是否可以在指定的消息类型基础上,将HTTP响应信息转换为指定的clazz?”

  • write(T t, @Nullable MediaType contentType, HttpOutputMessage outputMessage)
    如果该转换器的canWrite方法返回true,该方法才会被触发。在这个方法中,开发人员可以完成实际的HTTP响应信息到指定clazz类的实例对象的转换。请注意outputMessage参数,转换后的信息需要传递到该对象中,进行实际的输出。

除了了解以上HttpMessageConverter接口方法基本意义外,还需理解HttpMessageConverter接口及其子级接口和实现在使用过程中的关键工作特性:

  • HttpMessageConverter转换器涉及的工作意义分为两部分:接收HTTP请求时进行Controller层方法的映射和转换,以及发送HTTP响应时对Controller层方法的返回值进行映射和转换(即使Controller层方法返回值为void);

  • 但是HttpMessageConverter信息接收和转换的步骤涉及的canRead方法和read方法,只适用于进行HTTP请求时Body部分携带了信息的场景。或者反过来说,如果HTTP请求的Body部分没有携带信息,则不适用HttpMessageConverter接口涉及的canRead方法和read方法。这个适配工作将由Spring MVC中的HandlerMethodArgumentResolver参数解析器负责完成。

  • 相对于负责在HTTP请求时进行信息转化的canRead方法和read方法,负责在HTTP响应时进行信息转换的canWrite方法和write方法更需要着重进行理解。canWrite方法会被调用多次(具体来说是两次),第一次是在Spring MVC组件决定以哪种兼容的信息格式进行响应信息返回时;这时,canWrite方法的mediaType参数会为null,这是因为Spring MVC组件还没有明确以哪种mediaType进行返回。这时,如果该方法返回true,则表示本HttpMessageConverter接口的实现支持当前Controller层方法的返回类型,那么该HttpMessageConverter接口的实现中由getSupportedMediaTypes()返回的那些信息格式,将纳入Spring MVC最终采用的信息格式的考虑。

  • canWrite方法的第二次调用,是在Spring MVC已经确认最终以哪种信息格式进行HTTP响应信息的构建后。这时canWrite方法的mediaType参数将有一个确切的值被传入,开发人员需要注意这些细节。

  • 另外,某个HttpMessageConverter接口的具体实现,canRead方法和read方法不执行或者不支持执行,并不代表该具体实现的canWrite方法和write方法不会被执行。Spring MVC会综合考虑IOC容器中所有配置的转换器实现,并选择两个最科学的转换器,分别负责HTTP请求信息的转换和HTTP响应信息的转换。

  • 如果读者翻阅org.springframework.http.MediaType类的具体代码,就可以发现其中包括了几乎所有由Head部分的Content-Type属性规定的所有格式值,例如“application/json”、“application/json;charset=UTF-8”等等。

  • 最后HttpMessageConverter接口有一个泛型“HttpMessageConverter”,该泛型主要是为了帮助开发人员在具体转换器支持的转换范围内(包括HTTP请求的转换和HTTP响应的转换),进行类型的快速获取。是不是不好理解?没事,我们来看一个具体的自定义转换器demo:

// 自定义的HttpMessageConverter转换器demo,记得配置到IOC容器中(怎么配置本文就不讲解了,默认读者都会配置)
public class MyMessageConverter implements HttpMessageConverter<Object> {
  // 该方法返回本转换器支持的HTTP请求/响应的Body部分的信息格式
  @Override
  public List<MediaType> getSupportedMediaTypes() {
    return Lists.newArrayList(MediaType.TEXT_HTML , MediaType.TEXT_PLAIN , MediaType.TEXT_MARKDOWN);
  }
  // 该方法的意义已经介绍过,这里不再赘述
  @Override
  public boolean canRead(Class<?> clazz, MediaType mediaType) {
    // 按照demo的需求,只要接收到的请求中信息格式为text/html;charset=UTF-8,就可以使用该转换器
    if(MediaType.TEXT_HTML.equalsTypeAndSubtype(mediaType)) {
      return true;
    }  
    return false;
  }
  // 该方法被调用的前提是,canRead方法返回true
  @Override
  public YourBusiness read(Class<? extends Object> clazz, HttpInputMessage inputMessage) throws IOException, HttpMessageNotReadableException {
    // 从body部分读取信息
    StringBuffer bodyInfo = new StringBuffer();
    try(InputStream inputStream = inputMessage.getBody();) {
      byte[] contexts = new byte[4096];
      int realLen;
      while((realLen = inputStream.read(contexts, 0, 4096)) != -1) {
        bodyInfo.append(new String(contexts , 0 , realLen));
      }
    } 
    // 正式的代码中,还要对接收到的信息进行边界校验
    // 开始转换
    String[] infoArrays = StringUtils.split(bodyInfo.toString() , "|");
    YourBusiness yourBusiness = new YourBusiness();
    yourBusiness.setParam1(infoArrays[0]);
    yourBusiness.setParam2(infoArrays[1]);
    yourBusiness.setParam3(Integer.parseInt(infoArrays[2]));
    return yourBusiness;
  }
  
  // =========== 一定注意转换器中的canRead方法和read方法,以及同一转换器中的canWrite方法和write方法,并不存在执行的因果关系。
  // 该方法的意义已经介绍过,这里不再赘述
  @Override
  public boolean canWrite(Class<?> clazz, MediaType mediaType) {
    // 这里让转换器支持响应的条件是:
    // 1、当前返回类型信息的格式类型是application/json
    // 2、controller层方法的返回值类型为字符串
    if(MediaType.TEXT_PLAIN.equalsTypeAndSubtype(mediaType) && CharSequence.class.isAssignableFrom(clazz)) {
      return true;
    }
    return false;
  }
  
  @Override
  public void write(Object t, MediaType contentType, HttpOutputMessage outputMessage) throws IOException, HttpMessageNotWritableException {
    String value = t.toString();
    // 组装成一个json结构,然后返回给调用者
    JSONObject json = new JSONObject();
    json.put("value", value);
    OutputStream out = outputMessage.getBody();
    JSONObject.writeJSONString(out, json);
  }
}

是不是很简单?这样一来读者就已经可以根据自己的设计需求,自行定义信息转换器了,且读者也清楚了HttpMessageConverter转换器在使用时的基本注意事项。请注意,向Spring IOC容器注册了这个自定义的HttpMessageConverter转换器,并不代表其它转换器会失效

2、GenericHttpMessageConverter转换器及相关子类结构

除了实现HttpMessageConverter接口来设计自己的信息转换器外,还有基于GenericHttpMessageConverter接口的转换器,后者也是一个常用的转换器。实际上GenericHttpMessageConverter接口继承了HttpMessageConverter接口,如下图所示:

上图示例了HttpMessageConverter接口的一些关键子接口和抽象类,其中GenericHttpMessageConverter接口,又定义一些需要实现的方法,实现了GenericHttpMessageConverter接口的具体转换器可帮助具有泛型转换要求的场景完成处理过程,但请注意,这两个接口下的方法虽然类似,但携带参数的意义区别还是比较大的,现将该接口的主要方法介绍如下:

  • boolean canRead(Type type, @Nullable Class<?> contextClass, @Nullable MediaType mediaType)
    该方法工作意义与HttpMessageConverter接口提供的canRead方法的工作意义相似(但参数差异较大)。请注意该方法中的Type参数,该参数提供了本次要转换的目标类型,且如果存在内置的泛型要求,则泛型信息会携带在该type对象中。contextClass参数描述参数所在方法的承载类,例如具体的UserController类。

  • T read(Type type, @Nullable Class<?> contextClass, HttpInputMessage inputMessage)
    该方法工作意义与HttpMessageConverter接口提供的read方法的工作意义相似(但参数差异较大)。只有以上canRead(Type , Class , MediaType) 方法返回true的前提下,该方法才会被触发。另外,对于方法中type参数、contextClass参数的的解释,可参见上文说明。

  • boolean canWrite(@Nullable Type type, Class<?> clazz, @Nullable MediaType mediaType)
    该方法工作意义与HttpMessageConverter接口提供的canWrite方法的工作意义相似(但参数差异较大)。请注意其中的type参数,该参数表示要进行响应信息转换的原始类型(如果目标类型存在泛型,则这里会携带泛型信息);clazz参数同样也表示要进行信息转换的原始类型,但是无论如何也不携带泛型信息。

  • void write(T t, @Nullable Type type, @Nullable MediaType contentType, HttpOutputMessage outputMessage)
    该方法工作意义与HttpMessageConverter接口提供的write方法的工作意义相似。只有以上canWrite(Type , Class , MediaType) 方法返回true的前提下,该方法才会被触发。t参数表示本次要进行响应信息转换的原始对象信息;type参数为本次转换的原始类型,如果源类型中存在泛型描述,那么泛型信息将描述在type参数中(和本接口中canWrite方法携带的type参数的意义相同);

另外请注意Type接口,其全名为“java.lang.reflect.Type”。该接口是java.lang.Class类的父级接口,之所以要这样传入是为了匹配携带泛型描述的类信息,这些携带泛型类型的类都可以通过java.lang.reflect.ParameterizedType接口进行描述(请注意区别泛型类型变量、泛型参数类型、泛型表达式,这里就不再展开讲解了)。

如果由开发人员自定义的转换器实现了GenericHttpMessageConverter接口,那么Spring MVC会在适配HTTP信息转换器时(包括适配请求信息和适配响应信息时),优先调用GenericHttpMessageConverter接口中的相关方法,即使本次请求的Controller层方法要求适配的对象不涉及泛型信息。

为了简化实现HttpMessageConverter接口或者GenericHttpMessageConverter接口的难度,SpringMVC组件还为开发人员提供了两个抽象类(AbstractHttpMessageConverter类和AbstractGenericHttpMessageConverter类)。这两个抽象类将HttpMessageConverter和GenericHttpMessageConverter的实现要求进行了简化,使得开发人员更好理解。其中AbstractHttpMessageConverter抽象了用于简化对HttpMessageConverter接口的实现;经过简化后,开发人员只需要重写以下三个方法就可以了:

  • supports(Class<?>):将要被转化的目标类型,是否是本转换器所支持的类型。而mediaType不需要再单独被关注,只需要它从属于AbstractHttpMessageConverter具体示例化时由构造函数传入的supportedMediaTypes范围即可(可参见AbstractHttpMessageConverter抽象类的构造函数)。

  • T readInternal(Class<? extends T> , HttpInputMessage):如果supports(Class<?>)方法返回为true,则该方法才可能被触发。开发人员重写该方法,以便从HTTP请求的Body部分提取数据进行真正的数据对象转换。

  • writeInternal(T , HttpOutputMessage):该方法负责将controller层方法返回的结果(或者报错结果),转换为既定的数据格式返回给本次HTTP请求的调用者。

由此可见AbstractHttpMessageConverter类和AbstractGenericHttpMessageConverter类确实简化了开发人员在实现转换器中的关注点。开发人员只需要关注将要转换的目标/源类型本身。这里本文不计划再放出相似的代码示例,而将示例练习留给读者自己完成。读者完全可以编写继承自AbstractHttpMessageConverter类或者AbstractGenericHttpMessageConverter类的具体实现,并观察执行效果。

// ========

注:本片文章我们只介绍了HttpMessageConverter转换器的使用方法和表现出的工作步骤,但HttpMessageConverter转换器深层次的工作原理并未进行介绍。本专题的后续文章,我们将介绍HttpMessageConverter转换器的工作原理,期间涉及了Spring MVC中多个关键技术点,例如:AbstractHandlerMapping映射处理器、HandlerMethodArgumentResolver参数解析器,等等。

本文标签: 转换器方式系列知识boot