雖然 IPv6 實施很多年了,在平常的應用上,多半還是使用 IPv4,以下是 IPv4 的內部 IP 範圍:
- 10.0.0.0 ~ 10.255.255.255
- 172.16.0.0 ~ 172.31.255.255
- 192.168.0.0 ~192.168.255.255
雖然 IPv6 實施很多年了,在平常的應用上,多半還是使用 IPv4,以下是 IPv4 的內部 IP 範圍:
今天安裝了 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
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'
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 的內容,說明如下:
使用 vagrant 創建 VM 非常方便,每次創建,vagrant 會透過 VirutalBox 在「網路連線」中,新增一個介面卡。
如果 VM 不想要了,透過 vagrant destroy 指令,可以移除這些 VM 使用的介面卡。但是,萬一"忘了"使用上述指令移除呢? 「網路連線」中日積月累的,會產生不少垃圾,這時候可以使用以下指令移除:
vboxmanage hostonlyif remove "VirtualBox Host-Only Ethernet Adapter #2"後面雙引號中的字串,要和要移除的「裝置名稱」完全一致。
2020 年底,Spring Cloud 正式以 consul 取代 Eureka,成為 Spring Cloud 微服務架構中,服務註冊、查詢的元件。關於 consul 和 Eureka 的優劣,可以透過 google 得到不少訊息,在這裡就不多說,這裡整理的,主要是從程式開發的角度看, 當改用 consul,要怎麼開發、測試?
Spring Cloud 要部署的元件不少,我把它們部署在 Windows 10 Docker Desktop 裡的 Kubernetes 上,下面整理的資料會有一些 K8s 部署需要的 yaml 檔。
為了讓 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 檢查看看有沒有成功,應該可以看到如下的結果。
假設有個財富管理系統,命名為 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
程式中怎麼啟用 consul? 在 @SpringBootApplication 所在的 class 上加入 @EnableDiscoveryClient。
Controller 程式這裡就不多做說明,基本上就是要提供一個網址為 /customer/{idno} 的服務,可以透過身分證字號查詢客戶基本資料。
compile 好之後,要 push 到 docker hub,方法如下:
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
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
implementation 'org.springframework.boot:spring-boot-starter-actuator' implementation 'org.springframework.cloud:spring-cloud-starter' implementation 'org.springframework.cloud:spring-cloud-starter-consul-discovery'
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
@Configuration
public class RestConfig {
@Bean
@LoadBalanced
public RestTemplate restTemplate() {
return new RestTemplate();
}
}
第 4 行的 @LoadBalanced 很重要! 這樣子才可以透過 consul 上註冊的服務名稱存取到服務。
@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);
}
}
}
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
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
{"idno":"E243350588","name":"Nancy","phoneNo":"0909000911","email":"nancy@yahoo.com"}
| 項目 | 日期 | |
|---|---|---|
| * |
Programming
| |
| 1 | Spring Boot Admin | 2018/02/28
|
| 2 | Spring Cloud Eureka: getting started | 2018/06/09
|
| 3 | consul: getting started | 2021/02/15
|
| 4 | Spring Cloud Gateway: getting started | 2021/02/18
|
@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)
);
假設有家商店,有如上述的貨品,三類 (精油、鍵盤、滑鼠) 及多種品牌的商品,現在開始進行資料的操作: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()));
});
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()));
});
});
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);
});
});
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());
}
int total = products.stream()
.filter(p -> p.getKind().equals("鍵盤"))
.collect(summingInt(Product::getCount));
log.info("鍵盤數量: " + total);
Comparator<Product> kindComparator = Comparator.comparing(Product::getKind);
String s = products.stream()
.sorted(kindComparator)
.map(Product::getName)
.collect(joining(","));
log.info(s);
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()));
});
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());
});