系统开发背景介绍
鉴于协同办公平台内嵌子系统过多,各种大大小小的WEB项目大几十。平时项目组各自又都忙于日常的新项目开发任务,根本无暇顾及平台内的各子统运行情况。导致平台内某个子系统出现运行异常或者报错时,只有等用户反馈。这时候往往已经产生了脏数据,并且日志已经不知道到记录到哪个时间段啦,排查问题也比较浪费时间。
于是就萌生了做个平台异常监控告警的系统,用来实时监控平台下各个子系统的运行情况,一旦有子系统出现运行异常或者报错就会被立刻捕获推送到告警系统。告警系统会发送短信/邮件通知,并把捕获到的子系统报错信息广播到运维支撑监控平台,实现平台的系统运行实时监控和高效管理。
系统开发思路
虽然协同办公平台内嵌了很多子系统,但实则只是个portal门户,里面根据登录账号的角色来授权系统模块和菜单。各个子系统也是通过sso单点登录跟统一用户和组织管理以及统一授权来实现相互调用的,同时各个子系统都是springboot开发的。 所以我就想到采用spring的全局异常统一处理,利用@ControllerAdvice注解来自定义一个全局异常捕获类。
随后通过RestTemplate发送一个post请求,把ExceptionLog作为json参数发送到异常捕获告警系统上去。异常告警收到发送过来的新异常后,通过websocket把异常信息广播推送到平台运维支撑告警系统上,随后发送短信通知/邮件通知。
注: 本项目组主要负责开发的是区政府办公平台,内网居多。并发量不高,同时各子系统运行异常次数也不算多,故而对捕获的异常直接采用RestTemplate发送http请求推送到告警系统。如果是面对商用高并发场景,此处可改为使用MQ消息队列或者redis的list对象存储,实现各子系统的异常捕获推送到告警系统。
子系统异常捕获代码实现
1. 首先在子系统项目代码config的package下新建一个自定义全局异常处理类: GlobalExceptionHandler, 在里面定义一些常见异常的捕获。编写统一的异常捕获处理方法,封装请求信息和异常信息然后推送到告警系统。
* @author 作者: wangqingchen
* @date 时间: 2022/3/26 10:55
* @description 说明: 自定义全局异常处理类
@ControllerAdvice
public class GlobalExceptionHandler {
private static final Logger logger = LoggerFactory.getLogger(GlobalExceptionHandler.class);
// 配置文件的系统标识名称,推送到告警系统用来标识那个子系统报错
@Value("${spring.application.name}")
private String applicationName;
// 配置文件里面的异常推送告警系统路径
@Value("${hosts.SystemMonitorUrl}")
private String SystemMonitorUrl;
// 协同办公平台架构统一核心包,sso,bua(统一用户组织管理和统一授权)依赖包
@Autowired
CoreFactory coreFactory;
@ExceptionHandler(value = NullPointerException.class)
@ResponseBody
public Layui NPEHandler(HttpServletRequest req, NullPointerException e){
return exceptionHandler( req, e,"NULL_POINTER_EXCE");
@ExceptionHandler(value = TemplateInputException.class)
@ResponseBody
public Layui TemplateExceptionHandler(HttpServletRequest req, TemplateInputException e){
return exceptionHandler( req, e,"PAGE_TEMPLATE_PARSE_EXCE");
@ExceptionHandler(value = ClassCastException.class)
@ResponseBody
public Layui ClassCastExceptionHandler(HttpServletRequest req, ClassCastException e){
return exceptionHandler( req, e,"CLASS_CAST_EXCE");
@ExceptionHandler(value = FileNotFoundException.class)
@ResponseBody
public Layui FileNotFoundExceptionHandler(HttpServletRequest req, FileNotFoundException e){
return exceptionHandler( req, e,"FILE_NOT_FOUND_EXCE");
@ExceptionHandler(value = SQLException.class)
@ResponseBody
public Layui SQLExceptionHandler(HttpServletRequest req, SQLException e){
return exceptionHandler( req, e,"SQL_EXCE");
@ExceptionHandler(value = BadSqlGrammarException.class)
@ResponseBody
public Layui SQLSyntaxExceptionHandler(HttpServletRequest req, BadSqlGrammarException e){
return exceptionHandler( req, e,"SQL_EXCE");
@ExceptionHandler(value = IOException.class)
@ResponseBody
public Layui IOExceptionHandler(HttpServletRequest req, IOException e){
return exceptionHandler( req, e,"IO_EXCE");
@ExceptionHandler(value = IndexOutOfBoundsException.class)
@ResponseBody
public Layui IndexOutOfBoundsExceptionHandler(HttpServletRequest req, IndexOutOfBoundsException e){
return exceptionHandler( req, e, "INDEX_OUTOF_BOUNDS_EXCE");
@ExceptionHandler(value = TypeMismatchException.class)
@ResponseBody
public Layui Server_400_ExceptionHandler(HttpServletRequest req, TypeMismatchException e){
return exceptionHandler( req, e, "SERVER_400_EXCE");
@ExceptionHandler({ConversionNotSupportedException.class, HttpMessageNotWritableException.class})
@ResponseBody
public Layui Server_500_ExceptionHandler(HttpServletRequest req, RuntimeException e){
return exceptionHandler( req, e, "SERVER_500_EXCE");
@ExceptionHandler(value = RuntimeException.class)
@ResponseBody
public Layui RunTimeExceptionHandler(HttpServletRequest req, RuntimeException e){
return exceptionHandler( req, e, "RUNTIME_EXCE");
@ExceptionHandler(value = Exception.class)
@ResponseBody
public Layui otherExceptionHandler(HttpServletRequest req, Exception e){
return exceptionHandler( req, e, "OTHER_EXCE");
public Layui exceptionHandler(HttpServletRequest req, Exception e, String error_code){
StackTraceElement[] stackTrace = e.getStackTrace();
String className = stackTrace[0].getClassName();
String methodName = stackTrace[0].getMethodName();
String fileName = stackTrace[0].getFileName();
Integer errorlineNumber = stackTrace[0].getLineNumber();
UserInfo userInfo=coreFactory.UserInstance().getCurrentUser();
ExceptionLog log=new ExceptionLog(userInfo,applicationName);
String url = req.getRequestURI();
String message = ErrorEnum.getError_msg(url,error_code);
String remoteAddress = IPUtil.getClientIp(req);
StringBuffer paramJson = new StringBuffer() ;
Enumeration<?> temp = req.getParameterNames();
if (null != temp) {
paramJson.append("{");
while (temp.hasMoreElements()) {
String en = (String) temp.nextElement();
String value = req.getParameter(en);
if (null == value || "".equals(value)) {
value = "";
paramJson.append("\"");
paramJson.append(en);
paramJson.append("\":");
paramJson.append("\"");
paramJson.append(value);
paramJson.append("\",");
//如果字段的值为空,判断若值为空,则删除这个字段>
paramJson.append("}");
log.setRequestInfo(remoteAddress,fileName,errorlineNumber)
.setExceptionInfo(url,methodName,paramJson.toString(),className)
.setResponseInfo(error_code,message);
logger.error(message,e);
sendPost(log);
return Layui.data(0,null,message);
* 向目的URL发送post请求
public String sendPost( ExceptionLog entity) {
HttpHeaders headers = new HttpHeaders();
headers.add("Content-Type", "application/json");
RestTemplate restTemplate = new RestTemplate();
restTemplate.getMessageConverters().add(0, new StringHttpMessageConverter(Charset.forName("UTF-8")));
ResponseEntity<String> result = restTemplate.postForEntity(SystemMonitorUrl, entity, String.class);
String body = result == null ? null : result.getBody();
return body;
2. 在子系统项目代码entity的package下创建异常信息枚举类: ErrorEnum。
* @author 作者: wangqingchen
* @date 时间: 2022/3/26 14:05
* @description 说明: 统一异常处理枚举类型
public enum ErrorEnum {
NULL_POINTER_EXCE("空指针异常","NULL_POINTER_EXCE"),
CLASS_CAST_EXCE("类型转换异常","CLASS_CAST_EXCE"),
FILE_NOT_FOUND_EXCE("文件未发现异常","FILE_NOT_FOUND_EXCE"),
SQL_EXCE("SQL异常","SQL_EXCE"),
IO_EXCE("IO异常","IO_EXCE"),
INDEX_OUTOF_BOUNDS_EXCE("数组下标越界异常","INDEX_OUTOF_BOUNDS_EXCE"),
RUNTIME_EXCE ("运行时异常","RUNTIME_EXCE"),
SERVER_400_EXCE ("参数绑定400异常","RUNTIME_EXCE"),
SERVER_500_EXCE ("服务端500异常","RUNTIME_EXCE"),
PAGE_TEMPLATE_PARSE_EXCE ("服务端500异常","PAGE_TEMPLATE_PARSE_EXCE"),
OTHER_EXCE("其他异常","OTHER_EXCE");
//异常枚举的名称
private String error_msg;
//异常枚举的编码
private String error_code;
private ErrorEnum(String error_msg, String error_code){
this.error_msg = error_msg;
this.error_code = error_code;
public String getError_msg() {
return error_msg;
public String getError_code() {
return error_code;
public static String getError_msg(String url,String error_code) {
if (error_code != null){
for(ErrorEnum e : ErrorEnum.values()){
if (e.getError_code().equals(error_code)){
return "请求["+url+"]发生"+ e.getError_msg();
return null;
public static String getError_code(String error_msg) {
if (StringUtils.isNotBlank(error_msg)){
for(ErrorEnum e : ErrorEnum.values()){
if (e.getError_msg().equals(error_msg)){
return e.getError_code();
return null;
public void setError_msg(String error_msg) {
this.error_msg = error_msg;
public void setError_code(String error_code) {
this.error_code = error_code;
3. 在子系统项目代码entity的package下创建异常信息封装类: ExceptionLog。
* @author 作者: wangqingchen
* @date 时间: 2022/3/26 15:35
* @description 说明: 统一异常处理异常信息实体类
@Data
public class ExceptionLog implements Serializable {
Integer id;
String clientIp;
String requestUrl;
String method;
String param;
Date createTime;
* exception报错类型
String message;
* exception报错code
String errorCode;
* 请求接口的controller类名
String className;
* 发生异常的java文件名
String fileName;
* 异常发生的所在行
int errorLineNumber;
* 当系统code
String appCode;
* 当前请求的登录人账号userUid
String operationalUid;
* 当前请求的登录人名称
String operationalName;
public ExceptionLog(){
public ExceptionLog(UserInfo userInfo, String appCode ){
this.appCode = appCode;
this.createTime = new Date();
this.operationalName = userInfo.getName();
this.operationalUid = userInfo.getUserUid();
public ExceptionLog setRequestInfo(String remoteAddress, String fileName, Integer errorLineNumber){
this.clientIp = remoteAddress;
this.fileName = fileName;
this.errorLineNumber = errorLineNumber;
return this;
public ExceptionLog setResponseInfo(String errorCode,String message){
this.errorCode = errorCode;
this.message = message;
return this;
public ExceptionLog setExceptionInfo( String reqUri, String method, String param ,String className){
this.requestUrl = reqUri;
this.method = method;
this.param = param;
this.className = className;
return this;
异常捕获告警系统代码实现
1. 创建一个新项目,controller的package下创建异常信息接收控制器: ExceController。
* @author 作者: wangqingchen
* @date 时间: 2022/3/26 15:56
* @description 说明: 统一异常处理异常信息实体类
@Controller
public class MonitorController {
@Resource
private ExceptionLogService exceptionLogService;
@RequestMapping("/publish/error")
@ResponseBody
public String publishError(@RequestBody ExceptionLog log){
String MD5 = getMd5(log.getMessage());
//把异常广播到websocket客户端
publishMessage(log);
log.setStatus(0);
log.setIsDelete(0);
log.setMd5(MD5);
//判断10分钟内有同一个异常,防止短信提醒爆炸
if (queryExistThisMd5_CreateTime_Is_ToDay(MD5)){
sendExpectionSMS(log);
//异常信息插入数据库
exceptionLogService.insertSelective(log);
return "publish---ok !" ;
}catch (Exception e) {
e.printStackTrace();
return "publish---fail !" ;
//向客户端广播消息
public static void publishMessage(ExceptionLog log) {
List<ExceptionLog> list = new ArrayList<>();
list.add(log);
for(WebSocketServer client : webSocketSet){
client.sendMessage(JSONObject.toJSON(list).toString());
continue;
private boolean queryExistThisMd5_CreateTime_Is_ToDay(String md5){
ExceptionLog log = new ExceptionLog(0,md5,true);
List<ExceptionLog> select = exceptionLogService.queryAll(log);
return select == null || (select != null && select.size() == 0);
@RequestMapping("v1/client")
public String client(Model model){
List<Dictionary> list = new RestTemplate().getForObject( "http://xxxx/xxx/dictionary/getChildNodesByCode?code=SystemMonitorAppcodes", List.class);
model.addAttribute("appList",list);
return "v1/client.html";
@RequestMapping("v1/queryAll")
@ResponseBody
public Layui queryAll( Integer limit, Integer page,ExceptionLog log){
if (log.getStatus() == null) log.setStatus(0);
PageHelper.startPage(page, limit);
List<ExceptionLog> select = exceptionLogService.queryAll(log);
PageInfo pageInfo = new PageInfo(select);
return Layui.data((int) pageInfo.getTotal(), pageInfo.getList(), "未处理异常查询完毕。");
@RequestMapping("v1/updateLog")
@ResponseBody
public Layui updateLog(ExceptionLog log){
int row = exceptionLogService.updateByPrimaryKeySelective(log);
return Layui.data(row, null, row >0 ? "异常处理完毕。" : "异常处理失败。");
* 当websocket接收到异常处理机制后会立刻发送异常报错短信提醒
* @param log 异常信息
public void sendExpectionSMS(ExceptionLog log){
String [] phonsStr = {"137xxxx91","1806xxxx296","139xxxx378","139xxxx745"};
SMSInfo smsInfo = new SMSInfo(null, null,null ,
"您好: 平台运维支撑系统监控到有新的系统异常报错。详情如下: " +
" 发生异常服务器ip地址 ( "+log.getClientIp()
+" ) - 报错信息 ("+log.getMessage()
+") - 请求参数 ("+log.getParam()+")。",
new Date());
smsInfo.setReceiverPhones(Arrays.asList(phonsStr));
sendPost(smsInfo);
* 向目的URL发送post请求
public JsonResultMessage sendPost(SMSInfo entity) {
HttpHeaders headers = new HttpHeaders();
headers.add("Content-Type", "application/json");
RestTemplate restTemplate = new RestTemplate();
restTemplate.getMessageConverters().add(0, new StringHttpMessageConverter(Charset.forName("UTF-8")));
ResponseEntity<JsonResultMessage> result =
restTemplate.postForEntity("http://xxxxxxx/ServerApi/api/restSendSMS_ByPhons", entity, JsonResultMessage.class);
JsonResultMessage body = result == null ? null : result.getBody();
return body;
/**根据异常message生成MD5用来熔断短信爆炸
* @param str
* @return
public String getMd5(String str) {
try {
MessageDigest md5 = MessageDigest.getInstance("MD5");
byte[] bs = md5.digest(str.getBytes());
StringBuilder sb = new StringBuilder(40);
for(byte x:bs) {
if((x & 0xff)>>4 == 0) {
sb.append("0").append(Integer.toHexString(x & 0xff));
} else {
sb.append(Integer.toHexString(x & 0xff));
return sb.toString();
} catch (NoSuchAlgorithmException e) {
e.printStackTrace();
return null;
2. 在项目config包下创建websocket配置类。
@Configuration
public class WebSocketServerConfig {
// @Bean
public ServerEndpointExporter serverEndpointExporter(){
return new ServerEndpointExporter();
3. 创建异常接收客户端: 此处就先用layui画个html页面暂时先解决有无的问题。
<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
<meta charset="UTF-8">
<title>Title</title>
<link rel="stylesheet" th:href="@{/webjars/layui/css/layui.css}" media="all">
<script type="text/javascript" th:src="@{/webjars/jquery/jquery.js}"></script>
<script type="text/javascript" th:src="@{/webjars/layui/layui.js}" charset="utf-8" th:inline="none"></script>
<script type="text/javascript" th:src="@{/js/common.js}" charset="utf-8" th:inline="none"></script>
<script type="text/javascript" th:src="@{/webjars/sockjs-client/sockjs.min.js}" charset="utf-8" th:inline="none"></script>
<script type="text/javascript" th:src="@{/webjars/stomp-websocket/stomp.min.js}" charset="utf-8" th:inline="none"></script>
</head>
<blockquote class="layui-elem-quote layui-text">
<a><legend>系统运维监控广播页面</legend></a>
</blockquote>
<fieldset class="layui-elem-field layui-field-title" style="margin-top: 20px;">
<legend>平台异常捕获上报内容</legend>
</fieldset>
<form class="layui-form" action="">
<div class="layui-form-item" style="margin-top: 1rem;">
<label class="layui-form-label">所属系统: </label>
<div class="layui-input-inline">
<select name="appCode" >
<option value="">全部</option>
<option th:each="item:${appList}" th:value="${item.code}" th:text=" ${item.name}"></option>
</select>
<label class="layui-form-label">异常信息: </label>
<div class="layui-input-inline" style="width: 20rem;">
<input type="text" name="message" class="layui-input">
<label class="layui-form-label">报错方法: </label>
<div class="layui-input-inline">
<input type="text" name="method" class="layui-input">
<div class="layui-form-item" style="margin-top: 1rem;">
<label class="layui-form-label">异常状态: </label>
<div class="layui-input-inline">
<select name="status" >
<option value="">全部</option>
<option value="0" th:text="未处理" selected></option>
<option value="1" th:text="已接受"></option>
<option value="2" th:text="已处理"></option>
</select>
<label class="layui-form-label">捕获时间: </label>
<div class="layui-input-inline" style="width: 20rem;">
<input type="text" class="layui-input" name="superviseTime" id="test6" placeholder=" ~ ">
<hr class="layui-bg-gray">
<div class="layui-input-block" style="text-align: center;">
<a class="layui-btn" type="button" lay-submit lay-filter="search">查询</a>
<a class="layui-btn" lay-submit id="reset" lay-filter="reset">重置</a>
</form>
<table class="layui-table" id="table" lay-filter="listTable">
<thead class="layui-table-header">
<th lay-data="{type: 'checkbox', fixed: 'left'}">序号</th>
<th lay-data="{type: 'numbers',}">序号</th>
<th lay-data="{field:'clientIp'}">IP地址</th>
<th lay-data="{field:'message'}">异常信息</th>
<th lay-data="{field:'param'}">请求参数</th>
<th lay-data="{field:'createTime', sort:true}">捕获时间</th>
<th lay-data="{field:'className'}">报错类名</th>
<th lay-data="{field:'method'}">报错方法</th>
<th lay-data="{field:'errorLineNumber'}">异常发生位置</th>
<th lay-data="{toolbar: '#barDemo'}">操作</th>
</thead>
<tbody id="tableBody" class="layui-table-body"></tbody>
</table>
<script type="text/html" id="toolbarDemo">
<div class="layui-btn-container">
<button class="layui-btn layui-btn-sm" lay-event="getCheckData">获取选中行数据</button>
<button class="layui-btn layui-btn-sm" lay-event="getCheckLength">获取选中数目</button>
<button class="layui-btn layui-btn-sm" lay-event="isAll">验证是否全选</button>
</script>
<script type="text/html" id="barDemo">
<a class="layui-btn layui-btn-xs" lay-event="jieshou">接受</a>
<a class="layui-btn layui-btn-xs" lay-event="chuli">已处理</a>
<a class="layui-btn layui-btn-danger layui-btn-xs" lay-event="del">删除</a>
</script>
<script>
var index = 1
var socket ;
var table ;
var layer ;
layui.use(['table','form','layer'], function () {
table = layui.table;
var form = layui.form;
layer = layui.layer;
table.init('listTable', {
elem: '#table',
title: '运维监控异常捕获数据表',
url: '/SystemMonitor/v1/queryAll',
toolbar: true, //开启头部工具栏,并为其绑定左侧模板
page: {
layout: ['limit', 'count', 'prev', 'page', 'next', 'skip']
, groups: 1 , first: false , last: false
limits: [10,20,30,40,50,60,80,100,150,200,250,300]
table.on('tool(listTable)', function(obj){
var data = obj.data;
console.log("data =", data)
if(obj.event === 'del'){
layer.confirm('真的删除行么', function(index){
obj.del();
layer.close(index);
ajax("/SystemMonitor/v1/updateLog?id="+data.id+"&isDelete=1",'GET',null,"json",null,null)
} else if(obj.event === 'jieshou'){
layer.confirm('接受的异常请及时分配到对应的开发人员处理。', function(index){
obj.del();
ajax("/SystemMonitor/v1/updateLog?id="+data.id+"&status=1",'GET',null,"json",null,null)
layer.close(index);
}else if(obj.event === 'chuli'){
layer.confirm('确定本异常已经由开发人员处理掉了?', function(index){
obj.del();
ajax("/SystemMonitor/v1/updateLog?id="+data.id+"&status=2",'GET',null,"json",null,null)
layer.close(index);
form.on('submit(demo)', function (data) {
table_reload('table',"/SystemMonitor/v1/queryAll",data.field);
if(typeof (WebSocket) == 'undefined'){
layer.msg('您的浏览器不支持WebSocket');
}else {
socket = new WebSocket("ws://ip:port/SystemMonitor/webserver")
socket.open = function (){
layer.msg("socket 已经建立连接。")
socket.onmessage = function (msg){
layer.msg("异常监控新捕获了" + JSON.parse(msg.data).length +"个异常。")
showContent(msg.data)
// playSound()
function showContent(msg){
var list = JSON.parse(msg)
var tableBody = $("#tableBody")
var html = ''
for(var i = 0; i < list.length; i++){
var obj = list[i];
html += '<tr>' +
'<td style="width: 4rem">'+(index++)+'</td>' +
'<td>'+obj.clientIp+'</td>' +
'<td>'+obj.message+'</td>' +
'<td>'+obj.param+'</td>' +
'<td>'+new Date(obj.createTime).toLocaleDateString()+'</td>' +
'<td>'+obj.className+'</td>' +
'<td>'+obj.method+'</td>' +
'<td>'+obj.errorLineNumber+'行</td></tr>'
tableBody.append(html)
table.reload("table");
function playSound() {
var borswer = window.navigator.userAgent.toLowerCase();
if(borswer.indexOf('ie') >= 0) {
var strEmbed = '<embed name="embedPlay" th:src="@{/music/1.mp3}" autostart="true" hidden="true" loop="false" />';
if($('body').find('embed').length <= 0) {
$('body').append(strEmbed);
var embed = document.embedPlay;
//浏览器不支持audio,则使用embed播放
embed.volume = 100;
} else {
var audio = $('#audioPlay');
audio.play();