package com.kym.service.mq; import com.kym.service.aliyun.lot.AliyunLotConfig; import lombok.extern.slf4j.Slf4j; import org.apache.commons.codec.binary.Base64; import org.apache.qpid.jms.JmsConnection; import org.apache.qpid.jms.JmsConnectionListener; import org.apache.qpid.jms.message.JmsInboundMessageDispatch; import org.springframework.beans.factory.DisposableBean; import org.springframework.context.event.ContextRefreshedEvent; import org.springframework.context.event.EventListener; import org.springframework.scheduling.annotation.Async; import javax.crypto.Mac; import javax.crypto.spec.SecretKeySpec; import javax.jms.*; import javax.naming.Context; import javax.naming.InitialContext; import java.net.URI; import java.util.ArrayList; import java.util.Hashtable; import java.util.List; import java.util.concurrent.ExecutorService; import java.util.concurrent.LinkedBlockingQueue; import java.util.concurrent.ThreadPoolExecutor; import java.util.concurrent.TimeUnit; /** * 阿里云LOT AMQP消息处理(石斑鱼主板上报消息) * * @author skyline */ //@Component @Slf4j public class AmqpHandler implements DisposableBean { /** * 线程池 */ private final static ExecutorService executorService = new ThreadPoolExecutor( Runtime.getRuntime().availableProcessors(), Runtime.getRuntime().availableProcessors() * 2, 60, TimeUnit.SECONDS, new LinkedBlockingQueue<>(50000)); /** * AMQP连接 */ static List connections = new ArrayList<>(); // /** // * 工程代码泄露可能会导致 AccessKey 泄露,并威胁账号下所有资源的安全性。以下代码示例使用环境变量获取 AccessKey 的方式进行调用,仅供参考 // */ // private static String accessKey = "LTAI5tNhD2KFuLUN1hMEukmS"; // ; // private static String accessSecret = "dtVF6na8Hp9W8DmAoWI9k24VXwjNyM"; // private static String consumerGroupId = "DEFAULT_GROUP"; // //iotInstanceId:实例ID。若是2021年07月30日之前(不含当日)开通的公共实例,请填空字符串。 // private static String iotInstanceId = "iot-06z00hb4ys0z7ri"; // //控制台服务端订阅中消费组状态页客户端ID一栏将显示clientId参数。 // //建议使用机器UUID、MAC地址、IP等唯一标识等作为clientId。便于您区分识别不同的客户端。 // private static String clientId = "car-wash-1"; // //${YourHost}为接入域名,请参见AMQP客户端接入说明文档。 // //${uid}.iot-amqp.${YourRegionId}.aliyuncs.com 对于Java、.NET、Python 2.7、Node.js、Go客户端:端口号为5671 // // 公共实例endpoint:iot-06z00hb4ys0z7ri.amqp.iothub.aliyuncs.com // private static String host = "iot-06z00hb4ys0z7ri.amqp.iothub.aliyuncs.com"; // 指定单个进程启动的连接数 // 单个连接消费速率有限,请参考使用限制,最大64个连接 // 连接数和消费速率及rebalance相关,建议每500QPS增加一个连接 private static int connectionCount = 4; private static MessageListener messageListener = new MessageListener() { @Override public void onMessage(final Message message) { try { // 1.收到消息之后一定要ACK。 // 推荐做法:创建Session选择Session.AUTO_ACKNOWLEDGE,这里会自动ACK。 // 其他做法:创建Session选择Session.CLIENT_ACKNOWLEDGE,这里一定要调message.acknowledge()来ACK。 // message.acknowledge(); // 2.建议异步处理收到的消息,确保onMessage函数里没有耗时逻辑。 // 如果业务处理耗时过程过长阻塞住线程,可能会影响SDK收到消息后的正常回调。 executorService.submit(() -> processMessage(message)); } catch (Exception e) { log.error("submit task occurs exception ", e); } } }; private static JmsConnectionListener myJmsConnectionListener = new JmsConnectionListener() { /** * 连接成功建立。 */ @Override public void onConnectionEstablished(URI remoteURI) { log.info("onConnectionEstablished, remoteUri:{}", remoteURI); } /** * 尝试过最大重试次数之后,最终连接失败。 */ @Override public void onConnectionFailure(Throwable error) { log.error("onConnectionFailure, {}", error.getMessage()); } /** * 连接中断。 */ @Override public void onConnectionInterrupted(URI remoteURI) { log.info("onConnectionInterrupted, remoteUri:{}", remoteURI); } /** * 连接中断后又自动重连上。 */ @Override public void onConnectionRestored(URI remoteURI) { log.info("onConnectionRestored, remoteUri:{}", remoteURI); } @Override public void onInboundMessage(JmsInboundMessageDispatch envelope) { } @Override public void onSessionClosed(Session session, Throwable cause) { } @Override public void onConsumerClosed(MessageConsumer consumer, Throwable cause) { } @Override public void onProducerClosed(MessageProducer producer, Throwable cause) { } }; private final AliyunLotConfig aliyunLotConfig; public AmqpHandler(AliyunLotConfig aliyunLotConfig) { this.aliyunLotConfig = aliyunLotConfig; } /** * 订阅消息处理 * * @throws Exception */ private void doSubscribe() throws Exception { for (int i = 0; i < connectionCount; i++) { //参数说明,请参见AMQP客户端接入说明文档。 long timeStamp = System.currentTimeMillis(); //签名方法:支持hmacmd5、hmacsha1和hmacsha256。 String signMethod = "hmacsha1"; //userName组装方法,请参见AMQP客户端接入说明文档。 String userName = aliyunLotConfig.getClientId() + "-" + i + "|authMode=aksign" + ",signMethod=" + signMethod + ",timestamp=" + timeStamp + ",authId=" + aliyunLotConfig.getAccessKey() + ",iotInstanceId=" + aliyunLotConfig.getIotInstanceId() + ",consumerGroupId=" + aliyunLotConfig.getConsumerGroupId() + "|"; //计算签名,password组装方法,请参见AMQP客户端接入说明文档。 String signContent = "authId=" + aliyunLotConfig.getAccessKey() + "×tamp=" + timeStamp; String password = doSign(signContent, aliyunLotConfig.getAccessSecret(), signMethod); String connectionUrl = "failover:(amqps://" + aliyunLotConfig.getAmpqHost() + ":5671?amqp.idleTimeout=80000)" + "?failover.reconnectDelay=30"; Hashtable hashtable = new Hashtable<>(); hashtable.put("connectionfactory.SBCF", connectionUrl); hashtable.put("queue.QUEUE", "default"); hashtable.put(Context.INITIAL_CONTEXT_FACTORY, "org.apache.qpid.jms.jndi.JmsInitialContextFactory"); Context context = new InitialContext(hashtable); ConnectionFactory cf = (ConnectionFactory) context.lookup("SBCF"); Destination queue = (Destination) context.lookup("QUEUE"); // 创建连接。 Connection connection = cf.createConnection(userName, password); connections.add(connection); ((JmsConnection) connection).addConnectionListener(myJmsConnectionListener); // 创建会话。 // Session.CLIENT_ACKNOWLEDGE: 收到消息后,需要手动调用message.acknowledge()。 // Session.AUTO_ACKNOWLEDGE: SDK自动ACK(推荐)。 Session session = connection.createSession(false, Session.AUTO_ACKNOWLEDGE); connection.start(); // 创建Receiver连接。 MessageConsumer consumer = session.createConsumer(queue); consumer.setMessageListener(messageListener); } } /** * 计算签名,password组装方法,请参见AMQP客户端接入说明文档。 */ private static String doSign(String toSignString, String secret, String signMethod) throws Exception { SecretKeySpec signingKey = new SecretKeySpec(secret.getBytes(), signMethod); Mac mac = Mac.getInstance(signMethod); mac.init(signingKey); byte[] rawHmac = mac.doFinal(toSignString.getBytes()); return Base64.encodeBase64String(rawHmac); } /** * 在这里处理您收到消息后的具体业务逻辑。 */ private static void processMessage(Message message) { try { byte[] body = message.getBody(byte[].class); String content = new String(body); String topic = message.getStringProperty("topic"); String messageId = message.getStringProperty("messageId"); long generateTime = message.getLongProperty("generateTime"); log.info("receive message" + ",\n topic = " + topic + ",\n messageId = " + messageId + ",\n generateTime = " + generateTime + ",\n content = " + content); } catch (Exception e) { log.error("processMessage occurs error ", e); } } // 这里不能使用@PostConstruct,在初始化完成后, bean 进入增强阶段, 所以这个阶段的任何AOP都是无效的,https://www.cnblogs.com/eternityz/p/15330069.html @EventListener(classes = {ContextRefreshedEvent.class}) @Async public void init() { // 开启线程处理队列消息 handleMessage(); } private void handleMessage() { executorService.execute(() -> { try { doSubscribe(); } catch (Exception e) { throw new RuntimeException(e); } }); if (!executorService.isTerminated()) { try { Thread.sleep(100); } catch (InterruptedException e) { log.error("processing interrupted.", e); } } } /** * 关闭AMQP连接 * * @throws Exception */ @Override public void destroy() throws Exception { // 结束程序运行 log.info("run shutdown"); connections.forEach(c -> { try { c.close(); } catch (JMSException e) { log.error("failed to close connection", e); } }); executorService.shutdown(); if (executorService.awaitTermination(10, TimeUnit.SECONDS)) { log.info("shutdown success"); } else { log.info("failed to handle messages"); } } }