在學習並掌握了眾多基礎框架之後,我們的專案繁雜且難以掌握,那麼我們就需要開啟一門新的課程,也就是我們常說的微服務架構
隨著網際網路行業的發展,對服務的要求也越來越高,服務架構也從單體架構逐漸演變為現在流行的微服務架構。
這篇文章我們將會概括到下面幾個知識:
首先我們需要去了解這麼一個宏觀的概念——微服務
在微服務沒有出現之前,也就是我們之前的專案書寫,一般都是採用單體架構:
我們可以給出單體架構的直觀圖:
但是單體架構的優缺點也十分明顯:
當專案逐漸龐大之後,我們就開始使用分散式架構去處理專案:
我們給出分散式架構的直觀圖:
同樣我們也可以很直觀的獲得分散式架構的優缺點:
優點:
缺點:
我們從單體架構升級到分散式架構自然會存在一些我們目前無法解決的問題:
而我們的微服務架構為我們的上述問題提供了一個統一的標準,因而微服務就此而生!
下面我們就來介紹微服務,微服務架構實際上是分散式架構的一種細化,我們給出微服務的架構特徵:
我們同樣給出微服務的一張直觀圖:
微服務的上述特性其實是在給分散式架構制定一個標準,進一步降低服務之間的耦合度,提供服務的獨立性和靈活性。
因此,可以認為微服務是一種經過良好架構設計的分散式架構方案 。
微服務這種方案需要技術框架來落地,目前國內知名度較高的就是SpringCloud和阿里巴巴的Dubbo
我們針對三種微服務技術做一個簡單的對比:
Dubbo | SpringCloud | SpringCloudAlibaba | |
---|---|---|---|
註冊中心 | zookeeper、Redis | Eureka、Consul | Nacos、Eureka |
服務遠端呼叫 | Dubbo協定 | Feign(http協定) | Dubbo、Feign |
設定中心 | 無 | SpringCloudConfig | SpringCloudConfig、Nacos |
服務閘道器 | 無 | SpringCloudGateway、Zuul | SpringCloudGateway、Zuul |
服務監控和保護 | dubbo-admin,功能弱 | Hystix | Sentinel |
最後我們再給出目前企業所常使用的微服務組合:
最後我們介紹一下SpringCloud:
其中SpringCloud常用元件包括有:
下面一個小節我們來學習服務拆分和遠端呼叫兩方面
我們前面提及到了分散式架構需要將功能拆分出來並分離開發,那麼我們該如何進行拆分:
我們給出一個簡單的案例來展示服務拆分操作:
我們首先給出圖示邏輯:
我們需要滿足一下需求:
那麼我們給出案例書寫:
# order訂單資料庫
-- ----------------------------
-- Table structure for tb_order
-- ----------------------------
DROP TABLE IF EXISTS `tb_order`;
CREATE TABLE `tb_order` (
`id` bigint(20) NOT NULL AUTO_INCREMENT COMMENT '訂單id',
`user_id` bigint(20) NOT NULL COMMENT '使用者id',
`name` varchar(100) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL COMMENT '商品名稱',
`price` bigint(20) NOT NULL COMMENT '商品價格',
`num` int(10) NULL DEFAULT 0 COMMENT '商品數量',
PRIMARY KEY (`id`) USING BTREE,
UNIQUE INDEX `username`(`name`) USING BTREE
) ENGINE = InnoDB AUTO_INCREMENT = 109 CHARACTER SET = utf8 COLLATE = utf8_general_ci ROW_FORMAT = Compact;
-- ----------------------------
-- Records of tb_order
-- ----------------------------
INSERT INTO `tb_order` VALUES (101, 1, 'Apple 蘋果 iPhone 12 ', 699900, 1);
INSERT INTO `tb_order` VALUES (102, 2, '雅迪 yadea 新國標電動車', 209900, 1);
INSERT INTO `tb_order` VALUES (103, 3, '駱駝(CAMEL)休閒運動鞋女', 43900, 1);
INSERT INTO `tb_order` VALUES (104, 4, '小米10 雙模5G 驍龍865', 359900, 1);
INSERT INTO `tb_order` VALUES (105, 5, 'OPPO Reno3 Pro 雙模5G 視訊雙防抖', 299900, 1);
INSERT INTO `tb_order` VALUES (106, 6, '美的(Midea) 新能效 冷靜星II ', 544900, 1);
INSERT INTO `tb_order` VALUES (107, 2, '西昊/SIHOO 人體工學電腦椅子', 79900, 1);
INSERT INTO `tb_order` VALUES (108, 3, '梵班(FAMDBANN)休閒男鞋', 31900, 1);
SET FOREIGN_KEY_CHECKS = 1;
# user使用者資料庫
-- ----------------------------
-- Table structure for tb_user
-- ----------------------------
DROP TABLE IF EXISTS `tb_user`;
CREATE TABLE `tb_user` (
`id` bigint(20) NOT NULL AUTO_INCREMENT,
`username` varchar(100) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL COMMENT '收件人',
`address` varchar(255) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL COMMENT '地址',
PRIMARY KEY (`id`) USING BTREE,
UNIQUE INDEX `username`(`username`) USING BTREE
) ENGINE = InnoDB AUTO_INCREMENT = 109 CHARACTER SET = utf8 COLLATE = utf8_general_ci ROW_FORMAT = Compact;
-- ----------------------------
-- Records of tb_user
-- ----------------------------
INSERT INTO `tb_user` VALUES (1, '柳巖', '湖南省衡陽市');
INSERT INTO `tb_user` VALUES (2, '文二狗', '陝西省西安市');
INSERT INTO `tb_user` VALUES (3, '華沉魚', '湖北省十堰市');
INSERT INTO `tb_user` VALUES (4, '張必沉', '天津市');
INSERT INTO `tb_user` VALUES (5, '鄭爽爽', '遼寧省瀋陽市大東區');
INSERT INTO `tb_user` VALUES (6, '範兵兵', '山東省青島市');
SET FOREIGN_KEY_CHECKS = 1;
我們會建立一個如下框架的IDEA框架:
我們對上述資訊進行講解:
/* cloud-demo:父工程,攜帶pom.xml */
/* order-service訂單工程 user-service使用者工程 */
// 具有完整的dao,mapper,service,Controller層並完整書寫Application啟動類
// 具有yml組態檔,包含有port埠資訊,mysql資料庫資訊等
當我們執行程式後,我們可以在瀏覽器中查詢到order相關資料:
但是我們會發現我們是無法查詢到user的詳細資訊的,這是因為我們的order是沒有user的資料庫資訊的
所以我們在完成了服務拆分之後就需要去了解遠端呼叫:
那麼我們該如何實現遠端呼叫:
下面我們給出具體步驟及相關程式碼:
// 我們需要將RestTemplate設定為Bean物件(這裡直接在Application中設定Bean)
package cn.itcast.order;
import cn.itcast.feign.clients.UserClient;
import cn.itcast.feign.config.DefaultFeignConfiguration;
import org.mybatis.spring.annotation.MapperScan;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.client.loadbalancer.LoadBalanced;
import org.springframework.cloud.openfeign.EnableFeignClients;
import org.springframework.context.annotation.Bean;
import org.springframework.web.client.RestTemplate;
@MapperScan("cn.itcast.order.mapper")
@SpringBootApplication
public class OrderApplication {
public static void main(String[] args) {
SpringApplication.run(OrderApplication.class, args);
}
/**
* 建立RestTemplate並注入Spring容器
*/
@Bean
public RestTemplate restTemplate() {
return new RestTemplate();
}
}
package cn.itcast.order.service;
import cn.itcast.feign.clients.UserClient;
import cn.itcast.feign.pojo.User;
import cn.itcast.order.mapper.OrderMapper;
import cn.itcast.order.pojo.Order;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
// 這裡在Service業務層獲得資訊
@Service
public class OrderService {
@Autowired
private OrderMapper orderMapper;
// 這裡自動裝填RestTemplate物件
@Autowired
private RestTemplate restTemplate;
public Order queryOrderById(Long orderId) {
// 1.查詢訂單
Order order = orderMapper.findById(orderId);
// 2.利用RestTemplate發起http請求,查詢使用者
// 2.1.url路徑
String url = "http://localhost/8081/user/" + order.getUserId();
// 2.2.傳送http請求,實現遠端呼叫
// 這裡restTemplate具有getForObject方法和postForObject分別針對get和post請求,後面引數分別為url和對應的class
User user = restTemplate.getForObject(url, User.class);
// 3.封裝user到Order
order.setUser(user);
// 4.返回
return order;
}
}
最後我們針對服務拆分和遠端呼叫給出兩個理論角色概念:
我們需要注意的是:
下面我們來介紹一種註冊中心EUreka
首先我們需要知道Eureka是什麼:
我們給出一個簡單的圖示展示:
例如上圖:
那麼我們就需要注意到三個問題:
首先我們給出Eureka的具體結構並對其分析:
我們對上圖進行簡單介紹:
那麼我們就可以回答上述問題:
/* 問題1:order-service如何得知user-service範例地址? */
// - user-service服務範例啟動後,將自己的資訊註冊到eureka-server(Eureka伺服器端)。這個叫服務註冊
// - eureka-server儲存服務名稱到服務範例地址列表的對映關係
// - order-service根據服務名稱,拉取範例地址列表。這個叫服務發現或服務拉取
/* 問題2;order-service如何從多個user-service範例中選擇具體的範例? */
// - order-service從範例列表中利用負載均衡演演算法選中一個範例地址
// - 向該範例地址發起遠端呼叫
/* 問題3:order-service如何得知某個user-service範例是否依然健康,是不是已經宕機? */
// - user-service會每隔一段時間(預設30秒)向eureka-server發起請求,報告自己狀態,稱為心跳
// - 當超過一定時間沒有傳送心跳時,eureka-server會認為微服務範例故障,將該範例從服務列表中剔除
// - order-service拉取服務時,就能將故障範例排除了
下面我們逐漸介紹搭建Eureka-server的操作:
<!-- 依賴寫在Eureka-server的pom.xml檔案裡 -->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-eureka-server</artifactId>
</dependency>
package cn.itcast.eureka;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.netflix.eureka.server.EnableEurekaServer;
// 表示啟動Eureka
@EnableEurekaServer
@SpringBootApplication
public class EurekaApplication {
public static void main(String[] args) {
SpringApplication.run(EurekaApplication.class, args);
}
}
server:
port: 10086 # 伺服器埠
spring:
application:
name: eurekaserver # eureka的服務名稱
eureka:
client:
service-url: # eureka的地址資訊
defaultZone: http://127.0.0.1:10086/eureka
接下來我們來進行服務註冊功能:
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<parent>
<artifactId>cloud-demo</artifactId>
<groupId>cn.itcast.demo</groupId>
<version>1.0</version>
</parent>
<modelVersion>4.0.0</modelVersion>
<artifactId>user-service</artifactId>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
</dependency>
<!--mybatis-->
<dependency>
<groupId>org.mybatis.spring.boot</groupId>
<artifactId>mybatis-spring-boot-starter</artifactId>
</dependency>
<!--eureka使用者端依賴-->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
</dependency>
</dependencies>
<build>
<finalName>app</finalName>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>
</project>
spring:
application: # 設定服務名稱
name: userservice
eureka:
client:
service-url: # 設定對應註冊的Rureka地址
defaultZone: http://127.0.0.1:10086/eureka
// 1.複製一份user-service啟動設定(在啟動第一份後在左下角可以找到user-service,右鍵Copy Configuration)
// 2.在複製介面的VMoptions修改埠:-Dserver.port=8082
// 3.啟動即可在Eureka頁面看到兩個user-service
我們在前面的註冊環境已經將兩個user-service設定在同一服務中
那麼我們的order-service如果想要呼叫user-service的介面,我們就需要稍微修改程式碼使其在兩個伺服器中拉取資料:
package cn.itcast.order.service;
import cn.itcast.feign.clients.UserClient;
import cn.itcast.feign.pojo.User;
import cn.itcast.order.mapper.OrderMapper;
import cn.itcast.order.pojo.Order;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
@Service
public class OrderService {
@Autowired
private OrderMapper orderMapper;
@Autowired
private RestTemplate restTemplate;
public Order queryOrderById(Long orderId) {
Order order = orderMapper.findById(orderId);
// url路徑(這裡的路徑直接修改為服務名userservice,使其在相同服務名的伺服器中選擇)
String url = "http://userservice/user/" + order.getUserId();
User user = restTemplate.getForObject(url, User.class);
order.setUser(user);
return order;
}
}
package cn.itcast.order;
import cn.itcast.feign.clients.UserClient;
import cn.itcast.feign.config.DefaultFeignConfiguration;
import org.mybatis.spring.annotation.MapperScan;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.client.loadbalancer.LoadBalanced;
import org.springframework.cloud.openfeign.EnableFeignClients;
import org.springframework.context.annotation.Bean;
import org.springframework.web.client.RestTemplate;
@MapperScan("cn.itcast.order.mapper")
@SpringBootApplication
public class OrderApplication {
public static void main(String[] args) {
SpringApplication.run(OrderApplication.class, args);
}
// @LoadBalanced表示負載均衡
@Bean
@LoadBalanced
public RestTemplate restTemplate() {
return new RestTemplate();
}
}
下面我們來對上述的@Loadbalanced負載均衡進行部分介紹
我們首先給出負載均衡流程圖:
下面我們採用另一張圖來詳細解釋上述圖:
我們對其進行簡單解釋:
我們在上面的解釋中提及到了一個詞彙:
我們給出一張IRule的繼承圖:
我們對其部分規則進行解釋:
內建負載均衡規則類 | 規則描述 |
---|---|
RoundRobinRule | 簡單輪詢服務列表來選擇伺服器。它是Ribbon預設的負載均衡規則。 |
AvailabilityFilteringRule | 對以下兩種伺服器進行忽略: (1)在預設情況下,這臺伺服器如果3次連線失敗,這臺伺服器就會被設定為「短路」狀態。短路狀態將持續30秒,如果再次連線失敗,短路的持續時間就會幾何級地增加。 (2)並行數過高的伺服器。如果一個伺服器的並行連線數過高,設定了AvailabilityFilteringRule規則的使用者端也會將其忽略。並行連線數的上限,可以由使用者端的 |
WeightedResponseTimeRule | 為每一個伺服器賦予一個權重值。伺服器響應時間越長,這個伺服器的權重就越小。這個規則會隨機選擇伺服器,這個權重值會影響伺服器的選擇。 |
ZoneAvoidanceRule | 以區域可用的伺服器為基礎進行伺服器的選擇。使用Zone對伺服器進行分類,這個Zone可以理解為一個機房、一個機架等。而後再對Zone內的多個服務做輪詢。 |
BestAvailableRule | 忽略那些短路的伺服器,並選擇並行數較低的伺服器。 |
RandomRule | 隨機選擇一個可用的伺服器。 |
RetryRule | 重試機制的選擇邏輯 |
其中預設的實現就是ZoneAvoidanceRule,是一種輪詢方案
除此之外我們還可以去自定義實現負載均衡策略,下面我們來介紹兩種實現方法:
@Bean
public IRule randomRule(){
return new RandomRule();
}
userservice: # 給某個微服務設定負載均衡規則,這裡是userservice服務
ribbon:
NFLoadBalancerRuleClassName: com.netflix.loadbalancer.RandomRule # 負載均衡規則
我們的Ribbon預設是採用懶載入,即第一次存取時才會去建立LoadBalanceClient,請求時間會很長。
我們可以採用程式碼去修改使其變為餓漢式載入:
ribbon:
eager-load:
enabled: true # 是否自動載入
clients: userservice # 針對的client
國內公司一般都推崇阿里巴巴的技術,比如註冊中心,SpringCloudAlibaba也推出了一個名為Nacos的註冊中心。
Nacos是阿里巴巴的技術產品了,我們需要先對其進行下載才可使用:
# 跳轉路徑
cd 目錄名
# 啟動startup.cmd
startup.cmd -m standalone
# 這裡注意:下載後預設路徑8848,可以在conf的properties檔案修改port
我們來介紹nacos的服務註冊過程:
<!-- cloud-demo 父工程 (SpringCloudAlibaba依賴)-->
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-alibaba-dependencies</artifactId>
<version>2.2.6.RELEASE</version>
<type>pom</type>
<scope>import</scope>
</dependency>
<!-- user-service order-service子工程 (nacos-discovery)-->
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
</dependency>
<!--注意:nacos和Eureka座標衝突,需要註釋掉Eureka依賴-->
# 在user-service和order-service的application.yml中新增nacos地址:
# 同樣和Eureka衝突,記得註釋掉
spring:
cloud:
nacos:
server-addr: localhost:8848
我們首先給出一張服務分級儲存模型圖並對其解釋:
我們對其進行解釋:
下面我們來介紹如何設定叢集:
spring:
cloud:
nacos:
server-addr: localhost:8848
discovery:
cluster-name: HZ # 叢集名稱
但是我們預設的ZoneAvoidanceRule無法實現同叢集優先負載均衡操作,所以我們需要對其進行設定:
# 修改order-service的application.yml檔案,新增叢集設定:
spring:
cloud:
nacos:
server-addr: localhost:8848
discovery:
cluster-name: HZ # 叢集名稱
# 修改order-service的application.yml檔案,修改負載均衡規則:
userservice:
ribbon:
NFLoadBalancerRuleClassName: com.alibaba.cloud.nacos.ribbon.NacosRule # 負載均衡規則
在我們的Nacos控制系統中的物件都會有一個權重設定:
我們對權重進行簡單解釋:
Nacos提供了namespace來實現環境隔離功能:
首先我們先來了解如何在Nacos中新創namespace:
在新創名稱空間之後,我們如果希望資料上傳到指定名稱空間,需要手動修改部分程式碼:
# 例如我們在order-service的application.yml檔案中進行修改,那麼後面的order服務就會到達新的名稱空間中
spring:
cloud:
nacos:
server-addr: localhost:8848
discovery:
cluster-name: HZ
namespace: 478f4b7c-c1a5-4dee-a7c7-84774766654d # 名稱空間,填ID
Nacos和Eureka整體結構類似,服務註冊、服務拉取、心跳等待,但是也存在一些差異
我們首先給出Eureka的展示圖:
我們再給出Nacos的展示圖:
我們可以發現Nacos相比於Eureka有些許不同之處,首先是臨時範例和非臨時範例:
此外還有Nacos關於服務消費者的區別:
最後我們給出Nacos和Eureka的相同點與不同點
Nacos與Eureka的共同點:
Nacos與Eureka的不同點:
在前面我們學習了Nacos去完成微服務註冊功能,下面我們來學習Nacos的設定管理功能
首先我們先來學習Nacos中統一設定管理的基本內容:
下面我們需要知道一些關於Nacos和application的相關資訊:
下面我們繼續開始統一設定管理的內容:
<!--在使用統一設定的服務下-->
<!--nacos設定管理依賴-->
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-nacos-config</artifactId>
</dependency>
# 注:在userservice服務下
spring:
application:
name: userservice # 服務名稱
profiles:
active: dev #開發環境,這裡是dev
cloud:
nacos:
server-addr: localhost:8848 # Nacos地址
config:
file-extension: yaml # 檔案字尾名
@Slf4j
@RestController
@RequestMapping("/user")
public class UserController {
@Autowired
private UserService userService;
// 這裡採用@Value注入設定資訊,輸出成功證明收到Nacos設定資訊
@Value("${pattern.dateformat}")
private String dateformat;
@GetMapping("now")
public String now(){
return LocalDateTime.now().format(DateTimeFormatter.ofPattern(dateformat));
}
}
我們首先來介紹一下熱更新:
下面我們來介紹兩種方法來實現熱更新:
@Slf4j
@RestController
@RequestMapping("/user")
@RefreshScope // 熱更新註解,使用後Nacos的設定資訊即時更新
public class UserController {
@Autowired
private UserService userService;
@Value("${pattern.dateformat}")
private String dateformat;
@GetMapping("now")
public String now(){
return LocalDateTime.now().format(DateTimeFormatter.ofPattern(dateformat));
}
}
// 設定屬性實體類
@Component
@Data
@ConfigurationProperties(prefix = "pattern") // ConfigurationProperties表示熱更新註解,prefix表示共用字首
public class PatternProperties {
private String dateformat;
}
// 實時使用
@Slf4j
@RestController
@RequestMapping("/user")
public class UserController {
@Autowired
private UserService userService;
@Autowired
private PatternProperties patternProperties;
@GetMapping("now")
public String now(){
return LocalDateTime.now().format(DateTimeFormatter.ofPattern(patternProperties.getDateformat()));
}
// 略
}
我們之前採用了特定環境設定,其具體表示為:
[spring.application.name]-[spring.profiles.active].yaml
,例如:userservice-dev.yaml當我們不使用環境名稱時,其設定就會變為共用設定:
[spring.application.name].yaml
,例如:userservice.yaml我們給出一個簡單的範例:
// 設定屬性實體類
@Component
@Data
@ConfigurationProperties(prefix = "pattern") // ConfigurationProperties表示熱更新註解,prefix表示共用字首
public class PatternProperties {
private String dateformat;
private String envSharedValue;
}
// Controller層
@Slf4j
@RestController
@RequestMapping("/user")
public class UserController {
@Autowired
private UserService userService;
@Autowired
private PatternProperties properties;
@GetMapping("prop")
public PatternProperties properties(){
return properties;
}
}
// 在啟動userService時
// 以Edit Configuration開啟,並在Active profiles中修改名稱以修改環境
最後我們給出設定管理的優先順序展示:
下面我們來介紹一下Feign
首先我們簡單介紹一下Feign:
我們這裡回憶一下RestTemplate的遠端呼叫方法:
package cn.itcast.order.service;
import cn.itcast.feign.clients.UserClient;
import cn.itcast.feign.pojo.User;
import cn.itcast.order.mapper.OrderMapper;
import cn.itcast.order.pojo.Order;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
@Service
public class OrderService {
@Autowired
private OrderMapper orderMapper;
@Autowired
private RestTemplate restTemplate;
public Order queryOrderById(Long orderId) {
Order order = orderMapper.findById(orderId);
// 需要手動書寫url,並且加入id引數
String url = "http://userservice/user/" + order.getUserId();
// 需要呼叫restTemplate的固定方法並指定類class
User user = restTemplate.getForObject(url, User.class);
order.setUser(user);
return order;
}
}
我們下面給出Feign的基本使用:
<!--在order-service的pom檔案中引入Feign依賴-->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-openfeign</artifactId>
</dependency>
package cn.itcast.order;
import cn.itcast.feign.clients.UserClient;
import cn.itcast.feign.config.DefaultFeignConfiguration;
import org.mybatis.spring.annotation.MapperScan;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.client.loadbalancer.LoadBalanced;
import org.springframework.cloud.openfeign.EnableFeignClients;
import org.springframework.context.annotation.Bean;
import org.springframework.web.client.RestTemplate;
@MapperScan("cn.itcast.order.mapper")
@SpringBootApplication
@EnableFeignClients
public class OrderApplication {
public static void main(String[] args) {
SpringApplication.run(OrderApplication.class, args);
}
}
package cn.itcast.order.client;
import cn.itcast.order.pojo.User;
import org.springframework.cloud.openfeign.FeignClient;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
// 整體就是SpringMVC的REST風格
// @FeignClient類似RequestMapping,後面跟上具體的服務名
@FeignClient("userservice")
public interface UserClient {
// 這裡就是具體的方法,採用REST
@GetMapping("/user/{id}")
User findById(@PathVariable("id") Long id);
}
/*
這個使用者端主要是基於SpringMVC的註解來宣告遠端呼叫的資訊,比如:
- 服務名稱:userservice
- 請求方式:GET
- 請求路徑:/user/{id}
- 請求引數:Long id
- 返回值型別:User
*/
package cn.itcast.order.service;
import cn.itcast.feign.clients.UserClient;
import cn.itcast.feign.pojo.User;
import cn.itcast.order.mapper.OrderMapper;
import cn.itcast.order.pojo.Order;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
@Service
public class OrderService {
@Autowired
private OrderMapper orderMapper;
@Autowired
private UserClient userClient;
public Order queryOrderById(Long orderId) {
// 1.查詢訂單
Order order = orderMapper.findById(orderId);
// 2.用Feign遠端呼叫
User user = userClient.findById(order.getUserId());
// 3.封裝user到Order
order.setUser(user);
// 4.返回
return order;
}
}
下面我們來介紹一下Feign的自定義設定:
型別 | 作用 | 說明 |
---|---|---|
feign.Logger.Level | 修改紀錄檔級別 | 包含四種不同的級別:NONE、BASIC、HEADERS、FULL |
feign.codec.Decoder | 響應結果的解析器 | http遠端呼叫的結果做解析,例如解析json字串為java物件 |
feign.codec.Encoder | 請求引數編碼 | 將請求引數編碼,便於通過http請求傳送 |
feign. Contract | 支援的註解格式 | 預設是SpringMVC的註解 |
feign. Retryer | 失敗重試機制 | 請求失敗的重試機制,預設是沒有,不過會使用Ribbon的重試 |
大部分內容我們只需要使用預設就足以滿足我們日常需求了
我們簡單介紹一下Logger紀錄檔級別:
紀錄檔大體分為四種
NONE:不記錄任何紀錄檔資訊,這是預設值
BASIC:僅記錄請求的方法,URL以及響應狀態碼和執行時間
HEADERS:在BASIC的基礎上,額外記錄了請求和響應的頭資訊
FULL:記錄所有請求和響應的明細,包括頭資訊、請求體、後設資料
我們給出兩種修改預設設定的方法:
# 修改yaml組態檔
# 可以針對某個微服務修改
feign:
client:
config:
userservice: # 針對某個微服務的設定
loggerLevel: FULL # 紀錄檔級別
# 也可以針對全部微服務修改
feign:
client:
config:
default: # 這裡用default就是全域性設定,如果是寫服務名稱,則是針對某個微服務的設定
loggerLevel: FULL # 紀錄檔級別
// 宣告一個類,然後宣告一個Logger.Level的物件
public class DefaultFeignConfiguration {
@Bean
public Logger.Level feignLogLevel(){
return Logger.Level.BASIC; // 紀錄檔級別為BASIC
}
}
// 此外我們還需要將該類設定給該服務:
// 如果要全域性生效,將其放到啟動類的@EnableFeignClients這個註解中
@EnableFeignClients(defaultConfiguration = DefaultFeignConfiguration .class)
// 如果是區域性生效,則把它放到對應的@FeignClient這個註解中
@FeignClient(value = "userservice", configuration = DefaultFeignConfiguration .class)
Feign底層發起http請求,依賴於其它的框架,我們這裡給出一些底層框架:
URLConnection:預設實現,不支援連線池
Apache HttpClient :支援連線池
OKHttp:支援連線池
因此提高Feign的效能主要手段就是使用連線池代替預設的URLConnection。
我們給出使用連線池的範例:
<!--我們這裡以Apache HttpClient為例-->
<!--httpClient的依賴 -->
<dependency>
<groupId>io.github.openfeign</groupId>
<artifactId>feign-httpclient</artifactId>
</dependency>
# 在對應的yml檔案中設定連線池資訊(這裡就是order-service服務)
feign:
httpclient:
enabled: true # 開啟feign對HttpClient的支援
max-connections: 200 # 最大的連線數
max-connections-per-route: 50 # 每個路徑的最大連線數
我們可以發現Feign實際上和Controller的程式碼十分相似:
// Feign
@FeignClient("userservice")
public interface UserClient {
// 這裡就是具體的方法,採用REST
@GetMapping("/user/{id}")
User findById(@PathVariable("id") Long id);
}
// Controller
@Slf4j
@RestController
@RequestMapping("/user")
public class UserController {
@Autowired
private UserService userService;
@GetMapping("/{id}")
public User queryById(@PathVariable("id") Long id,
@RequestHeader(value = "Truth", required = false) String truth) {
System.out.println("truth: " + truth);
return userService.queryById(id);
}
}
我們給出一種抽取方法來減少相同程式碼的書寫:
將Feign的Client抽取為獨立模組,並且把介面有關的POJO、預設的Feign設定都放到這個模組中,提供給所有消費者使用。
例如,將UserClient、User、Feign的預設設定都抽取到一個feign-api包中,所有微服務參照該依賴包,即可直接使用。
我們給出具體範例:
<!--Feign依賴-->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-openfeign</artifactId>
</dependency>
<!--首先刪除掉order-service的全部Feign相關類和DefaultFeignConfiguration等設定-->
<!--匯入我們編寫的feign-api類-->
<dependency>
<groupId>cn.itcast.demo</groupId>
<artifactId>feign-api</artifactId>
<version>1.0</version>
</dependency>
<!--修改order-service中的所有與上述三個元件有關的導包部分,改成匯入feign-api中的包-->
/*
由於UserClient現在在cn.itcast.feign.clients包下,
而order-service的@EnableFeignClients註解是在cn.itcast.order包下,不在同一個包,無法掃描到UserClient
所以我們需要手動掃描包,其中可以採用兩種方法:
- 指定Feign應該掃描的包:@EnableFeignClients(basePackages = "cn.itcast.feign.clients")
- 指定需要載入的Client介面:@EnableFeignClients(clients = {UserClient.class})
*/
@MapperScan("cn.itcast.order.mapper")
@SpringBootApplication
@EnableFeignClients(clients = UserClient.class)
public class OrderApplication {
public static void main(String[] args) {
SpringApplication.run(OrderApplication.class, args);
}
}
最後我們來介紹一下GateWay服務閘道器
我們首先介紹一下GateWay:
我們給出一張GateWay的示意圖:
其中GateWay大致存在三種主要用途:
關於閘道器大致包括兩種:
下面我們通過一個簡單的案例來介紹GateWay的基本使用:
<!--在parent-demo中單獨建立gateway-demo模組,並加入以下閘道器依賴-->
<!--閘道器-->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-gateway</artifactId>
</dependency>
<!--nacos服務發現依賴-->
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
</dependency>
package cn.itcast.gateway;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
@SpringBootApplication
public class GatewayApplication {
public static void main(String[] args) {
SpringApplication.run(GatewayApplication.class, args);
}
}
# 路由設定包括
# 1. 路由id:路由的唯一標示
# 2. 路由目標(uri):路由的目標地址,http代表固定地址,lb代表根據服務名負載均衡
# 3. 路由斷言(predicates):判斷路由的規則,
# 4. 路由過濾器(filters):對請求或響應做處理
# 我們將符合`Path` 規則的一切請求,都代理到 `uri`引數指定的地址。
# 本例中,我們將 `/user/**`開頭的請求,代理到`lb://userservice`,lb是負載均衡,根據服務名拉取服務列表,實現負載均衡。
server:
port: 10010 # 閘道器埠
spring:
application:
name: gateway # 服務名稱
cloud:
nacos:
server-addr: localhost:8848 # nacos地址
gateway:
routes: # 閘道器路由設定
- id: user-service # 路由id,自定義,只要唯一即可
# uri: http://127.0.0.1:8081 # 路由的目標地址 http就是固定地址
uri: lb://userservice # 路由的目標地址 lb就是負載均衡,後面跟服務名稱
predicates: # 路由斷言,也就是判斷請求是否符合路由規則的條件
- Path=/user/** # 這個是按照路徑匹配,只要以/user/開頭就符合要求
/*
GateWay閘道器的port是10010
所以我們存取http://localhost:10010/user/1時,符合`/user/**`規則,請求轉發到uri:http://userservice/user/1
*/
最後我們給出一張閘道器過程展示圖:
下面我們來介紹一下斷言:
我們下面給出幾個簡單的斷言工廠(我們目前只需要PATH斷言工廠即可):
名稱 | 說明 | 範例 |
---|---|---|
After | 是某個時間點後的請求 | - After=2037-01-20T17:42:47.789-07:00[America/Denver] |
Before | 是某個時間點之前的請求 | - Before=2031-04-13T15:14:47.433+08:00[Asia/Shanghai] |
Between | 是某兩個時間點之前的請求 | - Between=2037-01-20T17:42:47.789-07:00[America/Denver], 2037-01-21T17:42:47.789-07:00[America/Denver] |
Cookie | 請求必須包含某些cookie | - Cookie=chocolate, ch.p |
Header | 請求必須包含某些header | - Header=X-Request-Id, \d+ |
Host | 請求必須是存取某個host(域名) | - Host=.somehost.org,.anotherhost.org |
Method | 請求方式必須是指定方式 | - Method=GET,POST |
Path | 請求路徑必須符合指定規則 | - Path=/red/{segment},/blue/** |
Query | 請求引數必須包含指定引數 | - Query=name, Jack或者- Query=name |
RemoteAddr | 請求者的ip必須是指定範圍 | - RemoteAddr=192.168.1.1/24 |
Weight | 權重處理 |
我們先簡單介紹一下GateWay過濾器:
我們給出一張GateWay過濾器展示圖:
其中Spring提供了31種過濾器,這裡僅僅介紹幾種:
名稱 | 說明 |
---|---|
AddRequestHeader | 給當前請求新增一個請求頭 |
RemoveRequestHeader | 移除請求中的一個請求頭 |
AddResponseHeader | 給響應結果中新增一個響應頭 |
RemoveResponseHeader | 從響應結果中移除有一個響應頭 |
RequestRateLimiter | 限制請求的流量 |
然後我們給出過濾器的使用方法:
# 在yaml中進行過濾器設定,我們可以通過各種過濾器達到不同目的,例如新增請求頭AddRequestHeader
# 我們可以採用服務uri路由名稱單獨給某個微服務設定過濾器
spring:
cloud:
gateway:
routes:
- id: user-service
uri: lb://userservice
predicates:
- Path=/user/**
filters: # 過濾器
- AddRequestHeader=Truth, Itcast is freaking awesome! # 新增請求頭
# 我們也可以採用全域性過濾器對所有微服務進行過濾(default-filters)
spring:
cloud:
gateway:
routes:
- id: user-service
uri: lb://userservice
predicates:
- Path=/user/**
default-filters: # 預設過濾項
- AddRequestHeader=Truth, Itcast is freaking awesome!
我們在前面學習了過濾器工廠,但是過濾器工廠只能實現已經設計好的方法
如果我們希望攔截業務來完成自己的功能增強或攔截,我們就需要設計過濾器:
全域性過濾器的底層原理是實現了GlobalFilter介面:
public interface GlobalFilter {
/**
* 處理當前請求,有必要的話通過{@link GatewayFilterChain}將請求交給下一個過濾器處理
*
* @param exchange 請求上下文,裡面可以獲取Request、Response等資訊
* @param chain 用來把請求委託給下一個過濾器
* @return {@code Mono<Void>} 返回標示當前過濾器業務結束
*/
Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain);
}
我們給出一個簡單的業務邏輯:
/*
定義全域性過濾器,攔截請求,判斷請求的引數是否滿足下面條件:
- 引數中是否有authorization,
- authorization引數值是否為admin
*/
package cn.itcast.gateway.filters;
import org.springframework.cloud.gateway.filter.GatewayFilterChain;
import org.springframework.cloud.gateway.filter.GlobalFilter;
import org.springframework.core.annotation.Order;
import org.springframework.http.HttpStatus;
import org.springframework.stereotype.Component;
import org.springframework.web.server.ServerWebExchange;
import reactor.core.publisher.Mono;
@Order(-1) // Order表示執行優先順序,越小優先順序越高
@Component
public class AuthorizeFilter implements GlobalFilter {
@Override
public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
// 1.獲取請求引數
MultiValueMap<String, String> params = exchange.getRequest().getQueryParams();
// 2.獲取authorization引數
String auth = params.getFirst("authorization");
// 3.校驗
if ("admin".equals(auth)) {
// 放行
return chain.filter(exchange);
}
// 4.攔截
// 4.1.禁止存取,設定狀態碼
exchange.getResponse().setStatusCode(HttpStatus.FORBIDDEN);
// 4.2.結束處理
return exchange.getResponse().setComplete();
}
}
目前我們已經接觸了三種過濾器:
最後我們需要思考GateWay過濾器的整體優先順序:
這篇文章中介紹了SpringCloud的整體框架及其知識點,屬於微服務的入門內容,下面我們會繼續學習微服務內容~
該文章屬於學習內容,具體參考B站黑馬程式設計師的SpringCloud課程
這裡附上視訊連結:微服務技術棧導學1_嗶哩嗶哩_bilibili