使用AOP

AspectJ 的切入点表达式

为了方便的使用通知,AspectJ定义了专门的表达式用于指定切入点。表达式的原型是:

execution ( 
	[modifiers-pattern]  访问权限类型,可选项,若不写则包含所有访问权限
	ret-type-pattern  返回值类型,必填项,*表示任意类型
	[declaring-type-pattern]  全限定名,可选,两个点“..”代表当前包以及子包下的所有类,省略则表示所有的类
	name-pattern(param-pattern)  方法名(参数名),必填,()表示没有参数,(..)参数任意,(*)只有1个参数
	[throws-pattern]  抛出异常类型 ,可选
)

切入点表达式要匹配的对象就是目标方法的方法名。所以,execution 表达式中明显就是方法的签名。注意,表达式中加[ ]的部分表示可省略部分,各部分间用空格分开。

举例:

execution(public * *(..)) 
指定切入点为:任意公共方法。

execution(* set*(..)) 
指定切入点为:任何一个以“set”开始的方法。

execution(* com.xyz.service.*.*(..)) 
指定切入点为:定义在 service 包里的任意类的任意方法。

execution(* com.xyz.service..*.*(..))
指定切入点为:定义在 service 包或者子包里的任意类的任意方法。“..”出现在类名中时,后面必须跟“*”,表示包、子包下的所有类。

execution(* *.service.*.*(..))
指定只有一级包下的 serivce 子包下所有类(接口)中所有方法为切入点 

execution(* *..service.*.*(..))
指定所有包下的 serivce 子包下所有类(接口)中所有方法为切入点 

execution(* *.ISomeService.*(..))
指定只有一级包下的 ISomeSerivce 接口中所有方法为切入点 

execution(* *..ISomeService.*(..))
指定所有包下的 ISomeSerivce 接口中所有方法为切入点 

execution(* com.xyz.service.IAccountService.*(..)) 
指定切入点为:  IAccountService  接口中的任意方法。 

execution(* com.xyz.service.IAccountService+.*(..)) 
指定切入点为:  IAccountService  若为接口,则为接口中的任意方法及其所有实现类中的任意方法;若为类,则为该类及其子类中的任意方法。 

execution(* joke(String,int)))
指定切入点为:所有的 joke(String,int)方法,且 joke()方法的第一个参数是 String,第二个参	数是 int。如果方法中的参数类型是 java.lang 包下的类,可以直接使用类名,否则必须使用全限定类名,如 joke( java.util.List, int)。 

execution(* joke(String,*))) 
指定切入点为:所有的 joke()方法,该方法第一个参数为 String,第二个参数可以是任意类型,如 joke(String s1,String s2)和 joke(String s1,double d2)都是,但 joke(String s1,double d2,String s3)不是。

execution(* joke(String,..)))   
指定切入点为:所有的 joke()方法,该方法第  一个参数为 String,后面可以有任意个参数且参数类型不限,如 joke(String s1)、joke(String s1,String s2)和 joke(Strings1,double d2,String s3)都是。

execution(* joke(Object))
指定切入点为:所有的 joke()方法,方法拥有一个参数,且参数是 Object 类型。joke(Object ob)是,但,joke(String s)与 joke(User u)均不是。

execution(* joke(Object+))) 
指定切入点为:所有的 joke()方法,方法拥有一个参数,且参数是 Object 类型或该类的子类。不仅 joke(Object ob)是,joke(String s)和 joke(User u)也是。

基于注解的AOP的实现方式

添加下面依赖

<dependency>
    <groupId>org.aspectj</groupId>
    <artifactId>aspectjweaver</artifactId>
    <version>1.9.19</version>
</dependency>

<dependency>
    <groupId>org.springframework</groupId>
    <artifactId>spring-aspects</artifactId>
    <version>6.0.5</version>
</dependency>

使用@Aspect来声明一个切面类MyAspect,在类中添加不同通知的方法便于测试,如果有多个切面的话且需设置切面的优先级时,可以使用@Order注解来标识切面类,为@Order注解的value指定一个整数型的数字,数字越小,优先级越高。

@Aspect
@Component
//@Order(1)设置切面的优先级
public class MyAspect {

