Spring AOP 实现 原

AOP(Aspect Orient Programming),我们一般称为面向切面编程,作为面向对象的一种补充,用于处理系统中分布于各个模块的横切关注点,比如事务、日志、缓存、分布式锁等等。AOP实现的关键在于AOP框架自动创建的AOP代理,AOP代理主要分为静态代理和动态代理,静态代理的代表为AspectJ;而动态代理则以Spring AOP为代表。Spring的主要动态代理有CGLib和JDK自动代理。

使用AspectJ的编译时增强实现AOP

AspectJ是静态代理的增强,所谓的静态代理就是AOP框架会在编译阶段生成AOP代理类,因此也称为编译时增强。

编译成字节码.class比原来的.java会多了一些代码,这就是AspectJ的静态代理,它会在编译阶段将Aspect织入Java字节码中, 运行的时候就是经过增强之后的AOP对象。

使用Spring AOP

与AspectJ的静态代理不同,Spring AOP使用的动态代理,所谓的动态代理就是说AOP框架不会去修改字节码,而是在内存中临时为方法生成一个AOP对象,这个AOP对象包含了目标对象的全部方法,并且在特定的切点做了增强处理,并回调原对象的方法。

Spring AOP中的动态代理主要有两种方式,JDK动态代理和CGLIB动态代理。JDK动态代理通过反射来接收被代理的类,并且要求被代理的类必须实现一个接口。JDK动态代理的核心是InvocationHandler接口和Proxy类。

如果目标类没有实现接口,那么Spring AOP会选择使用CGLIB来动态代理目标类。CGLIB(Code Generation Library),是一个代码生成的类库,可以在运行时动态的生成某个类的子类,注意,CGLIB是通过继承的方式做的动态代理,因此如果某个类被标记为final,那么它是无法使用CGLIB做动态代理的。

现在我们做一个测试:

首先定义一个接口:

package cn.chinotan.service;

/**
 * @program: test
 * @description: 动物
 * @author: xingcheng
 **/
public interface Animal {

    /**
     * 跑
     * @param where 在什么地方跑
     * @return
     */
    String run (String where);
    
}

其实现类:

package cn.chinotan.service.impl;

import cn.chinotan.aop.Action;
import cn.chinotan.service.Animal;
import org.springframework.aop.framework.AopContext;
import org.springframework.stereotype.Service;

/**
 * @program: test
 * @description: 狗
 * @author: xingcheng
 **/
@Service
public class Dog implements Animal {

    @Action
    @Override
    public String run(String where) {
        System.out.println("狗往" + where + "跑");
        return "地点是:" + where;
    }
}

其中@Action为自定义的注解,用来指定aop代理的切入点

package cn.chinotan.aop;

import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

/**
 * @program: test
 * @description: 动作
 * @author: xingcheng
 **/
@Target(ElementType.METHOD)
public @interface Action {
    
}

定义Aspect:

package cn.chinotan.aspect;

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

/**
 * @program: test
 * @description: 动作aop实现
 * @author: xingcheng
 **/
@Aspect
@Component
public class ActionAspect {

    @Pointcut("@annotation(cn.chinotan.aop.Action)")
    void actionPointCut() {
    }

    @Before("actionPointCut()")
    void beforeAction() {
        System.out.println("热身运动");
    }
}

其中有几种aop的通知注解:

  1. @Before: 前置通知, 在方法执行之前执行
  2. @After: 后置通知, 在方法执行之后执行
  3. @AfterRunning:返回通知, 在方法成功执行返回结果之后执行
  4. @AfterThrowing: 异常通知, 在方法抛出异常之后
  5. @Around: 环绕通知,围绕着方法执行

@Pointcut是切入点的注解:

这里使用了@annotation 可以在使用了自定义注解的配置方法上实现切入

也可以使用execution(* *(..))的形式:

    声明切入点     第一个*表示 方法  返回值(例如public int)     第二个* 表示方法的全限定名(即包名+类名)     perform表示目标方法参数括号两个.表示任意类型参数     方法表达式以“*”号开始,表明了我们不关心方法返回值的类型。然后,我们指定了全限定类名和方法名。对于方法参数列表,     我们使用两个点号(..)表明切点要选择任意的perform()方法,无论该方法的入参是什么     execution表示执行的时候触发

在启动的application.yml配置文件中加入

spring.aop.proxy-target-class: false

这个是控制aop的具体实现方式,为true 的话使用cglib,为false的话使用java的Proxy,默认是false

之后运行controller:

package cn.chinotan.controller;

import cn.chinotan.service.Animal;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

/**
 * @program: test
 * @description: test类
 * @author: xingcheng
 **/
@RestController
@RequestMapping("/test")
public class TestController {

    @Autowired
    Animal animal;

    @GetMapping("/aopRun")
    public String aopRun() {
        animal.run("狗窝");
        System.out.println("dog代理为:" + animal.getClass());
        
        return "ok";
    }
    
}

打印日志:

可以看到类型是com.sun.proxy.$Proxy71,也就是前面提到的Proxy类,因此这里Spring AOP使用了JDK的动态代理。

再来看看不实现接口的情况,修改Dog类:

配置proxy-target-class: false依旧

package cn.chinotan.service.impl;

import cn.chinotan.aop.Action;
import cn.chinotan.service.Animal;
import org.springframework.aop.framework.AopContext;
import org.springframework.stereotype.Service;

