簡單使用spring cloud 服務註冊做一個請求轉發中心

weiweiyi發表於2023-04-08

背景

上篇文章 記錄多專案共用一個公眾號邏輯修改, 實現了多個專案共用一個公眾號。
但是也存在幾點問題,比如:

  • 中間伺服器攔截了微信的請求,雖然方便了專案不再需要寫微信授權的程式碼,但如果以後需要再擴充新的微信請求,就需要再去中間伺服器寫api
  • 需要搭建前臺,手動維護各個專案的地址和key。

我們可以讓中間伺服器只是簡單地轉發請求,不再攔截,解決第一點問題。

同時利用 服務發現與註冊 解決第二個問題。

用服務註冊的好處就是,我們不需要再手動地維護專案資訊、各個專案的ip,只需要讓新來的專案向服務中心註冊一下就好了。

服務發現與註冊

服務註冊是指向服務註冊中心註冊一個服務例項,服務提供者將自己的服務資訊(如IP地址等)告知服務註冊中心。

image.png

服務發現是指當服務消費者需要消費另外一個服務時, 服務註冊中心能夠告知服務消費者它所要消費服務的例項資訊(如服務名、IP 地址等)。

通常情況下,一個服務既是服務提供者,也是服務消費者。

所以,如下圖方式,每個專案都向註冊中心註冊,就不需要建表維護各個專案的ip地址了。

image.png

常見的註冊中心

能夠實現服務註冊的元件很多,比如ZooKeeper、Eureka、Consul、Nacos等。

image.png

這裡選用比較常見的 Eureka。

Eureka

Eureka是Netflix開發的服務發現框架,SpringCloud將它整合在自己的子專案spring-cloud-netflix中, 實現SpringCloud的服務發現功能。

image.png

上圖簡要描述了Eureka的基本架構,由3個角色組成:

  1. Eureka Server 提供服務註冊和發現
  2. Service Provider 服務提供方 將自身服務註冊到Eureka,從而使服務消費方能夠找到
  3. Service Consumer 服務消費方 從Eureka獲取註冊服務列表,從而能夠消費服務

Eureka基本概念

Register——服務註冊

當 Eureka Client 向 Eureka Server 註冊時,Eureka Client 提供自身的後設資料,比如 IP 地址、 埠、執行狀況指標的 Url、主頁地址等資訊。

Renew——服務續約

  Eureka Client 在預設的情況下會每隔 30 秒傳送一次心跳來進行服務續約。透過服務續約 來告知 Eureka Server 該 Eureka Client 仍然可用,沒有出現故障。正常情況下,如果 Eureka Server 在 90 秒內沒有收到 Eureka Client 的心跳,Eureka Server 會將 Eureka Client 例項從註冊列表中刪除。注意:官網建議不要更改服務續約的間隔時間。

Eviction——服務剔除

在預設情況下(當然我們可以修改) ,當 Eureka Client 連續 90 秒沒有向 Eureka Server 傳送服務續約(即心跳) 時,Eureka Server 會將該服務例項從服務註冊列表刪除,即服務剔除。

Fetch Registries——獲取服務註冊列表資訊

   
Eureka Client 從 Eureka Server 獲取服務登錄檔資訊,並將其快取在本地。Eureka Client 會使用服務註冊列表資訊查詢其他服務的資訊,從而進行遠端呼叫。該註冊列表資訊定時(每 30 秒)更新一次,每次返回註冊列表資訊可能與 Eureka Client 的快取資訊不同,Eureka Client 會自己處理這些資訊。如果由於某種原因導致註冊列表資訊不能及時匹配,Eureka Client 會重新獲取整個登錄檔資訊。

下面就來用Eureka實現服務的註冊與發現

角色如下

image.png

註冊中心

1.引入依賴

<properties>
        <java.version>1.8</java.version>
        <spring-cloud.version>2021.0.5</spring-cloud.version>
    </properties>

    <dependencies>
        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-netflix-eureka-server</artifactId>
            <version>2.2.10.RELEASE</version>
        </dependency>

<dependencyManagement>
        <dependencies>
            <dependency>
                <groupId>org.springframework.cloud</groupId>
                <artifactId>spring-cloud-dependencies</artifactId>
                <version>2021.0.5</version>
                <type>pom</type>
                <scope>import</scope>
            </dependency>
        </dependencies>
    </dependencyManagement>

