浅谈Spring AOP 的三种使用方式

Spring AOP 作为 Spring Framework 的核心模块,对 Spring IOC 加以补充,Spring 内部使用它提供了企业级的服务,如事务、异步、缓存等,同时它也允许用户自定义 Aspect,以便用 AOP 补充对 OOP 的使用。通常情况下,我们会通过 AspectJ 的注解来使用 Spring AOP,那么 Spring 一共提供了哪些使用 AOP 的方式呢?

Spring AOP 使用方式

Spring 作为一个广为流行的 Java 框架,主要提供了三种使用方式:

  • 注解:将注解作为元数据,Spring IOC 容器运行时对指定类路径的类进行扫描,根据不同注解执行不同的行为。
  • 外部化配置:和注解类似,将 xml 或 properties 文件内容作为元数据,Spring IOC 容器运行时对配置进行读取。
  • API:这是 Spring 底层暴露给用户可以直接使用的 API ,通常来说使用较少。

注解

随着 JDK 5 注解新特性的添加,Spring 也对其进行了支持,由于注解天然和 Java 结合,可以省去大量的外部化配置,因此在Spring 中注解已经成为主流使用方式。

Spring AOP 的目标不是为了实现完整的 AOP 框架,它和 IOC 容器整合才能最大发挥其作用。Spring 的开发者认为 Spring AOP 和 AspectJ 是互补关系而不是竞争关系,由于 AspectJ 已经很优秀了,因此 Spring 对并未提出新的注解,而是直接对 AspectJ 中的注解进行了有限的支持,可以将 AspectJ 框架中的注解直接应用于 Spring 。

场景

假定我们需要通过 Spring AOP 打印如下方法的参数和返回值。

public interface IService {
    String doSomething(String param);
}

@Service
public class ServiceImpl implements IService {

    @Override
    public String doSomething(String param) {
        return "param is : " + param;
    }

}

依赖引入

在 Spring Framework 环境下,使用 AspectJ 注解需要分别引用如下两个依赖。

<dependency>
    <groupId>org.springframework</groupId>
    <artifactId>spring-context</artifactId>
    <version>5.0.2.RELEASE</version>
</dependency>
<dependency>
    <groupId>org.aspectj</groupId>
    <artifactId>aspectjweaver</artifactId>
    <version>1.9.6</version>
</dependency>

启用 AspectJ 支持

引入所需依赖之后我们还需要显式的开启对 AspectJ 的支持,这需要在配置类上添加 @EnableAspectJAutoProxy 注解。在 SpringBoot 环境下会自动配置,因此无需添加

@Configuration
@EnableAspectJAutoProxy
public class App {
    public static void main(String[] args) {
        AnnotationConfigApplicationContext applicationContext = new AnnotationConfigApplicationContext("com.zzuhkp.blog.aop");

        IService service = applicationContext.getBean(IService.class);
        service.doSomething("hello,aop");

        applicationContext.close();
    }
}

Aspect 定义

必要的工作都准备之后,我们就可以通过注解定义 Aspect ,将我们打印日志的逻辑全部放到这个 Aspect 中。在普通的类上加上 @Aspect 注解,这个类就成为一个 Aspect 了,除此之外由于 Spring 只会对 Bean 上的 @Aspect 注解处理,因此还需要将这个 Aspect 声明为 Spring 的一个 Bean

@Component
@Aspect
public class LogAspect {

}

Pointcut 定义

@Aspect 主要用于将 Pointcut、Advice 整合到一个类中。有了 Aspect 之后,我们就可以在这个 Aspect 中定义 Pointcut 。

通常情况下,我们可以在 Aspect 类中定义一个权限修饰符为 private 类型、无参的空方法,在方法上添加 @Pointcut 注解将这个方法指定为一个 Pointcut,其中方法名作为 Pointcut 的名称,这样在 Advice 中通过这个方法名就可以复用这个 Pointcut。

@Component
@Aspect
public class LogAspect {

    @Pointcut(value = "execution(* *(..))")
    private void pointcut() {
    }
    
}    

上述代码定义了一个名为 pointcut 的 Pointcut,@Pointcut 注解中的表达式表示 Spring AOP 会拦截所有 bean 的所有方法的执行。

Advice 定义

Pointcut 用于指定拦截哪些方法,拦截这些方法后执行哪些动作由 Advice 来确定。

AspectJ 将 Advice 分为三类,分别是 before、after、around,分别表示目标方法执行前、执行后、执行前后执行 Advice。

after 又可以细分为 after、after returning、after throwing。after 表示无论目标方法是否抛出异常都会执行 Advice;after returning 表示目标方法正常返回才会执行 Advice;after throwing 表示目标方法抛出异常才会执行 Advice;这三种 after Spring 是通过 try{} catch(exception){} finally{} 来实现的。

Spring 中的 Advice 通过也是通过方法来定义,通过在方法上添加不同类型的 Advice 注解来表示这个方法是一个 Advice 方法。

