Spring-Aop的基本使用

127 阅读6分钟

「这是我参与2022首次更文挑战的第7天,活动详情查看:2022首次更文挑战

AOP的出现主要是为了解决外围业务代码和核心业务代码分离的问题,例如:事务、日志记录、链路追踪等

aop的两种实现

  • aspectJ (静态织入)

AspectJ采用的是静态织入的方式,在编译期使用 AspectJacj 编译器把 aspect 类编译成class字节码后,在java目标类编译时织入,即先编译 aspect 类再编译目标类

  • spring-aop(动态织入)

spring-aop 采用的是基于运行时增强的代理技术,在运行时动态将要增强的代码织入到目标类中,实现技术主要有以下两种:

    • JDK的动态代理:默认使用,底层通过反射技术实现,必须要有对应的接口类
    • cglib的动态代理:采用字节码技术,为目标类创建一个子类,在子类中进行拦截,拦截父类方法调用并且织入切面逻辑

基于注解的spring Aop开发

示例代码

日志记录

/**
 * @author chenw
 * @date 2021/3/3 9:49
 *
 * 日志切面
 */
@Slf4j
@Aspect
@Component
public class LogAspect {
    //定义切入点
    @Pointcut("execution(public * com.example.aop_demo.controller.*.*(..))")
    public void weblog(){}
    //前置通知,在连接点方法前调用
    @Before("weblog()")
    public void deBefore(JoinPoint joinPoint)throws Throwable{
        // 接收到请求,记录请求内容
        ServletRequestAttributes attributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
        HttpServletRequest request = attributes.getRequest();
        log.info("URL:{}",request.getRequestURL().toString());
        log.info("ARGS:{}", Arrays.toString(joinPoint.getArgs()));
    }
    //返回通知,在连接点方法执行并正常返回后调用,要求连接点方法在执行过程中没有发生异常
    @AfterReturning(returning = "obj",pointcut = "weblog()")
    public void doAfterReturning(Object obj)throws Throwable{
        //方法返回值
        log.info("返回值:{}",obj);
    }
    //异常通知,当连接点方法异常时通知
    @AfterThrowing("weblog()")
    public void afterThrow(JoinPoint joinPoint){
        log.info("方法异常时执行");
    }
    //后置通知,在连接点方法后调用,不管有没有异常都会执行
    @After("weblog()")
    public void after(){
        log.info("方法最后执行");
    }
    //环绕通知,它将覆盖原有方法,但是允许你通过反射调用原有方法
    @Around("weblog()")
    public Object around(ProceedingJoinPoint proceedingJoinPoint){
        log.info("环绕通知");
        try {
            //执行目标方法
            Object proceed = proceedingJoinPoint.proceed();
            log.info("环绕执行结果:{}",proceed);
            return proceed;
        }catch (Throwable e){
            e.printStackTrace();
            return "哈哈哈哈";
        }
    }
}

Tips:使用 @Aspect 注解来定义这个为切面类,被定义为切面的类仍然是一个 bean ,需要使用 @Component 注解标注

切点指示符

通过 @Pointcut 注解进行定义,再通过 execution()中的正则表达式判断具体要拦截的是哪一个类哪一个方法

//定义规则
@Pointcut("execution(<访问权限> <返回类型> <方法名模式>(<参数模式>)<异常模式>)")
@Pointcut("execution(public * com.example.aop_demo.controller..*.*(..))")
通配符含义
..匹配方法定义中的任意数量的参数,此外还匹配类定义中的任意数量包*//任意返回值,任意名称,任意参数的公共方法*execution(public * *(..))
+匹配给定类的任意子类//匹配实现了DaoUser接口的所有子类的方法within(com.zejian.dao.DaoUser+)
*匹配任意数量的字符*//匹配com.zejian.service包及其子包中所有类的所有方法within(com.zeji an.service..)//匹配以set开头,参数为int类型,任意返回值的方法execution(* set*(int))

类型签名表达式(within)

为了方便类型(如接口、类名、包名)过滤方法,Spring AOP 提供了within关键字。其语法格式如下:

//type name 使用包名或者类名替换即可
within(<type name>)


//匹配com.zejian.dao包及其子包中所有类中的所有方法
@Pointcut("within(com.zejian.dao..*)")
//匹配UserDaoImpl类中所有方法
@Pointcut("within(com.zejian.dao.UserDaoImpl)")
//匹配UserDaoImpl类及其子类中所有方法
@Pointcut("within(com.zejian.dao.UserDaoImpl+)")
//匹配所有实现UserDao接口的类的所有方法
@Pointcut("within(com.zejian.dao.UserDao+)")

方法签名表达式(execution)

根据方法签名过滤

//scope :方法作用域,如public,private,protect
//returnt-type:方法返回值类型
//fully-qualified-class-name:方法所在类的完全限定名称
//parameters 方法参数
execution(<scope> <return-type> <fully-qualified-class-name>.*(parameters))


