2019年7月27日 星期六

設定中心 ( SpringCloud 2.x 七)

因為每一個微服務都有一個 application.yml,這裡是做一個統合的管理

※Server 端設定


※新增一個全新的專案,放在 github 上,裡面就放一個 application.yml,內容如下:
spring:
  profiles:
    active: dev
---
spring:
  profiles: dev
  application:
    name: xxx-dev
---
spring:
  profiles: uat
  application:
    name: xxx-uat
---
spring:
  profiles: test
  application:
    name: xxx-test


※ 連 --- 也不能省略

※spring.profiles.active 還可以配合 @Profile 使用



※新增一個 module,增加 pom

<dependency>
    <groupId>org.springframework.cloud</groupId>
    <artifactId>spring-cloud-config-server</artifactId>
</dependency>




※application.yml
server:
  port: 1111
spring:
  application:
    name: config-center
  cloud:
    config:
      server:
        git:
          uri: https://github.com/bruce12452002/SpringCloudConfigCenterPractice.git # git網址


※和 uri 同層的還有 username 和 password,是 github 的帳號和密碼,但本來 git clone 就不用打帳號密碼,所以沒有要 push 的話,可以不用寫

※uri 一打,啟動後的控製台就沒有預設的 8888 port 了

※main 方法增加 @EnableConfigServer,然後啟動測試看看

※hosts 增加 127.0.0.1       config.ooo1111 來模擬

※打上如下的網址,即可看到結果:
http://config.ooo1111:1111/application-dev.yml
http://config.ooo1111:1111/master/application-dev.yml

spring:
  application:
    name: xxx-dev
  profiles:
    active: dev



http://config.ooo1111:1111/application-uat.yml
spring:
  application:
    name: xxx-uat
  profiles:
    active: dev



http://config.ooo1111:1111/application-xxx.yml
spring:
  profiles:
    active: dev

※格式不是亂打的,看官網,label 是分支的意思



※Client 端設定


※在github 增加 abcxxx.yml,內容如下:
spring:
  profiles:
    active: dev
---
server:
  port: 8001
spring:
  profiles: dev
  application:
    name: my-config-dev
eureka:
  client:
    service-url:
      defaultZone: http://xxx.ooo9051:9051/eureka
---
server:
  port: 8002
spring:
  profiles: uat
  application:
    name: my-config-uat
eureka:
  client:
    service-url:
      defaultZone: http://xxx.ooo9051:9051/eureka
---
server:
  port: 8003
spring:
  profiles: test
  application:
    name: my-config-test
eureka:
  client:
    service-url:
      defaultZone: http://xxx.ooo9051:9051/eureka




※新建一個 model,加入 pom
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-web</artifactId>
</dependency>


※父 pom 已經有 spring-cloud-starter-config,所以不用加


※寫一個測試的方法
@RestController
public class ClientConfigInfo {
    @Value("${spring.application.name}")
    private String appName;
    
    @Value("${eureka.client.service-url.defaultZone}")
    private String eurekaServerName;
    
    @Value("${server.port}")
    private Integer port;
    
    @GetMapping("/configInfo")
    public String getConfigInfo() {
        StringBuilder sb = new StringBuilder();
        sb.append("appName=").append(appName)
        .append(", eurekaServerName=").append(eurekaServerName)
        .append(", port=").append(port);
        return sb.toString();
    }
}




※以下只能寫在 bootstrap.yml
spring:
  cloud:
    config:
      name: abcxxx # 讀 github 的 yml,但不能寫副檔名
      profile: dev
      label: master
      uri: http://config.ooo1111:1111  # 找 config 伺服器





※測試

hosts 增加 127.0.0.1       config-client.ooo 模擬,不能用「_」,會報錯

啟動時控製台出現以下訊息:


 ※表示連到了 8003,然後在網址打 http://config-client.ooo:8003/configInfo,即可看到結果

※修改 bootstrap.yml 裡的 spring.cloud.config.profile,只要 github 有的,重新啟動後即可抓到新的設定

Zuul ( SpringCloud 2.x 六)

增加一個 model

<dependency>
    <groupId>org.springframework.cloud</groupId>
    <artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
</dependency>
<dependency>
    <groupId>org.springframework.cloud</groupId>
    <artifactId>spring-cloud-starter-netflix-zuul</artifactId>
</dependency>



server:
  port: 9099
    
spring:
  application:
    name: my-zuul
    
