一、序言
身为一个表面上是后端,实际上是全栈的开发工程师,一直对运维方面有些本能的抗拒,缺乏合理有效的运维监控手段和方法,总是等着甲方反馈才去排查问题,遇到不好复现的问题找半天日志也找不到位置,与甲方沟通确认问题也是件很烦心的事情,于是就想是否存在"自动化运维"这种东西,结果一百度,好嘛,都是些完全看不懂的名词,什么系统预备Kickstart、Cobbler、OpenQRM、Spacewalk,什么配置管理Puppet、ControlTier、Func、Chef,什么监控报警Nagios、Cacti、Zabbix、OpenNMS,这都是什么玩意儿?
这次我也要像一个甲方大人一样,边摆手边皱眉地大声说:“这不是我要的东西,我要的不是这个。”
当然咱也不能真的把双手往胸口一揣,往椅背上一靠等方案自己跳出来,还得自己老实儿实儿的想点办法出来,多方打听之后了解到有一种方案是:系统上线之后如果发生了什么未定义的异常和错误,就会直接发送邮件给运维团队,提醒开发者记录并处理该异常。听起来不错,那就先奔着这个方案走。
二、需求
1.当系统发生异常后,自动将警告信息以邮件形式发送给代码的维护者
三、实现思路
从需求上不难看出要实现该功能有三个点需要关注
1.全局异常处理,这里使用@ExceptionHandler注解最适合不过
2.邮件发送,SpringBoot集成了Mail的自动配置,只需引入spring-boot-starter-mail依赖,再稍微做些配置即可
3.方法绑定代码维护者,一旦有异常需要马上定位到当前方法的开发者(维护者),所以使用自定义注解是个不错的选择
整理后流程图如下所示:
技术方案也确定下来,接下来就是编码了
四、代码
1.开发者及邮箱信息维护
因为这部分信息基本上是配置好后就不会再修改的,所以这里笔者采用了yml文件静态存储的方式
(1).在application.yml文件中定义好开发者及其邮箱信息
developer:
developerLists:
- name: 张三
email: 337xxxx786@qq.com
- name: 李四
email: 135xxxx294@qq.com
(2).创建DeveloperList对象读取yml文件中的数据,并提供一个通过开发者姓名获取邮箱的方法
@Component
@ConfigurationProperties(prefix = "developer")
@Data
public class DeveloperList {
/**
* 开发者列表
*/
private List<Developer> developerLists;
/**
* 根据开发者姓名获取yml文件中的邮箱
*
* @param developers 开发者列表
* @param developerName 开发者姓名
* @return 开发者邮箱
*/
public static String getEmail(DeveloperList developers, String developerName) {
List<Developer> developerVoList = developers.getDeveloperLists();
Map<String, String> developerMap = developerVoList.stream().collect(Collectors.toMap(Developer::getName, Developer::getEmail));
return developerMap.get(developerName);
}
}
2.引入SpringBoot邮箱发送功能
(1).POM文件
<!--邮件发送-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-mail</artifactId>
</dependency>
(2).发件人邮箱开启POP3/SMTP服务
登陆QQ邮箱,前往设置 -> 账户 -> 拉到"账户安全" -> 选择开启POP3/SMTP服务;
点击开启后应该是需要发送手机验证码和一些其他操作,第3步中的spring.mail.password参数好像也是在这些步骤中得到的随机值,需要复制保存,但笔者这部分配置是早就做好的,就不详细展示了,读者可暂时移步其他其他相关博客查看并配置;
(3).yml配置发送异常信息的邮箱
spring:
mail:
host: smtp.qq.com
username: 972xxxx48@qq.com
# QQ邮箱POP3/SMTP客户端服务授权码
password: zwlxxxxxybabbaa
default-encoding: UTF-8
properties:
mail:
smtp:
auth: true
starttls:
enable: true
requeired: true
(4).编辑邮件发送帮助类,Springboot支持发送的邮件有多种类型,如简单文本、带图片、带附件、带多个附件等,这里因为异常信息打印只有文本信息,所以笔者只放了简单文本邮件的发送方法;
/**
* @author Guo
* className MailUtil
* description 邮件发送的实现类
* date 2020/7/11 11:28
**/
@Service
@Slf4j
public class MailUtil {
@Resource
private JavaMailSender sender;
@Value("${spring.mail.username:''}")
private String from;
/**
* 发送纯文本的简单邮件
*
* @param to 收件人邮箱
* @param subject 邮件标题
* @param content 邮件主体内容
*/
public void sendSimpleMail(String to, String subject, String content) {
SimpleMailMessage message = new SimpleMailMessage();
message.setFrom(from);
// 默认抄送给主邮箱
message.setTo(new String[]{to, from});
message.setSubject(subject);
message.setText(content);
try {
sender.send(message);
log.info("简单邮件已经发送。");
} catch (Exception e) {
log.error("发送简单邮件时发生异常!", e);
}
}
}
3.自定义注解及自定义拦截器
(1).编写自定义注解DeveloperEmail,仅需维护开发者姓名,当然如果还有其他需求的话可以与DeveloperList.java和application-developer.yml一同修改维护
/**
* @author 郭超
* Date:2020-07-11 11:18
* Description: 自定义注解,异常信息发送邮件给相关开发者
*/
@Target({ElementType.METHOD, ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface DeveloperEmail {
String name() default "";
}
(2).将注解标记在Controller类上
@Slf4j
@Controller
@RequestMapping("/class")
@DeveloperEmail(name = "郭超")
public class ClassCrud {
}
(3).编写自定义AOP增强处理类,以便于通过注解获取方法开发者(维护者)的邮箱信息。这里笔者一开始采用的是全局拦截器,但这样一来就需要给所有类都添加@DeveloperEmail注解,工作量会多出很多,并且拦截器还需要在WebMvcConfig类中进行配置,这就需要邮件发送功能本身是一个Web应用,而非可以直接被其他RPC模块依赖的jar包,这就很难受,简单对两种方案做个对比:
方案①:对Controller添加AOP
优点:
A.只需要在Controller类上添加@DeveloperEmail注解
B.不需要主启动类,可直接作为DLC被其他业务模块引用
方案②:使用全局拦截器
缺点:
A.需要在所有可能抛出异常的类上添加@DeveloperEmail注解
B.需要WebMvcConfig配置类和主启动类,自身需要作为一个Web应用启动,或者直接配置在业务模块中,如果业务模块多的话就会大量冗余且不便于维护
结论:虽然在一定程度上有失偏颇,但在当前需求下,方案①更能适应多种场景
/**java
* @author 郭超
* Date:2020-11-17 15:58
* Description:
*/
@Slf4j
@Aspect
@Component
public class GetDeveloperName {
public static String DEVELOPER = "developerName";
public static String EMAIL = "email";
@Resource
private RedisUtil redisUtil;
@Resource
private Developer developer;
/**
* 在所有controller方法执行时添加增强功能
*/
@Pointcut("execution(public * com.qianmeng.computerroom.controller..*.*(..))")
public void getDeveloperNamePointCut() {
}
@Around(value = "getDeveloperNamePointCut()")
public Object insertDataProcessing(ProceedingJoinPoint joinPoint) throws Throwable {
log.info("AOP——Controller异常处理!来自Extends模块");
Object[] objs = joinPoint.getArgs();
// 通过方法所在的类的注解获取开发者姓名
DeveloperEmail obj = ((MethodSignature) joinPoint.getSignature()).getMethod().getDeclaringClass().getAnnotation(DeveloperEmail.class);
log.info(" =====DeveloperEmail obj==== = " + obj);
if (obj != null) {
String developerName = obj.name();
String existDeveloperName = (String) redisUtil.get(DEVELOPER);
String existEmail = (String) redisUtil.get(EMAIL);
log.info("developerName = " + developerName + " / existDeveloperName = " + existDeveloperName + " / existEmail = " + existEmail);
if (existDeveloperName != null && existDeveloperName.equals(developerName) && existEmail != null) {
// 如果Redis中开发者没变,则沿用
redisUtil.set(EMAIL, existEmail);
return joinPoint.proceed(objs);
} else {
// 否则根据开发者姓名获取邮箱
String email = GetEmailHelper.getEmail(developerName, developer);
log.info("email = " + email);
if (email != null && !"".equals(email.trim())) {
redisUtil.set(DEVELOPER, developerName);
redisUtil.set(EMAIL, email);
} else {
log.error("未能找到开发者" + developerName + "的邮箱信息");
}
}
} else {
log.error("未能通过注解获取到开发者邮箱!");
}
// 返回数据
return joinPoint.proceed(objs);
}
}
4.配置全局异常处理
(1).添加全局异常处理类,当发生异常时调用邮件发送方法
/**
* @author Guo
* className GlobalExceptionHandler
* description 全局异常处理类
* date 2020/7/11 11:16
**/
@RestControllerAdvice
@Slf4j
public class GlobalExceptionHandler {
@Resource
private HttpServletRequest request;
@Resource
private MailUtil mailUtil;
/**
* 全局异常捕捉处理,判断错误是否是已定义的已知错误,不是则由未知错误代替,同时记录在log中
*
* @param e 异常对象
* @return 报错信息
*/
@ExceptionHandler(value = Exception.class)
public ResponseResult exceptionGet(Exception e) {
// 从request中获取异常发生代码的维护者
String email = (String) request.getAttribute(EmailHandlerInterceptor.EMAIL);
// 发送邮件
if (StringUtils.isNotBlank(email)) {
// 将异常信息存到邮箱主体内容中
StringWriter sw = new StringWriter();
PrintWriter pw = new PrintWriter(sw);
e.printStackTrace(pw);
mailUtil.sendSimpleMail(email, "系统异常", sw.toString());
}
log.error("全局异常捕获", e);
return ResponseResult.fail(e.getMessage());
}
/**
* 应用到所有@RequestMapping注解方法,在其执行之前初始化数据绑定器
*
* @param binder
*/
@InitBinder
public void initBinder(WebDataBinder binder) {
}
}