常用配置

初始化 中所述,RestClientBuilder 同时支持提供 RequestConfigCallbackHttpClientConfigCallback,允许 Apache Async Http Client 暴露的任何自定义内容。 这些回调可以在不覆盖 RestClient 初始化配置的前提下,使修改客户端的特定行为成为可能。 本章节表述了一些需要为底层Java Rest客户端进行额外配置的常见场景。

超时

通过构造器构建 RestClient 的时候,可以提供 RequestConfigCallback 的实例来配置请求的超时时间。 该接口有一个方法接受参数 org.apache.http.client.config.RequestConfig.Builder 作为实例,然后返回相同的类型。 请求配置构造器可以被修改然后返回。 在下面的示例中,我们添加了一个连接超时(默认为1秒)的配置以及一个socket超时(默认30秒)配置。

RestClientBuilder builder = RestClient.builder(
    new HttpHost("localhost", 9200))
    .setRequestConfigCallback(
        new RestClientBuilder.RequestConfigCallback() {
            @Override
            public RequestConfig.Builder customizeRequestConfig(
                    RequestConfig.Builder requestConfigBuilder) {
                return requestConfigBuilder
                    .setConnectTimeout(5000)
                    .setSocketTimeout(60000);
            }
        });

超时时间也可以在发送每个请求的时候配置,这样做会覆盖 RestClient customizeRequestConfig。

RequestConfig requestConfig = RequestConfig.custom()
    .setConnectTimeout(5000)
    .setSocketTimeout(60000)
    .build();
RequestOptions options = RequestOptions.DEFAULT.toBuilder()
    .setRequestConfig(requestConfig)
    .build();

线程数量

Apache Http Async Client 在启动时默认情况会使用一个调度线程,连接管理器也会使用几个工作线程,这个数量相当于本地监测到的处理器数量 (取决于 Runtime.getRuntime().availableProcessors() 返回的值)。 这个数量可以修改,如下:

RestClientBuilder builder = RestClient.builder(
    new HttpHost("localhost", 9200))
    .setHttpClientConfigCallback(new HttpClientConfigCallback() {
        @Override
        public HttpAsyncClientBuilder customizeHttpClient(
                HttpAsyncClientBuilder httpClientBuilder) {
            return httpClientBuilder.setDefaultIOReactorConfig(
                IOReactorConfig.custom()
                    .setIoThreadCount(1)
                    .build());
        }
    });

基本身份认证

在通过构造器构建 RestClient 的时候,可以提供一个 HttpClientConfigCallback 来配置基本身份认证。 这个接口有一个方法接受 org.apache.http.impl.nio.client.HttpAsyncClientBuilder 示例作为参数,然后返回相同的类型。 HTTP 客户端构造器可以被修改并返回。 下面的示例中,我们为基本身份认证提供了一个默认的凭证。

final CredentialsProvider credentialsProvider =
    new BasicCredentialsProvider();
credentialsProvider.setCredentials(AuthScope.ANY,
    new UsernamePasswordCredentials("user", "test-user-password"));

RestClientBuilder builder = RestClient.builder(
    new HttpHost("localhost", 9200))
    .setHttpClientConfigCallback(new HttpClientConfigCallback() {
        @Override
        public HttpAsyncClientBuilder customizeHttpClient(
                HttpAsyncClientBuilder httpClientBuilder) {
            return httpClientBuilder
                .setDefaultCredentialsProvider(credentialsProvider);
        }
    });

抢占式身份认证可以禁用,禁用后每个请求都将在没有授权header的情况下发送,这样就能查看它是否被接受,在收到 401 HTTP 响应码后, 它将使用基本身份认证header重新发送相同的请求。 如果你想这样执行操作,可以通过 HttpAsyncClientBuilder 来禁用它:

final CredentialsProvider credentialsProvider =
    new BasicCredentialsProvider();
credentialsProvider.setCredentials(AuthScope.ANY,
    new UsernamePasswordCredentials("user", "test-user-password"));

RestClientBuilder builder = RestClient.builder(
    new HttpHost("localhost", 9200))
    .setHttpClientConfigCallback(new HttpClientConfigCallback() {
        @Override
        public HttpAsyncClientBuilder customizeHttpClient(
                HttpAsyncClientBuilder httpClientBuilder) {
            httpClientBuilder.disableAuthCaching(); (1)
            return httpClientBuilder
                .setDefaultCredentialsProvider(credentialsProvider);
        }
    });