eureka:
  client:
    service-url:
      defaultZone: http://xxx.ooo9051:9051/eureka,http://xxx.ooo9052:9052/eureka,http://xxx.ooo9053:9053/eureka
  instance:
    instance-id: bruce-zuul
    prefer-ip-address: true
    nonSecurePort: ${server.port}
    
info:
  xxx.ooo: xxoo.aa
  name: zuul.info
  jdk_version: @java.version@
  version: @version@
  chi_test: 梅山小路用9099
  ppp: @aaa.bbb@
    
zuul:
  routes:
    mycloud: # 隨便寫
      serviceId: PROVIDER1
      path: /xxx/**
  # prefix: /abc
  # ignored-services: PROVIDER1 # 大寫微服務名稱


 ※ 有些版本訪問 http://zuul.ooo9099:9099/PROVIDER1/testGet 是可以成功的,但這樣就暴露了微服務名稱了,所以可以設定 ignored-services,如果全部都不給訪問,可以用「"*"」

※serviceId 會被 path 取而代之



※main 方法加入三個 annotation 即可

@SpringBootApplication
@EnableZuulProxy
@EnableEurekaClient

※模擬用戶發 request 到 zuul,所以在 hosts 增加 127.0.0.1 zuul.ooo9099



※測試

開啟 eureka -> provider (9001 port) -> zuul

然後在網址打上 http://zuul.ooo9099:9099/xxx/testGet 即可訪問

又如果有加 prefix,那就要換成如下的網址
http://zuul.ooo9099:9099/abc/xxx/testGet


※遇到的問題

application.yml 一定要配 zuul,可能其他版本可以,但我試的結果就是不行

2019年7月21日 星期日

Hystrix ( SpringCloud 2.x 五)

 Hystrix 就像保險絲一樣,可以處理異常


※server 端的異常


※1.複製 provider 後,增加 pom

<dependency>
    <groupId>org.springframework.cloud</groupId>
    <artifactId>spring-cloud-starter-netflix-hystrix</artifactId>
</dependency>


※這樣才有 @EnableCircuitBreaker、@HystrixCommand 可用

※順便改個 yml,instance.instance-id: bruceProvider-hystrix



※2.增加 provider-hystrix 的 controller

@RestController
public class TestController {
    @GetMapping("/testGet")
    public ApiBean get() {
        ApiBean ab = new ApiBean();
        ab.setId(1);
        ab.setName("xxx9001");
        return ab;
    }
    
    @HystrixCommand(fallbackMethod = "xxx")
    @GetMapping("/testHystrix/{id}")
    public ApiBean getXxxById(@PathVariable("id") Integer id) { // throws ParseException {
        if (id == 1) {
            ApiBean ab = new ApiBean();
            ab.setId(1);
            ab.setName("xxx9001");
            return ab;
        } else {
            throw new RuntimeException("耶!掛了");
            // throw new ParseException("xxx", -1);
        }
    }
    
    private ApiBean xxx(@PathVariable("id") Integer id) {
        ApiBean ab = new ApiBean();
        ab.setId(-1);
        ab.setName("沒有" + id);
        return ab;
    }
}


※只要拋異常都會被 xxx 方法所接收並回傳

※main 方法要加 @EnableCircuitBreake,但加 @EnableHystrix也可以,因為這個註解裡面也用 @EnableCircuitBreake,所以二選一即可

※如果很多方法都要用同一個,可以用全域的方式,如下:
一. 在方法上加 @HystrixCommand,但不寫 fallbackMethod
二. 在 class 上加 @DefaultProperties(defaultFallback="方法名") 即可
本來的 @HystrixCommand(fallbackMethod="方法名") 並不會被影響



※3.consumerFegin 也增加呼叫對應的方法


@RestController
public class ConsumerController {
    @Resource
    private MyService myService;
    
    @GetMapping("/xxx")
    public ApiBean get() {
        return myService.get();
    }
    
    @GetMapping("/ooo/{id}")
    public ApiBean getHystrix(@PathVariable("id") Integer id) {
        return myService.get(id);
    }
}





※4.api 也增加對應的方法

@FeignClient("PROVIDER1")
public interface MyService {
    @GetMapping("/testGet")
    ApiBean get();
    
    @GetMapping("/testHystrix/{id}")
    ApiBean get(@PathVariable("id") Integer id);
}


※使用 http://localhost/ooo/1 是正常的,但只要不是 1,都會出現 hystrix 提供的功能



※client 端的異常


由於 server 端的做法太過於高耦合了,在 client 端可以針對介面做處理,就算 server 端掛了,還是可以儘量的顯示友好的訊息

※1.因為要解耦,所以不要 @HystrixCommand,連 xxx 方法也不需要了,然後在 api 專案的 @FeignClient 有個 fallbackFactory,指定一個類別,然後做異常處理

@FeignClient(name = "PROVIDER1", fallbackFactory = MyFallbackFactory.class)
public interface MyService {
    @GetMapping("/testGet")
    ApiBean get();
    
    @GetMapping("/testHystrix/{id}")
    ApiBean get(@PathVariable("id") Integer id);
}





※2.異常處理類
@Component
public class MyFallbackFactory implements FallbackFactory<MyService> {
    @Override
    public MyService create(Throwable throwable) {
        return new MyService() {
            @Override
            public ApiBean get() {
                return null;
            }
    
            @Override
            public ApiBean get(Integer id) {
                ApiBean ab = new ApiBean();
                ab.setId(-1);
                ab.setName("沒有" + id);
                return ab;
            }
        };
    }
}


※記得要有 @Component 之類的註解,否則出錯時會出現 feign.FeignException: status 500 reading MyService#get(Integer) 的錯誤訊息


※3.cousumerfeign 的 yml 要加上 hystrix.enabled: true


※4.cousumerfeign 啟動類別
@SpringBootApplication
@ComponentScan({"controller", "service"})
@EnableEurekaClient
@EnableFeignClients("service")
public class ConsumerFeignMain {
    public static void main(String[] args) {
        SpringApplication.run(ConsumerFeignMain.class, args);
    }
}


※注意 @ComponentScan 要加上 api MyFallbackFactory 的包名,只要這個沒加或 yml 沒設定為 true,在啟動時都會出現 No fallbackFactory instance of type class service.MyFallbackFactory found for feign client PROVIDER1

※測試時和 server 端一樣,但是多一個,將 provider 關閉時,還是能出現 client 端設定的訊息,不管是不是 1 都是這個結果,所以訊息可以改得好一點的



※Hystrix 儀錶版


就是監控 hystrix 的

<dependency>
    <groupId>org.springframework.cloud</groupId>
    <artifactId>spring-cloud-starter-netflix-hystrix-dashboard</artifactId>
</dependency>


※新增一個 module,然後增加 pom

※yml 很簡單,就是 port 而已,server.port: 9010



@SpringBootApplication
@EnableHystrixDashboard
public class App {
    public static void main(String[] args) {
        SpringApplication.run(App.class, args);
    }
}


※啟動類別增加 @EnableHystrixDashboard

※訪問 http://localhost:9010/hystrix 就可看到帶刺的豬

測試:開啟 eureka -> provider_hystrix -> consumerFeign -> hystrix_dashboard

被監視的 provider_hystrix 要加 management.endpoints.web.exposure.include=*,否則點下 Monitor Stream 的按鈕時,中間會有 Unable to connect to Command Metric Stream. 的紅字,成功是 Loading ...

※以下沒試出來
1.上圖反白的網址是要監控的網址,所以是 provider_hystrix的,改成 http://localhost:9001/actuator/hystrix.stream,然後開啟新網頁貼上會看到網頁一直再跑,我試的結果居是出現問我要不要下載

2.但第1項是文字介面的,所以在上圖可打網址的地方貼上1的網址就可看到圖形介面

3.用 consumerFeign 訪問 provider_hystrix 即可看到 dashboard 的球變大

2019年7月20日 星期六

Feign ( SpringCloud 2.x 四)

和 Ribbon 都是客户端的負載均衡,Ribbon 用 RestTemplate 封裝 HTTP 的請求;Feign 在這個基礎上,加上大家都熟悉的接口式編程

基本的 consumer 已經很複雜了,所以複製 consumer 成為一個新 module,刪除ribbon相關的內容(兩個 class 和 @RibbonClien,連 ConfigRestTemplate也不要了)


1.api 和 consumerFeign 的 pom
<dependency>
    <groupId>org.springframework.cloud</groupId>
    <artifactId>spring-cloud-starter-openfeign</artifactId>
</dependency>


※這樣才有 @FeignClient 和 @EnableFeignClients 可用


2.
@FeignClient("PROVIDER1")
public interface MyService {
    @GetMapping("/testGet")
    ApiBean get();
}


※@FeignClient 指定想訪問的 spring.application.name


3.
@SpringBootApplication
@ComponentScan("controller")
@EnableEurekaClient
@EnableFeignClients("service")
public class ConsumerFeignMain {
    public static void main(String[] args) {
        SpringApplication.run(ConsumerFeignMain.class, args);
    }
}


※@EnableFeignClients 指定 api 裡的包路徑,只要錯了,啟動時就會出現 A component required a bean of type 'service.MyService' that could not be found.


4.
@RestController
public class ConsumerController {
//    private static final String PROVIDER_URI = "http://localhost:9001";
/*    private static final String PROVIDER_URI = "http://"+ "provider1".toUpperCase(); // 網址列不分大小寫,但還是以 eureka 為準的好
    @Resource
    private RestTemplate restTemplate;
    
    @GetMapping("/xxx")
    public ApiBean get() {
        return restTemplate.getForObject(PROVIDER_URI + "/testGet", ApiBean.class);
    }
*/
    
    @Resource
    private MyService myService;
    
    @GetMapping("/xxx")
    public ApiBean get() {
        return myService.get();
    }
}


