springAop使用

springAop使用

参考

1.使用背景

开发中在多个模块间有某段重复的代码,在传统的面向过程编程中,将这段代码抽象成一个方法,
在需要的地方分别调用这个方法,当需要修改功能时,只改变这个方法就可以了。然而,需求总是变化的,
有一天,新增了一个需求,需要需要在多处做修改,我们需要再抽象出一个方法,
然后再在需要的地方分别调用这个方法,又或者我们不需要这个方法了,我们得删除掉每一处调用该方法的地方。
实际上涉及到多个地方具有相同的修改问题我们通过 AOP 来解决。

2.AOP术语

AOP 领域中的特性术语:

通知(Advice): AOP 框架中的增强处理。通知描述了切面何时执行以及如何执行增强处理。
连接点(join point): 连接点表示应用执行过程中能够插入切面的一个点,这个点可以是方法的调用、异常的抛出。在 Spring AOP 中,连接点总是方法的调用。
切点(PointCut): 可以插入增强处理的连接点。
切面(Aspect): 切面是通知和切点的结合。
引入(Introduction):引入允许我们向现有的类添加新的方法或者属性。
织入(Weaving): 将增强处理添加到目标对象中,并创建一个被增强的对象,这个过程就是织入。

3.SpringAop

3.1 特点

Spring AOP 不能拦截对对象字段的修改,也不支持构造器连接点,我们无法在 Bean 创建时应用通知。

3.2 Spring AOP 的简单例子

 第一步:创建接口
    public interface Buy {
        void buy();
    }
第二步:接口实现
    @Component
    public class BoyBuy implements Buy{
        public void buy() {
            System.out.println("boy buy");
        }
    }
    @Component
    public class GirlBuy implements Buy{
        public void buy() {
            System.out.println("girl buy");
        }
    }
第三步:配置文件,开启aop
    @Configurable
    @ComponentScan(basePackages="com.test.aop")
    @EnableAspectJAutoProxy(proxyTargetClass=true)
    public class BuyConfig {
    }
第四步:定义一个切面类
    @Aspect
    @Component
    public class BuyAspects {
        @Before("execution(* com.test.aop.Buy.buy(..))")
        public void buyAspect() {
            System.out.println("buy aspect");
        }
    }
注:@Aspect 表示该类是一个切面 
    @Before 注解,表示buyAspect方法在方法执行之前执行
    ("execution(* com.test.aop.Buy.buy(..))")声明了切点,
            表明在该切面的切点是com.test.aop.Buy.buy(..)这个接口中的buy方法
第五步:测试
    ApplicationContext aContext =new AnnotationConfigApplicationContext(BuyConfig.class);
    BoyBuy boyBuy = aContext.getBean("boyBuy",BoyBuy.class);
    boyBuy.buy();
    GirlBuy girlBuy = aContext.getBean("girlBuy",GirlBuy.class);
    girlBuy.buy();

    输出:
        buy aspect
        boy buy
        buy aspect
        girl buy

3.3 注解配置 Spring AOP

3.3.1 Spring AOP 所支持的 AspectJ 切点指示器

    arg()            限定连接点方法参数
    @args()          通过连接点方法参数上的注解进行限定
    execution()        用于匹配是连接点的执行方法
    this()            限制连接点匹配AOP代理Bean引用为指定的类型
    target            目标对象(即被代理对象)
    @target()        限制目标对象的配置了指定的注解
    within            限制连接点匹配执行的类型
    @windin()        限定连接点带有匹配注解类型
    @annotation        限定带有指定注解的连接点
注:
(1).在spring中尝试使用AspectJ其他指示器时,将会抛出IllegalArgumentException异常
(2).execution指示器是唯一的执行匹配,其他的指示器都是用于限制所匹配的切点。

3.3.2 execution指示器说明

(1).使用execution指示器选择Instrument的buy方法,
(2).方法表达式以 * 号开始,标识不关心方法的返回值类型,指定了全限定类名和方法名。
(3).对于方法参数列表,使用 .. 标识切点选择任意的buy方法,无论该方法的入参是什么。

多个匹配之间我们可以使用链接符 &&、||、!来表示 “且”、“或”、“非”的关系。
但在使用 XML 文件配置时,这些符号有特殊的含义,使用 “and”、“or”、“not”来表示。

用例一:限定该切点仅匹配的包是 com.test.aop,可以使用

execution(* com.test.aop.Buy.buy(..)) && within(com.test.aop.*)

用例二:在切点中选择 bean,可以使用

execution(* com.test.aop.Buy.buy(..)) && bean(girl)

用例三:切面只会对 GirlBuy.java 这个类生效

@Aspect
@Component
public class BuyAspects {
    @Before("execution(* com.test.aop.Buy.buy(..)) && within(com.test.aop.*) && bean(girlBuy)") 
    public void buyAspect() {
        System.out.println("buy aspect");
    }
}

输出:
    boy buy
    buy aspect
    girl buy

备注:within(com.test.aop.*) && bean(girlBuy) 代表:com.test.aop.GirlBuy,其中girlBuy是该类的bean定义

3.4 注解声明 5 种通知类型

Before                前置通知,方法在目标方法调用前执行
AfterReturning        后置通知,方法在目标方法返回后执行
AfterThrowing        异常通知,方法在目标方法抛出异常后执行
Around                环绕通知,方法将目标方法封装起来
After                最终通知,方法在目标方法返回或异常后执行

用例:修改切面类

@Aspect
@Component
public class BuyAspects {

    @Before("execution(* com.test.aop.Buy.buy(..))") 
    public void before() {
        System.out.println("before");
    }

    @After("execution(* com.test.aop.Buy.buy(..))") 
    public void after() {
        System.out.println("after");
    }

