细说Spring Aop

什么是aop ?

aop(Aspect-Oriented Programming) ,即面向切面编程,面向切面的目的就是,抽象重复代码,复用代码,提高编码效率

aop 实现原理?

Spring aop 底层原理很简单,即为动态代理模式,包含jdk 动态代理和cglb 动态代理

什么是动态代理?

要搞清楚什么是动态代理,那就要搞清楚什么是静态代理模式 ?
代理模式,属于23种设计模式中的结构型模式,copy 网上一句话来简单描述一下:

由于某些原因需要给某对象提供一个代理以控制对该对象的访问。这时,访问对象不适合或者不能直接引用目标对象,代理对象作为访问对象和目标对象之间的中介

、、、一脸懵逼

talk is cheap show me the code

静态代理

声明一个 User 类,

public class User {

    private String name;

    private int age;

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    public int getAge() {
        return age;
    }

    public void setAge(int age) {
        this.age = age;
    }
}

声明一个Talk 接口,有一个 hello 方法

public interface Talk {

    void hello(User user);
}

声明一个UserService 类,实现Talk接口

public class UserService implements Talk{

    @Override
    public void hello(User user) {
        System.out.println("你好,我是: "+user.getName());
    }
}

假如现在要实现一个功能,hello 方法执行前后打印日志,静态代理类就这样实现

public class StaticProxy implements Talk{

    private UserService userService = new UserService();

    @Override
    public void hello(User user) {
        System.out.println("before");
        userService.hello(user);
        System.out.println("after");
    }
}

写个main 方法来看看结果

   public static void main(String[] args) {
        User user = new User();
        user.setName("静态代理");
        StaticProxy staticProxy = new StaticProxy();
        staticProxy.hello(user);
    }

结果

before
你好,我是: 静态代理
after

分别在hello 方法执行前后打印日志,实现了我们的需求
实现这个需求后,再加入,现在有100个方法,需要在执行前后打印日志,那么这个时候怎么办呢,上面代码写100遍嘛?显然这是不科学的,科学的方法就是使用我们的动态代理,重复的事情交给代码做,由代码来生成代理类,动态代理就是这么回事儿,动态代理实现方式spring aop 采用的主要两种,jdk 动态代理和 cglib 动态代理

jdk 动态代理

下面我们使用jdk 动态代理来实现上述需求
声明一个 MyHandler 类,实现InvocationHandler接口

public class MyHandler implements InvocationHandler {
    private Object target;

    public MyHandler(Object target) {
        this.target = target;
    }

    @Override
    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
        System.out.println("before");
        Object result = method.invoke(target, args);
        System.out.println("after");
        return result;
    }

    public <T> T getProxyInstance(){
        return (T) Proxy.newProxyInstance(target.getClass().getClassLoader(),target.getClass().getInterfaces(),this);
    }

}

执行代理类

public static void main(String[] args) {
        System.getProperties().setProperty("sun.misc.ProxyGenerator.saveGeneratedFiles", "true");
        User user = new User();
        user.setName("jdk动态代理");

        MyHandler handler = new MyHandler(new UserService());
        Talk proxyInstance = handler.getProxyInstance();
        proxyInstance.hello(user);
    }

结果

before
你好,我是: jdk动态代理
after

结果符合预期

System.getProperties().setProperty("sun.misc.ProxyGenerator.saveGeneratedFiles", "true");

这一句的作用是,生成代理类字节码到本地,下面我们来看看生成的代理类

public final class $Proxy0 extends Proxy implements Talk {
    private static Method m1;
    private static Method m3;
    private static Method m2;
    private static Method m0;

    public $Proxy0(InvocationHandler var1) throws  {
        super(var1);
    }

    public final boolean equals(Object var1) throws  {
        try {
            return (Boolean)super.h.invoke(this, m1, new Object[]{var1});
        } catch (RuntimeException | Error var3) {
            throw var3;
        } catch (Throwable var4) {
            throw new UndeclaredThrowableException(var4);
        }
    }

    public final void hello(User var1) throws  {
        try {
            super.h.invoke(this, m3, new Object[]{var1});
        } catch (RuntimeException | Error var3) {
            throw var3;
        } catch (Throwable var4) {
            throw new UndeclaredThrowableException(var4);
        }
    }

