消息之极光推送
@
简介
Push 更适合于服务方单方向发消息给终端用户。如果想要双方向沟通,用基于 IM 的模型更合适。
推送平台
JPush 全面支持 Android, iOS, Winphone 三大手机平台。
消息形式
JPush 提供四种消息形式:通知,自定义消息,富媒体和本地通知。
-
通知
或者说 Push Notification,即指在手机的通知栏(状态栏)上会显示的一条通知信息。 通知主要用来达到提示用户的目的,应用于新闻内容、促销活动、产品信息、版本更新提醒、订单状态提醒等多种场景
开发者参考文档:Push API v3 notification
-
自定义消息
自定义消息不是通知,所以不会被 SDK 展示到通知栏上。其内容完全由开发者自己定义。 自定义消息主要用于应用的内部业务逻辑。一条自定义消息推送过来,有可能没有任何界面显示。
开发者参考文档:Push API v3 message
-
本地通知
本地通知 API 不依赖于网络,无网条件下依旧可以触发;本地通知的定时时间是自发送时算起的,不受中间关机等操作的影响。 本地通知与网络推送的通知是相互独立的,不受保留最近通知条数上限的限制。 本地通知适用于在特定时间发出的通知,如一些 Todo 和闹钟类的应用,在每周、每月固定时间提醒用户回到应用查看任务。
推送人群(Audience)
极光推送(JPush)在推送人群的选择上,支持如下几种方式:
- 广播(所有人)
- 注册ID(RegistrationID)
- 别名(alias)
- 标签(tag,分组)
- 用户分群(Segment)
推送人群可选类别
以下先分别解析以上几个推送人群类型,及其具体用法。之后再谈谈他们的适用场景,以及如何区别使用。
注册ID(RegistrationID)
RegistrationID 就是这台设备(以及当前这个 App),被推送服务器分配的唯一 ID。不同的设备、不同的 App 这个 ID 肯定不同的。
SDK 在第一次启动时会去服务器端进行注册并识别,服务器端会分配一个 RegistrationID。SDK 会把这个 ID 通过广播(或通知)的方式发给 App。SDK 也提供了获取 RegistrationID 的接口。
如果一个 App 在这台设备上之前安装过,然后被卸载掉。重新安装时,其获取到的 RegistrationID 有一定的可能性不变。这取决于平台以及条件。
- Android 上 JPush 会综合利用多个条件来判断设备是否相同,从而 RegistrationID 不变的可能性很大。
- iOS 老版本上,因为 device token 重新安装 App 时也不会变,从而 RegistrationID 也一般不会变。
- iOS8 以后重新安装 App 会导致 device token 变更,iOS 上如果不启用 IDFA 则没有其他可用于识别设备的手段,从而 RegistrationID 一般会变化。或者说,服务器端无法识别重新安装。所以如果你的业务有需要,建议启用 IDFA。
使用 RegistrationID 推送的关键于,App 开发者需要在开发 App 时,获取到这个 RegistrationID,保存到 App 业务服务器上去,并且与自己的用户标识对应起来。
建议 App 开发者尽可能做这个保存动作。因为这是最精确地定位到设备的。
别名(alias)
别名可以理解为基于 RegistrationID,设置一个更容易理解的『别名』,比如直接设置为当前登录用户的 username。
一个设备(在一个 App 里)只能设置一个别名。
别名的本质是,把 App 用户体系里的用户ID与 RegistrationID 的对应关系,保存到推送服务器上,而不是保存到 App 业务服务器上去。(使用 RegistrationID 就是把对应关系保存到 App 业务服务器上去。)
设置了别名后,推送时服务器端指定别名即可。推送服务器端来把别名转化到 RegistrationID找到设备。
别名可以在客户端设置,服务器端也提供了 REST API 进行设置。但是,在一个 App 的生命周期中,强烈建议不要既在客户端又在服务器端进行设置,否则会导致混乱。
标签(tag)
又或者称为分组推送。对于大量的设置了同一个标签的终端,可以一次推送到到达。一个应用里一个标签绑定到的设备数没有限制。
一个设备(在一个 App里)可以设置多个标签。
标签与别名类似,其对应关系也是保存在推送服务器侧的。
与别名类似,标签也是可以在客户端设置,服务器端也开放了 REST API 进行设置。同样,也是强烈建议,不要既在客户端设置标签,又在服务器端设置标签,以免造成混乱。
用户分群(Segment)
这是相对高级的使用方式了,开发者可以根据一些已知的条件,任意组合,创建一个 SegmentID。然后基于这个 SegmentID 进行推送。
上面说到的可以用于用户分群的条件有:tags,App 版本,SDK 版本,平台版本,用户注册时间,用户活跃时间,用户所在城市等。
广播(所有人)
技术上广播很好理解,就是推送给所有安装的 App 的设备终端。
极光推送对广播有一个特殊的选项:延迟推送。这是个很有特色的功能,让推送在一定时长内平均分配,而不是在太短时间内完成,以免对 App 业务服务器造成太大的压力。
根据业务场景选择推送人群
以上以极光推送为例介绍了支持的推送人群类别。但是,任何技术都有一个使用场景的问题。开发者需要想清楚自己的使用场景,来选择适当的类别。
单用户推送
RegistrationID 与别名是设计用来单用户推送的。
如果别名只是在一个设备上被设置,则其效果与 RegistrationID 是类似的。
不同的是,一个别名是可以被设置到多设备上的。一个常见的场景是,把 App 的用户帐号 username 作为别名。一个 App用户帐号可以在多设备上登录(大多数这样),就可以在多设备上绑定为别名。这样推送给这个别名,多设备上都收到。
由于别名的绑定关系保存在推送服务器上,App 业务上要做变更就不够灵活。所以,别名更适合于简单的使用场景,也是适合「懒」的开发者。而 App 想要有灵活性,建议使用 RegistrationID 的方式。
别名在使用时,有可能被误使用为 tag,即大量的设备上都设置同一个别名,其实就是 tag 的使用方式。极光推送发现了不少 App 这样子用。
用户分群与标签
标签是个很灵活的分组方法,可以被用于业务强相关的各种场景。主要的种类有:订阅,用户属性。
订阅类,比如彩票 App 用于用户订阅不同彩票类型的最新开奖信息,阅读类 App 用于让用户订阅多个频道的最新资讯等。
用 tag 来标注用户的属性,比如性别、年龄段、喜好、关注等,也是个很常见的作法,这样推送时就可以基于这些属性来做。这也是精细化推送的基础。
事实上,很多标签被开发者定义为:App 版本,用户城市,使用语言等等。现在很多这类的 tags 可以不要了,只需要直接使用用户分群功能就可以了。
依赖引入
<!-- jpush -->
<dependency>
<groupId>cn.jpush.api</groupId>
<artifactId>jpush-client</artifactId>
<version>3.3.9</version>
</dependency>
<dependency>
<groupId>cn.jpush.api</groupId>
<artifactId>jiguang-common</artifactId>
<version>1.0.3</version>
</dependency>
yml 配置文件
#极光推送参数设定
jpush:
appkey:
masterSecret:
#离线保存时间 7天 最长10天
liveTime: 604800
#推送开关
enable: true
配置类
@Component
@ConfigurationProperties(prefix="jpush")
public class JPushConfig {
public static String APP_KEY;
public static String MASTER_SECRET;
public static long LIVE_TIME;
/**
* 是否开启推送
*/
public static Boolean ENABLE;
public void setAppKey(String appKey) {
JPushConfig.APP_KEY = appKey;
}
public void setMasterSecret(String masterSecret) {
JPushConfig.MASTER_SECRET = masterSecret;
}
public void setLiveTime(long liveTime) {
JPushConfig.LIVE_TIME = liveTime;
}
public void setEnable(Boolean enable) {
JPushConfig.ENABLE = enable;
}
}
实现
实现的接口主要有通知和消息的自定义推送、定时推送和取消定时推送
public class JPushUtil {
private static final Logger logger = LoggerFactory.getLogger(JPushUtil.class);
/**
* 消息自定义即时推送
* @param jPushMsgVO
* @return
*/
public static PushLog messageCustomPush(JPushMsgVO jPushMsgVO) {
JPushClient jpushClient = buildJPushClient(jPushMsgVO.getLiveTime());
PushPayload payload = buildMessageCustomPushPayload(jPushMsgVO.getTitle(), jPushMsgVO.getContent(), jPushMsgVO.getExtrasMap(), jPushMsgVO.getPlatform(), jPushMsgVO.getPushType(), jPushMsgVO.getPushObject());
PushResult result = null;
PushLog pushLog = BeanUtils.copyResult(jPushMsgVO, PushLog::new);
pushLog.setCreateTime(new Date());
pushLog.setPushSort(PushSortEnum.MESSAGE.getCode());
try {
result = jpushClient.sendPush(payload);
pushLog.setMsgId(String.valueOf(result.msg_id));
logger.info("极光推送条件{},结果{}", payload,result);
} catch (APIConnectionException e) {
logger.error("极光推送连接错误,请稍后重试 ", e);
logger.error("SendNo: " + payload.getSendno());
} catch (APIRequestException e) {
logger.error("极光服务器响应出错,请修复! ", e);
logger.info("HTTP Status: {}", e.getStatus());
logger.info("Error Code: {}", e.getErrorCode());
logger.info("Error Message: {}", e.getErrorMessage());
logger.error("SendNo: {}", payload.getSendno());
pushLog.setMsgId(String.valueOf(e.getMsgId()));
pushLog.setStatusCode(e.getErrorCode());
pushLog.setErrMsg(e.getErrorMessage());
} finally {
pushLog.setPayload(payload.toString());
pushLog.setSendNo(payload.getSendno());
pushLog.setPushTime(new Date());
}
return pushLog;
}
/**
* 消息自定义定时推送
* @param jPushMsgVO
* @return
*/
public static PushLog messageCustomSchedulePush(JPushMsgVO jPushMsgVO) {
JPushClient jpushClient = buildJPushClient(jPushMsgVO.getLiveTime());
PushPayload payload = buildMessageCustomPushPayload(jPushMsgVO.getTitle(), jPushMsgVO.getContent(), jPushMsgVO.getExtrasMap(), jPushMsgVO.getPlatform(), jPushMsgVO.getPushType(), jPushMsgVO.getPushObject());
ScheduleResult result = null;
PushLog pushLog = BeanUtils.copyResult(jPushMsgVO, PushLog::new);
pushLog.setCreateTime(new Date());
pushLog.setStatusCode(-1);
try {
result = jpushClient.createSingleSchedule("CustomScheduleJPush", jPushMsgVO.getPushTime().toString(), payload);
pushLog.setMsgId(result.getSchedule_id());
logger.info("极光推送条件{},结果{}", payload,result);
} catch (APIConnectionException e) {
logger.error("极光推送连接错误,请稍后重试 ", e);
logger.error("SendNo: " + payload.getSendno());
} catch (APIRequestException e) {
logger.error("极光服务器响应出错,请修复! ", e);
logger.info("HTTP Status: {}", e.getStatus());
logger.info("Error Code: {}", e.getErrorCode());
logger.info("Error Message: {}", e.getErrorMessage());
logger.error("SendNo: {}", payload.getSendno());
pushLog.setStatusCode(e.getErrorCode());
pushLog.setErrMsg(e.getErrorMessage());
} finally {
pushLog.setPayload(payload.toString());
pushLog.setSendNo(payload.getSendno());
pushLog.setPushTime(new Date());
}
return pushLog;
}
/**
* 通知自定义即时推送
* @param jPushMsgVO
* @return 推送记录
*/
public static PushLog notifyCustomPush(JPushMsgVO jPushMsgVO) {
JPushClient jpushClient = buildJPushClient(jPushMsgVO.getLiveTime());
//构造推送条件
PushPayload payload = buildNotifyCustomPushPayload(jPushMsgVO.getTitle(), jPushMsgVO.getContent(), jPushMsgVO.getExtrasMap(), jPushMsgVO.getPlatform(), jPushMsgVO.getPushType(), jPushMsgVO.getPushObject());
PushResult result = null;
PushLog pushLog = BeanUtils.copyResult(jPushMsgVO, PushLog::new);
pushLog.setCreateTime(new Date());
pushLog.setStatusCode(-1);
try {
result = jpushClient.sendPush(payload);
pushLog.setMsgId(String.valueOf(result.msg_id));
pushLog.setPushMethod(PushMethodEnum.IMMEDIATE.getCode());
pushLog.setStatusCode(result.statusCode);
if(result.statusCode != 0){
pushLog.setErrMsg(result.error.getMessage());
}
pushLog.setPayload(payload.toString());
pushLog.setSendNo(payload.getSendno());
logger.info("极光推送条件{},结果{}", payload,result);
} catch (APIConnectionException e) {
logger.error("极光推送连接错误,请稍后重试 ", e);
logger.error("SendNo: " + payload.getSendno());
} catch (APIRequestException e) {
logger.error("极光服务器响应出错,请修复!", e);
logger.info("HTTP Status: {}", e.getStatus());
logger.info("Error Code: {}", e.getErrorCode());
logger.info("Error Message: {}", e.getErrorMessage());
logger.error("SendNo: {}", payload.getSendno());
} finally {
pushLog.setPushTime(new Date());
}
return pushLog;
}
/**
* 通知自定义定时推送
* @param jPushMsgVO
* @return
*/
public static PushLog notifyCustomSchedulePush(JPushMsgVO jPushMsgVO) {
JPushClient jpushClient = buildJPushClient(jPushMsgVO.getLiveTime());
PushPayload payload = buildNotifyCustomPushPayload(jPushMsgVO.getTitle(), jPushMsgVO.getContent(), jPushMsgVO.getExtrasMap(), jPushMsgVO.getPlatform(), jPushMsgVO.getPushType(), jPushMsgVO.getPushObject());
ScheduleResult result = null;
PushLog pushLog = new PushLog();
pushLog.setPushMethod(PushMethodEnum.TIMING.getCode());
pushLog.setCreateTime(new Date());
pushLog.setStatusCode(-1);
try {
result = jpushClient.createSingleSchedule("CustomScheduleJPush", jPushMsgVO.getPushTime().toString(), payload);
pushLog.setMsgId(result.getSchedule_id());
pushLog.setPayload(payload.toString());
pushLog.setSendNo(payload.getSendno());
logger.info("极光推送条件{},结果{}", payload,result);
} catch (APIConnectionException e) {
logger.error("极光推送连接错误,请稍后重试 ", e);
logger.error("SendNo: " + payload.getSendno());
} catch (APIRequestException e) {
logger.error("极光服务器响应出错,请修复! ", e);
logger.info("HTTP Status: {}", e.getStatus());
logger.info("Error Code: {}", e.getErrorCode());
logger.info("Error Message: {}", e.getErrorMessage());
logger.error("SendNo: {}", payload.getSendno());
pushLog.setStatusCode(e.getErrorCode());
pushLog.setErrMsg(e.getErrorMessage());
} finally {
pushLog.setPushTime(new Date());
}
return pushLog;
}
/**
* 取消定时推送
*/
public static boolean deleteSchedule(String scheduleId) {
boolean result;
try {
JPushClient jPushClient = new JPushClient(JPushConfig.MASTER_SECRET, JPushConfig.APP_KEY);
jPushClient.deleteSchedule(scheduleId);
result = true;
} catch (APIConnectionException e) {
logger.error("Connection error. Should retry later. ", e);
result = false;
} catch (APIRequestException e) {
logger.error("Error response from JPush server. Should review and fix it. ", e);
logger.info("HTTP Status: {}", e.getStatus());
logger.info("Error Code: {}", e.getErrorCode());
logger.info("Error Message: {}", e.getErrorMessage());
result = false;
}
return result;
}
/**
* 构建自定义消息的推送消息对象
*
* @param title 推送消息标题
* @param content 推送消息内容(为了单行显示全,尽量保持在22个汉字以下)
* @param extrasMap 额外推送信息(不会显示在通知栏,传递数据用)
* @param platform 推送的设备类型,默认全部类型
* @param pushTypeEnum 推送方式,默认广播推送
* @param pushObject PushTypeEnum为广播推送:此字段无意义;PushTypeEnum为别名推送:此字段为推送指定的别名;PushTypeEnum为标签推送:此字段为推送指定的标签
* @return 推送消息对象
*/
private static PushPayload buildMessageCustomPushPayload(String title, String content, Map<String, String> extrasMap, PushPlatformEnum platform, PushTypeEnum pushTypeEnum, List<String> pushObject) {
// 批量删除数组中空元素
return PushPayload.newBuilder()
// 设置推送的设备类型
.setPlatform(null == platform ? Platform.all() : getPlatform(platform))
// 设置推送的受众
.setAudience(getAudience(pushTypeEnum, pushObject))
// 设置推送标题、内容、额外信息
.setMessage(Message.newBuilder().setTitle(title).setMsgContent(content).addExtras(null == extrasMap ? new HashMap<>() : extrasMap).build())
.build();
}
/**
* 构建自定义的通知推送对象
*
* @param title 推送通知标题
* @param content 推送通知内容(为了单行显示全,尽量保持在22个汉字以下)
* @param extrasMap 额外推送信息(不会显示在通知栏,传递数据用)
* @param platform 推送的设备类型,默认全部类型
* @param PushTypeEnum 推送方式,默认广播推送
* @param pushObject PushTypeEnum为广播推送:此字段无意义;PushTypeEnum为别名推送:此字段为推送指定的别名;PushTypeEnum为标签推送:此字段为推送指定的标签
* @return 推送通知对象
*/
private static PushPayload buildNotifyCustomPushPayload(String title, String content, Map<String, String> extrasMap, PushPlatformEnum platform, PushTypeEnum PushTypeEnum, List<String> pushObject) {
extrasMap = extrasMap == null ? new HashMap<>() : extrasMap;
return PushPayload.newBuilder().setPlatform(null == platform ? Platform.all() : getPlatform(platform))
.setAudience(getAudience(PushTypeEnum, pushObject))
.setNotification(Notification.newBuilder().setAlert(content)
.addPlatformNotification(AndroidNotification.newBuilder().setTitle(title).addExtras(extrasMap).build())
.addPlatformNotification(IosNotification.newBuilder().incrBadge(1).addExtras(extrasMap).build())
.build())
.build();
}
/**
* 构建推送客户端
*/
private static JPushClient buildJPushClient(Long liveTime) {
ClientConfig clientConfig = ClientConfig.getInstance();
clientConfig.setTimeToLive(liveTime == null ? JPushConfig.LIVE_TIME : liveTime);
return new JPushClient(JPushConfig.MASTER_SECRET, JPushConfig.APP_KEY, null, clientConfig);
}
/**
* 查询记录推送成功条数(暂未使用)
*
* @param msg_id 在推送返回结果PushResult中保存
*/
public void countPush(String msg_id) {
JPushClient jpushClient = new JPushClient(JPushConfig.MASTER_SECRET, JPushConfig.APP_KEY);
try {
ReceivedsResult result = jpushClient.getReportReceiveds(msg_id);
ReceivedsResult.Received received = result.received_list.get(0);
logger.debug("Android接受信息:" + received.android_received + "\n IOS端接受信息:" + received.ios_apns_sent);
logger.debug("极光推送返回结果 - " + result);
} catch (APIConnectionException e) {
logger.error("极光推送连接错误,请稍后重试", e);
} catch (APIRequestException e) {
logger.error("检查错误,并修复推送请求", e);
logger.info("HTTP Status: " + e.getStatus());
logger.info("Error Code: " + e.getErrorCode());
logger.info("Error Message: " + e.getErrorMessage());
}
}
/**
* 异步请求推送方式,使用NettyHttpClient,异步接口发送请求,通过回调函数可以获取推送成功与否情况
*/
public void sendPushWithCallback(String title, String content, Map<String, String> extrasMap, PushPlatformEnum platform, PushTypeEnum PushTypeEnum, List<String> pushObject) {
ClientConfig clientConfig = ClientConfig.getInstance();
clientConfig.setTimeToLive(JPushConfig.LIVE_TIME);
String host = (String) clientConfig.get(ClientConfig.PUSH_HOST_NAME);
NettyHttpClient client = new NettyHttpClient(
ServiceHelper.getBasicAuthorization(JPushConfig.APP_KEY, JPushConfig.MASTER_SECRET), null, clientConfig);
try {
URI uri = new URI(host + clientConfig.get(ClientConfig.PUSH_PATH));
PushPayload payload = buildNotifyCustomPushPayload(title, content, extrasMap, platform, PushTypeEnum, pushObject);
client.sendRequest(HttpMethod.POST, payload.toString(), uri, responseWrapper -> {
if (200 == responseWrapper.responseCode) {
logger.info("极光推送成功");
} else {
logger.info("极光推送失败,返回结果: " + responseWrapper.responseContent);
}
});
} catch (URISyntaxException e) {
e.printStackTrace();
} finally {
// 需要手动关闭Netty请求进程,否则会一直保留
client.close();
}
}
/**
* 根据推送类型获取推送的受众
*/
private static Audience getAudience(PushTypeEnum pushTypeEnum, List<String> pushObject) {
switch (pushTypeEnum){
// 别名推送
case ALIAS:
return Audience.alias(filterEmptyAndRepeatElement(pushObject));
// 标签推送
case TAG:
return Audience.tag(filterEmptyAndRepeatElement(pushObject));
//注册ID
case REGISTRATION_ID:
return Audience.registrationId(filterEmptyAndRepeatElement(pushObject));
// 广播推送
default:
return Audience.all();
}
}
/**
* 过滤 空元素(需删除如:null,""," ")和重复的元素
*/
private static List<String> filterEmptyAndRepeatElement(List<String> stringList) {
return stringList.stream().filter(item -> item != null && !"".equals(item)).distinct().collect(Collectors.toList());
}
private static Platform getPlatform(PushPlatformEnum pushPlatformEnum){
switch (pushPlatformEnum){
case ALL:
return Platform.all();
case ANDROID:
return Platform.android();
case IOS:
return Platform.ios();
default:
return Platform.android_ios();
}
}
}