kubernetes开发运维系列_基础篇-容器化开发基础

 

 

Kubernetes 开发入门

依赖准备

核心依赖

<dependency>
    <groupId>io.fabric8</groupId>
    <artifactId>kubernetes-client</artifactId>
    <version>4.12.0</version>
</dependency>

辅助依赖

<dependency>
    <groupId>org.apache.httpcomponents</groupId>
    <artifactId>httpcore</artifactId>
    <version>4.4.10</version>
</dependency>
<dependency>
    <groupId>com.alibaba</groupId>
    <artifactId>fastjson</artifactId>
    <version>1.2.75</version>
</dependency>

<dependency>
    <groupId>org.apache.httpcomponents</groupId>
    <artifactId>httpclient</artifactId>
    <version>4.5.6</version>
</dependency>

 

代码演示及分析

客户端的创建

Kubernetes 的资源默认是对内部是可见并可以访问的,我们可以通过以下方式去访问Kubernetes 获取我们想要的资源。

1.对于Kubernetes 可以使用 JavaAPI 通过安全证书文件、Token或admin.conf配置外部访问

2.同样也可以配置这些配置文件通过HTTP访问apiService访问

3.内部也可以通过kube-proxy将内部数据暴露出来进行访问的方式。

 

通过安全证书访问

需要安全证书文件:apiserver-kubelet-client.crt、apiserver-kubelet-client.key、ca.crt

 

//创建 Config 用于创建 Client
Config config = new ConfigBuilder()
        .withMasterUrl("ip")
        .withCaCertData("ca.crt内容")
        .withClientCertData("apiserver-kubelet-client.crt内容")
        .withClientKeyData("apiserver-kubelet-client.key内容")
        .build();
// 创建Client
KubernetesClient kubernetesClient = new DefaultKubernetesClient(config);

 

 

通过配置文件访问

需要配置文件:admin.conf

Config config = Config.fromKubeconfig("admin.conf内容");
KubernetesClient kubernetesClient = new DefaultKubernetesClient(config);

 

 

kube-proxy 配置后通过 HTTP 直接访问

首先在服务器中开启kube-proxy代理暴露8080端口:nohup kubectl proxy --port=8080 &

public void main(String[] args) throws IOException {
    Config config = new ConfigBuilder().withMasterUrl("http://127.0.0.1:8080").build();
    KubernetesClient kubernetesClient = new DefaultKubernetesClient(config);
    //通过http访问k8s中的路径资源
    HttpClient httpClient = HttpClientBuilder.create().build();

    HttpGet httpGet = new HttpGet("http://127.0.0.1:8080/apis/devices.kubeedge.io/v1alpha1/devices");
    HttpResponse response = null;
    response = httpClient.execute(httpGet);
    HttpEntity responseEntity = response.getEntity();
    StatusLine statusLine = response.getStatusLine();
    int code = statusLine.getStatusCode();
    String result = EntityUtils.toString(responseEntity);
}

 

 

http浏览器直接访问K8S资源

安装证书

访问操作pod、node等基础资源

public static void main(String[] args) {
    KubernetesClient client = null;
    //查看Pod
    MixedOperation<Pod, PodList, DoneablePod, PodResource<Pod, DoneablePod>> operation = client.pods();
    //创建Pod,获取资源处理类,在传入组装号的Pod
    NonNamespaceOperation pods = client.pods().inNamespace("default");
    //配置Pod,还可以通过 pod 类组装,想要运行 这里的参数是不够的,仅作演示
    Pod pod1 = new PodBuilder().withNewMetadata().withName("pod1").withNamespace("default").and().build();
    pods.create(pod1);
    //删除同上
    pods.delete(pod1);
}

 

访问操作复杂管理器Deployment控制器

封装资源组装

public class K8sDeploymentConf {

    /**
     * 用于组合 Deploymen
     * @param appName
     * @param image
     * @param nodeName
     * @return
     */
    public static Deployment getDepandDeployment(String appName, String image, String nodeName) {
        //参数传递
        String appGroup = "appGroup";
        //参数
        Map<String, String> labels = new HashMap<String, String>();
        labels.put("app", appGroup);
        Map<String, String> nodeSelector = new HashMap<String, String>();
        nodeSelector.put("name", nodeName);
        //mataData 数据组装
        ObjectMeta mataData = new ObjectMeta();
        mataData.setName(appName);
        mataData.setLabels(labels);
        //镜像设置
        Container container = new Container();
        container.setName(appName);
        container.setImage(image);
        container.setImagePullPolicy("IfNotPresent");
        SecurityContext sc = new SecurityContext();
        sc.setPrivileged(true);
        container.setSecurityContext(sc);
        List<Container> containers = new ArrayList<>();
        containers.add(container);
        //Spec 数据组装
        //1.selector
        LabelSelector ls = new LabelSelector();
        ls.setMatchLabels(labels);
        //2.template
        ObjectMeta empMataData = new ObjectMeta();
        empMataData.setLabels(labels);
        PodSpec pods = new PodSpec();
        pods.setHostNetwork(true);
        pods.setNodeSelector(nodeSelector);
        pods.setContainers(containers);
        //2.2 组装
        PodTemplateSpec pt = new PodTemplateSpec();
        pt.setMetadata(empMataData);
        pt.setSpec(pods);
        //3.spec 组合
        DeploymentSpec ds = new DeploymentSpec();
        ds.setReplicas(1);
        ds.setSelector(ls);
        ds.setTemplate(pt);
        //Deployment 设置
        Deployment deployment = new Deployment();
        deployment.setApiVersion("apps/v1");
        deployment.setKind("Deployment");
        deployment.setMetadata(mataData);
        deployment.setSpec(ds);
        return deployment;
    }

}

 

 

Deployment操作

public static void main(String[] args) {
    KubernetesClient client = null;
    //查看Pod
    MixedOperation<Pod, PodList, DoneablePod, PodResource<Pod, DoneablePod>> operation = client.pods();
    //创建Pod,获取资源处理类,在传入组装号的Pod
    NonNamespaceOperation pods = client.pods().inNamespace("default");
    //配置Pod,还可以通过 pod 类组装,想要运行 这里的参数是不够的,仅作演示
    Pod pod1 = new PodBuilder().withNewMetadata().withName("pod1").withNamespace("default").and().build();
    pods.create(pod1);
    //删除同上
    pods.delete(pod1);
}

 

