iT邦幫忙

2025 iThome 鐵人賽

DAY 5
0

What is WebHook

A webhook is a lightweight, event-driven communication that automatically sends data between applications via HTTP. Triggered by specific events, webhooks automate communication between application programming interfaces (APIs) and can be used to activate workflows, such as in GitOps environments. -RedHat

https://www.redhat.com/en/topics/automation/what-is-a-webhook

簡單來說,webhook 是一種 push-based 的事件驅動 (event-driven) 通訊。
你只要在 server 端註冊一個 URL,server 發生事件時就會「主動推送」資料到這個端口。

這樣有什麼好處?

  • server 不用和 client 保持長連線
  • client 不用不斷 polling 問「有沒有更新」
  • 節省雙方 resources

三種模式比較:

  • Webhook = Push-based, stateless, event-driven
    → great for notifying changes.
  • Polling = Client-initiated, repetitive requests
    → inefficient and wasteful.
  • Open Connection = Real-time stream over long-lived connections
    → best for live updates, but resource-heavy.

以下是簡單的說文解字

1 Why is a webhook considered lightweight?

因為 webhook 只有在事件發生時才 push data,client 不需要 continuous polling。

2 How does event-driven architecture apply to webhooks?

只有事件發生時,server 才會通知 client
→ 就像把電話號碼給朋友,叫他「有事再打給你」,不需要你一直去問。

3 In what ways do webhooks automate inter-application communication?

它自動發 HTTP request (通常是 POST) 帶 event data

  • code push → 自動觸發 CI/CD pipeline
  • user send message → 自動觸發 LINE Bot webhook

4 How do webhooks facilitate communication between APIs?

Webhook 就像是一個 API-based notification,可以 decouple 兩個系統,並且直接傳遞 event data。

https://ithelp.ithome.com.tw/upload/images/20250919/201787751LtaPub6sk.png

https://developers.line.biz/en/docs/partner-docs/development-guidelines/#relationship-between-line-bot-and-channel

Customed Annotation

LINE Java SDK 有提供 custom annotations。

import com.linecorp.bot.spring.boot.handler.annotation.EventMapping;
import com.linecorp.bot.spring.boot.handler.annotation.LineMessageHandler;

它們在做什麼?

@LineMessageHandler

  • 告訴 Spring Boot:「這個 class 是一個 LINE Webhook Handler」。
  • 啟動時,Spring 會自動掃描這個 class,並加入到 ApplicationContext

@EventMapping

  • 表示「這個方法要處理某種類型的事件」。
  • MessageEvent → 當收到文字訊息時就執行。
  • 可以定義多個 @EventMapping,分別處理不同的事件型別(text、image、follow、join…)。

官方文件也有提到,這些 annotation 是 Spring Boot Starter 封裝後提供的語法糖,省下我們去手動解析 webhook JSON payload

How to make a customed annotation ?

一些 java 範例 :
先定義你的 annotation

@Target(ElementType.METHOD)         // 可以標在 method 上 
@Retention(RetentionPolicy.RUNTIME) 
public @interface MyEventMapping {
    String value() default "";      // 可以帶一個事件名稱 @MyEventMapping("事件名稱")
                                    // default 是為了 讓它是可選的 不寫也不會錯
}

將你的 annotation 標在需要的 method 上,注意至樣還只是補充 class 和 method 的訊息而已

public class MyWebhookHandler {

    @MyEventMapping("text")
    public void handleText(String message) {
        System.out.println("Handle TEXT event: " + message);
    }

    @MyEventMapping("image")
    public void handleImage(String message) {
        System.out.println("Handle IMAGE event: " + message);
    }
}

物件 MyDispatcher 依據這個 annotation 的訊息做事

public class MyDispatcher {
    private final Object handler;

    public MyDispatcher(Object handler) {
        this.handler = handler;
    }

