Google Code Prettify

2018年6月9日 星期六

Spring Cloud Eureka: getting started

Netflix 有提供了一個稱為 Eureka 的 open source,可以作為服務註冊之用,spring cloud 將它做了封裝,這裡來看一下怎麼使用。

我總共會寫三支程式,分別為 ...
  1. myEureka: eureka server,註冊服務的地方。
  2. myEureka-Client: 提供服務的程式,會到 eureka server 註冊,供其它程式使用。
  3. myConsumer: 想要使用服務的程式,也會到 eureka server 註冊,註冊後即可使用 server 上的服務。
  • myEureka
    • build.gradle
buildscript {
  ext {
    springBootVersion = '2.0.2.RELEASE'
  }
  repositories {
    mavenCentral()
  }
  dependencies {
    classpath("org.springframework.boot:spring-boot-gradle-plugin:${springBootVersion}")
  }
}

apply plugin: 'java'
apply plugin: 'eclipse'
apply plugin: 'org.springframework.boot'
apply plugin: 'io.spring.dependency-management'

group = 'idv.steven.eureka'
version = '1.0'
sourceCompatibility = 1.8

repositories {
  mavenCentral()
  maven { url "https://repo.spring.io/milestone" }
}


ext {
  springCloudVersion = 'Finchley.RC2'
}

configurations.all {
  exclude group: 'org.springframework.boot', module: 'spring-boot-starter-logging'
}

dependencies {
  def log4j2Version = '2.7'
 
  compile('org.springframework.boot:spring-boot-starter')
  compile('org.springframework.cloud:spring-cloud-starter-netflix-eureka-server')
  {
    exclude group: 'org.springframework.cloud', module: 'spring-cloud-starter-netflix-eureka-client'
  }
  compile('org.springframework.boot:spring-boot-starter-tomcat')
  compileOnly('org.projectlombok:lombok')
  testCompile('org.springframework.boot:spring-boot-starter-test')
 
  // Log4j2
  compile group: 'org.apache.logging.log4j', name: 'log4j-core', version: "${log4j2Version}"
  compile group: 'org.apache.logging.log4j', name: 'log4j-api', version: "${log4j2Version}"
  compile group: 'org.apache.logging.log4j', name: 'log4j-slf4j-impl', version: "${log4j2Version}"
    
  compile group: 'javax.xml.bind', name: 'jaxb-api', version: '2.3.0'
}

dependencyManagement {
  imports {
    mavenBom "org.springframework.cloud:spring-cloud-dependencies:${springCloudVersion}"
  }
}
使用 gradle 下載所有需要的 jar 檔,特別注意紅色的部份,是有關 spring cloud 及 eureka。
    • MyEurekaApplication.java
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.boot.builder.SpringApplicationBuilder;
import org.springframework.cloud.netflix.eureka.server.EnableEurekaServer;

@EnableEurekaServer
@SpringBootApplication
public class MyEurekaApplication {

  public static void main(String[] args) {
    new SpringApplicationBuilder(MyEurekaApplication.class).web(true).run(args);
  }
}
只要加入 @EnableEurekaServer 即可啟動一個 Eureka Server,當然,還是要有一些設定,如下面的 application.properties、application-peer1.properties、application-peer2.properties,為什麼會有三個設定檔? 因為我們會啟用兩個 Eureka Server,並讓兩個 server 相互註冊,這樣可以相互備援及負載平衡。兩個 server 分別放在hostname 為 peer1 及 peer2 的伺服器上,因為我是在一台電腦上測試,所以我在 hosts 中加入如下內容:
127.0.0.1 peer1
127.0.0.1 peer2
    • application.properties
spring.applicaion.name=eureka-server
server.port=1111

eureka.instance.hostname=localhost
eureka.client.register-with-eureka=false
eureak.client.fetch-registry=false
eureka.client.serviceUrl.defaultZone=http://${eureka.instance.hostname}:${server.port}/eureka
注意紅色的部份,eureka.client.register-with-eureka=false 表示不向註冊中心註冊自己,eureak.client.fetch-registry=false 表示不需要檢索服務。
    • application-peer1.properties
eureka.instance.hostname=peer1
server.port=1111

eureka.client.serviceUrl.defaultZone=http://peer2:1112/eureka/
peer1 上的 Eureka Server 啟動時,服務會佔用 1111 port,並且會向 peer2 註冊。
    • application-peer2.properties
eureka.instance.hostname=peer2
server.port=1112

eureka.client.serviceUrl.defaultZone=http://peer1:1111/eureka/
peer2 上的 Eureka Server 啟動時,服務會佔用 1112 port,並且會向 peer1 註冊。因為我實際上只有一台電腦,port 才會設不一樣,如果是真的在兩台電腦,port 最好設成一樣。程式都寫好之後,用 gradle build 指令產生 jar 檔。
    • 啟動 server
