SpringBoot+Dubbo+环信(即时通信)整合
1.注册环信账号
官网:https://www.easemob.com/ 稳定健壮,消息必达,亿级并发的即时通讯云
2. 了解平台架构
文档地址:http://docs-im.easemob.com/
平台架构:
3. 创建应用
这里选择授权注册,注册需要进行校验,防止随意添加用户
创建成功
创建应用成功后,注意下面标红的几个参数,需要在配置文件中配置一下的参数
4. 后端集成用户体系
文档:http://docs-im.easemob.com/im/server/ready/user
集成环信需要参考上面这个官方开发文档
5. 功能整体流程图
说明:
- 在APP端与后端系统,都需要完成与环信的集成。
- 在APP端,使用Android的SDK与环信进行通信,通信时需要通过后台系统的接口查询当前用户的环信用户名和密码,进行登录环信。
- 后台系统,在用户注册后,同步注册环信用户到环信平台,在后台系统中保存环信的用户名和密码。
- APP拿到用户名和密码后,进行登录环信,登录成功后即可向环信发送消息给好友。
- 后台系统也可以通过管理员的身份给用户发送系统信息。
6. 创建dubbo工程
dubbo工程创建采用开闭原则,开放接口,封装服务,只对外暴露接口,通过接口对外提供服务
先创建一个父工程my-tanhua-dubbo(maven工程),然后在父工程下面创建两个子模块:my-tanhua-dubbo-interface(maven工程) my-tanhua-dubbo-huanxin(springboot工程),接口模块只提供对外服务的接口,服务模块是具体的服务提供方,共工程结构如下图所示:
7. 编写dubbo服务
- 导入坐标
注意:这里导入的坐标是springboot的初始坐标和一些用到的坐标
**[**插入一个小知识点:这里my-tanhua-dubbo-huanxin模块引入了my-tanhua-dubbo-interface模块,如果interface使用了mongodb,而huanxin模块间接引入了mongodb服务,huanxin没有mongodb的配置,启动的时候会报错,如下图所示:
解决办法:
1.在pom中引入interface使用exclusions标签排除monggodb服务
2.在huanxin模块的引导类上排除mongodb
@SpringBootApplication(exclude = {MongoAutoConfiguration.class, MongoDataAutoConfiguration.class})
]
<dependencies>
<!--引入interface依赖-->
<dependency>
<groupId>cn.itcast.tanhua</groupId>
<artifactId>my-tanhua-dubbo-interface</artifactId>
<version>1.0-SNAPSHOT</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus</artifactId>
</dependency>
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-boot-starter</artifactId>
</dependency>
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
</dependency>
<!--dubbo的springboot支持-->
<dependency>
<groupId>com.alibaba.boot</groupId>
<artifactId>dubbo-spring-boot-starter</artifactId>
</dependency>
<!--dubbo框架-->
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>dubbo</artifactId>
</dependency>
<!--zk依赖-->
<dependency>
<groupId>org.apache.zookeeper</groupId>
<artifactId>zookeeper</artifactId>
</dependency>
<dependency>
<groupId>com.github.sgroschupf</groupId>
<artifactId>zkclient</artifactId>
</dependency>
<dependency>
<groupId>io.netty</groupId>
<artifactId>netty-all</artifactId>
</dependency>
<!--实用工具hutool-->
<dependency>
<groupId>cn.hutool</groupId>
<artifactId>hutool-all</artifactId>
</dependency>
<!-- spring中的实现重试的一个框架-->
<dependency>
<groupId>org.springframework.retry</groupId>
<artifactId>spring-retry</artifactId>
</dependency>
<dependency>
<groupId>org.aspectj</groupId>
<artifactId>aspectjweaver</artifactId>
</dependency>
</dependencies>
- 编写配置文件
application.properties:
# Spring boot application
spring.application.name = itcast-tanhua-dubbo-huanxin
# dubbo 扫描包配置
dubbo.scan.basePackages = com.tanhua.dubbo.server
dubbo.application.name = dubbo-provider-huanxin
#dubbo 对外暴露的端口信息
dubbo.protocol.name = dubbo
dubbo.protocol.port = 20881
#dubbo注册中心的配置
dubbo.registry.address = zookeeper://192.168.200.129:2181
dubbo.registry.client = zkclient
dubbo.registry.timeout = 60000
# Redis相关配置
spring.redis.jedis.pool.max-wait = 5000ms
spring.redis.jedis.pool.max-Idle = 100
spring.redis.jedis.pool.min-Idle = 10
spring.redis.timeout = 10s
#下面两行注释采用的是redis集群的方式配置
#spring.redis.cluster.nodes = 192.168.200.129:6379,192.168.200.129:6380,192.168.200.129:6381
#spring.redis.cluster.max-redirects=5
#这里我们使用的是单节点
spring.redis.port=6379
spring.redis.host=192.168.200.129
#数据库连接信息
spring.datasource.driver-class-name=com.mysql.jdbc.Driver
spring.datasource.url=jdbc:mysql://localhost:3306/tanhua?useUnicode=true&characterEncoding=utf8&autoReconnect=true&allowMultiQueries=true&useSSL=false
spring.datasource.username=root
spring.datasource.password=root
# 表名前缀
mybatis-plus.global-config.db-config.table-prefix=tb_
# id策略为自增长
mybatis-plus.global-config.db-config.id-type=auto
huanxin.properties:切记改成自己创建的应用的信息
#环信参数要使用自己创建的应用的信息
tanhua.huanxin.url=http://a1.easemob.com/
tanhua.huanxin.orgName=1105190515097562
tanhua.huanxin.appName=tanhua
tanhua.huanxin.clientId=YXA67ZofwHblEems-_Fh-17T2g
tanhua.huanxin.clientSecret=YXA60r45rNy2Ux5wQ7YYoEPwynHmUZk
- 编写配置类:加载环信的配置文件
package com.tanhua.dubbo.server.config;
import lombok.Data;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.PropertySource;
//配置类
@Configuration
//加载配置文件
@PropertySource("classpath:huanxin.properties")
@ConfigurationProperties(prefix = "tanhua.huanxin")
@Data
public class HuanXinConfig {
private String url;
private String orgName;
private String appName;
private String clientId;
private String clientSecret;
}
- 编写配置类:
package com.tanhua.dubbo.server.config;
import lombok.Data;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.PropertySource;
//这是一个配置类
@Configuration
//加载配置文件
@PropertySource("classpath:huanxin.properties")
//配置文件配置的前缀
@ConfigurationProperties(prefix = "tanhua.huanxin")
//lomback生成set和get等方法
@Data
public class HuanXinConfig {
private String url;
private String orgName;
private String appName;
private String clientId;
private String clientSecret;
}
-
管理员获取token
官方文档
环信提供的 REST API 需要权限才能访问,权限通过发送 HTTP 请求时携带 token 来体现,具体的获取token的业务逻辑在TokenService中完成。实现要点:
- 分析官方文档中的请求url、参数、响应数据等内容 - 请求到token需要缓存到redis中,不能频繁的获取token操作,可能会被封号
package com.tanhua.dubbo.server.service;
import cn.hutool.core.util.StrUtil;
import cn.hutool.http.HttpRequest;
import cn.hutool.http.HttpResponse;
import cn.hutool.json.JSONObject;
import cn.hutool.json.JSONUtil;
import com.tanhua.dubbo.server.config.HuanXinConfig;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.stereotype.Service;
import java.util.HashMap;
import java.util.Map;
import java.util.concurrent.TimeUnit;
//管理员(就是创建应用生成的那个id和密码的那个)获取token
@Service
@Slf4j
public class TokenService {
@Autowired
private RedisTemplate<String, String> redisTemplate;
private static final String REDIS_KEY = "HX_TOKEN";
@Autowired
private HuanXinConfig huanXinConfig;
/**
* 获取token,先从redis中获取,如果没有,再去环信接口获取
*
* @return
*/
public String getToken() {
String token = this.redisTemplate.opsForValue().get(REDIS_KEY);
if (StrUtil.isNotEmpty(token)) {
return token;
}
//访问环信接口获取token
return this.refreshToken();
}
/**
* 刷新token,请求环信接口,将token存储到redis中
*
* @return
*/
public String refreshToken() {
String targetUrl = this.huanXinConfig.getUrl() +
this.huanXinConfig.getOrgName() + "/" +
this.huanXinConfig.getAppName() + "/token";
Map<String, Object> param = new HashMap<>();
param.put("grant_type", "client_credentials");
param.put("client_id", this.huanXinConfig.getClientId());
param.put("client_secret", this.huanXinConfig.getClientSecret());
HttpResponse response = HttpRequest.post(targetUrl)
.body(JSONUtil.toJsonStr(param))
.timeout(20000) //请求超时时间
.execute();
if (!response.isOk()) {
log.error("刷新token失败~~~ ");
return null;
}
//拿到环信响应的响应体,获取到里面的token和过期时间expires_in.有效期为7天,但不能完全保证
String jsonBody = response.body();
JSONObject jsonObject = JSONUtil.parseObj(jsonBody);
String token = jsonObject.getStr("access_token");
if (StrUtil.isNotEmpty(token)) {
//将token数据缓存到redis中,缓存时间由expires_in决定
//提前1小时失效
long timeout = jsonObject.getLong("expires_in") - 3600;
this.redisTemplate.opsForValue().set(REDIS_KEY, token, timeout, TimeUnit.SECONDS);
return token;
}
return null;
}
}
注意:到现在为止真正的dubbo从下面开始编写,以上只是获取token并存储到redis中的逻辑实现
- 在interface中定义pojo对象
package com.tanhua.dubbo.server.pojo;
import com.baomidou.mybatisplus.annotation.TableName;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
import java.util.Date;
/**
* 环信用户对象
*/
@Data
@NoArgsConstructor
@AllArgsConstructor
@TableName("tb_huanxin_user")
public class HuanXinUser implements java.io.Serializable{
private static final long serialVersionUID = -6400630011196593976L;
private Long id; //主键Id
/**
* 环信 ID ;也就是 IM 用户名的唯一登录账号,长度不可超过64个字符长度
*/
private String username;
/**
* 登录密码,长度不可超过64个字符长度
*/
private String password;
/**
* 昵称(可选),在 iOS Apns 推送时会使用的昵称(仅在推送通知栏内显示的昵称),
* 并不是用户个人信息的昵称,环信是不保存用户昵称,头像等个人信息的,
* 需要自己服务器保存并与给自己用户注册的IM用户名绑定,长度不可超过100个字符
*/
private String nickname;
private Long userId; //用户id
private Date created; //创建时间
private Date updated; //更新时间
}
- 编写HuanXinUserMapper
注册成功后,在环信上的用户名和密码需要和其他信息需要存储到mysql数据库中
package com.tanhua.dubbo.server.mapper;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.tanhua.dubbo.server.pojo.HuanXinUser;
import org.apache.ibatis.annotations.Mapper;
//这个用于保存环信注册成功用户的用户名和密码
@Mapper
public interface HuanXinUserMapper extends BaseMapper<HuanXinUser> {
}
- 定义interface中的接口
package com.tanhua.dubbo.server.api;
import com.tanhua.dubbo.server.pojo.HuanXinUser;
/**
* 与环信平台集成的相关操作
*/
public interface HuanXinApi {
/**
* 获取环信token(获取管理员权限)
* 参见:http://docs-im.easemob.com/im/server/ready/user#%E8%8E%B7%E5%8F%96%E7%AE%A1%E7%90%86%E5%91%98%E6%9D%83%E9%99%90
*
* @return
*/
String getToken();
/**
* 注册环信用户
* 参见:http://docs-im.easemob.com/im/server/ready/user#%E6%B3%A8%E5%86%8C%E5%8D%95%E4%B8%AA%E7%94%A8%E6%88%B7_%E5%BC%80%E6%94%BE
*
* @param userId 用户id
* @return
*/
Boolean register(Long userId);
/**
* 根据用户Id询环信账户信息
*
* @param userId
* @return
*/
HuanXinUser queryHuanXinUser(Long userId);
/**
* 根据环信用户名查询用户信息
*
* @param userName
* @return
*/
HuanXinUser queryUserByUserName(String userName);
}
- 实现接口
在my-tanhua-dubbo-huanxin中完成。
package com.tanhua.dubbo.server.api;
import cn.hutool.core.util.IdUtil;
import cn.hutool.http.HttpResponse;
import cn.hutool.http.Method;
import cn.hutool.json.JSONUtil;
import com.alibaba.dubbo.config.annotation.Service;
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import com.tanhua.dubbo.server.config.HuanXinConfig;
import com.tanhua.dubbo.server.mapper.HuanXinUserMapper;
import com.tanhua.dubbo.server.pojo.HuanXinUser;
import com.tanhua.dubbo.server.service.RequestService;
import com.tanhua.dubbo.server.service.TokenService;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import java.util.Arrays;
import java.util.Date;
@Service(version = "1.0.0")
@Slf4j
public class HuanXinApiImpl implements HuanXinApi {
@Autowired
private TokenService tokenService;
@Autowired
private HuanXinConfig huanXinConfig;
@Autowired
private RequestService requestService;
@Autowired
private HuanXinUserMapper huanXinUserMapper;
/**
* 管理员获取token
* @return
*/
@Override
public String getToken() {
return this.tokenService.getToken();
}
/**
* 普通用户注册环信账号
* @param userId 用户id
* @return
*/
@Override
public Boolean register(Long userId) {
String targetUrl = this.huanXinConfig.getUrl()
+ this.huanXinConfig.getOrgName() + "/" +
this.huanXinConfig.getAppName() + "/users";
HuanXinUser huanXinUser = new HuanXinUser();
huanXinUser.setUsername("HX_" + userId); // 用户名
huanXinUser.setPassword(IdUtil.simpleUUID()); //随机生成的密码,这里的密码是服务器端自动生成的,不是环信给你生成的
//调用通用的方法向环信的rest接口发送请求,批量添加数据
HttpResponse response = this.requestService.execute(targetUrl, JSONUtil.toJsonStr(Arrays.asList(huanXinUser)), Method.POST);
if (response.isOk()) {
//将环信的账号信息保存到数据库
huanXinUser.setUserId(userId);
huanXinUser.setCreated(new Date());
huanXinUser.setUpdated(huanXinUser.getCreated());
this.huanXinUserMapper.insert(huanXinUser);
return true;
}
return false;
}
/**
* 根据用户的id查询环信信息
* @param userId
* @return
*/
@Override
public HuanXinUser queryHuanXinUser(Long userId) {
QueryWrapper<HuanXinUser> wrapper = new QueryWrapper<>();
wrapper.eq("user_id", userId);
return this.huanXinUserMapper.selectOne(wrapper);
}
/**
* 根据环信用户名查询用户名,注意:这里生成的环信用户名是HX_103,对应唯一的用户id
* @param userName
* @return
*/
@Override
public HuanXinUser queryUserByUserName(String userName) {
QueryWrapper<HuanXinUser> wrapper = new QueryWrapper<>();
wrapper.eq("username", userName);
return this.huanXinUserMapper.selectOne(wrapper);
}
}
- 编写统一的请求环信rest接口的请求类
这里使用了hutool工具来发送http请求的一个方法
1.先自定义一个异常
package com.tanhua.dubbo.server.exception;
import cn.hutool.http.Method;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
//自定义异常注解
@AllArgsConstructor
@NoArgsConstructor
@Data
public class UnauthorizedException extends RuntimeException {
private String url;
private String body;
private Method method;
}
2.导入重试框架(Spring-Retry)的坐标
Spring提供了重试的功能,使用非常的简单、优雅。
<!--Spring重试模块-->
<dependency>
<groupId>org.springframework.retry</groupId>
<artifactId>spring-retry</artifactId>
</dependency>
<dependency>
<groupId>org.aspectj</groupId>
<artifactId>aspectjweaver</artifactId>
</dependency>
3.编写统一的请求逻辑
请求逻辑中使用了重试框架
@Retryable用在重试方法上
@Recover用在重试全部失败后执行的方法上
package com.tanhua.dubbo.server.service;
import cn.hutool.http.HttpRequest;
import cn.hutool.http.HttpResponse;
import cn.hutool.http.Method;
import com.tanhua.dubbo.server.exception.UnauthorizedException;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.retry.annotation.Backoff;
import org.springframework.retry.annotation.Recover;
import org.springframework.retry.annotation.Retryable;
import org.springframework.stereotype.Service;
/**
* 环信接口通用请求服务
*/
@Service
@Slf4j
public class RequestService {
@Autowired
private TokenService tokenService;
/**
* 通用的发送请求方法(这里使用了一个spring里面的retry重试框架)
* 1.向环信rest接口发送的http请求,这里使用的是hutool的一个工具
* 2.这里我们在环信中创建的应用使用的是授权模式,每一次请求环信的rest接口都需要校验令牌(放在请求头中携带),切记Bearer ${token}中间有一个空格,
* 3.@Retryable 这个是重试注解,放在需要重试的方法上面
* @param url 请求地址
* @param body 请求参数
* @param method 请求方法
* @return
*/
//注解参数说明:UnauthorizedException:自定义的异常,触发重试的条件
//maxAttempts:最多重试次数
//backoff:重试的时间间隔策略,delay表示第一次重试后间隔两秒,multiplier表示以后每次间隔时间进行倍增
@Retryable(value = UnauthorizedException.class, maxAttempts = 5, backoff = @Backoff(delay = 2000L, multiplier = 2))
public HttpResponse execute(String url, String body, Method method) {
String token = this.tokenService.getToken();
HttpRequest httpRequest;
switch (method) {
case POST: {
httpRequest = HttpRequest.post(url);
break;
}
case DELETE: {
httpRequest = HttpRequest.delete(url);
break;
}
case PUT: {
httpRequest = HttpRequest.put(url);
break;
}
case GET: {
httpRequest = HttpRequest.get(url);
break;
}
default: {
return null;
}
}
//使用hutool的后台发送http请求
HttpResponse response = httpRequest
.header("Content-Type", "application/json") //设置请求内容类型
.header("Authorization", "Bearer " + token) //设置token,注意bearer后面有一个空格(不可缺),Bearer ${token}
.body(body) // 设置请求数据
.timeout(20000) // 超时时间
.execute(); // 执行请求
//返回值401,未授权[无token、token错误、token过期]
if (response.getStatus() == 401) {
//token失效,重新刷新token
this.tokenService.refreshToken();
//抛出异常,需要进行重试,重试到最大次数后就会执行下面的recover方法逻辑
throw new UnauthorizedException(url, body, method);
}
return response;
}
/**
* 全部重试失败后执行
* @param e 参数异常类型必须和触发重试的异常类型一致,因为这个方法要查看异常中的内容
* @return 方法返回值类型也必须和重试方法的返回值类型一致
*/
@Recover
public HttpResponse recover(UnauthorizedException e) {
log.error("获取token失败!url = " + e.getUrl() + ", body = " + e.getBody() + ", method = " + e.getMethod().toString());
//如果重试5次后,依然不能获取到token,说明网络或账号出现了问题,只能返回null了,后续的请求将无法再执行
return null;
}
}
@Retryable参数说明:
- value:抛出指定异常才会重试
- maxAttempts:最大重试次数,默认3次
- backoff:重试等待策略,默认使用@Backoff
- @Backoff 的value默认为1000L,我们设置为2000L;
- multiplier(指定延迟倍数)默认为0,表示固定暂停1秒后进行重试,如果把multiplier设置为2,则第一次重试为2秒,第二次为4秒,第三次为6秒。
@Recover标注的方法,是在所有的重试都失败的情况下,最后执行该方法,该方法有2个要求:
- 方法的第一个参数必须是 Throwable 类型,最好与 @Retryable 中的 value一致。
- 方法的返回值必须与@Retryable的方法返回值一致,否则该方法不能被执行。
- huanxin模块的引导类如下
package com.tanhua.dubbo.server;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.boot.autoconfigure.data.mongo.MongoDataAutoConfiguration;
import org.springframework.boot.autoconfigure.mongo.MongoAutoConfiguration;
import org.springframework.retry.annotation.EnableRetry;
@SpringBootApplication(exclude = {MongoAutoConfiguration.class, MongoDataAutoConfiguration.class}) //排除mongo的自动配置
//开启重试功能
@EnableRetry
public class HuanXinDubboApplication {
public static void main(String[] args) {
SpringApplication.run(HuanXinDubboApplication.class, args);
}
}
- 测试
package com.tanhua.dubbo.server.api;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.test.context.junit4.SpringRunner;
@SpringBootTest
@RunWith(SpringRunner.class)
public class TestHuanXinApi {
@Autowired
private HuanXinApi huanXinApi;
@Test
public void testGetToken(){
String token = this.huanXinApi.getToken();
System.out.println(token);
}
@Test
public void testRegister(){
//注册用户id为1的用户到环信
System.out.println(this.huanXinApi.register(1L));
}
@Test
public void testQueryHuanXinUser(){
//根据用户id查询环信用户信息
System.out.println(this.huanXinApi.queryHuanXinUser(1L));
}
}