SpringAOP---面向切面编程详解

上一篇文章中,我们对于代理模式有了基础了解:https://blog.csdn.net/szy2333/article/details/89338412

从没有使用代理模式到静态代理再到动态代理,我们对于代码已经有了大大的简化,但这些都是在可以不需要spring的情况下进行的。但其实,如果使用spring对其进行管理,代码还会进一步简化,并且,可插拔性也会更高

什么是SpringAOP?

首先我们需要先了解一些名词

代理:proxy   代理就是目标对象的加强。Spring中AOP既可以是JDK动态代理(基于接口),也可以是CGLIB代理(基于子类)

目标:target   目标对象,即被代理的对象,一般是方法的调用

通知:advice   包含了可重用的代码(例如事务代码),并调用目标

切点:pointcut   把通知和目标结合,是带有通知的连接点,它定义的是一种匹配规则,在程序中主要体现为书写切点表达式

切面:ascept   通常是一个类,里面定义切点和通知,即   切面 = 通知 + 切点   

而AOP就是  ascept oriented programming ---> 面向切面编程  --->  将重复代码取出来变成通知类并与目标类结合(和代理差不多)

接下来,我们用代码演示AOP的过程,依然是使用对于事务代码块简化的例子

首先,我们需要加入相关的依赖,之前文章中添加过的不再赘述,这次需要加入两个关于aop的相关的依赖

 <!--spring-aop-->
        <dependency>
            <groupId>org.springframework</groupId>
            <artifactId>spring-aop</artifactId>
            <version>4.3.18.RELEASE</version>
        </dependency>

        <!--第三方 aop依赖 aspectj-->
        <dependency>
            <groupId>org.aspectj</groupId>
            <artifactId>aspectjweaver</artifactId>
            <version>1.8.9</version>
        </dependency>

        <dependency>
            <groupId>org.aspectj</groupId>
            <artifactId>aspectjrt</artifactId>
            <version>1.8.9</version>
        </dependency>

UserService类

package spring.service;
public interface UserService {
    public void biz1();
    public void biz1(int i);
    public void biz1(int a, String b);
    public void biz2();
    public void abc();
}

UserServiceTarget类

package spring.service;

public class UserServiceTarget implements UserService {
    @Override
    public void biz1() {
        System.out.println("sql1");
        System.out.println("sql2");
    }

    @Override
    public void biz1(int i) {
        System.out.println("i = " +i);
    }

    @Override
    public void biz1(int a, String b) {
        System.out.println(b + " ---> " + a);
    }

    @Override
    public void biz2() {
        System.out.println("sql3");
        System.out.println("sql4");
        System.out.println("sql5");
    }

    @Override
    public void abc() {
        System.out.println("sql6");
        System.out.println("sql7");
    }
}

通知类

package spring.service;

import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;

@Aspect
public class TransactionAdvice {
    //@Around是环绕通知,within决定了这个通知方法会和哪些目标方法相结合
    @Around("within(spring.service.UserServiceTarget)")
    //该方法称为通用方法,里面包含了重用逻辑,以及负责调用的方法,参数必须是ProceedingJoinPoint
    public Object tansaction(ProceedingJoinPoint pjp) {
        System.out.println("开始事务");
        Object obj = null;
        try {
            //调用目标,使用proceed方法
            obj = pjp.proceed();
            System.out.println("提交事务");
        } catch (Throwable e) {
            System.out.println("回滚事务");
        }
        return obj;
    }
}

最后,我们还需要利用spring中的控制反转,让容器来管理方法调用

<?xml version="1.0" encoding="utf-8" ?>

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

    <bean id="userService" class="spring.service.UserService">
    </bean>

    <bean id="advice" class="spring.service.TransactionAdvice">
    </bean>

    <!--启用aop注解-->
    <aop:aspectj-autoproxy/>
</beans>

执行结果

2019041719435417.png

到这里,我们使用AOP就完成了最基础的内容啦,我们发现,步骤和原理其实和代理是一样的,只不过又将一部分内容交给spring并让其实现了控制反转。那么既然是代理,getBean的时候,返回的是接口或者目标类型还是其它什么类型呢?接下来,我们对于getBean返回的结果进行一个验证
验证代码

