• SpringBoot2.x 整合AOP切面编程


    面向方面编程(AOP)通过提供另一种思考程序结构的方式来补充面向对象编程(OOP)。
    OOP中模块化的关键单元是类,而在AOP中,模块化单元是方面。

    准备工作

    首先,使用AOP要在build.gradle中加入依赖

    1. //引入AOP依赖
    2. compile "org.springframework.boot:spring-boot-starter-aop:${springBootVersion}"

    然后在application.yml中加入

    1. spring:
    2. aop:
    3. proxy-target-class: true

    1.@Pointcut 切入点

    定义一个切点
    例如我们要在一个方法加上切入点,根据方法的返回的对象,方法名,修饰词来写成一个表达式或者是具体的名字

    我们现在来定义一个切点

    1. package com.example.aop;
    2. import org.aspectj.lang.annotation.Aspect;
    3. import org.aspectj.lang.annotation.Pointcut;
    4. import org.slf4j.Logger;
    5. import org.slf4j.LoggerFactory;
    6. import org.springframework.stereotype.Component;
    7. /**
    8. * 类定义为切面类
    9. */
    10. @Aspect
    11. @Component
    12. public class AopTestController {
    13. private static final Logger logger = LoggerFactory.getLogger(AopTestController.class);
    14. /**
    15. * 定义一个切点
    16. */
    17. @Pointcut(value = "execution(public String test (..))")
    18. public void cutOffPoint() {
    19. }
    20. }

    这里的切点定义的方法是

    1. @GetMapping("hello")
    2. public String test(){
    3. logger.info("欢迎关注Java知音");
    4. return "i love java";
    5. }

    如果你想写个切入点在所有返回对象为Area的方法,如下
    @Pointcut("execution(public com.example.entity.Area (..))")
    等很多写法,也可以直接作用在某些包下
    注意:private修饰的无法拦截

    2.@Before前置通知

    在切入点开始处切入内容

    在之前的AopTestController类中加入对test方法的前置通知

    1. @Before("cutOffPoint()")
    2. public void beforeTest(){
    3. logger.info("我在test方法之前执行");
    4. }

    这里@Before里的值就是切入点所注解的方法名

    在方法左侧出现的图标跟过去以后就是所要通知的方法 这里就是配置正确了,我们来浏览器调用一下方法

    联想一下,这样的效果可以用在哪里,想像如果要扩展一些代码,在不需要动源代码的基础之上就可以进行拓展,美滋滋

    3.@After 后置通知

    和前置通知相反,在切入点之后执行

    1. @After("cutOffPoint()")
    2. public void doAfter(){
    3. logger.info("我是在test之后执行的");
    4. }

    控制台执行结果

    这里定义一个通知需要重启启动类,而修改通知方法的内容是可以热部署

    4.@Around环绕通知

    和前两个写法不同,实现的效果包含了前置和后置通知
    当使用环绕通知时,proceed方法必须调用,否则拦截到的方法就不会再执行了
    环绕通知=前置+目标方法执行+后置通知,proceed方法就是用于启动目标方法执行的

    1. ThreadLocal<Long> startTime = new ThreadLocal<>();
    2. @Around("cutOffPoint()")
    3. public Object doAround(ProceedingJoinPoint pjp){
    4. startTime.set(System.currentTimeMillis());
    5. logger.info("我是环绕通知执行");
    6. Object obj;
    7. try{
    8. obj = pjp.proceed();
    9. logger.info("执行返回值 : " + obj);
    10. logger.info(pjp.getSignature().getName()+"方法执行耗时: " + (System.currentTimeMillis() - startTime.get()));
    11. } catch (Throwable throwable) {
    12. obj=throwable.toString();
    13. }
    14. return obj;
    15. }

    执行结果:

    1.环绕通知可以项目做全局异常处理
    2.日志记录
    3.用来做数据全局缓存
    4.全局的事物处理 等

    5.@AfterReturning

    切入点返回结果之后执行,也就是都前置后置环绕都执行完了,这个就执行了

    1. /**
    2. * 执行完请求可以做的
    3. * @param result
    4. * @throws Throwable
    5. */
    6. @AfterReturning(returning = "result", pointcut = "cutOffPoint()")
    7. public void doAfterReturning(Object result) throws Throwable {
    8. logger.info("大家好,我是@AfterReturning,他们都秀完了,该我上场了");
    9. }

    执行结果

    应用场景可以用来在订单支付完成之后就行二次的结果验证,重要参数的二次校验,防止在方法执行中的时候参数被修改等等

    6.@AfterThrowing

    这个是在切入执行报错的时候执行的

    1. // 声明错误e时指定的抛错类型法必会抛出指定类型的异常
    2. // 此处将e的类型声明为Throwable,对抛出的异常不加限制
    3. @AfterThrowing(throwing = "e",pointcut = "cutOffPoint()")
    4. public void doAfterReturning(Throwable e) {
    5. logger.info("大家好,我是@AfterThrowing,他们犯的错误,我来背锅");
    6. logger.info("错误信息"+e.getMessage());
    7. }

    在其他切入内容中随意整个错误出来,制造一个环境
    下面是@AfterThrowing的执行结果

    7.AOP用在全局异常处理

    定义切入点拦截ResultBean或者PageResultBean

    1. @Pointcut(value = "execution(public com.example.beans.PageResultBean *(..)))")
    2. public void handlerPageResultBeanMethod() {
    3. }
    4. @Pointcut(value = "execution(public com.example.beans.ResultBean *(..)))")
    5. public void handlerResultBeanMethod() {
    6. }

    下面是AopController.java

    1. package com.example.aop;
    2. import com.example.beans.PageResultBean;
    3. import com.example.beans.ResultBean;
    4. import com.example.entity.UnloginException;
    5. import com.example.exception.CheckException;
    6. import org.aspectj.lang.ProceedingJoinPoint;
    7. import org.aspectj.lang.annotation.Around;
    8. import org.aspectj.lang.annotation.Aspect;
    9. import org.aspectj.lang.annotation.Pointcut;
    10. import org.slf4j.Logger;
    11. import org.slf4j.LoggerFactory;
    12. import org.springframework.stereotype.Component;
    13. /**
    14. * 使用@Aspect注解将此类定义为切面类
    15. * 根据晓风轻著的ControllerAOP所修改
    16. * 晓风轻大佬(很大的佬哥了):https://xwjie.github.io/
    17. */
    18. @Aspect
    19. @Component
    20. public class AopController {
    21. private static final Logger logger = LoggerFactory.getLogger(AopController.class);
    22. ThreadLocal<ResultBean> resultBeanThreadLocal = new ThreadLocal<>();
    23. ThreadLocal<PageResultBean<?>> pageResultBeanThreadLocal = new ThreadLocal<>();
    24. ThreadLocal<Long> start = new ThreadLocal<>();
    25. /**
    26. * 定义一个切点
    27. */
    28. @Pointcut(value = "execution(public com.example.beans.PageResultBean *(..)))")
    29. public void handlerPageResultBeanMethod() {
    30. }
    31. @Pointcut(value = "execution(public com.example.beans.ResultBean *(..)))")
    32. public void handlerResultBeanMethod() {
    33. }
    34. @Around("handlerPageResultBeanMethod()")
    35. public Object handlerPageResultBeanMethod(ProceedingJoinPoint pjp) {
    36. start.set(System.currentTimeMillis());
    37. try {
    38. pageResultBeanThreadLocal.set((PageResultBean<?>)pjp.proceed());
    39. logger.info(pjp.getSignature() + " 方法执行耗时:" + (System.currentTimeMillis() - start.get()));
    40. } catch (Throwable e) {
    41. ResultBean<?> resultBean = handlerException(pjp , e);
    42. pageResultBeanThreadLocal.set(new PageResultBean<>().setMsg(resultBean.getMsg()).setCode(resultBean.getCode()));
    43. }
    44. return pageResultBeanThreadLocal.get();
    45. }
    46. @Around("handlerResultBeanMethod()")
    47. public Object handlerResultBeanMethod(ProceedingJoinPoint pjp) {
    48. start.set(System.currentTimeMillis());
    49. try {
    50. resultBeanThreadLocal.set((ResultBean<?>)pjp.proceed());
    51. logger.info(pjp.getSignature() + " 方法执行耗时:" + (System.currentTimeMillis() - start.get()));
    52. } catch (Throwable e) {
    53. resultBeanThreadLocal.set(handlerException(pjp , e));
    54. }
    55. return resultBeanThreadLocal.get();
    56. }
    57. /**
    58. * 封装异常信息,注意区分已知异常(自己抛出的)和未知异常
    59. */
    60. private ResultBean<?> handlerException(ProceedingJoinPoint pjp, Throwable e) {
    61. ResultBean<?> result = new PageResultBean();
    62. logger.error(pjp.getSignature() + " error ", e);
    63. // 已知异常
    64. if (e instanceof CheckException) {
    65. result.setMsg(e.getLocalizedMessage());
    66. result.setCode(ResultBean.FAIL);
    67. } else if (e instanceof UnloginException) {
    68. result.setMsg("Unlogin");
    69. result.setCode(ResultBean.NO_LOGIN);
    70. } else {
    71. result.setMsg(e.toString());
    72. result.setCode(ResultBean.FAIL);
    73. }
    74. return result;
    75. }
    76. }

    用上面的环绕通知可以对所有返回ResultBean或者PageResultBean的方法进行切入,这样子就不用在业务层去捕捉错误了,只需要去打印自己的info日志
    看下面一段代码

    1. @Transactional
    2. @Override
    3. public int insertSelective(Area record) {
    4. record.setAddress("test");
    5. record.setPostalcode(88888);
    6. record.setType(3);
    7. int i=0;
    8. try {
    9. i = areaMapper.insertSelective(record);
    10. }catch (Exception e){
    11. logger.error("AreaServiceImpl insertSelective error:"+e.getMessage());
    12. }
    13. return i;
    14. }

    假如上面的插入操作失败出错了? 你认为会回滚吗?

    答案是:不会。

    为什么?

    因为你把错误捕捉了,事物没检测到异常就不会回滚。

    那么怎么才能回滚呢?

    在catch里加throw new RuntimeException().

    可是那么多业务方法每个设计修改的操作都加,代码繁琐,怎么进行处理呢?

    在这里用到上面的AOP切入处理,错误不用管,直接抛,抛到控制层进行处理,这样的话,接口调用的时候,出错了,接口不会什么都不返回,而是会返回给你错误代码,以及错误信息,便于开发人员查错

    8.以上用的是log4j2的日志处理

    先移除springboot自带的log日志处理
    在build.gradle中增加

    1. configurations {
    2. providedRuntime
    3. // 去除SpringBoot自带的日志
    4. all*.exclude group: 'org.springframework.boot', module: 'spring-boot-starter-logging'
    5. }
    6. ext {
    7. springBootVersion = '2.0.1.RELEASE'
    8. }
    9. dependencies {
    10. compile "org.springframework.boot:spring-boot-starter-log4j2:${springBootVersion}"
    11. }

    然后在application.yml中增加

    1. #显示mysql执行日志
    2. logging:
    3. level:
    4. com:
    5. example:
    6. dao: debug
    7. config: classpath:log4j2-spring.xml

    log4j2-spring.xml

    1. <?xml version="1.0" encoding="UTF-8"?>
    2. <!--日志级别以及优先级排序: OFF > FATAL > ERROR > WARN > INFO > DEBUG > TRACE > ALL -->
    3. <!--Configuration后面的status,这个用于设置log4j2自身内部的信息输出,可以不设置,当设置成trace时,你会看到log4j2内部各种详细输出-->
    4. <!--monitorInterval:Log4j能够自动检测修改配置 文件和重新配置本身,设置间隔秒数-->
    5. <configuration status="INFO" monitorInterval="30">
    6. <!--先定义所有的appender-->
    7. <appenders>
    8. <!--这个输出控制台的配置-->
    9. <console name="Console" target="SYSTEM_OUT">
    10. <!--输出日志的格式-->
    11. <PatternLayout pattern="%highlight{[ %p ] [%-d{yyyy-MM-dd HH:mm:ss}] [ LOGID:%X{logid} ] [%l] %m%n}"/>
    12. </console>
    13. <!--文件会打印出所有信息,这个log每次运行程序会自动清空,由append属性决定,这个也挺有用的,适合临时测试用-->
    14. <File name="Test" fileName="logs/test.log" append="false">
    15. <PatternLayout pattern="%highlight{[ %p ] %-d{yyyy-MM-dd HH:mm:ss} [ %t:%r ] [%l] %m%n}"/>
    16. </File>
    17. <RollingFile name="RollingFileInfo" fileName="logs/log.log" filePattern="logs/info.log.%d{yyyy-MM-dd}">
    18. <!-- 只接受level=INFO以上的日志 -->
    19. <ThresholdFilter level="info" onMatch="ACCEPT" onMismatch="DENY"/>
    20. <PatternLayout pattern="%highlight{[ %p ] [%-d{yyyy-MM-dd HH:mm:ss}] [ LOGID:%X{logid} ] [%l] %m%n}"/>
    21. <Policies>
    22. <TimeBasedTriggeringPolicy modulate="true" interval="1"/>
    23. <SizeBasedTriggeringPolicy/>
    24. </Policies>
    25. </RollingFile>
    26. <RollingFile name="RollingFileError" fileName="logs/error.log" filePattern="logs/error.log.%d{yyyy-MM-dd}">
    27. <!-- 只接受level=WARN以上的日志 -->
    28. <Filters>
    29. <ThresholdFilter level="warn" onMatch="ACCEPT" onMismatch="DENY" />
    30. </Filters>
    31. <PatternLayout pattern="%highlight{[ %p ] %-d{yyyy-MM-dd HH:mm:ss} [ %t:%r ] [%l] %m%n}"/>
    32. <Policies>
    33. <TimeBasedTriggeringPolicy modulate="true" interval="1"/>
    34. <SizeBasedTriggeringPolicy/>
    35. </Policies>
    36. </RollingFile>
    37. </appenders>
    38. <!--然后定义logger,只有定义了logger并引入的appender,appender才会生效-->
    39. <loggers>
    40. <!--过滤掉spring和mybatis的一些无用的DEBUG信息-->
    41. <logger name="org.springframework" level="INFO"></logger>
    42. <logger name="org.mybatis" level="INFO"></logger>
    43. <root level="all">
    44. <appender-ref ref="Console"/>
    45. <appender-ref ref="Test"/>
    46. <appender-ref ref="RollingFileInfo"/>
    47. <appender-ref ref="RollingFileError"/>
    48. </root>
    49. </loggers>
    50. </configuration>

    之后在你要打印日志的类中增加

    1. private static final Logger logger = LoggerFactory.getLogger(你的类名.class);
    2. public static void main(String[] args) {
    3. logger.error("error级别日志");
    4. logger.warn("warning级别日志");
    5. logger.info("info级别日志");
    6. }

    有了日志后就很方便了,在你的方法接收对象时打印下,然后执行了逻辑之后打印下, 出错之后很明确了,就会很少去Debug的,养成多打日志的好习惯,多打印一点info级别的日志,用来在开发环境使用,在上线的时候把打印的最低级别设置为warning,这样你的info级别日志也不会影响到项目的重要Bug的打印

  • 相关阅读:
    Oracle触发器
    [FMMPEG] parse与 demuxer
    Vuex概述及核心概念
    AI虚拟人物 数字人直播,不用出镜,不用露脸的直播方式(附教程 软件)
    【编译原理】Chapter1概述
    前端开发入门笔记(八)CSS3属性详解:动画详解+Flex布局图文详解+Web字体
    数据挖掘:用ID3算法或者朴素贝叶斯分析一个数据集
    Android组件化开发,其实就这么简单
    同一篇文章版权被同一公司反复起诉
    计算机毕业设计Java校园网络维修系统(源码+系统+mysql数据库+Lw文档)
  • 原文地址:https://blog.csdn.net/LBWNB_Java/article/details/126578141