這裡有個比較深的坑,就是spring cloud的版本一定要和spring boot的版本相對應.

否則就會報異常

java.lang.ClassNotFoundException: org.springframework.boot.context.properties.ConfigurationBeanFactoryMetadata

首先 pom.xml 裡面是否有 dependencyManagement 版本管理的設定,因為這塊是會識別並載入所需要的依賴版本,比如我要載入 spring-cloud-starter-netflix-eureka-client ,首先確定好你的 SpringBoot 版本是否相容依賴的 SpringCloud 版本。

其次是否設定了 spring-cloud.version ,接著確認是否設定了 dependencyManagement 下面的 spring-cloud-dependencies 依賴,最後確認好要載入的 spring-cloud-starter-netflix-eureka-client ,這樣最終保證你所需要的依賴包能夠爭取無誤的載入下來。

可以去https://spring.io/projects/spring-cloud 檢視兩者對應的版本

image.png

2.配置application.yml(全域性配置)

spring:
  application:
    name: wechat-service #配置註冊的名字

server:
  port: 8761

eureka:
  client:
    register-with-eureka: true #是否將自己註冊到eureka-server中
    fetch-registry: true #是否從eureka-server中獲取服務註冊資訊
    service-url:
      defaultZone: http://localhost:${server.port}/eureka/  #設定服務註冊中心地址
  datacenter: cloud
  environment: product

3. 配置啟動類

@SpringBootApplication
@EnableEurekaServer
public class WechatServiceApplication {

    public static void main(String[] args) {
        SpringApplication.run(WechatServiceApplication.class, args);
    }

}

之後開啟 localhost:8761, 看到如下介面說明註冊中心啟動成功, 可以看到目前註冊了一個服務 wechat-service, 即該專案本身。
image.png

服務註冊到Eureka註冊中心

另起一個spring boot專案作為服務。

1.引入依賴

<properties>
  <java.version>1.8</java.version>
  <spring-cloud.version>2020.0.4</spring-cloud.version>
</properties>

<dependencyManagement>
        <dependencies>
            <dependency>
                <groupId>org.springframework.cloud</groupId>
                <artifactId>spring-cloud-dependencies</artifactId>
                <version>${spring-cloud.version}</version>
                <type>pom</type>
                <scope>import</scope>
            </dependency>
        </dependencies>
</dependencyManagement>
<dependencies>
  <dependency>
      <groupId>org.springframework.cloud</groupId>
      <artifactId>spring-cloud-dependencies</artifactId>
      <version>Hoxton.SR8</version>
      <type>pom</type>
      <scope>import</scope>
  </dependency>

  <dependency>
            <groupId>org.springframework.cloud</groupId>
            <version>2.2.5.RELEASE</version>
            <artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
        </dependency>
</dependencies>

這裡也要注意spring boot的版本和 spring cloud的對應。

2.配置 application.yml 檔案

spring:
  application:
    name: schedule  # 註冊名稱

eureka:
  client:
    serviceUrl:
      defaultZone: http://localhost:8761/eureka/  #註冊到 eureka
  instance:
    preferIpAddress: true

3.修改啟動類

新增 @EnableEurekaClient 註解

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

之後啟動該專案, 然後去註冊中心檢視,可以看到 schedule 專案成功註冊。

image.png

那麼註冊成功之後,我們怎麼獲取呢?

獲取服務

獲取所有服務:

如果註冊中心想獲取所有服務可以使用 eurekaClient 提供的 getRegisteredApplications 方法獲取所有服務。

@Controller
@RequestMapping("/eurekacenter")
public class EuServiceController {

  @Autowired
  private EurekaClient eurekaClient;

  @GetMapping("/services")
  @ResponseBody
  public List<Application> getServices() {
    return eurekaClient.getApplications().getRegisteredApplications();
  }
}

獲取指定服務:

一般來說,我們更常用的是獲取某一個服務。可以使用 discoveryClient.getInstances("服務的name"),來獲取一個服務。

@Autowired
private DiscoveryClient discoveryClient;

// 根據服務名稱獲取服務
List<ServiceInstance> serviceInstances = discoveryClient.getInstances(
"wechat-service");
if (CollectionUtils.isEmpty(serviceInstances)) {
  return null;
}
ServiceInstance si = serviceInstances.get(0);

String requestUrl = "http://" + si.getHost() + ":" + si.getPort() + "/request/getQrCode";

轉發請求功能實現