RestTemplate

若果是完全采用SpringBoot开发,可以基于RestTemplate调用K8S的API操作

RestTemplate只是对其他的HTTP客户端的封装,其本身并没有实现HTTP相关的基础功能。其底层实现是可以配置切换的

默认 RestTemplate进行http相关的请求的最底层的实现是利用的java原生的api java.net.URLConnection等实现的

 

原生JDK中的http请求处理

/**
 * java 原生http请求
 */
public class NativeHttp {

    public static void main(String[] args) {
        postDemo();
    }

    /**
     * POST请求
     */
    public static void postDemo() {
        try {
            // 请求的地址
            String spec = "http://localhost:8080/test022";
            // 根据地址创建URL对象
            URL url = new URL(spec);
            // 根据URL对象打开链接
            HttpURLConnection urlConnection = (HttpURLConnection) url
                    .openConnection();
            // 设置请求方法
            urlConnection.setRequestMethod("POST");
            // 设置请求的超时时间
            urlConnection.setReadTimeout(5000);
            urlConnection.setConnectTimeout(5000);
            // 传递的数据
            String data = "name=" + URLEncoder.encode("aaa", "UTF-8")
                    + "&school=" + URLEncoder.encode("bbbb", "UTF-8");
            // 设置请求的头
            urlConnection.setRequestProperty("Connection", "keep-alive");
            urlConnection.setRequestProperty("Content-Type", "application/x-www-form-urlencoded");
            urlConnection.setRequestProperty("Content-Length", String.valueOf(data.getBytes().length));
            // 发送POST请求必须设置允许输出
            urlConnection.setDoOutput(true);
            // 发送POST请求必须设置允许输入
            urlConnection.setDoInput(true);
            //setDoInput的默认值就是true
            //获取输出流,将参数写入
            OutputStream os = urlConnection.getOutputStream();
            os.write(data.getBytes());
            os.flush();
            os.close();
            urlConnection.connect();
            if (urlConnection.getResponseCode() == 200) {
                // 获取响应的输入流对象
                InputStream is = urlConnection.getInputStream();
                // 创建字节输出流对象
                ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream();
                // 定义读取的长度
                int len = 0;
                // 定义缓冲区
                byte[] buffer = new byte[1024];
                // 按照缓冲区的大小,循环读取
                while ((len = is.read(buffer)) != -1) {
                    // 根据读取的长度写入到os对象中
                    byteArrayOutputStream.write(buffer, 0, len);
                }
                // 释放资源
                is.close();
                byteArrayOutputStream.close();
                // 返回字符串
                String result = new String(byteArrayOutputStream.toByteArray());
                System.out.println(result);

            } else {
                System.out.println("请求失败");
            }
        } catch (Exception e) {
            e.printStackTrace();
        }
    }

}

@RequestMapping("/test022")
public Notice test022(String name,String school){
    Notice notice = new Notice();
    System.out.println(name+school);
    notice.setMsg("ssstest022");
    return notice;
}

 

 

了解了java原生的http请求对解析RestTemplate源码很有帮助。

 实现架构原理

概述

在介绍 RestTemplate 之前,我们先来谈谈 HTTP Client,谈谈选择一个优秀的 HTTP Client 实现的的重要性,以及一个优秀的 HTTP Client 应该具备哪些特性。

在 Java 社区中,HTTP Client 主要有 JDK 的 HttpURLConnection、Apache Commons HttpClient(或被称为 Apache HttpClient 3.x)、Apache HttpComponents Client(或被称为 Apache HttpClient 4.x)、Square 公司开源的 OkHttp。

除了这几个纯粹的 HTTP Client 类库以外,还有 Spring 的 RestTemplate、Square 公司的 Retrofit、Netflix 公司的 Feign,以及像 Apache CXF 中的 client 组件。这些框架和类库主要是针对 Web Service 场景,尤其是 RESTful Web Service。它们往往是基于前面提到的 HTTP Client 实现,并在其基础上提供了消息转换、参数映射等对于 Web Service 来说十分必要的功能。

(当然,像 Netty、Mina 这样的网络 IO 框架,实现 HTTP 自然也不再话下,但这些框架通常过于底层,不会被直接使用)

 

选择一个优秀的 HTTP Client 的重要性

虽然现在服务间的调用越来越多地使用了 RPC 和消息队列,但是 HTTP 依然有适合它的场景。

RPC 的优势在于高效的网络传输模型(常使用 NIO 来实现),以及针对服务调用场景专门设计协议和高效的序列化技术。而 HTTP 的优势在于它的成熟稳定、使用实现简单、被广泛支持、兼容性良好、防火墙友好、消息的可读性高。所以在开放 API、跨平台的服务间调用、对性能要求不苛刻的场景中有着广泛的使用。

正式因为 HTTP 存在着很广泛的应用场景,所以选择一个优秀的 HTTP Client 便是十分重要的。

 

优秀的 HTTP Client 需要具备的特性

 

  1. 连接池
  2. 超时时间设置(连接超时、读取超时等)
  3. 是否支持异步
  4. 请求和响应的编解码
  5. 可扩展性

 

连接池

因为目前 HTTP 1.1 不支持多路复用,只有 HTTP Pipeline 这用半复用的模型支持。所以,在需要频繁发送消息的场景中,连接池是必须支持的,以减少频繁建立连接所带来的不必要的性能损耗。

超时时间设置(连接超时、读取超时等)

当对端出现问题的时候,长时间的,甚至是无限的超时等待是绝对不能接受的。所以必须必须能够设置超时时间。

 

是否支持异步

HTTP 相关技术(服务器端和客户端)通常被人认为是性能低下的一个重要原因在于,在很长一段时间里,HTTP 的相关实现缺乏对异步的支持。这不仅指非阻塞 IO,也包括异步的编程模型。缺乏异步编程模型的后果就是,即便 HTTP 协议栈是基于非阻塞 IO 实现的,调用客户端的或者在服务端处理消息的线程有大量时间被浪费在了等待 IO 上面。所以,异步是非常重要的特性。

请求和响应的编解码

