RestTemplate组件:ClientHttpRequestFactory、ClientHttpRequestInterceptor、ResponseExtractor【享学Spring MVC】(中)

简介: RestTemplate组件:ClientHttpRequestFactory、ClientHttpRequestInterceptor、ResponseExtractor【享学Spring MVC】(中)

池化技术一般用于长连接,那么像Http这种适合连接池吗?

HttpClient 4.3以后中使用了PoolingHttpClientConnectionManager连接池来管理持有连接,同一条TCP链路上,连接是可以复用的。HttpClient通过连接池的方式进行连接持久化(所以它这个连接池其实是tcp的连接池。它里面有一个很重要的概念:Route的概念,代表一条线路。比如baidu.com是一个route,163.com是一个route…)。


连接池:可能是http请求,也可能是https请求

加入池话技术,就不用每次发起请求都新建一个连接(每次连接握手三次,效率太低)


AbstractClientHttpRequestFactoryWrapper


对其它ClientHttpRequestFactory的一个包装抽象类,它有如下两个子类实现


InterceptingClientHttpRequestFactory(重要)



Interceptor拦截的概念,还是蛮重要的。它持有的ClientHttpRequestInterceptor对于我们若想要拦截发出去的请求非常之重要(比如全链路压测中,可以使用它设置token之类的~)


// @since 3.1
public class InterceptingClientHttpRequestFactory extends AbstractClientHttpRequestFactoryWrapper {
  // 持有所有的请求拦截器
  private final List<ClientHttpRequestInterceptor> interceptors;
  public InterceptingClientHttpRequestFactory(ClientHttpRequestFactory requestFactory, @Nullable List<ClientHttpRequestInterceptor> interceptors) {
    super(requestFactory);
    // 拦截器只允许通过构造函数设置进来,并且并没有提供get方法方法~
    this.interceptors = (interceptors != null ? interceptors : Collections.emptyList());
  }
  // 此处返回的是一个InterceptingClientHttpRequest,显然它肯定是个ClientHttpRequest嘛~
  @Override
  protected ClientHttpRequest createRequest(URI uri, HttpMethod httpMethod, ClientHttpRequestFactory requestFactory) {
    return new InterceptingClientHttpRequest(requestFactory, this.interceptors, uri, httpMethod);
  }
}


InterceptingClientHttpRequest的execute()方法的特点是:若存在拦截器,交给给拦截器去执行发送请求return nextInterceptor.intercept(request, body, this),否则就自己上。


ClientHttpRequestInterceptor

关于请求拦截器,Spring MVC内置了两个最基础的实现


image.png


BasicAuthorizationInterceptor


// @since 4.3.1  但在Spring5.1.1后推荐使用BasicAuthenticationInterceptor
@Deprecated
public class BasicAuthorizationInterceptor implements ClientHttpRequestInterceptor {
  private final String username;
  private final String password;
  // 注意:username不允许包含:这个字符,但是密码是允许的
  public BasicAuthorizationInterceptor(@Nullable String username, @Nullable String password) {
    Assert.doesNotContain(username, ":", "Username must not contain a colon");
    this.username = (username != null ? username : "");
    this.password = (password != null ? password : "");
  }
  @Override
  public ClientHttpResponse intercept(HttpRequest request, byte[] body, ClientHttpRequestExecution execution) throws IOException {
    // 用户名密码连接起来后,用Base64对字节码进行编码~
    String token = Base64Utils.encodeToString((this.username + ":" + this.password).getBytes(StandardCharsets.UTF_8));
    // 放进请求头:key为`Authorization`  然后执行请求的发送
    request.getHeaders().add("Authorization", "Basic " + token);
    return execution.execute(request, body);
  }
}


这个拦截器木有对body有任何改动,只是把用户名、密码帮你放进了请求头上。


需要注意的是:若你的header里已经存在了Authorization这个key,这里也不会覆盖的,这会添加哦。但并不建议你有覆盖现象~