現在我們知道了,可以根據服務的name來獲取一個服務,那麼作為一個請求轉發中心,怎麼知道一個請求要轉發給哪個服務呢?

image.png

上一篇文章實現第三方登陸:微信掃碼登入 (spring boot) 瞭解到,請求微信伺服器需要生成一個場景值, 並且微信返回的請求 也會返回這個場景值。

image.png

所以,我們只要簡單地在場景值前面加一個服務的關鍵字就可以了.邏輯如下

image.png

註冊中心轉發請求邏輯

接收微信請求後,轉發請求的邏輯如下:

  1. 獲取請求源客戶端
  2. 設定轉發請求的引數,轉發給客戶端
  3. 獲取客戶端響應資料
  4. 返回響應資料給微信

程式碼如下:

/**
   * 當設定完微信公眾號的介面後,微信會把使用者傳送的訊息,掃碼事件等推送過來
   *
   * @param signature 微信加密簽名,signature結合了開發者填寫的 token 引數和請求中的 timestamp 引數、nonce引數。
   * @param encType 加密型別(暫未啟用加密訊息)
   * @param msgSignature 加密的訊息
   * @param timestamp 時間戳
   * @param nonce 隨機數
   * @throws IOException
   */
  @PostMapping(produces = "text/xml; charset=UTF-8")
  public void api(HttpServletRequest httpServletRequest,
                  HttpServletResponse httpServletResponse,
                  @RequestParam("signature") String signature,
                  @RequestParam(name = "encrypt_type", required = false) String encType,
                  @RequestParam(name = "msg_signature", required = false) String msgSignature,
                  @RequestParam("timestamp") String timestamp,
                  @RequestParam("nonce") String nonce) throws IOException {
    if (!this.weChatMpService.checkSignature(timestamp, nonce, signature)) {
      this.logger.warn("接收到了未透過校驗的微信訊息,這可能是token配置錯了,或是接收了非微信官方的請求");
      return;
    }

    // 獲取客戶端url
    String targetUrl = this.wechatService.selectClientUrl(httpServletRequest);

    UriComponentsBuilder builder = UriComponentsBuilder.fromUriString(targetUrl + RequestUrl)
        .queryParam("signature", signature)
        .queryParam("timestamp", timestamp)
        .queryParam("nonce", nonce);

    if (msgSignature != null) {
      builder.queryParam("msg_signature", msgSignature);
    }
    if (encType != null) {
      builder.queryParam("encrypt_type", encType);
    }

    URI uri = builder.build().encode().toUri();
    // 設定轉發請求的引數
    HttpURLConnection connection = (HttpURLConnection) uri.toURL().openConnection();
    connection.setRequestMethod("POST");
    connection.setRequestProperty("Content-Type", "text/xml; charset=UTF-8");
    connection.setDoOutput(true);
    connection.setDoInput(true);
    if (msgSignature != null) {
      connection.setRequestProperty("msg_signature", msgSignature);
    }
    if (encType != null) {
      connection.setRequestProperty("encrypt_type", encType);
    }

    // 將 httpServletRequest 中的資料寫入 轉發請求中
    OutputStream outputStream = connection.getOutputStream();
    String requestBody =  new RequestWrapper(httpServletRequest).getBodyString();
    outputStream.write(requestBody.getBytes(StandardCharsets.UTF_8));
    outputStream.flush();
    outputStream.close();

    // 獲取client 的響應資料
    InputStream inputStream = connection.getInputStream();
    ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream();
    byte[] buffer = new byte[1024];
    int bytesRead;
    while ((bytesRead = inputStream.read(buffer)) != -1) {
      byteArrayOutputStream.write(buffer, 0, bytesRead);
    }
    byte[] responseBytes = byteArrayOutputStream.toByteArray();
    inputStream.close();

    // 將響應資料返回給微信
    httpServletResponse.setContentType("text/xml; charset=UTF-8");
    httpServletResponse.setContentLength(responseBytes.length);
    httpServletResponse.getOutputStream().write(responseBytes);
    httpServletResponse.getOutputStream().flush();
    httpServletResponse.getOutputStream().close();
  }

效果圖和以前一樣:
image.png

原始碼:https://github.com/weiweiyi189/weChatServiceCenter

參考:

https://juejin.cn/post/6910031138048180238
https://blog.51cto.com/u_10401840/5179710
https://juejin.cn/post/6910031138048180238#heading-8

相關文章