在Spring Boot實作簡單的webhooks。
範例環境:
- Java 17
- Spring Boot 2.6.7
- Lombok
事前要求
參考「IntelliJ IDEA Community 建立Spring Boot專案教學」建立Spring Boot專案。
application.properties
#context path
server.servlet.context-path=/demo
#port
server.port=8080
注意本範例server的webhooks及註冊webhooks的client皆在同一個Spring Boot專案。
Server Webhhooks
Webhooks需要:
- 提供client註冊/訂閱webhooks的API endpoint
- 儲存client註冊webhooks的URL。
- 提供client設計接收webhooks發送請求的資料格式
建立WebhooksController
如下。POST|/register
用來給client註冊webhooks的URL。註冊時須提供名稱及URL的JSON request body,由WebhooksRegisterRequest
接收然後轉交WebhooksService
把註冊資料儲存起來,並返回webhooks的請求格式yaml文件給client。
GET|/register
可取得client註冊webhooks的URL清單。
WebhooksController.java
package com.abc.demo.controller;
import com.abc.demo.controller.request.WebhooksRegisterRequest;
import com.abc.demo.service.WebhooksService;
import org.apache.commons.io.IOUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.core.io.Resource;
import org.springframework.http.MediaType;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RestController;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.util.List;
@RestController
public class WebhooksController {
@Autowired
private WebhooksService webhooksService;
@Value("classpath:static/webhooks/events.yaml")
private Resource eventsYaml;
@PostMapping(value = "/register", produces = "text/yaml")
public String register(@RequestBody WebhooksRegisterRequest webhooksRegisterRequest) throws IOException {
webhooksService.save(webhooksRegisterRequest);
return IOUtils.toString(eventsYaml.getInputStream(), StandardCharsets.UTF_8);
}
@GetMapping(value = "/register", produces = MediaType.APPLICATION_JSON_VALUE)
public List<String> getUrlList() {
return webhooksService.getRegisteredUrlList();
}
}
WebhooksRegisterRequest
接收client註冊webhooks的JSON資料。
WebhooksRegisterRequest.java
package com.abc.demo.controller.request;
import lombok.Data;
@Data
public class WebhooksRegisterRequest {
private String name;
private String url;
}
events.yaml
為client呼叫POST|/register
註冊webhooks成功時返回的webhooks發送請求格式文件。格式為OpenAPI,可以Swagger Editor開啟。
events.yaml
openapi: 3.0.0
info:
contact:
name: "菜鳥工程師肉豬-Spring Boot Webhooks簡單實作"
url: "https://matthung0807.blogspot.com/2022/04/spring-boot-webhooks-simple-impl.html"
title: "Webhooks Events API"
version: "1.0.0"
paths:
"/hello":
post:
description: Hello
requestBody:
content:
application/json:
schema:
$ref: "#/components/schemas/Hello"
required: true
responses:
"200":
description: Success
content:
text/plain:
schema:
type: string
example: success
tags:
- Hello
components:
schemas:
Hello:
type: object
required:
- name
properties:
name:
type: string
WebhooksService
負責把WebhooksController
送來的client註冊請求轉為WebhooksRegisterDto
儲存在模擬資料庫的registeredMap
成員變數中。
getRegisteredDtoList()
方法從registeredMap
取得client註冊webhooks的資料用以在觸發webhooks時將請求送給client註冊的URL。
getRegisteredUrlList()
方法取得client註冊webhooks的url清單。
WebhooksService.java
package com.abc.demo.service;
import com.abc.demo.controller.request.WebhooksRegisterRequest;
import com.abc.demo.dto.WebhooksRegisterDto;
import org.springframework.stereotype.Service;
import java.time.LocalDateTime;
import java.util.List;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
@Service
public class WebhooksService {
private final Map<String, WebhooksRegisterDto> registeredMap = new ConcurrentHashMap<>();
public void save(WebhooksRegisterRequest webhooksRegisterRequest) {
var url = webhooksRegisterRequest.getUrl();
registeredMap.put(url, new WebhooksRegisterDto(
webhooksRegisterRequest.getName(), url, LocalDateTime.now()));
}
public List<WebhooksRegisterDto> getRegisteredDtoList() {
return registeredMap.values().stream().toList();
}
public List<String> getRegisteredUrlList() {
return registeredMap.values().stream()
.toList().stream()
.map(WebhooksRegisterDto::getUrl).toList();
}
}
WebhooksRegisterDto
為server儲存client註冊webhooks資料的資料物件。
WebhooksRegisterDto.java
package com.abc.demo.dto;
import lombok.AllArgsConstructor;
import lombok.Data;
import java.time.LocalDateTime;
@Data
@AllArgsConstructor
public class WebhooksRegisterDto {
private String name;
private String url;
private LocalDateTime createdAt;
}
GreetingController
為server的一簡單服務,執行時會以HttpClient
發送請求給client註冊的URL,發送的請求JSON格式即為client註冊webhooks成功時返回的yaml文件。
GreetingController.java
package com.abc.demo.controller;
import com.abc.demo.dto.WebhooksRegisterDto;
import com.abc.demo.service.WebhooksService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.MediaType;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;
import javax.net.ssl.SSLContext;
import javax.net.ssl.TrustManager;
import javax.net.ssl.X509TrustManager;
import java.net.URI;
import java.net.http.HttpClient;
import java.net.http.HttpRequest;
import java.net.http.HttpResponse;
import java.security.KeyManagementException;
import java.security.NoSuchAlgorithmException;
import java.security.SecureRandom;
import java.security.cert.X509Certificate;
import java.time.Duration;
@RestController
public class GreetingController {
@Autowired
private WebhooksService webhooksService;
@GetMapping("/greeting")
public void greeting() throws Exception {
System.out.println("Good day");
sendEvent();
}
private void sendEvent() throws Exception {
var registeredDtoList = webhooksService.getRegisteredDtoList();
if (registeredDtoList.isEmpty()) {
System.out.println("webhooks has no registered urls");
return;
}
var httpClient = HttpClient.newBuilder()
.version(HttpClient.Version.HTTP_1_1)
.connectTimeout(Duration.ofSeconds(3))
.sslContext(disabledSSLContext()) // disable SSL verify
.build();
for (WebhooksRegisterDto dto : registeredDtoList) {
HttpRequest request = HttpRequest.newBuilder()
.uri(URI.create(dto.getUrl()))
.POST(HttpRequest.BodyPublishers.ofString("{\"name\": \"" + dto.getName() + "\"}"))
.header("Content-Type", MediaType.APPLICATION_JSON_VALUE)
.build();
var response = httpClient.send(
request, HttpResponse.BodyHandlers.ofString());
if ("success".equals(response.body())) {
System.out.println("send event success");
}
}
}
private static SSLContext disabledSSLContext() throws KeyManagementException, NoSuchAlgorithmException {
SSLContext sslContext = SSLContext.getInstance("TLS");
sslContext.init(
null,
new TrustManager[]{
new X509TrustManager() {
public X509Certificate[] getAcceptedIssuers() {
return null;
}
public void checkClientTrusted(X509Certificate[] certs, String authType) {
}
public void checkServerTrusted(X509Certificate[] certs, String authType) {
}
}
},
new SecureRandom()
);
return sslContext;
}
}
Client
HelloController
的POST|/hello
為向server註冊的URL,當server的webhooks觸發時會呼叫此endpoints。接收的請求資料HelloRequest
格式為client註冊webhooks成功時返回的yaml文件。
HelloController.java
package com.abc.demo.controller;
import com.abc.demo.controller.request.HelloRequest;
import org.springframework.http.MediaType;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RestController;
@RestController
public class HelloController {
@PostMapping(value = "/hello", produces = MediaType.TEXT_PLAIN_VALUE)
public String hello(@RequestBody HelloRequest helloRequest) {
System.out.println("Hello " + helloRequest.getName());
return "success";
}
}
HelloRequest
為接收webhooks請求的資料物件。
HelloRequest.java
package com.abc.demo.controller.request;
import lombok.Data;
@Data
public class HelloRequest {
private String name;
}
測試
完成以上server webhooks及client後,接著以client的角度測試webhooks的效果。
啟動專案。使用curl呼叫server的POST|/register
註冊webhooks並以JSON提供註冊的名稱及URL http://localhost:8080/demo/hello
。成功後返回webhooks的請求格式文件。
$ curl -X POST "http://localhost:8080/demo/register" \
> -H 'content-type: application/json' \
> -d '{"url": "http://localhost:8080/demo/hello", "name": "john"}'
openapi: 3.0.0
info:
contact:
name: "菜鳥工程師肉豬-Spring Boot Webhooks簡單實作"
url: "https://matthung0807.blogspot.com/2022/04/spring-boot-webhooks-simple-impl.html"
title: "Webhooks Events API"
version: "1.0.0"
paths:
"/hello":
post:
description: Hello
requestBody:
content:
application/json:
schema:
$ref: "#/components/schemas/Hello"
required: true
responses:
"200":
description: Success
content:
text/plain:
schema:
type: string
example: success
tags:
- Hello
components:
schemas:
Hello:
type: object
required:
- name
properties:
name:
type: string
呼叫GET/register
即返回剛註冊的http://localhost:8080/demo/hello
。
$ curl -X GET "http://localhost:8080/demo/register"
["http://localhost:8080/demo/hello"]
呼叫GET/greeting
觸發webhooks。
$ curl -X GET "http://localhost:8080/demo/greeting"
此時在console會印出以下。其中Hello john
為client GET/hello
被server webhooks觸發的結果。
Good day
Hello john
send event success
觸發webhooks循序圖。
┌────────┐ ┌────────┐
│ client │ │ Server │
└────┬───┘ └────┬───┘
│ │
│ GET|/greeting │
┌┼─────────────────────►┌┤
││ ││
││ ││
││ │┼─print "Good day"
││ ││
││ ││
││ │┼─retrive webhooks registered urls
││ ││
││ POST|/hello ││
│┤◄─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─┼┼─send webooks event request
││ ││
Print "Hello john"─┼│ ││
││ ││
└┤ └┼─print "send event success"
│ │
│ │
▼ ▼
以上即為webhooks的簡單範例,但通常註冊時需要提供token做為client驗證。
沒有留言:
張貼留言