※使用 feign 後,可以不用寫服務名

※預設是輪詢算法,可以將上一篇的 MyCustomRibbonRule 複製過來,改變自己想要的算法或內鍵的其他算法

2019年7月14日 星期日

Ribbon ( SpringCloud 2.x 三)

Ribbon 為客戶端的負載均衡,所以會改 consumer

@Configuration
public class ConfigRestTemplate {
    @Bean
    // @LoadBalanced
    public RestTemplate getRestTemplate() {
        return new RestTemplate();
    }
}


※新增一個類別,因為要使用到 @LoadBananced 這個註解,只好放棄使用 @Import,但在使用前先註解,確保之前的程式是可以跑的


// @Import(RestTemplate.class)
public class consumerController {
    // private static final String PROVIDER_URI = "http://localhost:9001";
    private static final String PROVIDER_URI = "http://"+ "provider1".toUpperCase();
    // ...
}


※確保之前的程式能跑後,使用 ribbon 要三步
1.打開 @LoadBananced
2.訪問路徑改成 provider 的 spring.application.name 名稱,大小寫都可以,但 eureka 是大寫,最好都用一樣的

※以上缺一都會報錯

※不需要 ribbon 的 jar 包,eureka-client 已經有依賴了,這個 eureka-client 在第一篇已經加過了

