Spring AOP 功能使用详解

前言

AOP 既熟悉又陌生,了解过 Spring 人的都知道 AOP 的概念,即面向切面编程,可以用来管理一些和主业务无关的周边业务,如日志记录,事务管理等;陌生是因为在工作中基本没有使用过,AOP 的相关概念也是云里雾里;最近在看 Spring 的相关源码,所以还是先来捋一捋 Spring 中 AOP 的一个用法。

相关概念

在学习 Spring AOP 的用法之前,先来看看 AOP 的相关概念,

Spring AOP 的详细介绍,请参考官网 https://docs.spring.io/spring/docs/2.5.x/reference/aop.html

  1. Join point :连接点,表示程序执行期间的一个点,在 Spring AOP 表示的就是一个方法,即一个方法可以看作是一个 Join point

  2. pointcut :切点,就是与连接点匹配的谓词,什么意思呢,就是需要执行 Advice 的连接点就是切点

  3. Advice :增强,在连接点执行的操作,分为前置、后置、异常、最终、环绕增强五种

  4. Aspect :切面,由 pointcut 和 Advice 组成,可以简单的认为 @Aspect 注解的类就是一个切面

  5. Target object :目标对象,即 织入 advice 的目标对象

  6. AOP proxy :代理类,在 Spring AOP 中, 一个 AOP 代理是一个 JDK 动态代理对象或 CGLIB 代理对象

  7. `Weaving` :织入,将 Aspect 应用到目标对象中去

注:上述几个概念中,比较容易混淆的是 Join point   和  pointcut,可以这么来理解,在 Spring AOP 中,所有的可执行方法都是 Join point,所有的 Join point 都可以植入 Advice;而 pointcut 可以看作是一种描述信息,它修饰的是 Join point,用来确认在哪些 Join point 上执行 Advice,

栗子

在了解了 AOP 的概念之后,接下来就来看看如何使用  Spring Aop

  1. 要想使用 Spring  AOP ,首先先得在 Spring 配置文件中配置如下标签:

1

该标签有两个属性, expose-proxy 和 proxy-target-class ,默认值都为 false

expose-proxy : 是否需要将当前的代理对象使用 ThreadLocal 进行保存,这是什么意思呢,例如 Aop 需要对某个接口下的所有方法进行拦截,但是有些方法在内部进行自我调用,如下所示:

1    public void test_1()
2    {   
3        this.test_2();
4    }
5    public void test_2()
6    {
7    }

调用 test_1,此时 test_2 将不会被拦截进行增强,因为调用的是 AOP 代理对象而不是当前对象,而 在 test_1 方法内部使用的是 this 进行调用,所以 test_2 将不会被拦截增强,所以该属性 expose-proxy  就是用来解决这个问题的,即 AOP 代理的获取。

proxy-target-class :是否使用 CGLIB 进行代理,因为 Spring AOP 的底层技术就是使用的是动态代理,分为 JDK 代理 和 CGLIB 代理,该属性的默认值为 false,表示 AOP 底层默认使用的使用 JDK 代理,当需要代理的类没有实现任何接口的时候才会使用 CGLIB 进行代理,如果想都是用 CGLIB 进行代理,可以把该属性设置为 true 即可。

  1. 定义需要 aop 拦截的方法,模拟一个 User 的增删改操作:

接口:

1public interface IUserService {
2    void add(User user);
3    User query(String name);
4    List qyertAll();
5    void delete(String name);
6    void update(User user);
7}

接口实现:

 1@Service("userServiceImpl")
 2public class UserServiceImpl implements IUserService {
 3
 4    @Override
 5    public void add(User user) {
 6        System.out.println("添加用户成功,user=" + user);
 7    }
 8
 9    @Override
10    public User query(String name) {
11        System.out.println("根据name查询用户成功");
12        User user = new User(name, 20, 1, 1000, "java");
13        return user;
14    }
15
16    @Override
17    public List qyertAll() {
18        List users = new ArrayList<>(2);
19        users.add(new User("zhangsan", 20, 1, 1000, "java"));
20        users.add(new User("lisi", 25, 0, 2000, "Python"));
21        System.out.println("查询所有用户成功, users = " + users);
22        return users;
23    }
24
25    @Override
26    public void delete(String name) {
27        System.out.println("根据name删除用户成功, name = " + name);
28    }
29
30    @Override
31    public void update(User user) {
32        System.out.println("更新用户成功, user = " + user);
33    }
34}

