专栏首页BAT的乌托邦[享学Eureka] 九、远程通信模块:使用TransportClientFactory构建底层请求客户端完成服务注册、服务下线

[享学Eureka] 九、远程通信模块:使用TransportClientFactory构建底层请求客户端完成服务注册、服务下线

代码下载地址:https://github.com/f641385712/netflix-learning

前言

通过前两篇文章一步一步介绍了如何构建出一个JerseyApplicationClient实例来完成服务注册、服务下线等功能。而上文讲到了使用Eureka提供的EurekaJerseyClient通过Builder模式去构建已经比较方便了,但是仍旧存在一个小问题:无法摆脱Jersey关键字的感知,自然底层上也就无法摆脱对Jersey库的强依赖。

即使你并不需要使用Jersey的API来做事,但使用时你却明显的知道你实际就是在用Jersey来干活,似乎有点欲盖弥彰。这种“强耦合”很明显是制约了Eureka的弹性扩展性的,因此Eureka它提供了TransportClientFactory工厂用于屏蔽这一切,这便是本文讲述的内容。


正文

通过工厂方式创建实例的方式来解耦,是最为常用的一种设计模式。Eureka也不例外,使用该种模式能达到预期的效果。

当然喽,在此之前先要了解下Eureka中终端这个抽象概念:EurekaEndpoint


EurekaEndpoint 终端

EurekaEndpoint对于Eureka Client端来说就代表着一台远程的Server服务器。若S端是个集群,那么Client端便可获取到一个EurekaEndpoint的集合。该概念这在后文对Server集群的处理(ClusterResolver)时将会再次提及。

public interface EurekaEndpoint extends Comparable<Object> {
    String getServiceUrl();
    // 请使用getNetworkAddress方法代替此方法
    @Deprecated
    String getHostName();
    String getNetworkAddress();
    int getPort();
    boolean isSecure();
    String getRelativeUri();
}

接口方法见名之意,无需解释。它的继承图谱如下:


DefaultEndpoint

它的是Eureka的默认实现

public class DefaultEndpoint implements EurekaEndpoint {

    protected final String networkAddress;
    protected final int port;
    protected final boolean isSecure;
    protected final String relativeUri;
    protected final String serviceUrl;

	// serviceUr来自于配置,一般如:http://localhost:8080/eureka/
    public DefaultEndpoint(String serviceUrl) {
        this.serviceUrl = serviceUrl;
        try {
            URL url = new URL(serviceUrl);
            this.networkAddress = url.getHost();
            this.port = url.getPort();
            this.isSecure = "https".equals(url.getProtocol());
            this.relativeUri = url.getPath();
        } catch (Exception e) {
            throw new IllegalArgumentException("Malformed serviceUrl: " + serviceUrl);
        }
    }

	// 当然还提供了一个更为原子的构造器,也较为常用
	public DefaultEndpoint(String networkAddress, int port, boolean isSecure, String relativeUri) { ... }


    @Deprecated
    @Override
    public String getHostName() {
        return networkAddress;
    }
    @Override
    public String getNetworkAddress() {
        return networkAddress;
    }
    ...
}

另外,它还提供了一个静态方法:根据hostNames批量创建出List<EurekaEndpoint>

其实这里叫根据networkAddresses批量创建更为合适,但因为历史原因,方法名不便修改~

DefaultEndpoint:

    public static List<EurekaEndpoint> createForServerList(
            List<String> hostNames, int port, boolean isSecure, String relativeUri) {
        if (hostNames.isEmpty()) {
            return Collections.emptyList();
        }
        List<EurekaEndpoint> eurekaEndpoints = new ArrayList<>(hostNames.size());
        for (String hostName : hostNames) {
            eurekaEndpoints.add(new DefaultEndpoint(hostName, port, isSecure, relativeUri));
        }
        return eurekaEndpoints;
    }

代码示例
@Test
public void fun6() {
    EurekaEndpoint endpoint = new DefaultEndpoint("http://192.168.1.100:8080/eureka/");
    System.out.println(endpoint.getServiceUrl()); // http://localhost:8080/eureka/
    System.out.println(endpoint.getNetworkAddress()); // 192.168.1.100
    System.out.println(endpoint.getHostName()); // 192.168.1.100
    System.out.println(endpoint.getPort()); // 8080
    System.out.println(endpoint.isSecure()); // false
    System.out.println(endpoint.getRelativeUri()); // /eureka/


    // 静态方法测试  isSecure=true那用的就是https
    List<EurekaEndpoint> endpoints = DefaultEndpoint.createForServerList(Arrays.asList("192.168.1.1", "192.168.1.2"), 8080, true, "/eureka/");
    // [DefaultEndpoint{ serviceUrl='https://192.168.1.1:8080/eureka/}, DefaultEndpoint{ serviceUrl='https://192.168.1.2:8080/eureka/}]
    System.out.println(endpoints);
}

