微信公眾號開發是 Java 開發者常遇場景。本文詳述從賬號註冊、內網穿透、伺服器驗證到收發消息的完整流程,附帶完整 Servlet 實作代碼,助你快速構建第一個微信應用。
| 類型 | 適用主體 | 群發頻率 | 核心能力 |
|---|---|---|---|
| 訂閱號 | 個人/媒體/組織 | 每天 1 次 | 資訊傳播、品牌宣傳 |
| 服務號 | 企業/政府/組織 | 每月 4 次 | 服務互動、自定義菜單、微信支付、模板消息 |
| 企業號/企業微信 | 企業內部 | 無限制 | OA 辦公、客戶聯繫、應用開放平台 |
建議:初學者可先申請「訂閱號」或使用「微信公眾平台測試賬號」 (無需認證,功能完整)。
微信回調 URL 必須為 公網可訪問的 80/443 端口。開發階段常用工具:
ngrok http 8080 產生 https://xxx.ngrok.io

signature, timestamp, nonce, echostr// 1. 將 token、timestamp、nonce 字典序排序
// 2. 拼接成字串進行 SHA1 加密
// 3. 比對加密結果與 signature
String[] arr = {token, timestamp, nonce};
Arrays.sort(arr);
String content = String.join("", arr);
String sha1 = SHA1.encode(content);
if (sha1.equals(signature)) {
return echostr; // 原樣返回,驗證通過
}
echostr,接入生效org.dom4j dom4j 2.1.4 com.thoughtworks.xstream xstream 1.4.18 javax.servlet javax.servlet-api 4.0.1 provided
public class CheckUtil {
private static final String TOKEN = "your_token_here"; // 與公眾平台配置一致
public static boolean checkSignature(String signature, String timestamp, String nonce) {
String[] arr = {TOKEN, timestamp, nonce};
Arrays.sort(arr);
String content = String.join("", arr);
return SHA1Util.encode(content).equals(signature);
}
}
public class SHA1Util {
public static String encode(String str) {
try {
MessageDigest md = MessageDigest.getInstance("SHA-1");
byte[] digest = md.digest(str.getBytes(StandardCharsets.UTF_8));
return HexFormat.of().formatHex(digest).toLowerCase();
} catch (NoSuchAlgorithmException e) {
throw new RuntimeException("SHA-1 algorithm not found", e);
}
}
}
public class MessageUtil {
// 消息類型常量
public static final String MSG_TEXT = "text";
public static final String MSG_EVENT = "event";
public static final String EVENT_SUBSCRIBE = "subscribe";
/** XML 請求體轉 Map */
public static Map xmlToMap(HttpServletRequest request) throws Exception {
Map map = new HashMap<>();
SAXReader reader = new SAXReader();
Document doc = reader.read(request.getInputStream());
Element root = doc.getRootElement();
for (Element element : root.elements()) {
map.put(element.getName(), element.getText());
}
return map;
}
/** 文本消息 Bean 轉 XML 回覆 */
public static String textMessageToXml(TextMessage msg) {
XStream xstream = new XStream();
xstream.alias("xml", msg.getClass());
xstream.useAttributeFor(TextMessage.class, "MsgType"); // CDATA 處理需額外配置
return xstream.toXML(msg);
}
/** 快速建立文本回覆 */
public static String initText(String toUser, String fromUser, String content) {
TextMessage msg = new TextMessage();
msg.setToUserName(fromUser); // 注意:回覆時 To/From 互換
msg.setFromUserName(toUser);
msg.setCreateTime(System.currentTimeMillis() / 1000); // 秒級時間戳
msg.setMsgType(MSG_TEXT);
msg.setContent(content);
return textMessageToXml(msg);
}
// 菜單回覆內容
public static String menuText() { return "歡迎關注!回覆 \"幫助\" 查看功能。"; }
public static String firstMenu() { return "收到:喜歡你 ❤️"; }
public static String secondMenu() { return "收到:很喜歡你 💕"; }
}
@XStreamAlias("xml")
public class TextMessage {
private String ToUserName;
private String FromUserName;
private long CreateTime;
private String MsgType;
private String Content;
private long MsgId;
// Getter/Setter 省略,建議使用 Lombok @Data
}
@WebServlet("/weixin")
public class WeixinServlet extends HttpServlet {
@Override
protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws IOException {
String signature = req.getParameter("signature");
String timestamp = req.getParameter("timestamp");
String nonce = req.getParameter("nonce");
String echostr = req.getParameter("echostr");
PrintWriter out = resp.getWriter();
if (CheckUtil.checkSignature(signature, timestamp, nonce)) {
out.print(echostr); // 驗證通過原樣返回
}
}
@Override
protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws IOException {
req.setCharacterEncoding("UTF-8");
resp.setCharacterEncoding("UTF-8");
resp.setContentType("application/xml;charset=UTF-8");
PrintWriter out = resp.getWriter();
try {
Map map = MessageUtil.xmlToMap(req);
String msgType = map.get("MsgType");
String fromUser = map.get("FromUserName");
String toUser = map.get("ToUserName");
String reply = null;
// 1. 文本消息處理
if (MessageUtil.MSG_TEXT.equals(msgType)) {
String content = map.get("Content");
if ("喜歡你".equals(content)) {
reply = MessageUtil.initText(toUser, fromUser, MessageUtil.firstMenu());
} else if ("很喜歡你".equals(content)) {
reply = MessageUtil.initText(toUser, fromUser, MessageUtil.secondMenu());
} else if ("?".equals(content) || "?".equals(content)) {
reply = MessageUtil.initText(toUser, fromUser, MessageUtil.menuText());
} else {
reply = MessageUtil.initText(toUser, fromUser, "收到:" + content);
}
}
// 2. 事件推送:關注/取消關注
else if (MessageUtil.MSG_EVENT.equals(msgType)) {
String event = map.get("Event");
if (MessageUtil.EVENT_SUBSCRIBE.equals(event)) {
reply = MessageUtil.initText(toUser, fromUser, MessageUtil.menuText());
}
}
if (reply != null) out.print(reply);
} catch (Exception e) {
log.error("微信消息處理異常", e);
} finally {
out.close();
}
}
}

ContentPicUrl, MediaIdMediaId, Format, Recognition (語音識別結果)MediaId, ThumbMediaIdLocation_X, Location_Y, Scale, LabelTitle, Description, UrlEvent (subscribe/unsubscribe/CLICK/VIEW/掃碼/上報地理位置)
MsgId 或 FromUserName+CreateTime 去重