.

  1. 定义 AOP 切面

在 Spring AOP 中,使用 @Aspect  注解标识的类就是一个切面,然后在切面中定义切点(pointcut)和 增强(advice):

3.1 前置增强,@Before(),在目标方法执行之前执行

 1@Component
 2@Aspect
 3public class UserAspectj {
 4
 5    // 在方法执行之前执行
 6    @Before("execution(* main.tsmyk.mybeans.inf.IUserService.add(..))")
 7    public void before_1(){
 8        System.out.println("log: 在 add 方法之前执行....");
 9    }
10}

上述的方法 before_1() 是对接口的 add() 方法进行 前置增强,即在 add() 方法执行之前执行,
测试:

 1@RunWith(SpringJUnit4ClassRunner.class)
 2@ContextConfiguration("/resources/myspring.xml")
 3public class TestBean {
 4
 5    @Autowired
 6    private IUserService userServiceImpl;
 7
 8    @Test
 9    public void testAdd() {
10        User user = new User("zhangsan", 20, 1, 1000, "java");
11        userServiceImpl.add(user);
12    }
13}
14// 结果:
15// log: 在 add 方法之前执行....
16// 添加用户成功,user=User{name='zhangsan', age=20, sex=1, money=1000.0, job='java'}

如果想要获取目标方法执行的参数等信息呢,我们可在 切点的方法中添参数 JoinPoint ,通过它了获取目标对象的相关信息:

1    @Before("execution(* main.tsmyk.mybeans.inf.IUserService.add(..))")
2    public void before_2(JoinPoint joinPoint){
3        Object[] args = joinPoint.getArgs();
4        User user = null;
5        if(args[0].getClass() == User.class){
6            user = (User) args[0];
7        }
8        System.out.println("log: 在 add 方法之前执行, 方法参数 = " + user);
9    }

重新执行上述测试代码,结果如下:

1log: 在 add 方法之前执行, 方法参数 = User{name='zhangsan', age=20, sex=1, money=1000.0, job='java'}
2添加用户成功,user=User{name='zhangsan', age=20, sex=1, money=1000.0, job='java'}

3.2 后置增强,@After(),在目标方法执行之后执行,无论是正常退出还是抛异常,都会执行

1    // 在方法执行之后执行
2    @After("execution(* main.tsmyk.mybeans.inf.IUserService.add(..))")
3    public void after_1(){
4        System.out.println("log: 在 add 方法之后执行....");
5    }

执行 3.1 的测试代码,结果如下:

1添加用户成功,user=User{name='zhangsan', age=20, sex=1, money=1000.0, job='java'}
2log: ==== 方法执行之后 =====

3.3 返回增强,@AfterReturning(),在目标方法正常返回后执行,出现异常则不会执行,可以获取到返回值:

1@AfterReturning(pointcut="execution(* main.tsmyk.mybeans.inf.IUserService.query(..))", returning="object")
2public void after_return(Object object){
3    System.out.println("在 query 方法返回后执行, 返回值= " + object);
4}

测试:

1@Test
2public void testQuery() {
3    userServiceImpl.query("zhangsan");
4}
5// 结果:
6// 根据name查询用户成功
7// 在 query 方法返回后执行, 返回值= User{name='zhangsan', age=20, sex=1, money=1000.0, job='java'}

当一个方法同时被 @After() 和 @AfterReturning() 增强的时候,先执行哪一个呢?

1@AfterReturning(pointcut="execution(* main.tsmyk.mybeans.inf.IUserService.query(..))", returning="object")
2public void after_return(Object object){
3    System.out.println("===log: 在 query 方法返回后执行, 返回值= " + object);
4}
5
6@After("execution(* main.tsmyk.mybeans.inf.IUserService.query(..))")
7public void after_2(){
8    System.out.println("===log: 在 query 方法之后执行....");
9}

测试:

1根据name查询用户成功
2===log: 在 query 方法之后执行....
3===log: 在 query 方法返回后执行, 返回值= User{name='zhangsan', age=20, sex=1, money=1000.0, job='java'}

