spring框架中的AOP

什么是AOP

AOP 为 Aspect Oriented Programming 的缩写,意为:面向切面编程,可通过运行期动态代理实现程序功能的统一维护的一种技术。AOP 是 Spring 框架中的一个重要内容。利用 AOP可以对业务逻辑的各个部分进行隔离,从而使得业务逻辑各部分之间的耦合度降低,提高程序的可重用性,同时提高了开发的效率。AOP 的底层就是采用动态代理模式实现的。采用了两种代理:JDK 的动态代理,CGLIB的动态代理。

AOP编程术语

切面(Aspect)

切面泛指交叉业务逻辑。事务处理、日志处理就可以理解为切面。常用的切面是通知(Advice)。实际就是对主业务逻辑的一种增强。

连接点(JoinPoint)

连接点指可以被切面植入的具体方法。通常业务接口中的方法均为连接点。

切入点(Pointcut)

切入点指声明的一个或多个连接点的集合。通过切入点指定一组方法。被final修饰的方法是不能作为连接点与切入点的,因为最终的是不能被修改的,不能被增强的。

目标对象(Target)

目标对象指将要被增强的对象。即包含主业务逻辑的类的对象。

通知(Advice)

通知表示切面的执行时间,Advice也叫增强。通知定义了增强代码切入到目标代码的时间点,是目标方法执行之前还是执行之后。通知类型不同,切入时间不同。

AspectJ对AOP的实现

对于 AOP 这种编程思想,很多框架都进行了实现。Spring 就是其中之一,可以完成面向切面编程。然而,AspectJ 也实现了 AOP 的功能,且其实现方式更为简捷,使用更为方便,而且还支持注解式开发。所以,Spring 又将 AspectJ 的对于 AOP 的实现也引入到了自己的框架中。在 Spring 中使用 AOP 开发时,一般使用 AspectJ 的实现方式。

AspectJ的通知类型

AspectJ中常用的通知有五种类型:
(1)前置通知
(2)后置通知
(3)环绕通知
(4)异常通知
(5)最终通知

AspectJ的切入点表达式

AspectJ 定义了专门的表达式用于指定切入点。表达式的原型是:
execution(modifiers-pattern? ret-type-pattern declaring-type-pattern?name-pattern(param-pattern) throws-pattern?)

其中:
modifiers-pattern 访问权限类型
ret-type-pattern 返回值类型
declaring-type-pattern 包名类名
name-pattern(param-pattern) 方法名(参数类型和参数个数)
throws-pattern 抛出异常类型
?表示可选的部分

在表达式中可以使用符号来代替一些字符:
星号(*):表示0至多个任意字符
两点(. .):用在方法参数中,表示任意多个参数;用在包名后,表示当前包及其子包路径

例如:
execution(public * (…))
指定切入点为:任意公共方法
execution(
set*(…))
指定切入点为:任何一个以“set”开始的方法。
execution(* com.xyz.service..(…))
指定切入点为:定义在 service 包里的任意类的任意方法。

AspectJ的开发环境

(1)maven依赖
使用AspectJ需要加入maven依赖

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

(2)引入AOP约束
在 AspectJ 实现 AOP 时,要引入 AOP 的约束。配置文件中使用的 AOP 约束中的标签,均是 AspectJ 框架使用的,而非 Spring 框架本身在实现 AOP 时使用的。AspectJ 对于 AOP 的实现有注解和配置文件两种方式,常用是注解方式。

AspectJ基于注解的AOP实现

定义目标类

public interface SomeService {

    void doSome(String name, Integer age);
    
    String doOther(String name, Integer age);

    String doFirst(String name, Integer age);
}
public class SomeServiceImpl implements SomeService {

    @Override
    public void doSome(String name, Integer age) {
        System.out.println("目标类的doSome()方法执行");
    }

    @Override
    public String doOther(String name, Integer age) {
        System.out.println("目标类的doOther()方法执行");
        return "1234";
    }

    @Override
    public String doFirst(String name, Integer age) {
        System.out.println("目标类的doFirst()方法执行");
        return "doFirst";
    }
@Aspect 切面类注解

该注解表示当前类是切面类,出现在类定义的上面。

@Before 前置通知注解

前置通知注解用来表示前置通知方法。该注解有一个value属性,属性值就是切入点表达式,表示切面功能执行的位置。
前置通知方法是用来实现切面功能的,该方法可以没有参数,但是如果有参数的话,参数必须有JoinPoint,且必须在参数列表的第一位。该方法会在目标方法执行之前先执行。
JoinPoint指需要加入切面功能的那个目标方法,即切入点表达式指向的那个方法。他的作用是可以获取到目标方法执行时的信息,例如方法的名称、实参等。

切面类:

@Aspect
public class MyAspect {

