【小家Spring】Spring MVC容器的web九大组件之---HandlerAdapter源码详解---HttpMessageConverter 消息转换器详解(中)

简介: 【小家Spring】Spring MVC容器的web九大组件之---HandlerAdapter源码详解---HttpMessageConverter 消息转换器详解(中)

AbstractHttpMessageConverter


一个基础抽象实现,它也还是个泛型类。对于泛型的控制,有如下特点:


  • 最广的可以选择Object,不过Object并不都是可以序列化的,但是子类可以在覆盖的supports方法中进一步控制,因此选择Object是可以的
  • 最符合的是Serializable,既完美满足泛型定义,本身也是个Java序列化/反序列化的充要条件
  • 自定义的基类Bean,有些技术规范要求自己代码中的所有bean都继承自同一个自定义的基类BaseBean,这样可以在Serializable的基础上再进一步控制,满足自己的业务要求


若我们自己需要自定义一个消息转换器,大多数情况下也是继承抽象类再具体实现。比如我们最熟悉的:FastJsonHttpMessageConverter它就是一个子类实现


public abstract class AbstractHttpMessageConverter<T> implements HttpMessageConverter<T> {
  // 它主要内部维护了这两个属性,可议构造器赋值,也可以set方法赋值~~
  private List<MediaType> supportedMediaTypes = Collections.emptyList();
  @Nullable
  private Charset defaultCharset;
  // supports是个抽象方法,交给子类自己去决定自己支持的转换类型~~~~
  // 而canRead(mediaType)表示MediaType也得在我支持的范畴了才行(入参MediaType若没有指定,就返回true的)
  @Override
  public boolean canRead(Class<?> clazz, @Nullable MediaType mediaType) {
    return supports(clazz) && canRead(mediaType);
  }
  // 原理基本同上,supports和上面是同一个抽象方法  所以我们发现并不能入参处理Map,出餐处理List等等
  @Override
  public boolean canWrite(Class<?> clazz, @Nullable MediaType mediaType) {
    return supports(clazz) && canWrite(mediaType);
  }
  // 这是Spring的惯用套路:readInternal  虽然什么都没做,但我觉得还是挺有意义的。Spring后期也非常的好扩展了~~~~
  @Override
  public final T read(Class<? extends T> clazz, HttpInputMessage inputMessage)
      throws IOException, HttpMessageNotReadableException {
    return readInternal(clazz, inputMessage);
  }
  // 整体上就write方法做了一些事~~
  @Override
  public final void write(final T t, @Nullable MediaType contentType, HttpOutputMessage outputMessage)
      throws IOException, HttpMessageNotWritableException {
    final HttpHeaders headers = outputMessage.getHeaders();
    // 设置一个headers.setContentType 和 headers.setContentLength
    addDefaultHeaders(headers, t, contentType);
    if (outputMessage instanceof StreamingHttpOutputMessage) {
      StreamingHttpOutputMessage streamingOutputMessage = (StreamingHttpOutputMessage) outputMessage;
      // StreamingHttpOutputMessage增加的setBody()方法,关于它下面会给一个使用案例~~~~
      streamingOutputMessage.setBody(outputStream -> writeInternal(t, new HttpOutputMessage() {
        // 注意此处复写:返回的是outputStream ,它也是靠我们的writeInternal对它进行写入的~~~~
        @Override
        public OutputStream getBody() {
          return outputStream;
        }
        @Override
        public HttpHeaders getHeaders() {
          return headers;
        }
      }));
    }
    // 最后它执行了flush,这也就是为何我们自己一般不需要flush的原因
    else {
      writeInternal(t, outputMessage);
      outputMessage.getBody().flush();
    }
  }
  // 三个抽象方法
  protected abstract boolean supports(Class<?> clazz);
  protected abstract T readInternal(Class<? extends T> clazz, HttpInputMessage inputMessage)
      throws IOException, HttpMessageNotReadableException;
  protected abstract void writeInternal(T t, HttpOutputMessage outputMessage)
      throws IOException, HttpMessageNotWritableException;
}


关于StreamingHttpOutputMessage的使用:

表示允许设置流正文的HTTP输出消息,需要注意的是,此类消息通常不支持getBody()访问


