Google Code Prettify

2021年9月28日 星期二

IPv4 內部 IP

雖然 IPv6 實施很多年了,在平常的應用上,多半還是使用 IPv4,以下是 IPv4 的內部 IP 範圍:

  1. 10.0.0.0 ~ 10.255.255.255
  2. 172.16.0.0 ~ 172.31.255.255
  3. 192.168.0.0 ~192.168.255.255

2021年9月19日 星期日

解決 eclipse 2021-09 + lombok 產生的錯誤

 今天安裝了 eclipse 2021-09 版,加了 lombok 時是在 eclipse.ini 中加入如下設定:

-javaagent:C:\...\eclipse\lombok-1.18.20.jar
打開 eclipse,開啟 *.java 檔時,都會出現如下錯誤:
Unable to make protected final java.lang.Class java.lang.ClassLoader.defineClass(java.lang.String,byte[],int,int) throws java.lang.ClassFormatError accessible: module java.base does not "opens java.lang" to unnamed module @57c69937
解決的辦法是在 eclipse.ini 中加入如下內容,因為這是 Java Module 引起的。
--add-exports=java.base/sun.nio.ch=ALL-UNNAMED 
--add-opens=java.base/java.lang=ALL-UNNAMED 
--add-opens=java.base/java.lang.reflect=ALL-UNNAMED 
--add-opens=java.base/java.io=ALL-UNNAMED 
--add-exports=jdk.unsupported/sun.misc=ALL-UNNAMED

2021年2月18日 星期四

Spring Cloud Gateway: getting started

Spring Cloud 在 2020 年用 Gateway 取代原本的網關 Zuul,新的網關 Gateway 功能更強,效率更高,對 developer 來說,使用上是一樣的簡單。


如上圖是我的環境,我的所有服務放在一台 CentOS 7 上的 Docker 裡,Docker 上有服務註冊的 consul,有提供服務的 wealth-system,這個服務當輸入 http://192.168.50.13:8080/customer/E243350588 會傳回客戶的基本資料。現在要來看 gateway 怎麼寫 …

** 建立一個 spring boot 專案 **

         在 build.gradle 的 dependencies 中加入以下內容,以導入所需要的 jar 檔。

implementation 'org.springframework.cloud:spring-cloud-starter-consul-discovery'
implementation 'org.springframework.cloud:spring-cloud-starter-gateway'
implementation 'org.springframework.boot:spring-boot-starter-actuator'

  • 第 1 行是 consul 註冊、查詢服務所需要的 jar 檔
  • 第 2 行是導入新版 gateway
  • 第 3 行是 health check 所需要的 jar 檔

server:
  servlet:
    context-path: /
  port: 80
  
management:
  server:
    base-path: /
    port: 5001
  endpoint:
    health:
      probes:
        enabled: true
      show-details: always
      