※遇到的問題:

如果出現 Request URI does not contain a valid hostname 的錯,表示 spring.application.name 的名字找不到,不要忘了要加 http://
另外一個是 name 名稱不能有「_」,都會報這樣的錯

這個專案在隔天 run 時,居然出現找不到 PROVIDER1 的錯,結果 mvn clean install 就解決了



※測試 ribbon 預設的模式

新增兩個專案:
1.複製 pom.xml
2.複製 啟動類別和 controller,為了測試區別,/xxx 的內容三支都修改 ab.setName("xxx9002"); // 9001-9003
3.instance.instance-id 修改不同的名稱,但 spring.application.name 一樣
4.http://localhost/xxx 每重整一次會發現是有順序性的,如第一次循環是 132,就會一直按 132 的方式循環

畫面如下:
可看見 PROVIDER1 有三個實例



※改變預設模式


@Configuration
public class ConfigRestTemplate {
    @Bean
    @LoadBalanced
    public RestTemplate getRestTemplate() {
        return new RestTemplate();
    }
    
    @Bean
    public IRule myRibbonRule() {
    // return new RoundRobinRule();
    // return new RandomRule();
        return new RetryRule();
    }
}

※在自訂的 ConfigRestTemplate 增加 IRule 的回傳 Bean,RoundRobinRule 是預設的、
RandomRule 是隨機的、RetryRule 和預設的很像,差在如果其中有 provider 掛了就不一樣了,假設是 132 一直循環,然後 2 掛了,那就會是 13掛、13掛、經過幾次之後,就只會有13而已,可以將三個 provider 啟好後,關閉其中一個測試

※也可以寫個 class,然後回傳 IRule



※自定義規則



@Configuration
public class MyCustomRibbonRule {
    @Bean
    public IRule myRibbonRule() {
        // return new RandomRule();
        return new CalcRibbonRule();
    }
}