    public final String toString() throws  {
        try {
            return (String)super.h.invoke(this, m2, (Object[])null);
        } catch (RuntimeException | Error var2) {
            throw var2;
        } catch (Throwable var3) {
            throw new UndeclaredThrowableException(var3);
        }
    }

    public final int hashCode() throws  {
        try {
            return (Integer)super.h.invoke(this, m0, (Object[])null);
        } catch (RuntimeException | Error var2) {
            throw var2;
        } catch (Throwable var3) {
            throw new UndeclaredThrowableException(var3);
        }
    }

    static {
        try {
            m1 = Class.forName("java.lang.Object").getMethod("equals", Class.forName("java.lang.Object"));
            m3 = Class.forName("com.hzins.springboot.admin.pattern.proxy.Talk").getMethod("hello", Class.forName("com.hzins.springboot.admin.pattern.proxy.User"));
            m2 = Class.forName("java.lang.Object").getMethod("toString");
            m0 = Class.forName("java.lang.Object").getMethod("hashCode");
        } catch (NoSuchMethodException var2) {
            throw new NoSuchMethodError(var2.getMessage());
        } catch (ClassNotFoundException var3) {
            throw new NoClassDefFoundError(var3.getMessage());
        }
    }
}

我们看到$Proxy0这个代理类,继承了Proxy 类,实现了我们定义的Talk接口,看到这儿就想起了一个问题,jdk 动态代理的目标类,有一个必要条件,必须实现一个接口,不然jdk 动态代理机制没有办法为我们生成代理类,这也是为什么spring aop 需要采用jdk 和 cglib 2种方案来实现,因为cglib 是采用继承的方式,而不需要实现接口,(我们可以通配置参数???强制spring 采用cglib)
观察代理类发现,重写了hello方法,m3 通过反射获取目标类的Method实例

 super.h.invoke(this, m3, new Object[]{var1})

执行的是MyHandler 中实现的invoke 方法,最终实现了代理

接下来再看看cglib动态代理 是如何实现的

声明一个拦截器,实现MethodInterceptor

public class MyInterceptor implements MethodInterceptor {
    @Override
    public Object intercept(Object o, Method method, Object[] objects, MethodProxy methodProxy) throws Throwable {
        System.out.println("before");
        Object result = methodProxy.invokeSuper(o, objects);
        System.out.println("after");
        return result;
    }
}

执行cglib

 public static void main(String[] args) {
        System.setProperty(DebuggingClassWriter.DEBUG_LOCATION_PROPERTY, "D:\\code");
        User user = new User();
        user.setName("Cglib动态代理");

        Enhancer enhancer = new Enhancer();
        enhancer.setSuperclass(UserService.class);
        enhancer.setCallback(new MyInterceptor());
        Talk proxyInstance = (Talk) enhancer.create();
        proxyInstance.hello(user);
    }

结果

before
你好,我是: Cglib动态代理
after

cglib原理通过ASM框架生成代理类继承于目标类,使用FastClass机制访问方法,相较于反射,性能更好,通过设置下面参数,将代理类生产到D盘

System.setProperty(DebuggingClassWriter.DEBUG_LOCATION_PROPERTY, "D:\\code");
企业微信截图_16134566042291.png

可以看到,不同于jdk动态代理,这里生产了3个字节码文件,分别是代理类与两个FastClass类

FastClass机制
FastClass机制就是对一个类的方法建立索引,调用方法时根据方法的签名来计算索引,通过索引来直接调用相应的方法。2个FastClass类分别为目标类和代理类的FastClass

搞清楚了什么是动态代理后,想想我们的100个类方法执行前后打印日志这个需求,是不是就不用写100遍了,只需要通过代码生成100个代理即可,但是想一下我们生成代理类的代码长什么样

Talk proxyInstance = handler.getProxyInstance();

把这个代码写100遍,好像也很痛苦,这可怎么办呀 >.<,并且我们只想为某一些方法进行增强,而不是整个类。那么使用 Spring aop就行 ,它的出现就是为了让程序猿更方便的使用动态代理这一特性,让我们专注于业务代码,curd curd curd ...