AwsEndpoint

它是DefaultEndpoint的子类,使用在AWS云环境。额外增加了zone、region属性:

public class AwsEndpoint extends DefaultEndpoint {

    protected final String zone;
    protected final String region;
	...
	// 并且重新提供了静态方法createForServerList
	public static List<AwsEndpoint> createForServerList(List<String> hostNames, int port, boolean isSecure, String relativeUri, String region,String zone) {
		...
        for (String hostName : hostNames) {
            awsEndpoints.add(new AwsEndpoint(hostName, port, isSecure, relativeUri, region, zone));
        }
		...
	}
}



TransportClientFactory 工厂

EurekaHttpClient创建工厂接口,负责创建low level低等级的Client(后面会讲还有用于创建高级的EurekaHttpClient的工厂:EurekaHttpClientFactory),所以你看它的命名是Transport含义,表明自己是用于数据传输级别的。

public interface TransportClientFactory {
	// 根据终端EurekaEndpoint创建一个底层的可发送Http请求的Client
    EurekaHttpClient newClient(EurekaEndpoint serviceUrl);
    void shutdown();
}

该接口的主要实现便是JerseyEurekaHttpClientFactory,基于Jersey1.xApacheHttpClient4)实现的,用于创建JerseyApplicationClient客户端。


JerseyEurekaHttpClientFactory

该工厂顾名思义:用于创建JerseyApplicationClient实例。

成员属性:

public class JerseyEurekaHttpClientFactory implements TransportClientFactory {
	
    private final EurekaJerseyClient jerseyClient;
    private final ApacheHttpClient4 apacheClient;
    private final ApacheHttpClientConnectionCleaner cleaner;
    private final Map<String, String> additionalHeaders;

}
  • EurekaJerseyClient jerseyClient:为了得到一个ApacheHttpClient4而构建,当然它可以为null,因为外部可以把构件好ApacheHttpClient4直接传进来
  • ApacheHttpClient4 apacheClient:构建JerseyApplicationClient是需要它的,而构建它是通过高级的EurekaJerseyClient来完成的(默认)
  • ApacheHttpClientConnectionCleaner cleanerApacheHttpClient4 apacheClient的黄金搭档
  • Map<String, String> additionalHeaders:额外的http请求头

初始化:

JerseyEurekaHttpClientFactory:

	// jerseyClient是可以为null的
	// 若jerseyClient不为null,apacheClient由它来提供,否则接受外部传递的
	// 若jerseyClient为null才重新开启一个Cleaner任务,否则无需重新开启(因为jerseyClient内部自己会有定时清理任务)
    private JerseyEurekaHttpClientFactory(EurekaJerseyClient jerseyClient,
                                          ApacheHttpClient4 apacheClient,
                                          long connectionIdleTimeout,
                                          Map<String, String> additionalHeaders) {
        this.jerseyClient = jerseyClient;
        this.apacheClient = jerseyClient != null ? jerseyClient.getClient() : apacheClient;
        this.additionalHeaders = additionalHeaders;
        if (jerseyClient == null) {
            // the jersey client contains a cleaner already so only create this cleaner if we don't have a jersey client
            this.cleaner = new ApacheHttpClientConnectionCleaner(this.apacheClient, connectionIdleTimeout);
        } else { // 强制指定为null
            this.cleaner = null;
        }
    }

	... // 说明:它的public构造器都被标记为了@Deprecated,是因为只想你通过builder的方式去构建

工厂方法:创建一个JerseyApplicationClient实例

JerseyEurekaHttpClientFactory:

    @Override
    public EurekaHttpClient newClient(EurekaEndpoint endpoint) {
        return new JerseyApplicationClient(apacheClient, endpoint.getServiceUrl(), additionalHeaders);
    }

构建方式

为了构建一个JerseyEurekaHttpClientFactory实例,Eureka提供了多种方式:

静态方法:

JerseyEurekaHttpClientFactory:

	// ClientFilter:它是Jersey源生API的一个Filter。能够修改出站HTTP请求或入站HTTP响应
	// 常见的实现有:HTTPBasicAuthFilter  鉴权
	// LoggingFilter:打日志
	// GZIPContentEncodingFilter:支持Gzip压缩(这样response就会以压缩格式返回,通过此过滤器解压)
	// myInstanceInfo:实例信息
	// AbstractEurekaIdentity:用于表示Client客户端的唯一表示(id、name、version等)
    public static JerseyEurekaHttpClientFactory create(EurekaClientConfig clientConfig,
										            Collection<ClientFilter> additionalFilters,
										            InstanceInfo myInstanceInfo,
										            AbstractEurekaIdentity clientIdentity) {
        return create(clientConfig, additionalFilters, myInstanceInfo, clientIdentity, Optional.empty(), Optional.empty());
    }
    // 支持SSL的 很少使用
    public static JerseyEurekaHttpClientFactory create(...) {...}

