Spring Bootでカスタム钉钉Botを構築する実践ガイド

本記事では、Spring Bootを使って钉钉のカスタムBotをゼロから立ち上げ、メッセージの送受信まで一通り実装する手順を紹介します。

前提条件

  • 钉钉管理コンソールで「企業内部開発」→「アプリを作成」し、Client ID(旧appKey)とClient Secret(旧appSecret)を取得済み
  • アプリに「カスタムBot」能力を追加し、HTTPコールバックURLを設定済み(ローカル開発時はngrok等でトンネリング)
  • 必要なAPI権限(例:チャット情報の読み取り)を付与し、リリース済み
  • 対象の钉钉グループに上記Botを招待し、Webhook URLを取得済み

プロジェクト構成

<dependency>
    <groupId>com.aliyun</groupId>
    <artifactId>dingtalk</artifactId>
    <version>2.0.24</version>
</dependency>

Webhookコールバックの受信

钉钉からのPOSTリクエストを受け取るRESTコントローラーです。

@RestController
@RequestMapping("/webhook/ding")
@RequiredArgsConstructor
public class DingCallbackController {

    private final DingEventHandler dingEventHandler;

    @PostMapping("/chat")
    public ResponseEntity<String> onChatMessage(@RequestBody(required = false) JSONObject payload) {
        dingEventHandler.handle(payload);
        return ResponseEntity.ok("success");
    }
}

メッセージ送信ユーティリティ

Webhook URLを使って任意のチャットへメッセージを送信する共通クラスです。

@Component
public class DingTalkNotifier {

    private final ObjectMapper mapper = new ObjectMapper();

    public void push(ChatMessage msg) throws IOException {
        DingTalkClient client = new DefaultDingTalkClient(msg.getWebhook());
        OapiRobotSendRequest req = buildRequest(msg);
        OapiRobotSendResponse resp = client.execute(req);
        if (!resp.isSuccess()) {
            throw new IllegalStateException("钉钉送信失敗: " + resp.getErrmsg());
        }
    }

    private OapiRobotSendRequest buildRequest(ChatMessage msg) {
        OapiRobotSendRequest req = new OapiRobotSendRequest();
        req.setMsgtype(msg.getType().name().toLowerCase());

        switch (msg.getType()) {
            case TEXT:
                OapiRobotSendRequest.Text text = new OapiRobotSendRequest.Text();
                text.setContent(msg.getText());
                req.setText(text);
                break;
            case MARKDOWN:
                OapiRobotSendRequest.Markdown md = new OapiRobotSendRequest.Markdown();
                md.setTitle(msg.getTitle());
                md.setText(msg.getText());
                req.setMarkdown(md);
                break;
            case LINK:
                OapiRobotSendRequest.Link link = new OapiRobotSendRequest.Link();
                link.setTitle(msg.getTitle());
                link.setText(msg.getText());
                link.setMessageUrl(msg.getLinkUrl());
                link.setPicUrl(msg.getPicUrl());
                req.setLink(link);
                break;
            case ACTION_CARD:
                req.setActionCard(buildActionCard(msg));
                break;
            case FEED_CARD:
                req.setFeedCard(buildFeedCard(msg));
                break;
            default:
                throw new IllegalArgumentException("未対応のメッセージタイプ");
        }

        OapiRobotSendRequest.At at = new OapiRobotSendRequest.At();
        at.setAtMobiles(msg.getAtMobiles());
        at.setAtUserIds(msg.getAtUserIds());
        at.setIsAtAll(msg.isAtAll());
        req.setAt(at);

        return req;
    }

    private OapiRobotSendRequest.Actioncard buildActionCard(ChatMessage msg) {
        OapiRobotSendRequest.Actioncard card = new OapiRobotSendRequest.Actioncard();
        card.setTitle(msg.getTitle());
        card.setText(msg.getText());
        card.setBtnOrientation(msg.getBtnOrientation());
        card.setSingleTitle(msg.getSingleTitle());
        card.setSingleURL(msg.getSingleUrl());
        List<OapiRobotSendRequest.Btns> btns = msg.getButtons().stream()
                .map(b -> {
                    OapiRobotSendRequest.Btns btn = new OapiRobotSendRequest.Btns();
                    btn.setTitle(b.getTitle());
                    btn.setActionURL(b.getUrl());
                    return btn;
                })
                .collect(Collectors.toList());
        card.setBtns(btns);
        return card;
    }

    private OapiRobotSendRequest.Feedcard buildFeedCard(ChatMessage msg) {
        OapiRobotSendRequest.Feedcard feed = new OapiRobotSendRequest.Feedcard();
        List<OapiRobotSendRequest.Links> links = msg.getArticles().stream()
                .map(a -> {
                    OapiRobotSendRequest.Links l = new OapiRobotSendRequest.Links();
                    l.setTitle(a.getTitle());
                    l.setMessageURL(a.getUrl());
                    l.setPicURL(a.getPicUrl());
                    return l;
                })
                .collect(Collectors.toList());
        feed.setLinks(links);
        return feed;
    }
}

メッセージモデル

@Data
@Builder
public class ChatMessage {
    private String webhook;
    private MsgType type;
    private String title;
    private String text;
    private String linkUrl;
    private String picUrl;
    private String singleTitle;
    private String singleUrl;
    private String btnOrientation;
    private List<Button> buttons;
    private List<Article> articles;
    private List<String> atMobiles;
    private List<String> atUserIds;
    private boolean isAtAll;
}

@Getter
@AllArgsConstructor
public enum MsgType {
    TEXT,
    MARKDOWN,
    LINK,
    ACTION_CARD,
    FEED_CARD
}

@Data
@Builder
public class Button {
    private String title;
    private String url;
}

@Data
@Builder
public class Article {
    private String title;
    private String url;
    private String picUrl;
}

使用例

@Service
public class AlertService {

    private final DingTalkNotifier notifier;

    public void alertDeployment(String env) throws IOException {
        ChatMessage msg = ChatMessage.builder()
                .webhook("https://oapi.dingtalk.com/robot/send?access_token=xxx")
                .type(MsgType.MARKDOWN)
                .title("🚀 デプロイ通知")
                .text("### デプロイ完了\n" +
                      "- **環境**: " + env + "\n" +
                      "- **時刻**: " + LocalTime.now())
                .atMobiles(List.of("15012345678"))
                .build();
        notifier.push(msg);
    }
}

タグ: 钉钉 钉钉机器人 Java SDK Spring Boot Webhook

6月8日 22:40 投稿