    @Before("execution(* *..UserServiceImpl.addUser())")
    public void before() {
        System.out.println("========前置通知========");
    }

    @After("execution(* *..UserServiceImpl.selectUser())")
    public void after() {
        System.out.println("========最终通知========:");
    }

    @AfterThrowing(value = "execution(* *..UserServiceImpl.selectUserById(..))" ,throwing = "e")
    public void afterThrowing(Exception e) {
        System.out.println("========异常通知========:" + e);
    }

    @AfterReturning(value = "execution(* *..UserServiceImpl.updateUser())",returning = "result")
    public void afterReturning(int result) {
        System.out.println("========后置通知========:" + result);
    }

    @Around(value = "execution(* *..UserServiceImpl.deleteUser())")
    public Object around(ProceedingJoinPoint pjp) throws Throwable {
        System.out.println("========环绕通知:前========:");
        Object proceed = pjp.proceed();
        System.out.println("========环绕通知:后========:");

        return proceed;
    }
}

使用全注解的方式,先创建一个配置类

@Configuration
@ComponentScan("com.monkey1024")
@EnableAspectJAutoProxy
public class AopConfiguration {
}

其中的@EnableAspectJAutoProxy注解的作用是开启aop自动代理,开启后spring会扫描@Aspect标注的切面类,解析里面的切入点表达式,为符合表达式的类创建出代理对象。该注解中有1个属性proxyTargetClass默认是false,表示使用jdk的动态代理,若为true则使用cglib动态代理。

下面代码从spring容器中获取的是UserServiceImpl代理对象。

测试代码:

ApplicationContext context = new AnnotationConfigApplicationContext(AopConfiguration.class);
//这里获取的是代理对象,需要写接口
UserService userService = context.getBean("userServiceImpl", UserService.class);
userService.addUser();

在实际应用时,多个切入点表达式往往是相同的,如下示例:

@Aspect
@Component
public class OtherAspect {

    @Before("execution(* *..UserServiceImpl.*(..))")
    public void before() {
        System.out.println("========前置通知========");
    }

    @After("execution(* *..UserServiceImpl.*(..))")
    public void after() {
        System.out.println("========最终通知========:");
    }

    @AfterThrowing(value = "execution(* *..UserServiceImpl.*(..))" ,throwing = "e")
    public void afterThrowing(Exception e) {
        System.out.println("========异常通知========:" + e);
    }

    @AfterReturning(value = "execution(* *..UserServiceImpl.*(..))",returning = "result")
    public void afterReturning(int result) {
        System.out.println("========后置通知========:" + result);
    }

    @Around(value = "execution(* *..UserServiceImpl.*(..))")
    public Object around(ProceedingJoinPoint pjp) throws Throwable {
        System.out.println("========环绕通知:前========:");
        Object proceed = pjp.proceed();
        System.out.println("========环绕通知:后========:");

        return proceed;
    }
}

上面代码中的切入点表达式都是相同的,显得比较冗余,我们可以将切点表达式单独的定义出来,在需要的位置引入即可。

@Aspect
@Component
//@Order(1)设置切面的优先级
public class OtherAspect {

    @Pointcut("execution(* *..UserServiceImpl.*(..))")
    public void pointcut(){}
    
    //这里写的是方法名
    @Before("pointcut()")
    public void before() {
        System.out.println("========前置通知========");
    }

    @After("pointcut()")
    public void after() {
        System.out.println("========最终通知========:");
    }

    @AfterThrowing(value = "pointcut()" ,throwing = "e")
    public void afterThrowing(Exception e) {
        System.out.println("========异常通知========:" + e);
    }

    @AfterReturning(value = "pointcut()",returning = "result")
    public void afterReturning(int result) {
        System.out.println("========后置通知========:" + result);
    }

    @Around(value = "pointcut()")
    public Object around(ProceedingJoinPoint pjp) throws Throwable {
        System.out.println("========环绕通知:前========:");
        Object proceed = pjp.proceed();
        System.out.println("========环绕通知:后========:");

        return proceed;
    }
}

基于xml实现AOP的方式

创建切面类MyAspect:

import org.aspectj.lang.ProceedingJoinPoint;

public class MyAspect {

    public void before() {
        System.out.println("========前置通知========");
    }

