杰瑞科技汇

Java如何开发微信公众号?

目录

  1. 准备工作:注册公众号与获取凭证
    • 1 注册微信公众平台账号
    • 2 获取开发者凭证(AppID 和 AppSecret)
    • 3 服务器配置(服务器地址与Token)
  2. 技术选型与项目搭建
    • 1 技术栈选择
    • 2 创建Spring Boot项目
  3. 核心开发:接入微信公众平台
    • 1 微信服务器验证原理
    • 2 编写Token验证接口
    • 3 本地调试与穿透工具(Ngrok)
  4. 接收与处理用户消息
    • 1 消息与事件推送的XML格式
    • 2 解析微信服务器推送的XML
    • 3 处理文本消息
    • 4 处理菜单点击事件
  5. 回复用户消息
    • 1 回复文本消息
    • 2 回复图文消息
  6. 进阶功能
    • 1 获取Access Token
    • 2 菜单管理(创建自定义菜单)
    • 3 用户管理(获取用户信息)
  7. 完整代码示例
  8. 总结与后续学习

准备工作:注册公众号与获取凭证

在开始编码之前,你必须完成以下步骤。

Java如何开发微信公众号?-图1
(图片来源网络,侵删)

1 注册微信公众平台账号

  1. 访问 微信公众平台
  2. 点击“立即注册”,选择“服务号”(企业、政府、非营利组织等主体注册,个人只能注册订阅号,功能受限)。
  3. 按照提示完成邮箱验证、信息登记、主体认证等流程。注意:服务号需要进行微信认证(每年300元)才能获得发送模板消息等高级接口权限。 本教程的基础功能认证后即可使用。

2 获取开发者凭证

  1. 登录你的公众号后台。
  2. 在左侧菜单栏找到“设置与开发” -> “基本配置”。
  3. 你可以找到 AppID(应用ID)AppSecret(应用密钥),这两个是调用微信API的核心凭证,请妥善保管。

3 服务器配置

  1. 在“基本配置”页面,找到“服务器配置”。
  2. 点击“修改配置”。
  3. 填写以下信息:
    • URL: 你的服务器上用于接收微信消息和事件的接口地址。https://yourdomain.com/wechat注意:必须是httphttps协议,不能是IP地址,且端口必须是80(http)或443(https)。
    • Token: 可以任意填写,用作验证服务器地址的有效性。myWechatToken你需要在自己的代码中保存这个Token值。
    • EncodingAESKey: 随机生成或手动填写,用于消息加解密,暂时可以留空,选择“安全模式”或“兼容模式”后需要配置。
    • 消息加解密方式: 初学者可以先选择“明文模式”,方便调试。

技术选型与项目搭建

1 技术栈选择

  • 后端框架: Spring Boot (简化配置,快速开发)
  • Web框架: Spring Web (用于创建HTTP接口)
  • XML处理: Jackson XML (Spring Boot官方推荐,用于解析和生成XML)
  • HTTP客户端: OkHttp (用于调用微信API)
  • 构建工具: MavenGradle

2 创建Spring Boot项目

  1. 访问 Spring Initializr
  2. 填写项目信息:
    • Project: Maven Project
    • Language: Java
    • Spring Boot: 选择一个稳定版本(如 2.7.x 或 3.x.x)
    • Project Metadata: Group, Artifact, Name, Description 等。
    • Dependencies: 添加以下依赖:
      • Spring Web: 用于构建Web应用。
      • Spring Boot DevTools: 用于热部署,方便开发。
      • Jackson XML: 用于处理XML数据。
  3. 点击“GENERATE”下载项目压缩包,并用你的IDE(如IntelliJ IDEA或Eclipse)打开。

pom.xml 中,确保有 jackson-dataformat-xml 依赖:

<dependency>
    <groupId>com.fasterxml.jackson.dataformat</groupId>
    <artifactId>jackson-dataformat-xml</artifactId>
</dependency>

核心开发:接入微信公众平台

1 微信服务器验证原理

