网络知识 娱乐 WxJava微信公众号开发实战

WxJava微信公众号开发实战

本文从本人博客搬运,原文格式更加美观,可以移步原文阅读:WxJava微信公众号开发实战

之前我们介绍了Java如何进行微信公众号开发,阅读本文前小伙伴们可以先去了解下Java微信公众号开发

之前我们开发微信公众号时,都是要手动自己实现微信接收、响应消息的封装,消息类型的判断,access_token的过期时间管理等等,还是比较麻烦的。有没有已经封装好的开源项目来简化开发呢?这里推荐WxJava,它的地址如下:

  • github:https://github.com/Wechat-Group/WxJava

  • gitee:https://gitee.com/binary/weixin-java-tools

WxJava介绍

WxJava是一个java的微信开发工具包,支持包括微信支付、开放平台、公众号、企业微信/企业号、小程序等微信功能的后端开发,对微信开发相关内容进行了高度封装,极大简化了我们的编码

要使用WxJava只需要引入相关模块的maven依赖即可

<dependency>
  <groupId>com.github.binarywang</groupId>
  <artifactId>(不同模块参考下文)</artifactId>
  <version>4.0.0</version>
</dependency>

不同模块的名称如下:

  • 微信小程序:weixin-java-miniapp
  • 微信支付:weixin-java-pay
  • 微信开放平台:weixin-java-open
  • 公众号(包括订阅号和服务号):weixin-java-mp
  • 企业号/企业微信:weixin-java-cp

WxJava微信公众号开发

1.环境准备

创建Springboot工程,引入WxJava微信公众号整合springboot的依赖

<dependency>
    <groupId>com.github.binarywang</groupId>
    <artifactId>wx-java-mp-spring-boot-starter</artifactId>
    <version>4.0.0</version>
</dependency>

yml中配置微信公众号必要信息

wx:
  mp:
    app-id: appid
    secret: secret
    token: token # 配置消息回调地址接入公众号时需要的token
server:
  port: 8088

编写测试Controller

@RestController
@RequestMapping("/wxjava/mp")
@Slf4j
public class WxjavaTestController {
    @Autowired
    private WxMpService wxMpService;

    @Autowired
    private WxMpConfigStorage wxMpConfigStorage;

    @GetMapping("test")
    public void testAutowire(){
        System.out.println(wxMpService);
        System.out.println(wxMpConfigStorage);
    }
}

wx-java-mp-spring-boot-starter主要帮我们自动配置了如下两个对象:

  • WxMpService:可以完成微信公众号提供的各种功能
  • WxMpConfigStorage:保存了微信公众号配置信息

运行访问测试接口,打印如下

me.chanjar.weixin.mp.api.impl.WxMpServiceHttpClientImpl@17c0a627
{"appId":"...","secret":"...","token":"...","expiresTime":0,"httpProxyPort":0,"jsapiTicketExpiresTime":0,"sdkTicketExpiresTime":0,"cardApiTicketExpiresTime":0,"accessTokenLock":{"sync":{"state":0}},"jsapiTicketLock":{"sync":{"state":0}},"sdkTicketLock":{"sync":{"state":0}},"cardApiTicketLock":{"sync":{"state":0}}}

2.接入微信公众号回调

在Controller中添加接入方法

@GetMapping("message")
public String configAccess(String signature,String timestamp,String nonce,String echostr) {
    // 校验签名
    if (wxMpService.checkSignature(timestamp, nonce, signature)){
        // 校验成功原样返回echostr
        return echostr;
    }
    // 校验失败
    return null;
}

可以看出,与之前我们自己实现复杂的校验流程相比,代码简洁了很多

3.接收与响应消息

WxJava为了对不同类型的微信消息进行分类处理,避免出现很多if/else判断,设计了如下的消息处理流程:

  1. 针对不同类型的消息处理,我们需要自己实现消息处理器,消息处理器必须实现WxMpMessageHandler接口
public interface WxMpMessageHandler {

