SpringBoot2.x 整合AOP切面编程

面向方面编程(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里的值就是切入点所注解的方法名



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



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

3.@After 后置通知

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

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

控制台执行结果



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

4.@Around环绕通知

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

ThreadLocal<Long> 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;
    }

执行结果:



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,他们都秀完了,该我上场了");
    }

执行结果



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

6.@AfterThrowing

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

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

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



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<ResultBean> resultBeanThreadLocal = new ThreadLocal<>();
    ThreadLocal<PageResultBean<?>> pageResultBeanThreadLocal = new ThreadLocal<>();
    ThreadLocal<Long> 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中增加

#显示mysql执行日志 logging:
  level:
    com:
      example:
        dao: debug
  config: classpath:log4j2-spring.xml

log4j2-spring.xml

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

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

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的打印

#Java##程序员#
全部评论

相关推荐

头顶尖尖的程序员:我也是面了三四次才放平心态的。准备好自我介绍,不一定要背熟,可以记事本写下来读。全程控制语速,所有问题都先思考几秒,不要急着答,不要打断面试官说话。
点赞 评论 收藏
分享
评论
1
收藏
分享

创作者周榜

更多
牛客网
牛客网在线编程
牛客网题解
牛客企业服务