Spring AOP详解

1. Spring AOP实现方式

AOP并不是Spring框架特有的,Spring只是支持AOP编程的框架之一。在Spring中有4种方式去实现AOP的拦截功能:

  • 使用ProxyFactoryBean和对应的接口实现AOP
  • 使用XML配置AOP
  • 使用@AspectJ注解驱动切面
  • 使用AspectJ注入切面
    在Spring AOP的拦截方式中,常用的是@AspectJ注解的方式实现的切面,有时候XML配置也有一定的辅助作用。

2. 使用@AspectJ注解开发Spring AOP

(1)选择切点

Spring是方法级别的AOP框架,我们需要选择某个类的某个方法作为切点,用动态代理的理论来说,就是要拦截哪个方法织入对应的AOP通知。
接口类:

public interface RoleService {
    void printRoleInfo(Role role);
}

实现类:

@Service
public class RoleServiceImpl implements RoleService {
    public void printRoleInfo(Role role) {
        System.out.println(role);
    }
}

这时候我们要把printRoleInfo作为AOP的切点,用动态代理的语言就是为类RoleServiceImpl生成代理对象,然后拦截printRoleInfo方法,于是可以写入各种AOP通知方法。

(2)创建切面

切面对于动态代理而言,就如同一个拦截器,在Spring中只要使用@Aspect注解一个类,那么IOC容器就会认为这是一个切面了

@Aspect
public class RoleAspect {
    @Before("execution(* com.test.impl.RoleServiceImpl.printRoleInfo(...))")
    public void before() {
        System.out.println("Before.....");
    }
    @After("execution(* com.test.impl.RoleServiceImpl.printRoleInfo(...))")
    public void after() {
        System.out.println("After.....");
    }
    @AfterReturning("execution(* com.test.impl.RoleServiceImpl.printRoleInfo(...))")
    public void afterReturning() {
        System.out.println("AfterReturning.....");
    }
    @AfterThrowing("execution(* com.test.impl.RoleServiceImpl.printRoleInfo(...))")
    public void afterThrowing() {
        System.out.println("AfterThrowing.....");
    }
}

在这里插入图片描述

(3)连接点

Spring通过注解中的正则表达式来判断是否需要拦截某个方法。如上述代码中的:

execution(* com.test.impl.RoleServiceImpl.printRoleInfo(...))

依次对这个表达式做分析:

  • execution:代表执行方法时会触发
  • *:代表任意返回类型的方法
  • com.test.impl.RoleServiceImpl:代表类的全限定名
  • printRoleInfo:被拦截方法的名称
  • (…):代表任意参数
    这样就会按AOP通知的规则把方法织入流程中。上述的表达式还比较简单,它可以配置如下内容:在这里插入图片描述

注意:Spring只能支持表中所列出的AspectJ指示器,如果使用了非表中列出的指示器,那么它会抛出IllegalArgumentException异常

此外,Spring还根据自己的需求扩展了一个Bean()的指示器,使得我们可以根据bean id或者名称去定义对应的Bean。
如下方例子

