1. 程式人生 > >SpringBoot ,zookeeper 服務的註冊與發現

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,訪問該頁面,最後展示效果如下:
這裡寫圖片描述

有些不明白的地方,可以下載我的原始碼來參考。由於是初學,有些沒有考慮到的地方,歡迎大家給我提意見。