通常,开发人员希望面向对象使用各种服务(这里面自然也包括基于 HTTP 协议的服务),而不是直接面对原始的消息和响应开发。所以,透明地将 HTTP 请求和响应进行编解码是十分有必要,因为这可以很大程度地降低开发人员的工作量。

可扩展性

不论一个框架设计的多好,总有一些特殊场景是它们无法原生支持的。这时可扩展性的好坏便体现出来了。

总结

基于上述几点的考虑,RestTemplate 是相对好的选择。原因在于 RestTemplate 本身基于成熟的 HTTP Client 实现(Apache HttpClient、OkHttp 等),并可以灵活地在这些实现中切换,而且具有良好的扩展性。最重要的是提供了前面几个 HTTP Client 不具备的消息编解码能力。

这里要提一句为什么没有自己封装 HTTP Client 的原因。这个原因在于想要基于一种 HTTP Client 去提供消息编解码能力和一定的扩展能力并不难,但是如果要设计出一个通用的,对底层实现透明的,具有优秀如 Spring 的扩展性设计的框架并不是一件容易事。这里的不易并不在于技术有多高深,而是在于优秀的扩展性设计往往源自从众多优秀程序员、社区和软件公司得到的丰富的一线实践经验,再由像 Spring 转换为最终设计。这样的产品不是一朝一夕就能得到的。在我们觉得自己打造自己的工具之前,我们可以先深入了解现有的优秀功能都能做到什么。

 

使用 RestTemplate 的缺点

 

欲扬先抑,我们先来看加入使用 RestTemplate,可能会遇到哪些“坑”。

依赖 Spring 其它模块

虽然 spring-web 模块对其它 Spring 模块并没有显式的依赖(Maven dependency 的 scope 为 compile),但是对于一些功能,比如异步版本的 RestTemplate,要求必须有 4.1 以上版本的 spring-core 模块。

所以,要想 RestTemplate 完全发挥其功能,最好能有相近版本的其它的 Spring 模块相配合(spring-core、spring-context、spring-beans、spring-aop)

默认情况下 RestTemplate 存在的不足

Spring Web 模块中的 RestTemplate 是一个很不错的面向 RESTful Web 服务的客户端。它提供了很多简化对 RESTful Web 服务调用的功能,例如 Path Parameter 的格式化功能(/hotels/{hotel_id}/books/{book_id},这里的 hotel_id 和 book_id 就是 Path Paramter)、JSON 或 XML 等格式的数据与实体类之间的透明转换等。

所谓默认情况指的是不去扩展 RestTemplate 所提供的类或接口,而是完全依赖其本身提供的代码。在这种情况下,RestTemplate 还是有一些不便的地方。例如,它的 Path Parameter 格式化功能,对于普通 HTTP 服务的调用来说,反而成为了一个缺点,因为普通的 HTTP 服务的 GET 方法常使用 Query Parameter,而不是 Path Parameter。Query Paramter 的形式是 an_http_url?name1=value1&name2=value2。例如 getOrder.action?order_code=xxx。如果使用 RestTemplate,作为参数传递给 RestTemplate 的 URL 就必须是 getOrder.action?order_code={order_code}。如果是固定的参数还好,如果一个 HTTP 服务的 Query Parameter 是可变的,那就很不方便了。

 

扩展 RestTemplate

注意,下面涉及到的代码都是基于 spring-web 4.2.6.RELEASE 版本

设置 Query Params

上面提到,RestTemplate 的 getForEntity、getForObject、postForEntity 等方法中的 Map 参数是 uriVariables,即我们常说的 Path Param,而非 Query Param(这两个参数的定义可以参照 JAX-RS 中 @PathParam 和 @QueryParam 的定义)。

 

Path Param 是 URL 的一部分,RESTful 的 Web Service 会按照其定义的 URL Template 从 URL 中解析出其对应的值

RestTemplate 的这种机制面对 RESTful 的 Web Service 无疑是方便的,但很多情况下我们还是希望 RestTemplate 能够在开发人员不用编写额外代码的情况下将 Map 类型的参数当做 Query Param 发送给对端的服务。

幸好来自 Spring 大家庭的 RestTemplate 也具有良好的可扩展性,其具有一个名为 UriTemplateHandler 扩展点。因为不论是 Path Param 还是 Query Param,它们都是 URI 的一部分,所以只需实现自定义的 URI 生成机制即可解决这个问题。

 

通过扩展 DefaultUriTemplateHandler,我们可以将 Map<String, ?> uriVariables 也作为 Query Param。具体实现如下:

 

public class QueryParamsUrlTemplateHandler extends DefaultUriTemplateHandler {
    @Override
    public URI expand(String uriTemplate, Map<String, ?> uriVariables) {
        UriComponentsBuilder uriComponentsBuilder = UriComponentsBuilder.fromHttpUrl(uriTemplate);
        for (Map.Entry<String, ?> varEntry : uriVariables.entrySet()) {
            uriComponentsBuilder.queryParam(varEntry.getKey(), varEntry.getValue());
        }
        uriTemplate = uriComponentsBuilder.build().toUriString();
        return super.expand(uriTemplate, uriVariables);
    }
}

 

上面的实现基于 DefaultUriTemplateHandler,所以保有了原来设置 Path Param 的功能。

 

设置自定义的 HTTP Header

实现这个需求有多种方法,比如通过拦截器。这里使用另一个方法,通过一个自定义的 ClientHttpRequestFactory

 

public class CustomHeadersClientHttpRequestFactoryWrapper extends AbstractClientHttpRequestFactoryWrapper {
    private HttpHeaders customHeaders = new HttpHeaders();

    /**
     * Create a { @code AbstractClientHttpRequestFactoryWrapper} wrapping the given request factory.
     *
     * @param requestFactory the request factory to be wrapped
     */
    protected CustomHeadersClientHttpRequestFactoryWrapper(ClientHttpRequestFactory requestFactory) {
        super(requestFactory);
    }

    @Override
    protected ClientHttpRequest createRequest(URI uri, HttpMethod httpMethod,
                                              ClientHttpRequestFactory requestFactory) throws IOException {
        ClientHttpRequest request = requestFactory.createRequest(uri, httpMethod);

        for (Map.Entry<String, List<String>> headerEntry : customHeaders.entrySet()) {
            request.getHeaders().put(headerEntry.getKey(), headerEntry.getValue());
        }

        return request;
    }