@Before("execution(* com.*.*.*.printRoleInfo(...)) 
            && within(com.test.impl.*)")
public void before() {
    System.out.println("Before.....");
}

这里使用了within去限定execution定义的正则表达式下的包的匹配,这样Spring就会拿到com.test.impl下的类的printRoleInfo方法作为切点了。 &&表示并且,如果使用XML方式引入,在XML中&有特殊含义,因此也可以用and代替它。运算符||也可以用or代替,非运算符!可以用not代替。
上述代码中正则表达式需要写多次,比较麻烦,可以引入@Pointcut注解就可以避免这个麻烦。

@Aspect
public class RoleAspect {
    @Pointcut("execution(* com.test.impl.RoleServiceImpl.printRoleInfo(...))")
    public void print() {
    }
    @Before("print()")
    public void before() {
        System.out.println("Before.....");
    }
    @After("print()")
    public void after() {
        System.out.println("After.....");
    }
    @AfterReturning("print()")
    public void afterReturning() {
        System.out.println("AfterReturning.....");
    }
    @AfterThrowing("print()")
    public void afterThrowing() {
        System.out.println("AfterThrowing.....");
    }
}

这样我们就可以重复使用一个简易表达式取代需要多次书写的复杂表达式了。

(4)测试AOP

这时候可以编写测试代码来测试AOP内容,首先要对Spring的Bean进行配置,采用Java注解配置,代码如下:

@Configuration
@EnableAspectJAutoProxy
@ComponentScan("com.test")
public class AOPConfig {
    @Bean
    public RoleAspect getRoleAspect() {
        return new RoleAspect();
    }
}

@EnableAspectJAutoProxy注解代表启用AspectJ框架的自动代理,这时候Spring才会生成动态代理对象,进而可以使用AOP,而getRoleAspect方法则生成一个切面实例。
Spring还提供了XML配置方式,这里就需要使用AOP的命名空间了,配置如下:

<aop:aspectj-autoproxy/>
<bean id="roleAspect" class="com.test.aspect.RoleAspect"/>
<bean id="roleService" class="com.test.impl.RoleServiceImpl"/>

aop:aspectj-autoproxy/如图注解@EnableAspectJAutoProxy,采用的也是自动代理的功能。
以下是测试代码

public class TestAOP {
    public static void main(String[] args) {
        ApplicationContext ctx = new AnnotationConfigApplicationContext(AOPConfig.class);
        //用XML注解方式
        //ApplicationContext ctx = new ClassPathXmlApplicationContext("spring-cfg.xml");
        RoleService roleService = ctx.getBean(RoleService.class);
        Role role = new Role();
        role.setId(1L);
        role.setNote("note");
        role.setRoleName("name");
        roleService.printRoleInfo(role);
        role = null;//测试异常通知
        roleService.printRoleInfo(role);
    }
}

(5)环绕通知

环绕通知是Spring AOP中最强大的通知,它可以同时实现前置通知和后置通知。保留了调度被代理对象原有方法的功能。

@Around("print()")
public void arount(ProceedingJoinPoint jp) {
    System.out.println("around before......");
    try {
        jp.proceed();
    } catch (Throwable throwable) {
        throwable.printStackTrace();
    }
    System.out.println("arount after......");
}

这样在一个切面里通过@Around注解加入了切面的环绕通知,这个通知里有个ProceedingJoinPoint参数。这个参数是Spring提供的,使用它可以反射切点方法。

环绕通知在使用jp.proceed();方法后会先调度前置通知(@Before),然后反射切点方法,最后是后置通知(@After)和返回通知(@AfterReturning)(或异常通知(@AfterThrowing))

(6)织入

织入是生成代理对象的过程。上述代码中,切点所在的类是有接口的类,如果没有接口,Spring也能提供AOP功能。在使用JDK动态代理时,被代理类必须拥有接口,而使用CGLib则不需要。
Spring提供了一个规则:当类的实现存在接口时,Spring提供JDK动态代理,从而织入各个通知;而当类不存在接口的时候就没有办法使用JDK动态代理,Spring会采用CGLib来生成代理对象。
动态代理对象是由Spring Ioc容器根据描述生成的。Spring建议使用接口编程。

(7)给通知传递参数

public void printRole(Role role, int sort) {
    System.out.println(role);
    System.out.println(sort);
}

在连接点的定义上加入参数,就可以获取动态代理。这个获取是Spring通过动态代理解析正则表达式后传递的。

(8)引入

Spring AOP只是通过动态代理技术,把各类通知织入到它所约定的流程当中,而事实上,有时候我们希望通过引入其他类的方法来得到更好的实现,这时候就可以引入其他的方法了。
如上方在打印role信息前,需要加入一个检测器进行检测,当角色为空时不打印。定义RoleVerifier接口:

public interface RoleVerifier {
    public boolean verify(Role role);
}

定义实现类:

public class RoleVerifierImpl implements RoleVerifier {
    @Override
    public boolean verify(Role role) {
        return role != null;
    }
}

改写之前的切面,加入新的属性

@Aspect
public class RoleAspect {
    @DeclareParents(value="com.test.service.impl.RoleServiceImpl+",
        defaultImpl=RoleVerifierImpl.class)
    public RoleVerifier roleVerifier;
    
    @Pointcut("execution(* com.test.impl.RoleServiceImpl.printRoleInfo(...))")
    public void print() {
    }
    @Before("print()")
    public void before() {
        System.out.println("Before.....");
    }
    @After("print()")
    public void after() {
        System.out.println("After.....");
    }
    @AfterReturning("print()")
    public void afterReturning() {
        System.out.println("AfterReturning.....");
    }
    @AfterThrowing("print()")
    public void afterThrowing() {
        System.out.println("AfterThrowing.....");
    }
}

注解@DeclareParents的使用如上方代码所示。

  • value=“com.test.service.impl.RoleServiceImpl+”:表示对RoleServiceImpl类进行增强,也就是在RoleServiceImpl中引入一个新的接口。
  • defaultImpl:代表引入这个接口的默认实现类,这里是RoleVerifierImpl
    然后就可以使用这个方法了,生成的代理对象会同时实现RoleService和RoleVerifier两个接口。
    使用代码如下
ApplicationContext ctx = new AnnotationConfigApplicationContext(AopConfig.class);
RoleService roleService = ctx.getBean(RoleService.class));
RoleVerifier roleVerifier = (RoleVerifier) roleService;
Role role = new Role()
role.setId(1L);
role.setRoleName("roleName");
role.setNote("note");
if (roleVerifier.verify(role)) {
    roleService.printRole(role);
}