java -jar myEureka-1.0.jar --spring.profiles.active=peer1
java -jar myEureka-1.0.jar --spring.profiles.active=peer2
開兩個命令列視窗,進入 jar 檔所在目錄,第一個視窗執行第一條指令,第二個視窗執行第二條指令,這樣就可以分別啟動 peer1 及 peer2 的 server 了,如果對於 application.properties 檔的設定不熟,可以參考「application.properties@spring boot」。
    • 進入管理介面觀察系統狀況
開啟瀏覽器,輸入 http://localhost:1111/ 進入 peer1 的管理介面,可以看到如下,peer2 已經註冊到 peer1 來了。
再開啟一個新的頁面,輸入 http://localhost:1112/ 進入 peer2 的管理介面,會看到如下,peer1 註冊到 peer2 了。
  • myEureka-Client
    • build.gradle
buildscript {
  ext {
    springBootVersion = '2.0.2.RELEASE'
  }
  repositories {
    mavenCentral()
  }
  dependencies {
    classpath("org.springframework.boot:spring-boot-gradle-plugin:${springBootVersion}")
  }
}

apply plugin: 'java'
apply plugin: 'eclipse'
apply plugin: 'org.springframework.boot'
apply plugin: 'io.spring.dependency-management'

group = 'idv.steven.eureka'
version = '1.0'
sourceCompatibility = 1.8

repositories {
  mavenCentral()
  maven { url "https://repo.spring.io/milestone" }
}


ext {
  springCloudVersion = 'Finchley.RC2'
}

configurations.all {
  exclude group: 'org.springframework.boot', module: 'spring-boot-starter-logging'
}

dependencies {
  def log4j2Version = '2.7'

  compile('org.springframework.boot:spring-boot-starter-web')
  compile('org.springframework.cloud:spring-cloud-starter-netflix-eureka-client')
  compileOnly('org.projectlombok:lombok')
  testCompile('org.springframework.boot:spring-boot-starter-test')
 
  // Log4j2
  compile group: 'org.apache.logging.log4j', name: 'log4j-core', version: "${log4j2Version}"
  compile group: 'org.apache.logging.log4j', name: 'log4j-api', version: "${log4j2Version}"
  compile group: 'org.apache.logging.log4j', name: 'log4j-slf4j-impl', version: "${log4j2Version}"
}

dependencyManagement {
  imports {
    mavenBom "org.springframework.cloud:spring-cloud-dependencies:${springCloudVersion}"
  }
}
特別注意紅色部份,這樣就可以載入 Eureka Client 相關的 jar 檔。
    • MyEurekaClientApplication.java
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.client.discovery.EnableDiscoveryClient;

@EnableDiscoveryClient
@SpringBootApplication
public class MyEurekaClientApplication {

  public static void main(String[] args) {
    SpringApplication.run(MyEurekaClientApplication.class, args);
  }
}
加上 @EnableDiscoveryClient,這個程式就成為 Eureka Client,會將自己註冊到 server。
    • application.properties
spring.application.name=hello-service

eureka.client.serviceUrl.defaultZone=http://peer1:1111/eureka/,http://peer2:1112/eureka/
有兩台 server,就將它們都寫到 eureka.client.serviceUrl.defaultZone 後面,這樣 client 會將服務註冊到 peer1、peer2。
    • HelloController.java
import java.util.List;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.cloud.client.ServiceInstance;
import org.springframework.cloud.client.discovery.DiscoveryClient;
import org.springframework.cloud.client.serviceregistry.Registration;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.bind.annotation.RestController;

import lombok.extern.slf4j.Slf4j;

@RestController
@Slf4j
public class HelloController {
  @Autowired
  private DiscoveryClient client;
 
  @Autowired
  private Registration registration;
 
  @RequestMapping(value ="/hello", method = RequestMethod.GET)
  public String index() {
    List<ServiceInstance> list = client.getInstances(registration.getServiceId());
    list.forEach(s -> {
      System.out.println("service id: " + s.getServiceId());
    });
  
    return "Hello";
  }
}
在這裡紅色部份是可以不用寫,這裡寫出來只是為了展示 client 端可以透過這個方式抓到所有的 service instance。
    • 進入管理介面觀察結果
現在再觀察一下 peer1、peer2 的管理介面,可以看到 client 已經註冊到兩台 server,因為在 application.properties 中,我將 client 提供的服務命名為 hello-service,所以在兩台 server 上都可以看到這個服務。

  • myConsumer
    • build.gradle
buildscript {
  ext {
    springBootVersion = '2.0.2.RELEASE'
  }
  repositories {
    mavenCentral()
  }
  dependencies {
    classpath("org.springframework.boot:spring-boot-gradle-plugin:${springBootVersion}")
  }
}

apply plugin: 'java'
apply plugin: 'eclipse'
apply plugin: 'org.springframework.boot'
apply plugin: 'io.spring.dependency-management'

group = 'idv.steven.consumer'
version = '1.0'
sourceCompatibility = 1.8

repositories {
  mavenCentral()
  maven { url "https://repo.spring.io/milestone" }
}


ext {
  springCloudVersion = 'Finchley.RC2'
}