当你填写完服务器配置并提交后,微信服务器会向你的 URL 发送一个 GET 请求,请求中包含四个参数:

  • signature: 微信加密签名
  • timestamp: 时间戳
  • nonce: 随机数
  • echostr: 随机字符串

你的服务器需要做三件事来验证请求的合法性:

  1. tokentimestampnonce 三个参数进行字典序排序。
  2. 将三个参数字符串拼接成一个字符串进行 SHA1 加密。
  3. 将加密后的字符串与 signature 进行对比,如果相同,则说明请求来自微信,返回 echostr 即可接入成功。

2 编写Token验证接口

在你的Spring Boot项目中,创建一个Controller来处理这个验证请求。

Java如何开发微信公众号?-图2
(图片来源网络,侵删)
// WechatController.java
package com.example.demo.controller;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.util.Arrays;
@RestController
@RequestMapping("/wechat")
public class WechatController {
    // 这里填写你在公众号后台配置的Token
    private final String TOKEN = "myWechatToken";
    @GetMapping
    public String validate(String signature, String timestamp, String nonce, String echostr) {
        // 1. 将token, timestamp, nonce三个参数进行字典序排序
        String[] arr = new String[]{TOKEN, timestamp, nonce};
        Arrays.sort(arr);
        // 2. 将三个参数字符串拼接成一个字符串进行sha1加密
        StringBuilder content = new StringBuilder();
        for (String s : arr) {
            content.append(s);
        }
        String tmpStr = null;
        try {
            MessageDigest md = MessageDigest.getInstance("SHA-1");
            byte[] digest = md.digest(content.toString().getBytes());
            tmpStr = bytesToHex(digest);
        } catch (NoSuchAlgorithmException e) {
            e.printStackTrace();
        }
        // 3. 将sha1加密后的字符串与signature进行对比
        if (tmpStr != null && tmpStr.equals(signature)) {
            // 请求来自微信,验证成功,返回echostr
            return echostr;
        } else {
            // 验证失败
            return "error";
        }
    }
    // 字节数组转十六进制字符串
    private String bytesToHex(byte[] bytes) {
        StringBuilder hexStr = new StringBuilder();
        for (byte b : bytes) {
            String hex = Integer.toHexString(b & 0xFF);
            if (hex.length() == 1) {
                hexStr.append('0');
            }
            hexStr.append(hex);
        }
        return hexStr.toString();
    }
}

3 本地调试与穿透工具

由于公众号要求服务器是公网可访问的,而我们开发时在本地运行,所以需要使用内网穿透工具。

  1. 下载Ngrok: 访问 ngrok.com 下载适合你系统的版本。
  2. 注册并获取Authtoken: 注册后,在你的Dashboard页面找到Authtoken。
  3. 启动Ngrok: 在你的项目根目录(或包含jar文件的目录)打开终端,运行以下命令:
    ngrok http 8080 

    (假设你的Spring Boot应用运行在8080端口)

  4. 获取公网URL: Ngrok启动后,会给你两个公网URL(一个HTTP,一个HTTPS)。请使用 https 开头的那个
  5. 修改服务器配置: 回到微信公众平台,在“服务器配置”中,将 URL 修改为 Ngrok提供的HTTPS地址,https://1a2b-3c4d-5e6f.ngrok-free.app/wechat,Token填入你代码中的值。
  6. 提交: 点击“提交”,如果配置正确,会提示“成功”。

当你启动Spring Boot应用后,微信服务器就可以通过Ngrok访问到你的本地接口了。


接收与处理用户消息

当用户在公众号里给你发消息或点击菜单时,微信服务器会向你的 URL 发送一个 POST 请求,请求体是XML格式的数据。

1 消息与事件推送的XML格式

文本消息示例:

<xml>
  <ToUserName><![CDATA[toUser]]></ToUserName>
  <FromUserName><![CDATA[fromUser]]></FromUserName>
  <CreateTime>1348831860</CreateTime>
  <MsgType><![CDATA[text]]></MsgType>
  <Content><![CDATA[this is a test]]></Content>
  <MsgId>1234567890123456</MsgId>