因为代理对象挂了两个接口,所以能够互相转换,进而可以调用引入的方法,这样就能够通过引入功能,在原有的基础上再次增强Bean的功能了。
同样的如果RoleServiceImpl没有接口,那么它也会用CGLIB动态代理,使用增强者类(Enhancer)也会有一个interfaces的属性,允许代理对象挂到相应的多个接口下,于是也就可以按JDK动态代理那样使得对象可以在多个接口之间相互转换。

3. 使用XML配置开发Spring AOP

使用XML配置方式开发AOP需要在XML中引入AOP的命名空间。命名空间如下:

http://www.springframework.org/schema/aop
http://www.springframework.org/schema/aop/spring-aop-4.0.xsd

以下是AOP可配置的元素:在这里插入图片描述
以下是相关代码:
接口类

public interface RoleService {
    void printRoleInfo(Role role);
}

实现类:

public class RoleServiceImpl implements RoleService {
    public void printRoleInfo(Role role) {
        System.out.println("id=" + role.getId() + ", roleName=" + role.getRoleName() + ", note=" + role.getNote());
    }
}

切面类:

public class XmlAspect {
    public void before() {
        System.out.println("before...");
    }
    
    public void after() {
        System.out.println("after...");
    }
    
    public void afterReturning() {
        System.out.println("after returning...");
    }
    
    public void afterThrowing() {
        System.out.println("after throwing...");
    }
}

与注解配置AOP不同的是,代码中没有任何注解,我们需要用XML的方式去向Spring IOC容器描述它。

(1)前置通知、后置通知、返回通知和异常通知

<?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-4.0.xsd
        http://www.springframework.org/schema/context
         http://www.springframework.org/schema/beans/spring-context-4.0.xsd
        http://www.springframework.org/schema/aop
        http://www.springframework.org/schema/beans/spring-aop-4.0.xsd">

    <bean id="roleService" class="com.test.xml.service.RoleServiceImpl"></bean>
    <bean id="xmlAspect" class="com.test.xml.aspect.XmlAspect"></bean>

    <aop:config>
        <!--引用xmlAspect作为切面-->
        <aop:aspect ref="xmlAspect">
            <!--定义通知-->
            <aop:before method="before" pointcut="execution(* com.test.xml.service.RoleServiceImpl.printRoleInfo(..))"></aop:before>
            <aop:after method="after" pointcut="execution(* com.test.xml.service.RoleServiceImpl.printRoleInfo(..))"></aop:after>
            <aop:after-returning method="afterReturning" pointcut="execution(* com.test.xml.service.RoleServiceImpl.printRoleInfo(..))"></aop:after-returning>
            <aop:after-throwing method="afterThrowing" pointcut="execution(* com.test.xml.service.RoleServiceImpl.printRoleInfo(..))"></aop:after-throwing>
        </aop:aspect>
    </aop:config>
</beans>

首先通过引入的XML定义了AOP的命名空间,然后定义了业务Bean(roleService)和切面Bean(xmlAspect),最后通过aop:config配置AOP(向Spring IOC描述AOP配置)

  • aop:aspect:定义切面类,这里是xmlAspect
  • aop:before:定义前置通知
  • aop:after:定义后置通知
  • aop:after-returning:定义返回通知
  • aop:after-throwing:定义异常通知
    其中前置通知、后置通知、返回通知、异常通知需要配置通知对应的方法和切点。此处通知拦截的方法都使用了同一个正则表达式去匹配,显然有些冗余。和注解配置AOP一样,也可以通过定义切点,然后引用到别的通知上。代码如下:
<aop:config>
    <aop:aspect ref="xmlAspect">
        <aop:pointcut id="printRole" expression="execution(* com.test.xml.service.RoleServiceImpl.printRoleInfo(..))"/>
        <aop:before method="before" pointcut-ref="printRole"></aop:before>
        <aop:after method="after" pointcut-ref="printRole"></aop:after>
        <aop:after-returning method="afterReturning" pointcut-ref="printRole"></aop:after-returning>
        <aop:after-throwing method="afterThrowing" pointcut-ref="printRole"></aop:after-throwing>
    </aop:aspect>
</aop:config>

这段代码通过定义切点,然后使用pointcut-ref来引入,可以避免多次书写同一个正则表达式。

(2)环绕通知

和其它通知一样,环绕通知也可以织入到约定的流程中。在切面中加入环绕通知方法:

public void around(ProceedingJoinPoint joinPoint) {
    System.out.println("around before...");
    try {
        joinPoint.proceed();
    } catch (Throwable throwable) {
        throwable.printStackTrace();
    }
    System.out.println("around after...");
}

通过调用ProceedingJoinPoint对象的proceed方法就能够调用原有的流程了。在XML中加入以下配置即可定义环绕通知:

<aop:around method="around" pointcut-ref="printRole"></aop:around>

定义好所有的通知后,编写测试类测试:

public class Test {
    public static void main(String[] args) {
        ApplicationContext ctx = new ClassPathXmlApplicationContext("applicationContext.xml");
        RoleService roleService = (RoleService) ctx.getBean("roleService");
        Role role = new Role();
        role.setId(1L);
        role.setRoleName("roleName");
        role.setNote("note");
        roleService.printRoleInfo(role);
    }
}

得到结果如下:在这里插入图片描述

同一切面情况下通知顺序问题?
网上查找的其中一种答案是:
在这里插入图片描述
另一种答案是:在这里插入图片描述

(3)给通知传递参数

通过XML配置,也可以引入参数到通知中,以前置通知为例。
改写before方法:

public void before(Role role) {
    System.out.println("id=" + role.getId() + ", roleName=" + role.getRoleName() + ", note=" + role.getNote());
}

此时通知带上了role参数,配置文件修改如下:

<aop:before method="before" pointcut="execution(* com.test.xml.service.RoleServiceImpl.printRoleInfo(..)) and args(role)"></aop:before>

和注解方式不同的是,注解中的&&换成了and,因为在XML中&有特殊含义。

(4)引入

在注解中讲述了引入的实现原理,无论是使用JDK动态代理还是使用CGLIB动态代理,都可以将代理对象挂到多个接口下,这样就能引入新的方法了,XML配置也可以实现引入。
在切面中加入新的属性对象:

public RoleVerifier roleVerifier = null;

此时可以用XML配置它,配置的内容和注解引入的方法相当,它是使用aop:declare-parents去引入的

<aop:declare-parents type-matching="com.test.xml.service.RoleServiceImpl+"
    implement-interface="com.test.xml.service.RoleVerifier"
    default-impl="com.test.xml.service.RoleVerifierImpl"

4. 经典的Spring AOP应用程序

这是Spring早期提供的AOP实现,在现实中几乎已被废弃。它需要通过XML方式配置。例如完成前面的前置通知功能,把printRole作为切点,定义一个类来实现前置通知,它需要实现接口MethodBeforeAdvice的before方法。代码如下:

public class ProxyFactoryBeanAspect implements MethodBeforeAdvice {
    /**
     * 前置通知
     * @param method   被代理的方法(切点)
     * @param objects  方法参数
     * @param o        代理方法的目标对象
     * @throws Throwable
     */
    public void before(Method method, Object[] objects, Object o) throws Throwable {
        System.out.println("前置通知...");
    }
}

下面在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:p="http://www.springframework.org/schema/p"
       xsi:schemaLocation="http://www.springframework.org/schema/beans
       http://www.springframework.org/schema/beans/spring-beans-4.0.xsd">
    
    <bean id="proxyFactoryBeanAspect" class="com.test.xml.aspect.ProxyFactoryBeanAspect"></bean>

    <!--设定代理类-->
    <bean id="roleService" class="org.springframework.aop.framework.ProxyFactoryBean">
        <!--这里代理的是接口-->
        <property name="proxyInterfaces">
            <value>com.test.xml.service.RoleService</value>
        </property>
        <!--ProxyFactoryBean要代理的目标类-->
        <property name="target">
            <bean class="com.test.xml.service.RoleServiceImpl"></bean>
        </property>
        <!--定义通知-->
        <property name="interceptorNames">
            <list>
                <!--引入定义好的spring bean-->
                <value>proxyFactoryBeanAspect</value>
            </list>
        </property>
    </bean>
