自动(被动)运维——Bug警告消息抄送到个人

1,109 阅读7分钟

一、序言

身为一个表面上是后端,实际上是全栈的开发工程师,一直对运维方面有些本能的抗拒,缺乏合理有效的运维监控手段和方法,总是等着甲方反馈才去排查问题,遇到不好复现的问题找半天日志也找不到位置,与甲方沟通确认问题也是件很烦心的事情,于是就想是否存在"自动化运维"这种东西,结果一百度,好嘛,都是些完全看不懂的名词,什么系统预备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) {
    
    }
}

5.测试中发现的问题

当发生异常触发发送邮件方法时,控制台提示访问25端口超时,百度后说邮件服务提供商为了屏蔽垃圾邮件关闭了25端口,而POP3/STMP协议使用的正是25端口,笔者目前没有找到解决方案,等有空解决了再来完善本博客,如果读者有解决方案或者对本篇博客有什么意见或建议的话,也欢迎在评论区补充~