Spring全家桶系列–SpringBoot之AOP详解

//本文作者:cuifuan //本文将收录到菜单栏:《Spring全家桶》专栏中 面向方面编程(AOP)通过提供另一种思考程序结构的方式来补充面向对象编程(OOP)。 OOP中模块化的关键单元是类,而在AOP中,模块化单元是方面。 准备工作 首先,使用AOP要在build.gradle中加入依赖 复制代码 //引入AOP依赖 compile "org.springframework.boot:spring-boot-starter-aop:${springBootVersion}" 复制代码 然后在application.yml中加入 复制代码 spring: aop: proxy-target-class: true 复制代码 1.@Pointcut 切入点 定义一个切点。 例如我们要在一个方法加上切入点,根据方法的返回的对象,方法名,修饰词来写成一个表达式或者是具体的名字 我们现在来定义一个切点 复制代码 package com.example.aop; import org.aspectj.lang.annotation.Aspect; import org.aspectj.lang.annotation.Pointcut; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.stereotype.Component; /** * 类定义为切面类 */ @Aspect @Component public class AopTestController { private static final Logger logger = LoggerFactory.getLogger(AopTestController.class); /** * 定义一个切点 */ @Pointcut(value = "execution(public String test (..))") public void cutOffPoint() { } } 复制代码 这里的切点定义的方法是 复制代码 @GetMapping("hello") public String test(){ logger.info("欢迎关注Java知音"); return "i love java"; } 复制代码 如果你想写个切入点在所有返回对象为Area的方法,如下 @Pointcut("execution(public com.example.entity.Area (..))") 等很多写法,也可以直接作用在某些包下 注意:private修饰的无法拦截 2.@Before前置通知 在切入点开始处切入内容 在之前的AopTestController类中加入对test方法的前置通知 复制代码 @Before("cutOffPoint()") public void beforeTest(){ logger.info("我在test方法之前执行"); } 复制代码 这里@Before里的值就是切入点所注解的方法名 Spring全家桶系列--SpringBoot之AOP详解 在方法左侧出现的图标跟过去以后就是所要通知的方法 这里就是配置正确了,我们来浏览器调用一下方法 Spring全家桶系列--SpringBoot之AOP详解 联想一下,这样的效果可以用在哪里,想像如果要扩展一些代码,在不需要动源代码的基础之上就可以进行拓展,美滋滋 3.@After 后置通知 和前置通知相反,在切入点之后执行 复制代码 @After("cutOffPoint()") public void doAfter(){ logger.info("我是在test之后执行的"); } 复制代码 控制台执行结果 Spring全家桶系列--SpringBoot之AOP详解 这里定义一个通知需要重启启动类,而修改通知方法的内容是可以热部署的 4.@Around环绕通知 和前两个写法不同,实现的效果包含了前置和后置通知。 当使用环绕通知时,proceed方法必须调用,否则拦截到的方法就不会再执行了 环绕通知=前置+目标方法执行+后置通知,proceed方法就是用于启动目标方法执行的 复制代码 ThreadLocal startTime = new ThreadLocal<>(); @Around("cutOffPoint()") public Object doAround(ProceedingJoinPoint pjp){ startTime.set(System.currentTimeMillis()); logger.info("我是环绕通知执行"); Object obj; try{ obj = pjp.proceed(); logger.info("执行返回值 : " + obj); logger.info(pjp.getSignature().getName()+"方法执行耗时: " + (System.currentTimeMillis() - startTime.get())); } catch (Throwable throwable) { obj=throwable.toString(); } return obj; } 复制代码 执行结果: Spring全家桶系列--SpringBoot之AOP详解 1.环绕通知可以项目做全局异常处理 2.日志记录 3.用来做数据全局缓存 4.全局的事物处理 等 5.@AfterReturning 切入点返回结果之后执行,也就是都前置后置环绕都执行完了,这个就执行了 复制代码 /** * 执行完请求可以做的 * @param result * @throws Throwable */ @AfterReturning(returning = "result", pointcut = "cutOffPoint()") public void doAfterReturning(Object result) throws Throwable { logger.info("大家好,我是@AfterReturning,他们都秀完了,该我上场了"); } 复制代码 执行结果 Spring全家桶系列--SpringBoot之AOP详解 应用场景可以用来在订单支付完成之后就行二次的结果验证,重要参数的二次校验,防止在方法执行中的时候参数被修改等等 6.@AfterThrowing 这个是在切入执行报错的时候执行 复制代码 // 声明错误e时指定的抛错类型法必会抛出指定类型的异常 // 此处将e的类型声明为Throwable,对抛出的异常不加限制 @AfterThrowing(throwing = "e",pointcut = "cutOffPoint()") public void doAfterReturning(Throwable e) { logger.info("大家好,我是@AfterThrowing,他们犯的错误,我来背锅"); logger.info("错误信息"+e.getMessage()); } 复制代码 在其他切入内容中随意整个错误出来,制造一个环境。 下面是@AfterThrowing的执行结果 Spring全家桶系列--SpringBoot之AOP详解 7.AOP用在全局异常处理 定义切入点拦截ResultBean或者PageResultBean 复制代码 @Pointcut(value = "execution(public com.example.beans.PageResultBean *(..)))") public void handlerPageResultBeanMethod() { } @Pointcut(value = "execution(public com.example.beans.ResultBean *(..)))") public void handlerResultBeanMethod() { } 复制代码 下面是AopController.java 复制代码 package com.example.aop; import com.example.beans.PageResultBean; import com.example.beans.ResultBean; import com.example.entity.UnloginException; import com.example.exception.CheckException; import org.aspectj.lang.ProceedingJoinPoint; import org.aspectj.lang.annotation.Around; import org.aspectj.lang.annotation.Aspect; import org.aspectj.lang.annotation.Pointcut; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.stereotype.Component; /** * 使用@Aspect注解将此类定义为切面类 * 根据晓风轻著的ControllerAOP所修改 * 晓风轻大佬(很大的佬哥了):https://xwjie.github.io/ */ @Aspect @Component public class AopController { private static final Logger logger = LoggerFactory.getLogger(AopController.class); ThreadLocal resultBeanThreadLocal = new ThreadLocal<>(); ThreadLocal> pageResultBeanThreadLocal = new ThreadLocal<>(); ThreadLocal start = new ThreadLocal<>(); /** * 定义一个切点 */ @Pointcut(value = "execution(public com.example.beans.PageResultBean *(..)))") public void handlerPageResultBeanMethod() { } @Pointcut(value = "execution(public com.example.beans.ResultBean *(..)))") public void handlerResultBeanMethod() { } @Around("handlerPageResultBeanMethod()") public Object handlerPageResultBeanMethod(ProceedingJoinPoint pjp) { start.set(System.currentTimeMillis()); try { pageResultBeanThreadLocal.set((PageResultBean)pjp.proceed()); logger.info(pjp.getSignature() + " 方法执行耗时:" + (System.currentTimeMillis() - start.get())); } catch (Throwable e) { ResultBean resultBean = handlerException(pjp , e); pageResultBeanThreadLocal.set(new PageResultBean<>().setMsg(resultBean.getMsg()).setCode(resultBean.getCode())); } return pageResultBeanThreadLocal.get(); } @Around("handlerResultBeanMethod()") public Object handlerResultBeanMethod(ProceedingJoinPoint pjp) { start.set(System.currentTimeMillis()); try { resultBeanThreadLocal.set((ResultBean)pjp.proceed()); logger.info(pjp.getSignature() + " 方法执行耗时:" + (System.currentTimeMillis() - start.get())); } catch (Throwable e) { resultBeanThreadLocal.set(handlerException(pjp , e)); } return resultBeanThreadLocal.get(); } /** * 封装异常信息,注意区分已知异常(自己抛出的)和未知异常 */ private ResultBean handlerException(ProceedingJoinPoint pjp, Throwable e) { ResultBean result = new PageResultBean(); logger.error(pjp.getSignature() + " error ", e); // 已知异常 if (e instanceof CheckException) { result.setMsg(e.getLocalizedMessage()); result.setCode(ResultBean.FAIL); } else if (e instanceof UnloginException) { result.setMsg("Unlogin"); result.setCode(ResultBean.NO_LOGIN); } else { result.setMsg(e.toString()); result.setCode(ResultBean.FAIL); } return result; } } 复制代码 用上面的环绕通知可以对所有返回ResultBean或者PageResultBean的方法进行切入,这样子就不用在业务层去捕捉错误了,只需要去打印自己的info日志。 看下面一段代码 复制代码 @Transactional @Override public int insertSelective(Area record) { record.setAddress("test"); record.setPostalcode(88888); record.setType(3); int i=0; try { i = areaMapper.insertSelective(record); }catch (Exception e){ logger.error("AreaServiceImpl insertSelective error:"+e.getMessage()); } return i; } 复制代码 假如上面的插入操作失败出错了? 你认为会回滚吗? 答案是:不会。 为什么? 因为你把错误捕捉了,事物没检测到异常就不会回滚。 那么怎么才能回滚呢? 在catch里加throw new RuntimeException(). 可是那么多业务方法每个设计修改的操作都加,代码繁琐,怎么进行处理呢? 在这里用到上面的AOP切入处理,错误不用管,直接抛,抛到控制层进行处理,这样的话,接口调用的时候,出错了,接口不会什么都不返回,而是会返回给你错误代码,以及错误信息,便于开发人员查错。 8.以上用的是log4j2的日志处理 先移除springboot自带的log日志处理 在build.gradle中增加 复制代码 configurations { providedRuntime // 去除SpringBoot自带的日志 all*.exclude group: 'org.springframework.boot', module: 'spring-boot-starter-logging' } ext { springBootVersion = '2.0.1.RELEASE' } dependencies { compile "org.springframework.boot:spring-boot-starter-log4j2:${springBootVersion}" } 复制代码 然后在application.yml中增加 复制代码 logging: level: com: example: dao: debug config: classpath:log4j2-spring.xml 复制代码 log4j2-spring.xml 复制代码 复制代码 之后在你要打印日志的类中增加 复制代码 private static final Logger logger = LoggerFactory.getLogger(你的类名.class); public static void main(String[] args) { logger.error("error级别日志"); logger.warn("warning级别日志"); logger.info("info级别日志"); } 复制代码 有了日志后就很方便了,在你的方法接收对象时打印下,然后执行了逻辑之后打印下, 出错之后很明确了,就会很少去Debug的,养成多打日志的好习惯,多打印一点info级别的日志,用来在开发环境使用,在上线的时候把打印的最低级别设置为warning,这样你的info级别日志也不会影响到项目的重要Bug的打印 写这个博客的时候我也在同时跑着这个项目,有时候会出现一些错误,例如jar包版本,业务层引用无效,AOP设置不生效等等,也同时在排查解决,如果你遇到了同样的错误,可以去我的GitHub联系我,如小弟有时间或许也能帮到你,谢谢 Github地址:https://github.com/cuifuan ♥ 作者:后端技术精选,欢迎扫码右侧二维码关注公众号! ♠ 出处:https://www.cnblogs.com/javazhiyin/ https://www.cnblogs.com/javazhiyin/p/9993299.html
50000+
5万行代码练就真实本领
17年
创办于2008年老牌培训机构
1000+
合作企业
98%
就业率

联系我们

电话咨询

0532-85025005

扫码添加微信