可以看到,即使 @After() 放在  @AfterReturning() 的后面,它也先被执行,即 @After() 在 @AfterReturning() 之前执行。

3.4 异常增强,@AfterThrowing,在抛出异常的时候执行,不抛异常不执行。

1@AfterThrowing(pointcut="execution(* main.tsmyk.mybeans.inf.IUserService.query(..))", throwing = "ex")
2public void after_throw(Exception ex){
3    System.out.println("在 query 方法抛异常时执行, 异常= " + ex);
4}

现在来修改一下它增强的 query() 方法,让它抛出异常:

1@Override
2public User query(String name) {
3    System.out.println("根据name查询用户成功");
4    User user = new User(name, 20, 1, 1000, "java");
5    int a = 1/0;
6    return user;
7}

测试:

1@Test
2public void testQuery() {
3    userServiceImpl.query("zhangsan");
4}
5// 结果:
6// 在 query 方法抛异常时执行, 异常= java.lang.ArithmeticException: / by zero
7// java.lang.ArithmeticException: / by zero ...........

3.5 环绕增强,@Around,在目标方法执行之前和之后执行

1@Around("execution(* main.tsmyk.mybeans.inf.IUserService.delete(..))")
2public void test_around(ProceedingJoinPoint joinPoint) throws Throwable {
3    Object[] args = joinPoint.getArgs();
4    System.out.println("log : delete 方法执行之前, 参数 = " + args[0].toString());
5    joinPoint.proceed();
6    System.out.println("log : delete 方法执行之后");
7}

测试:

1@Test
2public void test5(){
3    userServiceImpl.delete("zhangsan");
4}
5
6// 结果:
7// log : delete 方法执行之前, 参数 = zhangsan
8// 根据name删除用户成功, name = zhangsan
9// log : delete 方法执行之后

以上就是 Spring AOP 的几种增强。

上面的栗子中,在每个方法上方的切点表达式都需要写一遍,现在可以使用 @Pointcut 来声明一个可重用的切点表达式,之后在每个方法的上方引用这个切点表达式即可:

 1// 声明 pointcut
 2@Pointcut("execution(* main.tsmyk.mybeans.inf.IUserService.query(..))")
 3public void pointcut(){
 4}
 5
 6@Before("pointcut()")
 7public void before_3(){
 8    System.out.println("log: 在 query 方法之前执行");
 9}
10@After("pointcut()")
11public void after_4(){
12    System.out.println("log: 在 query 方法之后执行....");
13}

指示符

在上面的栗子中,使用了 execution 指示符,它用来匹配方法执行的连接点,也是 Spring AOP 使用的主要指示符,在切点表达式中使用了通配符 ()  和  (.. ),其中,( )可以表示任意方法,任意返回值,(..)表示方法的任意参数 ,接下来来看下其他的指示符。

1. within