// @since 4.0
public interface StreamingHttpOutputMessage extends HttpOutputMessage {
  // 设置一个流的正文,提供回调
  void setBody(Body body);
  // 定义可直接写入@link outputstream的主体的协定。
  // 通过回调机制间接的访问HttpClient库很有作用
  @FunctionalInterface
  interface Body {
    // 把当前的这个body写进给定的OutputStream
    void writeTo(OutputStream outputStream) throws IOException;
  }
}


SourceHttpMessageConverter

处理一些和xml相关的资源,比如DOMSource、SAXSource、SAXSource等等,本文略过.


ResourceHttpMessageConverter

负责读取资源文件和写出资源文件数据


这个在上一篇Spring MVC下载的时候有提到过,它来处理把Resource进行写出去。当然它也可以把body的内容写进到Resource里来。


public class ResourceHttpMessageConverter extends AbstractHttpMessageConverter<Resource> {
  // 是否支持读取流信息
  private final boolean supportsReadStreaming;
  // 默认支持所有的MediaType~~~~~   但是它有个类型匹配,所以值匹配入参/返回类型是Resource类型的
  public ResourceHttpMessageConverter() {
    super(MediaType.ALL);
    this.supportsReadStreaming = true;
  }
  @Override
  protected boolean supports(Class<?> clazz) {
    return Resource.class.isAssignableFrom(clazz);
  }
  // 直观感受:读的时候也只支持InputStreamResource和ByteArrayResource这两种resource的直接封装
  @Override
  protected Resource readInternal(Class<? extends Resource> clazz, HttpInputMessage inputMessage)
      throws IOException, HttpMessageNotReadableException {
    if (this.supportsReadStreaming && InputStreamResource.class == clazz) {
      return new InputStreamResource(inputMessage.getBody()) {
        @Override
        public String getFilename() {
          return inputMessage.getHeaders().getContentDisposition().getFilename();
        }
      };
    }
    // 若入参类型是Resource接口,也是当作ByteArrayResource处理的
    else if (Resource.class == clazz || ByteArrayResource.class.isAssignableFrom(clazz)) {
      // 把inputSteeam转换为byte[]数组~~~~~~
      byte[] body = StreamUtils.copyToByteArray(inputMessage.getBody());
      return new ByteArrayResource(body) {
        @Override
        @Nullable
        public String getFilename() {
          return inputMessage.getHeaders().getContentDisposition().getFilename();
        }
      };
    }
    else {
      throw new HttpMessageNotReadableException("Unsupported resource class: " + clazz, inputMessage);
    }
  }
  @Override
  protected void writeInternal(Resource resource, HttpOutputMessage outputMessage)
      throws IOException, HttpMessageNotWritableException {
    writeContent(resource, outputMessage);
  }
  // 写也非常的简单,就是把resource这个资源的内容写到body里面去,此处使用的StreamUtils.copy这个工具方法,专门处理流
  // 看到此处我们自己并不需要flush,但是需要自己关闭流
  protected void writeContent(Resource resource, HttpOutputMessage outputMessage)
      throws IOException, HttpMessageNotWritableException {
    try {
      InputStream in = resource.getInputStream();
      try {
        StreamUtils.copy(in, outputMessage.getBody());
      }
      catch (NullPointerException ex) {
        // ignore, see SPR-13620
      }
      finally {
        try {
          in.close();
        }
        catch (Throwable ex) {
          // ignore, see SPR-12999
        }
      }
    }
    catch (FileNotFoundException ex) {
      // ignore, see SPR-12999
    }
  }
}


使用它模拟完成上传功能:上传表单如下:


<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>测试FormHttpMessageConverter</title>
</head>
<body>
<!-- 表单的enctype一定要标注成multipart形式,否则是拿不到二进制流的 -->
<form action="http://localhost:8080/demo_war_war/upload" method="post" enctype="multipart/form-data">
    用户名 <input type="text" name="userName">
    头像 <input type="file" name="touxiang">
    <input type="submit">
</form>
</body>
</html>

image.png


    // 模拟使用Resource进行文件的上传~~~
    @ResponseBody
    @RequestMapping(value = "/upload", method = RequestMethod.POST)
    public String upload(@RequestBody Resource resource) { //此处不能用接口Resource resource
        dumpStream(resource);
        return "success";
    }
    // 模拟写文件的操作(此处写到控制台)
    private static void dumpStream(Resource resource) {
        InputStream is = null;
        try {
            //1.获取文件资源
            is = resource.getInputStream();
            //2.读取资源
            byte[] descBytes = new byte[is.available()];
            is.read(descBytes);
            System.out.println(new String(descBytes, StandardCharsets.UTF_8));
        } catch (IOException e) {
            e.printStackTrace();
        } finally {
            try {
                //3.关闭资源
                is.close();
            } catch (IOException e) {
            }
        }
    }


