如果你喜欢底层开发,千万不要勉强自己去搞VC,找到你最真实的想法,程序员最不能忍受的就是万精油。
–> 返回专栏总目录 <–
代码下载地址:https://github.com/f641385712/netflix-learning
目录
前言
上篇文章介绍了Eureka底层通信模块中的默认基于Jersey实现:JerseyApplicationClient
,文末指出我们一般并不会通过构造器去直接构造出它的实例来完成请求的发送。本文将结合代码示例的方式进一步讲述为何不建议手动构建的原因,以及逐步引导出“规范”的使用方式。
正文
由于地层通信模块是理解Eureka,以及优化、定制Eureka的核心要点之一,因此本系列大着笔墨书写之,相信可以帮助到你理解到Eureka的核心内容点,这样对日后排查问题、调优都能做到心中有数。
Eureka Server端搭建(后同)
从本文开始,将搭建好Eureka的Server端。
关于Server端的搭建,第一篇文章已经说明了:本文借助spring-cloud-starter-netflix-eureka-server
来完成Server端的快速搭建。
Server端配置如下application.yaml
:
server:
port: 8761
# Eureka配置
eureka:
# server:
# # 关闭自我保护机制
# enable-self-preservation: false
client:
# 不用自己注册自己
register-with-eureka: false
启动Eureka Server,打开可视化页面,如图:
这样Client端只需要连接http://localhost:8761/eureka/
这个地址即可,也就是serviceUrl指定为它便可完成访问和服务注册。
纯手工构造问题抛出
想要使用JerseyApplicationClient
完成服务注册很直接:它只有一个构造器,按照规定传入指定参数即可,如下示例:
@Test
public void fun10() {
Client jerseyClient = Client.create();
JerseyApplicationClient client = new JerseyApplicationClient(jerseyClient, "http://localhost:8761/eureka/", null);
EurekaHttpResponse<Void> response = client.register(InstanceInfo.Builder.newBuilder()
.setInstanceId("account-001")
.setHostName("localhost")
.setIPAddr("127.0.0.1")
.setDataCenterInfo(new MyDataCenterInfo(DataCenterInfo.Name.MyOwn))
.setAppName("account") // 大小写无所谓
.build());
System.out.println("注册成功,状态码:" + response.getStatusCode());
}
这是最简实现,运行程序,报错:
Caused by: com.sun.jersey.api.client.ClientHandlerException: A message body writer for Java type, class com.netflix.appinfo.InstanceInfo, and MIME media type, application/json, was not found
at com.sun.jersey.api.client.RequestWriter.writeRequestEntity(RequestWriter.java:288)
...
这个错是Jersey库抛出的,原因也很容易理解:你指定了请求体格式为JSON格式(Content-type:application/json
),但是你却木有能够把InstanceInfo
序列化为JSON格式的序列化器,所以抛错了。
两种解决方案
既然定位了问题所在,就不缺解决方案了。本处给出两种途径:
- 给
com.sun.jersey.api.client.Client
指定可用的序列化、反序列化器- 优点:能解决问题
- 缺点:需要对
jersey
的玩法有较为深入了解,有较大的学习成本
- 既然只是为了解决Eureka的服务注册、下线等问题,那就从Eureka本身去寻找更佳的方案
- 目的:不用去了解具体的Http通信技术的实现,万一换了通信方式呢?还得学一遍麽?
综合来看,方案一显然是不可取的。那么本文主要研究方案二,找找仅依赖Eureka层级的高级API就能解决该问题的方案。
Eureka对底层通讯库是有封装的,目的是不希望让使用者感知到它的存在,这样也方便做无感知的底层技术切换(比如切换为OkHttp的实现等)。下面针对这套API的核心要点进行学习。
在Eureka
中使用Jersey的实现中,强耦合进了对Apache HttpClient的依赖,所以在API中你会经常看见ApacheXXX字样。
ApacheHttpClientConnectionCleaner
在后台运行的定时进程,清除Apache http客户端连接池中的空闲连接。定时清理,这可以防止在半关闭状态下积累未使用的连接。
它的原理很简单:使用一个ScheduledExecutorService
去定时执行,默认是30s
执行一次清理。至于连接多久算作空闲(可被回收)了,是由前面的介绍的EurekaClientConfig#getEurekaConnectionIdleTimeoutSeconds
来定的,它的默认值是30s
。
该值可以通过
eureka.eurekaserver.connectionIdleTimeoutInSeconds = xxx
来指定,单位是秒
EurekaJerseyClient
它对ApacheHttpClient4
进行的包装,目的是使使用者仅需接触Eureka的API即可。该接口用于得到/获取一个实际做事的Client(ApacheHttpClient4
)。
public interface EurekaJerseyClient {
// 此处绑定了,实现必须是基于Apache的ApacheHttpClient4
ApacheHttpClient4 getClient();
// 清理资源
void destroyResources();
}
说明:ApacheHttpClient4
是jersey-apache-client4
扩展包下的Client
实现类,底层使用Apache
的HttpClient
实现Http
请求的发送。
EurekaJerseyClientImpl
它是EurekaJerseyClient
接口的唯一实现类,目的就是把Jersey的API都“藏起来”。
成员属性:
public class EurekaJerseyClientImpl implements EurekaJerseyClient {
private final ApacheHttpClient4 apacheHttpClient;
private final ApacheHttpClientConnectionCleaner apacheHttpClientConnectionCleaner;
ClientConfig jerseyClientConfig;
}
ApacheHttpClient4 apacheHttpClient
:目标Client,getClient()
会返回它apacheHttpClientConnectionCleaner
:清理ApacheHttpClient4
空闲链接的定时器jerseyClientConfig
:com.sun.jersey.api.client.config.ClientConfig
Jersey的配置类,最终会应用到创建ApacheHttpClient4
身上
属性赋值(初始化动作)均在构造函数里:
EurekaJerseyClientImpl:
// 构造时需要指定连接超时、读取超时时间
public EurekaJerseyClientImpl(int connectionTimeout, int readTimeout, final int connectionIdleTimeout, ClientConfig clientConfig) {
try {
jerseyClientConfig = clientConfig;
apacheHttpClient = ApacheHttpClient4.create(jerseyClientConfig);
// 额外设置连接超时、读取超时时间
HttpParams params = apacheHttpClient.getClientHandler().getHttpClient().getParams();
HttpConnectionParams.setConnectionTimeout(params, connectionTimeout);
HttpConnectionParams.setSoTimeout(params, readTimeout);
// 启动定时任务
this.apacheHttpClientConnectionCleaner = new ApacheHttpClientConnectionCleaner(apacheHttpClient, connectionIdleTimeout);
} catch (Throwable e) {
throw new RuntimeException("Cannot create Jersey client", e);
}
}
该唯一构造器是唯一的初始化方法,需要指定4个参数,使用起来其实并不方便。并且它最大的一个弊端是:你依旧还得理解com.sun.jersey.api.client.config.ClientConfig
这个Jersey API。
为此,它提供了一个内部类:构建器EurekaJerseyClientBuilder
来方便外部使用构建其实例:
EurekaJerseyClientImpl:
public static class EurekaJerseyClientBuilder {
private boolean systemSSL;
private String clientName;
...
private int connectionTimeout;
private int readTimeout;
private int connectionIdleTimeout;
...
private EncoderWrapper encoderWrapper;
private DecoderWrapper decoderWrapper;
...
public EurekaJerseyClientBuilder withClientName(String clientName) {
this.clientName = clientName;
return this;
}
...
// 上面with定义好的属性们,最终都会收拢到内部类MyDefaultApacheHttpClient4Config里 这就是一个config
// 其中内部使用DiscoveryJerseyProvider完成序列化、发序列化
// 复杂点是处理SSL安全问题
public EurekaJerseyClient build() {
MyDefaultApacheHttpClient4Config config = new MyDefaultApacheHttpClient4Config();
try {
return new EurekaJerseyClientImpl(connectionTimeout, readTimeout, connectionIdleTimeout, config);
} catch (Throwable e) {
throw new RuntimeException("Cannot create Jersey client ", e);
}
}
}
代码示例
通过Builder构建出一个EurekaJerseyClient
实例,完全不需要感知到Jersey源生API的存在。
@Test
public void fun8() {
EurekaJerseyClient jerseyClient = new EurekaJerseyClientImpl.EurekaJerseyClientBuilder()
.withClientName("YoutBatman-Client")
.withConnectionTimeout(2000)
.withReadTimeout(3000)
.withConnectionIdleTimeout(30 * 60 * 1000)
.withMaxConnectionsPerHost(50)
//十分注意:此值必须设定,否则将获取不到连接(超时)
.withMaxTotalConnections(200)
.build();
// 构建请求的资源路径:可以访问任意的网络资源
ApacheHttpClient4 client = jerseyClient.getClient();
WebResource.Builder resourceBuilder = client.resource("http://www.baidu.com").path("").getRequestBuilder();
ClientResponse response = resourceBuilder
// .header("Accept-Encoding", "gzip") // 若开启了这个,对方就会以gzip的形式返回,你就得会解码才行,否则乱码哦~~~
// .type(MediaType.APPLICATION_JSON_TYPE)
// .accept(MediaType.APPLICATION_JSON)
.get(ClientResponse.class);
System.out.println("响应码:" + response.getStatus());
System.out.println("响应体:" + response.getEntity(String.class));
jerseyClient.destroyResources();
}
运行程序,控制台正常打印:
// 从这两句日志可以看出:若你木有指定编码器、解码器,默认
// Json格式使用Jackson序列化/反序列化
// xml格式使用XStreamXml序列化/反序列化
22:33:04.988 [main] INFO com.netflix.discovery.provider.DiscoveryJerseyProvider - Using JSON encoding codec LegacyJacksonJson
22:33:05.000 [main] INFO com.netflix.discovery.provider.DiscoveryJerseyProvider - Using JSON decoding codec LegacyJacksonJson
22:33:05.526 [main] INFO com.netflix.discovery.provider.DiscoveryJerseyProvider - Using XML encoding codec XStreamXml
22:33:05.526 [main] INFO com.netflix.discovery.provider.DiscoveryJerseyProvider - Using XML decoding codec XStreamXml
响应码:200
... // 省略百度首页的html
手动构建JerseyApplicationClient的解决方案
通过如上介绍,便可达到如下目的:
- 构建出一个可以发送Http请求的Eureka Client端
- 完全无需感知到底层实现API(如Jersey的API)的存在
下面代码演示服务注册示例:
@Test
public void fun9() {
EurekaJerseyClient jerseyClient = new EurekaJerseyClientImpl.EurekaJerseyClientBuilder()
.withClientName("YoutBatman-Client")
.withConnectionTimeout(2000)
.withReadTimeout(3000)
.withConnectionIdleTimeout(30 * 60 * 1000)
.withMaxConnectionsPerHost(50)
//十分注意:此值必须设定,否则将获取不到连接(超时)
.withMaxTotalConnections(200)
.build();
JerseyApplicationClient client = new JerseyApplicationClient(jerseyClient.getClient(), "http://localhost:8761/eureka/", null);
// 服务注册 构建的实例是InstanceInfo
EurekaHttpResponse<Void> response = client.register(InstanceInfo.Builder.newBuilder()
.setInstanceId("account-001")
.setHostName("localhost")
.setIPAddr("127.0.0.1")
.setDataCenterInfo(new MyDataCenterInfo(DataCenterInfo.Name.MyOwn))
.setAppName("account") // 大小写无所谓
.build());
System.out.println("注册成功,状态码:" + response.getStatusCode());
}
运行程序,控制台正常打印:
10:19:45.846 [main] INFO com.netflix.discovery.provider.DiscoveryJerseyProvider - Using JSON encoding codec LegacyJacksonJson
10:19:45.852 [main] INFO com.netflix.discovery.provider.DiscoveryJerseyProvider - Using JSON decoding codec LegacyJacksonJson
10:19:46.272 [main] INFO com.netflix.discovery.provider.DiscoveryJerseyProvider - Using XML encoding codec XStreamXml
10:19:46.272 [main] INFO com.netflix.discovery.provider.DiscoveryJerseyProvider - Using XML decoding codec XStreamXml
10:19:46.283 [main] DEBUG com.netflix.discovery.shared.transport.jersey.AbstractJerseyEurekaHttpClient - Created client for url: http://localhost:8761/eureka/
10:19:46.322 [main] DEBUG com.netflix.discovery.shared.transport.jersey.AbstractJerseyEurekaHttpClient - Jersey HTTP POST http://localhost:8761/eureka//apps/ACCOUNT with instance account-001; statusCode=204
// 小细节:状态码是204,不是200哦
注册成功,状态码:204
Eureka Server端也能看到这个注册上去的服务实例:
总结
关于远程通信模块:手动构建JerseyApplicationClient客户端完成服务注册、服务下线…就介绍到这。本文一步一步的介绍Eureka是如何做到底层API无关性的,这样对使用者是非常友好的:并不需要再去多学一门技术,而是只学Eureka的抽象便可,哪怕你换了底层实现都木有关系。
但是,这还不够纯粹,虽然你并没有直接的构造Jersey的API,但你仍旧接触到了Jersey字样,so那必然还和Jersey库存在一定的耦合性。那么下文将继续介绍全自动的构建方式,彻底做到零感知。
声明
原创不易,码字不易,多谢你的点赞、收藏、关注。把本文分享到你的朋友圈是被允许的,但拒绝抄袭
。你也可【左边扫码/或加wx:fsx641385712】邀请你加入我的 Java高工、架构师 系列群大家庭学习和交流。
- 3分钟带你了解轻量级依赖注入框架Google Guice【享学Java】
- [享学Eureka] 一、源生Eureka介绍 — 基于注册中心的服务发现
- [享学Eureka] 二、Eureka的最核心概念:InstanceInfo实例信息
- [享学Eureka] 三、Eureka配置之:EurekaInstanceConfig实例配置
- [享学Eureka] 四、Eureka配置之:EurekaClientConfig客户端配置
- [享学Eureka] 五、Eureka核心概念:应用(Application)和注册表(Applications)
- [享学Eureka] 六、InstanceInfo实例管理器:ApplicationInfoManager
- [享学Eureka] 七、远程通信模块:EurekaHttpClient接口抽象以及基于Jersey的Low-Level实现JerseyApplicationClient