</xml>

菜单点击事件示例:

<xml>
  <ToUserName><![CDATA[toUser]]></ToUserName>
  <FromUserName><![CDATA[fromUser]]></FromUserName>
  <CreateTime>1348831860</CreateTime>
  <MsgType><![CDATA[event]]></MsgType>
  <Event><![CDATA[CLICK]]></Event>
  <EventKey><![CDATA[V1001_TODAY_MUSIC]]></EventKey>
</xml>

2 解析微信服务器推送的XML

我们需要创建Java类来映射这些XML结构。

// WechatMessage.java (基础消息类)
package com.example.demo.model;
import com.fasterxml.jackson.dataformat.xml.annotation.JacksonXmlProperty;
import com.fasterxml.jackson.dataformat.xml.annotation.JacksonXmlRootElement;
@JacksonXmlRootElement(localName = "xml")
public class WechatMessage {
    @JacksonXmlProperty(localName = "ToUserName")
    private String toUserName;
    @JacksonXmlProperty(localName = "FromUserName")
    private String fromUserName;
    @JacksonXmlProperty(localName = "CreateTime")
    private Long createTime;
    @JacksonXmlProperty(localName = "MsgType")
    private String msgType;
    // ... getters and setters
}
// TextMessage.java
package com.example.demo.model;
public class TextMessage extends WechatMessage {
    @JacksonXmlProperty(localName = "Content")
    private String content;
    // ... getters and setters
}
// EventMessage.java
package com.example.demo.model;
public class EventMessage extends WechatMessage {
    @JacksonXmlProperty(localName = "Event")
    private String event;
    @JacksonXmlProperty(localName = "EventKey")
    private String eventKey;
    // ... getters and setters
}

3 处理文本消息

修改 WechatController,添加一个 POST 方法来处理消息。

// WechatController.java (添加新方法)
package com.example.demo.controller;
// ... imports ...
import com.example.demo.model.TextMessage;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RestController;
// ... other parts of the class ...
@PostMapping
public String handleWechatPost(String signature, String timestamp, String nonce, String openid, String encrypt_type, String msg_signature, @RequestBody String xmlData) {
    // 注意:在安全模式下,需要处理加密和解密,这里我们假设是明文模式。
    // 1. 解析XML数据
    ObjectMapper objectMapper = new ObjectMapper();
    TextMessage textMessage = null;
    try {
        // 使用Jackson解析XML到TextMessage对象
        // 注意:Jackson XML需要正确的配置来处理CDATA,这里简化处理
        // 更健壮的方式是使用JAXB或专门的XML库
        // 这里为了简化,我们直接处理字符串
        System.out.println("Received XML: " + xmlData);
        // 手动解析Content等字段(仅作演示,生产环境请用更健壮的库)
        if (xmlData.contains("<MsgType><![CDATA[text]]></MsgType>")) {
            String content = xmlData.substring(xmlData.indexOf("<><![CDATA[") + 9, xmlData.indexOf("]]></Content>"));
            String fromUser = xmlData.substring(xmlData.indexOf("<FromUserName><![CDATA[") + 20, xmlData.indexOf("]]></FromUserName>"));
            String toUser = xmlData.substring(xmlData.indexOf("<ToUserName><![CDATA[") + 18, xmlData.indexOf("]]></ToUserName>"));
            textMessage = new TextMessage();
            textMessage.setFromUserName(fromUser);
            textMessage.setToUserName(toUser);
            textMessage.setMsgType("text");
            textMessage.setContent(content);
            textMessage.setCreateTime(System.currentTimeMillis() / 1000);
        }
    } catch (Exception e) {
        e.printStackTrace();
        return "success"; // 即使处理失败,也返回success,避免微信重复推送
    }
    // 2. 处理消息
    if (textMessage != null && "text".equals(textMessage.getMsgType())) {
        String replyContent = "你发送了: " + textMessage.getContent();
        // 3. 构造回复消息
        String replyXml = buildTextReply(textMessage.getFromUserName(), textMessage.getToUserName(), replyContent);
        return replyXml;
    }
    // 如果是其他类型消息或事件,返回success
    return "success";
}
// 构造回复文本消息的XML
private String buildTextReply(String toUser, String fromUser, String content) {
    return "<xml>" +
            "<ToUserName><![CDATA[" + toUser + "]]></ToUserName>" +
            "<FromUserName><![CDATA[" + fromUser + "]]></FromUserName>" +
            "<CreateTime>" + System.currentTimeMillis() / 1000 + "</CreateTime>" +
            "<MsgType><![CDATA[text]]></MsgType>" +
            "<Content><![CDATA[" + content + "]]></Content>" +
            "</xml>";
}

