@
簡介
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();
}
}
}