//匹配UserDaoImpl类中的所有方法
@Pointcut("execution(* com.zejian.dao.UserDaoImpl.*(..))")
//匹配UserDaoImpl类中的所有公共的方法
@Pointcut("execution(public * com.zejian.dao.UserDaoImpl.*(..))")
//匹配UserDaoImpl类中的所有公共方法并且返回值为int类型
@Pointcut("execution(public int com.zejian.dao.UserDaoImpl.*(..))")
//匹配UserDaoImpl类中第一个参数为int类型的所有公共的方法
@Pointcut("execution(public * com.zejian.dao.UserDaoImpl.*(int , ..))")

其他指示符

  • bean :Spring Aop扩展的,用于匹配特定名称的Bean对象的执行方法
//匹配名称中带有后缀Service的Bean。
@Pointcut("bean(*Service)")
private void myPointcut1(){}
  • this :用于匹配当前AOP代理对象类型的执行方法;请注意是AOP代理对象的类型匹配,这样就可能包括引入接口也类型匹配
//匹配了任意实现了UserDao接口的代理对象的方法进行过滤
@Pointcut("this(com.zejian.spring.springAop.dao.UserDao)")
private void myPointcut2(){}
  • target :用于匹配当前目标对象类型的执行方法;
//匹配了任意实现了UserDao接口的目标对象的方法进行过滤
@Pointcut("target(com.zejian.spring.springAop.dao.UserDao)")
private void myPointcut3(){}
  • @within : 用于匹配所以持有指定注解类型内的方法;请注意与within是有区别的, within是用于匹配指定类型内的方法执行;
//匹配使用了MarkerAnnotation注解的类(注意是类)
@Pointcut("@within(com.zejian.spring.annotation.MarkerAnnotation)")
private void myPointcut4(){}
  • @annotation :根据所应用的注解进行方法过滤
//匹配使用了MarkerAnnotation注解的方法(注意是方法)
@Pointcut("@annotation(com.zejian.spring.annotation.MarkerAnnotation)")
private void myPointcut5(){}

Tips:切点指示符可以使用运算符语法进行表达式的混编,如 andornot&&||! 等等

//匹配了任意实现了UserDao接口的目标对象的方法并且该接口不在com.zejian.dao包及其子包下
@Pointcut("target(com.zejian.spring.springAop.dao.UserDao) !within(com.zejian.dao..*)")
private void myPointcut6(){}
//匹配了任意实现了UserDao接口的目标对象的方法并且该方法名称为addUser
@Pointcut("target(com.zejian.spring.springAop.dao.UserDao)&&execution(* com.zejian.spring.springAop.dao.UserDao.addUser(..))")
private void myPointcut7(){}

通知类型

@Pointcut :定义连接点(切点)

  • @Before :前置通知,在连接点方法前调用
  • @Around :环绕通知,它将覆盖原有的方法,但是允许你通过反射调用原有的方法
  • @After :最终通知,在连接点方法后调用
  • @AfterReturning :后置通知,在连接点方法执行并正常返回后调用,要求连接点方法在执行过程中没有发生异常
  • @AfterThrowing :异常通知,当连接点方法异常时调用

前置通知(@Before)

Tips:JoinPoint:可以通过此静态变量获取到目标对象的信息,例如类名称、方法参数、方法名称等

//前置通知,在连接点方法前调用
@Before("weblog()")
public void deBefore(JoinPoint joinPoint)throws Throwable{
    // 接收到请求,记录请求内容
    ServletRequestAttributes attributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
    HttpServletRequest request = attributes.getRequest();
    log.info("URL:{}",request.getRequestURL().toString());
    log.info("ARGS:{}", Arrays.toString(joinPoint.getArgs()));
}

后置通知(@AfterReturning)

returning :声明一个接收切点方法执行后的返回值的变量

//后置通知,在连接点方法执行并正常返回后调用,要求连接点方法在执行过程中没有发生异常
@AfterReturning(returning = "obj",pointcut = "weblog()")
public void doAfterReturning(JoinPoint joinPoint,Object obj)throws Throwable{
    //方法返回值
    log.info("返回值:{}",obj);
}

异常通知(@afterThrowing)

throwing :声明一个接受异常信息的变量

//异常通知,当连接点方法异常时通知
@AfterThrowing(value = "weblog()",throwing = "throwable")
public void afterThrow(JoinPoint joinPoint,Throwable throwable){
    log.info("方法异常时执行");
}

最终通知(@after)

类似与finally语句块,只要用了,不管什么情况下都会执行。

//最终通知,在连接点方法后调用,不管有没有异常都会执行
@After(value = "weblog()")
public void after(JoinPoint joinPoint){
    log.info("方法最后执行");
}

环绕通知(@Around)

环绕通知可以在方法执行前执行,也可以在方法执行后执行,但是必须要手动去调用目标方法

//环绕通知,它将覆盖原有方法,但是允许你通过反射调用原有方法
@Around(value = "weblog()")
public Object around(ProceedingJoinPoint proceedingJoinPoint){
    log.info("环绕通知");
    try {
      //执行目标方法
        Object proceed = proceedingJoinPoint.proceed();
        log.info("环绕执行结果:{}",proceed);
        return proceed;
    }catch (Throwable e){
        e.printStackTrace();
        return "哈哈哈哈";
    }
}