  /**
   * 处理微信推送消息.
   *
   * @param wxMessage      微信推送消息
   * @param context        上下文,如果handler或interceptor之间有信息要传递,可以用这个
   * @param wxMpService    服务类
   * @param sessionManager session管理器
   * @return xml格式的消息,如果在异步规则里处理的话,可以返回null
   * @throws WxErrorException 异常
   */
  WxMpXmlOutMessage handle(WxMpXmlMessage wxMessage,
                           Map<String, Object> context,
                           WxMpService wxMpService,
                           WxSessionManager sessionManager) throws WxErrorException;

}
  1. 自定义路由,将不同类型的消息交给不同的消息处理器来处理。路由对象为WxMpMessageRouter

根据上述流程,我们首先定义2个消息处理器,分别实现对文本消息和图片消息的处理,并存入Spring容器

// 文本消息处理器
@Component
public class TextHandler implements WxMpMessageHandler {
    @Override
    public WxMpXmlOutMessage handle(WxMpXmlMessage wxMessage, Map<String, Object> context, WxMpService wxMpService, WxSessionManager sessionManager) throws WxErrorException {
        // 接收的消息内容
        String inContent = wxMessage.getContent();
        // 响应的消息内容
        String outContent;
        // 根据不同的关键字回复消息
        if (inContent.contains("游戏")){
            outContent = "仙剑奇侠传";
        }else if (inContent.contains("动漫")){
            outContent = "进击的巨人";
        }else {
            outContent = inContent;
        }
        // 构造响应消息对象
        return WxMpXmlOutMessage.TEXT().content(outContent).fromUser(wxMessage.getToUser())
                .toUser(wxMessage.getFromUser()).build();
    }
}

// 图片消息处理器
@Component
public class ImageHandler implements WxMpMessageHandler {
    @Override
    public WxMpXmlOutMessage handle(WxMpXmlMessage wxMessage, Map<String, Object> context, WxMpService wxMpService, WxSessionManager sessionManager) throws WxErrorException {
        // 原样返回收到的图片
        return WxMpXmlOutMessage.IMAGE().fromUser(wxMessage.getToUser()).toUser(wxMessage.getFromUser())
                .mediaId(wxMessage.getMediaId()).build();
    }
}

然后创建配置类,创建一个WxMpMessageRouter,指定消息路由规则,并将其存入Spring容器

@Configuration
public class WxJavaConfig {
    @Autowired
    private WxMpService wxMpService;

    @Autowired
    private TextHandler textHandler;

    @Autowired
    private ImageHandler imageHandler;

    @Bean
    public WxMpMessageRouter messageRouter() {
        // 创建消息路由
        final WxMpMessageRouter router = new WxMpMessageRouter(wxMpService);
        // 添加文本消息路由
        router.rule().async(false).msgType(WxConsts.XmlMsgType.TEXT).handler(textHandler).end();
        // 添加图片消息路由
        router.rule().async(false).msgType(WxConsts.XmlMsgType.IMAGE).handler(imageHandler).end();
        return router;
    }
}

最后在Controller中注入WxMpMessageRouter,定义消息处理方法,将消息路由到对应的处理器

@PostMapping(value = "message", produces = "application/xml; charset=UTF-8")
public String handleMessage(@RequestBody String requestBody,
                            @RequestParam("signature") String signature,
                            @RequestParam("timestamp") String timestamp,
                            @RequestParam("nonce") String nonce) {
    log.info("handleMessage调用");
    // 校验消息是否来自微信
    if (!wxMpService.checkSignature(timestamp, nonce, signature)) {
        throw new IllegalArgumentException("非法请求,可能属于伪造的请求!");
    }
    // 解析消息体,封装为对象
    WxMpXmlMessage inMessage = WxMpXmlMessage.fromXml(requestBody);
    WxMpXmlOutMessage outMessage;
    try {
        // 将消息路由给对应的处理器,获取响应
        outMessage = wxMpMessageRouter.route(inMessage);
    } catch (Exception e) {
        log.error("微信消息路由异常", e);
        outMessage = null;
    }
    // 将响应消息转换为xml格式返回
    return outMessage == null ? "" : outMessage.toXml();
}