控制台结果为:


image.png


由此可见利用它是可以把客户端的资源信息都拿到的,从而间接的实现文件的上传的功能。


ByteArrayHttpMessageConverter


和上面类似,略


ObjectToStringHttpMessageConverter


它是对StringHttpMessageConverter的一个扩展。它在Spring内部并没有装配进去。若我们需要,可以自己装配到Spring MVC里面去

public class ObjectToStringHttpMessageConverter extends AbstractHttpMessageConverter<Object> {
  // 我们只需要自定定义这个转换器   让它实现String到Obj之间的互相转换~~~
  private final ConversionService conversionService;
  private final StringHttpMessageConverter stringHttpMessageConverter;
  ... // 下面省略
  // 读的时候先用stringHttpMessageConverter读成String,再用转换器转为Object对象
  // 写的时候先用转换器转成String,再用stringHttpMessageConverter写进返回的body里
}


Json相关转换器


image.png


可以看到一个是谷歌阵营,一个是jackson阵营。


GsonHttpMessageConverter


利用谷歌的Gson进行json序列化的处理~~~


// @since 4.1  课件它被Spring选中的时间还是比较晚的
public class GsonHttpMessageConverter extends AbstractJsonHttpMessageConverter {
  private Gson gson;
  public GsonHttpMessageConverter() {
    this.gson = new Gson();
  }
  // @since 5.0  调用者可以自己指定一个Gson对象了
  public GsonHttpMessageConverter(Gson gson) {
    Assert.notNull(gson, "A Gson instance is required");
    this.gson = gson;
  } 
  // 因为肯定是文本,所以这里使用Reader 没有啥问题
  // 父类默认用UTF-8把inputStream转为了更友好的Reader
  @Override
  protected Object readInternal(Type resolvedType, Reader reader) throws Exception {
    return getGson().fromJson(reader, resolvedType);
  }
  @Override
  protected void writeInternal(Object o, @Nullable Type type, Writer writer) throws Exception {
    // 如果带泛型  这里也是特别的处理了兼容处理~~~~
    if (type instanceof ParameterizedType) {
      getGson().toJson(o, type, writer);
    } else {
      getGson().toJson(o, writer);
    }
  }
  // 父类定义了它支持的MediaType类型~
  public AbstractJsonHttpMessageConverter() {
    super(MediaType.APPLICATION_JSON, new MediaType("application", "*+json"));
    setDefaultCharset(DEFAULT_CHARSET);
  }
}


MappingJackson2HttpMessageConverter


利用亲儿子Jackson进行json序列化(当然,它并不是真正的亲儿子)


// @since 3.1.2  出来可谓最早。正统太子
public class MappingJackson2HttpMessageConverter extends AbstractJackson2HttpMessageConverter {
  // 该属性在父类定义~~~
  protected ObjectMapper objectMapper;
  @Nullable
  private String jsonPrefix;
  // 支持指定的MediaType类型~~
  public MappingJackson2HttpMessageConverter(ObjectMapper objectMapper) {
    super(objectMapper, MediaType.APPLICATION_JSON, new MediaType("application", "*+json"));
  }
  // 所有的读、写都在父类AbstractJackson2HttpMessageConverter里统一实现的,稍微有点复杂性
}


总体上看,jackson的实现是最为完善的~~~


备注:Gson和Jackson转换器他俩都是支持jsonPrefix我们可以自定义Json前缀的~~~


若你的返回值是Map、List等,只要MediaType对上了,这种json处理器都是可以处理的。因为他们泛型上都是Object表示入参、 返回值任意类型都可以处理~~~


ProtobufHttpMessageConverter、ProtobufJsonFormatHttpMessageConverter



StringHttpMessageConverter


这个是使用得非常广泛的一个消息转换器,专门处理入参/出参字符串类型。