先看我们对 Advice 的定义:

@Slf4j
@Component
@Aspect
public class LogAspect {

    @Pointcut(value = "execution(* *(..))")
    private void pointcut() {

    }

    @Before(value = "pointcut() && args(param)", argNames = "param")
    public void before(String param) {
        log.info("before,param is:[{}]", param);
    }

    @After("pointcut()")
    public void after(JoinPoint joinPoint) {
        log.info("after:[{}]", joinPoint.getSignature());
    }
    
    @AfterReturning(value = "pointcut() && args(param)", argNames = "param,result", returning = "result")
    public void afterReturning(String param, String result) {
        log.info("after returning,param is:[{}],result is:[{}],", param, result);
    }

    @AfterThrowing(value = "pointcut()", throwing = "throwable")
    public void afterThrowing(Throwable throwable) {
        log.warn("after throwing", throwable);
    }
    
    @Around("pointcut()")
    public Object around(ProceedingJoinPoint joinPoint) throws Throwable {
        log.info("around before,param is:[{}]", joinPoint.getArgs());
        Object result = joinPoint.proceed();
        log.info("around after,result is:[{}]", result);
        return result;
    }

}

关于 AspectJ 中的 Advice ,可以总结如下约定:

  • 所有的 Advice 注解都需要指定 value 属性,值为 Pointcut 表达式,用以确定拦截的方法,这个表达式可以是对 Pointcut 的引用,如 @After("pointcut()")。
  • Advice 方法可以有一些参数,这些参数需要在注解中的 argNames 属性按照顺序指定参数名称,以便在 Pointcut 表达式中引用。如 @AfterReturning(value = "pointcut() && args(param)", argNames = "param,result", returning = "result")。参数可选类型如下:
    • 参数可以为目标方法的参数,用以接收目标方法参数。
    • 参数可以为 JoinPoint、JoinPoint.StaticPart,对于 around 类型的 Advice 方法参数还可以为 ProceedingJoinPoint 以便调用目标方法,如果这些参数在第一个位置还可以在 argNames 中省略这几个参数的名称。
    • 对于 after returing 类型的 Advice,参数可以为目标方法返回值类型,此时需要通过注解属性 returning 指定目标方法返回值在 Advice 方法中的参数名称。
    • 对于 after throwing 类型的 Advice,参数可以为目标方法抛出的异常类型,此时需要通过注解属性 throwing 指定目标方法抛出的异常在 Advice 方法中的参数名称。

执行测试代码后打印的日志如下:

11:28:18.982 [main] INFO com.zzuhkp.blog.aop.LogAspect - around before,param is:[hello,aop]
11:28:18.986 [main] INFO com.zzuhkp.blog.aop.LogAspect - before,param is:[hello,aop]
11:28:18.986 [main] INFO com.zzuhkp.blog.aop.LogAspect - after returning,param is:[hello,aop],result is:[param is : hello,aop],
11:28:18.988 [main] INFO com.zzuhkp.blog.aop.LogAspect - after:[String com.zzuhkp.blog.aop.IService.doSomething(String)]
11:28:18.988 [main] INFO com.zzuhkp.blog.aop.LogAspect - around after,result is:[param is : hello,aop]

说明不同类型的 Advice 执行顺序如下:around、before、after returning、after、around。around 打印两次则是因为 around 类型的 advice 在目标方法执行前后都添加了打印逻辑,这还可以看出 Spring 为目标类创建了多层代理。

对于同一个 Aspect 中的同一种 advice,如果想指定执行顺序还可以在 advice 方法上添加 @Order 注解,对于不同 Aspect 中的 Advice ,如果想指定顺序,需要在 Aspect 类上添加 @Order 注解或实现 Ordered 方法。

外部化配置

Spring 使用的外部化配置文件主要是 xml 和 properties,在注解出现之前,Spring 主要使用 xml 配置 bean,将上述 Aspect 注解转换为 xml 的表达形式如下:

<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
       xmlns:context="http://www.springframework.org/schema/context"
       xmlns:aop="http://www.springframework.org/schema/aop"
       xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd
       http://www.springframework.org/schema/context https://www.springframework.org/schema/context/spring-context.xsd
       http://www.springframework.org/schema/aop https://www.springframework.org/schema/aop/spring-aop.xsd">

    <bean id="service" class="com.zzuhkp.blog.aop.ServiceImpl"/>

    <bean id="logAspect" class="com.zzuhkp.blog.aop.LogAspect"/>

    <aop:config>
        <aop:aspect ref="logAspect">
            <aop:pointcut id="pointcut" expression="execution(* *(..))"/>
            <aop:around method="around" pointcut-ref="pointcut"/>
            <aop:before method="before" pointcut="execution(* *(..)) and args(param)" arg-names="param"/>
            <aop:after-returning method="afterReturning" pointcut="execution(* *(..)) and args(param)"
                                 arg-names="param,result" returning="result"/>
            <aop:after method="after" pointcut-ref="pointcut"/>
            <aop:after-throwing method="afterThrowing" pointcut-ref="pointcut" arg-names="throwable" throwing="throwable"/>
        </aop:aspect>
    </aop:config>
