項目里遇到需要在ActiveMQ上記錄任務日志的場景。其實AMQ本身自帶一個LoggingPlugin,但是用起來總是不切合項目的實際場景。思來想去,正好前段時間為其他項目做了個MQTT協議認證的插件,技術基礎已經有了,還是自己給項目寫一個定制化版的插件吧。
在我之前的文章ActiveMQ插件開發里介紹了如何開發一個AMQ的插件。其實這次的功能就是基于之前的代碼里的內容進行修改的。主要功能是每次AMQ接收到一個任務消息后,就往一臺服務器上使用HTTP POST方法發送一條消息。表示任務流經MQ。
先來看入口類,相比之前的代碼,增加了兩個參數,這兩個參數可以在配置文件activemq.xml中實現手動配置。
package com.cn.amqs;
import org.apache.activemq.broker.Broker;
import org.apache.activemq.broker.BrokerPlugin;
import org.apache.activemq.plugin.StatisticsBrokerPlugin;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
public class MessageLogPlugin implements BrokerPlugin {
private Log log = LogFactory.getLog(StatisticsBrokerPlugin.class);
private String seviceUrl;
private String sign;
public Broker installPlugin(Broker broker) throws Exception {
log.info("install MessageLogPlugin");
return new MessageLog(broker,serviceUrl,sign);
}
public void setServiceUrl(String serviceUrl) {
this.serviceUrl=serviceUrl;
}
……
}
主要功能在MessageLog類中,實現了幾個功能:
- 每來一個任務消息,判斷消息是否曾經來過,如果是第一次收到,則發送一條post消息到服務器上
- 記錄一個任務的消息數量
- 為了防止任務數量無限增長,設置了定時清理機制(但是由于每個任務都設置了Timer,適用的場景應該是任務較少或者任務可清理時間較短的場景,否則也是對資源的消耗)
- 區分任務上行還是任務下行
package com.cn.amqs;
import java.util.Timer;
import java.util.TimerTask;
import java.util.concurrent.ConcurrentHashMap;
import org.apache.activemq.broker.Broker;
import org.apache.activemq.broker.BrokerFilter;
import org.apache.activemq.broker.ProducerBrokerExchange;
import org.apache.activemq.command.Message;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
/**
* 實現每次任務到達MQ時自動往一個地址上送一條信息
* @author MiSterRabbit
*/
public class MessageLog extends BrokerFilter{
private Log log;
/**下行任務HashMap*/
private ConcurrentHashMap<Object, Integer> downWards;
/**上行任務HashMap*/
private ConcurrentHashMap<Object, Integer> upWards;
private String seviceUrl;
private String sign;
public MessageLog(Broker next,String seviceUrl,String sign) {
super(next);
downWards = new ConcurrentHashMap<Object, Integer>();
upWards = new ConcurrentHashMap<Object, Integer>();
this.seviceUrl=seviceUrl;
this.sign=sign.isEmpty()? "分部":"總部";
log = LogFactory.getLog(com.cn.amqs.MessageLog.class);
log.info("initialize Message Log plugin");
}
/**
* Timer類,實現定時清理日志HashMap,防止Map的無限增長
*/
class missionTimer extends TimerTask {
private String missionID;
private Log log;
private ConcurrentHashMap<Object, Integer> map;
public missionTimer(String missionID, Log log, ConcurrentHashMap<Object, Integer> map) {
this.missionID=missionID;
this.log=log;
this.map=map;
}
@Override
public void run() {
this.map.remove(missionID);
this.log.info("[FLOW_LOG] Remove expired mission: "+missionID);
}
}
/**
* 判斷日志是否在map中,如果不在,則發送一條消息,若存在,則增加計數器
* @param missionID 任務號
* @param map 任務下發和任務上送使用不同的map
*/
public synchronized void insertIntoMap(String missionID, ConcurrentHashMap<Object,Integer> map,String direction) {
if(map.containsKey(missionID)) {
int count = map.get(missionID)+1;
map.put(missionID,count);
this.log.debug("[FLOW_LOG] "+map);
} else{
map.put(missionID,1);
this.log.info("[FLOW_LOG] Receive a new "+direction+" mission: "+missionID);
// 開啟一個線程發送一條任務數據,這里的MissionSend類其實就是開啟一個線程發送一條http post消息
if (direction.equalsIgnoreCase("DOWNWARD")){
MissionSend tmqs = new MissionSend(missionID, super.getBrokerName().toString().substring(3), "ActiveMQ", "/opt/activemq/apache-activemq-5.13.4/data/mission.log", this.sign+"MQ收到下行任務", "ok", this.seviceUrl);
new Thread(tmqs,"mission_send").start();
} else {
MissionSend tmqs = new MissionSend(missionID, super.getBrokerName().toString().substring(3), "ActiveMQ", "/opt/activemq/apache-activemq-5.13.4/data/mission.log", this.sign+"MQ收到上行任務", "ok", this.seviceUrl);
new Thread(tmqs,"mission_send").start();
}
// 使用Timer定時清理,1800秒后清理這個任務
Timer timer =new Timer();
TimerTask task = new missionTimer(missionID,this.log,downWards);
timer.schedule(task,1800000);
}
}
/**
* 每當MQ收到一條生產者發送過來的消息的時候執行判斷。
*/
public void send(ProducerBrokerExchange producerExchange, Message messageSend) throws Exception {
// 如果任務ID不為空,且不是經由集群內部發過來的消息
if ((messageSend.getProperty("misid") != null) &&
(!producerExchange.getProducerState().getInfo().getProducerId().toString().contains("MQ_"))) {
// 如果目的地不包含UPLAOD字段,則判斷為消息下行,否則為消息上行。記錄一個日志,然后調用insertIntoMap判斷是否需要發送http post消息
if (!messageSend.getDestination().toString().toLowerCase().contains("upload")) {
this.log.info("[FLOW_LOG] Down Mission: " + messageSend.getProperty("misid") + ". Destination: " + messageSend.getDestination() + ". Producer: "+producerExchange.getConnectionContext().getConnection().getRemoteAddress());
insertIntoMap(messageSend.getProperty("misid").toString(),downWards,"DOWNWARD");
} else {
this.log.info("[FLOW_LOG] Up Mission: " + messageSend.getProperty("misid") + ". Destination: " + messageSend.getDestination()+". Producer: "+producerExchange.getConnectionContext().getConnection().getRemoteAddress());
insertIntoMap(messageSend.getProperty("misid").toString(),upWards,"UPWARD");
}
}
super.send(producerExchange, messageSend);
}
}
MissionSend的類可以自由擴展。我就不贅述了。
插件功能為AMQ帶來了極強的擴展性,用戶可以實現在不對現有功能進行修改的前提下進行功能的二次開發。有空我會整理一個插件可以實現的功能清單。其實如果有空,看看BrokerFilter這個類,就能明白插件能實現的功能了。