Spring AOP 的三种使用方式(万字长文)

前言

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

Spring AOP 使用方式

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

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

Spring AOP 的使用方式也无外乎这三种,AOP 基于代理,如果正在阅读的小伙伴对代理和AOP基本概念不熟悉可以先阅读我前面的文章《Java 中创建代理的几种方式》《从代理到AOP,如何手写一个AOP框架》,下面对 Spring AOP 的几种使用方式具体介绍。

注解

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

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

0. 场景

假定我们需要通过 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;
    }

}

1. 依赖引入

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

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

2. 启用 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();
    }
}

3. Aspect 定义

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

@Component
@Aspect
public class LogAspect {

}

4. 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 的所有方法的执行。

5. 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 Bean 生命周期感兴趣的小伙伴可以参考我前面文章《Java 面试必备的 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 顺序同样与注解有所不同,需要注意。

非常规操作

上述示例中,注解和 XML 配置属于容器自动创建代理,而通过 API 变成的方式则属于手动创建代理,相对来说还属于比较常规的操作。下面简单介绍一些 Spring 中和容器整合的不常用的非常规操作,不感兴趣的小伙伴可以跳过。

自动代理创建

前面内容,我们通过 @EnableAspectJAutoProxy 开启了对 AspectJ 注解的支持,Spring 容器会在 Bean 的生命周期创建代理,底层是创建了一个类型为 AnnotationAwareAspectJAutoProxyCreator 的 bean,在 Spring 的事务管理中则会创建一个类型为 InfrastructureAdvisorAutoProxyCreator 的 bean,它们的实现方式类似。

除了上面两个 Spring 内部使用的用于自动创建代理的 bean,Spring 还预留了两个类给用户使用,将这两个类声明为 bean 即可,这两个类分别如下。

  • BeanNameAutoProxyCreator :可以指定需要创建代理的 bean,不能指定自定义 Advice,能力有限。
  • DefaultAdvisorAutoProxyCreator:使用容器中的 Advisor bean 为其他 bean 创建代理。

我们利用 DefaultAdvisorAutoProxyCreator 再次实现日志打印的 AOP。

Pointcut 代码如下。

public class CustomPointcut extends 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;
    }
}

用于实现 Before Advice 的代码如下。

@Slf4j
@Data
public class CustomBeforeAdvisor implements PointcutAdvisor {

    @Autowired
    private CustomPointcut pointcut;

    @Override
    public Pointcut getPointcut() {
        return pointcut;
    }

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

    @Override
    public boolean isPerInstance() {
        return false;
    }
}

用于实现 After Advice 的 Advisor 代码如下。

@Slf4j
@Data
public class CustomAfterReturningAdvisor implements PointcutAdvisor {

    @Autowired
    private CustomPointcut pointcut;

    @Override
    public Pointcut getPointcut() {
        return pointcut;
    }

    @Override
    public Advice getAdvice() {
        return 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);
            }
        };
    }

    @Override
    public boolean isPerInstance() {
        return false;
    }
}

测试代码如下。

@Slf4j
public class App {

    public static void main(String[] args) {
        AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext();
        context.register(ServiceImpl.class, CustomPointcut.class, CustomBeforeAdvisor.class, CustomAfterReturningAdvisor.class,
                DefaultAdvisorAutoProxyCreator.class
        );

        context.refresh();

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

        context.close();
    }
}

为了简化篇幅,我们仅实现了 Before Advice 和 After Advice,日志打印如下。

15:44:15.081 [main] INFO com.zzuhkp.blog.aop.CustomBeforeAdvisor - before,param is:[hello,aop]
15:44:15.082 [main] INFO com.zzuhkp.blog.aop.CustomAfterReturningAdvisor - after returning,param is:[hello,aop],result is:[param is : hello,aop]

表示通过 DefaultAdvisorAutoProxyCreator 实现了我们的需求,这种方式创建了大量的类,并且需要对 Spring API 有一定的了解才能使用,因此实际使用场景较少。