    public void dispatch(String eventType, String payload) {
        Method[] methods = handler.getClass().getDeclaredMethods();
        Arrays.stream(methods).forEach(method -> {
            if (method.isAnnotationPresent(MyEventMapping.class)) {
                MyEventMapping mapping = method.getAnnotation(MyEventMapping.class);
                if (mapping.value().equals(eventType)) {
                    try {
                        method.invoke(handler, payload); // 呼叫對應的方法
                    } catch (Exception e) {
                        e.printStackTrace();
                    }
                }
            }
        });
    }
}
public class Demo {
    public static void main(String[] args) {
        MyWebhookHandler handler = new MyWebhookHandler();
        MyDispatcher dispatcher = new MyDispatcher(handler);

        dispatcher.dispatch("text", "Hello from user!");
        dispatcher.dispatch("image", "Image uploaded.");
    }
}
Handle TEXT event: Hello from user!
Handle IMAGE event: Image uploaded.

Annotation 就像「小貼紙」(metadata) 一樣,如果只是貼上去,程式本身不會自動「知道要做什麼」,要讓它動起來,就會需要有人去「看」這張小貼紙,並根據這個 metadata 做一些邏輯。

https://ithelp.ithome.com.tw/upload/images/20250920/20178775RzkaR7lz8q.png
你可以實作 Annotation 當 Aspect 用。 (Spring Start Here)

Retention Policy

