You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@rocketmq.apache.org by st...@apache.org on 2021/10/31 03:30:01 UTC
[rocketmq-dashboard] branch master updated: [ISSUE #30]Added DLQ
message management (#31)
This is an automated email from the ASF dual-hosted git repository.
styletang pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/rocketmq-dashboard.git
The following commit(s) were added to refs/heads/master by this push:
new 4b2b61e [ISSUE #30]Added DLQ message management (#31)
4b2b61e is described below
commit 4b2b61e39421720bdc29d84cd2d74b29bc337a5d
Author: zhangjidi2016 <10...@qq.com>
AuthorDate: Sun Oct 31 11:29:55 2021 +0800
[ISSUE #30]Added DLQ message management (#31)
* [ISSUE #30]Added DLQ message management
* remove the specific namesrvAddr in application.properties.
Co-authored-by: zhangjidi <zh...@cmss.chinamobile.com>
---
pom.xml | 13 ++
.../dashboard/controller/DlqMessageController.java | 73 ++++++++
.../dashboard/model/DlqMessageExcelModel.java | 81 +++++++++
.../dashboard/service/DlqMessageService.java | 26 +++
.../service/impl/DlqMessageServiceImpl.java | 68 ++++++++
.../apache/rocketmq/dashboard/util/ExcelUtil.java | 56 +++++++
src/main/resources/static/index.html | 1 +
src/main/resources/static/src/app.js | 3 +
src/main/resources/static/src/dlqMessage.js | 184 +++++++++++++++++++++
src/main/resources/static/src/i18n/en.js | 5 +-
src/main/resources/static/src/i18n/zh.js | 7 +-
src/main/resources/static/view/layout/_header.html | 1 +
src/main/resources/static/view/pages/consumer.html | 32 ++--
.../view/pages/{message.html => dlqMessage.html} | 143 ++++++++--------
src/main/resources/static/view/pages/message.html | 2 +-
.../controller/DlqMessageControllerTest.java | 138 ++++++++++++++++
16 files changed, 735 insertions(+), 98 deletions(-)
diff --git a/pom.xml b/pom.xml
index ff1384d..2ebb77a 100644
--- a/pom.xml
+++ b/pom.xml
@@ -100,6 +100,8 @@
<mockito-inline.version>3.3.3</mockito-inline.version>
<jaxb-api.version>2.3.1</jaxb-api.version>
<commons-pool2.version>2.4.3</commons-pool2.version>
+ <easyexcel.version>2.2.10</easyexcel.version>
+ <asm.version>4.2</asm.version>
</properties>
<dependencies>
@@ -234,6 +236,17 @@
<artifactId>commons-pool2</artifactId>
<version>${commons-pool2.version}</version>
</dependency>
+ <dependency>
+ <groupId>com.alibaba</groupId>
+ <artifactId>easyexcel</artifactId>
+ <version>${easyexcel.version}</version>
+ </dependency>
+ <dependency>
+ <groupId>org.ow2.asm</groupId>
+ <artifactId>asm</artifactId>
+ <version>${asm.version}</version>
+ </dependency>
+
</dependencies>
<build>
<plugins>
diff --git a/src/main/java/org/apache/rocketmq/dashboard/controller/DlqMessageController.java b/src/main/java/org/apache/rocketmq/dashboard/controller/DlqMessageController.java
new file mode 100644
index 0000000..500040d
--- /dev/null
+++ b/src/main/java/org/apache/rocketmq/dashboard/controller/DlqMessageController.java
@@ -0,0 +1,73 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.rocketmq.dashboard.controller;
+
+import com.google.common.collect.Lists;
+import javax.annotation.Resource;
+import javax.servlet.http.HttpServletResponse;
+import org.apache.rocketmq.common.MixAll;
+import org.apache.rocketmq.common.message.MessageExt;
+import org.apache.rocketmq.dashboard.exception.ServiceException;
+import org.apache.rocketmq.dashboard.model.DlqMessageExcelModel;
+import org.apache.rocketmq.dashboard.model.request.MessageQuery;
+import org.apache.rocketmq.dashboard.permisssion.Permission;
+import org.apache.rocketmq.dashboard.service.DlqMessageService;
+import org.apache.rocketmq.dashboard.util.ExcelUtil;
+import org.apache.rocketmq.tools.admin.MQAdminExt;
+import org.springframework.stereotype.Controller;
+import org.springframework.web.bind.annotation.GetMapping;
+import org.springframework.web.bind.annotation.RequestBody;
+import org.springframework.web.bind.annotation.RequestMapping;
+import org.springframework.web.bind.annotation.RequestMethod;
+import org.springframework.web.bind.annotation.RequestParam;
+import org.springframework.web.bind.annotation.ResponseBody;
+
+@Controller
+@RequestMapping("/dlqMessage")
+@Permission
+public class DlqMessageController {
+
+ @Resource
+ private DlqMessageService dlqMessageService;
+
+ @Resource
+ private MQAdminExt mqAdminExt;
+
+ @RequestMapping(value = "/queryDlqMessageByConsumerGroup.query", method = RequestMethod.POST)
+ @ResponseBody
+ public Object queryDlqMessageByConsumerGroup(@RequestBody MessageQuery query) {
+ return dlqMessageService.queryDlqMessageByPage(query);
+ }
+
+ @GetMapping(value = "/exportDlqMessage.do")
+ public void exportDlqMessage(HttpServletResponse response, @RequestParam String consumerGroup,
+ @RequestParam String msgId) {
+ MessageExt messageExt = null;
+ try {
+ String topic = MixAll.DLQ_GROUP_TOPIC_PREFIX + consumerGroup;
+ messageExt = mqAdminExt.viewMessage(topic, msgId);
+ } catch (Exception e) {
+ throw new ServiceException(-1, String.format("Failed to query message by Id: %s", msgId));
+ }
+ DlqMessageExcelModel excelModel = new DlqMessageExcelModel(messageExt);
+ try {
+ ExcelUtil.writeExcel(response, Lists.newArrayList(excelModel), "dlq", "dlq", DlqMessageExcelModel.class);
+ } catch (Exception e) {
+ throw new ServiceException(-1, String.format("export dlq message failed!"));
+ }
+ }
+}
diff --git a/src/main/java/org/apache/rocketmq/dashboard/model/DlqMessageExcelModel.java b/src/main/java/org/apache/rocketmq/dashboard/model/DlqMessageExcelModel.java
new file mode 100644
index 0000000..2476a23
--- /dev/null
+++ b/src/main/java/org/apache/rocketmq/dashboard/model/DlqMessageExcelModel.java
@@ -0,0 +1,81 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.rocketmq.dashboard.model;
+
+import com.alibaba.excel.annotation.ExcelProperty;
+import com.alibaba.excel.annotation.write.style.ColumnWidth;
+import com.alibaba.excel.metadata.BaseRowModel;
+import com.alibaba.excel.util.DateUtils;
+import com.google.common.base.Charsets;
+import java.io.Serializable;
+import java.util.Date;
+import lombok.Data;
+import org.apache.rocketmq.common.message.MessageExt;
+
+@Data
+public class DlqMessageExcelModel extends BaseRowModel implements Serializable {
+
+ @ExcelProperty(value = "topic", index = 0)
+ @ColumnWidth(value = 15)
+ private String topic;
+
+ @ExcelProperty(value = "msgId", index = 1)
+ @ColumnWidth(value = 15)
+ private String msgId;
+
+ @ExcelProperty(value = "bornHost", index = 2)
+ @ColumnWidth(value = 15)
+ private String bornHost;
+
+ @ExcelProperty(value = "bornTimestamp", index = 3)
+ @ColumnWidth(value = 25)
+ private String bornTimestamp;
+
+ @ExcelProperty(value = "storeTimestamp", index = 4)
+ @ColumnWidth(value = 25)
+ private String storeTimestamp;
+
+ @ExcelProperty(value = "reconsumeTimes", index = 5)
+ @ColumnWidth(value = 25)
+ private int reconsumeTimes;
+
+ @ExcelProperty(value = "properties", index = 6)
+ @ColumnWidth(value = 20)
+ private String properties;
+
+ @ExcelProperty(value = "messageBody", index = 7)
+ @ColumnWidth(value = 20)
+ private String messageBody;
+
+ @ExcelProperty(value = "bodyCRC", index = 8)
+ @ColumnWidth(value = 15)
+ private int bodyCRC;
+
+ public DlqMessageExcelModel(MessageExt messageExt) {
+ this.topic = messageExt.getTopic();
+ this.msgId = messageExt.getMsgId();
+ this.bornHost = messageExt.getBornHostString();
+ this.bornTimestamp = DateUtils.format(new Date(messageExt.getBornTimestamp()), DateUtils.DATE_FORMAT_19);
+ this.storeTimestamp = DateUtils.format(new Date(messageExt.getStoreTimestamp()), DateUtils.DATE_FORMAT_19);
+ this.reconsumeTimes = messageExt.getReconsumeTimes();
+ this.properties = messageExt.getProperties().toString();
+ this.messageBody = new String(messageExt.getBody(), Charsets.UTF_8);
+ this.bodyCRC = messageExt.getBodyCRC();
+ }
+
+}
diff --git a/src/main/java/org/apache/rocketmq/dashboard/service/DlqMessageService.java b/src/main/java/org/apache/rocketmq/dashboard/service/DlqMessageService.java
new file mode 100644
index 0000000..5cf9eb9
--- /dev/null
+++ b/src/main/java/org/apache/rocketmq/dashboard/service/DlqMessageService.java
@@ -0,0 +1,26 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.rocketmq.dashboard.service;
+
+import org.apache.rocketmq.dashboard.model.MessagePage;
+import org.apache.rocketmq.dashboard.model.request.MessageQuery;
+
+public interface DlqMessageService {
+
+ MessagePage queryDlqMessageByPage(MessageQuery query);
+}
diff --git a/src/main/java/org/apache/rocketmq/dashboard/service/impl/DlqMessageServiceImpl.java b/src/main/java/org/apache/rocketmq/dashboard/service/impl/DlqMessageServiceImpl.java
new file mode 100644
index 0000000..6fb822a
--- /dev/null
+++ b/src/main/java/org/apache/rocketmq/dashboard/service/impl/DlqMessageServiceImpl.java
@@ -0,0 +1,68 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.rocketmq.dashboard.service.impl;
+
+import com.google.common.base.Throwables;
+import java.util.ArrayList;
+import java.util.List;
+import javax.annotation.Resource;
+import lombok.extern.slf4j.Slf4j;
+import org.apache.rocketmq.client.exception.MQClientException;
+import org.apache.rocketmq.common.MixAll;
+import org.apache.rocketmq.common.protocol.ResponseCode;
+import org.apache.rocketmq.dashboard.model.MessagePage;
+import org.apache.rocketmq.dashboard.model.MessageView;
+import org.apache.rocketmq.dashboard.model.request.MessageQuery;
+import org.apache.rocketmq.dashboard.service.DlqMessageService;
+import org.apache.rocketmq.dashboard.service.MessageService;
+import org.apache.rocketmq.tools.admin.MQAdminExt;
+import org.springframework.data.domain.PageImpl;
+import org.springframework.data.domain.PageRequest;
+import org.springframework.stereotype.Service;
+
+@Service
+@Slf4j
+public class DlqMessageServiceImpl implements DlqMessageService {
+
+ @Resource
+ private MQAdminExt mqAdminExt;
+
+ @Resource
+ private MessageService messageService;
+
+ @Override
+ public MessagePage queryDlqMessageByPage(MessageQuery query) {
+ List<MessageView> messageViews = new ArrayList<>();
+ PageRequest page = PageRequest.of(query.getPageNum(), query.getPageSize());
+ String topic = query.getTopic();
+ try {
+ mqAdminExt.examineTopicRouteInfo(topic);
+ } catch (MQClientException e) {
+ // If the %DLQ%Group does not exist, the message returns null
+ if (topic.startsWith(MixAll.DLQ_GROUP_TOPIC_PREFIX)
+ && e.getResponseCode() == ResponseCode.TOPIC_NOT_EXIST) {
+ return new MessagePage(new PageImpl<>(messageViews, page, 0), query.getTaskId());
+ } else {
+ throw Throwables.propagate(e);
+ }
+ } catch (Exception e) {
+ throw Throwables.propagate(e);
+ }
+ return messageService.queryMessageByPage(query);
+ }
+}
diff --git a/src/main/java/org/apache/rocketmq/dashboard/util/ExcelUtil.java b/src/main/java/org/apache/rocketmq/dashboard/util/ExcelUtil.java
new file mode 100644
index 0000000..e95d165
--- /dev/null
+++ b/src/main/java/org/apache/rocketmq/dashboard/util/ExcelUtil.java
@@ -0,0 +1,56 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.rocketmq.dashboard.util;
+
+import com.alibaba.excel.EasyExcel;
+import com.alibaba.excel.support.ExcelTypeEnum;
+import com.alibaba.excel.write.metadata.style.WriteCellStyle;
+import com.alibaba.excel.write.metadata.style.WriteFont;
+import com.alibaba.excel.write.style.HorizontalCellStyleStrategy;
+import java.io.OutputStream;
+import java.net.URLEncoder;
+import java.util.List;
+import javax.servlet.http.HttpServletResponse;
+import org.apache.poi.ss.usermodel.HorizontalAlignment;
+
+public class ExcelUtil {
+
+ public static void writeExcel(HttpServletResponse response, List<? extends Object> data, String fileName,
+ String sheetName, Class clazz) throws Exception {
+ WriteCellStyle headWriteCellStyle = new WriteCellStyle();
+ WriteFont writeFont = new WriteFont();
+ writeFont.setFontHeightInPoints((short)12);
+ writeFont.setFontName("Microsoft YaHei UI");
+ headWriteCellStyle.setWriteFont(writeFont);
+ headWriteCellStyle.setHorizontalAlignment(HorizontalAlignment.CENTER);
+
+ WriteCellStyle contentWriteCellStyle = new WriteCellStyle();
+ contentWriteCellStyle.setWriteFont(writeFont);
+ contentWriteCellStyle.setHorizontalAlignment(HorizontalAlignment.CENTER);
+ HorizontalCellStyleStrategy horizontalCellStyleStrategy = new HorizontalCellStyleStrategy(headWriteCellStyle, contentWriteCellStyle);
+ EasyExcel.write(getOutputStream(fileName, response), clazz)
+ .excelType(ExcelTypeEnum.XLSX).sheet(sheetName).registerWriteHandler(horizontalCellStyleStrategy).doWrite(data);
+ }
+
+ private static OutputStream getOutputStream(String fileName, HttpServletResponse response) throws Exception {
+ fileName = URLEncoder.encode(fileName, "UTF-8");
+ response.setContentType("application/vnd.ms-excel");
+ response.setCharacterEncoding("utf8");
+ response.setHeader("Content-Disposition", "attachment;filename=" + fileName + ".xlsx");
+ return response.getOutputStream();
+ }
+}
diff --git a/src/main/resources/static/index.html b/src/main/resources/static/index.html
index 2b14c29..f212527 100644
--- a/src/main/resources/static/index.html
+++ b/src/main/resources/static/index.html
@@ -107,6 +107,7 @@
<script type="text/javascript" src="src/consumer.js?timestamp=6"></script>
<script type="text/javascript" src="src/producer.js"></script>
<script type="text/javascript" src="src/message.js"></script>
+<script type="text/javascript" src="src/dlqMessage.js"></script>
<script type="text/javascript" src="src/messageTrace.js"></script>
<script type="text/javascript" src="src/ops.js?timestamp=7"></script>
<script type="text/javascript" src="src/remoteApi/remoteApi.js"></script>
diff --git a/src/main/resources/static/src/app.js b/src/main/resources/static/src/app.js
index f8183fb..7fa68dc 100644
--- a/src/main/resources/static/src/app.js
+++ b/src/main/resources/static/src/app.js
@@ -195,6 +195,9 @@ app.config(['$routeProvider', '$httpProvider','$cookiesProvider','getDictNamePro
}).when('/message', {
templateUrl: 'view/pages/message.html',
controller:'messageController'
+ }).when('/dlqMessage', {
+ templateUrl: 'view/pages/dlqMessage.html',
+ controller:'dlqMessageController'
}).when('/messageTrace', {
templateUrl: 'view/pages/messageTrace.html',
controller:'messageTraceController'
diff --git a/src/main/resources/static/src/dlqMessage.js b/src/main/resources/static/src/dlqMessage.js
new file mode 100644
index 0000000..fd82db1
--- /dev/null
+++ b/src/main/resources/static/src/dlqMessage.js
@@ -0,0 +1,184 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+var module = app;
+const SYS_GROUP_TOPIC_PREFIX = "%SYS%";
+module.controller('dlqMessageController', ['$scope', 'ngDialog', '$http', 'Notification', function ($scope, ngDialog, $http, Notification) {
+ $scope.allConsumerGroupList = [];
+ $scope.selectedConsumerGroup = [];
+ $scope.messageId = "";
+ $scope.queryDlqMessageByConsumerGroupResult = [];
+ $scope.queryDlqMessageByMessageIdResult = {};
+ $http({
+ method: "GET",
+ url: "consumer/groupList.query"
+ }).success(function (resp) {
+ if (resp.status == 0) {
+ for (const consumerGroup of resp.data) {
+ if (!consumerGroup.group.startsWith(SYS_GROUP_TOPIC_PREFIX)) {
+ $scope.allConsumerGroupList.push(consumerGroup.group);
+ }
+ }
+ $scope.allConsumerGroupList.sort();
+ } else {
+ Notification.error({message: resp.errMsg, delay: 2000});
+ }
+ });
+ $scope.timepickerBegin = moment().subtract(3, 'hour').format('YYYY-MM-DD HH:mm');
+ $scope.timepickerEnd = moment().format('YYYY-MM-DD HH:mm');
+ $scope.timepickerOptions = {format: 'YYYY-MM-DD HH:mm', showClear: true};
+
+ $scope.taskId = "";
+
+ $scope.paginationConf = {
+ currentPage: 1,
+ totalItems: 0,
+ itemsPerPage: 20,
+ pagesLength: 15,
+ perPageOptions: [10],
+ rememberPerPage: 'perPageItems',
+ onChange: function () {
+ $scope.queryDlqMessageByConsumerGroup()
+ }
+ };
+
+ $scope.queryDlqMessageByConsumerGroup = function () {
+ $("#noMsgTip").css("display", "none");
+ if ($scope.timepickerEnd < $scope.timepickerBegin) {
+ Notification.error({message: "endTime is later than beginTime!", delay: 2000});
+ return
+ }
+ if ($scope.selectedConsumerGroup === [] || (typeof $scope.selectedConsumerGroup) == "object") {
+ return
+ }
+ $http({
+ method: "POST",
+ url: "dlqMessage/queryDlqMessageByConsumerGroup.query",
+ data: {
+ topic: DLQ_GROUP_TOPIC_PREFIX + $scope.selectedConsumerGroup,
+ begin: $scope.timepickerBegin.valueOf(),
+ end: $scope.timepickerEnd.valueOf(),
+ pageNum: $scope.paginationConf.currentPage,
+ pageSize: $scope.paginationConf.itemsPerPage,
+ taskId: $scope.taskId
+ }
+ }).success(function (resp) {
+ if (resp.status === 0) {
+ $scope.messageShowList = resp.data.page.content;
+ if ($scope.messageShowList.length == 0){
+ $("#noMsgTip").removeAttr("style");
+ }
+ console.log($scope.messageShowList);
+ if (resp.data.page.first) {
+ $scope.paginationConf.currentPage = 1;
+ }
+ $scope.paginationConf.currentPage = resp.data.page.number + 1;
+ $scope.paginationConf.totalItems = resp.data.page.totalElements;
+ $scope.taskId = resp.data.taskId
+ } else {
+ Notification.error({message: resp.errMsg, delay: 2000});
+ }
+ });
+ }
+
+ $scope.queryDlqMessageDetail = function (messageId, consumerGroup) {
+ $http({
+ method: "GET",
+ url: "messageTrace/viewMessage.query",
+ params: {
+ msgId: messageId,
+ topic: DLQ_GROUP_TOPIC_PREFIX + consumerGroup
+ }
+ }).success(function (resp) {
+ if (resp.status == 0) {
+ console.log(resp);
+ ngDialog.open({
+ template: 'dlqMessageDetailViewDialog',
+ data: resp.data
+ });
+ } else {
+ Notification.error({message: resp.errMsg, delay: 2000});
+ }
+ });
+ };
+
+ $scope.queryDlqMessageByMessageId = function (messageId, consumerGroup) {
+ $http({
+ method: "GET",
+ url: "messageTrace/viewMessage.query",
+ params: {
+ msgId: messageId,
+ topic: DLQ_GROUP_TOPIC_PREFIX + consumerGroup
+ }
+ }).success(function (resp) {
+ if (resp.status == 0) {
+ $scope.queryDlqMessageByMessageIdResult = resp.data;
+ } else {
+ Notification.error({message: resp.errMsg, delay: 2000});
+ }
+ });
+ };
+
+ $scope.changeShowMessageList = function (currentPage, totalItem) {
+ var perPage = $scope.paginationConf.itemsPerPage;
+ var from = (currentPage - 1) * perPage;
+ var to = (from + perPage) > totalItem ? totalItem : from + perPage;
+ $scope.messageShowList = $scope.queryMessageByTopicResult.slice(from, to);
+ $scope.paginationConf.totalItems = totalItem;
+ };
+
+ $scope.onChangeQueryCondition = function () {
+ console.log("change")
+ $scope.taskId = "";
+ $scope.paginationConf.currentPage = 1;
+ $scope.paginationConf.totalItems = 0;
+ }
+
+ $scope.resendDlqMessage = function (messageView, consumerGroup) {
+ const topic = messageView.properties.RETRY_TOPIC;
+ const msgId = messageView.properties.ORIGIN_MESSAGE_ID;
+ $http({
+ method: "POST",
+ url: "message/consumeMessageDirectly.do",
+ params: {
+ msgId: msgId,
+ consumerGroup: consumerGroup,
+ topic: topic
+ }
+ }).success(function (resp) {
+ if (resp.status == 0) {
+ ngDialog.open({
+ template: 'operationResultDialog',
+ data: {
+ result: resp.data
+ }
+ });
+ } else {
+ ngDialog.open({
+ template: 'operationResultDialog',
+ data: {
+ result: resp.errMsg
+ }
+ });
+ }
+ });
+ };
+
+ $scope.exportDlqMessage = function (msgId, consumerGroup) {
+ window.location.href = "dlqMessage/exportDlqMessage.do?msgId=" + msgId + "&consumerGroup=" + consumerGroup;
+ }
+}]);
\ No newline at end of file
diff --git a/src/main/resources/static/src/i18n/en.js b/src/main/resources/static/src/i18n/en.js
index 0792691..48f73b4 100644
--- a/src/main/resources/static/src/i18n/en.js
+++ b/src/main/resources/static/src/i18n/en.js
@@ -41,6 +41,7 @@ var en = {
"RESEND_MESSAGE":"Resend Message",
"VIEW_EXCEPTION":"View Exception",
"MESSAGETRACE":"MessageTrace",
+ "DLQ_MESSAGE":"DLQMessage",
"COMMIT": "Commit",
"OPERATION": "Operation",
"ADD": "Add",
@@ -108,5 +109,7 @@ var en = {
"ENABLE_MESSAGE_TRACE":"Enable Message Trace",
"MESSAGE_TRACE_DETAIL":"Message Trace Detail",
"TRACE_TOPIC":"TraceTopic",
- "SELECT_TRACE_TOPIC":"selectTraceTopic"
+ "SELECT_TRACE_TOPIC":"selectTraceTopic",
+ "EXPORT": "export",
+ "NO_MATCH_RESULT": "no match result"
}
\ No newline at end of file
diff --git a/src/main/resources/static/src/i18n/zh.js b/src/main/resources/static/src/i18n/zh.js
index af813d0..dad46d7 100644
--- a/src/main/resources/static/src/i18n/zh.js
+++ b/src/main/resources/static/src/i18n/zh.js
@@ -39,8 +39,9 @@ var zh = {
"PRODUCER":"生产者",
"MESSAGE":"消息",
"MESSAGE_DETAIL":"消息详情",
- "RESEND_MESSAGE":"重新消费",
+ "RESEND_MESSAGE":"重新发送",
"VIEW_EXCEPTION":"查看异常",
+ "DLQ_MESSAGE":"死信消息",
"MESSAGETRACE":"消息轨迹",
"OPERATION": "操作",
"ADD": "新增",
@@ -109,5 +110,7 @@ var zh = {
"ENABLE_MESSAGE_TRACE":"开启消息轨迹",
"MESSAGE_TRACE_DETAIL":"消息轨迹详情",
"TRACE_TOPIC":"消息轨迹主题",
- "SELECT_TRACE_TOPIC":"选择消息轨迹主题"
+ "SELECT_TRACE_TOPIC":"选择消息轨迹主题",
+ "EXPORT": "导出",
+ "NO_MATCH_RESULT": "没有查到符合条件的结果"
}
\ No newline at end of file
diff --git a/src/main/resources/static/view/layout/_header.html b/src/main/resources/static/view/layout/_header.html
index 21ed7cf..0309947 100644
--- a/src/main/resources/static/view/layout/_header.html
+++ b/src/main/resources/static/view/layout/_header.html
@@ -34,6 +34,7 @@
<li ng-class="path =='consumer' ? 'active':''"><a ng-href="#/consumer">{{'CONSUMER' | translate}}</a></li>
<li ng-class="path =='producer' ? 'active':''"><a ng-href="#/producer">{{'PRODUCER' | translate}}</a></li>
<li ng-class="path =='message' ? 'active':''"><a ng-href="#/message">{{'MESSAGE' | translate}}</a></li>
+ <li ng-class="path =='dlqMessage' ? 'active':''"><a ng-href="#/dlqMessage">{{'DLQ_MESSAGE' | translate}}</a></li>
<li ng-class="path =='messageTrace' ? 'active':''"><a ng-href="#/messageTrace">{{'MESSAGETRACE' | translate}}</a></li>
</ul>
<ul class="nav navbar-nav navbar-right">
diff --git a/src/main/resources/static/view/pages/consumer.html b/src/main/resources/static/view/pages/consumer.html
index 1f952a8..3a9ec17 100644
--- a/src/main/resources/static/view/pages/consumer.html
+++ b/src/main/resources/static/view/pages/consumer.html
@@ -209,8 +209,8 @@
<div class="modal-body " ng-repeat="item in ngDialogData.consumerRequestList">
<form id="addAppForm" name="addAppForm" class="form-horizontal" novalidate>
<div class="form-group" ng-hide="ngDialogData.bIsUpdate">
- <label class="control-label col-sm-4">clusterName:</label>
- <div class="col-sm-8">
+ <label class="control-label col-sm-3">clusterName:</label>
+ <div class="col-sm-9">
<select name="mySelectClusterNameList" multiple chosen
ng-model="item.clusterNameList"
ng-options="clusterNameItem for clusterNameItem in ngDialogData.allClusterNameList">
@@ -219,8 +219,8 @@
</div>
</div>
<div class="form-group">
- <label class="control-label col-sm-4">brokerName:</label>
- <div class="col-sm-8">
+ <label class="control-label col-sm-3">brokerName:</label>
+ <div class="col-sm-9">
<select name="mySelectBrokerNameList" multiple chosen
ng-disabled="ngDialogData.bIsUpdate"
ng-model="item.brokerNameList"
@@ -230,16 +230,16 @@
</div>
</div>
<div class="form-group">
- <label class="control-label col-sm-4">groupName:</label>
- <div class="col-sm-8">
+ <label class="control-label col-sm-3">groupName:</label>
+ <div class="col-sm-9">
<input class="form-control" ng-model="item.subscriptionGroupConfig.groupName" type="text"
ng-disabled="ngDialogData.bIsUpdate" required/>
<span class="text-danger" ng-show="addAppForm.name.$error.required">编号不能为空.</span>
</div>
</div>
<div class="form-group">
- <label class="control-label col-sm-4">consumeEnable:</label>
- <div class="col-sm-8">
+ <label class="control-label col-sm-3">consumeEnable:</label>
+ <div class="col-sm-9">
<md-switch class="md-primary" ng-disabled="{{!ngDialogData.writeOperationEnabled}}" md-no-ink
aria-label="Switch No Ink" ng-model="item.subscriptionGroupConfig.consumeEnable">
</md-switch>
@@ -257,8 +257,8 @@
<!--</div>-->
<!--</div>-->
<div class="form-group">
- <label class="control-label col-sm-4">consumeBroadcastEnable:</label>
- <div class="col-sm-8">
+ <label class="control-label col-sm-3">consumeBroadcastEnable:</label>
+ <div class="col-sm-9">
<md-switch class="md-primary" ng-disabled="{{!ngDialogData.writeOperationEnabled}}" md-no-ink
aria-label="Switch No Ink"
ng-model="item.subscriptionGroupConfig.consumeBroadcastEnable">
@@ -266,8 +266,8 @@
</div>
</div>
<div class="form-group">
- <label class="control-label col-sm-4">retryQueueNums:</label>
- <div class="col-sm-8">
+ <label class="control-label col-sm-3">retryQueueNums:</label>
+ <div class="col-sm-9">
<input class="form-control" ng-model="item.subscriptionGroupConfig.retryQueueNums"
type="text" ng-disabled="{{!ngDialogData.writeOperationEnabled}}"
required/>
@@ -284,16 +284,16 @@
<!--</div>-->
<!--</div>-->
<div class="form-group">
- <label class="control-label col-sm-2">brokerId:</label>
- <div class="col-sm-8">
+ <label class="control-label col-sm-3">brokerId:</label>
+ <div class="col-sm-9">
<input class="form-control" ng-model="item.subscriptionGroupConfig.brokerId" type="text"
ng-disabled="{{!ngDialogData.writeOperationEnabled}}" required/>
<span class="text-danger" ng-show="addAppForm.name.$error.required">编号不能为空.</span>
</div>
</div>
<div class="form-group">
- <label class="control-label col-sm-2">whichBrokerWhenConsumeSlowly:</label>
- <div class="col-sm-8">
+ <label class="control-label col-sm-3">whichBrokerWhenConsumeSlowly:</label>
+ <div class="col-sm-9">
<input class="form-control"
ng-model="item.subscriptionGroupConfig.whichBrokerWhenConsumeSlowly" type="text"
ng-disabled="{{!ngDialogData.writeOperationEnabled}}" required/>
diff --git a/src/main/resources/static/view/pages/message.html b/src/main/resources/static/view/pages/dlqMessage.html
similarity index 74%
copy from src/main/resources/static/view/pages/message.html
copy to src/main/resources/static/view/pages/dlqMessage.html
index ca626e4..f2a68f8 100644
--- a/src/main/resources/static/view/pages/message.html
+++ b/src/main/resources/static/view/pages/dlqMessage.html
@@ -19,19 +19,19 @@
<div ng-cloak="" class="tabsdemoDynamicHeight">
<md-content>
<md-tabs md-dynamic-height="" md-border-bottom="">
- <md-tab label="Topic">
+ <md-tab label="Consumer">
<h5 class="md-display-5">Total {{paginationConf.totalItems}} Messages</h5>
<md-content class="md-padding" style="min-height:600px">
<div class="row">
<form class="form-inline pull-left">
<div class="form-group">
- <label>{{'TOPIC' | translate}}:</label>
+ <label>{{'CONSUMER' | translate}}:</label>
</div>
<div class="form-group ">
<div style="width: 300px">
- <select name="mySelectTopic" chosen
- ng-model="selectedTopic"
- ng-options="item for item in allTopicList"
+ <select name="mySelectGroup" chosen
+ ng-model="selectedConsumerGroup"
+ ng-options="item for item in allConsumerGroupList"
ng-change="onChangeQueryCondition()"
required>
<option value=""></option>
@@ -61,8 +61,8 @@
<button id="searchAppsButton" type="button"
class="btn btn-raised btn-sm btn-primary"
data-toggle="modal"
- ng-click="queryMessagePageByTopic()">
- <span class="glyphicon glyphicon-search"></span>{{ 'SEARCH' | translate}}
+ ng-click="queryDlqMessageByConsumerGroup()">
+ <span class="glyphicon glyphicon-search"></span> {{ 'SEARCH' | translate}}
</button>
</form>
<table class="table table-bordered text-middle">
@@ -73,6 +73,9 @@
<th class="text-center">StoreTime</th>
<th class="text-center">Operation</th>
</tr>
+ <tr style="display: none" id="noMsgTip">
+ <td colspan="6" style="text-align: center">{{'NO_MATCH_RESULT' | translate}}</td>
+ </tr>
<tr ng-repeat="item in messageShowList">
<td class="text-center">{{item.msgId}}</td>
<td class="text-center">{{item.properties.TAGS}}</td>
@@ -81,7 +84,16 @@
</td>
<td class="text-center">
<button class="btn btn-raised btn-sm btn-primary" type="button"
- ng-click="queryMessageByMessageId(item.msgId,item.topic)">{{'MESSAGE_DETAIL' | translate}}
+ ng-click="queryDlqMessageDetail(item.msgId, selectedConsumerGroup)">
+ {{'MESSAGE_DETAIL' | translate}}
+ </button>
+ <button class="btn btn-raised btn-sm btn-primary" type="button"
+ ng-click="resendDlqMessage(item, selectedConsumerGroup)">
+ {{'RESEND_MESSAGE' | translate}}
+ </button>
+ <button class="btn btn-raised btn-sm btn-primary" type="button"
+ ng-click="exportDlqMessage(item.msgId, selectedConsumerGroup)">
+ {{'EXPORT' | translate}}
</button>
</td>
</tr>
@@ -90,33 +102,33 @@
</div>
</md-content>
</md-tab>
- <md-tab label="Message Key">
- <h5 class="md-display-5">Only Return 64 Messages</h5>
+ <md-tab label="Message ID">
+ <h5 class="md-display-5">topic can't be empty if you producer client version>=v3.5.8</h5>
<md-content class="md-padding" style="min-height:600px">
<div class="row">
<form class="form-inline pull-left">
<div class="form-group">
- <label>Topic:</label>
+ <label>{{'CONSUMER' | translate}}:</label>
</div>
- <div class="form-group">
+ <div class="form-group ">
<div style="width: 300px">
- <select name="mySelectTopic" chosen
- ng-model="selectedTopic"
- ng-options="item for item in allTopicList"
+ <select name="mySelectGroup" chosen
+ ng-model="selectedConsumerGroup"
+ ng-options="item for item in allConsumerGroupList"
+ ng-change="onChangeQueryCondition()"
required>
<option value=""></option>
</select>
</div>
</div>
<div class="form-group">
- <label>Key:</label>
- <input class="form-control" style="width: 450px" type="text" ng-model="key"
+ <label>MessageId:</label>
+ <input class="form-control" style="width: 450px" type="text" ng-model="messageId"
required/>
</div>
-
<button type="button" class="btn btn-raised btn-sm btn-primary" data-toggle="modal"
- ng-click="queryMessageByTopicAndKey()">
- <span class="glyphicon glyphicon-search"></span>{{ 'SEARCH' | translate}}
+ ng-click="queryDlqMessageByMessageId(messageId, selectedConsumerGroup)">
+ <span class="glyphicon glyphicon-search"></span> {{ 'SEARCH' | translate}}
</button>
</form>
<table class="table table-bordered text-middle">
@@ -127,7 +139,7 @@
<th class="text-center">StoreTime</th>
<th class="text-center">Operation</th>
</tr>
- <tr ng-repeat="item in queryMessageByTopicAndKeyResult">
+ <tr ng-repeat="item in queryDlqMessageByMessageIdResult">
<td class="text-center">{{item.msgId}}</td>
<td class="text-center">{{item.properties.TAGS}}</td>
<td class="text-center">{{item.properties.KEYS}}</td>
@@ -135,7 +147,16 @@
</td>
<td class="text-center">
<button class="btn btn-raised btn-sm btn-primary" type="button"
- ng-click="queryMessageByMessageId(item.msgId,item.topic)">Message Detail
+ ng-click="queryDlqMessageDetail(item.msgId, selectedConsumerGroup)">
+ {{'MESSAGE_DETAIL' | translate}}
+ </button>
+ <button class="btn btn-raised btn-sm btn-primary" type="button"
+ ng-click="resendDlqMessage(item, selectedConsumerGroup)">
+ {{'RESEND_MESSAGE' | translate}}
+ </button>
+ <button class="btn btn-raised btn-sm btn-primary" type="button"
+ ng-click="exportDlqMessage(item.msgId, selectedConsumerGroup)">
+ {{'EXPORT' | translate}}
</button>
</td>
</tr>
@@ -143,37 +164,6 @@
</div>
</md-content>
</md-tab>
- <md-tab label="Message ID">
- <h5 class="md-display-5">topic can't be empty if you producer client version>=v3.5.8</h5>
- <md-content class="md-padding" style="min-height:600px">
- <div class="row">
- <form class="form-inline pull-left">
- <div class="form-group">
- <label>Topic:</label>
- </div>
- <div class="form-group ">
- <div style="width: 300px">
- <select name="mySelectTopic" chosen
- ng-model="selectedTopic"
- ng-options="item for item in allTopicList"
- required>
- <option value=""></option>
- </select>
- </div>
- </div>
- <div class="form-group">
- <label>MessageId:</label>
- <input class="form-control" style="width: 450px" type="text" ng-model="messageId"
- required/>
- </div>
- <button type="button" class="btn btn-raised btn-sm btn-primary" data-toggle="modal"
- ng-click="queryMessageByMessageId(messageId,selectedTopic)">
- <span class="glyphicon glyphicon-search"></span>{{ 'SEARCH' | translate}}
- </button>
- </form>
- </div>
- </md-content>
- </md-tab>
</md-tabs>
</md-content>
</div>
@@ -182,7 +172,7 @@
</div>
-<script type="text/ng-template" id="messageDetailViewDialog">
+<script type="text/ng-template" id="dlqMessageDetailViewDialog">
<md-content class="md-padding">
<div>
<form id="addAppForm" name="addAppForm" class="form-horizontal" novalidate>
@@ -199,6 +189,20 @@
</div>
</div>
<div class="form-group">
+ <label class="control-label col-sm-2">Properties:</label>
+ <div class="col-sm-10">
+ <textarea class="form-control"
+ style="min-height:60px; resize: none"
+ readonly>{{ngDialogData.messageView.properties}}</textarea>
+ </div>
+ </div>
+ <div class="form-group">
+ <label class="control-label col-sm-2">ReconsumeTimes:</label>
+ <div class="col-sm-10">
+ <label class="form-control">{{ngDialogData.messageView.reconsumeTimes}}</label>
+ </div>
+ </div>
+ <div class="form-group">
<label class="control-label col-sm-2">Tag:</label>
<div class="col-sm-10">
<label class="form-control">{{ngDialogData.messageView.properties.TAGS}}</label>
@@ -217,39 +221,22 @@
</div>
</div>
<div class="form-group">
+ <label class="control-label col-sm-2">StoreHost:</label>
+ <div class="col-sm-10">
+ <label class="form-control">{{ngDialogData.messageView.storeHost}}</label>
+ </div>
+ </div>
+ <div class="form-group">
<label class="control-label col-sm-2">Message body:</label>
<div class="col-sm-10">
<textarea class="form-control"
ng-model="ngDialogData.messageView.messageBody"
- style="min-height:200px; resize: none"
+ style="min-height:100px; resize: none"
readonly></textarea>
</div>
</div>
</form>
</div>
- <p>messageTrackList:</p>
- <table class="table-bordered table">
- <tr>
- <th class="text-center">consumerGroup</th>
- <th class="text-center">trackType</th>
- <!--<th class="text-center">exceptionDesc</th>-->
- <th class="text-center">Operation</th>
- </tr>
- <tr ng-repeat="item in ngDialogData.messageTrackList">
- <td class="text-center">{{item.consumerGroup}}</td>
- <td class="text-center">{{item.trackType}}</td>
- <td class="text-center">
- <button class="btn btn-raised btn-sm btn-primary" type="button"
- ng-click="resendMessage(ngDialogData.messageView,item.consumerGroup)">
- {{ 'RESEND_MESSAGE' | translate}}
- </button>
- <button class="btn btn-raised btn-sm btn-primary" type="button"
- ng-click="showExceptionDesc(item.exceptionDesc)">
- {{ 'VIEW_EXCEPTION' | translate}}
- </button>
- </td>
- </tr>
- </table>
</md-content>
<div class="modal-footer">
<div class="ngdialog-buttons">
diff --git a/src/main/resources/static/view/pages/message.html b/src/main/resources/static/view/pages/message.html
index ca626e4..a2bc2b6 100644
--- a/src/main/resources/static/view/pages/message.html
+++ b/src/main/resources/static/view/pages/message.html
@@ -228,7 +228,7 @@
</form>
</div>
<p>messageTrackList:</p>
- <table class="table-bordered table">
+ <table class="table-bordered table text-middle">
<tr>
<th class="text-center">consumerGroup</th>
<th class="text-center">trackType</th>
diff --git a/src/test/java/org/apache/rocketmq/dashboard/controller/DlqMessageControllerTest.java b/src/test/java/org/apache/rocketmq/dashboard/controller/DlqMessageControllerTest.java
new file mode 100644
index 0000000..4254299
--- /dev/null
+++ b/src/test/java/org/apache/rocketmq/dashboard/controller/DlqMessageControllerTest.java
@@ -0,0 +1,138 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.rocketmq.dashboard.controller;
+
+import com.alibaba.fastjson.JSON;
+import com.google.common.collect.Lists;
+import org.apache.rocketmq.client.exception.MQClientException;
+import org.apache.rocketmq.common.MixAll;
+import org.apache.rocketmq.common.protocol.ResponseCode;
+import org.apache.rocketmq.common.protocol.route.TopicRouteData;
+import org.apache.rocketmq.dashboard.model.MessagePage;
+import org.apache.rocketmq.dashboard.model.MessageView;
+import org.apache.rocketmq.dashboard.model.request.MessageQuery;
+import org.apache.rocketmq.dashboard.service.impl.DlqMessageServiceImpl;
+import org.apache.rocketmq.dashboard.service.impl.MessageServiceImpl;
+import org.apache.rocketmq.dashboard.util.MockObjectUtil;
+import org.junit.Test;
+import org.mockito.InjectMocks;
+import org.mockito.Mock;
+import org.mockito.Spy;
+import org.springframework.data.domain.PageImpl;
+import org.springframework.data.domain.PageRequest;
+import org.springframework.http.MediaType;
+import org.springframework.test.web.servlet.request.MockMvcRequestBuilders;
+
+import static org.hamcrest.Matchers.hasSize;
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.Mockito.when;
+import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.content;
+import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath;
+import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
+
+public class DlqMessageControllerTest extends BaseControllerTest {
+
+ @InjectMocks
+ private DlqMessageController dlqMessageController;
+
+ @Spy
+ private DlqMessageServiceImpl dlqMessageService;
+
+ @Mock
+ private MessageServiceImpl messageService;
+
+ @Test
+ public void testQueryDlqMessageByConsumerGroup() throws Exception {
+ final String url = "/dlqMessage/queryDlqMessageByConsumerGroup.query";
+ MessageQuery query = new MessageQuery();
+ query.setPageNum(1);
+ query.setPageSize(10);
+ query.setTopic(MixAll.DLQ_GROUP_TOPIC_PREFIX + "group_test");
+ query.setTaskId("");
+ query.setBegin(System.currentTimeMillis() - 3 * 24 * 60 * 60 * 1000);
+ query.setEnd(System.currentTimeMillis());
+ {
+ TopicRouteData topicRouteData = MockObjectUtil.createTopicRouteData();
+ when(mqAdminExt.examineTopicRouteInfo(any()))
+ .thenThrow(new MQClientException(ResponseCode.TOPIC_NOT_EXIST, "topic not exist"))
+ .thenThrow(new MQClientException(ResponseCode.NO_MESSAGE, "query no message"))
+ .thenThrow(new RuntimeException())
+ .thenReturn(topicRouteData);
+ MessageView messageView = MessageView.fromMessageExt(MockObjectUtil.createMessageExt());
+ PageRequest page = PageRequest.of(query.getPageNum(), query.getPageSize());
+ MessagePage messagePage = new MessagePage
+ (new PageImpl<>(Lists.newArrayList(messageView), page, 0), query.getTaskId());
+ when(messageService.queryMessageByPage(any())).thenReturn(messagePage);
+ }
+ requestBuilder = MockMvcRequestBuilders.post(url);
+ requestBuilder.contentType(MediaType.APPLICATION_JSON_UTF8);
+ requestBuilder.content(JSON.toJSONString(query));
+ // 1、%DLQ%group_test is not exist
+ perform = mockMvc.perform(requestBuilder);
+ perform.andExpect(status().isOk())
+ .andExpect(jsonPath("$.data.page.content", hasSize(0)));
+
+ // 2、Other MQClientException occur
+ perform = mockMvc.perform(requestBuilder);
+ perform.andExpect(status().isOk())
+ .andExpect(jsonPath("$.data").doesNotExist())
+ .andExpect(jsonPath("$.status").value(-1))
+ .andExpect(jsonPath("$.errMsg").isNotEmpty());
+
+ // 3、Other Exception occur
+ perform = mockMvc.perform(requestBuilder);
+ perform.andExpect(status().isOk())
+ .andExpect(jsonPath("$.data").doesNotExist())
+ .andExpect(jsonPath("$.status").value(-1))
+ .andExpect(jsonPath("$.errMsg").isNotEmpty());
+
+ // 4、query dlq message success
+ perform = mockMvc.perform(requestBuilder);
+ perform.andExpect(status().isOk())
+ .andExpect(jsonPath("$.data.page.content", hasSize(1)))
+ .andExpect(jsonPath("$.data.page.content[0].msgId").value("0A9A003F00002A9F0000000000000319"));
+ }
+
+ @Test
+ public void testExportDlqMessage() throws Exception {
+ final String url = "/dlqMessage/exportDlqMessage.do";
+ {
+ when(mqAdminExt.viewMessage(any(), any()))
+ .thenThrow(new RuntimeException())
+ .thenReturn(MockObjectUtil.createMessageExt());
+ }
+ requestBuilder = MockMvcRequestBuilders.get(url);
+ requestBuilder.param("consumerGroup", "group_test");
+ requestBuilder.param("msgId", "0A9A003F00002A9F0000000000000319");
+ // 1、viewMessage exception
+ perform = mockMvc.perform(requestBuilder);
+ perform.andExpect(status().isOk())
+ .andExpect(jsonPath("$.data").doesNotExist())
+ .andExpect(jsonPath("$.status").value(-1))
+ .andExpect(jsonPath("$.errMsg").isNotEmpty());
+
+ // 2、export dlqMessage success
+ perform = mockMvc.perform(requestBuilder);
+ perform.andExpect(status().is(200))
+ .andExpect(content().contentType("application/vnd.ms-excel"));
+
+ }
+
+ @Override protected Object getTestController() {
+ return dlqMessageController;
+ }
+}