spring 框架 AOP 面向切面编程原理及应用

面向切面编程

在传统的编程业务逻辑处理代码时,通常会习惯性地做几件事:日志记录、事务控制及权限控制等,然后才是编写核心的业务逻辑处理代码,完成这几件事之后会发现代码量巨大,但是真正用于核心业务的代码很少,因此引入了切面编程,它把所有共有的代码全部抽出,放置在一个集中的地方进行管理,让后集体运行时,再由容器动态织入这些共有的代码,这样不但可以提高效率,而且也会使代码变得更加简洁。

AOP (aspect 切面 oriented 面向  programming 编程)

  • 切面 aspect = 通知 adivce + 切点 pointcut 
  • 通知:是一个方法,其中包含了重复的逻辑(计时,事务)
  • 切点:是一种匹配条件, 与条件相符合的目标方法,才会应用通知方法

使用步骤

1) 添加 maven 依赖

<dependency>
    <groupId>org.springframework</groupId>
    <artifactId>spring-context</artifactId>
    <version>4.3.22.RELEASE</version>
</dependency>
<dependency>
    <groupId>org.aspectj</groupId>
    <artifactId>aspectjrt</artifactId>
    <version>1.8.13</version>
</dependency>
<dependency>
    <groupId>org.aspectj</groupId>
    <artifactId>aspectjweaver</artifactId>
    <version>1.8.13</version>