@Test
    public void test1() {
        ClassPathXmlApplicationContext context = new ClassPathXmlApplicationContext("spring.xml");
        //尽量使用接口类型,可扩展性强
        UserService service = context.getBean(UserService.class);
        service.biz1();
        System.out.println(service.getClass());
    }

验证结果

通过调用.getClass()方法,我们明确的看到它返回的不是target类型,而是一个代理类型,这个代理类就是spring容器生成的

其底层产生代理的技术就是:动态代理技术

20190417195537666.png

工作流程和动态代理差不多

首先spring容器返回一个代理对象,代理对象调用biz1() 的时候,就会在内部去调用通知TransactionAdvice对象,然后间接执行里面的Transaction方法,然后该方法先进行判断,看方法和目标是否符合,符合了才会去执行(即调用的biz1()确实是和@Around注解里面的东西是相符合的)

接下来,我们对于上面的几个名词进行一下深入的了解

切点  pointcut 

在@Around注解括号中的东西,是具体定位到方法的连接点,比上面代码中的“within(spring.service.UserServiceTarget) ”

有这几种类型

1.within(包名.类名)   这个类中的所有方法执行时,都会应用通知方法

这种方式就是上面所演示的方式,但是这种匹配规则范围太广,有的时候,我们希望进行更细致的匹配,而不是对于类中所有的方法都进行匹配,这样这个就不适用了

2.execution(访问修饰符  返回值类型   包名.类名   方法名(参数类型))    

这种方式就很好的解决了within中存在的问题,能够根据需要的方法时才启用事务

注意:" * " 可以匹配任意类型,“ . . ” 可以匹配任意类型的参数

我们用代码进行一下测试,在执行上述中biz1(int a, String b)时,才需要启用事务

通知类代码

import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;

@Aspect
public class TransactionAdvice {
    //@Around是环绕通知,within决定了这个通知方法会和哪些目标方法相结合
    @Around("execution(public void UserServiceTarget.biz1(int, String))")
    //该方法称为通用方法,里面包含了重用逻辑,以及负责调用的方法,参数必须是ProceedingJoinPoint
    public Object tansaction(ProceedingJoinPoint pjp) {
        System.out.println("开始事务");
        Object obj = null;
        try {
            //调用目标,使用proceed方法
            obj = pjp.proceed();
            System.out.println("提交事务");
        } catch (Throwable e) {
            System.out.println("回滚事务");
        }
        return obj;
    }
}

测试代码

@Test
    public void test1() {
        ClassPathXmlApplicationContext context = new ClassPathXmlApplicationContext("spring.xml");
        //尽量使用接口类型,可扩展性强
        UserService service = context.getBean(UserService.class);
        service.biz1(98, "成绩:");    //测试1
        service.biz1();                     //测试2
        System.out.println(service.getClass());
    }

测试结果

当进行测试1时,执行结果如左图,进行测试2时,结果如右图

20190417204922156.png                    20190417204959113.png

3.@annotation(包名.注解名)

   它是根据方法上的注解进行匹配,如果有注解,则匹配,是最灵活的!

代码测试:

UserServiceTarget类中的biz2()方法上面加上注解

package spring.service;

import org.springframework.transaction.annotation.Transactional;

public class UserServiceTarget implements UserService {
    @Transactional
    @Override
    public void biz2() {
        System.out.println("sql3");
        System.out.println("sql4");
        System.out.println("sql5");
    }

    @Override
    public void abc() {
        System.out.println("sql6");
        System.out.println("sql7");
    }
}

通知类---也需要改变注解

package spring.service;

import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
@Aspect
public class TransactionAdvice {
    //@Around是环绕通知,within决定了这个通知方法会和哪些目标方法相结合
    @Around("@annotation(org.springframework.transaction.annotation.Transactional)")
    //该方法称为通用方法,里面包含了重用逻辑,以及负责调用的方法,参数必须是ProceedingJoinPoint
    public Object tansaction(ProceedingJoinPoint pjp) {
        System.out.println("开始事务");
        Object obj = null;
        try {
            //调用目标,使用proceed方法
            obj = pjp.proceed();
            System.out.println("提交事务");
        } catch (Throwable e) {
            System.out.println("回滚事务");
        }
        return obj;
    }
}

测试代码