dependencies {
  compile('org.springframework.boot:spring-boot-starter-web')
  compile('org.springframework.cloud:spring-cloud-starter-netflix-eureka-client')
  compileOnly('org.projectlombok:lombok')
  testCompile('org.springframework.boot:spring-boot-starter-test')
}

dependencyManagement {
  imports {
    mavenBom "org.springframework.cloud:spring-cloud-dependencies:${springCloudVersion}"
  }
}
載入 Eureka Client 所需的 jar 檔。
    • application.properties
spring.application.name=myconsumer
server.port=9090
eureka.client.serviceUrl.defaultZone=http://peer1:1111/eureka/,http://peer2:1112/eureka/
我只在一台電腦上執行四支程式,這裡的 port 就避開上面三支程式的即可,我設為 9090,另外,這也是一個 client 程式,所以也要將自己註冊到 server。
    • MyConsumerApplication.java
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.client.discovery.EnableDiscoveryClient;
import org.springframework.cloud.client.loadbalancer.LoadBalanced;
import org.springframework.context.annotation.Bean;
import org.springframework.web.client.RestTemplate;

@EnableDiscoveryClient
@SpringBootApplication
public class MyConsumerApplication {
  @Bean
  @LoadBalanced
  public RestTemplate restTemplate() {
    return new RestTemplate();
  }

  public static void main(String[] args) {
    SpringApplication.run(MyConsumerApplication.class, args);
  }
}
與前面的 client 服務程式一樣,加上 @EnableDiscoverClient,因為這也是一支 Eureka Client 程式,只是不提供服務,而是要到 server 上去找到別的服務並執行該服務。RestTemplate 設定為 @Bean 讓 spring 控管,是為了下面的程式要使用,加上 @LoadBalanced 的話,會開啟 client 的負載平衡。
    • ConsumerController.java
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.client.RestTemplate;

@RestController
public class ConsumerController {
  @Autowired
  private RestTemplate restTemplate;
 
  @RequestMapping(value = "/myconsumer", method = RequestMethod.GET)
  public String helloConsumer() {
    return restTemplate.getForEntity("http://HELLO-SERVICE/hello", String.class).getBody();
  }
}
這支程式只是發動一個 REST request 到上面那支 client 服務去使用該服務,要注意的是,網址寫的是該服務註冊於 server 上的名稱,也就是說,使用服務不再需要知道服務所在的伺服器在那,只要將自己註冊到 Eureka Server,即可以服務的名稱存取到該服務。
    • 測試
開啟一個瀏覽器,輸入 http://localhost:9090/myconsumer 去觸發這支要使用服務的程式,讓這支程式發動一個 REST reuqest 去 HELLO-SERVICE 存取該服務,如下所示,服務傳回 Hello。

2018年6月3日 星期日

application.properties@spring boot

spring boot 預設會讀取 classpath 下的 application.properties,但是,大部份的維運團隊通常會同時面對一個系統要部署到開發、測試、正式等多個環境的問題,為解決這個問題,spring boot 有了以下的預設。
  • application.properties
  • spring.profiles.active=test
    
    util.count=0
    util.total=4
    
    紅色部份可以用來設定環境,在這裡,我假定會有 dev、test、prod 三個環境,application.properties 放的是所有環境都會用到的設定,dev、test、prod 這三個各別的環境如果有什麼特殊的設定,則可以在 application-dev.properties、application-test.properties、application-prod.properties 這三個設定檔分別設定。萬一,這三個設定檔裡的設定是 application.properties 已經有的,則會覆蓋預設的設定。假設,另三個設定檔的內容如下。
  • application-dev.properties
  • util.count=1
  • application-test.properties
  • util.count=2
    
  • application-prod.properties
  • util.count=3
在上述的設定下,因為 spring.profiles.active 設定為 test,spring boot 載入的設定會如下:
util.count=2
util.total=4
寫個程式測試看看 …
  • ForTestUtil (單純的載入兩個設定參數並提供印出的 method)
  • import org.springframework.beans.factory.annotation.Value;
    import org.springframework.stereotype.Component;
    
    @Component
    public class ForTestUtil {
        @Value("${util.count}")
        private Long count;
     
        @Value("${util.total}")
        private Long total;
     
        public void print() {
            System.out.println("count = " + count);
            System.out.println("total = " + total);
        }
    }
  • ForTestUtilTest (上述類別的單元測試程式)
  • import org.junit.Test;
    import org.junit.runner.RunWith;
    import org.springframework.beans.factory.annotation.Autowired;
    import org.springframework.boot.test.context.SpringBootTest;
    import org.springframework.test.context.junit4.SpringRunner;
    
    import idv.steven.demo2.Demo2Application;
    
    @RunWith(SpringRunner.class)
    @SpringBootTest(classes = Demo2Application.class)
    public class ForTestUtilTest {
        @Autowired
        private ForTestUtil util;
    
        @Test
        public void test() {
            util.print();
        }
    }
執行結果,會如下:
count = 2
total = 4