    public void addHeader(String header, String... values) {
        customHeaders.put(header, Arrays.asList(values));
    }
}

 

RestTemplate 原理解析

 

HTTP Client 实现

RestTemplate 本身并没有做 HTTP 底层的实现,而是利用了现有的技术,如 JDK 或 Apache HttpClient 等。

RestTemplate 需要使用一个实现了 ClientHttpRequestFactory 接口的类为其提供 ClientHttpRequest 实现(另外还有 AsyncClientHttpRequestFactory 对应于异步 HTTP 实现,这里暂且不介绍)。而 ClientHttpRequest 则实现封装了组装消息、发送 HTTP 消息,以及解析响应的的底层细节。

目前(4.2.6.RELEASE)的 RestTemplate 主要有四种 ClientHttpRequestFactory 的实现,它们分别是:

 

  • 基于 JDK HttpURLConnection 的 SimpleClientHttpRequestFactory
  • 基于 Apache HttpComponents Client 的 HttpComponentsClientHttpRequestFactory
  • 基于 OkHttp 2(OkHttp 最新版本为 3,有较大改动,包名有变动,不和老版本兼容)的 OkHttpClientHttpRequestFactory
  • 基于 Netty4 的 Netty4ClientHttpRequestFactory
  • 另外,还有用于提供拦截器功能的 InterceptingClientHttpRequestFactory。

 

写消息

写消息指的是 requestBody 转换为某一种格式,如 JSON、XML 的数据的过程。

spring-web 模块提供了一个 HttpMessageConverter 接口,用来读写 HTTP 消息。这个接口不仅被 RestTemplate 使用,也被 Spring MVC 所使用。

spring-web 模块提供了基于 Jackson、GSON 等类库的 HttpMessageConverter,用于进行 JSON 或 XML 格式数据的转换。

RestTemplate 在发送消息时,会根据消息的 ContentType 或者 RequestBody 对象本身的一些属性判断究竟是使用哪个 HttpMessageConverter 写消息。

具体来说,如果 RequestBody 是一个 HttpEntity 的话,会从中读取 ContentType 属性。同时,RequestBody 对象本身也会决定一个 HttpMessageConverter 是否会处理这个对象。例如,ProtobufHttpMessageConverter

会要求RequestBody 对象必须实现 com.google.protobuf.Message 接口 。

读消息

读消息指的是读取 HTTP Response 中的数据,转换为用户指定的格式(通过 Class<T> responseType 参数指定)。类似于写消息的处理,读消息的处理也是通过 ContentType 和 responseType 来选择的相应 HttpMessageConverter 来进行的。

错误处理

RestTemplate 提供了一个 ResponseErrorHandler 的接口,用来处理错误的 Response。可以通过设置自定义的 ResponseErrorHandler 来实现扩展。

总结

根据我上面表达的思想,一个统一、规范和简化 RestTemplate 使用的工具已经产生,不过暂时由于其代码是公司项目的一部分,所以暂时不便公开。而且我希望是在这个工具经过了更多的实践考验之后再贡献出来会更好。

目前的一个完整使用案例如下:

@Configuration
public class SpringConfigurationDemo {

    @Bean
    public RestTemplate myRestTemplate() {
        return RestTemplateBuilder.create()
                .withClientKey("myRestTemplate")
                .implementation(HttpClientImplementation.OK_HTTP)
                .clearMessageConverters()
                .setMessageConverter(new MappingJackson2HttpMessageConverter(), MediaType.TEXT_PLAIN)
                .enableAutoQueryParams()
                .connectTimeout(100)
                .readTimeout(200)
                .header(HttpHeaders.USER_AGENT, "MyAgent")
                .build();
    }
}

 

虽然 RestTemplate 是一个很不错的 HTTP Client,但 Netflix 已经开源了一个更好地 HTTP Client 工具 - Feign。它是一个声明式的 HTTP Client,在易用性、可读性等方面大幅领先于现有的工具。我打算稍后写一篇文章分析 Feign 的思想、原理和优点(原理其实不复杂,但是能想到这么做的却没几个,原创的创新思想永远是最可贵的)

源码分析

核心doExecute方法

无论用RestTemplate进行get、post、delete、put等http请求他都要走各种execute重载方法,而任何execute方法都会走doExecute这个方法,看下他的源码

 

/**
 * Execute the given method on the provided URI.
 * <p>The { @link ClientHttpRequest} is processed using the { @link RequestCallback};
 * the response with the { @link ResponseExtractor}.
 * @param url the fully-expanded URL to connect to
 * @param method the HTTP method to execute (GET, POST, etc.)
 * @param requestCallback object that prepares the request (can be { @code null})
 * @param responseExtractor object that extracts the return value from the response (can be { @code null})
 * @return an arbitrary object, as returned by the { @link ResponseExtractor}
 */
@Nullable
protected <T> T doExecute(URI url, @Nullable HttpMethod method, @Nullable RequestCallback requestCallback,
      @Nullable ResponseExtractor<T> responseExtractor) throws RestClientException {

   Assert.notNull(url, "URI is required");
   Assert.notNull(method, "HttpMethod is required");
   ClientHttpResponse response = null;
   try {
      ClientHttpRequest request = createRequest(url, method);
      if (requestCallback != null) {
         requestCallback.doWithRequest(request);
      }
      response = request.execute();
      handleResponse(url, method, response);
      return (responseExtractor != null ? responseExtractor.extractData(response) : null);
   }
   catch (IOException ex) {
      String resource = url.toString();
      String query = url.getRawQuery();
      resource = (query != null ? resource.substring(0, resource.indexOf('?')) : resource);
      throw new ResourceAccessException("I/O error on " + method.name() +
            " request for \"" + resource + "\": " + ex.getMessage(), ex);
   }
   finally {
      if (response != null) {
         response.close();
      }
   }
}

 

分别解释下各个参数的意义:

URI url:请求的http地址,会利用url.openConnection()来链接。

HttpMethod method:这个就比较简单了,代表http请求的方法,GET, HEAD, POST, PUT, PATCH, DELETE, OPTIONS, TRACE。