注意: 上述代码中手动解析XML只是为了演示。在生产环境中,强烈建议使用JAXB或Jackson的更高级配置来自动解析XML,这样代码更健壮、更易维护,这里为了简化教程,使用了字符串截取。

4 处理菜单点击事件

同样,在 handleWechatPost 方法中增加对事件的处理逻辑。

// 在handleWechatPost方法中,解析XML后增加事件处理逻辑
// ...
// 在解析TextMessage之后,检查是否是事件
if (xmlData.contains("<MsgType><![CDATA[event]]></MsgType>")) {
    String fromUser = xmlData.substring(xmlData.indexOf("<FromUserName><![CDATA[") + 20, xmlData.indexOf("]]></FromUserName>"));
    String toUser = xmlData.substring(xmlData.indexOf("<ToUserName><![CDATA[") + 18, xmlData.indexOf("]]></ToUserName>"));
    String event = xmlData.substring(xmlData.indexOf("<Event><![CDATA[") + 11, xmlData.indexOf("]]></Event>"));
    String eventKey = xmlData.substring(xmlData.indexOf("<EventKey><![CDATA[") + 14, xmlData.indexOf("]]></EventKey>"));
    if ("CLICK".equals(event)) {
        String replyContent = "你点击了菜单: " + eventKey;
        String replyXml = buildTextReply(fromUser, toUser, replyContent);
        return replyXml;
    }
}
// ... 其他代码 ...

回复用户消息

1 回复文本消息

上面已经演示了如何构造和回复文本消息,核心是构造一个符合微信规范的XML,并通过HTTP POST响应返回给微信服务器。

2 回复图文消息

图文消息更复杂一些,它包含一个图文消息列表。

图文消息XML结构:

