此框架代碼爲單線程收發, 適用於用kafka轉送消息的業務,
如果要發送大量數據, 並且發送端有大量併發請求, 應當修改發送代碼.
代碼可以免費應用於商業代碼, 但請保留創作者信息.
本框架包含如下內容:
下面就把各類完整代碼發上來
AbstractConfig類:
package org.test;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.Properties;
/**
* 消息配置
* @see KafkaTemplate
* @author guishuanglin 2019-09-5
*/
public abstract class AbstractConfig {
//必須配置
protected String projectName; //接收消息的項目名稱
protected String servers; //主機地址, 集羣設置也只要填寫其中一個,如: 127.0.0.1:9092
protected String groupId; //接收組id, 不是做集羣,則ID不能相同.
protected String clientId; //客戶端ID, 都不能相同,一般用 [標識_本機IP]方式
protected List<String> topics = new ArrayList<String>(); //接收主題列表
public String getProjectName() {
return projectName;
}
public void setProjectName(String projectName) {
this.projectName = projectName;
}
public String getServers() {
return servers;
}
public void setServers(String servers) {
this.servers = servers;
}
public String getGroupId() {
return groupId;
}
public void setGroupId(String groupId) {
this.groupId = groupId;
}
public String getClientId() {
return clientId;
}
public void setClientId(String clientId) {
this.clientId = clientId;
}
public List<String> getTopics() {
return topics;
}
public void setTopics(List<String> topics) {
this.topics = topics;
}
/**
* 把 [a;b;c] 格式的主題轉換成數組
*/
public int setTopics(String str){
int size = 0;
if(str.length()>0){
String[] sArray=str.split(";");
for(String ts:sArray){
this.topics.add(ts);
}
}
return size;
}
/**
* 初始化配置參數
*/
public abstract boolean initConfig();
/**
* 把字段組成 Properties 格式的配置屬性
*/
public abstract Properties getPropertiesConfig();
/**
* 把字段組成 Map 格式的配置屬性
*/
public abstract Map<String,Object> getMapConfig();
/**
* 檢查配置是否正常
*/
public abstract boolean checkConfig();
}
DemoReceiveCallback 類:
package org.test;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.LinkedBlockingQueue;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import com.alibaba.fastjson.JSON;
/**
* 業務處理實現例子: 收發雙向場景回調處理 <br>
*
* <br>只發不收業務直接使用 KafkaTemplate 模板即可,不需要回調.<br>
*
* <br> 本例子實現了重發/超時/清理等業務:<br>
*
* <br>如果要提高處理速度可採用如下方式:<br>
* 1, receiveProcess 只接收消息,並放入本地接收隊列.<br>
* 2, 啓用本類中的處理線程,獨立處理[本地接收隊列].<br>
*
* <br>重點說明:<br>
* 增加或修改 IReceiveCallback 回調實現, 一定要同步修改 MsgReceivePreprocessor 接收處理判斷邏輯.
*
* @author guishuanglin 2019-09-5
*
*/
public class DemoReceiveCallback implements IReceiveCallback{
private Log logger = LogFactory.getLog(this.getClass());
private String fn ="[讀開關命令]";
//發送主題
private String topic = MsgTopic.TOPIC_COMMAND_SEND;
//發送子主題
private String subTopic = "r-switch";
//接收時消息主題
private String recTopic = MsgTopic.TOPIC_COMMAND_RECE;
//接收時消息子主題
private String recSubTopic = "r-switch";
//發送列表, 以便進行超時/收到回覆時處理 Object 是根據業務定義的bean
public final static Map<String, MsgRecord> sendMsgMap = new ConcurrentHashMap<String, MsgRecord>();
//接收列表, 用來存放處理好的接收結果
public final static Map<String, MsgRecord> receMsgMap = new ConcurrentHashMap<String, MsgRecord>();
//接收隊列(可以不需要), 如果消息太多無法快速處理, 則先放入本地隊列, 處理完之後結果放入receMsgMap
private final static LinkedBlockingQueue< MsgRecord > receiveQueue = new LinkedBlockingQueue< MsgRecord >();
//線程是否在運行
private static boolean isRuning = false;
//運行接收線程
private boolean receNext =true;
//運行重發/超時線程
private boolean sendNext =true;
//運行清理線程
private boolean clearNext =true;
//消息清理,最長保留時間(毫秒),默認5分鐘
private long clearTime =300000 ;
public DemoReceiveCallback() {
//回調增加到工廠
ReceiveProcessFactory.setReceiveCallback(this, this.getRecTopic(), this.getRecSubTopic());
//運行處理線程
runThread();
}
//--------------------- 業務處理子類, 發送消息實現 -----------------------------------------
/**
* 發送消息處理
* @param msgId 消息唯一ID
* @param key 消息key關鍵標籤
* @param message 消息體
* @return 返回msgId
*/
public String sendMessage(String topic, String key, String message, String msgId) {
KafkaTemplate.getInstance().sendMessage(topic, key, message, this);
MsgRecord msgRecord = new MsgRecord(topic, key, message, msgId);
sendMsgMap.put(msgId, msgRecord);
return msgId;
}
/**
* 發送消息處理
* @param MsgRecord 本地消息對象
* @return 返回msgId
*/
public String sendMessage(MsgRecord msgRecord) {
if(msgRecord.getMsgId() == null) {
logger.error(fn + ",發送消息錯誤: MsgId不能爲空");
return null;
}
KafkaTemplate.getInstance().sendMessage(msgRecord, this);
sendMsgMap.put(msgRecord.getMsgId(), msgRecord);
return msgRecord.getMsgId();
}
/**
* 查詢發送消息
*/
public MsgRecord getSendMessage(String msgId) {
return sendMsgMap.get(msgId);
}
/**
* 查詢回覆消息
*/
public MsgRecord getReceMessage(String msgId) {
return receMsgMap.get(msgId);
}
/**
* 查詢回覆消息
* @param msgId 消息唯一ID
* @param timeout 如果沒有取到消息,需要等待的最長時間 (ms)
* @return MsgRecord 查詢超時則返回空
*/
public MsgRecord getReceMessage(String msgId, int timeout) {
MsgRecord msgRecord = receMsgMap.get(msgId);
long tvb = System.currentTimeMillis();
while(msgRecord == null) {
msgRecord = receMsgMap.get(msgId);
if(msgRecord == null) {
long tve = System.currentTimeMillis();
if((tve - tvb) > timeout) {
break;
}
try {
Thread.sleep(2000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
if(msgRecord != null) {
if(logger.isInfoEnabled()) {
logger.info(fn +",查到消息:key = "+ msgId +", value = "+msgRecord.getValue());
}
}else {
if(logger.isInfoEnabled()) {
logger.info(fn +",查到消息:key = "+ msgId +", value = 空");
}
}
return msgRecord;
}
//--------------------- 業務處理子類, 接收消息回調 -----------------------------------------
@Override
public void receiveProcess(MsgRecord msgRecord) {
//1,直接入 接收 receMsgMap; 2,入接收隊列receiveQueue,
logger.info(fn +",接收消息:topic ="+ msgRecord.getTopic() +",key ="+ msgRecord.getKey() +",value ="+msgRecord.getValue());
//receMsgMap.put(msgRecord.getMsgId(), msgRecord);
try {
receiveQueue.put(msgRecord);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
@Override
public String getRecSubTopic() {
return this.recSubTopic;
}
@Override
public String getRecTopic() {
return this.recTopic;
}
/**
* 運行處理線程,如果已經運行,則不再重複運行.
*/
private void runThread() {
if(isRuning) return;
isRuning = true;
new Thread(new receiveThread()).start(); // 運行接收處理線程
//new Thread(new sendThread()).start(); // 重發,超時
//new Thread(new clearThread()).start(); // 清理緩存數據
}
/**
* 處理接收的消息線程, 獨立處理 receiveProcess 方法收到的消息,如果不需要處理則直接放入 receMsgMap 列表
*/
private class receiveThread implements Runnable{
@Override
public void run() {
MsgRecord msgRecord = null;
while (receNext) {
try {
// 出隊方法 receiveQueue.take(), 入隊方法 receiveQueue.put(E o) 必須配對使用, 否則隊列接收後可能不會及時觸發;
msgRecord = receiveQueue.take();
if(msgRecord != null) {
//處理消息....
receMsgMap.put(msgRecord.getMsgId(), msgRecord);
} else {
Thread.sleep(2000);
}
}catch (Exception e) {
logger.error(fn+",接收線程異常", e);
} finally {
}
}
}
}
/**
* 重發,超時(如果需要重發時用)
*/
private class sendThread implements Runnable{
@Override
public void run() {
long tvb = System.currentTimeMillis();
MsgRecord msgRecord = null;
boolean br = false;
while (sendNext) {
try {
//20秒運行一次
Thread.sleep(20000);
//
for(Map.Entry<String, MsgRecord> et : sendMsgMap.entrySet()){
msgRecord = et.getValue();
//是否收到
if(receMsgMap.containsKey(msgRecord.getMsgId())) {
continue;
}
if(msgRecord.getCount() > 3 ) {
//重發3次,則超時
msgRecord.setStatus(2);
}else {
br = KafkaTemplate.getInstance().sendMessage(topic, msgRecord.getKey(), msgRecord.getValue());
if(br) {
//重發成功
msgRecord.setStatus(1);
msgRecord.setCount(msgRecord.getCount() +1);
}else {
//重發失敗
msgRecord.setStatus(3);
msgRecord.setCount(msgRecord.getCount() +1);
}
}
}
}catch (Exception e) {
logger.error(fn+",重發線程異常", e);
} finally {
}
}
}
}
/**
* 清理線程
*/
private class clearThread implements Runnable{
@Override
public void run() {
long tvb = System.currentTimeMillis();
MsgRecord msgRecord = null;
boolean br = false;
while (clearNext) {
try {
for(Map.Entry<String, MsgRecord> et : sendMsgMap.entrySet()){
msgRecord = et.getValue();
if(msgRecord != null && (tvb - msgRecord.getMsgTime()) > clearTime ) {
//清理垃圾時間到
sendMsgMap.remove(et.getKey());
}
}
for(Map.Entry<String, MsgRecord> et : receMsgMap.entrySet()){
msgRecord = et.getValue();
if(msgRecord != null && (tvb - msgRecord.getMsgTime()) > clearTime ) {
//清理垃圾時間到
receMsgMap.remove(et.getKey());
}
}
//5分鐘運行一次
Thread.sleep(300000);
}catch (Exception e) {
logger.error(fn+",清理線程異常", e);
} finally {
}
}
}
}
public void close() {
receNext =false;
sendNext =false;
clearNext =false;
sendMsgMap.clear();
receMsgMap.clear();
receiveQueue.clear();
isRuning = false;
}
//測試
public static void main(String[] args) {
KafkaTemplate tlp = KafkaTemplate.getInstance();
//接收配置
MsgConsumerConfig cConfig = new MsgConsumerConfig();
//接收預處理
MsgReceivePreprocessor prep = new MsgReceivePreprocessor();
cConfig.setTopics("command-send;command-rece;data-up");
//啓用接收線程
tlp.runListener(cConfig, prep);
//調用發送方法
// for(int i = 0; i < 5; i++) {
// System.out.println("KafkaProduce ->"+i);
// 發送命令:讀取[上海項目]的[10000898]設備的[0號]開關狀態
// tlp.sendMessage("command-send", "r-switch", "{\"project\":\"sh\",\"id\":10000898,\"seq\":1567647660299001,\"command\":\"r-switch\",\"value\":0}");
//
// try {
// Thread.sleep(1 * 1000);
// } catch (InterruptedException e) {
// e.printStackTrace();
// }
// }
//
// try {
// Thread.sleep(3 * 1000);
// } catch (InterruptedException e) {
// e.printStackTrace();
// }
// //關閉接收/發送
// tlp.closeListener();
// tlp.closeProducer();
System.out.println("===========end=============");
}
}
DemoReceiveCallback2 類:
package org.test;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.LinkedBlockingQueue;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import com.alibaba.fastjson.JSON;
/**
* 業務處理實現例子: 只收不發的場景回調處理 ,<br>
*
* <br>只發不收業務直接使用 KafkaTemplate 模板即可,不需要回調.<br>
*
* <br>如果要提高處理速度可採用如下方式:<br>
* 1, receiveProcess 只接收消息,並放入本地接收隊列.<br>
* 2, 啓用本類中的處理線程,獨立處理[本地接收隊列].<br>
*
* <br>重點說明:<br>
* 增加或修改 IReceiveCallback 回調實現, 一定要同步修改 MsgReceivePreprocessor 接收處理判斷邏輯.
*
* @author guishuanglin 2019-09-5
*
*/
public class DemoReceiveCallback2 implements IReceiveCallback{
private Log logger = LogFactory.getLog(this.getClass());
private String fn ="[上報警告數據]";
//接收時消息主題
private String recTopic = MsgTopic.TOPIC_DATA_UP;
//接收時消息子主題
private String recSubTopic = MsgTopic.DATA_UP_ALARM;
//接收列表, 用來存放處理好的接收結果
public final static Map<String, MsgRecord> receMsgMap = new ConcurrentHashMap<String, MsgRecord>();
//接收隊列(可以不需要), 如果消息太多無法快速處理, 則先放入本地隊列, 處理完之後結果放入receMsgMap
private final static LinkedBlockingQueue< MsgRecord > receiveQueue = new LinkedBlockingQueue< MsgRecord >();
//線程是否在運行
private static boolean isRuning = false;
//運行接收線程
private boolean receNext =true;
//運行清理線程
private boolean clearNext =true;
//消息請理,最長保留時間(毫秒),默認5分鐘
private long clearTime =300000 ;
public DemoReceiveCallback2() {
//增加到工廠
ReceiveProcessFactory.setReceiveCallback(this, this.getRecTopic(), this.getRecSubTopic());
//運行處理線程
runThread();
}
//--------------------- 業務處理子類, 接收消息回調 -----------------------------------------
@Override
public void receiveProcess(MsgRecord msgRecord) {
//1,直接入 接收 receMsgMap; 2,入接收隊列receiveQueue,
logger.info(fn +",接收消息:topic ="+ msgRecord.getTopic() +",key ="+ msgRecord.getKey() +",value ="+msgRecord.getValue());
//receMsgMap.put(msgRecord.getMsgId(), msgRecord);
try {
receiveQueue.put(msgRecord);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
@Override
public String getRecSubTopic() {
return this.recSubTopic;
}
@Override
public String getRecTopic() {
return this.recTopic;
}
/**
* 運行處理線程,如果已經運行,則不再重複運行.
*/
private void runThread() {
if(isRuning) return;
isRuning = true;
new Thread(new receiveThread()).start(); // 運行接收處理線程
new Thread(new clearThread()).start(); // 清理緩存數據
}
/**
* 處理接收的消息線程
*/
private class receiveThread implements Runnable{
@Override
public void run() {
MsgRecord msgRecord = null;
while (receNext) {
try {
// 出隊方法 receiveQueue.take(), 入隊方法 receiveQueue.put(E o) 必須配對使用, 否則隊列接收後可能不會及時觸發;
msgRecord = receiveQueue.take();
if(msgRecord != null) {
//處理消息...
receMsgMap.put(msgRecord.getMsgId(), msgRecord);
} else {
Thread.sleep(2000);
}
}catch (Exception e) {
logger.error(fn+",接收線程異常", e);
} finally {
}
}
}
}
/**
* 清理線程
*/
private class clearThread implements Runnable{
@Override
public void run() {
long tvb = System.currentTimeMillis();
MsgRecord msgRecord = null;
boolean br = false;
while (clearNext) {
try {
for(Map.Entry<String, MsgRecord> et : receMsgMap.entrySet()){
msgRecord = et.getValue();
if(msgRecord != null && (tvb - msgRecord.getMsgTime()) > clearTime ) {
//清理垃圾時間到
receMsgMap.remove(et.getKey());
}
}
//5分鐘運行一次
Thread.sleep(300000);
}catch (Exception e) {
logger.error(fn+",清理線程異常", e);
} finally {
}
}
}
}
public void close() {
receNext =false;
clearNext =false;
receMsgMap.clear();
receiveQueue.clear();
isRuning = false;
}
//測試
public static void main(String[] args) {
KafkaTemplate tlp = KafkaTemplate.getInstance();
//接收配置
MsgConsumerConfig cConfig = new MsgConsumerConfig();
//接收預處理
MsgReceivePreprocessor prep = new MsgReceivePreprocessor();
cConfig.setTopics("command-send;command-rece;data-up");
//啓用接收線程
tlp.runListener(cConfig, prep);
//調用發送方法
// for(int i = 0; i < 5; i++) {
// System.out.println("KafkaProduce ->"+i);
// 發送命令:讀取[上海項目]的[10000898]設備的[0號]開關狀態
// tlp.sendMessage("command-send", "r-switch", "{\"project\":\"sh\",\"id\":10000898,\"seq\":1567647660299001,\"command\":\"r-switch\",\"value\":0}");
//
// try {
// Thread.sleep(1 * 1000);
// } catch (InterruptedException e) {
// e.printStackTrace();
// }
// }
//
// try {
// Thread.sleep(3 * 1000);
// } catch (InterruptedException e) {
// e.printStackTrace();
// }
// //關閉接收/發送
// tlp.closeListener();
// tlp.closeProducer();
System.out.println("===========end=============");
}
}
IKafkaOperation 接口:
package org.test;
/**
* kafka 操作模板接口
*
* @see KafkaTemplate
* @author guishuanglin 2019-09-5
*
*/
public interface IKafkaOperation {
//------------------------發送--------------------------------
/**
* kafka 發送數據
* @param topic 主題名稱
* @param message 消息內容
*/
public abstract boolean sendMessage(String topic, String message);
/**
* kafka 發送數據
* @param topic 主題名稱
* @param message 消息內容
* @param callback 回調處理,發送消息後,需要接收方回覆時則提供回調處理
*/
public abstract boolean sendMessage(String topic, String message, IReceiveCallback callback);
/**
* kafka 發送數據
* @param topic 主題名稱
* @param key 消息key
* @param message 消息內容
*/
public abstract boolean sendMessage(String topic, String key, String message);
/**
* kafka 發送數據
* @param topic 主題名稱
* @param key 消息key
* @param message 消息內容
* @param callback 回調處理, 發送消息後, 需要接收方回覆時, 則提供回調處理
*/
public abstract boolean sendMessage(String topic, String key, String message, IReceiveCallback callback);
/**
* kafka 發送數據
* @param msgRecord 本地消息對象
*/
public abstract boolean sendMessage(MsgRecord msgRecord);
/**
* kafka 發送數據
* @param msgRecord 本地消息對象
* @param callback 回調處理, 發送消息後, 需要接收方回覆時, 則提供回調處理
*/
public abstract boolean sendMessage(MsgRecord msgRecord, IReceiveCallback callback);
//-------------------------接收監聽線程-------------------------------
/**
* 註冊回調類,會在接收監聽時進行回調.
*/
public abstract void addReceiveCallback(IReceiveCallback callback);
/**
* 啓動 kafka 的 Consumer 數據監聽對象線程.
* 啓動前請調用 setConsumerConfig 設置參數
*/
public abstract boolean runListener(AbstractConfig config, IReceivePreprocessor prepr);
/**
* 關閉 kafka 的 Consumer 數據監聽對象線程.
*/
public abstract boolean closeListener();
/**
* 關閉 kafka 的 Producer 發送數據對象.
*/
public abstract boolean closeProducer();
}
IReceiveCallback 接口:
package org.test;
/**
* 接收消息,回調處理接口 <br>
* 一般由同個業務類來實現[發送]/[接收]處理一體化處理, 同時在業務類中有自己的發送/接收列表, <br>
* 等收到回覆消息後配合發送消息列表, 以實現消息交互處理<br>
* 接收消息之前,請把回調類增加到 ReceiveProcessFactory 中.
* <br>
* 在發送消息的業務中,分以下幾種情況:<br>
* 1 定時任務發送方式, 2 發送線程方式, 3 頁面請求方式.<br>
* <br>
* 在接收消息的業務中,分以下幾種情況:<br>
* 1 普通接收處理方式, 2 接收消息線程方式.<br>
*
* @author guishuanglin 2019-09-5
*
*/
public interface IReceiveCallback {
/**
* 接收消息回調方法
*/
public void receiveProcess(MsgRecord record);
/**
* 接收主題的名稱( 發送與接收主題可能不同 ), 比如:<br>
* 發送 device-control 主題<br>
* 接收 device-state 主題<br>
*/
public String getRecTopic();
/**
* 接收子主題名稱, 或叫分類名稱 ( 發送子主題,與接收主子題可能不同, 儘量要相同爲好 ), 比如:<br>
* device-control 主題中: Switch子類,<br>
* device-state 主題中 : switch子類<br>
* <br>
* 消息子主題應用場景:<br>
* 1, 子主題主要是爲了把同一主題下不同業務分開處理, 如果不需要子主題, 則返回主題名=主題名.<br>
*
* 提醒: 子主題用來在消息回調時, 用來獲取回調業務類.
*/
public String getRecSubTopic();
}
IReceivePreprocessor 接口:
package org.test;
import java.util.Map;
import org.apache.kafka.clients.consumer.ConsumerRecord;
/**
* 接收消息,預處理處理接口<br>
* 提取/解析一些常用的關鍵數據,方便後處理/判斷.
*
* @author guishuanglin 2019-09-5
*
*/
public interface IReceivePreprocessor {
/**
* 接收預處理
*/
public MsgRecord preprocessor(ConsumerRecord<String, String> inRecord, Map<String,Object> inMap);
}