Builder方式:若上面两个create()不是你想要的,那你也可以使用Builder方式自定义构建:

JerseyEurekaHttpClientFactory:

	// 使用它居多:不开启使用是特性
	// 当然你是可以通过EurekaClientConfig#getExperimental来配置的,默认是不开启的
    public static JerseyEurekaHttpClientFactoryBuilder newBuilder() {
        return new JerseyEurekaHttpClientFactoryBuilder().withExperimental(false);
    }
    public static JerseyEurekaHttpClientFactoryBuilder experimentalBuilder() {
        return new JerseyEurekaHttpClientFactoryBuilder().withExperimental(true);
    }

JerseyEurekaHttpClientFactoryBuilder继承的抽象父类:EurekaClientFactoryBuilder,管理着非常非常多的属性,而这些属性值基本来自于配置EurekaClientConfig(前文已介绍)。

说明:JerseyEurekaHttpClientFactoryBuilder的构造器是public的,若你愿意也可以自己创建一个Builder实例来构建实体

它的build()方法如下:

JerseyEurekaHttpClientFactory.JerseyEurekaHttpClientFactoryBuilder:

    @Override
    public JerseyEurekaHttpClientFactory build() {
        Map<String, String> additionalHeaders = new HashMap<>();
        if (allowRedirect) {
        	// X-Discovery-AllowRedirect:true
            additionalHeaders.put(HTTP_X_DISCOVERY_ALLOW_REDIRECT, "true");
        }
        if (EurekaAccept.compact == eurekaAccept) {
        	// X-Eureka-Accept:full(全部的) or compact(秀珍的)
            additionalHeaders.put(EurekaAccept.HTTP_X_EUREKA_ACCEPT, eurekaAccept.name());
        }

		// 若开启了实验室功能,就单独构建试验室实例返回
        if (experimental) {
            return buildExperimental(additionalHeaders);
        }
        // 大部分会是经过此方法,构建普通实例
        return buildLegacy(additionalHeaders, systemSSL);
    }


	// =====这里面的代码已经很熟悉了,基本同上篇文章的示例代码=====
	private JerseyEurekaHttpClientFactory buildLegacy(Map<String, String> additionalHeaders, boolean systemSSL) {
		
		// 通过EurekaJerseyClientBuilder目的是构建出一个EurekaJerseyClient
	    EurekaJerseyClientBuilder clientBuilder = new EurekaJerseyClientBuilder()
	             .withClientName(clientName)
	             .withUserAgent("Java-EurekaClient")
	             .withConnectionTimeout(connectionTimeout)
	             .withReadTimeout(readTimeout)
	             .withMaxConnectionsPerHost(maxConnectionsPerHost)
	             .withMaxTotalConnections(maxTotalConnections)
	             .withConnectionIdleTimeout((int) connectionIdleTimeout)
	             .withEncoderWrapper(encoderWrapper)
	             .withDecoderWrapper(decoderWrapper)
	             .withProxy(proxyHost,String.valueOf(proxyPort),proxyUserName,proxyPassword);
		...
		EurekaJerseyClient jerseyClient = clientBuilder.build();
		
		... //给Jersey添加上必要的ClientFilter,如EurekaIdentityHeaderFilter、GZIPContentEncodingFilter等等
		
		return new JerseyEurekaHttpClientFactory(jerseyClient, additionalHeaders);
	}

使用TransportClientFactories构建工厂实例

public interface TransportClientFactories<F> {
    
    @Deprecated
    public TransportClientFactory newTransportClientFactory(final Collection<F> additionalFilters,
                                                                   final EurekaJerseyClient providedJerseyClient);
    public TransportClientFactory newTransportClientFactory(final EurekaClientConfig clientConfig,
                                                                   final Collection<F> additionalFilters,
                                                                   final InstanceInfo myInstanceInfo);
    public TransportClientFactory newTransportClientFactory(final EurekaClientConfig clientConfig,
            final Collection<F> additionalFilters,
            final InstanceInfo myInstanceInfo,
            final Optional<SSLContext> sslContext,
            final Optional<HostnameVerifier> hostnameVerifier);
}

实现仅有Jersey1TransportClientFactories,一般这么用:new Jersey1TransportClientFactories()#xxx,有了builder,该方法有点多此一举的赶脚。