4.事件推送

我们来实现一个接收关注、取消关注事件推送的处理。首先定义关注、取消关注消息处理器,存入容器

// 关注处理器
@Component
@Slf4j
public class SubscribeHandler implements WxMpMessageHandler {
    @Override
    public WxMpXmlOutMessage handle(WxMpXmlMessage wxMessage, Map<String, Object> context, WxMpService wxMpService, WxSessionManager sessionManager) throws WxErrorException {
        log.info("SubscribeHandler调用");
        return WxMpXmlOutMessage.TEXT().fromUser(wxMessage.getToUser()).toUser(wxMessage.getFromUser())
                .content("欢迎关注").build();
    }
}

// 取消关注处理器
@Component
@Slf4j
public class UnSubscribeHandler implements WxMpMessageHandler {
    @Override
    public WxMpXmlOutMessage handle(WxMpXmlMessage wxMessage, Map<String, Object> context, WxMpService wxMpService, WxSessionManager sessionManager) throws WxErrorException {
        log.info("UnSubscribeHandler调用");
        // 因为已经取消关注,所以即使回复消息也收不到
        return WxMpXmlOutMessage.TEXT().fromUser(wxMessage.getToUser()).toUser(wxMessage.getFromUser())
                .content("请别离开我").build();
    }
}

然后修改路由,添加对应的推送消息与处理器的关联

5.自定义菜单

我们来尝试创建2个按钮,第2个按钮包含3个子按钮,只要用WxJava封装好的菜单和按钮对象,非常方便:

  • WxMenu:菜单对象
  • WxMenuButton:按钮对象
@GetMapping("createMenu")
public String createMenu() throws WxErrorException {
    // 创建菜单对象
    WxMenu menu = new WxMenu();
    // 创建按钮1
    WxMenuButton button1 = new WxMenuButton();
    button1.setType(WxConsts.MenuButtonType.CLICK);
    button1.setName("今日歌曲");
    button1.setKey("V1001_TODAY_MUSIC");
    // 创建按钮2
    WxMenuButton button2 = new WxMenuButton();
    button2.setName("菜单");
    // 创建按钮2的子按钮1
    WxMenuButton button21 = new WxMenuButton();
    button21.setType(WxConsts.MenuButtonType.VIEW);
    button21.setName("搜索");
    button21.setUrl("https://www.baidu.com/");
    // 创建按钮2的子按钮2
    WxMenuButton button22 = new WxMenuButton();
    button22.setType(WxConsts.MenuButtonType.VIEW);
    button22.setName("视频");
    button22.setUrl("https://v.qq.com/");
    // 创建按钮2的子按钮3
    WxMenuButton button23 = new WxMenuButton();
    button23.setType(WxConsts.MenuButtonType.CLICK);
    button23.setName("赞一下我们");
    button23.setKey("V1001_GOOD");
    // 将子按钮添加到按钮2
    button2.getSubButtons().add(button21);
    button2.getSubButtons().add(button22);
    button2.getSubButtons().add(button23);
    // 将按钮1和你按钮2添加到菜单
    menu.getButtons().add(button1);
    menu.getButtons().add(button2);
    // 创建按钮
    return wxMpService.getMenuService().menuCreate(menu);
}

成功创建菜单后效果如下

6.发送模板消息

测试模板如下

在controller中添加发送模板消息的方法

