年前公司的需求裏面有用到釘釘機器人,使用以後發現真的很是簡單,不得不感嘆阿里的牛逼,這篇文章總結了一下我的使用釘釘機器人的經驗,同時介紹我的據此構建一個工具類來方便後續直接「開箱即用」,但願對於讀者有所啓發。java
機器人的使用仍是很是簡單的,直接參考文檔就能夠進行構建,若是瞭解過這一部分能夠直接跳到編寫工具類的部分進行文章的後續閱讀。linux
https://developers.dingtalk.c...android
因爲釘釘的官方文檔更新較爲頻繁,這裏的鏈接可能在之後會失效
文檔裏面介紹的比較詳細了,咱們根據文檔的內容進行實戰一下便可。這裏使用了 新手體驗羣 建立的機器人進行實驗。下面的內容包括建立自定義機器人以及測試機器人如何使用。git
隨意點擊一個機器人,右擊菜單,出現「更多機器人」,進入到界面web
點擊「更多機器人」spring
選擇釘釘的自定義機器人進行使用:shell
這裏還有不少其餘的機器人,若是感興趣能夠查看釘釘的文檔進行更多的瞭解
在下面的界面選擇添加:apache
到達下一個界面,根據指示須要填寫以下的內容:編程
下面說明一下安全設置的內容:json
31000
和對應的錯誤信息。這裏建議保存一下前面和關鍵字,固然忘記了也能夠在構建完成以後從設置裏面查看:簽名:
SECf075e3890b7d79ca645e51b42644fc57c2402577d5a955bce51cb980cec0a3b6
關鍵詞:
新人
至此,咱們成功建立了一個釘釘的自定義機器人,整個過程十分簡單,這裏記得保存一下對應的信息:
https://oapi.dingtalk.com/robot/send?access_token=381c2f405e0f906fd556b27cea9f66864120860b5d8b117bb046e10b6599b050
上面爲我的的配置。發文的時候此機器人已經刪除,因此讀者本身實驗便可。
經過上面的步驟,咱們已經構建了一個基本的機器人爲咱們使用,再進行下一步以前,咱們須要驗證一下釘釘機器人是否能夠正常使用。這裏針對不一樣的平臺說下比較簡單快捷的驗證方法。
windows 推薦使用git
的一個shell
命令框進行測試,由於windows 自己是沒有curl
這個命令的,固然也有其餘的辦法,可是爲了圖省事直接使用git
給咱們開發的一個小工具便可。
以下圖所示,咱們選擇Git Bash Here
,打開命令行的界面
咱們根據上一步的機器人配置,構建一個CURL
請求進行測試:
curl 'https://oapi.dingtalk.com/robot/send?access_token=381c2f405e0f906fd556b27cea9f66864120860b5d8b117bb046e10b6599b050×tamp=1613211530113&secret=SEC2e67120c5e4affa1177ac25fe8dc77ba1c5b49284a9dc7e1888770bc3b76b1fc' \ -H 'Content-Type: application/json' \ -d '{"msgtype": "text","text": {"content": "新人內容測試"}}'
不出所料,這裏按照官方文檔給的方式驗證失敗了,這是爲何呢?緣由有幾個:
timestamp = 1613212103494
sign = MO79EJ58O9lmuQJo1dB1KGMhkZI%2BM5KkyD0NYuNe8%2B8%3D
咱們根據上面的說明修復一下,注意在URL增長了兩個參數:
curl 'https://oapi.dingtalk.com/robot/send?access_token=381c2f405e0f906fd556b27cea9f66864120860b5d8b117bb046e10b6599b050×tamp=1613212722591&sign=SsKKlkvwM%2F4tsCPE6YoGls8vgkQqWJGHYpvWbW7hTGM%3D' \ -H 'Content-Type: application/json' \ -d '{"msgtype": "text","text": {"content": "新人爲何你這麼牛逼"}}'
關於這一部份內容,已經彙總到「問題彙總」這一部分,若是仍是感到迷惑能夠參考。
咱們再次驗證一下,發現依然失敗,比較奇怪,我的設置的關鍵字在請求content裏面卻失敗了:
zhaoxudong@LAPTOP-MEUFMP1M MINGW64 /d/Users/zhaoxudong/Desktop $ curl 'https://oapi.dingtalk.com/robot/send?access_token=381c2f405e0f906fd556b27cea9f66864120860b5d8b117bb046e10b6599b050×tamp=1613212722591&sign=SsKKlkvwM%2F4tsCPE6YoGls8vgkQqWJGHYpvWbW7hTGM%3D' \ > -H 'Content-Type: application/json' \ > -d '{"msgtype": "text","text": {"content": "新人爲何你這麼牛逼"}}' % Total % Received % Xferd Average Speed Time Time Time Current Dload Upload Total Spent Left Speed 100 178 100 115 100 63 991 543 --:--:-- --:--:-- --:--:-- 1534 {"errcode":310000,"errmsg":"keywords not in content, more: [https://ding-doc.dingtalk.com/doc#/serverapi2/qf2nxq]"}
排查問題以後發現因爲windows系統默認使用了gb2312
的編碼,因此咱們此時須要切換一下系統的編碼,爲了證實是系統編碼的問題,咱們先驗證一下編碼:
打開window的cmd
窗口,咱們輸入chcp
命令進入到具體的頁面,能夠看到下面936,百度一下發現就是GB2312
,在請求發送的過程當中被轉碼致使亂碼。
C:\Users\zhaoxudong>chcp 活動代碼頁: 936
解決辦法也比較簡單,改一下整改系統的編碼便可,關於設置的方法:https://blog.csdn.net/robinhu...
插曲:我的在設置事後,由於編碼的問題致使編輯器沒法編譯,通過覈實發現是因爲 文件夾的編碼亂碼找不到類的問題,因此這裏建議放置Java項目的時候放置到 全英文的目錄。因此更推薦linux的方式,能夠省去不少麻煩
linux 驗證比較簡單,並且出問題的機率比較小,根據window內容得知最後須要三個參數才能請求成功,這裏直接給出一個類似的CURL請求做爲案例說明:
curl 'https://oapi.dingtalk.com/robot/send?access_token=381c2f405e0f906fd556b27cea9f66864120860b5d8b117bb046e10b6599b050×tamp=1613212722591&sign=SsKKlkvwM%2F4tsCPE6YoGls8vgkQqWJGHYpvWbW7hTGM%3D' \ -H 'Content-Type: application/json' \ -d '{"msgtype": "text","text": {"content": "新人爲何你這麼牛逼"}}'
咱們把這個請求放到linux
命令行裏面進行運行,若是errorcode返回0,說明請求成功:
{"errcode":0,"errmsg":"ok"}
請求成功以後,咱們能夠看到對應的結果:
注意一下釘釘機器人不能請求過於頻繁。建議限制一下每分鐘的請求QPS
從上一節能夠看到,整個釘釘機器人的構建仍是十分簡單的。可是使用起來不是特別的方便,我的以前有使用釘釘作過一個預警的小需求,爲了後續能夠直接開箱即用,本身構建了工具類,下面的部分主要說我的的工具類的設計以及我的的構建思路
我的水平有限,工具類還有很大的改進空間,可是對於我來講暫時沒有遇到使用的瓶頸。
這裏我的的小工具類整合到了我的小項目裏面,想要參考的能夠直接進行下載,下面的文章代碼也是來源於這個項目裏面。
具體請查看:com.zxd.interview.dingrobot
這個包
具體的代碼地址:https://gitee.com/lazyTimes/i...
把整個請求的流程須要的組件分爲了如下的幾個部分:
構建基本的請求環境:也就是須要的請求地址,請求籤名或者關鍵字等參數,這些參數都是必須的,不然請求沒法正常運行,因此咱們提出來做爲環境使用。
構建請求參數:因爲釘釘支持很是多的msgtype
也就是文本類型,我的參考了一下SDK,對應構建了一個請求的參數類,爲了方便擴展,設計了一個接口進行後續的擴展和兼容。
使用JAVA代碼發送請求:本着最小依賴的原則,使用最多見的HttpClient
進行模擬JAVA的請求發送。可是在這個基礎上作了一點點的封裝,方便後續擴展
org.springframework.web.bind.annotation.RequestMethod
或者直接使用枚舉構建常量便可。返回請求結果:包含了錯誤碼,錯誤信息,以及其餘的參數等,也能夠修改成直接返回字符串,由客戶端決定如何處理
請求以後返回結果:將上面的錯誤碼或者錯誤信息等封裝爲一個簡單對象進行返回,一樣若是不喜歡也能夠改成返回字符串的結果。
在介紹正式的結果以前,咱們看下結果,下面是效果截圖,包含了釘釘文檔裏面的全部類型,包含了目前釘釘文檔支持的幾種主要的類型:
下面爲單元測試的代碼,整個單元測試測試各類不一樣請求類型,調用工具包發送請求:
注意下面的請求text裏面包含了以前請求示例裏面設置的關鍵字,沒有關鍵字是沒法請求成功的
import com.alibaba.fastjson.JSON; import org.apache.commons.codec.binary.Base64; import org.junit.jupiter.api.Test; import javax.crypto.Mac; import javax.crypto.spec.SecretKeySpec; import java.net.URLEncoder; import java.nio.charset.StandardCharsets; import java.util.ArrayList; import java.util.Arrays; import java.util.List; /** * @author zxd * @version v1.0.0 * @Package : com.zxd.interview.dingrobot * @Description : 釘釘機器人測試類 * @Create on : 2021/2/7 11:06 **/ public class DingRobotUtilsTest { /** 運行下面五個單元測試的結果 */ @Test public void testAll() { testText(); testLink(); testMarkdown(); testActionCard(); testFeedCard(); } /** * 構建當前的系統時間戳 */ @Test public void generateSystemCurrentTime() throws Exception { long currentTimeMillis = System.currentTimeMillis(); String secret = "SEC2e67120c5e4affa1177ac25fe8dc77ba1c5b49284a9dc7e1888770bc3b76b1fc"; String sign = generateSign(currentTimeMillis, secret); System.out.println("timestamp = " + currentTimeMillis); System.out.println("sign = " + sign); } /** * 測試link類型的請求 */ @Test public void testLink() { DingRobotRequest.Builder builder = new DingRobotRequest.Builder(); DingRobotRequest build = builder.secret("SEC2e67120c5e4affa1177ac25fe8dc77ba1c5b49284a9dc7e1888770bc3b76b1fc") .url("https://oapi.dingtalk.com/robot/send") .accessToken("381c2f405e0f906fd556b27cea9f66864120860b5d8b117bb046e10b6599b050") .msg(generateLink()).build(); try { DingRobotResponseMsg dingRobotResponseMsg = DingRobotUtils.notifyRobot(build); System.err.println(JSON.toJSONString(dingRobotResponseMsg)); } catch (Exception e) { e.printStackTrace(); } } /** * 測試text類型 */ @Test public void testText() { DingRobotRequest.Builder builder = new DingRobotRequest.Builder(); DingRobotRequest build = builder.secret("SEC2e67120c5e4affa1177ac25fe8dc77ba1c5b49284a9dc7e1888770bc3b76b1fc") .url("https://oapi.dingtalk.com/robot/send") .accessToken("381c2f405e0f906fd556b27cea9f66864120860b5d8b117bb046e10b6599b050") .msg(generateText()).build(); try { DingRobotResponseMsg dingRobotResponseMsg = DingRobotUtils.notifyRobot(build); System.err.println(JSON.toJSONString(dingRobotResponseMsg)); } catch (Exception e) { e.printStackTrace(); } } /**測試markdown 類型 */ @Test public void testMarkdown() { DingRobotRequest.Builder builder = new DingRobotRequest.Builder(); DingRobotRequest build = builder.secret("SEC2e67120c5e4affa1177ac25fe8dc77ba1c5b49284a9dc7e1888770bc3b76b1fc") .url("https://oapi.dingtalk.com/robot/send") .accessToken("381c2f405e0f906fd556b27cea9f66864120860b5d8b117bb046e10b6599b050") .msg(generateMarkdown()).build(); try { DingRobotResponseMsg dingRobotResponseMsg = DingRobotUtils.notifyRobot(build); System.err.println(JSON.toJSONString(dingRobotResponseMsg)); } catch (Exception e) { e.printStackTrace(); } } /**測試ActionCard 類型 */ @Test public void testActionCard() { DingRobotRequest.Builder builder = new DingRobotRequest.Builder(); DingRobotRequest build = builder.secret("SEC2e67120c5e4affa1177ac25fe8dc77ba1c5b49284a9dc7e1888770bc3b76b1fc") .url("https://oapi.dingtalk.com/robot/send") .accessToken("381c2f405e0f906fd556b27cea9f66864120860b5d8b117bb046e10b6599b050") .msg(generateActionCard()).build(); try { DingRobotResponseMsg dingRobotResponseMsg = DingRobotUtils.notifyRobot(build); System.err.println(JSON.toJSONString(dingRobotResponseMsg)); } catch (Exception e) { e.printStackTrace(); } } /**測試FeedCard 類型 */ @Test public void testFeedCard() { DingRobotRequest.Builder builder = new DingRobotRequest.Builder(); DingRobotRequest build = builder.secret("SEC2e67120c5e4affa1177ac25fe8dc77ba1c5b49284a9dc7e1888770bc3b76b1fc") .url("https://oapi.dingtalk.com/robot/send") .accessToken("381c2f405e0f906fd556b27cea9f66864120860b5d8b117bb046e10b6599b050") .msg(generateFeed()).build(); try { DingRobotResponseMsg dingRobotResponseMsg = DingRobotUtils.notifyRobot(build); System.err.println(JSON.toJSONString(dingRobotResponseMsg)); } catch (Exception e) { e.printStackTrace(); } } private DingRobotRequestBody generateFeed() { List<DingRobotRequestBody.FeedCard.FeedItem> list = new ArrayList<>(); DingRobotRequestBody dingRobotRequestBody = new DingRobotRequestBody(); DingRobotRequestBody.FeedCard feedCard = new DingRobotRequestBody.FeedCard(); DingRobotRequestBody.FeedCard.FeedItem feedItem = new DingRobotRequestBody.FeedCard.FeedItem(); feedItem.setMessageURL("https://www.dingtalk.com/"); feedItem.setTitle("新人時代的火車向前開"); feedItem.setPicURL("https://img.alicdn.com/tfs/TB1NwmBEL9TBuNjy1zbXXXpepXa-2400-1218.png"); list.add(feedItem); feedCard.setLinks(list); dingRobotRequestBody.setFeedCard(feedCard); dingRobotRequestBody.setMsgType("feedCard"); return dingRobotRequestBody; } private DingRobotRequestBody generateActionCard() { DingRobotRequestBody dingRobotRequestBody = new DingRobotRequestBody(); DingRobotRequestBody.ActionCard actionCard = new DingRobotRequestBody.ActionCard(); actionCard.setBtnOrientation("0"); actionCard.setSingleTitle("閱讀全文"); actionCard.setSingleURL("https://www.dingtalk.com/"); actionCard.setText("新人![screenshot](https://gw.alicdn.com/tfs/TB1ut3xxbsrBKNjSZFpXXcXhFXa-846-786.png) \n" + " ### 喬布斯 20 年前想打造的蘋果咖啡廳 \n" + " Apple Store 的設計正從原來滿滿的科技感走向生活化,而其生活化的走向其實能夠追溯到 20 年前蘋果一個創建咖啡館的計劃"); actionCard.setTitle("喬布斯 20 年前想打造一間蘋果咖啡廳,而它正是 Apple Store 的前身"); dingRobotRequestBody.setMsgType("actionCard"); dingRobotRequestBody.setActionCard(actionCard); return dingRobotRequestBody; } private DingRobotRequestBody generateMarkdown() { DingRobotRequestBody dingRobotRequestBody = new DingRobotRequestBody(); DingRobotRequestBody.MarkDown markDown = new DingRobotRequestBody.MarkDown(); dingRobotRequestBody.setMsgType("markdown"); markDown.setTitle("杭州天氣"); markDown.setText("新人測試 標題\n" + "# 一級標題\n" + "## 二級標題\n" + "### 三級標題\n" + "#### 四級標題\n" + "##### 五級標題\n" + "###### 六級標題\n" + "\n" + "引用\n" + "> A man who stands for nothing will fall for anything.\n" + "\n" + "文字加粗、斜體\n" + "**bold**\n" + "*italic*\n" + "\n" + "連接\n" + "[this is a link](http://name.com)\n" + "\n" + "圖片\n" + "![](http://name.com/pic.jpg)\n" + "\n" + "無序列表\n" + "- item1\n" + "- item2\n" + "\n" + "有序列表\n" + "1. item1\n" + "2. item2"); dingRobotRequestBody.setMarkDown(markDown); return dingRobotRequestBody; } private DingRobotRequestBody generateText() { DingRobotRequestBody dingRobotRequestBody = new DingRobotRequestBody(); DingRobotRequestBody.Text text = new DingRobotRequestBody.Text(); text.setContent("新人爲何這麼牛逼"); DingRobotRequestBody.At at = getnerateAt(); dingRobotRequestBody.setMsgType("text"); dingRobotRequestBody.setAt(at); dingRobotRequestBody.setText(text); return dingRobotRequestBody; } private DingRobotRequestBody generateLink() { DingRobotRequestBody dingRobotRequestBody = new DingRobotRequestBody(); DingRobotRequestBody.Link link = new DingRobotRequestBody.Link(); link.setMessageUrl("https://www.dingtalk.com/s?__biz=MzA4NjMwMTA2Ng==&mid=2650316842&idx=1&sn=60da3ea2b29f1dcc43a7c8e4a7c97a16&scene=2&srcid=09189AnRJEdIiWVaKltFzNTw&from=timeline&isappinstalled=0&key=&ascene=2&uin=&devicetype=android-23&version=26031933&nettype=WIFI"); link.setPicUrl(""); link.setTitle("時代的火車向前開"); link.setText("新人:這個即將發佈的新版本,創始人xx稱它爲紅樹林。而在此以前,每當面臨重大升級,產品經理們都會取一個應景的代號,這一次,爲何是紅樹林"); DingRobotRequestBody.At at = getnerateAt(); dingRobotRequestBody.setMsgType("link"); dingRobotRequestBody.setAt(at); dingRobotRequestBody.setLink(link); return dingRobotRequestBody; } /** * 構建at請求 * * @return */ private DingRobotRequestBody.At getnerateAt() { DingRobotRequestBody.At at = new DingRobotRequestBody.At(); at.setAtAll(true); at.setAtMobiles(Arrays.asList("xxxxx", "123456789")); return at; } /** * 構建簽名方法 * * @param timestamp 時間戳 * @param secret 祕鑰 * @return * @throws Exception */ private String generateSign(Long timestamp, String secret) throws Exception { String stringToSign = timestamp + "\n" + secret; Mac mac = Mac.getInstance("HmacSHA256"); mac.init(new SecretKeySpec(secret.getBytes(StandardCharsets.UTF_8), "HmacSHA256")); byte[] signData = mac.doFinal(stringToSign.getBytes(StandardCharsets.UTF_8)); return URLEncoder.encode(new String(Base64.encodeBase64(signData)), "UTF-8"); } }
下面就上面的單元測試,說明一下我的的基本設計。咱們根據思路構建一個支持拿來即用的釘釘工具類。
在進行具體的代碼編寫以前,須要引入對應的依賴,我的秉持最小依賴的原則,使用的三方jar包僅僅爲一些測試工具包和Httpclient請求工具包還有最熟悉的fastjson的工具包。
<!-- https://mvnrepository.com/artifact/org.apache.httpcomponents/httpclient --> <dependency> <groupId>org.apache.httpcomponents</groupId> <artifactId>httpclient</artifactId> <version>4.5.6</version> </dependency> <!-- https://mvnrepository.com/artifact/com.alibaba/fastjson --> <dependency> <groupId>com.alibaba</groupId> <artifactId>fastjson</artifactId> <version>1.2.75</version> </dependency> <!-- https://mvnrepository.com/artifact/junit/junit --> <dependency> <groupId>junit</groupId> <artifactId>junit</artifactId> <version>4.12</version> </dependency>
類結構包含了以前設計思路里面說明的狀況,包含請求類,工具類,參數封裝和請求對象結構封裝等。
+ DingRobotRequest.java 釘釘請求對象 + DingRobotRequestAble.java 請求接口,容許發送釘釘請求的接口 + DingRobotRequestBody.java 容許發送釘釘請求的接口具體的實現類,比較重要,對接文檔的釘釘對象 + DingRobotRequestMsg.java 廢棄對象,可是依然保留s + DingRobotResponseMsg.java 請求返回對象 + DingRobotUtils.java 釘釘請求工具類,很是重要的一個類 + HttpClientUtil.java httpclient請求工具類 + HttpConfig.java 請求參數構建類 + HttpMethods.java 請求方法類
構建基本的請求環境,咱們使用對象來封裝全部的環境參數,而且使用建造模式構建一個建造器,使用建造來構建咱們須要的環境參數,它的使用方式以下:
DingRobotRequest build = builder.secret("SEC2e67120c5e4affa1177ac25fe8dc77ba1c5b49284a9dc7e1888770bc3b76b1fc") .url("https://oapi.dingtalk.com/robot/send") .accessToken("381c2f405e0f906fd556b27cea9f66864120860b5d8b117bb046e10b6599b050") .msg(generateActionCard()).build();
具體的源代碼以下,包含了幾個簡單的必要參數,以及一個建造器,注意對於構造器的私有化,對外只容許使用構建器進行初始化:
/** * @author zxd * @version v1.0.0 * @Package : com.dcc.common.field * @Description : 釘釘機器人請求實體類 * @Create on : 2021/2/5 15:40 **/ public class DingRobotRequest { /** * 請求URL */ private String url; /** * token */ private String accessToken; /** * 祕鑰 */ private String secret; /** * 請求msg */ private DingRobotRequestBody msg; private DingRobotRequest(){ } private DingRobotRequest(Builder builder) { this.url = builder.url; this.accessToken = builder.accessToken; this.secret = builder.secret; this.msg = builder.msg; } public static class Builder { private String url; private String accessToken; private String secret; private DingRobotRequestBody msg; public DingRobotRequest.Builder url(String url){ this.url = url; return this; } public DingRobotRequest.Builder accessToken(String accessToken){ this.accessToken = accessToken; return this; } public DingRobotRequest.Builder secret(String secret){ this.secret = secret; return this; } public DingRobotRequest.Builder msg(DingRobotRequestBody msg){ this.msg = msg; return this; } public DingRobotRequest build(){ return new DingRobotRequest(this); } } public String getUrl() { return url; } public void setUrl(String url) { this.url = url; } public String getAccessToken() { return accessToken; } public void setAccessToken(String accessToken) { this.accessToken = accessToken; } public String getSecret() { return secret; } public void setSecret(String secret) { this.secret = secret; } public DingRobotRequestBody getMsg() { return msg; } public void setMsg(DingRobotRequestBody msg) { this.msg = msg; } @Override public String toString() { return "DingRobotRequest{" + "url='" + url + '\'' + ", accessToken='" + accessToken + '\'' + ", secret='" + secret + '\'' + ", msg='" + msg + '\'' + '}'; } }
下面是請求參數的構建案例,咱們可使用鏈式調用的方式構建不一樣的request請求:
/** * 釘釘機器人的默認配置 * * @param dingRobotRequest 釘釘機器人請求對象 * @param dingRobotRequestMsg 釘釘機器人請求實體 * @return */ private static HttpConfig buildDefaultHttpConfig(DingRobotRequest dingRobotRequest, DingRobotRequestAble dingRobotRequestMsg) { return HttpConfig.custom().headers(defaultBasicHeader()) .url(dingRobotRequest.getUrl()) .encoding("UTF-8") .method(HttpMethods.POST) .json(JSON.toJSONString(dingRobotRequestMsg)); }
從上面的案例能夠看到下面對於請求配置類,構建HttpConfig
請求,一樣相似構建器進行對象的參數構建,咱們定義了基本的請求encoding
、請求header
,請求方法參數,請求的context等對應的參數配置。
/** * 請求配置類 * */ public class HttpConfig { private HttpConfig() { } // 傳入參數特定類型 public static final String ENTITY_STRING = "$ENTITY_STRING$"; public static final String ENTITY_MULTIPART = "$ENTITY_MULTIPART$"; /** * 獲取實例 * * @return */ public static HttpConfig custom() { return new HttpConfig(); } /** * HttpClient對象 */ private HttpClient client; /** * Header頭信息 */ private Header[] headers; /** * 是否返回response的headers */ private boolean isReturnRespHeaders; /** * 請求方法 */ private HttpMethods method = HttpMethods.GET; /** * 請求方法名稱 */ private String methodName; /** * 用於cookie操做 */ private HttpContext context; /** * 傳遞參數 */ private Map<String, Object> map; /** * 以json格式做爲輸入參數 */ private String json; /** * 輸入輸出編碼 */ private String encoding = Charset.defaultCharset().displayName(); /** * 輸入編碼 */ private String inenc; /** * 輸出編碼 */ private String outenc; /** * 解決多線程下載時,strean被close的問題 */ private static final ThreadLocal<OutputStream> outs = new ThreadLocal<OutputStream>(); /** * 解決多線程處理時,url被覆蓋問題 */ private static final ThreadLocal<String> urls = new ThreadLocal<String>(); /** * HttpClient對象 */ public HttpConfig client(HttpClient client) { this.client = client; return this; } /** * 資源url */ public HttpConfig url(String url) { urls.set(url); return this; } /** * Header頭信息 */ public HttpConfig headers(Header[] headers) { this.headers = headers; return this; } /** * Header頭信息(是否返回response中的headers) */ public HttpConfig headers(Header[] headers, boolean isReturnRespHeaders) { this.headers = headers; this.isReturnRespHeaders = isReturnRespHeaders; return this; } /** * 請求方法 */ public HttpConfig method(HttpMethods method) { this.method = method; return this; } /** * 請求方法 */ public HttpConfig methodName(String methodName) { this.methodName = methodName; return this; } /** * cookie操做相關 */ public HttpConfig context(HttpContext context) { this.context = context; return this; } /** * 傳遞參數 */ public HttpConfig map(Map<String, Object> map) { synchronized (getClass()) { if (this.map == null || map == null) { this.map = map; } else { this.map.putAll(map); ; } } return this; } /** * 以json格式字符串做爲參數 */ public HttpConfig json(String json) { this.json = json; map = new HashMap<String, Object>(); map.put(ENTITY_STRING, json); return this; } /** * 上傳文件時用到 */ public HttpConfig files(String[] filePaths) { return files(filePaths, "file"); } /** * 上傳文件時用到 * * @param filePaths 待上傳文件所在路徑 */ public HttpConfig files(String[] filePaths, String inputName) { return files(filePaths, inputName, false); } /** * 上傳文件時用到 * * @param filePaths 待上傳文件所在路徑 * @param inputName 即file input 標籤的name值,默認爲file * @param forceRemoveContentTypeChraset * @return */ public HttpConfig files(String[] filePaths, String inputName, boolean forceRemoveContentTypeChraset) { synchronized (getClass()) { if (this.map == null) { this.map = new HashMap<String, Object>(); } } map.put(ENTITY_MULTIPART, filePaths); map.put(ENTITY_MULTIPART + ".name", inputName); map.put(ENTITY_MULTIPART + ".rmCharset", forceRemoveContentTypeChraset); return this; } /** * 輸入輸出編碼 */ public HttpConfig encoding(String encoding) { //設置輸入輸出 inenc(encoding); outenc(encoding); this.encoding = encoding; return this; } /** * 輸入編碼 */ public HttpConfig inenc(String inenc) { this.inenc = inenc; return this; } /** * 輸出編碼 */ public HttpConfig outenc(String outenc) { this.outenc = outenc; return this; } /** * 輸出流對象 */ public HttpConfig out(OutputStream out) { outs.set(out); return this; } public HttpClient client() { return client; } public Header[] headers() { return headers; } public boolean isReturnRespHeaders() { return isReturnRespHeaders; } public String url() { return urls.get(); } public HttpMethods method() { return method; } public String methodName() { return methodName; } public HttpContext context() { return context; } public Map<String, Object> map() { return map; } public String json() { return json; } public String encoding() { return encoding; } public String inenc() { return inenc == null ? encoding : inenc; } public String outenc() { return outenc == null ? encoding : outenc; } public OutputStream out() { return outs.get(); } }
以前說明,咱們使用最經常使用的Httpclient
進行設計請求,根據Httpclient
請求工具包構建一個基本的工具類:
這個類是一個很難複用和擴展的高耦合類,而且設計不是很是良好。
/** * httpclient 請求工具封裝類 */ public class HttpClientUtil { public static String doGet(String url, Map<String, String> param) { // 建立Httpclient對象 CloseableHttpClient httpclient = HttpClients.createDefault(); String resultString = ""; CloseableHttpResponse response = null; try { // 建立uri URIBuilder builder = new URIBuilder(url); if (param != null) { for (String key : param.keySet()) { builder.addParameter(key, param.get(key)); } } URI uri = builder.build(); // 建立http GET請求 HttpGet httpGet = new HttpGet(uri); // 執行請求 response = httpclient.execute(httpGet); // 判斷返回狀態是否爲200 if (response.getStatusLine().getStatusCode() == 200) { resultString = EntityUtils.toString(response.getEntity(), "UTF-8"); } } catch (Exception e) { e.printStackTrace(); } finally { try { if (response != null) { response.close(); } httpclient.close(); } catch (IOException e) { e.printStackTrace(); } } return resultString; } public static String doGet(String url) { return doGet(url, null); } public static String doPost(String url, Map<String, String> param) { // 建立Httpclient對象 CloseableHttpClient httpClient = HttpClients.createDefault(); CloseableHttpResponse response = null; String resultString = ""; try { // 建立Http Post請求 HttpPost httpPost = new HttpPost(url); // 建立參數列表 if (param != null) { List<NameValuePair> paramList = new ArrayList<>(); for (String key : param.keySet()) { paramList.add(new BasicNameValuePair(key, param.get(key))); } // 模擬表單 UrlEncodedFormEntity entity = new UrlEncodedFormEntity(paramList); httpPost.setEntity(entity); } // 執行http請求 response = httpClient.execute(httpPost); resultString = EntityUtils.toString(response.getEntity(), "utf-8"); } catch (Exception e) { e.printStackTrace(); } finally { try { response.close(); } catch (IOException e) { e.printStackTrace(); } } return resultString; } public static String doPost(String url) { return doPost(url, null); } public static String doPostJson(String url, String json) { // 建立Httpclient對象 CloseableHttpClient httpClient = HttpClients.createDefault(); CloseableHttpResponse response = null; String resultString = ""; try { // 建立Http Post請求 HttpPost httpPost = new HttpPost(url); // 建立請求內容 StringEntity entity = new StringEntity(json, ContentType.APPLICATION_JSON); httpPost.setEntity(entity); // 執行http請求 response = httpClient.execute(httpPost); resultString = EntityUtils.toString(response.getEntity(), "utf-8"); } catch (Exception e) { e.printStackTrace(); } finally { try { if(response != null){ response.close(); } } catch (IOException e) { e.printStackTrace(); } } return resultString; } /** * 根據請求Config 進行請求發送 * @param httpConfig * @return */ public static String send(HttpConfig httpConfig) { return doPostJson(httpConfig.url(), httpConfig.json()); } }
接着根據請求的結果設計一個釘釘機器人的返回對象,返回對象的設計也比較的簡單。
/** * @author zxd * @version v1.0.0 * @Package : com.dcc.common.field * @Description : 釘釘機器人返回對象 * @Create on : 2021/2/5 18:26 **/ public class DingRobotResponseMsg { /** * 錯誤碼 */ private String errcode; /** * 錯誤信息 */ private String errmsg; /** * 更多連接 */ private String more; public DingRobotResponseMsg(String errcode, String errmsg, String more) { this.errcode = errcode; this.errmsg = errmsg; this.more = more; } public DingRobotResponseMsg() { } public String getErrcode() { return errcode; } public String getErrmsg() { return errmsg; } public String getMore() { return more; } public void setErrcode(String errcode) { this.errcode = errcode; } public void setErrmsg(String errmsg) { this.errmsg = errmsg; } public void setMore(String more) { this.more = more; } }
最後,也是最重要的,咱們要根據釘釘的文檔,構建一個全部類型的請求對象類,這個類包含了釘釘文檔目前支持的全部類型。內部使用了大量的內部類,客戶端須要瞭解必定的細節才能夠具體的調用。下面簡要說明一下內容類的基本使用結構。
/** * @author zxd * @version v1.0.0 * @Package : com.dcc.common.field * @Description : 釘釘機器人請求實體對象 * 請求案例:{"msgtype": "text","text": {"content": "自定義具體內容"}} * @link {https://developers.dingtalk.com/document/app/custom-robot-access} * * @Create on : 2021/2/5 11:55 **/ public class DingRobotRequestBody implements DingRobotRequestAble { /** * 艾特對象內容 */ private At at; /** * 類型 */ private String msgtype; /** * 文本類型 */ private Text text; /** * 鏈接類型 */ private Link link; /** * markdown 類型 */ private MarkDown markdown; /** * 總體跳轉ActionCard類型 */ private ActionCard actionCard; /** * FeedCard類型 */ private FeedCard feedCard; /** * FeedCard類型 * * msgtype String 是 此消息類型爲固定feedCard。 * title String 是 單條信息文本。 * messageURL String 是 點擊單條信息到跳轉連接。 * picURL String 是 單條信息後面圖片的URL。 */ public static class FeedCard{ private List<FeedItem> links; /** * 表明 FeedCard類型 子類型 */ public static class FeedItem{ private String title; private String messageURL; private String picURL; public String getTitle() { return title; } public void setTitle(String title) { this.title = title; } public String getMessageURL() { return messageURL; } public void setMessageURL(String messageURL) { this.messageURL = messageURL; } public String getPicURL() { return picURL; } public void setPicURL(String picURL) { this.picURL = picURL; } } public List<FeedItem> getLinks() { return links; } public void setLinks(List<FeedItem> links) { this.links = links; } } /** * 總體跳轉ActionCard類型 * msgtype String 是 消息類型,此時固定爲:actionCard。 * title String 是 首屏會話透出的展現內容。 * text String 是 markdown格式的消息。 * singleTitle String 是 單個按鈕的標題。 * * 注意 設置此項和singleURL後,btns無效。 * * singleURL String 是 點擊singleTitle按鈕觸發的URL。 * btnOrientation String 否 0:按鈕豎直排列1:按鈕橫向排列 */ public static class ActionCard{ private String title; private String text; private String btnOrientation; private String singleTitle; private String singleURL; public String getTitle() { return title; } public void setTitle(String title) { this.title = title; } public String getText() { return text; } public void setText(String text) { this.text = text; } public String getBtnOrientation() { return btnOrientation; } public void setBtnOrientation(String btnOrientation) { this.btnOrientation = btnOrientation; } public String getSingleTitle() { return singleTitle; } public void setSingleTitle(String singleTitle) { this.singleTitle = singleTitle; } public String getSingleURL() { return singleURL; } public void setSingleURL(String singleURL) { this.singleURL = singleURL; } } /** * 艾特類 */ public static class At{ /** * 是否通知所有人 */ private boolean atAll; /** * 須要@的手機號數組 */ private List<String> atMobiles; public boolean isAtAll() { return atAll; } public void setAtAll(boolean atAll) { this.atAll = atAll; } public List<String> getAtMobiles() { return atMobiles; } public void setAtMobiles(List<String> atMobiles) { this.atMobiles = atMobiles; } } /** * * markdown 類型, 能夠發送markdown 的語法格式 * msgtype String 是 消息類型,此時固定爲:markdown。 * title String 是 首屏會話透出的展現內容。 * text String 是 markdown格式的消息。 * atMobiles Array 否 被@人的手機號。 注意 在text內容裏要有@人的手機號。 * isAtAll Boolean 否 是否@全部人。 */ public static class MarkDown{ private String title; private String text; public String getTitle() { return title; } public void setTitle(String title) { this.title = title; } public String getText() { return text; } public void setText(String text) { this.text = text; } } /** * 釘釘請求:連接類型 * msgtype String 是 消息類型,此時固定爲:link。 title String 是 消息標題。 text String 是 消息內容。若是太長只會部分展現。 messageUrl String 是 點擊消息跳轉的URL。 picUrl String 否 圖片URL。 */ public static class Link{ private String text; private String messageUrl; private String picUrl; private String title; public String getText() { return text; } public void setText(String text) { this.text = text; } public String getMessageUrl() { return messageUrl; } public void setMessageUrl(String messageUrl) { this.messageUrl = messageUrl; } public String getPicUrl() { return picUrl; } public void setPicUrl(String picUrl) { this.picUrl = picUrl; } public String getTitle() { return title; } public void setTitle(String title) { this.title = title; } } /** * 釘釘請求:純文本類型 */ public static class Text{ /** * text請求內容 */ private String content; public String getContent() { return content; } public void setContent(String content) { this.content = content; } } @Override public void setMsgType(String msgtype) { this.msgtype = msgtype; } @Override public void setText(Text text) { this.text = text; } @Override public void setLink(Link link) { this.link = link; } @Override public void setMarkDown(MarkDown markDown) { this.markdown = markDown; } @Override public void setActionCard(ActionCard actionCard) { this.actionCard = actionCard; } @Override public void setFeedCard(FeedCard feedCard) { this.feedCard = feedCard; } public At getAt() { return at; } public void setAt(At at) { this.at = at; } public String getMsgtype() { return msgtype; } public Text getText() { return text; } public Link getLink() { return link; } public MarkDown getMarkdown() { return markdown; } public ActionCard getActionCard() { return actionCard; } public FeedCard getFeedCard() { return feedCard; } }
插曲:在生成具體的釘釘對應請求對象時候,咱們構建了一個對應的接口
/** * @author zxd * @version v1.0.0 * @Package : com.zxd.interview.dingrobot * @Description : 容許發送釘釘請求的接口 * @Create on : 2021/2/7 11:45 **/ public interface DingRobotRequestAble { /** * 全部的子類須要集成該接口 * @return */ void setMsgType(String msgType); /** * 普通文本類型 * @param text */ void setText(DingRobotRequestBody.Text text); /** * link類型 * @param link */ void setLink(DingRobotRequestBody.Link link); /** * markdown 類型 * @param markDown */ void setMarkDown(DingRobotRequestBody.MarkDown markDown); /** * 總體跳轉ActionCard類型 * @param actionCard */ void setActionCard(DingRobotRequestBody.ActionCard actionCard); /** * feedcard 類型 * @param feedCard */ void setFeedCard(DingRobotRequestBody.FeedCard feedCard); }
介紹完上面全部的輔助對象以後,咱們着手構建核心的釘釘請求工具類,釘釘的請求工具類包含了基本的請求步驟,提供對外的請求方法,調用者根據請求對象構建對應的請求參數便可,從下面的代碼能夠看到最核心的方法是notifyRobot
這個方法,這個方法很是簡單,內部的邏輯分爲以下的幾步:
/** * @author zxd * @version v1.0.0 * @Package : com.dcc.common.utils * @Description : 釘釘機器人工具類 * @Create on : 2021/2/4 00:11 **/ public class DingRobotUtils { private static final Logger LOGGER = LoggerFactory.getLogger(DingRobotUtils.class); public static DingRobotResponseMsg notifyRobot(DingRobotRequest dingRobotRequest, long currentTimeMillis) throws Exception { Map<String, Object> param = buildParam(dingRobotRequest, currentTimeMillis); String s = buildParamUrl(param); // 釘釘的請求參數須要拼接到URL連接 dingRobotRequest.setUrl(String.format("%s?%s", dingRobotRequest.getUrl(), s)); HttpConfig httpConfig = buildDefaultHttpConfig(dingRobotRequest, dingRobotRequest.getMsg()); return parseResponse(notifyRobot(httpConfig)); } /** * 轉化爲對應對象 * * @param notifyRobot 轉化JSON * @return */ private static DingRobotResponseMsg parseResponse(String notifyRobot) { try { return JSON.parseObject(notifyRobot, DingRobotResponseMsg.class); } catch (Exception e) { LOGGER.error("類型轉化失敗,失敗緣由爲:{}", e.getMessage()); throw e; } } /** * 按照自定時間戳進行通知 * * @param dingRobotRequest 釘釘機器人請求 * @throws Exception */ public static DingRobotResponseMsg notifyRobot(DingRobotRequest dingRobotRequest) throws Exception { long currentTimeMillis = System.currentTimeMillis(); return notifyRobot(dingRobotRequest, currentTimeMillis); } /** * 構建請求環境參數 * * @param dingRobotRequest 請求request * @param currentTimeMillis 當前時間戳 * @return * @throws Exception */ private static Map<String, Object> buildParam(DingRobotRequest dingRobotRequest, long currentTimeMillis) throws Exception { Map<String, Object> param = new HashMap<>(3); param.put("access_token", dingRobotRequest.getAccessToken()); param.put("timestamp", currentTimeMillis); param.put("sign", generateSign(currentTimeMillis, dingRobotRequest.getSecret())); return param; } /** * 釘釘機器人的默認配置 * * @param dingRobotRequest 釘釘機器人請求對象 * @param dingRobotRequestMsg 釘釘機器人請求實體 * @return */ private static HttpConfig buildDefaultHttpConfig(DingRobotRequest dingRobotRequest, DingRobotRequestAble dingRobotRequestMsg) { return HttpConfig.custom().headers(defaultBasicHeader()) .url(dingRobotRequest.getUrl()) .encoding("UTF-8") .method(HttpMethods.POST) .json(JSON.toJSONString(dingRobotRequestMsg)); } /** * 默認headers配置 * * @return */ private static Header[] defaultBasicHeader() { Header[] headers = new Header[1]; headers[0] = new BasicHeader("Content-Type", "application/json"); return headers; } private static String notifyRobot(HttpConfig httpConfig) throws Exception { String send = ""; try { send = HttpClientUtil.send(httpConfig); } catch (Exception e) { LOGGER.error("HTTPClient請求發送失敗, 失敗緣由爲:{}", e.getMessage()); throw e; } return send; } /** * 根據時間戳和祕鑰生成一份簽名 * * @param timestamp 時間戳 * @param secret 祕鑰 * @return * @throws Exception */ private static String generateSign(Long timestamp, String secret) throws Exception { String stringToSign = timestamp + "\n" + secret; Mac mac = Mac.getInstance("HmacSHA256"); mac.init(new SecretKeySpec(secret.getBytes(StandardCharsets.UTF_8), "HmacSHA256")); byte[] signData = mac.doFinal(stringToSign.getBytes(StandardCharsets.UTF_8)); return URLEncoder.encode(new String(Base64.encodeBase64(signData)), "UTF-8"); } /** * 構建URL參數 * * @param param 請求MAP參數 * @return */ private static String buildParamUrl(Map<String, Object> param) { if (null == param || param.size() == 0) { return ""; } StringBuilder stringBuilder = new StringBuilder(); param.forEach((key, value) -> { stringBuilder.append(key).append("=").append(value); stringBuilder.append("&"); }); stringBuilder.deleteCharAt(stringBuilder.length() - 1); return stringBuilder.toString(); } }
下面是本工具類的使用方式,只須要傳入環境參數而且傳入必須的請求msg,就能夠直接發送請求而且返回對應的結果。
DingRobotRequest.Builder builder = new DingRobotRequest.Builder(); DingRobotRequest build = builder.secret("SEC2e67120c5e4affa1177ac25fe8dc77ba1c5b49284a9dc7e1888770bc3b76b1fc") .url("https://oapi.dingtalk.com/robot/send") .accessToken("381c2f405e0f906fd556b27cea9f66864120860b5d8b117bb046e10b6599b050") .msg(generateActionCard()).build(); try { DingRobotResponseMsg dingRobotResponseMsg = DingRobotUtils.notifyRobot(build); System.err.println(JSON.toJSONString(dingRobotResponseMsg)); } catch (Exception e) { e.printStackTrace(); }
至此,一個工具類構建就完成了,整個構建的過程仍是十分簡單的。此次的工具代碼也是不斷進行小改動的成果。我的的代碼水平功底有限,若是有什麼意見歡迎點評。
下面彙總了一些我的使用釘釘花的時間比較多的點。
吐槽:其實我的感受釘釘的機器人在錯誤碼這一塊並非特別的直觀,下面說下我的踩到的一些小坑。
31000
的問題若是在添加機器人的時候進行加簽是須要加入對應的sign
和timestamp
參數才能夠測試成功,這裏我的卡了一下子才明白設計者的意圖,雖然很好理解,可是對於第一次使用的人不是十分友好,同時在文檔裏面明顯對於這一塊的描述比較少,這裏提供一下我的的小坑說明:
首先,咱們須要根據請求的時間戳和祕鑰生成簽名
..... /** * 構建當前的系統時間戳 */ @Test public void generateSystemCurrentTime() throws Exception { long l = System.currentTimeMillis(); String secret = "SEC2e67120c5e4affa1177ac25fe8dc77ba1c5b49284a9dc7e1888770bc3b76b1fc"; String sign = generateSign(l, secret); System.out.println("timestamp = "+ l); System.out.println("sign = " + sign); } private String generateSign(Long timestamp, String secret) throws Exception { String stringToSign = timestamp + "\n" + secret; Mac mac = Mac.getInstance("HmacSHA256"); mac.init(new SecretKeySpec(secret.getBytes(StandardCharsets.UTF_8), "HmacSHA256")); byte[] signData = mac.doFinal(stringToSign.getBytes(StandardCharsets.UTF_8)); return URLEncoder.encode(new String(Base64.encodeBase64(signData)), "UTF-8"); } ....
生成簽名以後,咱們須要把時間戳和簽名放入到請求的URL參數裏面,測試方可經過:
https://oapi.dingtalk.com/robot/send?access_token=381c2f405e0f906fd556b27cea9f66864120860b5d8b117bb046e10b6599b050×tamp=1613212722591&sign=SsKKlkvwM%2F4tsCPE6YoGls8vgkQqWJGHYpvWbW7hTGM%3D
提示:仍是注意一下,在設置裏面增長了加簽
本文主要爲記錄我的使用釘釘的一些心得體會,以及以此編寫了一個工具包方便之後有須要的時候能夠直接拿來使用。
釘釘機器人的使用就告一段落了,目前工具類已經應用到公司項目正常的發送請求通知。後續看心情對於HttpClient請求工具類重構,可是目前我的還在參考和學習設計記錄,發現能夠拆分的對象仍是很多的。包含請求方法,請求Header,請求編碼等各類形式的轉化。
最後,我的最近從《代碼簡潔之道》裏面學習了不少有用的編程技巧和編寫代碼的細節問題,推薦讀者看一看這本書,對於寫出一個好代碼和好註釋或者想要學習改良本身的代碼都是頗有好處的,後續我的也會寫一篇學習筆記,感興趣的能夠關注一波。