RequestCallback requestCallback:这个会在进行http请求前处理好http请求的head,如果是post这种带body的请求也会做好body的处理。常见的中文乱码的问题就是在这发生的,当然,涉及到的类不止他一个。

ResponseExtractor<T> responseExtractor:这个是用来处理http的response的,当我们直接通过log.error("http response error,{}",e)打印出来的日志不是我们想要的完整日志,就是这边捣的鬼。

因为doExecute方法实际也没几行,所以我们一行行分析;

 

ClientHttpRequest request = createRequest(url, method)

/**
 * Create a new { @link ClientHttpRequest} via this template's { @link ClientHttpRequestFactory}.
 * @param url the URL to connect to
 * @param method the HTTP method to execute (GET, POST, etc)
 * @return the created request
 * @throws IOException in case of I/O errors
 * @see #getRequestFactory()
 * @see ClientHttpRequestFactory#createRequest(URI, HttpMethod)
 */
protected ClientHttpRequest createRequest(URI url, HttpMethod method) throws IOException {
   ClientHttpRequest request = getRequestFactory().createRequest(url, method);
   initialize(request);
   if (logger.isDebugEnabled()) {
      logger.debug("HTTP " + method.name() + " " + url);
   }
   return request;
}

 

 

他的注释是这样的Create a new {@link ClientHttpRequest} via this template's {@link ClientHttpRequestFactory}。这个很好理解,通过ClientHttpRequestFactory获取了一个ClientHttpRequest的实例。

 

首先是

    public ClientHttpRequestFactory getRequestFactory() {

        return this.requestFactory;

    }

this.requestFactory就是取得全局变量 private ClientHttpRequestFactory requestFactory = new SimpleClientHttpRequestFactory();这边可以看到默认的是SimpleClientHttpRequestFactory,那么如果我想换个ClientHttpRequestFactory呢,答案就是在创建RestTemplate实例时指定ClientHttpRequestFactory,当然这个是在配置时做。

 

    @Bean

    public RestTemplate restTemplate(ClientHttpRequestFactory factory) {

        return new RestTemplate(factory);

    }

 

 这边我们以SimpleClientHttpRequestFactory继续往下走。

 

@Override
public ClientHttpRequest createRequest(URI uri, HttpMethod httpMethod) throws IOException {
   HttpURLConnection connection = openConnection(uri.toURL(), this.proxy);
   prepareConnection(connection, httpMethod.name());

   if (this.bufferRequestBody) {
      return new SimpleBufferingClientHttpRequest(connection, this.outputStreaming);
   }
   else {
      return new SimpleStreamingClientHttpRequest(connection, this.chunkSize, this.outputStreaming);
   }
}

 

 

HttpURLConnection connection = openConnection(uri.toURL(), this.proxy);这一步就是执行了url.openConnection(),

prepareConnection(connection, httpMethod.name());这一步设置了http请求的方法,同时判断请求的方法做一些java原生态api需要做的设置,具体如下:

 

 

/**
 * Template method for preparing the given { @link HttpURLConnection}.
 * <p>The default implementation prepares the connection for input and output, and sets the HTTP method.
 * @param connection the connection to prepare
 * @param httpMethod the HTTP request method ({ @code GET}, { @code POST}, etc.)
 * @throws IOException in case of I/O errors
 */
protected void prepareConnection(HttpURLConnection connection, String httpMethod) throws IOException {
   if (this.connectTimeout >= 0) {
      connection.setConnectTimeout(this.connectTimeout);
   }
   if (this.readTimeout >= 0) {
      connection.setReadTimeout(this.readTimeout);
   }

   connection.setDoInput(true);

   if ("GET".equals(httpMethod)) {
      connection.setInstanceFollowRedirects(true);
   }
   else {
      connection.setInstanceFollowRedirects(false);
   }

   if ("POST".equals(httpMethod) || "PUT".equals(httpMethod) ||
         "PATCH".equals(httpMethod) || "DELETE".equals(httpMethod)) {
      connection.setDoOutput(true);
   }
   else {
      connection.setDoOutput(false);
   }

   connection.setRequestMethod(httpMethod);
}

 

 

最后是生成并返回了一个ClientHttpRequest实例,

 

if (this.bufferRequestBody) {

//如果是默认的SimpleClientHttpRequestFactory,那么返回的就是这个了
   return new SimpleBufferingClientHttpRequest(connection, this.outputStreaming);
}
else {
   return new SimpleStreamingClientHttpRequest(connection, this.chunkSize, this.outputStreaming);
}

 

requestCallback.doWithRequest(request);

RequestCallback的实现类一般用到的就是定义在RestTemplate中的HttpEntityRequestCallback和AcceptHeaderRequestCallback,其中HttpEntityRequestCallback extends AcceptHeaderRequestCallback

 

 

既然HttpEntityRequestCallback继承了AcceptHeaderRequestCallback,那么比AcceptHeaderRequestCallback多做了什么呢?答案就是:HttpEntityRequestCallback多做了关于http请求body的处理。

首先我们先看看AcceptHeaderRequestCallback的doWithRequest方法

 

 @Override

        public void doWithRequest(ClientHttpRequest request) throws IOException {

            if (this.responseType != null) {

                Class<?> responseClass = null;

                if (this.responseType instanceof Class) {

                    responseClass = (Class<?>) this.responseType;

                }

                List<MediaType> allSupportedMediaTypes = new ArrayList<MediaType>();

                for (HttpMessageConverter<?> converter : getMessageConverters()) {

                    if (responseClass != null) {

                        if (converter.canRead(responseClass, null)) {

                            allSupportedMediaTypes.addAll(getSupportedMediaTypes(converter));

                        }

                    }

                    else if (converter instanceof GenericHttpMessageConverter) {

                        GenericHttpMessageConverter<?> genericConverter = (GenericHttpMessageConverter<?>) converter;

                        if (genericConverter.canRead(this.responseType, null, null)) {

                            allSupportedMediaTypes.addAll(getSupportedMediaTypes(converter));

                        }

                    }

                }

                if (!allSupportedMediaTypes.isEmpty()) {

                    MediaType.sortBySpecificity(allSupportedMediaTypes);

                    if (logger.isDebugEnabled()) {

                        logger.debug("Setting request Accept header to " + allSupportedMediaTypes);

                    }

                    request.getHeaders().setAccept(allSupportedMediaTypes);

                }

            }

        }

 

 