spring:
  application:
    name: fstop-gateway
  profiles:
    active: ${SPRING_PROFILES_ACTIVE}
  main:
    banner-mode: off
  cloud:
    consul:
      enabled: true
      host: 192.168.50.13
      port: 8500
      discovery:
        prefer-ip-address: true
        instance-id: ${spring.application.name}:${random.value}
        register-health-check: true
        health-check-path: /actuator/health
        health-check-interval: 30s
        tags: ${spring.application.name}
    gateway:
      routes:
      - id: wealth
        uri: lb://wealth-system
        predicates:
        - Path=/customer/**

上面是 application.yml 的內容,說明如下:

  • 第 24~34 行,將 gateway 註冊到 consul,如果不註冊到 consul,就無法使用 consul 的服務查詢功能。
  • 第 35~40 行是 gateway 的設定,第 37 行只是個標識 (命名),取個有意義的名稱即可,第 38 行指出,當網址有 /customer/** 時,導向 wealth-system 這個服務。特別注意,這裡寫的是 lb://wealth-system (lb: load balance),就是要使用 consul 提供的服務發現的方式導向,也可以寫成 http://192.168.50.8080,這樣就是直接導向服務所在的網址。

2021年2月16日 星期二

移除 VirtualBox 產生的「網路連線」介面卡

 使用 vagrant 創建 VM 非常方便,每次創建,vagrant 會透過 VirutalBox 在「網路連線」中,新增一個介面卡。

如果 VM 不想要了,透過 vagrant destroy 指令,可以移除這些 VM 使用的介面卡。但是,萬一"忘了"使用上述指令移除呢? 「網路連線」中日積月累的,會產生不少垃圾,這時候可以使用以下指令移除:

vboxmanage hostonlyif remove "VirtualBox Host-Only Ethernet Adapter #2"
後面雙引號中的字串,要和要移除的「裝置名稱」完全一致。

2021年2月15日 星期一

consul: getting started

2020 年底,Spring Cloud 正式以 consul 取代 Eureka,成為 Spring Cloud 微服務架構中,服務註冊、查詢的元件。關於 consul 和 Eureka 的優劣,可以透過 google 得到不少訊息,在這裡就不多說,這裡整理的,主要是從程式開發的角度看, 當改用 consul,要怎麼開發、測試?

Spring Cloud 要部署的元件不少,我把它們部署在 Windows 10 Docker Desktop 裡的 Kubernetes 上,下面整理的資料會有一些 K8s 部署需要的 yaml 檔。

1. 部署 consul

為了讓 Kubernetes 外可以 consul 存取,要有一個 service,內容如下:

apiVersion: v1
kind: Service
metadata:
  name: consul-svc
  labels:
    app: consul
spec:
  type: LoadBalancer
  ports:
    - name: port8301
      protocol: TCP
      port: 8301
      targetPort: 8301
    - name: port8302
      protocol: TCP
      port: 8302
      targetPort: 8302
    - name: port8300
      protocol: TCP
      port: 8300
      targetPort: 8300
    - name: ui
      port: 8500
      targetPort: 8500
    - name: port8600
      protocol: TCP
      port: 8600
      targetPort: 8600
  selector:
    app: consul

因為是 Docker Desktop 單節點的 Kubernetes,EXTERNAL-IP 會是 localhost,從本機透過 8500 即可存取。接著要部署 consul,官網建議部署 consul 最好是 3 個或 5 個成為 cluster,這裡單純是要測試用,只部署一個,因為是成為各服務註冊的元件,部署時選擇 StatefulSet,yaml 檔如下。

apiVersion: apps/v1
kind: StatefulSet
metadata:
  name: consul
spec:
  serviceName: consul-svc
  podManagementPolicy: "Parallel"
  replicas: 1
  template: 
    metadata:
      labels:
        app: consul
    spec:
      terminationGracePeriodSeconds: 10
      containers:
      - name: consul
        image: consul:latest
        args:
             - "agent"
             - "-server"
             - "-bootstrap-expect=1"
             - "-ui"
             - "-data-dir=/consul/data"
             - "-bind=0.0.0.0"
             - "-client=0.0.0.0"
             - "-advertise=$(PODIP)"
        env:
            - name: PODIP
              valueFrom:
                fieldRef:
                  fieldPath: status.podIP
            - name: NAMESPACE
              valueFrom:
                fieldRef:
                  fieldPath: metadata.namespace
        ports:
            - containerPort: 8500
              name: ui-port
            - containerPort: 8400
              name: alt-port
            - containerPort: 53
              name: udp-port
            - containerPort: 8443
              name: https-port
            - containerPort: 8080
              name: http-port
            - containerPort: 8301
              name: serflan
            - containerPort: 8302
              name: serfwan
            - containerPort: 8600
              name: consuldns
            - containerPort: 8300
              name: server
  selector:
    matchLabels:
      app: consul

部署好之後,以 kubectl get statefulset 檢查看看有沒有成功,應該可以看到如下的結果。



打開瀏覽器輸入 http://localhost:8500,應該可以看到如下內容,consul 正常運作。


2. 註冊一個服務到 consul

假設有個財富管理系統,命名為 wealth-system,裡面提供了一個服務 /customer/{idno},可以透過身分證字號查詢客戶基本資料,要如何註冊到 consul ? 首先在 build.gradle 中引入相關的 jar 檔,在 dependencies 中加入如下內容:

implementation 'org.springframework.boot:spring-boot-starter-actuator'
implementation 'org.springframework.cloud:spring-cloud-starter-config'
implementation 'org.springframework.cloud:spring-cloud-starter-consul-discovery'
implementation 'org.springframework.cloud:spring-cloud-starter-consul-config'

接著在 applicatoin.yml 中設定如下內容:

management:
  server:
    base-path: /
    port: 5001
  endpoint:
    health:
      probes:
        enabled: true
      show-details: always

spring:
  application:
    name: wealth-system
  profiles:
    active: ${SPRING_PROFILES_ACTIVE}
  main:
    banner-mode: off
  cloud:
    consul:
      enabled: true
      host: consul-svc
      port: 8500
      discovery:
        prefer-ip-address: true
        instance-id: ${spring.application.name}:${random.value}
        service-name: ${spring.application.name}
        register-health-check: true
        health-check-path: /actuator/health/liveness
        health-check-interval: 60s

  • 第 1 ~ 9 行,設定 management,這是設定監控工具 Actuator 傾聽 5001 port,其中第 6 ~ 8 行指出要提供 health check 所需要的探針, 第 28 行即是指出探針的網址。
  • 同一個服務部署到 consul,會有好幾個 instance,以避免一個 instance 掛了,就無法提供服務,所以第 25 行指出 instance 的名稱,並且後面以亂數來表示,這樣每個 instance 的 intance id 自然就會不同。
  • 第 21、22 行,指出 consul 所在的 IP 和 port。

程式中怎麼啟用 consul? 在 @SpringBootApplication 所在的 class 上加入 @EnableDiscoveryClient。

Controller 程式這裡就不多做說明,基本上就是要提供一個網址為 /customer/{idno} 的服務,可以透過身分證字號查詢客戶基本資料。

compile 好之後,要 push 到 docker hub,方法如下:

(1) 製作 image

Dockerfile 內容如下:

FROM openjdk:11
ADD wealth-system.jar wealth-system.jar
ENV SPRING_PROFILES_ACTIVE sit
EXPOSE 80/tcp
EXPOSE 5001/tcp
ENTRYPOINT ["java","-jar","/wealth-system.jar"]

第 3 行傳入的參數,代表這是一個 sit 環境。第 4、5 行則是對外開放的 port。

在 Dockerfile 所在目錄下指令將 image push 到 docker hub,我在 docker hub 有一個帳號是 twleader 所以第 1 行產生 image 時指定的 image 名稱前有 twleader/。
docker build -t twleader/wealth-system .
docker push

(2) 部署 image 到 Kubernetes

consul 需要固定的 IP,採用 StatefulSet 方式部署,一般的服務每次產生時 IP 可以不一樣,採用 Deployment 方式部署,用這種方式部署會先產生 ReplicaSet 再產 Pod,yaml 檔內容如下:
apiVersion: apps/v1
kind: Deployment
metadata:
  name: wealth 
spec:
  replicas: 2
  selector:
    matchLabels:
      app: wealth
  template:
    metadata:
      name: wealth
      labels:
        app: wealth 
    spec:
      containers:
      - image: twleader/wealth-system:latest
        name: wealth
        ports:
        - containerPort: 80
          name: api-port
        - containerPort: 5001
          name: management-port
        env:
        - name: SPRING_PROFILES_ACTIVE
          value: "sit"
        - name: POD_IP
          valueFrom:
            fieldRef:
              fieldPath: status.podIP
        volumeMounts:
        - name: my-pvc
          mountPath: /mnt/logs
      volumes:
      - name: my-pvc
        persistentVolumeClaim:
          claimName: my-pvc
  • 第 6 行指出會產生兩個 Pod
  • 第 31~37 行,這支程式產生的 log 會放在 /mnt/logs 目錄下,透過這樣的設定,可以讓 log 不會因為 Pod 消失而不見 (會儲存到 PV 所指定的地方)。
部署之後,應該可以在 consul ui 看到如下的內容,一開始可能會是紅色的,需過一段時間,讓 consul 透過 health check 確認服務已經 ready,才會轉為綠色。

3. client

上面已經將服務註冊到 consul 了,那麼其它程式或服務要怎麼存取這些服務呢?

(1) build.gradle

在 dependencies 中加入如下的依賴:
implementation 'org.springframework.boot:spring-boot-starter-actuator'
implementation 'org.springframework.cloud:spring-cloud-starter'
implementation 'org.springframework.cloud:spring-cloud-starter-consul-discovery'

(2) application.yml

management:
  server:
    base-path: /
    port: 15001
  endpoint:
    health:
      probes:
        enabled: true
      show-details: always

spring:
  application:
    name: consul-client
  profiles:
    active: ${SPRING_PROFILES_ACTIVE}
  main:
    banner-mode: off
  cloud:
    consul:
      enabled: true
      host: consul-svc
      port: 8500
      discovery:
        instance-id: ${spring.application.name}:${random.value}
        service-name: ${spring.application.name}
        register-health-check: true
        health-check-path: /actuator/health/liveness
        health-check-interval: 60s

(3) RestTemplate

@Configuration
public class RestConfig {
    @Bean
    @LoadBalanced
    public RestTemplate restTemplate() {
        return new RestTemplate();
    }
}
第 4 行的 @LoadBalanced 很重要! 這樣子才可以透過 consul 上註冊的服務名稱存取到服務。

(4) Controller

@RestController
@RequestMapping("/wealth")
@Slf4j
public class WealthController {
    @Autowired
    RestTemplate rest;
	
    @GetMapping("/customer/{idno}")
    public ResponseEntity<?> findCustomerByIdno(@PathVariable String idno) {
		
        log.info("idno = " + idno);
		
        try {
	    ResponseEntity<CustomerModel> response = rest.getForEntity("http://wealth-system/customer/" + idno, CustomerModel.class);
	    CustomerModel customer = response.getBody();
			
	    log.info(customer.toString());
		
	    return new ResponseEntity<CustomerModel>(customer, HttpStatus.OK);
	}
	catch (Exception e) {
	    log.error(e.getMessage(), e);
		
	    return new ResponseEntity<Exception>(e, HttpStatus.INTERNAL_SERVER_ERROR);
	}
    }
}
  • 看第 5、6 行,RestTemplate 不能直接 new,需要用 @Autowired (註)。 
  • 第 14 行,要對一個服務 request 時,不需要指出該服務的網址或IP,而是直接對服務名稱 wealth-system。

(5) 產生 image

Dockerfile 內容如下:
FROM openjdk:11
ADD consul-client.jar consul-client.jar
ENV SPRING_PROFILES_ACTIVE sit
EXPOSE 18080/tcp
EXPOSE 15001/tcp
ENTRYPOINT ["java","-jar","/consul-client.jar"]
於 Dockerfile 所在目錄下底下的指令:
docker build -t twleader/consul-client .
docker push twleader/consul-client

(6) 部署到 Kubernetes

先為 client 建立一個 service,方便測試,yaml 檔如下:
apiVersion: v1
kind: Service
metadata:
  name: consul-client-svc
spec:
  type: LoadBalancer
  ports:
  - name: consul-client
    port: 18080
    targetPort: 18080
  - name: consul-client-management
    port: 15001
    targetPort: 15001
  selector:
    app: consul-client
接著當然要部署 POD, 使用如下的 deployment yaml file:
apiVersion: apps/v1
kind: Deployment
metadata:
  name: consul-client
spec:
  replicas: 2
  selector:
    matchLabels:
      app: consul-client
  template:
    metadata:
      name: consul-client
      labels:
        app: consul-client 
    spec:
      containers:
      - image: twleader/consul-client:latest
        name: consul-client
        ports:
        - containerPort: 18080
          name: api-port
        - containerPort: 15001
          name: management-port
        env:
        - name: SPRING_PROFILES_ACTIVE
          value: "sit"
        - name: POD_IP
          valueFrom:
            fieldRef:
              fieldPath: status.podIP
        volumeMounts:
        - name: my-pvc
          mountPath: /mnt/logs
      volumes:
      - name: my-pvc
        persistentVolumeClaim:
          claimName: my-pvc

一切部署妥當,應該可以在 consul ui 中看到如下內容,這次要使用到的服務都已經在上面了!


打開瀏覽器,輸入 http://localhost:18080/wealth/customer/E243350588 可以得到傳回的 JSON format 資料如下:
{"idno":"E243350588","name":"Nancy","phoneNo":"0909000911","email":"nancy@yahoo.com"}

2020年11月7日 星期六

Java Stream: Collections

這一篇要整理的是 import static java.util.stream.Collectors.* 配合 stream 所進行的操作,它的作用就像 reduce 一樣,但是提供了更多高可讀性的操作,看一下實作 … 先定義資料如下:
@Data
public class Product implements Serializable {
	private String name;		//商品名稱
	private String brand;		//品牌
	private String kind;		//分類
	private int price;		//價格
	private int count;		//數量
	
	public Product() { }
	
	public Product(String name, String brand, String kind, int price, int count) {
		this.name = name;
		this.brand = brand;
		this.kind = kind;
		this.price = price;
		this.count = count;
	}
}

List<Product> products = Arrays.asList(
	//精油
	new Product("橙花純精油", "怡森氏", "精油", 1179, 10),
	new Product("快樂鼠尾草純精油", "怡森氏", "精油", 879, 8),
	new Product("純天然玫瑰純精油", "怡森氏", "精油", 1569, 5),
	new Product("薰衣草純精油", "奧地利KP", "精油", 680, 12),
	new Product("真正薰衣草", "ANDZEN", "精油", 288, 25),
	//鍵盤
	new Product("G512 RGB 機械遊戲鍵盤(青軸)", "羅技", "鍵盤", 3290, 2),
	new Product("黑寡婦蜘蛛幻彩版鍵盤", "Razer", "鍵盤", 2190, 1),
	new Product("Cynosa Lite 薩諾狼蛛Lite版鍵盤", "Razer", "鍵盤", 990, 7),
	new Product("Vigor GK50 Low Profile 短軸機械式鍵盤", "MSI微星", "鍵盤", 2990, 13),
	new Product("Vigor GK70 Cherry MX RGB機械電競鍵盤 (紅軸版)", "MSI微星", "鍵盤", 2999, 6),
	new Product("Vigor GK60 CL TC 電競鍵盤", "MSI微星", "鍵盤", 3290, 3),
	//滑鼠
	new Product("MX Master 2S 無線滑鼠", "羅技", "滑鼠", 1880, 25),
	new Product("MX Master 3 無線滑鼠", "羅技", "滑鼠", 3490, 18),
	new Product("M590 多工靜音無線滑鼠", "羅技", "滑鼠", 849, 30)
);
假設有家商店,有如上述的貨品,三類 (精油、鍵盤、滑鼠) 及多種品牌的商品,現在開始進行資料的操作:
  1. 所有商品按種類分組
  2. Map<String, List<Product>> groupByKind = products.stream()
    		.collect(groupingBy(Product::getKind));
    		
    groupByKind.forEach((k, v) -> {
    		log.info("Kind: " + k);
    		v.forEach(p -> log.info("  product: " + p.getName()));
    	});
    
    Kind: 滑鼠
      product: MX Master 2S 無線滑鼠
      product: MX Master 3 無線滑鼠
      product: M590 多工靜音無線滑鼠
    Kind: 精油
      product: 橙花純精油
      product: 快樂鼠尾草純精油
      product: 純天然玫瑰純精油
      product: 薰衣草純精油
      product: 真正薰衣草
    Kind: 鍵盤
      product: G512 RGB 機械遊戲鍵盤(青軸)
      product: 黑寡婦蜘蛛幻彩版鍵盤
      product: Cynosa Lite 薩諾狼蛛Lite版鍵盤
      product: Vigor GK50 Low Profile 短軸機械式鍵盤
      product: Vigor GK70 Cherry MX RGB機械電競鍵盤 (紅軸版)
      product: Vigor GK60 CL TC 電競鍵盤
    用 collect  可以展現出 functional programming 的優勢,從指令就可以看的出來,我們的需求是什麼,至於程式實際怎麼執行,不勞我們擔心,Java 會幫我們找出辦法。上述 groupingBy 指令指出以 kind (商品種類) 分組,匯整在 map 裡,後面我們將它依序輸出。
  3. 所有商品按種類分組後再依品牌分組
  4. Map<String, Map<String, List<Product>>> result = products.stream()
    	.collect(groupingBy(Product::getKind,
    		groupingBy(Product::getBrand)
    	));
    		
    result.forEach((kind, p1) -> {
    	log.info("Kind: " + kind);
    	p1.forEach((brand, p2) -> {
    		log.info("  Brand: " + brand);
    		p2.forEach(p3 -> log.info("    Product: " + p3.getName()));
    	});
    });
    
    Kind: 滑鼠
      Brand: 羅技
        Product: MX Master 2S 無線滑鼠
        Product: MX Master 3 無線滑鼠
        Product: M590 多工靜音無線滑鼠
    Kind: 精油
      Brand: 怡森氏
        Product: 橙花純精油
        Product: 快樂鼠尾草純精油
        Product: 純天然玫瑰純精油
      Brand: 奧地利KP
        Product: 薰衣草純精油
        Brand: ANDZEN
        Product: 真正薰衣草
    Kind: 鍵盤
      Brand: 羅技
        Product: G512 RGB 機械遊戲鍵盤(青軸)
      Brand: Razer
        Product: 黑寡婦蜘蛛幻彩版鍵盤
        Product: Cynosa Lite 薩諾狼蛛Lite版鍵盤
      Brand: MSI微星
        Product: Vigor GK50 Low Profile 短軸機械式鍵盤
        Product: Vigor GK70 Cherry MX RGB機械電競鍵盤 (紅軸版)
        Product: Vigor GK60 CL TC 電競鍵盤
    上面語法,我們要多層次分組,也是很明白的"說出"我們的需求,先以種類分組再以品牌分組,就得到我們要的結果了。
  5. 計算各種類中各品牌的商品樣式數量
  6. Map<String, Map<String, Long>> result = products.stream()
    	.collect(groupingBy(Product::getKind,
    		groupingBy(Product::getBrand, counting())
    	));
    
    result.forEach((kind, p1) -> {
    	log.info("Kind: " + kind);
    	p1.forEach((brand, count) -> {
    		log.info("  Brand: " + brand + " = " + count);
    	});
    });
    
    Kind: 滑鼠
      Brand: 羅技 = 3
    Kind: 精油
      Brand: 怡森氏 = 3
      Brand: 奧地利KP = 1
      Brand: ANDZEN = 1
    Kind: 鍵盤
      Brand: 羅技 = 1
      Brand: Razer = 2
      Brand: MSI微星 = 3
    利用 Collections 中的 counting() 就可以計算出數量了!
  7. 最貴的滑鼠
  8. Comparator<Product> priceComparator = Comparator.comparingInt(Product::getPrice);
    		
    Optional<Product> mouse = products.stream()
    	.filter(p -> p.getKind().equals("滑鼠"))
    	.collect(maxBy(priceComparator));
    		
    if (mouse.isPresent()) {
    	log.info(mouse.get().getName());
    }
    
    MX Master 3 無線滑鼠
    先定義 Comparator 讓 collect 的 maxBy 知道比較的方式,這裡就是比價格,在 stream 中配合 filter 找出滑鼠比一下價格,就找出最貴的滑鼠了! 這裡要注意的是,找出來的結果為 Optional, 因為有可能根本沒有滑鼠!
  9. 全部的鍵盤有幾個?
  10. int total = products.stream()
    	.filter(p -> p.getKind().equals("鍵盤"))
    	.collect(summingInt(Product::getCount));
    		
    log.info("鍵盤數量: " + total);
    
    鍵盤數量: 32
    非常簡單的,過濾出鍵盤後,用 summingInt 將數量加總。
  11. 全部的商品以種類先排序,再以分號分隔列出
  12. Comparator<Product> kindComparator = Comparator.comparing(Product::getKind);
    		
    String s = products.stream()
    	.sorted(kindComparator)
    	.map(Product::getName)
    	.collect(joining(","));
    		
    log.info(s);
    
    MX Master 2S 無線滑鼠,MX Master 3 無線滑鼠,M590 多工靜音無線滑鼠,橙花純精油,快樂鼠尾草純精油,純天然玫瑰純精油,薰衣草純精油,真正薰衣草,G512 RGB 機械遊戲鍵盤(青軸),黑寡婦蜘蛛幻彩版鍵盤,Cynosa Lite 薩諾狼蛛Lite版鍵盤,Vigor GK50 Low Profile 短軸機械式鍵盤,Vigor GK70 Cherry MX RGB機械電競鍵盤 (紅軸版),Vigor GK60 CL TC 電競鍵盤
    實務上資料常常要以 csv 檔產出,這時候可以用 joining,這是連接字串的 method,這裡在列出商品時,用 joining 連接並於中間加上逗點。
  13. 將精油分成怡森氏和非怡森氏兩組
  14. Map<Boolean, List<Product>> result = products.stream()
    	.filter(p -> p.getKind().equals("精油"))
    	.collect(partitioningBy(p -> p.getBrand().equals("怡森氏")));
    		
    result.forEach((k, v) -> {
    	log.info(k ? "怡森氏" : "非怡森氏");
    	v.forEach(p -> log.info("  " + p.getName()));
    });
    
    非怡森氏
      薰衣草純精油
      真正薰衣草
    怡森氏
      橙花純精油
      快樂鼠尾草純精油
      純天然玫瑰純精油
    分組除了可以用 groupingBy, 還有另一個選擇 -- partitioningBy,partitioningBy 要帶入的參數是 Predicate,只能分 true、false 兩組,但是效率比較高,如果剛好適用,partitioningBy 會是更好的選擇。
  15. 找出鍵盤中,微星與非微星品牌中最貴的
  16. Map<Boolean, Product> result = products.stream()
    	.filter(p -> p.getKind().equals("鍵盤"))
    	.collect(
    		partitioningBy(
    			p -> p.getBrand().equals("MSI微星"),
    			collectingAndThen(
    				maxBy(Comparator.comparingInt(Product::getPrice)), 
    				Optional::get)
    			)
    		);
    		
    result.forEach((k, v) -> {
    	log.info(k ? "MSI微星" : "非MSI微星");
    	log.info("  Product: " + v.getName() + ": " + v.getPrice());
    });
    
    非MSI微星
      Product: G512 RGB 機械遊戲鍵盤(青軸): 3290
    MSI微星
      Product: Vigor GK60 CL TC 電競鍵盤: 3290
    partitioningBy 還有個優點就是,在分組後,還可以使用 collectingAndThen 做進一步的處理,上面在分組後,我們在該組裡找出最貴的鍵盤。