Spring aop 的基本概念

  • Aspect 切面是Pointcut和Advice的集合,一般单独作为一个类。Pointcut和Advice共同定义了关于切面的全部内容,在什么时候,何处完成何种功能
  • Advice 这是在方法执行之前或之后采取的实际操作。 这是在Spring AOP框架的程序执行期间调用的实际代码片段
  • JoinPoint 连接点,一般是被拦截的方法
  • Pointcut 这是一组一个或多个切入点,在切点应该执行Advice。 可以使用表达式或模式指定切入点
  • Introduction 引用允许我们向现有的类添加新的方法或者属性
  • Weaving 创建一个被增强对象的过程。这可以在编译时完成(例如使用AspectJ编译器),也可以在运行时完成。Spring和其他纯Java AOP框架一样,在运行时完成织入

Spring aop 的2种使用方式

  • 使用 XML 的方式来配置,使用 命名空间 <aop />
  • @AspectJ 配置:注解方式。

Spring aop 注解方式实现日志打印需求

开启AOP

@EnableAspectJAutoProxy

定义切面 Aspect

@Component
@Aspect
public class UserServiceAspect {

}

定义切点 Pointcut ,拦截签名为hello方法

@Pointcut("execution(* hello(..))")
public void pointcut() {}

定义通知(增强)Advice

    @Before("com.hzins.springboot.admin.pattern.proxy.UserServiceAspect.pointcut()")
    public void doBefore(JoinPoint joinPoint) {
        // 前置增强
        System.out.println("before");
    }

    @After("com.hzins.springboot.admin.pattern.proxy.UserServiceAspect.pointcut()")
    public void doAfter(JoinPoint joinPoint){
        // 后置增强
        System.out.println("after");
    }

执行单元测试

@RunWith(SpringJUnit4ClassRunner.class)
@SpringBootTest(classes = AdminServerApplication.class)
public class AopTest {
    @Autowired
    private UserService userService;

    @Test
    public void test(){
        User user = new User();
        user.setName("spring aop");
        userService.hello(user);
    }
}

结果

before
你好,我是: spring aop
after

通过Pointcut,可以灵活指定需要被代理的方法,Pointcut表达式有多种方式
切点表达式,主要分为匹配方法(execution),匹配注解(@within,@target,@args,@annotation),匹配包/类型(within()),匹配对象(target,this),匹配参数(args)

  • execution()
    execution(modifiers-pattern? ret-type-pattern declaring-type-pattern?name-pattern(param-pattern) throws-pattern?)

  • within() 用于匹配指定的类及其子类中的所有方法
    within(declaring-type-pattern)

  • this() 匹配可以向上转型为this指定的类型的代理对象中的所有方法

  • target() 匹配可以向上转型为target指定的类型的目标对象中的所有方法

  • args() 用于匹配当前执行的方法传入的参数为指定类型的执行方法
    args(param-pattern)

  • @annotation() 匹配带指定注解方法的连接点
    @annotation(annotation-type)

  • @within() 匹配指定注解标注的类内的方法都执行
    @within(annotation-type)

  • @target 用于匹配当前目标对象类型的执行方法,其中目标对象持有指定的注解

  • @args() 用于匹配运行时 传入的参数列表的类型持有 注解列表对应的注解的方法
    @args(annotation-type)
    通配符
    * 匹配任何数量字符
    .. 匹配任何数量字符的重复,如在类型模式中匹配任何数量子包;而在方法参数模式中匹配任何数量参数
    + 匹配指定类型的子类型
    操作符
    &&或者 and 与操作符
    || 或者 or 或操作符
    ! 或者 not 非操作符

Advice
执行顺序

image.png

多个Aspect切面,advice执行顺序

Spring AOP通过指定aspect的优先级,来控制不同aspect,advice的执行顺序,有两种方式:

Aspect 类添加注解:org.springframework.core.annotation.Order,使用注解value属性指定优先级。
Aspect 类实现接口:org.springframework.core.Ordered,实现 Ordered 接口的 getOrder() 方法。
数值越低,表明优先级越高,@Order 默认为最低优先级,即最大数值


image.png

先入后出,后入先出