BasicAuthenticationInterceptor:

它是用来代替上类的。它使用标准的授权头来处理,参考HttpHeaders#setBasicAuth、HttpHeaders#AUTHORIZATION


public class BasicAuthenticationInterceptor implements ClientHttpRequestInterceptor {
  private final String username;
  private final String password;
  // 编码,一般不用指定
  @Nullable
  private final Charset charset;
  ... // 构造函数略
  @Override
  public ClientHttpResponse intercept(HttpRequest request, byte[] body, ClientHttpRequestExecution execution) throws IOException {
    HttpHeaders headers = request.getHeaders();
    // 只有当请求里不包含`Authorization`这个key的时候,此处才会设置授权头哦
    if (!headers.containsKey(HttpHeaders.AUTHORIZATION)) {
      // 这个方法是@since 5.1之后才提供的~~~~~
      // 若不包含此key,就设置标准的授权头(根据用户名、密码) 它内部也有这如下三步:
      // String credentialsString = username + ":" + password;
      // byte[] encodedBytes = Base64.getEncoder().encode(credentialsString.getBytes(charset));
      // String encodedCredentials = new String(encodedBytes, charset);
      // 注意:它内部最终还是调用set(AUTHORIZATION, "Basic " + encodedCredentials);这个方法的
      headers.setBasicAuth(this.username, this.password, this.charset);
    }
    return execution.execute(request, body);
  }
}


说明:这两个请求拦截器虽是Spring提供,但默认都是没有被"装配"的,所亲需要,请手动装配~


BufferingClientHttpRequestFactory

包装其它ClientHttpRequestFactory,使得具有缓存的能力。若开启缓存功能(有开关可控),会使用BufferingClientHttpRequestWrapper包装原来的ClientHttpRequest。这样发送请求后得到的是BufferingClientHttpResponseWrapper响应。

ResponseErrorHandler


用于确定特定响应是否有错误的策略接口。

// @since 3.0
public interface ResponseErrorHandler {
  // response里是否有错
  boolean hasError(ClientHttpResponse response) throws IOException;
  // 只有hasError = true时才会调用此方法
  void handleError(ClientHttpResponse response) throws IOException;
   // @since 5.0
  default void handleError(URI url, HttpMethod method, ClientHttpResponse response) throws IOException {
    handleError(response);
  }
}


继承树如下:


image.png


DefaultResponseErrorHandler

Spring对此策略接口的默认实现,RestTemplate默认使用的错误处理器就是它。


// @since 3.0
public class DefaultResponseErrorHandler implements ResponseErrorHandler {
  // 是否有错误是根据响应码来的,所以请严格遵守响应码的规范啊
  // 简单的说4xx和5xx都会被认为有错,否则是无错的  参考:HttpStatus.Series
  @Override
  public boolean hasError(ClientHttpResponse response) throws IOException {
    int rawStatusCode = response.getRawStatusCode();
    HttpStatus statusCode = HttpStatus.resolve(rawStatusCode);
    return (statusCode != null ? hasError(statusCode) : hasError(rawStatusCode));
  }
  ...
  // 处理错误
  @Override
  public void handleError(ClientHttpResponse response) throws IOException {
    HttpStatus statusCode = HttpStatus.resolve(response.getRawStatusCode());
    if (statusCode == null) {
      throw new UnknownHttpStatusCodeException(response.getRawStatusCode(), response.getStatusText(), response.getHeaders(), getResponseBody(response), getCharset(response));
    }
    handleError(response, statusCode);
  }
  // protected方法,子类对它有复写
  protected void handleError(ClientHttpResponse response, HttpStatus statusCode) throws IOException {
    String statusText = response.getStatusText();
    HttpHeaders headers = response.getHeaders();
    byte[] body = getResponseBody(response); // 拿到body,把InputStream转换为字节数组
    Charset charset = getCharset(response); // 注意这里的编码,是从返回的contentType里拿的~~~
    // 分别针对于客户端错误、服务端错误 包装为HttpClientErrorException和HttpServerErrorException进行抛出
    // 异常内包含有状态码、状态text、头、body、编码等等信息~~~~
    switch (statusCode.series()) {
      case CLIENT_ERROR:
        throw HttpClientErrorException.create(statusCode, statusText, headers, body, charset);
      case SERVER_ERROR:
        throw HttpServerErrorException.create(statusCode, statusText, headers, body, charset);
      default:
        throw new UnknownHttpStatusCodeException(statusCode.value(), statusText, headers, body, charset);
    }
  }
  ...
}


