背景
上篇文章 記錄多專案共用一個公眾號邏輯修改, 實現了多個專案共用一個公眾號。
但是也存在幾點問題,比如:
- 中間伺服器攔截了微信的請求,雖然方便了專案不再需要寫微信授權的程式碼,但如果以後需要再擴充新的微信請求,就需要再去中間伺服器寫api
- 需要搭建前臺,手動維護各個專案的地址和key。
我們可以讓中間伺服器只是簡單地轉發請求,不再攔截,解決第一點問題。
同時利用 服務發現與註冊 解決第二個問題。
用服務註冊的好處就是,我們不需要再手動地維護專案資訊、各個專案的ip,只需要讓新來的專案向服務中心註冊一下就好了。
服務發現與註冊
服務註冊是指向服務註冊中心註冊一個服務例項,服務提供者將自己的服務資訊(如IP地址等)告知服務註冊中心。
服務發現是指當服務消費者需要消費另外一個服務時, 服務註冊中心能夠告知服務消費者它所要消費服務的例項資訊(如服務名、IP 地址等)。
通常情況下,一個服務既是服務提供者,也是服務消費者。
所以,如下圖方式,每個專案都向註冊中心註冊,就不需要建表維護各個專案的ip地址了。
常見的註冊中心
能夠實現服務註冊的元件很多,比如ZooKeeper、Eureka、Consul、Nacos等。
這裡選用比較常見的 Eureka。
Eureka
Eureka是Netflix開發的服務發現框架,SpringCloud將它整合在自己的子專案spring-cloud-netflix中, 實現SpringCloud的服務發現功能。
上圖簡要描述了Eureka的基本架構,由3個角色組成:
- Eureka Server 提供服務註冊和發現
- Service Provider 服務提供方 將自身服務註冊到Eureka,從而使服務消費方能夠找到
- 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實現服務的註冊與發現
角色如下
註冊中心
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 檢視兩者對應的版本
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, 即該專案本身。
服務註冊到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 專案成功註冊。
那麼註冊成功之後,我們怎麼獲取呢?
獲取服務
獲取所有服務:
如果註冊中心想獲取所有服務可以使用 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來獲取一個服務,那麼作為一個請求轉發中心,怎麼知道一個請求要轉發給哪個服務呢?
上一篇文章實現第三方登陸:微信掃碼登入 (spring boot) 瞭解到,請求微信伺服器需要生成一個場景值, 並且微信返回的請求 也會返回這個場景值。
所以,我們只要簡單地在場景值前面加一個服務的關鍵字就可以了.邏輯如下
註冊中心轉發請求邏輯
接收微信請求後,轉發請求的邏輯如下:
- 獲取請求源客戶端
- 設定轉發請求的引數,轉發給客戶端
- 獲取客戶端響應資料
- 返回響應資料給微信
程式碼如下:
/**
* 當設定完微信公眾號的介面後,微信會把使用者傳送的訊息,掃碼事件等推送過來
*
* @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();
}
效果圖和以前一樣:
原始碼:https://github.com/weiweiyi189/weChatServiceCenter
參考:
https://juejin.cn/post/6910031138048180238
https://blog.51cto.com/u_10401840/5179710
https://juejin.cn/post/6910031138048180238#heading-8