Weaving
Spring和其他纯Java AOP框架一样,在运行时完成织入,那么我们通过源码来看看这个织入过程
AspectJAwareAdvisorAutoProxyCreator是aop的入口类,它的父类AbstractAutoProxyCreator继承了BeanPostProcessor,所以在目标类注入时后置回调,生成代理类

public Object postProcessAfterInitialization(Object bean, String beanName) throws BeansException {
    if (bean != null) {
        Object cacheKey = getCacheKey(bean.getClass(), beanName);
        if (!this.earlyProxyReferences.contains(cacheKey)) {
                        // 生成代理类
            return wrapIfNecessary(bean, beanName, cacheKey);
        }
    }
    return bean;
}

protected Object wrapIfNecessary(Object bean, String beanName, Object cacheKey) {
    // Create proxy if we have advice.
    //先看它的注释,大意说:如果有通知,就创建代理。
    //其实就是在bean.getClass()找到所有的通知和advisor
    //这里面其实又分为两个步骤:
    //第一,在Bean工厂找到所有的Advisor 第二,根据beanClass和Pointcut去做匹配
    Object[] specificInterceptors = getAdvicesAndAdvisorsForBean(bean.getClass(), beanName, null);
    if (specificInterceptors != DO_NOT_PROXY) {
        this.advisedBeans.put(cacheKey, Boolean.TRUE);
        //真正创建代理并返回,这时候返回的就是代理类了,把真实的bean替换掉
        Object proxy = createProxy(bean.getClass(), beanName, specificInterceptors, new SingletonTargetSource(bean));
        this.proxyTypes.put(cacheKey, proxy.getClass());
        return proxy;
    }
    this.advisedBeans.put(cacheKey, Boolean.FALSE);
    return bean;
}

createProxy 创建代理的过程,大概就是默认使用jdk为有接口的类创建代理,无接口的类使用cglib创建代理,然后返回注入;

仔细阅读源码发现,还有调用wrapIfNecessary 的地方

    @Override
    public Object getEarlyBeanReference(Object bean, String beanName) throws BeansException {
        Object cacheKey = getCacheKey(bean.getClass(), beanName);
        if (!this.earlyProxyReferences.contains(cacheKey)) {
            this.earlyProxyReferences.add(cacheKey);
        }
        return wrapIfNecessary(bean, beanName, cacheKey);
    }

getEarlyBeanReference 是SmartInstantiationAwareBeanPostProcessor 接口的方法,这个方法是获取bean提前引用的意思,Spring中解决循环引用的时候有调用这个方法

代理模式,可以说是无处不在,很多框架中都有使用到
比如在catfish中,也使用到了cglib代理来为我们的rpc接口生成代理类 (公司框架)

public class CglibClientProxy implements ClientProxy {


    @SuppressWarnings("unchecked")
    @Override
    public <T> T buildProxy(Class<T> serviceInterface, ClientConfig clientConfig, RemoteClientProcessor remoteClientProcessor) {
        Enhancer enhancer = new Enhancer();
        enhancer.setInterfaces(new Class[]{serviceInterface});
        enhancer.setNamingPolicy(CatfishNamingPolicy.INSTANCE);
        enhancer.setCallback(new ClientMethodInterceptor(clientConfig, remoteClientProcessor));
        return (T) enhancer.create();
    }

}

ScanClientAnnotationPostProcessor 实现 BeanPostProcessor 接口进行代理织入(创建)

    @Override
    public Object postProcessBeforeInitialization(Object bean, String beanName) throws BeansException {
        registerClientBean(beanName, bean);
        return bean;
    }
... 省略
    setTargetProxyBean(bean, field, convertClientConfig(AnnotationUtils.findAnnotation(field, CatfishClient.class)));
... 省略
    field.set(bean, clientProxy.buildProxy(field.getType(), clientConfig, remoteClientProcessor));
...
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 216,997评论 6 502
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 92,603评论 3 392
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 163,359评论 0 353
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 58,309评论 1 292
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 67,346评论 6 390
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 51,258评论 1 300
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 40,122评论 3 418
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 38,970评论 0 275
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 45,403评论 1 313
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,596评论 3 334
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,769评论 1 348
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 35,464评论 5 344
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 41,075评论 3 327
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,705评论 0 22
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,848评论 1 269
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 47,831评论 2 370
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,678评论 2 354

推荐阅读更多精彩内容