    public void after() {
        System.out.println("========最终通知========:");
    }

    public void afterThrowing(Exception e) {
        System.out.println("========异常通知========:" + e);
    }

    public void afterReturning(int result) {
        System.out.println("========后置通知========:" + result);
    }

    public Object around(ProceedingJoinPoint pjp) throws Throwable {
        System.out.println("========环绕通知:前========:");
        Object proceed = pjp.proceed();
        System.out.println("========环绕通知:后========:");

        return proceed;
    }
}

spring的配置文件:

<?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:aop="http://www.springframework.org/schema/aop"
       xmlns:context="http://www.springframework.org/schema/context"
       xsi:schemaLocation="http://www.springframework.org/schema/beans
        http://www.springframework.org/schema/beans/spring-beans.xsd
        http://www.springframework.org/schema/aop
        http://www.springframework.org/schema/aop/spring-aop.xsd
        http://www.springframework.org/schema/context
        http://www.springframework.org/schema/context/spring-context.xsd">
	
    
    <context:component-scan base-package="com.monkey1024"/>
    <!--
        相当于注解中的@EnableAspectJAutoProxy
    -->
    <aop:aspectj-autoproxy  proxy-target-class="false"/>
    
    <bean id="myAspect" class="com.monkey1024.aspect.MyAspect"/>
    
        <!--配置aop-->
    <aop:config>
        <!--定义切入点-->
        <aop:pointcut id="addUserPointcut" expression="execution(* com.monkey1024.service.impl.UserServiceImpl.addUser())"/>
        <aop:pointcut id="selectUserPointcut" expression="execution(* com.monkey1024.service.impl.UserServiceImpl.selectUser())"/>
        <aop:pointcut id="selectUserByIdPointcut" expression="execution(* com.monkey1024.service.impl.UserServiceImpl.selectUserById(..))"/>
        <aop:pointcut id="updateUserPointcut" expression="execution(* com.monkey1024.service.impl.UserServiceImpl.updateUser())"/>
        <aop:pointcut id="deleteUserPointcut" expression="execution(* com.monkey1024.service.impl.UserServiceImpl.deleteUser())"/>

        <!--定义切面-->
        <aop:aspect ref="myAspect">
            <!--前置通知-->
            <aop:before method="before" pointcut-ref="addUserPointcut"/>
            <!--后置通知-->
            <aop:after-returning method="afterReturning" pointcut-ref="updateUserPointcut" returning="result"/>
            <!--异常通知-->
            <aop:after-throwing method="afterThrowing" pointcut-ref="selectUserByIdPointcut" throwing="e"/>
            <!--最终通知-->
            <aop:after method="after" pointcut-ref="selectUserPointcut"/>
            <!--环绕通知-->
            <aop:around method="around" pointcut-ref="deleteUserPointcut"/>
        </aop:aspect>
    </aop:config>

</beans>

配置文件中,除了要定义目标类与切面的 Bean 外,最主要的是在 aop:config 中进行aop 的配置。而该标签的底层,会根据其子标签的配置,生成自动代理。

通过其子标签aop:pointcut定义切入点,该标签有两个属性,id 与 expression。分别用于指定该切入点的名称及切入点的值。expression 的值为 execution 表达式。

aop:aspect的 ref 属性用于指定使用哪个切面。
aop:aspect的子标签是各种不同的通知类型。不同的通知所包含的属性是不同的,但也有共同的属性。
method:指定该通知使用的切面中的哪个方法。
pointcut-ref:指定该通知要织入的切入点表达式。
AspectJ 通知的 XML 标签如下:

<aop:before/>:前置通知 
<aop:after-returning/>:  后置通知 
<aop:around/>:环绕通知 
<aop:after-throwing/>:异常通知 
<aop:after/>:最终通知

使用spring AOP还是AspectJ

spring AOP要比使用AspectJ简单,如果通知(advice)是加到spring容器中的对象,使用spring AOP就足够了,否则需要使用AspectJ,另外Spring AOP仅支持方法作为连接点,若要操作属性上的连接点,则需要使用AspectJ。

spring AOP属于运行时增强,底层使用代理实现,而AspectJ属于编译时增强,底层基于字节码实现。

