因为不知aop能干嘛,因此用aop做个小功能,再结合最近学的springboot-Email做了个系统异常自动邮件通知的功能,
感觉满满的成就感。
AOP不懂的可以看上一篇:https://www.cnblogs.com/zgq7/p/11310142.html
spring-email不懂的看上一篇:https://www.cnblogs.com/zgq7/p/11314895.html
先看看这个功能的总体规划图:
因此需要思考的是:
1:如何捕获异常?
总不能在每个会发生异常的地方写 throw 或者 try-catch 语句吧?因此利用AOP进行统一捕获并进行下一步处理。
2:如何将异常信息发送到开发者(用户)邮箱?
这就需要用到javaMail技术了,而我在maven仓库搜寻时看到了springboot-Email,因此去自发了解了这个开发流程。
本来这里打算使用原生的JavaMail的,但是springboot集成了就没用了。
原生的可参考这里:https://www.cnblogs.com/LUA123/p/5575134.html
下面开始我的设计思路的实现流程。
1:添加相关springboot-mail以及springboot-aop 相关依赖
<!-- https://mvnrepository.com/artifact/org.springframework.boot/spring-boot-starter-aop --> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-aop</artifactId> <version>2.1.6.RELEASE</version> </dependency> <!-- https://mvnrepository.com/artifact/org.springframework.boot/spring-boot-starter-mail --> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-mail</artifactId> <version>2.1.6.RELEASE</version> </dependency>
2:构造一个本地线程池,以防高并发。
package com.dev.config; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.beans.factory.DisposableBean; import org.springframework.beans.factory.InitializingBean; import java.lang.reflect.Method; import java.util.concurrent.*; /** * Created on 2019-08-05 14:00. * * @author zgq7 */ public class LocalThreadPool implements InitializingBean, DisposableBean { public static final String PACKAGE_BEAN_NAME = "localThreadPool"; private static final Logger logger = LoggerFactory.getLogger(LocalThreadPool.class); /** * capacity 队列容量 **/ private final BlockingQueue<Runnable> workQueue = new ArrayBlockingQueue<>(5000); private final RejectedExecutionHandler handler = new ThreadPoolExecutor.CallerRunsPolicy(); private final ThreadFactory defaultThreadFactory = Executors.defaultThreadFactory(); private final ThreadFactory privilegedThreadFactory = Executors.privilegedThreadFactory(); /** * corePoolSize 核心线程数量 * maximumPoolSize 线程池允许最大线程池数量 * keepAliveTime 当线程数超过本地线程核心数同时又小于设置的最大线程数,需要重新创建一个新线程时需要等待的时间 * TimeUnit.MILLISECONDS 时间单位,这里我设置的是豪秒 * workQueue 一个队列:当一个task被执行前进行使用 * threadFactory 用于创建新线程的线程工厂 * handler 一个处理器:当线程被锁、或者队列的容量达到上限时 被调用 **/ public final ThreadPoolExecutor threadPoolExecutor = new ThreadPoolExecutor(5, 10, 3000, TimeUnit.MILLISECONDS , workQueue, defaultThreadFactory, handler); /** * 本地线程池被销毁后的回调方法 **/ @Override public void destroy() throws Exception { logger.info("指令->[本地线程池已销毁]"); } /** * 本地线程池成功初始化的回调犯法 **/ @Override public void afterPropertiesSet() throws Exception { logger.info("指令->[本地线程池已成功初始化]"); } }
3:邮件工具类请参考我上一篇博客:https://www.cnblogs.com/zgq7/p/11314895.html
4:编写一个切面类,用于全局捕捉程序产生的异常
package com.dev.config.aop; import com.dev.config.LocalThreadPool; import com.dev.model.email.EmailModel; import com.dev.utils.email.MailSendUtils; import com.dev.utils.exception.ExceptionCodes; import com.dev.utils.exception.ServiceException; import org.aspectj.lang.JoinPoint; import org.aspectj.lang.annotation.*; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.core.annotation.Order; import java.util.Arrays; /** * Created on 2019-07-31 9:41. * * @author zgq7 */ @Aspect @Order(2) public class RuntimeExceptionAspectJ { @Autowired private MailSendUtils mailSendUtils; @Autowired private LocalThreadPool localThreadPool; private final Logger log = LoggerFactory.getLogger(this.getClass()); //@Pointcut("execution(public * com.dev..*(..))") @Pointcut("execution(public * com.dev.controller.TestController.*(..))") private void runtimeExceptionAspect() { } /** * 切面报错 **/ @AfterThrowing(value = "runtimeExceptionAspect()", throwing = "exception") public void afterThrowing(Throwable exception) { Class klass = exception.getClass(); log.error("occured a [{}] , msg : [{}]", klass.getSimpleName(), ExceptionCodes.getMsgByKlass(klass)); EmailModel emailModel = new EmailModel(); emailModel.setEmailTheme("测试"); emailModel.setRecieverName("测试"); emailModel.setEmailContent(exception.toString() + ": " + Arrays.toString(exception.getStackTrace())); emailModel.setRecieverEmailAddress("xxx@qq.com"); localThreadPool.threadPoolExecutor.getThreadFactory().newThread(() -> mailSendUtils.sendEmailAsSysExceptionHtml(emailModel)).start(); throw new ServiceException(ExceptionCodes.getCodeByKlass(klass), ExceptionCodes.getMsgByKlass(klass)); } }
4.1:为了更方便的区分异常类型,我在程序内部设计了这个枚举(各位看官看看就好)
package com.dev.utils.exception; import java.io.FileNotFoundException; import java.io.IOException; import java.net.ProtocolException; import java.sql.SQLException; /** * Created on 2019-07-31 16:01. * * @author zgq7 * @apiNote 自定义的code - XxxException.class */ public enum ExceptionCodes { //RuntimeException 类型异常 RUNTIME_EXCEPTION(100, "运行时异常", RuntimeException.class), NULL_POINT_EXCEPTION(101, "空指针异常", NullPointerException.class), ARITHMETIC_EXCEPTION(102, "数学错误,被0除", ArithmeticException.class), INDEX_OUT_OF_BOUNDS_EXCEPTION(103, "当某对象的索引超出范围时抛出异常", IndexOutOfBoundsException.class), ARRAY_INDEX_OUT_OF_EXCEPTION(103001, "数组下标越界", ArrayIndexOutOfBoundsException.class), CLASS_CAST_EXCEPTION(104, "强制转换异常", ClassCastException.class), ILLEGAL_ARGUMENT_EXCEPTION(105, "非法转换", IllegalArgumentException.class), NUMBER_FORMAT_EXCEPTION(105001, "字符串转换为数字异常类", NumberFormatException.class), PROTOCOL_EXCEPTION(106, "网络协议有错误", ProtocolException.class), //IOException 类型异常 IO_EXCEPTION(200, "IO 流异常", IOException.class), FILE_NOT_FOUND_EXCEPTION(201, "文件找不到", FileNotFoundException.class), //数据库 sql 操作异常 SQL_EXCEPTION(300, "操作数据库异常", SQLException.class), //其他相关异常 REFLECTIVE_OPERATION_EXCEPTION(400, "", ReflectiveOperationException.class), ILLEGAL_ACESS_EXCEPTION(401, "访问某类被拒绝时抛出的异常", IllegalAccessException.class); private int code; private String msg; private Class<Exception> klass; ExceptionCodes(int code, String msg, Class klass) { this.code = code; this.msg = msg; this.klass = klass; } public int getCode() { return this.code; } public String getMsg() { return this.msg; } public Class<Exception> getKlass() { return this.klass; } /** * 通过异常码获取对应异常类 **/ public static Class<Exception> getKlassByCode(int code) { for (ExceptionCodes exceptionCodes : ExceptionCodes.values()) { if (exceptionCodes.getCode() == code) return exceptionCodes.getKlass(); } return null; } /** * 根据异常码获取对应异常信息 **/ public static String getMsgByCode(int code) { for (ExceptionCodes exceptionCodes : ExceptionCodes.values()) { if (exceptionCodes.getCode() == code) return exceptionCodes.getMsg(); } return null; } /** * 根据异常类获取异常码 **/ public static int getCodeByKlass(Class<? extends Exception> klass) { for (ExceptionCodes exceptionCodes : ExceptionCodes.values()) { if (exceptionCodes.getKlass() == klass) return exceptionCodes.getCode(); } return 3000; } /** * 根据异常类获取异常信息 **/ public static String getMsgByKlass(Class<? extends Exception> klass) { for (ExceptionCodes exceptionCodes : ExceptionCodes.values()) { if (exceptionCodes.getKlass() == klass) return exceptionCodes.getMsg(); } return null; } }
5:注册相关bean
package com.dev.config; import com.dev.config.aop.BaseAop; import com.dev.config.aop.RuntimeExceptionAspectJ; import com.dev.filter.BaseFilter; import com.dev.utils.email.MailSendUtils; import org.springframework.boot.autoconfigure.AutoConfigureAfter; import org.springframework.boot.web.servlet.FilterRegistrationBean; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.context.annotation.EnableAspectJAutoProxy; import org.springframework.core.annotation.Order; import org.springframework.mail.javamail.JavaMailSender; import org.springframework.mail.javamail.JavaMailSenderImpl; import java.util.*; /** * Created by zgq7 on 2019/6/6. * 注册一些bean进入ioc * * @EnableAspectJAutoProxy 开启aop代理 */ @Configuration @EnableAspectJAutoProxy public class BeanRegistryCenterConfig {/** * 邮箱工具类 bean 注册 **/ @Bean public MailSendUtils mailSendUtils() { return new MailSendUtils(); } /** * 本地线程 bean 注册 **/ @Bean(name = LocalThreadPool.PACKAGE_BEAN_NAME) public LocalThreadPool localThreadPool() { return new LocalThreadPool(); }/** * 异常捕获类 RuntimeExceptionAspectJ bean 注册 **/ @Bean public RuntimeExceptionAspectJ runtimeExceptionAspectJ() { return new RuntimeExceptionAspectJ(); } }
6:controller中制造一个runtimeException类型的异常,如下
package com.dev.controller; import com.dev.controller.bases.BaseController; import com.dev.service.AopiService; import com.dev.utils.exception.ServiceException; import com.google.common.collect.ImmutableMap; import okhttp3.*; import org.springframework.web.bind.annotation.*; import javax.annotation.Resource; import java.io.*; import java.util.Collections; import java.util.Map; /** * Created by zgq7 on 2019/6/6. */ @RestController @RequestMapping(value = "/dev") public class TestController extends BaseController { @Resource(name = AopiService.PACKAGE_BEAN_NAME) private AopiService aopiService; @GetMapping(value = "") public Map<Object, Object> get() { if (1 == 1) throw new NullPointerException(); return ImmutableMap.of("code", aopiService.getAopList()); } }
7:运行项目进行测试
7.1:先看启动日志
项目成功启动,本地线程池也初始化成功!!!
7.2:使用postMan进行测试
7.3:邮箱接收到的邮件
总结:做到这一步,程序在遇到异常时,便可主动给开发者发报错信息了,若线上出了什么小异常,可以直接查看。
如有不理解的地方请下方留言,喜欢的点个赞。
本文项目地址:https://github.com/zgq7/devloper-mine