※改變預設模式的 @Bean 要註解或改名,不然會有 2 個 IRule


public class CalcRibbonRule extends AbstractLoadBalancerRule {
    private int currentIndex = 1; //  PROVIDER1 的機器號碼
    
    private Server choose(ILoadBalancer lb, Object key) {
        if (lb == null) {
            return null;
        }
        Server server = null;
    
        while (server == null) {
            if (Thread.interrupted()) {
                return null;
            }
            List<Server> upList = lb.getReachableServers(); // 到的有幾台機器
            List<Server> allList = lb.getAllServers(); // 全部有幾台機器
    
            int serverCount = allList.size();
            if (serverCount == 0) {
                return null;
            }
    
            // 主要邏輯在這
            if (upList.size() % 2 == 1) {
                server = upList.get(currentIndex);
                currentIndex++;
                System.out.println("size==>" + upList.size());
                System.out.println("currentIndex==>" + currentIndex);
                if (currentIndex >= serverCount) {
                    currentIndex = 1;
                }
            }
    
            if (server == null) {
                Thread.yield();
                continue;
            }
    
            if (server.isAlive()) {
                return (server);
            }
            server = null;
            Thread.yield();
        }
        return server;
    }
    
    @Override
    public Server choose(Object key) {
        return choose(getLoadBalancer(), key);
    }
    
    @Override
    public void initWithNiwsConfig(IClientConfig clientConfig) {}
}


※我想不到什麼好規則,所以就是奇數的機器才去訪問,但並不是 9051 和 9053 這兩台機器的意思,機器的順序不是我們決定的,假設是 132,那就是 1 和 2

※本來是將 CalcRibbonRule 寫在 MyCustomRibbonRule 裡面,成為內部類別,但訪問的時候報錯了,NoSuchMethodException: controller.MyCustomRibbonRule$CalcRibbonRule.

※最後 consumer 的 main 方法要加上 @RibbonClient(value = "PROVIDER1", configuration = MyCustomRibbonRule.class) 即可

2019年7月13日 星期六

Eureka 集群 ( SpringCloud 2.x 二)

要做集群,在本機要訪問網址不方便,所以在 hosts 新增如下的網址
Windows 的 C:\Windows\system32\drivers\etc 或 unix 的 /etc
127.0.0.1       xxx.ooo9051
127.0.0.1       xxx.ooo9052
127.0.0.1       xxx.ooo9053


※新增兩個 model,並複製 pom 裡的 spring-cloud-starter-netflix-eureka-server,然後修改三個 application.yml,以9052為例

server:
  port: 9052
    
eureka:
  instance:
    hostname: xxx.ooo9052
  client:
    register-with-eureka: false     #不註冊自己
    fetch-registry: false     #取得註冊資訊,因為自己就是註冊中心,不需要取得註冊資訊
    service-url:
      defaultZone: http://xxx.ooo9051:9051/eureka,http://xxx.ooo9053:9053/eureka


※9051 內容寫 9052 和 9053;9052 內容寫 9051 和 9053

※自己的寫在 eureka.instance.hostname


eureka:
  client:
    service-url:
      #defaultZone: http://localhost:9051/eureka
      defaultZone: http://xxx.ooo9051:9051/eureka,http://xxx.ooo9052:9052/eureka,http://xxx.ooo9053:9053/eureka


※provider 和 consumer 的 defaultZone 也得改

※結果畫面如下,以 xxx.ooo9052:9052 為例
※可以看見 DS Replicas 還有兩個 eureka,內容有 consumer1 和 provider1

2019年7月12日 星期五

向註冊中心註冊與發現服務 ( SpringCloud 2.x 一)

※POM

※父POM

<packaging>pom</packaging>
<modules>
    <module>provider</module>
    <module>consumer</module>
    <module>api</module>
    <module>eureka</module>
</modules>
    
<groupId>scp</groupId>
<artifactId>SpringCloudPractice</artifactId>
<version>1.0-SNAPSHOT</version>
    
<parent>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-parent</artifactId>
    <version>2.1.5.RELEASE</version>
    <relativePath/> <!-- lookup parent from repository -->
</parent>
    
<properties>
    <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
    <maven.compiler.source>1.8</maven.compiler.source>
    <maven.compiler.target>1.8</maven.compiler.target>
    <spring-cloud-release.version>Greenwich.RELEASE</spring-cloud-release.version>