通过它产出的Client,唯一就是在其基础增加了MetricsCollecting指标信息收集的功能~


代码示例

下面演示的是标准的、使用工厂方式来构建请求客户端来完成注册的实例。

@Test
public void fun11() {
    // TransportClientFactory factory = JerseyEurekaHttpClientFactory.create();
    TransportClientFactory factory = JerseyEurekaHttpClientFactory.newBuilder()
		 .withClientName("YoutBatman-Client") //必填
		 .build();
		 
    // 准备远端Server的地址
    EurekaEndpoint endpoint = new DefaultEndpoint("http://localhost:8761/eureka/");
    EurekaHttpClient client = factory.newClient(endpoint);


    // 注册服务
    EurekaHttpResponse<Void> response = client.register(InstanceInfo.Builder.newBuilder()
            .setInstanceId("account-002")
            .setHostName("localhost")
            .setIPAddr("127.0.0.1")
            .setDataCenterInfo(new MyDataCenterInfo(DataCenterInfo.Name.MyOwn))
            .setAppName("account") // 大小写无所谓
            .build());
    System.out.println("注册成功,状态码:" + response.getStatusCode());
}

运行程序,Server端页面情况如下阶段(完美,注册成功):


总结

关于远程通信模块:使用TransportClientFactory构建底层请求客户端完成服务注册、服务下线就介绍到这,可以说这是使用者标准的使用姿势,供以你学习和参考。

当然喽,和Server端的通讯不可能需要我们手动构建,也不可能直接是赤裸裸的单连,重试呢?会话保持呢?这些高级功能都还木有,而其实这些Eureka本身也都有提供,请接着下文继续学习。

本文参与腾讯云自媒体分享计划,欢迎正在阅读的你也加入,一起分享。

我来说两句

0 条评论
登录 后参与评论

相关文章

  • 关于Spring属性处理器PropertyResolver以及应用运行环境Environment的深度分析,强大的StringValueResolver使用和解析【享学Spring】

    若直接提PropertyResolver或者StringValueResolver可能很小伙伴会觉得非常的陌生,但是我若提Environment和Embedde...

    YourBatman
  • 聊聊Spring中的数据绑定 --- WebDataBinder、ServletRequestDataBinder、WebBindingInitializer...【享学Spring】

    上篇文章聊了DataBinder,这篇文章继续聊聊实际应用中的数据绑定主菜:WebDataBinder。

    YourBatman
  • 【小家java】Java之Apache Commons-IO使用精讲(FileUtils、IOUtils、FileFilter全覆盖)

    该工具类可能是平时使用得最多的工具类了。 IOUtils包含处理读、写和复制的工具方法。方法对InputStream、OutputStream、Reader和...

    YourBatman
  • 自定义参数解析器

    开发中,app端给服务端会传基础参数、其他参数,一般基础参数app端都会传给服务端,其他参数则是根据不同接口传不同参数。若以表单的形式提交的数据:

    LiosWong
  • ProcessFunction:Flink最底层API使用踩坑记录

    DataStream与KeyedStreamd都有Process方法, DataStream接收的是ProcessFunction,而KeyedStream接收...

    王知无
  • Flume+Kafka+Storm+Hbase+HDSF+Poi整合

    举例:这个网站www.hongten.com(当然这是一个我虚拟的电商网站),用户在这个网站里面可以有很多行为,比如注册,登录,查看,点击,双击,购买东西,加入...

    Hongten
  • 不了解这 12 个语法糖,别说你会 Java!

    本文从 Java 编译原理角度,深入字节码及 class 文件,抽丝剥茧,了解 Java 中的语法糖原理及用法,帮助大家在学会如何使用 Java 语法糖的同时,...

    Java技术江湖
  • 不懂这12个语法糖,别说你会Java!

    本文从 Java 编译原理角度,深入字节码及 class 文件,抽丝剥茧,了解 Java 中的语法糖原理及用法,帮助大家在学会如何使用 Java 语法糖的同时,...

    Java技术江湖
  • 在Java中12个常见的语法糖!

    本文从 Java 编译原理角度,深入字节码及 class 文件,抽丝剥茧,了解 Java 中的语法糖原理及用法,帮助大家在学会如何使用 Java 语法糖的同时,...

    Java3y
  • 不懂这12个语法糖,别说你会Java!

    本文从 Java 编译原理角度,深入字节码及 class 文件,抽丝剥茧,了解 Java 中的语法糖原理及用法,帮助大家在学会如何使用 Java 语法糖的同时,...

    纯洁的微笑

扫码关注云+社区

领取腾讯云代金券