<xml>
  <ToUserName><![CDATA[toUser]]></ToUserName>
  <FromUserName><![CDATA[fromUser]]></FromUserName>
  <CreateTime>1348831860</CreateTime>
  <MsgType><![CDATA[news]]></MsgType>
  <ArticleCount>2</ArticleCount>
  <Articles>
    <item>
      <Title><![CDATA[标题1]]></Title>
      <Description><![CDATA[描述1]]></Description>
      <PicUrl><![CDATA[http://www.example.com/1.jpg]]></PicUrl>
      <Url><![CDATA[http://www.example.com/1]]></Url>
    </item>
    <item>
      <Title><![CDATA[标题2]]></Title>
      <Description><![CDATA[描述2]]></Description>
      <PicUrl><![CDATA[http://www.example.com/2.jpg]]></PicUrl>
      <Url><![CDATA[http://www.example.com/2]]></Url>
    </item>
  </Articles>
</xml>

创建模型类和构建方法:

// Article.java
package com.example.demo.model;
public class Article {
    private String title;
    private String description;
    private String picUrl;
    private String url;
    // getters and setters
}
// NewsMessage.java
package com.example.demo.model;
import java.util.List;
public class NewsMessage extends WechatMessage {
    private Integer articleCount;
    private List<Article> articles;
    // getters and setters
}

构建回复方法:

// 在WechatController中添加
private String buildNewsReply(String toUser, String fromUser, List<Article> articles) {
    StringBuilder sb = new StringBuilder();
    sb.append("<xml>");
    sb.append("<ToUserName><![CDATA[").append(toUser).append("]]></ToUserName>");
    sb.append("<FromUserName><![CDATA[").append(fromUser).append("]]></FromUserName>");
    sb.append("<CreateTime>").append(System.currentTimeMillis() / 1000).append("</CreateTime>");
    sb.append("<MsgType><![CDATA[news]]></MsgType>");
    sb.append("<ArticleCount>").append(articles.size()).append("</ArticleCount>");
    sb.append("<Articles>");
    for (Article article : articles) {
        sb.append("<item>");
        sb.append("<Title><![CDATA[").append(article.getTitle()).append("]]></Title>");
        sb.append("<Description><![CDATA[").append(article.getDescription()).append("]]></Description>");
        sb.append("<PicUrl><![CDATA[").append(article.getPicUrl()).append("]]></PicUrl>");
        sb.append("<Url><![CDATA[").append(article.getUrl()).append("]]></Url>");
        sb.append("</item>");
    }
    sb.append("</Articles>");
    sb.append("</xml>");
    return sb.toString();
}

然后在 handleWechatPost 中调用它:

// 在事件处理部分
if ("CLICK".equals(event) && "V1001_NEWS".equals(eventKey)) { // 假设菜单KEY是V1001_NEWS
    List<Article> articles = new ArrayList<>();
    Article article1 = new Article();
    article1.setTitle("欢迎关注我的公众号");
    article1.setDescription("这是一个图文消息示例");
    article1.setPicUrl("http://your-domain.com/image1.jpg");
    article1.setUrl("http://your-domain.com");
    articles.add(article1);
    String replyXml = buildNewsReply(fromUser, toUser, articles);
    return replyXml;
}

进阶功能

1 获取Access Token

调用绝大多数微信API都需要一个 access_token,它的有效期为2小时,需要全局缓存。

获取方式: GET请求 https://api.weixin.qq.com/cgi-bin/token?grant_type=client_credential&appid=APPID&secret=APPSECRET

Java实现:

// WechatService.java
package com.example.demo.service;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Service;
import okhttp3.OkHttpClient;
import okhttp3.Request;
import okhttp3.Response;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
import java.io.IOException;
@Service
public class WechatService {
    @Value("${wechat.appid}")
    private String appid;
    @Value("${wechat.secret}")
    private String secret;
    private String accessToken;
    private long expireTime;
    private final OkHttpClient client = new OkHttpClient();
    private final ObjectMapper objectMapper = new ObjectMapper();
    public String getAccessToken() throws IOException {
        // 如果token存在且未过期,直接返回
        if (accessToken != null && System.currentTimeMillis() < expireTime) {
            return accessToken;
        }
        // 否则,重新获取
        String url = String.format("https://api.weixin.qq.com/cgi-bin/token?grant_type=client_credential&appid=%s&secret=%s", appid, secret);
        Request request = new Request.Builder().url(url).build();
        try (Response response = client.newCall(request).execute()) {
            if (!response.isSuccessful()) throw new IOException("Unexpected code " + response);
            String responseBody = response.body().string();
            JsonNode jsonNode = objectMapper.readTree(responseBody);
            accessToken = jsonNode.get("access_token").asText();
            expireTime = System.currentTimeMillis() + (jsonNode.get("expires_in").asLong() * 1000);
            return accessToken;
        }
    }
}

application.properties 中配置:

wechat.appid=你的AppID
wechat.secret=你的AppSecret

2 菜单管理

创建自定义菜单也需要调用API,并使用 access_token

创建菜单接口: POST https://api.weixin.qq.com/cgi-bin/menu/create?access_token=ACCESS_TOKEN

请求体是一个JSON格式的菜单结构,你需要根据微信文档构建这个JSON,然后使用OkHttp发送POST请求。

3 用户管理

获取用户列表、用户信息等也需要调用相应API,流程与获取Token类似,都是构造请求、发送请求、解析响应。


完整代码示例

由于篇幅限制,这里提供一个精简版的完整项目结构,你可以基于此进行扩展。

项目结构:

src/main/java/com/example/demo/
├── DemoApplication.java
├── controller/
│   └── WechatController.java
├── model/
│   ├── WechatMessage.java
│   ├── TextMessage.java
│   ├── EventMessage.java
│   └── Article.java
├── service/
│   └── WechatService.java
└── config/
    └── WechatConfig.java (用于存放配置属性)

WechatController.java (最终版整合)

package com.example.demo.controller;
// ... imports ...
import com.example.demo.model.Article;
import com.example.demo.model.TextMessage;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.*;
import java.util.Arrays;
import java.util.List;
@RestController
@RequestMapping("/wechat")
public class WechatController {
    @Value("${wechat.token}")
    private String TOKEN;
    @Autowired
    private WechatService wechatService;
    @GetMapping
    public String validate(String signature, String timestamp, String nonce, String echostr) {
        // ... (验证逻辑与之前相同) ...
        String[] arr = {TOKEN, timestamp, nonce};
        Arrays.sort(arr);
        // ... SHA1比较逻辑 ...
        // 如果匹配,返回echostr
        return echostr;
    }
    @PostMapping
    public String handleWechatPost(@RequestBody String xmlData) {
        // 1. 解析XML
        // (这里简化,实际项目中用Jackson或JAXB)
        System.out.println("Received: " + xmlData);
        String fromUser = extractValue(xmlData, "FromUserName");
        String toUser = extractValue(xmlData, "ToUserName");
        String msgType = extractValue(xmlData, "MsgType");
        String event = extractValue(xmlData, "Event");
        String eventKey = extractValue(xmlData, "EventKey");
        String content = extractValue(xmlData, "Content");
        // 2. 处理消息和事件
        String replyXml = "success"; // 默认成功响应
        if ("text".equals(msgType)) {
            replyXml = buildTextReply(fromUser, toUser, "Echo: " + content);
        } else if ("event".equals(msgType) && "CLICK".equals(event)) {
            if ("ABOUT_US".equals(eventKey)) {
                replyXml = buildTextReply(fromUser, toUser, "关于我们:一个用Java开发的公众号示例。");
            } else if ("SHOW_NEWS".equals(eventKey)) {
                List<Article> articles = Arrays.asList(
                    new Article("新闻标题1", "新闻描述1", "http://example.com/img1.jpg", "http://example.com/link1"),
                    new Article("新闻标题2", "新闻描述2", "http://example.com/img2.jpg", "http://example.com/link2")
                );
                replyXml = buildNewsReply(fromUser, toUser, articles);
            }
        }
        return replyXml;
    }
    // ... buildTextReply, buildNewsReply, bytesToHex, extractValue 等辅助方法 ...
    private String extractValue(String xml, String tag) {
        String startTag = "<" + tag + "><![CDATA[";
        String endTag = "]]></" + tag + ">";
        int startIndex = xml.indexOf(startTag);
        if (startIndex == -1) return "";
        startIndex += startTag.length();
        int endIndex = xml.indexOf(endTag, startIndex);
        if (endIndex == -1) return "";
        return xml.substring(startIndex, endIndex);
    }
    // ... (其他构建XML的方法) ...
}

总结与后续学习

恭喜!你已经完成了微信公众号Java开发的入门,掌握了最核心的接入、接收消息、回复消息流程。

后续可以学习的内容:

  1. 消息加解密: 学习如何配置并处理加密模式下的消息,确保通信安全。
  2. 模板消息: 学习如何向用户发送重要的服务通知,如订单确认、物流更新等。
  3. 网页授权 (OAuth2): 学习如何获取用户的openid和基本信息,实现网页登录。
  4. 微信支付: 集成微信支付功能。
  5. Jenkins/Git CI/CD: 将你的项目部署到云服务器(如阿里云、腾讯云),并实现自动化部署。
  6. 使用更成熟的框架: 考虑使用如 WxJava 这样的成熟开源SDK,它封装了几乎所有微信API,能极大提高开发效率。

这份教程是一个坚实的基础,希望能帮助你顺利开启微信公众号的Java开发之旅!

分享:
扫描分享到社交APP
上一篇
下一篇