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

本文涉及的产品
容器镜像服务 ACR,镜像仓库100个 不限时长
简介: 【小家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 应用服务中间件
高级java面试---spring.factories文件的解析源码API机制
【11月更文挑战第20天】Spring Boot是一个用于快速构建基于Spring框架的应用程序的开源框架。它通过自动配置、起步依赖和内嵌服务器等特性,极大地简化了Spring应用的开发和部署过程。本文将深入探讨Spring Boot的背景历史、业务场景、功能点以及底层原理,并通过Java代码手写模拟Spring Boot的启动过程,特别是spring.factories文件的解析源码API机制。
86 2
|
2月前
|
数据采集 监控 前端开发
二级公立医院绩效考核系统源码,B/S架构,前后端分别基于Spring Boot和Avue框架
医院绩效管理系统通过与HIS系统的无缝对接,实现数据网络化采集、评价结果透明化管理及奖金分配自动化生成。系统涵盖科室和个人绩效考核、医疗质量考核、数据采集、绩效工资核算、收支核算、工作量统计、单项奖惩等功能,提升绩效评估的全面性、准确性和公正性。技术栈采用B/S架构,前后端分别基于Spring Boot和Avue框架。
|
29天前
|
存储 缓存 Java
Spring面试必问:手写Spring IoC 循环依赖底层源码剖析
在Spring框架中,IoC(Inversion of Control,控制反转)是一个核心概念,它允许容器管理对象的生命周期和依赖关系。然而,在实际应用中,我们可能会遇到对象间的循环依赖问题。本文将深入探讨Spring如何解决IoC中的循环依赖问题,并通过手写源码的方式,让你对其底层原理有一个全新的认识。
51 2
|
2月前
|
前端开发 Java 开发者
Spring生态学习路径与源码深度探讨
【11月更文挑战第13天】Spring框架作为Java企业级开发中的核心框架,其丰富的生态系统和强大的功能吸引了无数开发者的关注。学习Spring生态不仅仅是掌握Spring Framework本身,更需要深入理解其周边组件和工具,以及源码的底层实现逻辑。本文将从Spring生态的学习路径入手,详细探讨如何系统地学习Spring,并深入解析各个重点的底层实现逻辑。
69 9
|
2月前
|
负载均衡 算法 Java
除了 Ribbon,Spring Cloud 中还有哪些负载均衡组件?
这些负载均衡组件各有特点,在不同的场景和需求下,可以根据项目的具体情况选择合适的负载均衡组件来实现高效、稳定的服务调用。
103 5
|
3月前
|
Java Spring
Spring底层架构源码解析(三)
Spring底层架构源码解析(三)
182 5
|
3月前
|
XML Java 数据格式
Spring底层架构源码解析(二)
Spring底层架构源码解析(二)
|
3月前
|
Java Spring 容器
Spring IOC、AOP与事务管理底层原理及源码解析
【10月更文挑战第1天】Spring框架以其强大的控制反转(IOC)和面向切面编程(AOP)功能,成为Java企业级开发中的首选框架。本文将深入探讨Spring IOC和AOP的底层原理,并通过源码解析来揭示其实现机制。同时,我们还将探讨Spring事务管理的核心原理,并给出相应的源码示例。
150 9
|
3月前
|
设计模式 JavaScript Java
Spring 事件监听机制源码
Spring 提供了事件发布订阅机制,广泛应用于项目中。本文介绍了如何通过自定义事件类、订阅类和发布类实现这一机制,并展示了如何监听 SpringBoot 启动过程中的多个事件(如 `ApplicationStartingEvent`、`ApplicationEnvironmentPreparedEvent` 等)。通过掌握这些事件,可以更好地理解 SpringBoot 的启动流程。示例代码展示了从事件发布到接收的完整过程。
|
3月前
|
缓存 Java Spring
源码解读:Spring如何解决构造器注入的循环依赖?
本文详细探讨了Spring框架中的循环依赖问题,包括构造器注入和字段注入两种情况,并重点分析了构造器注入循环依赖的解决方案。文章通过具体示例展示了循环依赖的错误信息及常见场景,提出了三种解决方法:重构代码、使用字段依赖注入以及使用`@Lazy`注解。其中,`@Lazy`注解通过延迟初始化和动态代理机制有效解决了循环依赖问题。作者建议优先使用`@Lazy`注解,并提供了详细的源码解析和调试截图,帮助读者深入理解其实现机制。
76 1