@Test
    public void test1() {
        ClassPathXmlApplicationContext context = new ClassPathXmlApplicationContext("spring.xml");
        //尽量使用接口类型,可扩展性强
        UserService service = context.getBean(UserService.class);
        service.biz2();    //测试1
        service.abc();                     //测试2
        System.out.println(service.getClass());
    }

测试结果

20190417223938342.png

通知类型

环绕通知:@Around(切点表达式)加在通知方法上(是功能最全的)     前面的例子使用的是这种通知

前置通知:@Before    通知代码只会出现在目标方法之前

正常结束通知:@AfterRunning   在目标方法正常结束后,调用通知方法

异常通知:@AfterThrowing   在目标方法出现异常后,调用通知方法

结束通知:@After   在目标方法结束后(正常或异常),总会调用的通知

以前置通知为例,进行代码测试示范

在通知类前面加注解

@Aspect
public class Advice2 {
    @Before("within(spring.service.UserServiceTarget)")
    public void advice() {
        System.out.println("通知类---在目标方法之前调用");
    }
}

测试代码

@Test
    public void test2() {
        ClassPathXmlApplicationContext context = new ClassPathXmlApplicationContext("spring.xml");
        UserService bean = context.getBean(UserService.class);
        bean.biz1();
    }

测试结果

20190417232214202.png

前面所有的实现方式,我们都先要目标类实现一个父类接口,那么如果不实现接口,而是直接使用目标类,spring支持这种方式吗?接下来,我们测验是否能使用

无接口方式实现AOP

首先,创建一个不用实现父类接口的新目标类型OrderService

package spring.service;

public class OrderService {
    public void ord1() {
        System.out.println("订单交易完成");
    }

    public void ord2() {
        System.out.println("订单已删除");
    }
}

接下来,创建新的通知类Advice3,使用@Before的方式

package spring.service;

import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Before;

@Aspect
public class Advice3 {
    @Before("within(spring.service.OrderService)")
    public void transaction() {
        System.out.println("前置通知...");
    }
}

然后,将这两个类添加到spring.xml中,让spring容器对其进行管理

<!--没有实现接口的目标类-->
    <bean id="orderService" class="spring.service.OrderService"></bean>

    <bean id="advice3" class="spring.service.Advice3"></bean>

测试文件

@Test
    public void test3() {
        ClassPathXmlApplicationContext context = new ClassPathXmlApplicationContext("spring.xml");
        OrderService service = context.getBean(OrderService.class);
        service.ord1();
    }

测试结果
2019041810260285.png

从结果我们可以看出,即使没有实现父类接口,却仍然可以使用AOP实现代理的目的,那么是之前讲的东西都不成立吗?

想要验证,我们首先看一下,通过getBean得到的service是什么类型

测试代码

 @Test
    public void test3() {
        ClassPathXmlApplicationContext context = new ClassPathXmlApplicationContext("spring.xml");
        OrderService service = context.getBean(OrderService.class);
        service.ord1();
        System.out.println("service的真正类型是 : " + service.getClass());
    }

测试结果

20190418103220834.png

通过测验结果我可以看到,它的真正类型和之前测试得到的代理类型不一样,而是包名.类名$$EnhancerBySpringCGLIB$$字符串

答案是:其实在spring中除了之前的jdk代理和动态代理除外,还有其他代理类型,比如CGLIB代理,这种代理类型不需要目标实现特殊的接口,而我们这次就是使用的这种代理类型来实现的

那么没有实现接口,我们为什么能把得到的service当做OrderService来用呢?两者之间有什么关系?

其实就是底层生成了一个子类代理对象,然后把它当做父类变量来使用,这对于程序员来说,是一个非常简洁的方式,因为不需要再刻意去实现一个父类接口了

java中生成代理的常见技术有哪些?

jdk的动态代理      只能针对接口实现代理

cglib动态代理       既可以针对接口实现代理,也可以生成子类充当父类的代理

javasist动态代理      是struct2,hibernate框架中常用的代理

到那时值得注意的是:在spring中,并不是cglib代理的适用性更加广泛,就会直接使用这个代理,而是优先使用jdk动态代理,因为在jdk高版本中,jdk动态代理技术相比于cglib的性能更优(虽然cglib也不差),所以spring会优先考虑jdk动态代理,如果没有实现父类接口,才会退而求其次去使用cglib动态代理

 

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值