SpringBoot ,zookeeper 服務的註冊與發現
本文主要講zookeeper如何註冊服務並使用這些服務。簡單流程是使用springboot開發簡單的restapi.並把api註冊到zookeeper。最後在客戶端連線到zookeeper呼叫api並把結果返回到客戶端。
- springboot開發restapi
- 註冊服務到zookeeper
- 發現服務
springboot開發restapi
搭建springboot開發環境
新建一個maven工程,並引入spring boot的jar包。pom配置是:
<parent>
<groupId>org.springframework.boot</groupId >
<artifactId>spring-boot-starter-parent</artifactId>
<version>1.3.3.RELEASE</version>
</parent>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</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伺服器搭建
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.msa</groupId>
<artifactId>msa-framework</artifactId>
<version>0.0.1-SNAPSHOT</version>
</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<RequestMappingInfo,HandlerMethod> 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.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<dependency>
<groupId>org.apache.zookeeper</groupId>
<artifactId>zookeeper</artifactId>
<version>3.4.6</version>
</dependency>
頁面實現:
<!DOCTYPE html>
<html>
<head>
<title>TestZk.html</title>
<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">
<!--<link rel="stylesheet" type="text/css" href="./styles.css">-->
</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>hello</servlet-name>
<servlet-class>demo.msa.servlet.MsaDemoServlet</servlet-class>
<load-on-startup>0</load-on-startup>
</servlet>
<servlet-mapping>
<servlet-name>hello</servlet-name>
<url-pattern>/hello</url-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<String> 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();
}
}
}
部署到tomcat,訪問該頁面,最後展示效果如下:
有些不明白的地方,可以下載我的原始碼來參考。由於是初學,有些沒有考慮到的地方,歡迎大家給我提意見。