这里面有个for循环for (HttpMessageConverter<?> converter : getMessageConverters()),getMessageConverters()是这样子的

/**
 * Return the list of message body converters.
 * <p>The returned { @link List} is active and may get appended to.
 */
public List<HttpMessageConverter<?>> getMessageConverters() {
   return this.messageConverters;
}

 

 

this.messageConverters是这样子的

 

private final List<HttpMessageConverter<?>> messageConverters = new ArrayList<>();

 

那么messageConverters里面的值是什么呢?答案是,RestTemplate实例化时初始化了messageConverters的值

 

/**
 * Create a new instance of the { @link RestTemplate} using default settings.
 * Default { @link HttpMessageConverter HttpMessageConverters} are initialized.
 */
public RestTemplate() {
   this.messageConverters.add(new ByteArrayHttpMessageConverter());
   this.messageConverters.add(new StringHttpMessageConverter());
   this.messageConverters.add(new ResourceHttpMessageConverter(false));
   try {
      this.messageConverters.add(new SourceHttpMessageConverter<>());
   }
   catch (Error err) {
      // Ignore when no TransformerFactory implementation is available
   }
   this.messageConverters.add(new AllEncompassingFormHttpMessageConverter());

   if (romePresent) {
      this.messageConverters.add(new AtomFeedHttpMessageConverter());
      this.messageConverters.add(new RssChannelHttpMessageConverter());
   }

   if (jackson2XmlPresent) {
      this.messageConverters.add(new MappingJackson2XmlHttpMessageConverter());
   }
   else if (jaxb2Present) {
      this.messageConverters.add(new Jaxb2RootElementHttpMessageConverter());
   }

   if (jackson2Present) {
      this.messageConverters.add(new MappingJackson2HttpMessageConverter());
   }
   else if (gsonPresent) {
      this.messageConverters.add(new GsonHttpMessageConverter());
   }
   else if (jsonbPresent) {
      this.messageConverters.add(new JsonbHttpMessageConverter());
   }

   if (jackson2SmilePresent) {
      this.messageConverters.add(new MappingJackson2SmileHttpMessageConverter());
   }
   if (jackson2CborPresent) {
      this.messageConverters.add(new MappingJackson2CborHttpMessageConverter());
   }

   this.uriTemplateHandler = initUriTemplateHandler();
}

 

 

那些if判断其实就是看项目中有没有引入这些jar

 

private static final boolean romePresent;

private static final boolean jaxb2Present;

private static final boolean jackson2Present;

private static final boolean jackson2XmlPresent;

private static final boolean jackson2SmilePresent;

private static final boolean jackson2CborPresent;

private static final boolean gsonPresent;

private static final boolean jsonbPresent;

static {
   ClassLoader classLoader = RestTemplate.class.getClassLoader();
   romePresent = ClassUtils.isPresent("com.rometools.rome.feed.WireFeed", classLoader);
   jaxb2Present = ClassUtils.isPresent("javax.xml.bind.Binder", classLoader);
   jackson2Present = ClassUtils.isPresent("com.fasterxml.jackson.databind.ObjectMapper", classLoader) &&
         ClassUtils.isPresent("com.fasterxml.jackson.core.JsonGenerator", classLoader);
   jackson2XmlPresent = ClassUtils.isPresent("com.fasterxml.jackson.dataformat.xml.XmlMapper", classLoader);
   jackson2SmilePresent = ClassUtils.isPresent("com.fasterxml.jackson.dataformat.smile.SmileFactory", classLoader);
   jackson2CborPresent = ClassUtils.isPresent("com.fasterxml.jackson.dataformat.cbor.CBORFactory", classLoader);
   gsonPresent = ClassUtils.isPresent("com.google.gson.Gson", classLoader);
   jsonbPresent = ClassUtils.isPresent("javax.json.bind.Jsonb", classLoader);
}

 

 

 

 下面就是调用converter的canRead方法

canRead方法其实就是判断这个converter是否支持对这种类型进行解析。

然后拿出这些converter支持的MediaType

 

allSupportedMediaTypes.addAll(getSupportedMediaTypes(converter));

 

最后将这些MediaType设置到head中去

 

request.getHeaders().setAccept(allSupportedMediaTypes);

 

以上是适用于没有body的请求,如果有body的请求,会走HttpEntityRequestCallback,他继承自AcceptHeaderRequestCallback,多做了关于body的处理

public void doWithRequest(ClientHttpRequest httpRequest) throws IOException {
    super.doWithRequest(httpRequest);
    if (!this.requestEntity.hasBody()) {
        HttpHeaders httpHeaders = httpRequest.getHeaders();
        HttpHeaders requestHeaders = this.requestEntity.getHeaders();
        if (!requestHeaders.isEmpty()) {
            for (Map.Entry<String, List<String>> entry : requestHeaders.entrySet()) {
                httpHeaders.put(entry.getKey(), new LinkedList<String>(entry.getValue()));
            }
        }
        if (httpHeaders.getContentLength() < 0) {
            httpHeaders.setContentLength(0L);
        }
    }
    else {
        Object requestBody = this.requestEntity.getBody();
        Class<?> requestBodyClass = requestBody.getClass();
        Type requestBodyType = (this.requestEntity instanceof RequestEntity ?
                ((RequestEntity<?>)this.requestEntity).getType() : requestBodyClass);
        HttpHeaders httpHeaders = httpRequest.getHeaders();
        HttpHeaders requestHeaders = this.requestEntity.getHeaders();
        MediaType requestContentType = requestHeaders.getContentType();
        for (HttpMessageConverter<?> messageConverter : getMessageConverters()) {
            if (messageConverter instanceof GenericHttpMessageConverter) {
                GenericHttpMessageConverter<Object> genericMessageConverter = (GenericHttpMessageConverter<Object>) messageConverter;
                if (genericMessageConverter.canWrite(requestBodyType, requestBodyClass, requestContentType)) {
                    if (!requestHeaders.isEmpty()) {
                        for (Map.Entry<String, List<String>> entry : requestHeaders.entrySet()) {
                            httpHeaders.put(entry.getKey(), new LinkedList<String>(entry.getValue()));
                        }
                    }
                    if (logger.isDebugEnabled()) {
                        if (requestContentType != null) {
                            logger.debug("Writing [" + requestBody + "] as \"" + requestContentType +
                                    "\" using [" + messageConverter + "]");
                        }
                        else {
                            logger.debug("Writing [" + requestBody + "] using [" + messageConverter + "]");
                        }

                    }
                    genericMessageConverter.write(
                            requestBody, requestBodyType, requestContentType, httpRequest);
                    return;
                }
            }
            else if (messageConverter.canWrite(requestBodyClass, requestContentType)) {
                if (!requestHeaders.isEmpty()) {
                    for (Map.Entry<String, List<String>> entry : requestHeaders.entrySet()) {
                        httpHeaders.put(entry.getKey(), new LinkedList<String>(entry.getValue()));
                    }
                }
                if (logger.isDebugEnabled()) {
                    if (requestContentType != null) {
                        logger.debug("Writing [" + requestBody + "] as \"" + requestContentType +
                                "\" using [" + messageConverter + "]");
                    }
                    else {
                        logger.debug("Writing [" + requestBody + "] using [" + messageConverter + "]");
                    }

                }
                ((HttpMessageConverter<Object>) messageConverter).write(
                        requestBody, requestContentType, httpRequest);
                return;
            }
        }
        String message = "Could not write request: no suitable HttpMessageConverter found for request type [" +
                requestBodyClass.getName() + "]";
        if (requestContentType != null) {
            message += " and content type [" + requestContentType + "]";
        }
        throw new RestClientException(message);
    }
}

 

 