    @AfterReturning("execution(* com.test.aop.Buy.buy(..))") 
    public void afterReturning() {
        System.out.println("afterReturning");
    }

    @AfterThrowing("execution(* com.test.aop.Buy.buy(..))") 
    public void afterThrowing() {
        System.out.println("afterThrowing");
    }

    @Around("execution(* com.test.aop.Buy.buy(..))") 
    public void around(ProceedingJoinPoint point) {
        try {
            System.out.println("around  AAA");
            point.proceed();
            System.out.println("around  BBB");
        } catch (Throwable e) {
            e.printStackTrace();
        }
    }
}

main 方法:
public static void main(String[] args) {
    ApplicationContext aContext =new AnnotationConfigApplicationContext(BuyConfig.class);
    BoyBuy boyBuy = aContext.getBean("boyBuy",BoyBuy.class);
    boyBuy.buy();
}

输出:
    around  AAA
    before
    boy buy
    around  BBB
    after
    afterReturning

备注:
    @Around 修饰的环绕通知类型,是将整个目标方法封装起来,
    在使用时,必须传入了 ProceedingJoinPoint类型的参数且需要调用 ProceedingJoinPoint 的 proceed() 方法。 

    若没有调用 该方法,原目标方法被阻塞调用,执行结果为 :
        around  AAA
        around  BBB
        after
        afterReturning

3.5 注解声明切点表达式

上面多个通知使用了相同的切点表达式,
对于像这样频繁出现的相同的表达式,我们可以使用 @Pointcut注解声明切点表达式,然后使用表达式,

修改代码如下:
    @Aspect
    @Component
    public class BuyAspects {

        /**
         * (1).声明了一个切点表达式,
         * (2).该方法 point 的内容并不重要,方法名也不重要,
         * (3).实际上它只是作为一个标识,供通知使用
         */
        @Pointcut("execution(* com.test.aop.Buy.buy(..))")
        public void point() {}

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

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

        @AfterReturning("point()") 
        public void afterReturning() {
            System.out.println("afterReturning");
        }

        @AfterThrowing("point()") 
        public void afterThrowing() {
            System.out.println("afterThrowing");
        }

        @Around("point()") 
        public void around(ProceedingJoinPoint point) {
            try {
                System.out.println("around  AAA");
                point.proceed();
                System.out.println("around  BBB");
            } catch (Throwable e) {
                e.printStackTrace();
            }
        }
    }

3.6 注解处理通知中的参数

下面我们给接口增加一个参数,表示购买所花的金钱。通过AOP 增强处理,如果女孩买衣服超过了 68 元,就可以赠送一双袜子

第一步:编写接口
public interface Buy {
    String buy(double price);
}

第二步:实现接口
@Component
public class BoyBuy implements Buy{
    public String buy(double price) {
        System.out.println(String.format("男孩花了%s买了游戏机",price));
        return "游戏机";
    }
}
@Component
public class GirlBuy implements Buy{
    public String buy(double price) {
        System.out.println(String.format("女孩花了%s买了鞋子",price));
        return "鞋子";
    }
}

第三步:配置文件,开启aop
@Configurable
@ComponentScan(basePackages="com.test.aop")
@EnableAspectJAutoProxy(proxyTargetClass=true)
public class BuyConfig {

}

第四步:编写切点
@Aspect
@Component
public class BuyAspects {

    /**
     * (1).声明了一个切点表达式,
     * (2).该方法 point 的内容并不重要,方法名也不重要,
     * (3).实际上它只是作为一个标识,供通知使用
     */
    @Pointcut("execution(* com.test.aop.Buy.buy(double)) && args(price) && bean(girlBuy)")
    public void point(double price) {}

    @Around("point(price)") 
    public String around(ProceedingJoinPoint point,double price) {
        try {
            point.proceed();
            if(price>60) {
                System.out.println("女孩买衣服超过了68元,赠送一双袜子");
            }    
            return "衣服和袜子";
        } catch (Throwable e) {
            e.printStackTrace();
        }
        return "衣服";
    }
}

第五步:main方法
    public static void main(String[] args) {
        ApplicationContext aContext =new AnnotationConfigApplicationContext(BuyConfig.class);
        BoyBuy boyBuy = aContext.getBean("boyBuy",BoyBuy.class);
        String boyString = boyBuy.buy(40);
        GirlBuy girlBuy = aContext.getBean("girlBuy",GirlBuy.class);
        String girlString = girlBuy.buy(70);

        System.out.println("男孩买到了"+boyString);
        System.out.println("女孩买到了"+girlString);
    }

输出:
    男孩花了40.0买了游戏机
    女孩花了70.0买了鞋子
    女孩买衣服超过了68元,赠送一双袜子
    男孩买到了游戏机
    女孩买到了衣服和袜子

备注:
    当不关心方法返回值的时候,编写切点指示器时候使用了 * , 
    不关心方法参数的时候,我们使用了 ..。

    需要传入参数且有返回值的时候,则需要使用对应的类型。
    在编写通知的时候,需要声明对应的返回值类型和参数类型。

3.7 注解配置织入的方式

Bean named 'girlBuy' is expected to be of type 'com.test.aop.GirlBuy' but was actually of type 'com.sun.proxy.$Proxy19'

注解 @EnableAspectJAutoProxy() 启用Spring AOP 的时候,proxyTargetClass 参数决定了代理的机制。

proxyTargetClass 为 false:,通过jdk的基于接口的方式,
    代理生成的是一个接口对象,将这个接口对象强制转换为实现该接口的一个类,自然就抛出了上述类型转换异常。

proxyTargetClass 为 true:使用 cglib 动态代理方式。缺点是拓展类的方法被final修饰时,无法进行织入。

文章标题:springAop使用

发布时间:2021-08-23, 20:11:40

最后更新:2021-08-23, 20:10:22