// @since 3.0  出生非常早
public class StringHttpMessageConverter extends AbstractHttpMessageConverter<String> {
  // 这就是为何你return中文的时候会乱码的原因(若你不设置它的编码的话~)
  public static final Charset DEFAULT_CHARSET = StandardCharsets.ISO_8859_1;
  @Nullable
  private volatile List<Charset> availableCharsets;
  // 标识是否输出 Response Headers:Accept-Charset(默认true表示输出)
  private boolean writeAcceptCharset = true;
  public StringHttpMessageConverter() {
    this(DEFAULT_CHARSET);
  }
  public StringHttpMessageConverter(Charset defaultCharset) {
    super(defaultCharset, MediaType.TEXT_PLAIN, MediaType.ALL);
  }
  //Indicates whether the {@code Accept-Charset} should be written to any outgoing request.
  // Default is {@code true}.
  public void setWriteAcceptCharset(boolean writeAcceptCharset) {
    this.writeAcceptCharset = writeAcceptCharset;
  }
  // 只处理String类型~
  @Override
  public boolean supports(Class<?> clazz) {
    return String.class == clazz;
  }
  @Override
  protected String readInternal(Class<? extends String> clazz, HttpInputMessage inputMessage) throws IOException {
    // 哪编码的原则为:
    // 1、contentType自己指定了编码就以指定的为准
    // 2、没指定,但是类型是`application/json`,统一按照UTF_8处理
    // 3、否则使用默认编码:getDefaultCharset  ISO_8859_1
    Charset charset = getContentTypeCharset(inputMessage.getHeaders().getContentType());
    // 按照此编码,转换为字符串~~~
    return StreamUtils.copyToString(inputMessage.getBody(), charset);
  }
  // 显然,ContentLength和编码也是有关的~~~
  @Override
  protected Long getContentLength(String str, @Nullable MediaType contentType) {
    Charset charset = getContentTypeCharset(contentType);
    return (long) str.getBytes(charset).length;
  }
  @Override
  protected void writeInternal(String str, HttpOutputMessage outputMessage) throws IOException {
    // 默认会给请求设置一个接收的编码格式~~~(若用户不指定,是所有的编码都支持的)
    if (this.writeAcceptCharset) {
      outputMessage.getHeaders().setAcceptCharset(getAcceptedCharsets());
    }
    // 根据编码把字符串写进去~
    Charset charset = getContentTypeCharset(outputMessage.getHeaders().getContentType());
    StreamUtils.copy(str, charset, outputMessage.getBody());
  }
  ...
}


我们有可以这么来写,达到我们一定的目的:

  // 因为它支持MediaType.TEXT_PLAIN, MediaType.ALL所有类型,所以你的contentType无所谓~~~ 它都能够处理
    @ResponseBody
    @RequestMapping(value = "/test", method = RequestMethod.POST)
    public String upload(@RequestBody String body) {
        return "Hello World";
    }


这种书写方式它不管是入参,还是返回值处理的转换器,都是用到的StringHttpMessageConverter。用它来接收入参和上面例子Resource有点像,只是StringHttpMessageConverter它只能解析文本内容,而Resource可以处理所有。


需要注意的是:若你的项目中大量使用到了此转换器,请一定要注意编码问题。一般不建议直接使用StringHttpMessageConverter,而是我们配置好编码(UTF-8)后,再把它加入到Spring MVC里面,这样就不会有乱码问题了


另外我们或许看到过有的小伙伴竟这么来写:为了给前端返回一个json串


    @ResponseBody
    @RequestMapping(value = "/test")
    public String test() {
        return "{\"status\":0,\"errmsg\":null,\"data\":{\"query\":\"酒店查询\",\"num\":65544,\"url\":\"www.test.com\"}}";
    }


虽然这么做结果是没有问题的,但是非常非常的不优雅,属于低级的行为。

通过自己构造Json串的形式(虽然你可能直接借助Fastjson去转,但也很低级),现在看来这么做是低级的、愚蠢的,小伙伴们千万别~~~~这么去做


BufferedImageHttpMessageConverter


处理java.awt.image.BufferedImage,和awt相关。略

GenericHttpMessageConverter 子接口


GenericHttpMessageConverter接口继承自HttpMessageConverter接口,二者都是在org.springframework.http.converter包下。它的特点就是:它处理目标类型为泛型类型的类型~~~