</properties>
    
<dependencyManagement>
    <dependencies>
        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-dependencies</artifactId>
            <version>${spring-cloud-release.version}</version>
            <type>pom</type>
            <scope>import</scope>
        </dependency>
    
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-dependencies</artifactId>
            <version>2.1.5.RELEASE</version>
            <type>pom</type>
            <scope>import</scope>
        </dependency>
    
        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
            <version>1.18.8</version>
            <scope>provided</scope>
        </dependency>
    </dependencies>
</dependencyManagement>
    
<dependencies>
    <dependency>
        <groupId>org.springframework.cloud</groupId>
        <artifactId>spring-cloud-starter-config</artifactId>
    </dependency>
    
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-test</artifactId>
        <scope>test</scope>
    </dependency>
</dependencies>
    
<build>
    <plugins>
        <plugin>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-maven-plugin</artifactId>
        </plugin>
    </plugins>
</build>


※生產者和消費者向註冊中心註冊,api 為生產者和消費者共用的方法


※consumer pom

<parent>
    <artifactId>SpringCloudPractice</artifactId>
    <groupId>scp</groupId>
    <version>1.0-SNAPSHOT</version>
</parent>
    
<artifactId>consumer</artifactId>
    
<properties>
    <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
    <maven.compiler.source>1.8</maven.compiler.source>
    <maven.compiler.target>1.8</maven.compiler.target>
</properties>
    
<dependencies>
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-web</artifactId>
    </dependency>
    
    <dependency>
        <artifactId>api</artifactId>
        <groupId>scp</groupId>
        <version>1.0-SNAPSHOT</version>
    </dependency>
    
    <dependency>
        <groupId>org.springframework.cloud</groupId>
        <artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
    </dependency>
</dependencies>



※provider pom

<parent>
    <artifactId>SpringCloudPractice</artifactId>
    <groupId>scp</groupId>
    <version>1.0-SNAPSHOT</version>
</parent>
<modelVersion>4.0.0</modelVersion>
    
<artifactId>provider</artifactId>
    
<properties>
    <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
    <maven.compiler.source>1.8</maven.compiler.source>
    <maven.compiler.target>1.8</maven.compiler.target>
</properties>
    
<dependencies>
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-web</artifactId>
    </dependency>
    
    <dependency>
        <artifactId>api</artifactId>
        <groupId>scp</groupId>
        <version>1.0-SNAPSHOT</version>
    </dependency>
    
    <dependency>
        <groupId>org.springframework.cloud</groupId>
        <artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
    </dependency>
</dependencies>



※api pom

<parent>
    <artifactId>SpringCloudPractice</artifactId>
    <groupId>scp</groupId>
    <version>1.0-SNAPSHOT</version>
</parent>
    
<artifactId>api</artifactId>
    
<properties>
    <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
    <maven.compiler.source>1.8</maven.compiler.source>
    <maven.compiler.target>1.8</maven.compiler.target>
</properties>
    
<dependencies>
    <dependency>
        <groupId>org.projectlombok</groupId>
        <artifactId>lombok</artifactId>
    </dependency>
</dependencies>
    
<build>
    <finalName>cyc</finalName>
    <plugins>
        <plugin>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-maven-plugin</artifactId>
            <configuration>
                <classifier>exec</classifier>
            </configuration>
        </plugin>
    </plugins>
</build>

※因為 api 有程式碼給 consumer 和 provider 使用,maven install 會失敗,所以要在 api 加上 plugin 才可以

※eureka pom

<parent>
    <artifactId>SpringCloudPractice</artifactId>
    <groupId>scp</groupId>
    <version>1.0-SNAPSHOT</version>
</parent>
    
<artifactId>eureka</artifactId>
    
<properties>
    <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
    <maven.compiler.source>1.8</maven.compiler.source>
    <maven.compiler.target>1.8</maven.compiler.target>
</properties>
    
<dependencies>
    <dependency>
        <groupId>org.springframework.cloud</groupId>
        <artifactId>spring-cloud-starter-netflix-eureka-server</artifactId>
        <!--<artifactId>spring-cloud-netflix-eureka-server</artifactId>没有 starter 是错的-->
    </dependency>
</dependencies>




※java 類別


※provider

package controller;
    
@RestController
public class TestController {
    @GetMapping("/testGet")
    public ApiBean get() {
        System.out.println("xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx");
        ApiBean ab = new ApiBean();
        ab.setId(1);
        ab.setName("xxx");
        return ab;
    }
}



