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

本文涉及的产品
容器镜像服务 ACR,镜像仓库100个 不限时长
容器服务 Serverless 版 ACK Serverless,317元额度 多规格
容器服务 Serverless 版 ACK Serverless,952元额度 多规格
简介: 【小家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转换器为例加以说明:

相关文章
|
4天前
|
缓存 Java Spring
手写Spring Ioc 循环依赖底层源码剖析
在Spring框架中,IoC(控制反转)是一个核心特性,它通过依赖注入(DI)实现了对象间的解耦。然而,在实际开发中,循环依赖是一个常见的问题。
13 4
|
9天前
|
XML 缓存 Java
spring源码剖析-spring-beans(内部核心组件,BeanDefinition的注册,BeanWapper创建)
spring源码剖析-spring-beans(内部核心组件,BeanDefinition的注册,BeanWapper创建)
38 10
|
9天前
|
XML 存储 Java
Spring-源码深入分析(二)
Spring-源码深入分析(二)
|
9天前
|
XML 设计模式 Java
Spring-源码深入分析(一)
Spring-源码深入分析(一)
|
1月前
|
数据库 开发者 Python
web应用开发
【9月更文挑战第1天】web应用开发
40 1
|
21天前
|
数据可视化 图形学 UED
只需四步,轻松开发三维模型Web应用
为了让用户更方便地应用三维模型,阿里云DataV提供了一套完整的三维模型Web模型开发方案,包括三维模型托管、应用开发、交互开发、应用分发等完整功能。只需69.3元/年,就能体验三维模型Web应用开发功能!
41 8
只需四步,轻松开发三维模型Web应用
|
11天前
|
安全 API 开发者
Web 开发新风尚!Python RESTful API 设计与实现,让你的接口更懂开发者心!
在当前的Web开发中,Python因能构建高效简洁的RESTful API而备受青睐,大大提升了开发效率和用户体验。本文将介绍RESTful API的基本原则及其在Python中的实现方法。以Flask为例,演示了如何通过不同的HTTP方法(如GET、POST、PUT、DELETE)来创建、读取、更新和删除用户信息。此示例还包括了基本的路由设置及操作,为开发者提供了清晰的API交互指南。
45 6
|
10天前
|
存储 JSON API
实战派教程!Python Web开发中RESTful API的设计哲学与实现技巧,一网打尽!
在数字化时代,Web API成为连接前后端及构建复杂应用的关键。RESTful API因简洁直观而广受欢迎。本文通过实战案例,介绍Python Web开发中的RESTful API设计哲学与技巧,包括使用Flask框架构建一个图书管理系统的API,涵盖资源定义、请求响应设计及实现示例。通过准确使用HTTP状态码、版本控制、错误处理及文档化等技巧,帮助你深入理解RESTful API的设计与实现。希望本文能助力你的API设计之旅。
33 3
|
11天前
|
JSON API 数据库
从零到英雄?一篇文章带你搞定Python Web开发中的RESTful API实现!
在Python的Web开发领域中,RESTful API是核心技能之一。本教程将从零开始,通过实战案例教你如何使用Flask框架搭建RESTful API。首先确保已安装Python和Flask,接着通过创建一个简单的用户管理系统,逐步实现用户信息的增删改查(CRUD)操作。我们将定义路由并处理HTTP请求,最终构建出功能完整的Web服务。无论是初学者还是有经验的开发者,都能从中受益,迈出成为Web开发高手的重要一步。
33 4
|
9天前
|
开发框架 JSON 缓存
震撼发布!Python Web开发框架下的RESTful API设计全攻略,让数据交互更自由!
在数字化浪潮推动下,RESTful API成为Web开发中不可或缺的部分。本文详细介绍了在Python环境下如何设计并实现高效、可扩展的RESTful API,涵盖框架选择、资源定义、HTTP方法应用及响应格式设计等内容,并提供了基于Flask的示例代码。此外,还讨论了版本控制、文档化、安全性和性能优化等最佳实践,帮助开发者实现更流畅的数据交互体验。
29 1
下一篇
无影云桌面