@GetMapping("sendTemplateMessage")
public String sendTemplateMessage() throws WxErrorException {
    // 创建模板消息,设置模板id、指定模板消息要发送的目标用户
    WxMpTemplateMessage wxMpTemplateMessage = WxMpTemplateMessage.builder()
        .templateId("uCl1-JREW8k1vW084PTcFmrvMvFJX9H2Xs51gQeGG2I")
        .toUser("ommzW5192wiIazYpp2WRzcsL_6Vk")
        .build();
    // 填充模板消息中的变量
    wxMpTemplateMessage.addData(new WxMpTemplateData("goodsName", "华为mate40pro"));
    wxMpTemplateMessage.addData(new WxMpTemplateData("time", "2020-10-25"));
    wxMpTemplateMessage.addData(new WxMpTemplateData("price", "6999"));
    wxMpTemplateMessage.addData(new WxMpTemplateData("remark", "麒麟9000牛逼"));
    // 发送模板消息,返回消息id
    return wxMpService.getTemplateMsgService().sendTemplateMsg(wxMpTemplateMessage);
}

发送成功后效果如下

7.将accesstoken持久化到redis

默认情况下,微信相关配置都会保存到内存中的WxMpConfigStorage对象。比如我们创建自定义菜单、发送模板消息都需要首先获取access_token,而WxJava会将获取access_token的过程封装到对应的api中,比如我们发送模板消息前后输出WxMpConfigStorage,会发现多了access_token的数据

但是这样做的话,每次重启项目,原先的access_token就失效了,即便它可能还没有过期。此时需要重新获取access_token,这样做的坏处是:

  • 需要重新发起网络请求,效率低
  • 获取access_token的微信接口有调用次数限制,没过期就调用可能会因为达到次数上限而获取失败
  • 如果是分布式的环境下,每个服务都要各自去获取access_token,没有必要

所以我们可以将access_token持久化到redis。在yaml中增加redis配置

wx:
  mp:
    app-id: ...
    secret: ...
    token: ...
    config-storage:
      type: redistemplate
spring:
  redis:
    host: 192.168.157.130
    port: 6379
    database: 0
server:
  port: 8088

注意,wx.mp.config-storage.type只有选择jedis,wxjava才会读取自己配置的redis连接信息,利用jedis连接。如果配置为redisTemplate,那么wxjava将忽略自己的redis连接信息,使用Spring容器中的redisTemplate操作redis,所以此时要引入Springboot整合redis的依赖

添加redis依赖

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>

此时再调用发送模板消息等需要access_token的接口之前,会先获取到token并存入redis

我们分别在调用前后打印access_token

可以发现存入redis的token有效期大约为2小时,与微信官方一致。此时重新启动项目后,如果token没有过期,会从redis中取出token,直接使用,而不是再次调用接口获取

我们可以发现,access_token存入redis的key为wx:access_token:公众号的appid,这也是为什么重启后可以再次从redis中获取到

8.网页授权

首先要构建授权页面url

@GetMapping("buildAuthPage")
public String auth() {
    WxOAuth2Service oAuth2Service = wxMpService.getOAuth2Service();
    // 构建授权url
    return oAuth2Service.buildAuthorizationUrl("https://baobao.cn.utools.club/wxjava/mp/callback", 
                                               WxConsts.OAuth2Scope.SNSAPI_USERINFO, null);
}

其中buildAuthorizationUrl的说明如下:

/**
   * 
   * 构造oauth2授权的url连接.
   * 详情请见: http://mp.weixin.qq.com/wiki/index.php?title=网页授权获取用户基本信息
   * 
* * @param redirectUri 用户授权完成后的重定向链接,无需urlencode, 方法内会进行encode * @param scope scope * @param state state * @return url */
String buildAuthorizationUrl(String redirectUri, String scope, String state);

然后编写用户确认授权后的回调处理,利用code获取accessToken,再利用accessToken获取用户信息

@GetMapping("callback")
public WxOAuth2UserInfo callback(String code) throws WxErrorException {
    WxOAuth2Service oAuth2Service = wxMpService.getOAuth2Service();
    // 利用code获取accessToken
    WxOAuth2AccessToken accessToken = oAuth2Service.getAccessToken(code);
    // 利用accessToken获取用户信息
    WxOAuth2UserInfo userInfo = oAuth2Service.getUserInfo(accessToken, null);
    return<