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);
}

 

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