※consumer

package controller;
    
@RestController
@Import(RestTemplate.class)
public class consumerController {
    @Resource
    private RestTemplate restTemplate;
    
    @GetMapping("/xxx")
    public ApiBean get() {
        return restTemplate.getForObject("http://localhost:9001" + "/testGet", ApiBean.class);
    }
}


※因為 RestTemplate 是內鍵的 class,並沒有類似 @Component 這種註解,spring 不知道,所以用了 @Import 將它註冊到 spring

※api

package scp;
    
@Data
public class ApiBean implements Serializable {
    private int id;
    private String name;
}




※eureka

package main;
    
@SpringBootApplication
@EnableEurekaServer
public class EurekaMain {
    public static void main(String[] args) {
        SpringApplication.run(EurekaMain.class, args);
    }
}




※application.yml


※provider

server:
  port: 9001
eureka:
  client:
    service-url:
      defaultZone: http://localhost:9051/eureka



※consumer

server:
  port: 80
eureka:
  client:
    service-url:
      defaultZone: http://localhost:9051/eureka



※eureka

server:
  port: 9051
eureka:
  client:
    register-with-eureka: false     # 不註冊自己
    fetch-registry: false     # 取得註冊資訊,因為自己就是註冊中心,不需要取得註冊資訊
    service-url:
      defaultZone: http://localhost:${server.port}/eureka/   # Eureka Server 的位址


※ fetch-registry 一定要 false 才有辦法啟動,否則會報 Cannot execute request on any known server

※ register-with-eureka 如果註冊自己可以順利啟動成功,只是沒什麼意義,感覺就像麥當勞的員工自己也在排隊買漢堡一樣

※server.port 如果給 0,會隨機產生一個,看控製台即可知道


※遇到的問題

invalid LOC header
jar 檔簽名壞了,從倉庫刪除後重新下載即可

Make sure your own configuration does not rely on that class. This can also happen if you are @ComponentScanning a springframework package
main 方法要有 package,還有另外一個錯誤是要有 main 方法,但錯誤訊息很明顯,所以一個專案放一個有 package 的 main 方法就可以了,方法裡什麼都不寫也行



※Eureka 註冊中心

 啟動後會有現都是 UNKNOWN,所以 consumer 和 provider 還要在 application.yml 加上 spring.application.name,這樣 eureka 才會分辯的出來,修改後如下,eureka 會變成全部大寫英文


 如果想改右邊的 Status 顯示的名稱,還得在 consumer 和 provider 加上 eureka.instance.instance-id,修改後如下,這裡 eureka 不會變成大寫


上上一張圖的左下角有網址,是滑鼠在 status 的 consumer 或 provider 上時顯示的,我記得不改是 localhost 之類的,但不知哪一版改了,如果是 localhost 想改成 ip,還可以加上 eureka.instance.prefer-ip-address: true 就可以了
還有一個很像的設定 eureka.instance.ip-address: ,因為有可能有多張網卡,可以寫死其中一張網卡的 IP,如果兩個都設定,最終註冊在 eureka 會是這裡的設定



※啟動順序 

必需先啟動 eureka,再啟動 provider,最後是 consumer
如果 eureka 在啟動前有其他的專案先啟動了,eureka 抓不到
如果 consumer 在 provider 之前,我試的結果沒問題,但理論上是先有提供者,這樣消費者才抓的到
如果已經照順序啟動好了,將 provider 或 consumer 的 spring.application.name 名字換掉,只重啟改掉的專案,會發現 eureka 會有紅色的警告,因為有個 CAP 理論

Consistency(一致性)
Availability(可用性)
Partition tolerance(分区容错性)

目前不可能全部做到,會犧牲一個


eureka 保證 AP
zookeeper 保證 CP

長時間沒有人連 eureka 會報這個警告,或者我將名稱換掉也會,因為他要保證可用性,有可能只是網路不順,寧可記錄錯的資料也不要刪掉,而預設過了 90 秒後,真的都沒有人連了,才會刪除,如果不喜歡這個功能,可以關掉
eureka.server.enable-self-preservation: false


※actuator/info