/**
 * @program: test
 * @description: 狗
 * @author: xingcheng
 **/
@Service
public class Dog {

    @Action
    public String run(String where) {
        System.out.println("狗往" + where + "跑");
        return "地点是:" + where;
    }
}

打印日志:

可以看到类被CGLIB增强了,也就是动态代理。这里的CGLIB代理就是Spring AOP的代理,这个类也就是所谓的AOP代理,AOP代理类在切点动态地织入了增强处理。

可以看到:

    AspectJ在编译时就增强了目标对象,Spring AOP的动态代理则是在每次运行时动态的增强,生成AOP代理对象,区别在于生成AOP代理对象的时机不同,相对来说AspectJ的静态代理方式具有更好的性能,但是AspectJ需要特定的编译器进行处理,而Spring AOP则无需特定的编译器处理。

    java动态代理是利用反射机制生成一个实现代理接口的匿名类,在调用具体方法前调用InvokeHandler来处理。而cglib动态代理是利用asm开源包,对代理对象类的class文件加载进来,通过修改其字节码生成子类来处理。如果目标对象实现了接口,默认情况下会采用JDK的动态代理实现AOP,如果目标对象实现了接口,可以强制使用CGLIB实现AOP,如果目标对象没有实现了接口,必须采用CGLIB库,spring会自动在JDK动态代理和CGLIB之间转换

误区注意:

在平时开发中,我们通常在Service中定义了一个方法并且切入之后,从Controller里面调用该方法可以实现切入,但是当在同一个Service中实现另一方法并调用改方法时却无法切入

类似于:

package cn.chinotan.service.impl;

import cn.chinotan.aop.Action;
import cn.chinotan.service.Animal;
import org.springframework.aop.framework.AopContext;
import org.springframework.stereotype.Service;

/**
 * @program: test
 * @description: 狗
 * @author: xingcheng
 * @create: 2018-10-27 16:00
 **/
@Service
public class Dog implements Animal {

    @Action
    @Override
    public String run(String where) {
        System.out.println("狗往" + where + "跑");
        return "地点是:" + where;
    }

    @Override
    public void runToEat(String food) {
        run("狗窝");
        System.out.println("狗在吃" + food);
    }
}


package cn.chinotan.service.impl;

import cn.chinotan.aop.Action;
import cn.chinotan.service.Animal;
import org.springframework.aop.framework.AopContext;
import org.springframework.stereotype.Service;

/**
 * @program: test
 * @description: 狗
 * @author: xingcheng
 * @create: 2018-10-27 16:00
 **/
@Service
public class Dog implements Animal {

    @Action
    @Override
    public String run(String where) {
        System.out.println("狗往" + where + "跑");
        return "地点是:" + where;
    }

    @Override
    public void runToEat(String food) {
        run("狗窝");
        System.out.println("狗在吃" + food);
    }
}

我们在执行runToEat方法时,调用了自己类中的另一个方法,结果为:

可以看到run()的切面方法并没有执行,以上结果的出现与Spring AOP的实现原理息息相关,由于Spring AOP采用了动态代理实现AOP,在Spring容器中的bean(也就是目标对象)会被代理对象代替,代理对象里加入了我们需要的增强逻辑,当调用代理对象的方法时,目标对象的方法就会被拦截,

通过调用代理对象的action方法,在其内部会经过切面增强,然后方法被发射到目标对象,在目标对象上执行原有逻辑,如果在原有逻辑中嵌套调用了work方法,则此时work方法并没有被进行切面增强,因为此时它已经在目标对象内部。而解决方案很好地说明了,将嵌套方法发射到代理对象,这样就完成了切面增强。可以看下源码:

在代码3处,如果配置了exposeProxy开关,则会将代理对象暴露在当前线程中,以供其它需要的地方使用,通过使用静态的全局ThreadLocal变量就解决了问题。

spring提供了一个这样的类:

可以看到他可以获取到当前的aop代理,但是在获取之前,得开启exposeProxy开关

@EnableAspectJAutoProxy(proxyTargetClass = false, exposeProxy = true)

这样就可以进行代理了,打印日志为:

既然这样可以,那是不是直接applicationContext.getBean()也可以呢?实验过后得到的结果是可行,而且配置中的expose-proxy也不用设置成true,那试一下:

package cn.chinotan.service.impl;

import cn.chinotan.aop.Action;
import cn.chinotan.service.Animal;
import org.springframework.aop.framework.AopContext;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.ApplicationContext;
import org.springframework.stereotype.Service;

/**
 * @program: test
 * @description: 狗
 * @author: xingcheng
 * @create: 2018-10-27 16:00
 **/
@Service
public class Dog implements Animal {
    
    @Autowired
    ApplicationContext applicationContext;

    @Action
    @Override
    public String run(String where) {
        System.out.println("狗往" + where + "跑");
        return "地点是:" + where;
    }

    @Override
    public void runToEat(String food) {
//        Dog dog = (Dog) AopContext.currentProxy();
        Dog dog = (Dog) applicationContext.getBean("dog");
        dog.run("狗窝");
        System.out.println("狗在吃" + food);
    }
}

打印日志为:

可见同样可以

(adsbygoogle = window.adsbygoogle || []).push({});

本文参与腾讯云自媒体分享计划,欢迎正在阅读的你也加入,一起分享。

发表于

我来说两句

0 条评论
登录 后参与评论

扫码关注云+社区

领取腾讯云代金券