上面的 code 中 RetentionPolicy.RUNTIME 表示註解的資料在 runtime 也會保留,這樣就可以透過 java reflection 讀取。javadoc 有提及 Retention Policy,來定義 Annotation 要保留多久。
(https://www.geeksforgeeks.org/java/reflection-in-java/ 可簡單參閱 Reflection API)
有三種:

  • SOURCE:只存在於原始碼,編譯後就消失。
    😸 寫註解給 IDE 或編譯器用,但 JVM 執行時完全不知道。
  • CLASS:會寫進 .class 檔,但 JVM 執行時不會載入。
    😸 給 annotation processors 使用(例如 Lombok、MapStruct 這種 compile-time 工具)。
  • RUNTIME:會寫進 .class 檔,而且在 JVM 執行時還能保留。
    😸 透過 Reflection API 讀取,像 Spring、JUnit、LINE SDK 這類框架都用它來在 runtime 掃描與執行。

如果你只是寫文件註解用,SOURCE 就好。eg. @Override
如果你想讓框架在 runtime 用 annotation 來 dispatch 事件(像 @EventMapping),就必須用 RUNTIME。

可參見 https://www.baeldung.com/java-custom-annotation

實作 Echo Line Bot

當初我也在 Maven 找很久的 dependency :

        <dependency>
            <groupId>com.linecorp.bot</groupId>
            <artifactId>line-bot-messaging-api-client</artifactId>
            <version>9.11.0</version>
        </dependency>
        <dependency>
            <groupId>com.linecorp.bot</groupId>
            <artifactId>line-bot-webhook</artifactId>
            <version>9.11.0</version>
        </dependency>
        <dependency>
            <groupId>com.linecorp.bot</groupId>
            <artifactId>line-bot-spring-boot-webmvc</artifactId>
            <version>9.11.0</version>
        </dependency>
        <dependency>
            <groupId>com.linecorp.bot</groupId>
            <artifactId>line-bot-spring-boot-client</artifactId>
            <version>9.11.0</version>
        </dependency>
        <dependency>
            <groupId>com.linecorp.bot</groupId>
            <artifactId>line-bot-spring-boot-handler</artifactId>
            <version>9.11.0</version>
        </dependency>

LineWebhookHandler.java

@LineMessageHandler
public class LineWebhookHandler {
    private final MessagingApiClient messagingApiClient;
    private final ArticleService articleService;

    public LineWebhookHandler(
            MessagingApiClient messagingApiClient,
            ArticleService articleService) {
        this.messagingApiClient = messagingApiClient;
        this.articleService = articleService;
    }

    // When a user messages your bot, LINE sends a POST to your webhook URL.
    @EventMapping
    public void handleTextMessageEvent(MessageEvent event) {
        System.out.println("event: " + event);
        final String originalMessageText = ((TextMessageContent) event.message()).text();
        messagingApiClient.replyMessage(
                new ReplyMessageRequest.Builder(
                        event.replyToken(),
                        List.of(new TextMessage(originalMessageText))
                ).build()
        );
    }
    
    // further method
    @EventMapping
    public void handleDefaultMessageEvent(Event event) {
        System.out.println("event: " + event);
    }
}
new TextMessage(originalMessageText)

這裡就是可以 回傳訊息 的地方。小心 LINE 的 Reply 和 Push API 都有一些限制,請參見官方文件。
我自己踩過的坑是:訊息可以「切開來分批傳送」(有字數上限),要小心每一個訊息也不能為空。一個比較好的做法是:你可以另外包一個 LineMessageRender,專門來 handle 這些細節。

還有這裡要注意訊息的型態 (若有寫錯請指正~)

  • 接收 (Webhook event) → TextMessageContent (interface, incoming)。
  • 回覆 (Reply API) → TextMessage (class, outgoing, implements Message)。

TextMessage 不是 TextMessageContent。但 TextMessage 可以被當成 Message 傳給 ReplyMessageRequest,因為TextMessage implements Message interface
(回覆訊息必須是 List<Message>)。

/callback 配置

當使用者傳文字訊息時,LINE 會 POST 到 /callback
application.yml

line.bot:
  channel-token: ${LINE_CHANNEL_TOKEN}
  channel-secret: ${LINE_CHANNEL_SECRET}
  handler:
    path: /callback
    #The starter binds /callback for you; you don’t need a manual @PostMapping. This is exactly how the official sample is set up.

What is ngrok

你需要 ngrok 來測試 line bot 及 webhook 有沒有接通~

Ngrok is a secure tunneling tool that exposes a local server (e.g., localhost:8080) to the internet via a public HTTPS URL. It's commonly used to test webhooks, third-party API callbacks, or mobile app integrations during development—without deploying to a public server.

它常用來 測試 webhooks、第三方 API callback,或是 mobile app integration,在開發階段就能模擬外部服務,完全不用先部署到雲端。有點像 proxy port 的概念:
https://www.browserstack.com/guide/proxy-port

ngrok 採用 reverse proxy model

當有人 access 公開的 ngrok URL(例如 LINE server 發訊息通知你,順便附上 token),ngrok 會把這個 HTTP traffic forward 回你本機的 server,就像是在你電腦和外部世界之間挖了一條安全的「隧道」。

ngrok is a light-weight local server :

  • 開發 API 或 webhook receiver 時不用額外租 VPS
  • 測試外部服務(LINE、Stripe、GitHub)的 HTTP request
  • 想要一個 public URL,但還不想把 app deploy 到 cloud

ngrok 使用

ngrok config add-authtoken <token>
ngrok http 80 

https://ngrok.com/downloads/windows?tab=download

  • 到 ngrok 官網下載,並註冊帳號拿到 authtoken
  • 啟動後你本機的 localhost:8080 就會被公開,例如 : https://a75422dd43a2.ngrok-free.app URL。
  • 取得這個 ngrok 產生的 URL 之後,可以到 LINE Developer Console 註冊 webhook。 記得在 URL 後面加上你在 application.yml 裡設定的 /callback
  • https://a75422dd43a2.ngrok-free.app/callback 登記,之後 LINE server 就會透過這個 webhook 把事件推送給 ngrok,而 ngrok 會再把 POST request forward 到你本地端正在跑的 Spring Boot core app。

Resources

如果你不會創辦 Line Developer 帳號 :
請參考 https://www.youtube.com/watch?v=Mw3cODdkaFM 他甚至有python實作

java sdk 相關請參閱 :
https://github.com/line/line-bot-sdk-java?tab=readme-ov-file

或是前人分享的文章 :
https://ithelp.ithome.com.tw/articles/10311687
https://ithelp.ithome.com.tw/articles/10196397


上一篇
Day 4|六邊形的拓展 : 增新更多 Adapter
下一篇
Day 6|貓咪的持久化 🐾 (上):認識 JPA、Hibernate、Spring Data
系列文
Spring 冒險日記:六邊形架構、DDD 與微服務整合6
圖片
  熱門推薦
圖片
{{ item.channelVendor }} | {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言