</beans>

修改测试代码:

public class App {

    public static void main(String[] args) {
        ClassPathXmlApplicationContext applicationContext = new ClassPathXmlApplicationContext("context-aop.xml");
        applicationContext.refresh();

        IService service = applicationContext.getBean(IService.class);
        service.doSomething("hello,aop");

        applicationContext.close();
    }
}

执行结果如下:

13:41:59.881 [main] INFO com.zzuhkp.blog.aop.LogAspect - around before,param is:[hello,aop]
13:41:59.883 [main] INFO com.zzuhkp.blog.aop.LogAspect - before,param is:[hello,aop]
13:41:59.883 [main] INFO com.zzuhkp.blog.aop.LogAspect - around after,result is:[param is : hello,aop]
13:41:59.883 [main] INFO com.zzuhkp.blog.aop.LogAspect - after returning,param is:[hello,aop],result is:[param is : hello,aop],
13:41:59.884 [main] INFO com.zzuhkp.blog.aop.LogAspect - after:[String com.zzuhkp.blog.aop.IService.doSomething(String)]

使用 xml 配置之后,虽然 before advice 在 after advice 之前执行,但是 around 类型的 advice 和其他 advice 类型执行的顺序和注解有所不同,需要加以留意。

API

上述通过注解和 XML 方式的形式配置 Aspect,Spring 会在 bean 的生命周期自动创建代理。除此之外,Spring 还提供了手动通过 API 创建代理对象的方式,这种方式不依赖于 Spring 容器,不依赖 AspectJ,需要对 Spring AOP 底层 API 较为熟悉才能使用。参照上面的样例,我们再次用 API 的方式实现如下:

@Slf4j
public class App {

    public static void main(String[] args) {
        Pointcut pointcut = new StaticMethodMatcherPointcut() {
            @Override
            public boolean matches(Method method, Class<?> targetClass) {
                return method.getDeclaringClass() == IService.class &&
                        "doSomething".equals(method.getName()) &&
                        method.getParameterCount() == 1 && method.getParameterTypes()[0] == String.class;
            }
        };

        IService service = new ServiceImpl();
        ProxyFactory proxyFactory = new ProxyFactory(service);

        proxyFactory.addAdvisor(new DefaultPointcutAdvisor(pointcut, new MethodBeforeAdvice() {
            @Override
            public void before(Method method, Object[] args, Object target) throws Throwable {
                log.info("before,param is:[{}]", args[0]);
            }
        }));

        proxyFactory.addAdvisor(new DefaultPointcutAdvisor(pointcut, new AfterReturningAdvice() {
            @Override
            public void afterReturning(Object returnValue, Method method, Object[] args, Object target) throws Throwable {
                log.info("after returning,param is:[{}],result is:[{}],", args[0], returnValue);
            }
        }));

        class CustomThrowsAdvice implements ThrowsAdvice {
            public void afterThrowing(Throwable throwable) {
                log.warn("after throwing", throwable);
            }
        }
        proxyFactory.addAdvisor(new DefaultPointcutAdvisor(pointcut, new CustomThrowsAdvice()));

        proxyFactory.addAdvice(new MethodInterceptor() {
            @Override
            public Object invoke(MethodInvocation invocation) throws Throwable {
                log.info("around before,param is:[{}]", invocation.getArguments());
                Object result = invocation.proceed();
                log.info("around after,result is:[{}]", result);
                return result;
            }
        });

        IService proxy = (IService) proxyFactory.getProxy();
        proxy.doSomething("hello,aop");
    }
}

PrxoyFactory 是 Spring 提供给我们用于创建代理的核心类,我们创建了 IService 的代理,然后添加了一些 PointcutAdvisor,Advisor 是 Advice 的容器,PointcutAdvisor 还可以指定哪些 Pointcut 满足后执行 Advice,同时我们还添加了 MethodInterceptor 用于表示 around advice,这同样也是 Spring 实现各种 Advice 的基础。关于 Spring AOP,我这里总结了一个思维导图给大家参考,感兴趣的可以点击查看大图:

Spring AOP

上述示例打印结果如下:

14:57:04.924 [main] INFO com.zzuhkp.blog.aop.App - before,param is:[hello,aop]
14:57:04.926 [main] INFO com.zzuhkp.blog.aop.App - around before,param is:[hello,aop]
14:57:04.926 [main] INFO com.zzuhkp.blog.aop.App - around after,result is:[param is : hello,aop]
14:57:04.926 [main] INFO com.zzuhkp.blog.aop.App - after returning,param is:[hello,aop],result is:[param is : hello,aop],

这里的 Advice 顺序同样与注解有所不同,需要注意。

 

参考:

 

posted @ 2022-01-10 22:48  残城碎梦  阅读(372)  评论(0编辑  收藏  举报