本文是看某課閘道器於 SpringCloud 微服務實戰的視訊總結的筆記,其中涉及了
- Eureka Server 和 Eureka Client 的配置
- Eureka Server 高可用性
- 服務間通訊的兩種方式:RestTemplate 和 Feign
- RabbitMQ 的安裝和使用
- 配置中心的使用
- Spring Cloud Stream 的使用
- 服務閘道器 Zuul 的各種用法
由於是隨堂筆記,寫的有點隨意,大佬們見諒~
文中提到的大部分技術都會在我的一個開源專案中用到,這個專案後端業務邏輯部分已經基本寫完了,就差許可權驗證、閘道器配置和後期優化啦,感興趣的大佬可以看看。
一、關於命令列
啟動 SpringBoot 專案
java -jar test.jar
複製程式碼
啟動 SpringBoot 專案並指定埠
java -jar -Dserver.port=8899 test.jar
複製程式碼
啟動 SpringBoot 專案並指定後臺執行
nohup java -jar test.jar > /dev/null 2>&1 &
複製程式碼
檢視程式
ps -ef | grep eureka
複製程式碼
殺死程式
kill -9 程式號
複製程式碼
在本地安裝專案到本地 maven 倉庫
mvn -Dmaven.test.skip=true -U clean install
複製程式碼
二、Eureka Server
2.1 新建
選 CloudDiscovery -> Eureka Server
注意 SpringBoot 版本
2.2 配置
在啟動類 Application 上加註解
@EnableEurekaServer
複製程式碼
在配置檔案 application.yml
註冊服務
eureka:
client:
service-url:
defaultZone: http://localhost:8080/eureka/
複製程式碼
啟動專案,瀏覽器輸入地址 http://localhost:8080
即可看到專案正常啟動,並將自己註冊上去了
ApplicationName
是 UNKNOWN
,想改應用名字的話在 application.yml
做以下配置
eureka:
client:
service-url:
defaultZone: http://localhost:8080/eureka/
spring:
application:
name: eureka
複製程式碼
再啟動專案,瀏覽器中檢視,名字正確顯示
如果不想讓註冊中心出現在註冊列表中,配置 register-with-eureka: false
eureka:
client:
service-url:
defaultZone: http://localhost:8080/eureka/ #配置預設註冊地址
register-with-eureka: false #不讓該服務顯示在應用列表中
spring:
application:
name: eureka #配置應用名字
複製程式碼
三、Eureka Client
3.1 新建專案
選 CloudDiscovery -> Eureka Discovery
注意 SpringBoot 和 SpringCloud 版本與server一致
3.2 配置
- 入口
Application
新增註解@EnableDiscoveryClient
@SpringBootApplication
@EnableDiscoveryClient
public class ClientApplication {
public static void main(String[] args) {
SpringApplication.run(ClientApplication.class, args);
}
}
複製程式碼
- 配置 server 地址
eureka:
client:
service-url:
defaultZone: http://localhost:8761/eureka/
spring:
application:
name: client
複製程式碼
- 自定義連結地址,配置完後瀏覽器地址會變成
http://clientname:8080/
eureka:
instance:
hostname: clientName
複製程式碼
- 如果頻繁重啟 client 服務,會出現如下警告
這是 SpringCloud 的自我保護機制,就是不管這個服務在不線上,都把它當成線上。開發環境中為了除錯方便可以關閉這個功能,注意生產環境一定要開啟。
在 server 的 applicaitono.yml
中做如下配置
eureka:
server:
enable-self-preservation: false
複製程式碼
四、Eureka Server 高可用性
目前是 Client 註冊到一個 Eureka Server 上,如果這個 Server 掛掉了怎麼辦呢?
可以啟動多個 Eureka Server ,讓他們相互註冊。
這裡演示啟動三個 Eureka Server 相互註冊,並把 Client 分別註冊到這三個 Server 上。
配置 server
分別在 8761, 8762, 8763 三個埠上啟動 EurekaApplication
、EurekaApplication2
、EurekaApplication3
三個服務,在三個服務的 applicaiton.yml
中分別配置其他兩個服務的地址。
如EurekaApplication
就配 http://localhost:8762/eureka/,http://localhost:8763/eureka/
,
EurekaApplication2
就配 http://localhost:8761/eureka/,http://localhost:8763/eureka/
,
EurekaApplication3
就配 http://localhost:8761/eureka/,http://localhost:8762/eureka/
,
EurekaApplication
的 applicaiton.yml
如下:
eureka:
client:
service-url:
defaultZone: http://localhost:8762/eureka/,http://localhost:8763/eureka/
複製程式碼
這樣就把三個服務互相關聯上了。
配置 client
然後在 Client 的 applicaiton.yml
中把三個服務地址都配上
eureka:
client:
service-url:
defaultZone: http://localhost:8761/eureka/,http://localhost:8762/eureka/,http://localhost:8763/eureka/
複製程式碼
檢視EurekaApplication
,發現註冊到了8762 和 8763 上。三個server只要還有一個活著,服務就不會掛。
五、應用間通訊
應用間通訊有兩種主流通訊方式:
HTTP代表: SpringCloud
RPC代表: Dubbo
SpringCloud 中服務間兩種 restful 呼叫方式
- RestTemplate
- Feign
5.1 RestTemplate 方式
RestTemplate 呼叫一共有三種方法,下面一一介紹。
先在要提供資料的應用裡寫個 Controller 暴露介面,叫 ServerController
吧
@RestController
@RequestMapping("/product")
public class ServerController {
@GetMapping("/msg")
public String getMsg(){
return "I am product msg";
}
}
複製程式碼
然後在需要接收資料的應用寫個 Controller ,叫 ClientController
5.1.1 方法一
直接使用 RestTemplate
手動寫入提供資料的 url 地址
@RestController
@RequestMapping("/order")
@Slf4j
public class ClientController {
@GetMapping("/getmsg")
public String getMsg(){
RestTemplate restTemplate = new RestTemplate();
String result = restTemplate.getForObject("http://localhost:8080/product/msg", String.class);
log.info("result={}", result);
return result;
}
}
複製程式碼
5.1.2 方法二
不手動輸入 url 地址,使用 LoadBalancerClient
通過應用名動態獲取,然後再使用 RestTemplate
。
loadBalancerClient.choose("product");
這個 product
是提供資料的應用 id
@RestController
@RequestMapping("/order")
@Slf4j
public class ClientController {
@Autowired
LoadBalancerClient loadBalancerClient;
@GetMapping("/getmsg")
public String getMsg(){
ServiceInstance serviceInstance = loadBalancerClient.choose("product");
String url = String.format("http://%s:%s/product/msg", serviceInstance.getHost(), serviceInstance.getPort());
RestTemplate restTemplate = new RestTemplate();
String result = restTemplate.getForObject(url, String.class);
return result;
}
}
複製程式碼
5.1.3方法三
用 @LoadBalanced
註解
新建 RestTemplateConfig
類
@Component
public class RestTemplateConfig {
@Bean
@LoadBalanced
public RestTemplate restTemplate(){
return new RestTemplate();
}
}
複製程式碼
然後在 ClientController
中使用。
restTemplate.getForObject("http://product/product/msg", String.class);
url 中的兩個 product
,第一個表示應用名,第二個是 api 的地址。如果 api 地址是 /abc
,那 url 就是 http://product/abc
@RestController
@RequestMapping("/order")
@Slf4j
public class ClientController {
@Autowired
RestTemplate restTemplate;
@GetMapping("/getmsg")
public String getMsg(){
String result = restTemplate.getForObject("http://product/product/msg", String.class);
return result;
}
}
複製程式碼
5.2 Feign 方式
使用 Feign 有以下幾個步驟
步驟一:引入依賴
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-openfeign</artifactId>
</dependency>
複製程式碼
注意
這裡注意一個問題,有的視訊和文章裡引的依賴是 spring-cloud-starter-feign
,剛開始我引的也是這個,可是死活引不進來。這時到 maven 倉庫 mvnrepository.com/ 裡看一下,搜 spring-cloud-starter-feign
看到上面寫著:
Spring Cloud Starter Feign (deprecated, please use spring-cloud-starter-openfeign)
說 spring-cloud-starter-feign
已經廢棄了,請使用 spring-cloud-starter-openfeign
。
我用的 SpringCloud 版本比較高,可能不支援 spring-cloud-starter-feign
了。
步驟二:配置 @EnableFeignClients 註解
在程式的入口類配置 @EnableFeignClients
註解
@SpringBootApplication
@EnableDiscoveryClient
@EnableFeignClients
public class OrderApplication {
public static void main(String[] args) {
SpringApplication.run(OrderApplication.class, args);
}
}
複製程式碼
找不到 @EnableFeignClients
的話請檢查依賴是否引對,版本是否正確。
步驟三:建立封裝介面
import org.springframework.cloud.openfeign.FeignClient;
import org.springframework.stereotype.Component;
import org.springframework.web.bind.annotation.GetMapping;
@FeignClient(name = "product")
@Component
public interface ProductClient {
@GetMapping("/product/msg")
String getMsg();
}
複製程式碼
介面上加 @FeignClient
註解,括號裡的 name = "product"
宣告瞭要去應用名為 product
的應用找資料(應用名大小寫不敏感)。
@GetMapping("/product/msg")
註明請求方式和路徑。
所以 getMsg()
方法的意思是要請求 product
應用裡 api 為 /product/msg
的字串。
步驟四:呼叫
@RestController
@RequestMapping("/order")
@Slf4j
public class ClientController {
@Autowired
ProductClient productClient;
@GetMapping("/getmsg")
public String getMsg(){
return productClient.getMsg();
}
}
複製程式碼
注入第三步建立的 ProductClient
,然後直接呼叫介面裡定義的方法即可。
我這裡注入 ProductClient
編輯器會報錯,但不影響編譯。
Could not autowire. No beans of 'ProductClient' type found
複製程式碼
看著不順眼就在 ProductClient
上加了個 @Component
註解。
最後總結下 Feign :
- 宣告式 REST 客戶端(偽RPC)
- 採用了基於介面的註解
六、安裝 RabbitMQ
本文用 Docker 安裝 RabbitMQ,Docker教程看 這裡
開啟 RabbitMQ 官方下載頁面 www.rabbitmq.com/download.ht… Docker
點選 Docker image
連結進入到詳情頁
可以看到最新版本是 3.7.7
,複製 3.7.7-management
,在命令列敲以下程式碼並執行
docker run -d --hostname my-rabbit -p 5672:5672 -p 15672:15672 rabbitmq:3.7.7-management
複製程式碼
使用 docker ps 來檢視我們正在執行的容器
Solo-mac:~ solo$ docker ps
CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES
345859e88ead rabbitmq:3.7.7-management "docker-entrypoint.s…" 2 minutes ago Up 2 minutes 4369/tcp, 5671/tcp, 0.0.0.0:5672->5672/tcp, 15671/tcp, 25672/tcp, 0.0.0.0:15672->15672/tcp goofy_volhard
複製程式碼
瀏覽器輸入 http://localhost:15672
開啟 RabbitMQ ,第一次會讓輸使用者名稱密碼,使用者名稱和密碼都是 guest
, 輸入之後進入管理介面
到此 RabbitMQ 安裝完成。
七、配置中心
7.1 配置中心服務端配置
-
新建專案 config
勾選 Cloud Config -> Config Server 和 Cloud Discovery -> Eureka Discovery 複製程式碼
-
在啟動類上新增註解
@SpringBootApplication @EnableDiscoveryClient @EnableConfigServer public class ConfigApplication { public static void main(String[] args) { SpringApplication.run(ConfigApplication.class, args); } } 複製程式碼
-
在 github 上或碼雲上新建一個專案,將
order
專案的application.yml
配置檔案傳上去,用來測試。 -
配置專案的
application.yml
檔案eureka: client: service-url: defaultZone: http://localhost:8761/eureka/ server: port: 8081 spring: application: name: config cloud: config: server: git: uri: https://gitee.com/xxxxxxx username: xxxxxx password: xxxxxx basedir: xxxxxx #本地的路徑 複製程式碼
uri 是倉庫地址,username 和 password 是倉庫的使用者名稱密碼
-
配置完成後啟動專案,可以在註冊中心看到專案註冊上去了,瀏覽器中訪問
http://localhost:8081/order-a.yml
,也能正常讀取 git 上的配置檔案。訪問地址輸入的字尾是 '/order-a.yml', 這裡解釋一下。
/{name}-{profiles}.yml /{label}/{name}-{profiles}.yml name: 服務名,這裡是 order profiles 環境 label 分支(branch) 不寫的話預設是 master 分支 複製程式碼
7.2 配置中心客戶端配置
用 order 專案作為客戶端
- 在 order 的 server 模組的
pom.xml
檔案裡新增config-client
依賴
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-config-client</artifactId>
</dependency>
複製程式碼
-
將
application.yml
改名為bootstrap.yml
-
配置
bootstrap.yml
spring: application: name: order cloud: config: discovery: enabled: true service-id: config #配置中心server的應用名 profile: dev eureka: client: service-url: defaultZone: http://localhost:8761/eureka/ 複製程式碼
配置完後啟動專案,可以正常執行。
注意:
- 不要忘記改配置檔名為
bootstrap.yml
- 在本地配置檔案中配置 eureka 的 service-url,而不是從 config 中讀取,原因是如果eureka 的埠號不是預設的 8761 ,會找不到。
- 如果git上有
order.yml
,order-dev.yml
,配置的是order-dev.yml
,那載入的時候也會預設載入order.yml
並將兩個檔案合併。利用這一特性,可以在order.yml
裡寫公共配置。
7.3 Spring Cloud Bus 自動重新整理配置
7.3.1 config 專案新增spring cloud bus 依賴
在 config 專案新增 spring cloud bus 依賴
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-bus-amqp</artifactId>
</dependency>
複製程式碼
啟動專案,在 RabbitMQ 控制檯檢視,有一個連線,說明配置成功。
7.3.2 order 專案新增spring cloud bus 依賴
同上在 order 的 server 模組裡新增依賴
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-bus-amqp</artifactId>
</dependency>
複製程式碼
執行再看 RabbitMQ ,出現兩個連線
7.3.3 配置自動重新整理
配置 config 專案的 application.yml
檔案,將 bus-refresh
介面暴露出來
management:
endpoints:
web:
exposure:
include: "*"
複製程式碼
在 order 中新建一個 controller,用來讀取遠端配置裡的 env 欄位
value方式取配置
@RestController
@RequestMapping("/env")
@RefreshScope
public class EnvController {
@Value("${env}")
private String env;
@GetMapping("/print")
public String print(){
return env;
}
}
複製程式碼
注意一定要加
@RefreshScope
註解,否則不會自動重新整理配置
再次啟動兩個專案,訪問 http://localhost:8899/env/print
,得到結果是 git 上配置的 env 的值。
更改 git 上 env 的值,傳送 post 請求 http://127.0.0.1:8081/actuator/bus-refresh
重新整理訊息佇列,再重新整理 http://localhost:8899/env/print
會看到沒有重啟專案但 env 的值改變了。
字首方式取配置
git 配置
env: dev5
girl:
name: lili
age: 18
複製程式碼
新建類 GirlConfig
@Data
@Component
@ConfigurationProperties("girl")
@RefreshScope
public class GirlConfig {
private String name;
private Integer age;
}
複製程式碼
新建 GirlController
@RestController
public class GirlController {
@Autowired
GirlConfig girlConfig;
@GetMapping("girl/print")
public String print(){
return "name= " + girlConfig.getName() + ", age= " + girlConfig.getAge();
}
}
複製程式碼
瀏覽器輸入 http://localhost:8899/girl/print
,得到結果 name= lili, age= 18
。
跟上面一樣改變 git 的配置,傳送 post 請求 http://127.0.0.1:8081/actuator/bus-refresh
重新整理訊息佇列,可以看到得到的結果也跟著改變了。
如果發請求 http://127.0.0.1:8081/actuator/bus-refresh
返回值是 500,那就是 bus 沒配好。最後可能的原因是版本問題,把 SpringBoot
版本改成 2.0.0.BUILD-SNAPSHOT
,SpringCloud
版本改成 Finchley.BUILD-SNAPSHOT
應該就沒問題了。
八、RabbitMQ 的基本使用
在 order 專案中演示
先在配置檔案中配置 rabbitmq
的資訊。這些配置可以放到遠端 git 上
spring:
rabbitmq:
host: localhost
port: 5672
username: guest
password: guest
複製程式碼
8.1 基本使用
接收訊息有三種基本用法
方法1:必須提前宣告佇列
- 先在 RabbitMQ 控制檯建立一個佇列
myQueue
- 再建立訊息接收者,收到訊息後在控制檯列印
/**
* RabbitMQ 訊息接收者
*/
@Slf4j
@Component
public class MqReceiver {
@RabbitListener(queues = "myQueue")
public void process(String msg){
log.info("reveicer: " + msg);
}
}
複製程式碼
-
建立訊息傳送者,簡單起見在測試類裡寫個方法
/** * RabbitMQ 訊息傳送方 */ @Component public class RabbitMQTest extends OrderApplicationTests { @Autowired AmqpTemplate amqpTemplate; @Test public void test1(){ amqpTemplate.convertAndSend("myQueue", "now " + new Date()); } } 複製程式碼
執行測試,控制檯成功列印出收到的訊息。
方法2:自動建立佇列
先將方法一建立的佇列 myQueue
刪除,傳送方不變,改一下接收方
@RabbitListener(queuesToDeclare = @Queue("myQueue"))
public void process(String msg){
log.info("reveicer: " + msg);
}
複製程式碼
用 queuesToDeclare
會自動建立佇列。
方法3:自動建立佇列,並將 queue 和 exchange 繫結
先將佇列 myQueue
刪除,傳送方不變,改一下接收方
@RabbitListener(bindings = @QueueBinding(
value = @Queue("myQueue"),
exchange = @Exchange("myExchange")
))
public void process(String msg){
log.info("reveicer: " + msg);
}
複製程式碼
8.2 訊息分組
假設訂單服務有兩個分組,數碼供應商和水果供應商。下單之後是電腦的訂單會被髮給數碼供應商,是水果的訂單會被髮給水果供應商。兩個供應商各自接收各自的訊息。
接收者
/**
* 數碼供應商接收訊息
* @param msg
*/
@RabbitListener(bindings = @QueueBinding(
exchange = @Exchange("myOrder"),
key = "computer",
value = @Queue("computerOrder")
))
public void processComputer(String msg){
log.info("computerOrder reveicer: " + msg);
}
/**
* 水果供應商接收訊息
* @param msg
*/
@RabbitListener(bindings = @QueueBinding(
exchange = @Exchange("myOrder"),
key = "fruit",
value = @Queue("fruitOrder")
))
public void processFruit(String msg){
log.info("fruitOrder reveicer: " + msg);
}
複製程式碼
訊息傳送者
@Test
public void send(){
amqpTemplate.convertAndSend("myOrder", "computer", "now " + new Date());
}
複製程式碼
這裡傳送的是電腦的訂單,convertAndSend()
三個引數依次是 exchange
, routingKey
, message
傳送訊息之後只有 computerOrder
接收到了訊息。
檢視 RabbitMQ 控制體臺可以清楚的看到 exchange 和 queue 的關係
九、Spring Cloud Stream
Spring Cloud Stream is a framework for building highly scalable event-driven microservices connected with shared messaging systems.
複製程式碼
Spring Cloud Stream 目前支援的訊息中介軟體只有 RabbitMQ
和 Kafka
9.1 使用步驟
下面結合 RabbitMQ
演示 Spring Cloud Stream 的用法
-
引入依賴
<dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-starter-stream-rabbit</artifactId> </dependency> 複製程式碼
-
配置 RabbitMQ,跟上節一樣
spring: rabbitmq: host: localhost port: 5672 username: guest password: guest 複製程式碼
-
建立介面 StreamClient
import org.springframework.cloud.stream.annotation.Input; import org.springframework.cloud.stream.annotation.Output; import org.springframework.messaging.MessageChannel; import org.springframework.messaging.SubscribableChannel; public interface StreamClient { String INPUT = "messageInput"; String OUTPUT = "messageOut"; @Input(INPUT) SubscribableChannel input(); @Output(OUTPUT) MessageChannel output(); } 複製程式碼
-
建立訊息接受者,這裡先接收字串
@Component @EnableBinding(StreamClient.class) @Slf4j public class StreamReceiver { @StreamListener(StreamClient.OUTPUT) public void process(String obj){ log.info("StreamReceiver: " + obj); } } 複製程式碼
-
建立訊息傳送者
import com.solo.order.message.StreamClient; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.messaging.support.MessageBuilder; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.RestController; import java.util.Date; @RestController public class SendMessageController { @Autowired private StreamClient streamClient; @GetMapping("/sendMessage") public void send() { String message = "now: " + new Date(); streamClient.output().send(MessageBuilder.withPayload(message).build()); } } 複製程式碼
注意 MessageBuilder 別引錯包了
9.2 訊息分組
如果同時開啟了多個例項,有可能多個例項都收到訊息,為避免這個問題,可以用訊息分組。
在配置檔案裡新增
spring:
cloud:
#訊息分組
stream:
bindings:
messageInput: #自己定義的佇列名
group: order # group 名可以隨意起
複製程式碼
9.3 傳送接收物件
改造訊息接收者
/**
* 接收物件
* @param dto
*/
@StreamListener(StreamClient.OUTPUT)
public void process(OrderDTO dto){
log.info("StreamReceiver: " + dto);
}
複製程式碼
改造訊息傳送者
/**
* 傳送物件
*/
@GetMapping("/sendMessage")
public void send() {
OrderDTO dto = new OrderDTO();
dto.setOrderId("12345678");
streamClient.output().send(MessageBuilder.withPayload(dto).build());
}
複製程式碼
如果想在 MQ 控制檯看到序列化之後的 json 字串而不是物件名,更改配置如下
spring:
cloud:
#訊息分組
stream:
bindings:
messageInput: #自己定義的佇列名
group: order # group 名可以隨意起
content-type: application/json #讓mq裡顯示json字串而不是物件
複製程式碼
新增 content-type: application/json
9.4 收到訊息後回應
在 StreamClient 裡新增兩個介面
public interface StreamClient {
String INPUT = "messageInput";
String OUTPUT = "messageOut";
String INPUT2 = "messageInput2";
String OUTPUT2 = "messageOut2";
@Input(INPUT)
SubscribableChannel input();
@Output(OUTPUT)
MessageChannel output();
@Input(INPUT2)
SubscribableChannel input2();
@Output(OUTPUT2)
MessageChannel output2();
}
複製程式碼
訊息接收者做如下更改
@StreamListener(StreamClient.OUTPUT)
@SendTo(StreamClient.OUTPUT2)
public String process(OrderDTO dto){
log.info("StreamReceiver: " + dto);
return "Received...";
}
@StreamListener(StreamClient.OUTPUT2)
public void process2(String msg){
log.info("StreamReceiver2: " + msg);
}
複製程式碼
主要是新增一個 @SendTo(StreamClient.OUTPUT2)
註解,然後返回需要的值。再定義一個接收 StreamClient.OUTPUT2
的接收者。
十、Redis 安裝與簡單使用
10.1 安裝
通過 Docker 安裝並啟動
docker run -d -p 6379:6379 redis:4.0.8
複製程式碼
mac 下的 redis 視覺化工具:Redis Desktop Manager
,簡稱 RDM
10.2 使用
先新增依賴
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
複製程式碼
然後配置 redis 的地址和埠號
spring:
redis:
host: localhost
port: 6379
複製程式碼
十一、服務閘道器 Zuul
11.1 簡介
服務閘道器的要素
- 穩定性,高可用
- 效能、併發
- 安全性
- 擴充套件性
常用閘道器方案
- Nginx + Lua
- Kong ( 基於 Nginx + Lua )
- Tyk
- Spring Cloud Zuul
Zuul 的特點
- 路由 + 過濾器
- 核心是一系列的過濾器
Zuul 的四種過濾器 API
- 前置(Pre)
- 路由(Route)
- 後置(Post)
- 錯誤(Error)
11.2 用 Zuul 實現簡單的路由轉發
新建專案 api-gateway ,勾選 Cloud Config -> Config Client,CloudDiscovery -> Eureka Discovery,Cloud Routing -> Zuul 三個選項,點下一步完成建立
修改 application.properties
檔案為 bootstrap.yml
並做如下配置
spring:
application:
name: api-gateway
cloud:
config:
discovery:
enabled: true
service-id: config
profile: dev
eureka:
client:
service-url:
defaultZone: http://localhost:8761/eureka/
複製程式碼
入口類新增 @EnableZuulProxy
註解
@SpringBootApplication
@EnableZuulProxy
public class ApiGatewayApplication {
public static void main(String[] args) {
SpringApplication.run(ApiGatewayApplication.class, args);
}
}
複製程式碼
在埠 9000 啟動專案,就可以通過閘道器訪問其他專案的 api 啦
如要訪問 product 專案的 product/list
介面,直接在瀏覽器輸入 http://localhost:9000/product/product/list
即可。
訪問格式是 http://localhost:9000/應用id/api地址
11.3 自定義路由
bootstrap.yml
新增
zuul:
routes:
myProduct: #自己定義的名字
path: /myProduct/**
serviceId: product
複製程式碼
即可通過 http://localhost:9000/myProduct/product/list
訪問上面的介面
簡潔寫法
zuul:
routes:
product: /myProduct/**
複製程式碼
11.4 排除某些路由
排除掉 /product/list
,使它不能被訪問
zuul:
routes:
# 簡介寫法
product: /myProduct/**
# 排除某些路由
ignored-patterns:
- /**/product/list
複製程式碼
11.5 Cookie 和動態路由
讀取Cookie
預設會過濾掉 cookie,如果想拿到cookie,設定 sensitiveHeaders:
為空即可
zuul:
routes:
myProduct:
path: /myProduct/**
serviceId: product
sensitiveHeaders:
複製程式碼
全域性設定敏感頭
zuul:
# 全域性設定敏感頭
sensitive-headers:
複製程式碼
動態配置路由
在 Git 上新建 api-gateway-dev.yml
將 zuul 的配置移到 git 上
新建配置類或直接在入口類上寫字首方式取配置
@SpringBootApplication
@EnableZuulProxy
public class ApiGatewayApplication {
public static void main(String[] args) {
SpringApplication.run(ApiGatewayApplication.class, args);
}
@ConfigurationProperties("zuul")
@RefreshScope
public ZuulProperties ZuulProperties(){
return new ZuulProperties();
}
}
複製程式碼
11.6 Pre 和 Post 過濾器
用 Pre 過濾器實現 token 校驗
下面用 Zuul 的 pre 過濾器實現請求的 token 校驗
新建 TokenFilter
import com.netflix.zuul.ZuulFilter;
import com.netflix.zuul.context.RequestContext;
import com.netflix.zuul.exception.ZuulException;
import org.springframework.http.HttpStatus;
import org.springframework.stereotype.Component;
import org.springframework.util.StringUtils;
import javax.servlet.http.HttpServletRequest;
import static org.springframework.cloud.netflix.zuul.filters.support.FilterConstants.PRE_DECORATION_FILTER_ORDER;
import static org.springframework.cloud.netflix.zuul.filters.support.FilterConstants.PRE_TYPE;
@Component
public class TokenFilter extends ZuulFilter {
@Override
public String filterType() {
return PRE_TYPE;
}
@Override
public int filterOrder() {
return PRE_DECORATION_FILTER_ORDER - 1;
}
@Override
public boolean shouldFilter() {
return true;
}
@Override
public Object run() throws ZuulException {
RequestContext requestContext = RequestContext.getCurrentContext();
HttpServletRequest request = requestContext.getRequest();
//這裡從url裡獲取,也可以從
String token = request.getParameter("token");
if (StringUtils.isEmpty(token)){
requestContext.setSendZuulResponse(false);
requestContext.setResponseStatusCode(HttpStatus.UNAUTHORIZED.value());
}
return null;
}
}
複製程式碼
沒有攜帶 token 的請求將會報 401 錯誤。
用 Post 過濾器在返回頭裡加內容
新建 AddResponseFilter
import com.netflix.zuul.ZuulFilter;
import com.netflix.zuul.context.RequestContext;
import com.netflix.zuul.exception.ZuulException;
import org.springframework.cloud.netflix.zuul.filters.support.FilterConstants;
import org.springframework.stereotype.Component;
import javax.servlet.http.HttpServletResponse;
import java.util.UUID;
@Component
public class AddResponseFilter extends ZuulFilter {
@Override
public String filterType() {
return FilterConstants.POST_TYPE;
}
@Override
public int filterOrder() {
return FilterConstants.SEND_RESPONSE_FILTER_ORDER - 1;
}
@Override
public boolean shouldFilter() {
return true;
}
@Override
public Object run() throws ZuulException {
RequestContext requestContext = RequestContext.getCurrentContext();
HttpServletResponse response = requestContext.getResponse();
response.addHeader("X-Foo", UUID.randomUUID().toString());
return null;
}
}
複製程式碼
在返回頭裡加了 X-Foo
,重啟專案請求介面發現值被成功新增了進去
11.7 Zuul 限流
import com.google.common.util.concurrent.RateLimiter;
import com.netflix.zuul.ZuulFilter;
import com.netflix.zuul.exception.ZuulException;
import com.solo.apigateway.exception.RateLimitException;
import static org.springframework.cloud.netflix.zuul.filters.support.FilterConstants.PRE_TYPE;
import static org.springframework.cloud.netflix.zuul.filters.support.FilterConstants.SERVLET_DETECTION_FILTER_ORDER;
/**
* 限流攔截器. 令牌桶, 用 google 的 guava 實現
*/
public class RateLimitFilter extends ZuulFilter {
public static final RateLimiter RATE_LIMITER = RateLimiter.create(100);
@Override
public String filterType() {
return PRE_TYPE;
}
@Override
public int filterOrder() {
return SERVLET_DETECTION_FILTER_ORDER - 1;
}
@Override
public boolean shouldFilter() {
return true;
}
@Override
public Object run() throws ZuulException {
if (RATE_LIMITER.tryAcquire()){
throw new RateLimitException();
}
return null;
}
}
複製程式碼
十二、Zuul 鑑權和新增使用者服務
待完善
十三、Zuul 跨域
跨域問題的解決方法有很多種,可以在單個介面上加註解,也可以在 Zuul 閘道器上統一處理
13.1 在介面上新增註解實現跨域
在介面上新增 @CrossOrigin
註解即可使這個介面實現跨域
13.2 Zuul 解決跨域
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.cors.CorsConfiguration;
import org.springframework.web.cors.UrlBasedCorsConfigurationSource;
import org.springframework.web.filter.CorsFilter;
import java.util.Arrays;
/**
* 跨域配置
*/
@Configuration
public class CorsConfig {
@Bean
public CorsFilter corsFilter() {
final UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
final CorsConfiguration config = new CorsConfiguration();
config.setAllowCredentials(true); //是否支援 cookie 跨域
config.setAllowedHeaders(Arrays.asList("*"));
config.setAllowedOrigins(Arrays.asList("*"));
config.setAllowedMethods(Arrays.asList("*"));
config.setMaxAge(300l); //快取時間。在這個時間段內,相同的跨域請求將不再檢查
source.registerCorsConfiguration("/**", config);
return new CorsFilter(source);
}
}
複製程式碼
隨著開源專案的進行,後期會寫多篇文章結合專案實戰詳細介紹這些技術,歡迎關注~