    @Before(value = "execution(* *..SomeServiceImpl.doSome(..))")
    public void myBefore(JoinPoint jp){
        //获取方法的完整定义
        System.out.println("方法的签名(定义)=" + jp.getSignature());
        //获取方法的名称
        System.out.println("方法的名称=" + jp.getSignature().getName());
        //获取方法的实参
        Object args[] = jp.getArgs();
        for(Object obj : args){
            System.out.println("参数=" + obj);
        }
        System.out.println("前置通知,切面功能:在目标方法执行前输出执行时的时间" + new Date());
    }
}

测试类:

public class MyTest01 {
    @Test
    public void test01(){
        String config = "applicationContext.xml";
        ApplicationContext ctx = new ClassPathXmlApplicationContext(config);
        //从容器中获取目标对象,这里得到的其实是代理对象
        SomeService proxy = (SomeService) ctx.getBean("someService");
        //将得到的代理对象进行输出
        System.out.println("proxy:" + proxy.getClass().getName());
        //通过代理对象执行目标方法,实现方法执行时的功能增强
        proxy.doSome("zhangsan", 30);
    }
}

测试结果:
在这里插入图片描述

@AfterReturning 后置通知注解

后置通知注解用来表示后置通知方法。该注解有两个属性,一个是value属性,属性值是切入点表达式(这点与@Before相同);另一个是returning属性,属性值是一个自定义的变量,表示目标方法的返回值,自定义变量名必须与后置通知方法的形参名相同。
后置通知方法与前置通知方法相同,也是用来实现切面功能的。该方法有一个参数,推荐使用Object类型的参数。该方法在目标方法执行之后再执行,可以获取到目标方法的返回值,因此可以根据返回值做一些处理。

切面类:

@Aspect
public class MyAspect {

    @AfterReturning(value="execution(* *..SomeServiceImpl.doOther(..))",
            returning = "res")
    public void myAfterReturning(Object res){
        //Object res:目标方法执行之后的返回值
        System.out.println("后置通知:在目标方法执行之后执行,获取的返回值是:" + res);
        //根据返回值做切面处的功能处理
        if(res != null){
            res = 111;
            System.out.println("res=" + res);
        }
    }
}

测试类:

public class MyTest02 {
    @Test
    public void test01(){
        String config = "applicationContext.xml";
        ApplicationContext ctx = new ClassPathXmlApplicationContext(config);
        //从容器中获取目标对象,这里得到的其实是代理对象
        SomeService proxy = (SomeService) ctx.getBean("someService");
        //将得到的代理对象进行输出
        System.out.println("proxy:" + proxy.getClass().getName());
        //通过代理对象执行目标方法,实现方法执行时的功能增强
        String str = proxy.doOther("lisi", 20);

        System.out.println("str=" + str);
    }
}

测试结果:

从测试结果我们可以看出,虽然我们对返回值进行了更改,但是并没有影响最后调用的结果

@Around 环绕通知注解

环绕通知注解用来表示环绕通知方法。该注解只有一个value属性,属性值是切入点表达式。该注解有以下特点:
(1)是功能最强的通知
(2)在目标方法执行前后都能增强功能
(3)可以控制目标方法是否执行
(4)可以修改原来目标方法的执行结果,并影响最后的调用结果
环绕通知方法要求必须有返回值,推荐使用Object类型。并且该方法可以包含一个ProceedingJoinPoint类型的参数,该参数就等同于jdk动态代理中invoke()方法中的Method,作用是执行目标方法。

切面类:

@Aspect
public class MyAspect {

    @Around(value = "execution(* *..SomeServiceImpl.doFirst(..))")
    public Object myAround(ProceedingJoinPoint pjp) throws Throwable {
        Object res = null;

        //通过ProceedingJoinPoint得到目标方法的参数
        String name = null;
        Object args[] = pjp.getArgs();
        if(args != null && args.length > 1){
            Object arg = args[0];
            name = (String) arg;
        }

        //在目标方法之前增强输出时间功能
        System.out.println("环绕通知:在目标方法之前输出执行时间=" + new Date());

        //通过目标方法的参数,控制目标方法是否执行
        if("zhangsan".equals(name)){
            //调用执行目标方法
            res = pjp.proceed();//等同于jdk动态代理中的method.invoke();Object res = doFirst();
        }

        //在目标方法之后增强提交事务功能
        System.out.println("环绕通知:在目标方法之后提交事务,res=" + res);

        //修改原来的目标方法的执行结果,影响最后的调用结果
        if(res != null){
            res = "aaaaa";
            System.out.println("res=" + res);
        }
        return res;
    }
}

测试类:

public class MyTest03 {
    @Test
    public void test01(){
        String config = "applicationContext.xml";
        ApplicationContext ctx = new ClassPathXmlApplicationContext(config);
        //从容器中获取目标对象,这里得到的其实是代理对象
        SomeService proxy = (SomeService) ctx.getBean("someService");
        //将得到的代理对象进行输出
        System.out.println("proxy:" + proxy.getClass().getName());
        //通过代理对象执行目标方法,实现方法执行时的功能增强
        String str = proxy.doFirst("zhangsan", 20);//实际上执行的是myAround()

        System.out.println("str=" + str);
    }
}

测试结果:
在这里插入图片描述
从测试结果我们可以看出,我们在切面类中对返回值进行了修改,最终影响了最后的调用结果

Pointcut 定义切入点的注解

当较多的通知增强方法使用相同的 execution 切入点表达式时,编写、维护均较为麻烦。AspectJ 提供了@Pointcut 注解,用于定义 execution 切入点表达式。其用法是,将@Pointcut 注解在一个方法之上,以后所有的 execution 的 value 属性值均可使用该方法名作为切入点。代表的就是@Pointcut 定义的切入点。这个使用@Pointcut 注解的方法一般使用 private 的标识方法,即没有实际作用的方法

@Before(value = "mypt()")
public void myAfter(){
	System.out.println("前置通知");
}
			
@Pointcut(value = "execution(* *..SomeServiceImpl.doSome(..))")
private void mypt(){
	//无需代码
}
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值