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