public interface GenericHttpMessageConverter<T> extends HttpMessageConverter<T> {
  //This method should perform the same checks than {@link HttpMessageConverter#canRead(Class, MediaType)} with additional ones related to the generic type.
  // 它的效果同父接口的canRead,但是它是加了一个泛型类型~~~来加以更加详细的判断
  boolean canRead(Type type, @Nullable Class<?> contextClass, @Nullable MediaType mediaType);
  // 一样也是加了泛型类型
  T read(Type type, @Nullable Class<?> contextClass, HttpInputMessage inputMessage)
      throws IOException, HttpMessageNotReadableException;
  //@since 4.2
  boolean canWrite(@Nullable Type type, Class<?> clazz, @Nullable MediaType mediaType);
  // @since 4.2
  void write(T t, @Nullable Type type, @Nullable MediaType contentType, HttpOutputMessage outputMessage) throws IOException, HttpMessageNotWritableException;
}



image.png


可以看出处理Json方面的转换器,都实现了此接口。此处主要以阿里巴巴的FastJson转换器为例加以说明:

相关文章
|
5月前
|
XML Java 测试技术
《深入理解Spring》:IoC容器核心原理与实战
Spring IoC通过控制反转与依赖注入实现对象间的解耦,由容器统一管理Bean的生命周期与依赖关系。支持XML、注解和Java配置三种方式,结合作用域、条件化配置与循环依赖处理等机制,提升应用的可维护性与可测试性,是现代Java开发的核心基石。
|
5月前
|
前端开发 Java 微服务
《深入理解Spring》:Spring、Spring MVC与Spring Boot的深度解析
Spring Framework是Java生态的基石,提供IoC、AOP等核心功能;Spring MVC基于其构建,实现Web层MVC架构;Spring Boot则通过自动配置和内嵌服务器,极大简化了开发与部署。三者层层演进,Spring Boot并非替代,而是对前者的高效封装与增强,适用于微服务与快速开发,而深入理解Spring Framework有助于更好驾驭整体技术栈。
|
5月前
|
XML Java 应用服务中间件
【SpringBoot(一)】Spring的认知、容器功能讲解与自动装配原理的入门,带你熟悉Springboot中基本的注解使用
SpringBoot专栏开篇第一章,讲述认识SpringBoot、Bean容器功能的讲解、自动装配原理的入门,还有其他常用的Springboot注解!如果想要了解SpringBoot,那么就进来看看吧!
629 2
|
7月前
|
运维 数据可视化 C++
2025 热门的 Web 化容器部署工具对比:Portainer VS Websoft9
2025年热门Web化容器部署工具对比:Portainer与Websoft9。Portainer以轻量可视化管理见长,适合技术团队运维;Websoft9则提供一站式应用部署与容器管理,内置丰富开源模板,降低中小企业部署门槛。两者各有优势,助力企业提升容器化效率。
498 1
2025 热门的 Web 化容器部署工具对比:Portainer VS Websoft9
|
8月前
|
前端开发 Java API
Spring Cloud Gateway Server Web MVC报错“Unsupported transfer encoding: chunked”解决
本文解析了Spring Cloud Gateway中出现“Unsupported transfer encoding: chunked”错误的原因,指出该问题源于Feign依赖的HTTP客户端与服务端的`chunked`传输编码不兼容,并提供了具体的解决方案。通过规范Feign客户端接口的返回类型,可有效避免该异常,提升系统兼容性与稳定性。
575 0
|
8月前
|
SQL Java 数据库连接
Spring、SpringMVC 与 MyBatis 核心知识点解析
我梳理的这些内容,涵盖了 Spring、SpringMVC 和 MyBatis 的核心知识点。 在 Spring 中,我了解到 IOC 是控制反转,把对象控制权交容器;DI 是依赖注入,有三种实现方式。Bean 有五种作用域,单例 bean 的线程安全问题及自动装配方式也清晰了。事务基于数据库和 AOP,有失效场景和七种传播行为。AOP 是面向切面编程,动态代理有 JDK 和 CGLIB 两种。 SpringMVC 的 11 步执行流程我烂熟于心,还有那些常用注解的用法。 MyBatis 里,#{} 和 ${} 的区别很关键,获取主键、处理字段与属性名不匹配的方法也掌握了。多表查询、动态
234 0
|
7月前
|
Kubernetes Docker Python
Docker 与 Kubernetes 容器化部署核心技术及企业级应用实践全方案解析
本文详解Docker与Kubernetes容器化技术,涵盖概念原理、环境搭建、镜像构建、应用部署及监控扩展,助你掌握企业级容器化方案,提升应用开发与运维效率。
1082 108