到这里就可以给大家解释一下,为何经常能看到客户端错误,然后还有状态码+一串信息了,就是因为这两个异常。


HttpClientErrorException:

public class HttpClientErrorException extends HttpStatusCodeException {
  ...
  public static HttpClientErrorException create(
      HttpStatus statusCode, String statusText, HttpHeaders headers, byte[] body, @Nullable Charset charset) {
    switch (statusCode) {
      case BAD_REQUEST:
        return new HttpClientErrorException.BadRequest(statusText, headers, body, charset);
      case UNAUTHORIZED:
        return new HttpClientErrorException.Unauthorized(statusText, headers, body, charset);
      case FORBIDDEN:
        return new HttpClientErrorException.Forbidden(statusText, headers, body, charset);
      case NOT_FOUND:
        return new HttpClientErrorException.NotFound(statusText, headers, body, charset);
      case METHOD_NOT_ALLOWED:
        return new HttpClientErrorException.MethodNotAllowed(statusText, headers, body, charset);
      case NOT_ACCEPTABLE:
        return new HttpClientErrorException.NotAcceptable(statusText, headers, body, charset);
      case CONFLICT:
        return new HttpClientErrorException.Conflict(statusText, headers, body, charset);
      case GONE:
        return new HttpClientErrorException.Gone(statusText, headers, body, charset);
      case UNSUPPORTED_MEDIA_TYPE:
        return new HttpClientErrorException.UnsupportedMediaType(statusText, headers, body, charset);
      case TOO_MANY_REQUESTS:
        return new HttpClientErrorException.TooManyRequests(statusText, headers, body, charset);
      case UNPROCESSABLE_ENTITY:
        return new HttpClientErrorException.UnprocessableEntity(statusText, headers, body, charset);
      default:
        return new HttpClientErrorException(statusCode, statusText, headers, body, charset);
    }
  }
  ...
}


它针对不同的状态码HttpStatus,创建了不同的类型进行返回,方便使用者控制,这在监控上还是蛮有意义的


BadRequest、Unauthorized、Forbidden…等等都是HttpClientErrorException的子类


HttpServerErrorException代码类似,略~


ExtractingResponseErrorHandler

继承自DefaultResponseErrorHandler。在RESTful大行其道的今天,Spring5.0开始提供了此类。它将http错误响应利用HttpMessageConverter转换为对应的RestClientException

