【小家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转换器为例加以说明:

相关文章
|
2月前
|
缓存 安全 Java
《深入理解Spring》过滤器(Filter)——Web请求的第一道防线
Servlet过滤器是Java Web核心组件,可在请求进入容器时进行预处理与响应后处理,适用于日志、认证、安全、跨域等全局性功能,具有比Spring拦截器更早的执行时机和更广的覆盖范围。
|
2月前
|
XML Java 测试技术
《深入理解Spring》:IoC容器核心原理与实战
Spring IoC通过控制反转与依赖注入实现对象间的解耦,由容器统一管理Bean的生命周期与依赖关系。支持XML、注解和Java配置三种方式,结合作用域、条件化配置与循环依赖处理等机制,提升应用的可维护性与可测试性,是现代Java开发的核心基石。
|
2月前
|
XML Java 应用服务中间件
【SpringBoot(一)】Spring的认知、容器功能讲解与自动装配原理的入门,带你熟悉Springboot中基本的注解使用
SpringBoot专栏开篇第一章,讲述认识SpringBoot、Bean容器功能的讲解、自动装配原理的入门,还有其他常用的Springboot注解!如果想要了解SpringBoot,那么就进来看看吧!
444 4
|
3月前
|
存储 安全 Java
如何在 Spring Web 应用程序中使用 @SessionScope 和 @RequestScope
Spring框架中的`@SessionScope`和`@RequestScope`注解用于管理Web应用中的状态。`@SessionScope`绑定HTTP会话生命周期,适用于用户特定数据,如购物车;`@RequestScope`限定于单个请求,适合无状态、线程安全的操作,如日志记录。合理选择作用域能提升应用性能与可维护性。
179 1
|
4月前
|
存储 NoSQL Java
探索Spring Boot的函数式Web应用开发
通过这种方式,开发者能以声明式和函数式的编程习惯,构建高效、易测试、并发友好的Web应用,同时也能以较小的学习曲线迅速上手,因为这些概念与Spring Framework其他部分保持一致性。在设计和编码过程中,保持代码的简洁性和高内聚性,有助于维持项目的可管理性,也便于其他开发者阅读和理解。
160 0
|
5月前
|
前端开发 Java API
Spring Cloud Gateway Server Web MVC报错“Unsupported transfer encoding: chunked”解决
本文解析了Spring Cloud Gateway中出现“Unsupported transfer encoding: chunked”错误的原因,指出该问题源于Feign依赖的HTTP客户端与服务端的`chunked`传输编码不兼容,并提供了具体的解决方案。通过规范Feign客户端接口的返回类型,可有效避免该异常,提升系统兼容性与稳定性。
377 0
|
XML Java 数据格式
京东一面:spring ioc容器本质是什么? ioc容器启动的步骤有哪些?
京东一面:spring ioc容器本质是什么? ioc容器启动的步骤有哪些?
|
7月前
|
XML Java 数据格式
Spring IoC容器的设计与实现
Spring 是一个功能强大且模块化的 Java 开发框架,其核心架构围绕 IoC 容器、AOP、数据访问与集成、Web 层支持等展开。其中,`BeanFactory` 和 `ApplicationContext` 是 Spring 容器的核心组件,分别定位为基础容器和高级容器,前者提供轻量级的 Bean 管理,后者扩展了事件发布、国际化等功能。
|
设计模式 前端开发 Java
了解 Spring MVC 架构、Dispatcher Servlet 和 JSP 文件的关键作用
Spring MVC 是 Spring 框架的一部分,是一个 Web 应用程序框架。它旨在使用 Model-View-Controller(MVC) 设计模式轻松构建Web应用程序。
248 0
|
存储 设计模式 前端开发
什么是SpringMVC?简单好理解!什么是应用分层?SpringMVC与应用分层的关系? 什么是三层架构?SpringMVC与三层架构的关系?
文章解释了SpringMVC的概念和各部分功能,探讨了应用分层的原因和具体实施的三层架构,以及SpringMVC与三层架构之间的关系和联系。
732 1
什么是SpringMVC?简单好理解!什么是应用分层?SpringMVC与应用分层的关系? 什么是三层架构?SpringMVC与三层架构的关系?