这边也是先循环converter,然后选出能处理的converter,然后写入到body

 

((HttpMessageConverter<Object>) messageConverter).write(

                                requestBody, requestContentType, httpRequest);

 

这边以StringHttpMessageConverter举例,调用writeInternal方法

 

@Override
protected void writeInternal(String str, HttpOutputMessage outputMessage) throws IOException {
   HttpHeaders headers = outputMessage.getHeaders();
   if (this.writeAcceptCharset && headers.get(HttpHeaders.ACCEPT_CHARSET) == null) {
      headers.setAcceptCharset(getAcceptedCharsets());
   }
   Charset charset = getContentTypeCharset(headers.getContentType());
   StreamUtils.copy(str, charset, outputMessage.getBody());
}

 

 

注意:乱码问题就是在这边产生的

Charset charset = getContentTypeCharset(headers.getContentType());

首先会看你的请求是否有设置charset,charset的设置在自己编写restTemplate请求代码时做的如下代码就指定了编码格式为UTF-8

 

HttpHeaders headers = new HttpHeaders();
MediaType type = MediaType.parseMediaType("application/json; charset=UTF-8");        headers.setContentType(type);
    headers.add("Accept", MediaType.APPLICATION_JSON.toString());
HttpEntity request = new HttpEntity<>(jsonObject, headers);
JSONObject response = restTemplate.postForObject("http://127.0.0.1:8089/getBodyTest", request, JSONObject.class);

 

如果MediaType没有指定charset,而是这样的

MediaType type = MediaType.parseMediaType("application/json");

 

那么将使用converter的默认编码,还是以StringHttpMessageConverter为例

private Charset getContentTypeCharset(@Nullable MediaType contentType) {
   if (contentType != null && contentType.getCharset() != null) {
      return contentType.getCharset();
   }
   else if (contentType != null &&
         (contentType.isCompatibleWith(MediaType.APPLICATION_JSON) ||
               contentType.isCompatibleWith(APPLICATION_PLUS_JSON))) {
      // Matching to AbstractJackson2HttpMessageConverter#DEFAULT_CHARSET
      return StandardCharsets.UTF_8;
   }
   else {
      Charset charset = getDefaultCharset();
      Assert.state(charset != null, "No default charset");
      return charset;
   }
}

 

没有设置就会调用getDefaultCharset()

/**
 * Return the default character set, if any.
 * @since 4.3
 */
@Nullable
public Charset getDefaultCharset() {
   return this.defaultCharset;
}

 

this.defaultCharset是这样的

 

@Nullable
private Charset defaultCharset;

 

 

这边没有给默认值,说明在其他地方赋值的,那我们看下StringHttpMessageConverter的构造方法

 

/**
 * A default constructor that uses { @code "ISO-8859-1"} as the default charset.
 * @see #StringHttpMessageConverter(Charset)
 */
public StringHttpMessageConverter() {
   this(DEFAULT_CHARSET);
}

 

/**
 * A constructor accepting a default charset to use if the requested content
 * type does not specify one.
 */
public StringHttpMessageConverter(Charset defaultCharset) {
   super(defaultCharset, MediaType.TEXT_PLAIN, MediaType.ALL);
}

 

而DEFAULT_CHARSET的值如下

 

/**
 * The default charset used by the converter.
 */
public static final Charset DEFAULT_CHARSET = StandardCharsets.ISO_8859_1;

 

所以说,如果我们在通过restTemplate发起http请求,如果body是String格式的,那么他的默认编码是"ISO-8859-1",那么就会出现乱码的可能,解决的方法也很简单,就是自己设置下charset

 

response = request.execute();

这边以SimpleBufferingClientHttpRequest举例

执行的核心代码是这个

@Override
protected ClientHttpResponse executeInternal(HttpHeaders headers, byte[] bufferedOutput) throws IOException {
   addHeaders(this.connection, headers);
   // JDK <1.8 doesn't support getOutputStream with HTTP DELETE
   if (getMethod() == HttpMethod.DELETE && bufferedOutput.length == 0) {
      this.connection.setDoOutput(false);
   }
   if (this.connection.getDoOutput() && this.outputStreaming) {
      this.connection.setFixedLengthStreamingMode(bufferedOutput.length);
   }
   this.connection.connect();
   if (this.connection.getDoOutput()) {
      FileCopyUtils.copy(bufferedOutput, this.connection.getOutputStream());
   }
   else {
      // Immediately trigger the request in a no-output scenario as well
      this.connection.getResponseCode();
   }
   return new SimpleClientHttpResponse(this.connection);
}

 

主要是进行了this.connection.connect();然后将connection包装在了SimpleClientHttpResponse中并返回了

 