1 禁用抢占式身份认证

其它身份认证方法

Elasticsearch 访问令牌服务

如果希望客户端使用Elasticsearch访问令牌来进行身份认证,请设置相关的HTTP请求头。 如果客户端仅为一个用户发送请求,则可以一个必要的 授权 header作为默认展示的请求header,如下所示:

RestClientBuilder builder = RestClient.builder(
    new HttpHost("localhost", 9200, "http"));
Header[] defaultHeaders =
    new Header[]{new BasicHeader("Authorization",
        "Bearer u6iuAxZ0RG1Kcm5jVFI4eU4tZU9aVFEwT2F3")};
builder.setDefaultHeaders(defaultHeaders);

Elasticsearch API 密钥

如果希望客户端使用Elasticsearch API密钥来进行身份认证,请设置相关的HTTP请求头。 如果客户端仅为一个用户发送请求,则可以一个必要的 授权 header作为默认展示的请求header,如下所示:

String apiKeyId = "uqlEyn8B_gQ_jlvwDIvM";
String apiKeySecret = "HxHWk2m4RN-V_qg9cDpuX";
String apiKeyAuth =
    Base64.getEncoder().encodeToString(
        (apiKeyId + ":" + apiKeySecret)
            .getBytes(StandardCharsets.UTF_8));
RestClientBuilder builder = RestClient.builder(
    new HttpHost("localhost", 9200, "http"));
Header[] defaultHeaders =
    new Header[]{new BasicHeader("Authorization",
        "ApiKey " + apiKeyAuth)};
builder.setDefaultHeaders(defaultHeaders);

加密通讯

您也可以通过配置 HttpClientConfigCallback 使用TLS加密通讯。 org.apache.http.impl.nio.client.HttpAsyncClientBuilder 会作为参数接受并暴露一些配置加密通讯的方法: setSSLContext, setSSLSessionStrategysetConnectionManager, 根据顺序越来越重要。

当在HTTP层的基础上为Elasticsearch集群安装TLS时,客户端需要信任Elasticsearch正在使用的证书。 下面的例子展示了,当PKCS#12密钥库中的CA证书可用时,将客户端设置为信任Elasticsearch正在使用的CA证书:

Path trustStorePath = Paths.get("/path/to/truststore.p12");
KeyStore truststore = KeyStore.getInstance("pkcs12");
try (InputStream is = Files.newInputStream(trustStorePath)) {
    truststore.load(is, keyStorePass.toCharArray());
}
SSLContextBuilder sslBuilder = SSLContexts.custom()
    .loadTrustMaterial(truststore, null);
final SSLContext sslContext = sslBuilder.build();
RestClientBuilder builder = RestClient.builder(
    new HttpHost("localhost", 9200, "https"))
    .setHttpClientConfigCallback(new HttpClientConfigCallback() {
        @Override
        public HttpAsyncClientBuilder customizeHttpClient(
                HttpAsyncClientBuilder httpClientBuilder) {
            return httpClientBuilder.setSSLContext(sslContext);
        }
    });

下面的例子展示了,当PEM编码文件可用时,将客户端设置为信任Elasticsearch正在使用的CA证书:

Path caCertificatePath = Paths.get("/path/to/ca.crt");
CertificateFactory factory =
    CertificateFactory.getInstance("X.509");
Certificate trustedCa;
try (InputStream is = Files.newInputStream(caCertificatePath)) {
    trustedCa = factory.generateCertificate(is);
}
KeyStore trustStore = KeyStore.getInstance("pkcs12");
trustStore.load(null, null);
trustStore.setCertificateEntry("ca", trustedCa);
SSLContextBuilder sslContextBuilder = SSLContexts.custom()
    .loadTrustMaterial(trustStore, null);
final SSLContext sslContext = sslContextBuilder.build();
RestClient.builder(
    new HttpHost("localhost", 9200, "https"))
    .setHttpClientConfigCallback(new HttpClientConfigCallback() {
        @Override
        public HttpAsyncClientBuilder customizeHttpClient(
            HttpAsyncClientBuilder httpClientBuilder) {
            return httpClientBuilder.setSSLContext(sslContext);
        }
    });

当Elasticsearch被配置为需要客户端TLS认证,例如当配置了PKI realm的时候,这时客户端需要在TLS握手期间提供证书来进行身份认证。 下面的例子展示了,使用存储在PKCS#12密钥库中的证书和私钥为客户端配置TLS身份认证:

Path trustStorePath = Paths.get("/path/to/your/truststore.p12");
Path keyStorePath = Paths.get("/path/to/your/keystore.p12");
KeyStore trustStore = KeyStore.getInstance("pkcs12");
KeyStore keyStore = KeyStore.getInstance("pkcs12");
try (InputStream is = Files.newInputStream(trustStorePath)) {
    trustStore.load(is, trustStorePass.toCharArray());
}
try (InputStream is = Files.newInputStream(keyStorePath)) {
    keyStore.load(is, keyStorePass.toCharArray());
}
SSLContextBuilder sslBuilder = SSLContexts.custom()
    .loadTrustMaterial(trustStore, null)
    .loadKeyMaterial(keyStore, keyStorePass.toCharArray());
final SSLContext sslContext = sslBuilder.build();
RestClientBuilder builder = RestClient.builder(
    new HttpHost("localhost", 9200, "https"))
    .setHttpClientConfigCallback(new HttpClientConfigCallback() {
        @Override
        public HttpAsyncClientBuilder customizeHttpClient(
            HttpAsyncClientBuilder httpClientBuilder) {
            return httpClientBuilder.setSSLContext(sslContext);
        }
    });

如果客户端证书和密钥在密钥库中不可用,而是使用PEM编码的文件,这时候就不能直接使用它们来构建 SSLContext。 你必须使用外部库来将PEM密钥解析为私有密钥实例。 或者,你也可以使用外部工具用PEM文件构建密钥库,如下面的例子:

openssl pkcs12 -export -in client.crt -inkey private_key.pem \
        -name "client" -out client.p12

如果没有提供明确的配置,则会使用 系统默认配置

其它

如果您需要其它的配置,可以参考Apache HttpAsyncClient的文档: https://hc.apache.org/httpcomponents-asyncclient-4.1.x/

如果你的应用运行在security manager下,则可能会收到JVM默认策略的约束, 即针对解析成功的主机名进行无限期的缓存,针对解析失败的则缓存10秒。 如果要将客户端连接到主机的的地址随时间的变化而变化,那么可能需要修改JVM的默认行为。 如果需要对其进行修改的话,可以通过添加 networkaddress.cache.ttl=<timeout> 以及 networkaddress.cache.negative.ttl=<timeout> 选项到你的 Java security policy 中。

节点选择器

客户端以循环的方式,将每个请求发送到已配置的节点中的一个上面。 在初始化客户端时,可以提供节点选择器来对节点进行筛选。 在启用嗅探功能时,这一点非常有用,可以防止HTTP请求只命中专用主节点 对每个请求,客户端都会运行最后一个配置的节点选择器对候选节点进行筛选,然后从剩余的节点中选择下一个。

RestClientBuilder builder = RestClient.builder(
        new HttpHost("localhost", 9200, "http"));
builder.setNodeSelector(new NodeSelector() { (1)
    @Override
    public void select(Iterable<Node> nodes) {
        /*
         * Prefer any node that belongs to rack_one. If none is around
         * we will go to another rack till it's time to try and revive
         * some of the nodes that belong to rack_one.
         */
        boolean foundOne = false;
        for (Node node : nodes) {
            String rackId = node.getAttributes().get("rack_id").get(0);
            if ("rack_one".equals(rackId)) {
                foundOne = true;
                break;
            }
        }
        if (foundOne) {
            Iterator<Node> nodesIt = nodes.iterator();
            while (nodesIt.hasNext()) {
                Node node = nodesIt.next();
                String rackId = node.getAttributes().get("rack_id").get(0);
                if ("rack_one".equals(rackId) == false) {
                    nodesIt.remove();
                }
            }
        }
    }
});
1 Set an allocation aware node selector that allows to pick a node in the local rack if any available, otherwise go to any other node in any rack. It acts as a preference rather than a strict requirement, given that it goes to another rack if none of the local nodes are available, rather than returning no nodes in such case which would make the client forcibly revive a local node whenever none of the nodes from the preferred rack is available.
Node selectors that do not consistently select the same set of nodes will make round-robin behaviour unpredictable and possibly unfair. The preference example above is fine as it reasons about availability of nodes which already affects the predictability of round-robin. Node selection should not depend on other external factors or round-robin will not work properly.