// @since 5.0 它出现得还是很晚的。继承自DefaultResponseErrorHandler 
// 若你的RestTemplate想使用它,请调用RestTemplate#setErrorHandler(ResponseErrorHandler)设置即可
public class ExtractingResponseErrorHandler extends DefaultResponseErrorHandler {
  private List<HttpMessageConverter<?>> messageConverters = Collections.emptyList();
  // 对响应码做缓存
  private final Map<HttpStatus, Class<? extends RestClientException>> statusMapping = new LinkedHashMap<>();
  private final Map<HttpStatus.Series, Class<? extends RestClientException>> seriesMapping = new LinkedHashMap<>();
  // 构造函数、set方法给上面两个Map赋值。因为我们可以自己控制哪些状态码应该报错,哪些不应该了~
  // 以及可以自定义:那个状态码抛我们自定义的异常,哪一系列状态码抛我们自定义的异常,这个十分的便于我们做监控
  ... // 省略构造函数和set方法。。。
  // 增加缓存功能~~~  否则在交给父类
  @Override
  protected boolean hasError(HttpStatus statusCode) {
    if (this.statusMapping.containsKey(statusCode)) {
      return this.statusMapping.get(statusCode) != null;
    } else if (this.seriesMapping.containsKey(statusCode.series())) {
      return this.seriesMapping.get(statusCode.series()) != null;
    } else {
      return super.hasError(statusCode);
    }
  }
  // 这个它做的事:extract:提取
  @Override
  public void handleError(ClientHttpResponse response, HttpStatus statusCode) throws IOException {
    if (this.statusMapping.containsKey(statusCode)) {
      extract(this.statusMapping.get(statusCode), response);
    } else if (this.seriesMapping.containsKey(statusCode.series())) {
      extract(this.seriesMapping.get(statusCode.series()), response);
    } else {
      super.handleError(response, statusCode);
    }
  }
  private void extract(@Nullable Class<? extends RestClientException> exceptionClass, ClientHttpResponse response) throws IOException {
    if (exceptionClass == null) {
      return;
    }
    // 这里使用到了ResponseExtractor返回值提取器,从返回值里提取内容(本文是提取异常)
    HttpMessageConverterExtractor<? extends RestClientException> extractor =
        new HttpMessageConverterExtractor<>(exceptionClass, this.messageConverters);
    RestClientException exception = extractor.extractData(response);
    if (exception != null) { // 若提取到了异常信息,抛出即可
      throw exception;
    }
  }
}


若你想定制请求异常的处理逻辑,你也是可以自定义这个接口的实现的,当然还是建议你通过继承DefaultResponseErrorHandler来扩展~

