我们在详细了解了Zookeeper之后,我们就可以自己动手来实现一个基于Zookeeper实现的服务注册与发现,首先我们就需要建立两个微服务,一个订单服务(OrderService),一个产品服务(ProductService),其中订单服务时需要调用产品服务的。
首先我们就先来看一看产品服务(ProductService)是如何注册到Zookeeper上的,首先我们就来新来建立一个SpringBoot的项目
在该服务启动的时候我们就需要将该服务给注册到Zookeeper上去,这里我们就需要借助Servlet API中的ServletContextListener接口,它能够监听 ServletContext 对象的生命周期,实际上就是监听 Web 应用的生命周期。
当Servlet容器启动或终止Web应用时,会触发ServletContextEvent事件,该事件由ServletContextListener 来处理。在ServletContextListener 接口中定义了处理ServletContextEvent 事件的两个方法。
@WebListener
public class InitListener implements ServletContextListener {
@Override
public void contextInitialized(ServletContextEvent sce) {
try {
Properties properties = new Properties();
properties.load(InitListener.class.getClassLoader().getResourceAsStream("application.properties"));
String hostAddress = InetAddress.getLocalHost().getHostAddress();
int port = Integer.valueOf(properties.getProperty("server.port"));
ServiceRegister.register(hostAddress,port);
} catch (Exception e) {
e.printStackTrace();
}
}
@Override
public void contextDestroyed(ServletContextEvent sce) {
}
}
这里我们使用了@WebListener
注解,这个我们曾在Web的三大组件的简单使用,从web.xml到注解中简单介绍过,但是这里我们不要忘记了在SpringBoot的启动类上,还需要加上@ServletComponentScan
注解
当然我们还可以使用另外一种方式,我们不使用@WebListener
@ServletComponentScan
注解也是可以的,直接在SpringBoot的启动类中,使用@Bean
亦可,如下:
接下来我们来仔细看一下InitListener类中,是如何将其注册到Zookeeper上的,这里首先是获取到了项目中的配置文件application.properties,至于如何获取,在Java获取resources下的文件路径 介绍过多种方式
当然在我们还可以将其添上注解@Component,添加到Spring环境中,然后我们使用@Value获取配置文件中的服务端口号了
在上述不仅获取了端口号,还获取到了IP地址,我们就要将IP+PORT给注册到Zookeeper上去,这里我们肯定想到的就是之前介绍的节点存储IP+PORT信息嘛,这里我们就可以使用我们之前介绍的三个客户端中的任意一种,这里我们就选择Curator,该客户端比原生的客户端的优点,我们之前已经详细介绍过了,这里我们就需要引入相关的依赖了
<dependency>
<groupId>org.apache.zookeeper</groupId>
<artifactId>zookeeper</artifactId>
<version>3.4.12</version>
<exclusions>
<exclusion>
<artifactId>slf4j-log4j12</artifactId>
<groupId>org.slf4j</groupId>
</exclusion>
</exclusions>
</dependency>
<!--对zookeeper的底层api的一些封装-->
<dependency>
<groupId>org.apache.curator</groupId>
<artifactId>curator-framework</artifactId>
<version>4.3.0</version>
</dependency>
<!--封装了一些高级特性,如:Cache事件监听、选举、分布式锁、分布式Barrier-->
<dependency>
<groupId>org.apache.curator</groupId>
<artifactId>curator-recipes</artifactId>
<version>4.3.0</version>
</dependency>
这里稍微需要注意的就是我们需要Zookeeper中的slf4j,因为这里和会SpringBoot中的slf4j发送版本冲突,然后就是非常简单的将信息注册到Zookeeper的节点上了,这里如果我们使用的是原生客户端,那我们就需要更多额外的操作了,比如一层层地判断父节点是否存在,若不存在则创建父节点,还需要在连接客户的的时候使用CountDownLatch及Watch进行等待成功连接
public class ServiceRegister {
private static final String CONNENT_ADDR = "127.0.0.1:2181";
private static final String BASE_SERVICES = "/services";
private static final String SERVICE_NAME = "/products";
public static void register(String address, int port) throws Exception {
CuratorFramework curatorFramework = CuratorFrameworkFactory.builder()
.connectString(CONNENT_ADDR)
.connectionTimeoutMs(5000) //连接超时时间
.sessionTimeoutMs(5000) //会话超时时间
.retryPolicy(new ExponentialBackoffRetry(1000, 3))
.build();
curatorFramework.start();
String path = BASE_SERVICES + SERVICE_NAME;
String server_path = address + ":" + port;
curatorFramework.create()
.creatingParentsIfNeeded()
.withMode(CreateMode.EPHEMERAL_SEQUENTIAL)
.forPath(path + "/child", server_path.getBytes());
}
}
接下来我们几乎就完成了产品服务的工作,我们可以改变application.properties中的端口号,启动多个产品服务,其产品服务向外提供的接口如下,就是简单返回该服务的端口号
在完成了产品服务的注册,接下来肯定就是订单服务的发现啦,首先同时我们也是需要新建一个SpringBoot项目的,同样的我们还是需要借助Servlet API中的ServletContextListener接口
与产品服务不同的是,产品服务中的InitListener类是为了在当Servlet容器启动时,将产品服务的IP+PORT信息注册到Zookeeper上去,而订单服务中的InitListener是为了在Servlet容器启动时,将Zookeeper中注册的产品服务信息拉取下来,以供订单服务来调用
public class InitListener implements ServletContextListener {
private static final String CONNENT_ADDR = "127.0.0.1:2181";
private static final String BASE_SERVICES = "/services";
private static final String SERVICE_NAME = "/products";
@Override
public void contextInitialized(ServletContextEvent sce) {
CuratorFramework curatorFramework = CuratorFrameworkFactory.builder()
.connectString(CONNENT_ADDR)
.connectionTimeoutMs(5000) //连接超时时间
.sessionTimeoutMs(5000) //会话超时时间
.retryPolicy(new ExponentialBackoffRetry(1000, 3))
.build();
curatorFramework.start();
List<String> newServerList = new ArrayList<>();
String path = BASE_SERVICES + SERVICE_NAME;
final PathChildrenCache cache = new PathChildrenCache(curatorFramework, path, true);
try {
cache.start(PathChildrenCache.StartMode.POST_INITIALIZED_EVENT);
cache.getListenable().addListener(new PathChildrenCacheListener() {
//监听子节点的变化
@Override
public void childEvent(CuratorFramework cf, PathChildrenCacheEvent event) throws Exception {
List<String> list = curatorFramework.getChildren().forPath(path);
for (String subNode : list) {
byte[] bytes = curatorFramework.getData().forPath(path + "/" + subNode);
String host = new String(bytes, "utf-8");
newServerList.add(host);
}
LoadBalance.SERVICE_LIST = newServerList;
}
});
} catch (Exception e) {
e.printStackTrace();
}
}
@Override
public void contextDestroyed(ServletContextEvent sce) {
}
}
这里我们需要监听Zookeeper上节点的变化,因为万一有产品服务宕机或者新增了产品服务,我们需要能够及时获取到信息,有关Curator监听的相关介绍,可见Zookeeper客户端(五)—— Curator
我们在监听到节点的变化时,我们就会将其更新值LoadBalance类中,其中主要就是简单模拟下负载均衡,这里我们就是在订单服务调用产品服务时,在所有的产品服务中随机选取一个服务
public class LoadBalance {
public volatile static List<String> SERVICE_LIST;
public String choseService() {
String result = "";
if (!CollectionUtils.isEmpty(SERVICE_LIST)) {
int index = new Random().nextInt(SERVICE_LIST.size());
result = SERVICE_LIST.get(index);
}
return result;
}
}
这里我们基于Zookeeper的服务注册与发现就基本完成了,然后我们在订单服务中也提供一个Controller,该Controller会去调用产品服务的Controller,这里借助了RestTemplate,如下:
@RestController
@RequestMapping("/order")
public class OrderController {
@Resource
private RestTemplate restTemplate;
private LoadBalance loadBalance = new LoadBalance();
@RequestMapping("/getOrder")
public Object getProduct(HttpServletRequest request) {
String host = loadBalance.choseService();
return restTemplate.getForObject("http://" + host + "/product/getProduct", Object.class);
}
}
以上就完成了一个简单的服务注册与发现,这里我们来进行测试一下,我们可以改变产品服务(ProductService)中的applicat.properties文件中的端口号,来启动多个产品服务,这里我们启动了8080,8081两个服务服务,我们可以通过ZooInspector来查看下Zookeeper的节点信息,如下:
然后我们在启动一个订单服务,在浏览器中不停的刷新,就可以看到会随机调用不同的产品服务