</dependency>
```

2) 编写切面类

@Aspect
public class 切面类 {

    @Around("切点表达式") // 切点表达式用来匹配目标和通知
    public Object 通知方法(ProceedingJoinPoint pjp) {
        // 写一些重复的逻辑, 计时, 事务控制,权限控制
        // 调用目标 , 其中 result 是目标方法返回的结果
        Object result = pjp.proceed();
        return result;
    }

}

3) 把切面类和目标类都交给 spring 容器管理,把以下拷入spring.xml中,把包名补全就好了

<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
       xmlns:context="http://www.springframework.org/schema/context"
       xmlns:aop="http://www.springframework.org/schema/aop"
       xmlns:tx="http://www.springframework.org/schema/tx"
       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/aop http://www.springframework.org/schema/aop/spring-aop.xsd
        http://www.springframework.org/schema/tx http://www.springframework.org/schema/tx/spring-tx.xsd">
    <!-- 启用切面编程的相关注解,例如: @Aspect, @Around, 还提供了自动产生代理类的功能-->
    <aop:aspectj-autoproxy/>
    <!--填写需要扫描的包,配合注解使用的,如:service,aspect包,可以用逗号分隔写多个包名-->
    <context:component-scan base-package="包名"/>

</beans>

4) 使用切面

从容器 getBean 根据接口获取,容器返回代理对象,代理对象中进行切点匹配,匹配到了执行通知,通知内部间接调用目标

// 1. 偷梁换柱, spring 容器返回的是一个代理对象
UserService userService = context.getBean(UserService.class);

System.out.println(userService.getClass());

// 2. 调用的是代理对象的 a 方法
userService.a();

切点表达式

within 匹配类中的所有方法(粒度粗)

语法:  within(包名.类名)

如下,有两个类,都在service包中,用within可以匹配:

UserServiceTarget
    insert
    update
OrderServiceTarget
    a
    b
    c
within(service.*ServiceTarget)  // 其中 * 表示所有,表示两个类中的方法都可以匹配到,因为后缀相同

execution 表达式 可以精确到类中的每个方法 (粒度细)

语法:  execution(访问修饰符 返回值 包名.类名.方法名(参数信息))

  • 返回值的位置写 * 表示返回值得类型是任意的
  • 类名 写 * 代表任意的类
  • 方法名中出现 *, 例如下面的表达式会匹配所有以 find 开头的方法

@Around("execution(public * service.UserService.*a())")

但是 * 写在参数位置,只能匹配一个任意类型的参数

@Around("execution(public * service.UserService.a(*))")

a(int) 匹配
a(long) 匹配

a(int, int) 不匹配
a() 不匹配

符号  ..  写在参数位置, 匹配任意个数和任意类型的参数

@Around("execution(public * service.UserService.a(..))")

a(int) 匹配
a(long) 匹配
a(int, int) 匹配
a() 匹配

@annotation
语法:   @annotation(包名.注解名)

看执行方法上有没有这个注解,有的话就算匹配

通知类型

类型:

  • @Around - 环绕通知 (功能最强大, 可以自由定义通知代码的位置)
  • @Before - 前置通知
  • @After - 后置通知
  • @AfterReturning - 正常返回通知
  • @AfterThrowing - 异常返回通知

这几种通知实际上对应的就是通知代码相对于目标代码的位置,如下:

@Before - 前置通知
try {
    目标方法
    @AfterReturning - 正常返回通知
} catch(Exception e) {
    @AfterThrowing - 异常返回通知
} finally {
    @After - 后置通知
}

面向切面的应用

1) spring 中的事务管理(详情请观看:https://blog.csdn.net/grey_mouse/article/details/87738891

@Transactional
public void deleteByIds(List<Integer> ids) {

}
// 代理对象.deleteByIds, 代理对象中与@Transactional进行匹配,发现方法上有这个注解,就调用事务通知, 继续调用目标方法
// TransactionInterceptor(跟事务相关的通知代码)

2) 统一的权限控制

3) 缓存的控制

4) 统一的日志记录

spring 中如果目标类没有实现接口

  •  如果目标对象实现了接口:底层 spring 会调用 jdk的动态代理技术 来生成代理, 要求代理对象和目标对象实现共同的接口
  •  如果目标对象没有实现接口:底层 spring 会调用 cglib 代理技术 来生成代理,生成了一个子类作为代理对象

spring 中的单元测试

加入依赖后(如果不知道怎么添加依赖,请关注:https://blog.csdn.net/grey_mouse/article/details/85345278),在测试类上写上这两个标签,加完之后就可以使用单元测试了。

@RunWith(SpringJUnit4ClassRunner.class) // 让单元测试类 支持 spring
@ContextConfiguration("classpath:spring.xml") // 指定spring配置文件的位置, 让测试类运行时根据此文件创建 spring 容器

代码演示 

1、切面编程演示

切面类

import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Before;
import org.springframework.stereotype.Component;

@Aspect//切面注解
@Component
public class OrderAspect {
    //前置通知,在代理调用方法前被调用,witin里填写的是需要匹配的包下的类,这样可以匹配类中的方法
    @Before("within(service.OrderService)")
    public void before() {
        System.out.println("在order service 之前被调用...");
    }
    //后置通知,在代理调用方法后被调用
    @After("within(service.OrderService)")
    public void after() {
        System.out.println("在order service 之后被调用...");
    }
}

目标类

import org.springframework.stereotype.Service;

@Service
public class OrderService {

    public void insert() {
        System.out.println("执行 OrderService insert()");
    }
}

测试类

import org.springframework.context.support.ClassPathXmlApplicationContext;
import service.OrderService;
import service.UserService;

public class TestAop {

    public static void main(String[] args) {
        //得到spring容器
        ClassPathXmlApplicationContext context = new ClassPathXmlApplicationContext("spring.xml");

        // 1. 偷梁换柱, spring 容器返回的是一个代理对象
        OrderService orderService = context.getBean(OrderService.class);
        // 调用的是代理对象的方法
        orderService.insert();

    }
}

结果

//before注解的在代理对象调用的方法前被调用,而after在之后被调用
在order service 之前被调用...
执行 OrderService insert()
在order service 之后被调用...

这里底层调用方法是通过代理类来调用的,这一过程看不到,已经被封装了,只用@Aspect标签,就可以表示这一过程了,也就是动态代理的过程啦,这不过这一过程被spring框架包揽了,我们需要写出业务逻辑即可,用少量的代码生成代理对象。

总结

spring把动态代理这一过程封装,使代码变得更加简洁,少量的配置即可高效的完成业务逻辑,大大减轻了我们的工作量,是我们的好福音呀。

猜你喜欢

转载自blog.csdn.net/grey_mouse/article/details/87740633