相关文章
|
15天前
|
前端开发 Java 微服务
《深入理解Spring》:Spring、Spring MVC与Spring Boot的深度解析
Spring Framework是Java生态的基石,提供IoC、AOP等核心功能;Spring MVC基于其构建,实现Web层MVC架构;Spring Boot则通过自动配置和内嵌服务器,极大简化了开发与部署。三者层层演进,Spring Boot并非替代,而是对前者的高效封装与增强,适用于微服务与快速开发,而深入理解Spring Framework有助于更好驾驭整体技术栈。
|
7月前
|
NoSQL 安全 Java
深入理解 RedisConnectionFactory:Spring Data Redis 的核心组件
在 Spring Data Redis 中,`RedisConnectionFactory` 是核心组件,负责创建和管理与 Redis 的连接。它支持单机、集群及哨兵等多种模式,为上层组件(如 `RedisTemplate`)提供连接抽象。Spring 提供了 Lettuce 和 Jedis 两种主要实现,其中 Lettuce 因其线程安全和高性能特性被广泛推荐。通过手动配置或 Spring Boot 自动化配置,开发者可轻松集成 Redis,提升应用性能与扩展性。本文深入解析其作用、实现方式及常见问题解决方法,助你高效使用 Redis。
674 4
|
8月前
|
安全 Java 数据安全/隐私保护
微服务——SpringBoot使用归纳——Spring Boot中集成 Shiro——Shiro 三大核心组件
本课程介绍如何在Spring Boot中集成Shiro框架,主要讲解Shiro的认证与授权功能。Shiro是一个简单易用的Java安全框架,用于认证、授权、加密和会话管理等。其核心组件包括Subject(认证主体)、SecurityManager(安全管理员)和Realm(域)。Subject负责身份认证,包含Principals(身份)和Credentials(凭证);SecurityManager是架构核心,协调内部组件运作;Realm则是连接Shiro与应用数据的桥梁,用于访问用户账户及权限信息。通过学习,您将掌握Shiro的基本原理及其在项目中的应用。
276 0
|
8月前
|
前端开发 Java 测试技术
微服务——SpringBoot使用归纳——Spring Boot中的MVC支持——@RequestParam
本文介绍了 `@RequestParam` 注解的使用方法及其与 `@PathVariable` 的区别。`@RequestParam` 用于从请求中获取参数值(如 GET 请求的 URL 参数或 POST 请求的表单数据),而 `@PathVariable` 用于从 URL 模板中提取参数。文章通过示例代码详细说明了 `@RequestParam` 的常用属性,如 `required` 和 `defaultValue`,并展示了如何用实体类封装大量表单参数以简化处理流程。最后,结合 Postman 测试工具验证了接口的功能。
410 0
微服务——SpringBoot使用归纳——Spring Boot中的MVC支持——@RequestParam
|
8月前
|
JSON 前端开发 Java
微服务——SpringBoot使用归纳——Spring Boot中的MVC支持——@RequestBody
`@RequestBody` 是 Spring 框架中的注解,用于将 HTTP 请求体中的 JSON 数据自动映射为 Java 对象。例如,前端通过 POST 请求发送包含 `username` 和 `password` 的 JSON 数据,后端可通过带有 `@RequestBody` 注解的方法参数接收并处理。此注解适用于传递复杂对象的场景,简化了数据解析过程。与表单提交不同,它主要用于接收 JSON 格式的实体数据。
633 0
|
4月前
|
前端开发 Java API
Spring Cloud Gateway Server Web MVC报错“Unsupported transfer encoding: chunked”解决
本文解析了Spring Cloud Gateway中出现“Unsupported transfer encoding: chunked”错误的原因,指出该问题源于Feign依赖的HTTP客户端与服务端的`chunked`传输编码不兼容,并提供了具体的解决方案。通过规范Feign客户端接口的返回类型,可有效避免该异常,提升系统兼容性与稳定性。
272 0
|
4月前
|
JSON 前端开发 Java
Spring MVC 核心组件与请求处理机制详解
本文解析了 Spring MVC 的核心组件及请求流程,核心组件包括 DispatcherServlet(中央调度)、HandlerMapping(URL 匹配处理器)、HandlerAdapter(执行处理器)、Handler(业务方法)、ViewResolver(视图解析),其中仅 Handler 需开发者实现。 详细描述了请求执行的 7 步流程:请求到达 DispatcherServlet 后,经映射器、适配器找到并执行处理器,再通过视图解析器渲染视图(前后端分离下视图解析可省略)。 介绍了拦截器的使用(实现 HandlerInterceptor 接口 + 配置类)及与过滤器的区别
265 0
|
4月前
|
SQL Java 数据库连接
Spring、SpringMVC 与 MyBatis 核心知识点解析
我梳理的这些内容,涵盖了 Spring、SpringMVC 和 MyBatis 的核心知识点。 在 Spring 中,我了解到 IOC 是控制反转,把对象控制权交容器;DI 是依赖注入,有三种实现方式。Bean 有五种作用域,单例 bean 的线程安全问题及自动装配方式也清晰了。事务基于数据库和 AOP,有失效场景和七种传播行为。AOP 是面向切面编程,动态代理有 JDK 和 CGLIB 两种。 SpringMVC 的 11 步执行流程我烂熟于心,还有那些常用注解的用法。 MyBatis 里,#{} 和 ${} 的区别很关键,获取主键、处理字段与属性名不匹配的方法也掌握了。多表查询、动态
133 0
|
4月前
|
JSON 前端开发 Java
第05课:Spring Boot中的MVC支持
第05课:Spring Boot中的MVC支持
207 0
|
开发框架 前端开发 .NET
ASP.NET CORE 3.1 MVC“指定的网络名不再可用\企图在不存在的网络连接上进行操作”的问题解决过程
ASP.NET CORE 3.1 MVC“指定的网络名不再可用\企图在不存在的网络连接上进行操作”的问题解决过程
412 0

热门文章

最新文章

下一篇
开通oss服务