handleResponse(url, method, response);

这一步就是对http的response做处理,这里有个知识点,就是开头提到的直接通过log.error("http response error,{}",e)打印出来的日志不是我们想要的完整日志

/**
 * Handle the given response, performing appropriate logging and
 * invoking the { @link ResponseErrorHandler} if necessary.
 * <p>Can be overridden in subclasses.
 * @param url the fully-expanded URL to connect to
 * @param method the HTTP method to execute (GET, POST, etc.)
 * @param response the resulting { @link ClientHttpResponse}
 * @throws IOException if propagated from { @link ResponseErrorHandler}
 * @since 4.1.6
 * @see #setErrorHandler
 */
protected void handleResponse(URI url, HttpMethod method, ClientHttpResponse response) throws IOException {
   ResponseErrorHandler errorHandler = getErrorHandler();
   boolean hasError = errorHandler.hasError(response);
   if (logger.isDebugEnabled()) {
      try {
         int code = response.getRawStatusCode();
         HttpStatus status = HttpStatus.resolve(code);
         logger.debug("Response " + (status != null ? status : code));
      }
      catch (IOException ex) {
         // ignore
      }
   }
   if (hasError) {
      errorHandler.handleError(url, method, response);
   }
}

 

getErrorHandler是这样子的,

/**
 * Return the error handler.
 */
public ResponseErrorHandler getErrorHandler() {
   return this.errorHandler;
}

 

而this.errorHandler是这样子的

 

private ResponseErrorHandler errorHandler = new DefaultResponseErrorHandler();

 

所以,如果我们没做任何设置,那么这里的ResponseErrorHandler就是DefaultResponseErrorHandler

对于DefaultResponseErrorHandler,他对hasError(response)的实现就是判断是否是400等4开头或者500等5开头的返回状态吗,如果是则把返回包装下,抛出一条异常

/**
 * Handle the error based on the resolved status code.
 *
 * <p>The default implementation delegates to
 * { @link HttpClientErrorException#create} for errors in the 4xx range, to
 * { @link HttpServerErrorException#create} for errors in the 5xx range,
 * or otherwise raises { @link UnknownHttpStatusCodeException}.
 *
 * @since 5.0
 * @see HttpClientErrorException#create
 * @see HttpServerErrorException#create
 */
protected void handleError(ClientHttpResponse response, HttpStatus statusCode) throws IOException {
   String statusText = response.getStatusText();
   HttpHeaders headers = response.getHeaders();
   byte[] body = getResponseBody(response);
   Charset charset = getCharset(response);
   String message = getErrorMessage(statusCode.value(), statusText, body, charset);

   switch (statusCode.series()) {
      case CLIENT_ERROR:
         throw HttpClientErrorException.create(message, statusCode, statusText, headers, body, charset);
      case SERVER_ERROR:
         throw HttpServerErrorException.create(message, statusCode, statusText, headers, body, charset);
      default:
         throw new UnknownHttpStatusCodeException(message, statusCode.value(), statusText, headers, body, charset);
   }
}

 

 

由于doExecute已经包了一层tray/catch,所以等我们拿到时就是这样的异常了

 

String resource = url.toString();
String query = url.getRawQuery();
resource = (query != null ? resource.substring(0, resource.indexOf('?')) : resource);
        throw new ResourceAccessException("I/O error on " + method.name() +
        " request for \"" + resource + "\": " + ex.getMessage(), ex);

 

 

 

所以我们的打印日志中无法显示返回的body。

如果想打印出body,这个有个简单的方式,就是自己实现ResponseErrorHandler,如下

 

public class RestTemplateResponseErrorHandler implements ResponseErrorHandler {

    @Override

    public boolean hasError(ClientHttpResponse response) throws IOException {

        return true;

    }

 

    @Override

    public void handleError(ClientHttpResponse response) throws IOException {

 

    }

}

 

 

然后在resttemplate初始化时将这个handler设置下就行了

 

    @Bean

    public RestTemplate restTemplate(ClientHttpRequestFactory factory) {

        RestTemplate restTemplate = new RestTemplate(factory);

        restTemplate.setErrorHandler(new RestTemplateResponseErrorHandler());

        return restTemplate;

}

 

这样子,返回的原始信息我们都可以拿到了。 

responseExtractor.extractData(response);

public T extractData(ClientHttpResponse response) throws IOException {
   MessageBodyClientHttpResponseWrapper responseWrapper = new MessageBodyClientHttpResponseWrapper(response);
   if (!responseWrapper.hasMessageBody() || responseWrapper.hasEmptyMessageBody()) {
      return null;
   }
   MediaType contentType = getContentType(responseWrapper);

   try {
      for (HttpMessageConverter<?> messageConverter : this.messageConverters) {
         if (messageConverter instanceof GenericHttpMessageConverter) {
            GenericHttpMessageConverter<?> genericMessageConverter =
                  (GenericHttpMessageConverter<?>) messageConverter;
            if (genericMessageConverter.canRead(this.responseType, null, contentType)) {
               if (logger.isDebugEnabled()) {
                  ResolvableType resolvableType = ResolvableType.forType(this.responseType);
                  logger.debug("Reading to [" + resolvableType + "]");
               }
               return (T) genericMessageConverter.read(this.responseType, null, responseWrapper);
            }
         }
         if (this.responseClass != null) {
            if (messageConverter.canRead(this.responseClass, contentType)) {
               if (logger.isDebugEnabled()) {
                  String className = this.responseClass.getName();
                  logger.debug("Reading to [" + className + "] as \"" + contentType + "\"");
               }
               return (T) messageConverter.read((Class) this.responseClass, responseWrapper);
            }
         }
      }
   }
   catch (IOException | HttpMessageNotReadableException ex) {
      throw new RestClientException("Error while extracting response for type [" +
            this.responseType + "] and content type [" + contentType + "]", ex);
   }

   throw new UnknownContentTypeException(this.responseType, contentType,
         response.getRawStatusCode(), response.getStatusText(), response.getHeaders(),
         getResponseBody(response));
}

 

 

这一步就是通过converter将response中的body转换成我们设置的类,大致代码和wirte差不多,只是这边是read

猜你喜欢

转载自blog.csdn.net/Coder_Boy_/article/details/113101400