</beans>

测试类代码和前面的一样,运行结果如下:
在这里插入图片描述
这样Spring AOP就被用起来了,这就是经典的方式,但现在已经不是主流方式了。

5. 多个切面

以上讨论的都是一个切面的问题,Spring支持多个切面。多个切面时切面之间不会存在任何顺序,顺序是随机的。有时候我们希望它能按照指定的顺序运行,因此需要一些配置。
首先定义一个切点方法:

public interface MultService {
    void testMult();
}
@Service
public class MultServiceImpl implements MultService {
    @Override
    public void testMult() {
        System.out.println("test multiple aspects");
    }
}
@Aspect
public class Aspect1 {

    @Pointcut("execution(* com.test.multAspect.service.MultServiceImpl.testMult(..))")
    public void print() {}

    @Before("print()")
    public void before() {
        System.out.println("before 1...");
    }

    @After("print()")
    public void after() {
        System.out.println("after 1...");
    }

    @AfterReturning("print()")
    public void afterReturning() {
        System.out.println("after returning 1...");
    }

    @AfterThrowing("print()")
    public void afterThrowing() {
        System.out.println("after throwing 1...");
    }
}
@Aspect
public class Aspect2 {

    @Pointcut("execution(* com.test.multAspect.service.MultServiceImpl.testMult(..))")
    public void print() {}

    @Before("print()")
    public void before() {
        System.out.println("before 2...");
    }

    @After("print()")
    public void after() {
        System.out.println("after 2...");
    }

    @AfterReturning("print()")
    public void afterReturning() {
        System.out.println("after returning 2...");
    }

    @AfterThrowing("print()")
    public void afterThrowing() {
        System.out.println("after throwing 2...");
    }

}

@Aspect
public class Aspect3 {

    @Pointcut("execution(* com.test.multAspect.service.MultServiceImpl.testMult(..))")
    public void print() {}

    @Before("print()")
    public void before() {
        System.out.println("before 3...");
    }

    @After("print()")
    public void after() {
        System.out.println("after 3...");
    }

    @AfterReturning("print()")
    public void afterReturning() {
        System.out.println("after returning 3...");
    }

    @AfterThrowing("print()")
    public void afterThrowing() {
        System.out.println("after throwing 3...");
    }

}

这三个切面就拦截了这个切点方法,搭建测试程序来看它的执行顺序
配置类:

@Configuration
@EnableAspectJAutoProxy
@ComponentScan("com.test.multAspect")
public class MultConfig {

    @Bean
    public Aspect1 getAspect1() {
        return new Aspect1();
    }

    @Bean
    public Aspect2 getAspect2() {
        return new Aspect2();
    }

    @Bean
    public Aspect3 getAspect3() {
        return new Aspect3();
    }
}

通过AnnotationConfigApplicationContext加载配置文件,多次测试后发现其顺序不一定。显然多个切面是无序的。如何让它有序执行,Spring中有多种方法,如果使用注解的切面,可以给切面加上注解@Order,比如在Aspect1中加入@Order(1)。

@Aspect
@Order(1)
public class Aspect3 {
    ...
}

依次给Aspect2和Aspect3加入@Order(2)和@Order(3),再次测试,得到结果:
在这里插入图片描述
在多个切面中,Ordered.getValue()方法返回值(或者注解值)较小值的那个切面拥有较高优先级
众所周知,Spring AOP的实现方法是动态代理。而在多个代理的情况下,Spring通过责任链模式来处理多个切面。而实现按指定顺序执行的方法也可以是让切面实现Ordered(org.springframework.core.Ordered)接口,重写getOrder方法。如下图是多个切面执行顺序的示意图:
在这里插入图片描述
在这里插入图片描述
实现Ordered接口的代码如下:

@Aspect
public class Aspect1 implements Ordered {
    @Override
    public int getOrder() {
        return 1;
    }
}

另外,还可以用XML方式配置执行顺序,代码如下

<aop:aspect ref="aspect1" order="1">
...
</aop:aspect>
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值