kafka 簡易發送/接收框架 之1

此框架代碼爲單線程收發, 適用於用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);
}

 

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章