匹配特定包下的所有类的所有 Joinpoint(方法),包括子包,注意是所有类,而不是接口,如果写的是接口,则不会生效,如 within(main.tsmyk.mybeans.impl.* 将会匹配 main.tsmyk.mybeans.impl 包下所有类的所有 Join pointwithin(main.tsmyk.mybeans.impl..* 两个点将会匹配该包及其子包下的所有类的所有 Join point
栗子:

1@Pointcut("within(main.tsmyk.mybeans.impl.*)")
2public void testWithin(){
3}
4
5@Before("testWithin()")
6public void test_within(){
7    System.out.println("test within 在方法执行之前执行.....");
8}

执行该包下的类 UserServiceImpl 的 delete 方法,结果如下:

1@Test
2public void test5(){
3    userServiceImpl.delete("zhangsan");
4}
5
6// 结果:
7// test within 在方法执行之前执行.....
8// 根据name删除用户成功, name = zhangsan

2. @within

匹配所有持有指定注解类型的方法,如 @within(Secure),任何目标对象持有Secure注解的类方法;必须是在目标对象上声明这个注解,在接口上声明的对它不起作用。

3. target

匹配的是一个目标对象,target(main.tsmyk.mybeans.inf.IUserService)匹配的是该接口下的所有 Join point :

 1@Pointcut("target(main.tsmyk.mybeans.inf.IUserService)")
 2public void anyMethod(){
 3}
 4
 5@Before("anyMethod()")
 6public void beforeAnyMethod(){
 7    System.out.println("log: ==== 方法执行之前 =====");
 8}
 9
10@After("anyMethod()")
11public void afterAnyMethod(){
12    System.out.println("log: ==== 方法执行之后 =====");
13}

之后,执行该接口下的任意方法,都会被增强。

4. @target

匹配一个目标对象,这个对象必须有特定的注解,如 @target(org.springframework.transaction.annotation.Transactional) 匹配任何 有 @Transactional 注解的方法

5. this

匹配当前AOP代理对象类型的执行方法,this(service.IPointcutService),当前AOP对象实现了 IPointcutService接口的任何方法

6. arg

匹配参数,

 1    // 匹配只有一个参数 name 的方法
 2    @Before("execution(* main.tsmyk.mybeans.inf.IUserService.query(String)) && args(name)")
 3    public void test_arg(){
 4
 5    }
 6
 7    // 匹配第一个参数为 name 的方法
 8    @Before("execution(* main.tsmyk.mybeans.inf.IUserService.query(String)) && args(name, ..)")
 9    public void test_arg2(){
10
11    }
12
13    // 匹配第二个参数为 name 的方法
14    @Before("execution(* main.tsmyk.mybeans.inf.IUserService.query(String)) && args(*, name, ..)")
15    public void test_arg3(){
16
17    }

7. @arg

匹配参数,参数有特定的注解,@args(Anno)),方法参数标有Anno注解。

8. @annotation

匹配特定注解
@annotation(org.springframework.transaction.annotation.Transactional) 匹配 任何带有 @Transactional 注解的方法。

9. bean

匹配特定的 bean 名称的方法

 1    // 匹配 bean 的名称为 userServiceImpl 的所有方法
 2    @Before("bean(userServiceImpl)")
 3    public void test_bean(){
 4        System.out.println("===================");
 5    }
 6
 7    // 匹配 bean 名称以 ServiceImpl 结尾的所有方法
 8    @Before("bean(*ServiceImpl)")
 9    public void test_bean2(){
10        System.out.println("+++++++++++++++++++");
11    }

测试:
执行该bean下的方法:

1@Test
2public void test5(){
3    userServiceImpl.delete("zhangsan");
4}
5//结果:
6// ===================
7// +++++++++++++++++++
8// 根据name删除用户成功, name = zhangsan

以上就是 Spring AOP 所有的指示符的使用方法了。

Spring AOP 原理

Spring AOP 的底层使用的使用 动态代理;共有两种方式来实现动态代理,一个是 JDK 的动态代理,一种是 CGLIB 的动态代理,下面使用这两种方式来实现以上面的功能,即在调用 UserServiceImpl 类方法的时候,在方法执行之前和之后加上日志。

JDK 动态代理

实现 JDK 动态代理,必须要实现 InvocationHandler 接口,并重写 invoke 方法:

 1public class UserServiceInvocationHandler implements InvocationHandler {
 2
 3    // 代理的目标对象
 4    private Object target;
 5
 6    public UserServiceInvocationHandler(Object target) {
 7        this.target = target;
 8    }
 9
10    @Override
11    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
12
13        System.out.println("log: 目标方法执行之前, 参数 = " + args);
14
15        // 执行目标方法
16        Object retVal = method.invoke(target, args);
17
18        System.out.println("log: 目标方法执行之后.....");
19
20        return retVal;
21    }
22}

测试:

 1public static void main(String[] args) throws IOException {
 2
 3    // 需要代理的对象
 4    IUserService userService = new UserServiceImpl();
 5    InvocationHandler handler = new UserServiceInvocationHandler(userService);
 6    ClassLoader classLoader = userService.getClass().getClassLoader();
 7    Class[] interfaces = userService.getClass().getInterfaces();
 8
 9    // 代理对象
10    IUserService proxyUserService = (IUserService) Proxy.newProxyInstance(classLoader, interfaces, handler);
11
12    System.out.println("动态代理的类型  = " + proxyUserService.getClass().getName());
13    proxyUserService.query("zhangsan");
14
15    // 把字节码写到文件
16    byte[] bytes = ProxyGenerator.generateProxyClass("$Proxy", new Class[]{UserServiceImpl.class});
17    FileOutputStream fos =new FileOutputStream(new File("D:/$Proxy.class"));
18    fos.write(bytes);
19    fos.flush();
20
21}