上面的 Status 一點進去就掛了,這裡是設定裡面的資訊

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-actuator</artifactId>
</dependency>
-----------------------------
<build>
    <finalName>SpringCloudPractice</finalName>
    <resources>
        <resource>
            <directory>src/main/resources</directory>
            <filtering>true</filtering>
        </resource>
    </resources>
    
    <plugins>
        <plugin>
            <groupId>org.apache.maven.plugins</groupId>
            <artifactId>maven-resources-plugin</artifactId>
            <configuration>
                <delimiters>
                    <delimit>@</delimit>
                </delimiters>
            </configuration>
        </plugin>
    </plugins>
</build>


※這裡的 pom 是全域的

※delimit 裡面的符號是用在註冊進 eureka 的專案要用到的,前後用這個符號包起來即可

※以下以 provider 為例,在 application.yml 增加如下的設定

info:
  xxx.ooo: xxoo.aa
  name: provider.info
  jdk_version: @java.version@
  version: @version@
  chi_test: 梅山小路用


※info 下的 key 和 value 都可以隨便亂打,但 @ 包起來的部分會去 pom 抓,有自己的就抓自己的,沒有就抓父 pom 的,雖然 provider 有 api,但不會去抓,如果都沒有,啟動會報錯




※服務發現


使用 DiscoveryClient 可以發現 Eureka 上註冊的服務

@Resource
private DiscoveryClient client;
    
@GetMapping("/discovery")
public DiscoveryClient discoveryTest() {
    List<String> services = client.getServices();
    System.out.println("所有服務名稱=" + services);
    
    // List<ServiceInstance> instances = client.getInstances("provider1".toUpperCase()); // 一定要大寫
    services.forEach(serviceName -> {
        List<ServiceInstance> instances = client.getInstances(serviceName);
        instances.forEach(s -> {
            System.out.println("host=" + s.getHost()); // 192.168.254.104
            System.out.println("instanceId=" + s.getInstanceId()); // bruceProvider1
            System.out.println("scheme=" + s.getScheme()); // null
            System.out.println("serviceId=" + s.getServiceId()); // PROVIDER1
            System.out.println("port=" + s.getPort()); //  9001
            System.out.println("uri=" + s.getUri()); // http://192.168.254.104:9001
    
            Map<String, String> metadata = s.getMetadata();
            metadata.forEach((k, v) -> {
                System.out.print("key=" + k); // management.port
                System.out.println(", value=" + v); // 9001
            });
        });
        System.out.println("------------------------------------");
    });
    return client;
}


※在 provider 加上上面的程式碼

※注意 DiscoveryClient是
org.springframework.cloud.client.discovery 的包

※在 provider 的啟動類上加上 @EnableDiscoveryClient,但我試的結果,不加也可以



private static final String PROVIDER_URI = "http://localhost:9001";
    
@GetMapping("/consumer/discovery")
public Object discovery() {
    return restTemplate.getForObject(PROVIDER_URI + "/discovery", Object.class);
}


※provider 最後是要給 consumer 的,所以這裡新增這一段程式碼,但回傳值我給 DiscoveryClient 後,打上網址 localhost/consumer/discovery 會報錯



※Eureka 增加帳號密碼

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-security</artifactId>
</dependency>


※在 Eureka server 增加 security 的 POM

※application.yml 增加 spring.security.user,下一層有 name 和 password 可以設定帳號密碼

※如果有集群,每個都要加 pom 和設定密碼,也可以設定不一樣的帳密

※如果只有加 pom,連網址就會出現打帳密的視窗,但因為沒有帳密進不去


@SpringBootApplication
@EnableEurekaServer
public class EurekaMain extends WebSecurityConfigurerAdapter {
    public static void main(String[] args) {
        SpringApplication.run(EurekaMain.class, args);
    }
    
    @Override
    protected void configure(HttpSecurity http) throws Exception {
        // http.csrf().disable(); // 不處理 CSRF 攻擊
        http.csrf().ignoringAntMatchers("/eureka/**");
        super.configure(http);
    }
}


※繼承 WebSecurityConfigurerAdapter 後,覆寫 configure,參數是 HttpSecurity 的

※也可以自己寫一個類別繼承 WebSecurityConfigurerAdapte

※不繼承,server 端不會有問題,差在 client 端要連的時候會報 Cannot execute request on any known server 的錯

※用戶端連 eureka 時,只要在 application.yml 的 defaultZone 改一下就可以,如
http://xxx:ooo@xxx.ooo9051:9051/eureka
就是在原本的網址加上「帳號:密碼@」