本文主要讲zookeeper如何注册服务并使用这些服务。简单流程是使用springboot开发简单的restapi.并把api注册到zookeeper。最后在客户端连接到zookeeper调用api并把结果返回到客户端。
新建一个maven工程,并引入spring boot的jar包。pom配置是:
<parent>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starter-parentartifactId>
<version>1.3.3.RELEASEversion>
parent>
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starter-webartifactId>
dependency>
写一个测试的api
package demo.msa.sample;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.bind.annotation.RestController;
@RestController
@SpringBootApplication(scanBasePackages="demo.msa")
public class SampleApplication {
@RequestMapping(name="HelloService",method=RequestMethod.GET,path="/hello")
public String hello(){
return "hello";
}
public static void main(String[] args) {
SpringApplication.run(SampleApplication.class, args);
}
}
启动后访问效果如下:
端口配置是在application.properties
zookeeper本地搭建,网上有很多教程,这里不在赘述。说一下服务注册的原理
zookeeper的结构是基于znode的树状结构。根节点是‘/’,我们可以在根节点下扩展任意节点。
下面开始具体的开发:
(1)新建两个项目,msa-framework和msa-sample-api。
(2)在msa-framework定义服务注册表接口
package demo.msa.framework.registry;
public interface ServiceRegistry {
/** * 注册服务信息 * @param serviceName 服务名称 * @param serviceAddress 服务地址 */
void register(String serviceName,String serviceAddress);
}
打包,并引入到msa-sample-api工程
<dependency>
<groupId>demo.msagroupId>
<artifactId>msa-frameworkartifactId>
<version>0.0.1-SNAPSHOTversion>
dependency>
(3)在msa-sample-api项目中创建一个ServiceRegistry的实现类。实现注册接口。
package demo.msa.frame.registry;
import java.util.concurrent.CountDownLatch;
import org.apache.zookeeper.CreateMode;
import org.apache.zookeeper.WatchedEvent;
import org.apache.zookeeper.Watcher;
import org.apache.zookeeper.ZooDefs;
import org.apache.zookeeper.ZooKeeper;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Component;
import demo.msa.framework.registry.ServiceRegistry;
@Component
public class ServiceRegistryImpl implements Watcher, ServiceRegistry {
private static final int SESSION_TIMEOUT = 5000;
private static final String REGISTRY_PATH = "/registry";
private static CountDownLatch latch = new CountDownLatch(1);
private static Logger logger = LoggerFactory
.getLogger(ServiceRegistryImpl.class);
private ZooKeeper zk;
public ServiceRegistryImpl() {
logger.debug("初始化类");
}
public ServiceRegistryImpl(String zkServers) {
try {
zk = new ZooKeeper(zkServers, SESSION_TIMEOUT, this);
latch.await();
logger.debug("connected to zookeeper");
} catch (Exception e) {
logger.error("create zookeeper client failuer", e);
}
}
@Override
public void register(String serviceName, String serviceAddress) {
String registryPath = REGISTRY_PATH;
try {
logger.debug("-zk---------"+zk);
//创建根节点:持久节点
if (zk.exists(registryPath, false) == null) {
zk.create(registryPath, null, ZooDefs.Ids.OPEN_ACL_UNSAFE,
CreateMode.PERSISTENT);
logger.debug("create registry node:{}",registryPath);
}
//创建服务节点:持久节点
String servicePath=registryPath+"/"+serviceName;
if(zk.exists(servicePath, false)==null){
zk.create(servicePath, null, ZooDefs.Ids.OPEN_ACL_UNSAFE, CreateMode.PERSISTENT);
logger.debug("create service node :{}"+servicePath);
}
//创建地址节点:临时顺序节点
String addressPath=servicePath+"/address-";
String addressNode=zk.create(addressPath, serviceAddress.getBytes(), ZooDefs.Ids.OPEN_ACL_UNSAFE, CreateMode.EPHEMERAL_SEQUENTIAL);
logger.debug("create node address:{}=>{}"+addressNode);
} catch (Exception e) {
logger.error("create node failure",e);
}
}
@Override
public void process(WatchedEvent event) {
if (event.getState() == Event.KeeperState.SyncConnected) {
latch.countDown();
}
}
}
(4)创建一个配置类来读取application.properties的配置
package demo.msa.configure;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import demo.msa.frame.registry.ServiceRegistryImpl;
import demo.msa.framework.registry.ServiceRegistry;
@Configuration
@ConfigurationProperties(prefix="registry")
public class RegistryConfig {
private String servers;
@Bean
public ServiceRegistry serviceRegistry(){
return new ServiceRegistryImpl(servers);
}
public String getServers() {
return servers;
}
public void setServers(String servers) {
this.servers = servers;
}
}
(4)由于我们的服务是以javaweb的形式发布的。所以在每个应用初始化时,去完成接口的注册是一个好时机。因此我们新加一个类去实现ServletContextListener,并把这个类交给spring来管理。
package demo.msa.listener;
import java.util.Map;
import javax.servlet.ServletContext;
import javax.servlet.ServletContextEvent;
import javax.servlet.ServletContextListener;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.ApplicationContext;
import org.springframework.stereotype.Component;
import org.springframework.web.context.support.WebApplicationContextUtils;
import org.springframework.web.method.HandlerMethod;
import org.springframework.web.servlet.mvc.method.RequestMappingInfo;
import org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerMapping;
import demo.msa.framework.registry.ServiceRegistry;
@Component
public class WebListener implements ServletContextListener{
private static Logger logger = LoggerFactory
.getLogger(WebListener.class);
@Value("${server.address}")
private String serverAddress;
@Value("${server.port}")
private int serverPort;
@Value("${server.name}")
private String serverName;
@Autowired
private ServiceRegistry serviceRegistry;
@Override
public void contextDestroyed(ServletContextEvent event) {
// TODO Auto-generated method stub
}
@Override
public void contextInitialized(ServletContextEvent event) {
//获取请求映射
ServletContext servletContext=event.getServletContext();
ApplicationContext applicationContext=WebApplicationContextUtils.getRequiredWebApplicationContext(servletContext);
RequestMappingHandlerMapping mapping=applicationContext.getBean(RequestMappingHandlerMapping.class);
Map infoMap=mapping.getHandlerMethods();
for(RequestMappingInfo info:infoMap.keySet()){
String serviceName=info.getName();
logger.debug("-----------"+serviceName);
if(null!=serviceName){
serviceRegistry.register(serviceName,String.format("%s:%d/%s", serverAddress,serverPort,serverName) );
}
}
}
}
启动msa-sample-api应用程序,我们可以在控制台看到以下信息,说明服务已经注册成功了。
一个服务注册到zookeeper可以有多个地址节点,当有多个地址节点的时候,可以通过轮询或者hash的方式来获取地址节点
新建一个工程msa-sample-web,这个工程相当于这个架构中的前端部分。因此我们需要一个html页面来发送一个ajax请求。
具体实现如下:
引入相关jar:
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starter-webartifactId>
dependency>
<dependency>
<dependency>
<groupId>org.apache.zookeepergroupId>
<artifactId>zookeeperartifactId>
<version>3.4.6version>
dependency>
页面实现:
<html>
<head>
<title>TestZk.htmltitle>
<meta http-equiv="keywords" content="keyword1,keyword2,keyword3">
<meta http-equiv="description" content="this is my page">
<meta http-equiv="content-type" content="text/html; charset=UTF-8">
head>
<body>
This is my HTML page. <br>
<div >
<textarea id="console" value='1234' >
textarea>div>
<script type="text/javascript" src="js/jquery-1.6.2.min.js">script>
<script type="text/javascript"> $(function(){ $.ajax({ method:'GET', dataType: 'JSONP', url:'hello', dataType:'text', headers:{ 'Service-Name':'HelloService'}, success: function (data,textStatus){ $('#console').text(data); window.location.href = 'http://'+data; }, error:function (XMLHttpRequest, textStatus, thrownError) { alert("error"); var result = XMLHttpRequest.responseText; alert(result); } }); }); script>
body>
html>
web.xml配置
<servlet>
<servlet-name>helloservlet-name>
<servlet-class>demo.msa.servlet.MsaDemoServletservlet-class>
<load-on-startup>0load-on-startup>
servlet>
<servlet-mapping>
<servlet-name>helloservlet-name>
<url-pattern>/hellourl-pattern>
servlet-mapping>
servlet实现
package demo.msa.servlet;
import java.io.IOException;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import demo.msa.help.ServiceHelp;
import demo.msa.help.ServiceHelpImpl;
public class MsaDemoServlet extends HttpServlet{
/** * */
private static final long serialVersionUID = 1L;
private String addressData="";
private static Logger logger = LoggerFactory
.getLogger(MsaDemoServlet.class);
protected void doGet(HttpServletRequest request,
HttpServletResponse response) throws ServletException, IOException {
try {
String serviceName=request.getHeader("Service-Name");
ServiceHelp help=new ServiceHelpImpl("127.0.0.1:2181");
addressData=help.getData(serviceName);
} catch (Exception e) {
logger.error("get address node data error",e);
}
response.setContentType("application/json; charset=UTF-8");
response.setCharacterEncoding("UTF-8");
response.getWriter().print(addressData);
}
protected void doPost(HttpServletRequest request,
HttpServletResponse response) throws ServletException, IOException {
doGet(request,response);
}
}
定义一个接口获取地址节点的数据
package demo.msa.help;
public interface ServiceHelp {
public String getData(String serviceName);
}
实现该接口的类:
package demo.msa.help;
import java.util.List;
import java.util.concurrent.CountDownLatch;
import org.apache.zookeeper.WatchedEvent;
import org.apache.zookeeper.Watcher;
import org.apache.zookeeper.ZooKeeper;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Component;
@Component
public class ServiceHelpImpl implements Watcher,ServiceHelp{
private static final int SESSION_TIMEOUT = 5000;
private static final String REGISTRY_PATH = "/registry";
private static CountDownLatch latch = new CountDownLatch(1);
private static Logger logger = LoggerFactory
.getLogger(ServiceHelpImpl.class);
private ZooKeeper zk;
public ServiceHelpImpl() {
logger.debug("初始化类");
}
public ServiceHelpImpl(String zkServers) {
try {
zk = new ZooKeeper(zkServers, SESSION_TIMEOUT, this);
latch.await();
logger.debug("connected to zookeeper");
} catch (Exception e) {
logger.error("create zookeeper client failuer", e);
}
}
@Override
public String getData(String serviceName) {
try {
String servicePath=REGISTRY_PATH+"/"+serviceName;
logger.debug("service_path is :"+servicePath);
List childPath;
childPath = zk.getChildren(servicePath, false);
if(childPath.size()==0){
logger.error("%s address node is not exsited",serviceName);
throw(new Exception("地址未找到"));
}
String addressPath=servicePath+"/";
if(childPath.size()==1){
addressPath+=childPath.get(0);
}
if(childPath.size()>1){
addressPath+=childPath.get((int)(Math.random()*childPath.size()));
}
logger.debug("address node is "+addressPath);
byte[] data=zk.getData(addressPath, null,null);
return new String(data);
} catch (Exception e) {
logger.error("get data failure",e);
}
return "";
}
@Override
public void process(WatchedEvent event) {
if (event.getState() == Event.KeeperState.SyncConnected) {
latch.countDown();
}
}
}
有些不明白的地方,可以下载我的源码来参考。由于是初学,有些没有考虑到的地方,欢迎大家给我提意见。