使用xml还是注解

总的来看,目前注解是比较流行的,spring AOP使用的就是AspectJ提供的注解,这可以让我们非常轻松的从spring AOP切换到AspectJ。另外,xml对于一些配置支持的不太好,比如下面注解就不能使用xml配置:

@Pointcut("a() && b()")
public void one() {}

Introduction使用

在不能修改原有类且希望为其添加新的方法时就可以使用spring中的introduction。比如购买手机后,会给手机进行包装,比如贴膜,套壳等,这两个操作都不会修改原有的手机,接下来结合代码利用Introduction来模拟此情况。

创建手机接口

public interface Phone {
    void call();
}

手机接口实现类

/*
    小米23代系列手机
 */
@Component
public class XiaoMi23 implements Phone{
    @Override
    public void call() {
        System.out.println("使用小米23手机打电话");
    }
}

包装接口

/*
    包装接口
 */
public interface Wrapper {
    void wrapperThings();
}

包装接口的实现类

/*
    包装手机
 */
@Component
public class PhoneWrapper implements Wrapper{
    @Override
    public void wrapperThings() {
        System.out.println("贴膜,套手机壳");
    }
}

创建手机切面类,在所有的Phone接口子类中引入PhoneWrapper类。

@Aspect
@Component
public class PhoneConfig {

    /*
    	+表示所有的Phone子类,defaultImpl表示引入的类
    	可以认为是给所有Phone的子类添加一个父类PhoneWrapper,这样就可以父类中的方法了。
    */
    @DeclareParents(value = "com.monkey1024.bean.Phone+",defaultImpl = PhoneWrapper.class)
    private Wrapper wrapper;
}

创建配置类

/*
    spring配置类
 */
@Configuration
@ComponentScan(basePackages = "com.monkey1024")
@EnableAspectJAutoProxy
public class SpringConfiguration {
}

测试代码

ApplicationContext context = new AnnotationConfigApplicationContext(SpringConfiguration.class);

/*
	从容器中获取xiaoMi23对象,由于在PhoneConfiguration中配置
	xiaoMi23引入PhoneWrapper类,因此这里可以被强转为Wrapper类型
*/
Wrapper wrapper = (Wrapper)context.getBean("xiaoMi23");
wrapper.wrapperThings();

使用aware接口解决aop的问题

spring aop底层是代理,有了代理对象才能将通知织入。下面创建一个service类(父接口略),添加两个方法

@Service
public class StudentServiceImpl implements StudentService{

    @Override
    public void study() {
        System.out.println("好好学习");
        
        this.play();//这里会调用StudentServiceImpl对象,并非调用到代理对象,无法织入play方法通知
    }

    @Override
    public void play() {
        System.out.println("寻欢作乐");
    }

}

创建切面类

/*
    切面类
 */
@Component
@Aspect
public class QuestionAspect {

    @After("execution(* *..StudentServiceImpl.study())")
    public void gain() {
        System.out.println("====金榜题名====");
    }

    @After("execution(* *..StudentServiceImpl.play())")
    public void weak() {
        System.out.println("====浑身无力====");
    }

}

创建测试代码,执行后只能看到study方法最终通知的执行

AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext(AopConfiguration.class);

StudentService studentService = context.getBean("studentServiceImpl", StudentService.class);
studentService.study();

若想看到play方法最终通知的执行,需要在study方法中获取到StudentServiceImpl的代理对象,该对象是从spring容器中取得,因此现在的问题是如何获取spring容器对象。

之前讲过在spring中提供了aware相关的接口,通过这些接口可以获取spring内部对象,这里就可以利用ApplicationContextAware接口获取spring容器了。修改StudentServiceImpl如下:

@Service
public class StudentServiceImpl implements StudentService, ApplicationContextAware {

    private ApplicationContext context;

    @Override
    public void study() {
        System.out.println("好好学习");

        //从容器中获取代理对象
        StudentService studentService = context.getBean("studentServiceImpl", StudentService.class);
        //这样就可以看到play方法最终通知的织入
        studentService.play();
    }

    @Override
    public void play() {
        System.out.println("寻欢作乐");
    }

    @Override
    public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
        context = applicationContext;
    }