手动代理创建

除了通过 DefaultAdvisorAutoProxyCreator 自动创建代理,我们还可以通过 ProxyFactoryBean 手动为单个 bean 对象创建代理。

我们在上述的样例代码中继续修改,修改 Spring 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"
       xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd">

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

    <bean id="pointcut" class="com.zzuhkp.blog.aop.CustomPointcut"/>
    <bean id="beforeAdvisor" class="com.zzuhkp.blog.aop.CustomBeforeAdvisor">
        <property name="pointcut" ref="pointcut"/>
    </bean>
    <bean id="afterAdvisor" class="com.zzuhkp.blog.aop.CustomAfterReturningAdvisor">
        <property name="pointcut" ref="pointcut"/>
    </bean>

    <bean id="serviceProxy" class="org.springframework.aop.framework.ProxyFactoryBean">
        <property name="targetName" value="service"/>
        <property name="interceptorNames" value="beforeAdvisor,afterAdvisor"/>
    </bean>

</beans>

使用 ProxyFactoryBean 创建代理,需要使用 targetName 指定目标 bean 的名称,使用 interceptorNames 指定 Advisor bean 的名称。

继续修改测试代码。

public class App {

    public static void main(String[] args) {
        ClassPathXmlApplicationContext context = new ClassPathXmlApplicationContext("context-aop.xml");
        IService service = context.getBean("serviceProxy",IService.class);
        service.doSomething("hello,aop");

        context.close();
    }
}

打印代码如下。

16:17:37.401 [main] INFO com.zzuhkp.blog.aop.CustomBeforeAdvisor - before,param is:[hello,aop]
16:17:39.229 [main] INFO com.zzuhkp.blog.aop.CustomAfterReturningAdvisor - after returning,param is:[hello,aop],result is:[param is : hello,aop]

成功为 IService 创建了代理。

总结

本篇主要介绍了 Spring AOP 三种使用方式,目前注解是主流的使用方式,同时 Spring 还支持使用 xml 配置自动创建代理,如果你对 Spring AOP 的 API 比较熟悉,还可以使用 ProxyFactory 手动为目标对象创建代理。除此之外,本篇还介绍了两个不常用创建代理的方式,分别是 DefaultAdvisorAutoProxyCreator 和 ProxyFactoryBean。相关源码阅读注释已上传到 github,感兴趣的小伙伴可自行下载阅读。

回答: Spring AOP提供了四种方式来实现面向切面编程,它们分别是:基于XML的配置方式、基于注解的方式、基于@AspectJ注解的方式和基于编程的方式。 1. 基于XML的配置方式:通过在XML配置件中定义切点和通知,将切面逻辑与业务逻辑分离。可以使用<aop:config>元素来配置切面和通知,<aop:aspect>元素用于定义切面,<aop:pointcut>元素用于定义切点,<aop:before>、<aop:after>等元素用于定义通知。 2. 基于注解的方式:通过在Java类中使用注解来定义切面和通知。可以使用@Aspect注解来定义切面,@Pointcut注解来定义切点,@Before、@After等注解来定义通知。需要在Spring配置件中启用注解支持,可以使用<context:annotation-config>或者<aop:aspectj-autoproxy>元素来实现。 3. 基于@AspectJ注解的方式:与基于注解的方式类似,但是使用了更强大的@AspectJ注解来定义切面和通知。可以使用@Aspect注解来定义切面,@Pointcut注解来定义切点,@Before、@After等注解来定义通知。需要在Spring配置件中启用@AspectJ支持,可以使用<aop:aspectj-autoproxy>元素来实现。 4. 基于编程的方式:通过编写Java代码来实现切面和通知。可以使用ProxyFactoryBean类来创建代理对象,使用Advice接口的实现类来定义通知。可以在Java类中直接编写切面逻辑,也可以通过实现MethodInterceptor接口来编写通知逻辑。 这些方式都可以实现面向切面编程,选择哪种方式取决于具体的需求和个人偏好。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

大鹏cool

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值