代码下载地址:https://github.com/f641385712/netflix-learning
通过前两篇文章一步一步介绍了如何构建出一个JerseyApplicationClient
实例来完成服务注册、服务下线等功能。而上文讲到了使用Eureka提供的EurekaJerseyClient
通过Builder模式去构建已经比较方便了,但是仍旧存在一个小问题:无法摆脱Jersey
关键字的感知,自然底层上也就无法摆脱对Jersey库的强依赖。
即使你并不需要使用Jersey的API来做事,但使用时你却明显的知道你实际就是在用Jersey来干活,似乎有点欲盖弥彰。这种“强耦合”很明显是制约了Eureka的弹性扩展性的,因此Eureka它提供了TransportClientFactory
工厂用于屏蔽这一切,这便是本文讲述的内容。
通过工厂方式创建实例的方式来解耦,是最为常用的一种设计模式。Eureka也不例外,使用该种模式能达到预期的效果。
当然喽,在此之前先要了解下Eureka中终端这个抽象概念: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();
}
接口方法见名之意,无需解释。它的继承图谱如下:
它的是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);
}
它是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));
}
...
}
}
EurekaHttpClient
创建工厂接口,负责创建low level
低等级的Client(后面会讲还有用于创建高级的EurekaHttpClient
的工厂:EurekaHttpClientFactory
),所以你看它的命名是Transport
含义,表明自己是用于数据传输级别的。
public interface TransportClientFactory {
// 根据终端EurekaEndpoint创建一个底层的可发送Http请求的Client
EurekaHttpClient newClient(EurekaEndpoint serviceUrl);
void shutdown();
}
该接口的主要实现便是JerseyEurekaHttpClientFactory
,基于Jersey1.x
(ApacheHttpClient4
)实现的,用于创建JerseyApplicationClient
客户端。
该工厂顾名思义:用于创建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 cleaner
:ApacheHttpClient4 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);
}
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本身也都有提供,请接着下文继续学习。