结果:

1动态代理的类型  = com.sun.proxy.$Proxy0
2log: 目标方法执行之前, 参数 = [Ljava.lang.Object;@2ff4acd0
3根据name查询用户成功
4log: 目标方法执行之后.....

可以看到在执行目标方法的前后已经打印了日志;刚在上面的 main 方法中,我们把代理对象的字节码写到了文件里,现在来分析下:

反编译 &Proxy.class 文件如下:

Spring AOP 功能使用详解_第1张图片

可以看到它通过实现接口来实现的。

JDK 只能代理那些实现了接口的类,如果一个类没有实现接口,则无法为这些类创建代理。此时可以使用 CGLIB 来进行代理。

CGLIB 动态代理

接下来看下 CGLIB 是如何实现的。

首先新建一个需要代理的类,它没有实现任何接口:

1public class UserServiceImplCglib{
2    public User query(String name) {
3        System.out.println("根据name查询用户成功, name = " + name);
4        User user = new User(name, 20, 1, 1000, "java");
5        return user;
6    }
7}

现在需要使用 CGLIB 来实现在方法 query 执行的前后加上日志:

使用 CGLIB 来实现动态代理,也需要实现接口 MethodInterceptor,重写 intercept 方法:

 1public class CglibMethodInterceptor implements MethodInterceptor {
 2
 3    @Override
 4    public Object intercept(Object obj, Method method, Object[] args, MethodProxy methodProxy) throws Throwable {
 5
 6        System.out.println("log: 目标方法执行之前, 参数 = " + args);
 7
 8        Object retVal = methodProxy.invokeSuper(obj, args);
 9
10        System.out.println("log: 目标方法执行之后, 返回值 = " + retVal);
11        return retVal;
12    }
13}

测试:

 1public static void main(String[] args) {
 2
 3    // 把代理类写入到文件
 4    System.setProperty(DebuggingClassWriter.DEBUG_LOCATION_PROPERTY, "D:\\");
 5
 6    Enhancer enhancer = new Enhancer();
 7    enhancer.setSuperclass(UserServiceImplCglib.class);
 8    enhancer.setCallback(new CglibMethodInterceptor());
 9
10    // 创建代理对象
11    UserServiceImplCglib userService = (UserServiceImplCglib) enhancer.create();
12    System.out.println("动态代理的类型 = " + userService.getClass().getName());
13
14    userService.query("zhangsan");
15}

结果:

1动态代理的类型 = main.tsmyk.mybeans.impl.UserServiceImplCglib$$EnhancerByCGLIB$$772edd85
2log: 目标方法执行之前, 参数 = [Ljava.lang.Object;@77556fd
3根据name查询用户成功, name = zhangsan
4log: 目标方法执行之后, 返回值 = User{name='zhangsan', age=20, sex=1, money=1000.0, job='java'}

可以看到,结果和使用 JDK 动态代理的一样,此外,可以看到代理类的类型为 main.tsmyk.mybeans.impl.UserServiceImplCglib$$EnhancerByCGLIB$$772edd85,它是 UserServiceImplCglib 的一个子类,即 CGLIB 是通过 继承的方式来实现的。

总结

  1. JDK 的动态代理是通过反射和拦截器的机制来实现的,它会为代理的接口生成一个代理类。

  2. CGLIB 的动态代理则是通过继承的方式来实现的,把代理类的class文件加载进来,通过修改其字节码生成子类的方式来处理。

  3. JDK 动态代理只能对实现了接口的类生成代理,而不能针对类。

  4. CGLIB是针对类实现代理,主要是对指定的类生成一个子类,覆盖其中的方法,但是因为采用的是继承, 所以 final 类或方法无法被代理。

  5. Spring AOP 中,如果实现了接口,默认使用的是 JDK 代理,也可以强制使用 CGLIB 代理,如果要代理的类没有实现任何接口,则会使用 CGLIB 进行代理,Spring 会进行自动的切换。

上述实现 Spring AOP 的栗子采用的是 注解的方法来实现的,此外,还可以通过配置文件的方式来实现 AOP 的功能。以上就是 Spring AOP 的一个详细的使用过程。

你可能感兴趣的:(源码,Spring系列,spring,java,后端)