spring揭秘07-aop基本要素及代理模式3种实现

【README】

本文总结自《spring揭秘》,作者王福强,非常棒的一本书,墙裂推荐;

1)业务场景: 需要为每个访问数据库的dao方法新增统计sql执行耗时逻辑; 如何实现? 每个dao方法都调用统计耗时的公共逻辑? 会不会太复杂? 我想,一个正常的技术架构不应该是这样的。 此外,公共逻辑包括但不限于方法执行监控,访问日志收集,数据脱敏等,这些公共逻辑都涉及到如何注入到正常业务逻辑的问题;

  • 解决方法: 把统计sql执行耗时公共逻辑通过代理模式织入到dao方法被调用的上文和下文;
  • 如何实现代理模式? 通过静态代理,还是JDK动态代理,亦或是CGLIB字节码增强动态代理? 对的,这就是引入AOP的目的,即通过代理模式把公共逻辑织入到正常业务功能的上下文,一定程度上可以做到业务代码无侵入织入,实现公共逻辑代码高内聚而不是散落在各个业务方法中

2)AOP: Aspect-Oriented Programming,面向切面编程; AOP中,公共逻辑被抽取为切面, 用户代码通过硬编码或配置或注解把切面逻辑织入到业务功能的上文或下文或者上下文;


【1】AOP思想演进

【1.1】AOL:面向切面语言

与OOP类似,AOP也是一种软件架构概念,它也需要编程语言来实现;

1)AOL面向切面语言:实现AOP的编程语言;AOL语言类型不需要与应用系统编程语言相同;(如应用系统使用java,AOL可以选择AspectC,理论上而言)

2)AOL清单:

  • AspectJ
  • AspectC
  • AspectC++

3)切面织入:把面向切面组件集成(注入)到OOP组件的过程;或者把切面逻辑织入到业务逻辑上下文的过程;

【1.2】AOP实现设想

1)静态AOP: 以最初的AspectJ为代表;底层原理是: 通过特定编译器,把切面逻辑编译成字节码,并把字节码织入到业务class文件形成新的class字节码文件(理解为编译时织入);

  • 优点:没有任何性能损失;
  • 缺点:灵活性不够; 因为有任何变动,都需要修改切面逻辑,重新编译其字节码,重新织入;

2)动态AOP: 在系统运行之后织入切面到切点,而不是在编译时;而且织入信息采用XML文件或其他配置文件保存,运维成本低;若有修改,则修改配置即可;

  • 缺点:有性能损失; 因为动态AOP是在系统运行期间织入切面,会有一定的性能损失(理解为运行时织入 );

【1.3】java平台的AOP实现机制

【1.3.1】动态代理(需要目标类实现接口)

1)动态代理:底层使用JDK动态代理 api,把切面逻辑封装到动态代理的InvocationHandler接口实现类中;

  • 缺点1: 所有需要织入切面逻辑的模块类都需要实现对应接口; 因为动态代理机制仅针对接口有效
  • 缺点2:动态代理在运行时使用反射的运行时织入,相对于编译时织入,有性能损失;
  • 补充:Spring AOP 默认使用JDK动态代理实现AOP

【1.3.2】动态字节码增强(不需要目标类实现接口)

1)动态字节码增强(生成)技术: 使用ASM或CGLIB等java工具库,在程序运行期间为目标对象生成子类,修改子类class文件字节码文件(如新增切面逻辑字节码), 只要修改后的子类class文件满足jvm规范,其就可以正常运行

2)基于动态字节码增强的AOP: 通过动态字节码增强技术针对被织入的类(目标对象类)生成相应子类,并把切面逻辑字节码添加到子类,应用系统在执行期间使用子类bean(代理bean)处理业务逻辑;

3)当目标类没有实现接口, spring 使用动态字节码增强实现AOP

【1.3.3】自定义加载器

1)自定义加载器: 加载器通过读取外部文件规定的织入规则和必要信息,在加载class文件时就把切面逻辑织入到class文件,然后把改动后的class文件交给jvm运行;

  • AspectJ项目的AspectWerkz框架采用的是自定义类加载器实现AOP;
  • 缺点: 某些应用服务器会控制整个类加载器,用户可能无法自定义类加载器的情况;

【2】AOP基本要素

【2.1】Joinpoint切点

1)Joinpoint切点:被织入切面逻辑的程序位置(执行点);

2)切点类型:

  • 方法调用: 当前方法被外部组件调用的程序位置;如构造方法调用;
  • 方法调用执行:当期方法内部执行开始位置到结束位置;
    • 包括构造方法执行,字段设置, 字段获取, 异常处理执行,类初始化(静态代码块初始化);

【2.2】Pointcut切点表达式

1)Pointcut切点表达式: 表示切点位置的表达式;

2)切点表达式类型:

  • 直接指定切点所在方法名称;
  • 正则表达式;
  • 使用特定的Pointcut表述语言;

【2.3】Advice通知(横切逻辑)

1)Advice通知:被织入到切点的横切逻辑(简单理解: 横切逻辑就是切面逻辑,本文认为横切这个术语更加符合aop语义 );

2)横切逻辑类型:

  • Before Advice: 在切点位置之前执行;
  • After Advice: 在切点位置之后执行;
    • After returning Advice: 当切点位置的程序正常执行完成后,才被执行(正常返回);
    • After throwing Advice: 当切点位置的程序抛出异常时,才被执行(抛出异常,非正常返回);
    • After Advice(After Finally Advice): 当切点位置的程序执行正常或异常,都被执行;
  • Around Advice:环绕通知; 在切点位置之前或者之后执行;
  • Introduction:引入通知; 这个非常重要;

3)非引入通知与引入通知区别:

  • 非引入通知:包括Before,After,Around等通知; 以上类型通知是把目标对象(被织入横切逻辑的对象)已有方法作为锚点(载体)织入横切逻辑;(简单理解:纵向织入; 织入横切逻辑到已有方法上下文,织入动作影响已有方法逻辑
  • 引入通知:仅 Introduction,顾名思义,引入的意思是为目标对象织入新方法,它不会把目标对象方法作为锚点 ;(简单理解:横向织入;织入新方法,织入动作不影响已有方法

【2.4】Aspect切面

1)Aspect切面:封装多个Pointcut切点表达式与Advice切面逻辑的实体;

【2.5】织入器

1)spring aop织入器:ProxyFactory类是spring aop最通用的织入器;

2)织入器职责: 把切面逻辑织入到切点;

【2.6】目标对象

1)目标对象: 被织入横切逻辑的对象;



【3】代理模式

1)spring aop底层使用JDK动态代理或动态字节码增强技术把横切逻辑织入到目标对象

2)代理模式有3种实现方式:

  • 静态代理;
  • JDK动态代理;
  • CGLIB动态代理(动态字节码增强(生成)技术)

【3.1】静态代理模式

1)静态代理模式: 需要目标类与代理类都实现相同接口;

【StaticProxyMain】

public class StaticProxyMain {
    public static void main(String[] args) {
        new StaticProxyMsgSenderImpl(new YidongMsgSenderImpl()).send("您好,您有待办事项需要处理", "123456");
    }
}

【StaticProxyMsgSenderImpl】静态代理类 ( 实现接口

public class StaticProxyMsgSenderImpl implements IMsgSender {

    private IMsgSender msgSendSupport;

    public StaticProxyMsgSenderImpl(IMsgSender msgSendSupport) {
        this.msgSendSupport = msgSendSupport;
    }

    @Override
    public void send(String msg, String phoneNum) {
        System.out.println("static proxy busi: before");
        msgSendSupport.send(msg, phoneNum);
        System.out.println("static proxy busi: after");
    }
}

【IMsgSender】接口

public interface IMsgSender {

    void send(String msg, String phoneNum);

    default boolean checkAuth(String phoneNum) {
        System.out.printf("IMsgSender#checkAuth(): 校验权限; 电话号码:[%s]\n", phoneNum);
        return false;
    }
}

【YidongMsgSenderImpl】接口实现类 ( 实现接口

public class YidongMsgSenderImpl implements IMsgSender {

    @Override
    public void send(String msg, String phoneNum) {
        System.out.printf("运营商:[中国移动],短信内容:[%s] ; 电话号码:[%s] \n", msg, phoneNum);
    }
}

【打印日志】

static proxy busi: before
运营商:[中国移动],短信内容:[您好,您有待办事项需要处理] ; 电话号码:[123456] 
static proxy busi: after

【3.1.1】静态代理模式的问题

1)业务场景: 应用系统不仅有短信发送器(IMsgSender),还有邮件发送器(IEmailSender),两个发送器的发送方法都是send(),且系统监控需求都需要对两个send() 方法做监控; 按照静态代理思想, 岂不是还要为邮件发送器新建一个邮件发送器实现类? 如果还有微信发送器(IWechatSender),那还要再新建一个微信发送器实现类? 累不累?

  • 这就是静态代理模式的局限所在了, 要求每个目标对象的代理对象都要实现对应接口,即便拦截的方法名相同;显然,静态代理模式就不适合做aop ,因为通用性不够

【3.2】JDK动态代理模式

1)基于JDK动态代理API实现动态代理: 对于方法名相同而目标对象不同的代理,仅需要提供1个InvocationHandler接口的实现类;

【JdkDynamicProxyMain】JDK动态代理测试入口main

public class JdkDynamicProxyMain {
    public static void main(String[] args) {
        // 发送短信动态代理
        IMsgSender msgSenderTarget = new YidongMsgSenderImpl();
        IMsgSender dynamicProxyMsgSender = (IMsgSender) Proxy.newProxyInstance(
                msgSenderTarget.getClass().getClassLoader(), new Class[]{IMsgSender.class}, new SenderInvocationHandler(msgSenderTarget));
        String message = "您好,您有待办任务需要处理";
        String phoneNum = "123456";
        dynamicProxyMsgSender.checkAuth(phoneNum);
        dynamicProxyMsgSender.send(message, phoneNum);

        System.out.println("\n我是分割线=================\n");
        // 发送邮箱动态代理
        IEmailSender emailSenderTarget = new GoogleEmailSenderImpl();
        IEmailSender dynamicProxyEmailSender = (IEmailSender) Proxy.newProxyInstance(
                emailSenderTarget.getClass().getClassLoader(), new Class[]{IEmailSender.class}, new SenderInvocationHandler(emailSenderTarget));
        dynamicProxyEmailSender.checkAuth("123@gamil.com");
        dynamicProxyEmailSender.send(message, "123@gamil.com");
    }
}

【SenderInvocationHandler】InvocationHandler接口实现类

public class SenderInvocationHandler implements InvocationHandler {

    private Object target;

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

    @Override
    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
        Object result = null;
        if ("send".equals(method.getName())) {
            System.out.println("send() execute : before");
            result = method.invoke(target, args);
            System.out.println("send() execute : after");
        } else {
            result = method.invoke(target, args);
        }
        return result;
    }
}

【IEmailSender】邮件发送器接口

public interface IEmailSender {

    void send(String content, String addr);

    default boolean checkAuth(String addr) {
        System.out.printf("IEmailSender#checkAuth(): 校验权限; 邮箱地址:[%s]\n", addr);
        return false;
    }
}

【GoogleEmailSenderImpl】邮件发送器接口实现类

public class GoogleEmailSenderImpl implements IEmailSender {
    @Override
    public void send(String content, String addr) {
        System.out.printf("发送邮件: 内容=[%s], 邮箱地址=[%s]\n", content, addr);
    }
}

【打印日志】

IMsgSender#checkAuth(): 校验权限; 电话号码:[123456]
send() execute : before // 横切逻辑上文
运营商:[中国移动],短信内容:[您好,您有待办任务需要处理] ; 电话号码:[123456] 
send() execute : after  // 横切逻辑下文

我是分割线=================

IEmailSender#checkAuth(): 校验权限; 邮箱地址:[123@gamil.com]
send() execute : before // 横切逻辑上文上文
发送邮件: 内容=[您好,您有待办任务需要处理], 邮箱地址=[123@gamil.com]
send() execute : after  // 横切逻辑下文 

【3.2.1】JDK动态代理模式的问题

1)总结: JDK动态代理解决了静态代理模式中每个目标对象的代理对象都要实现对应接口,即便拦截的方法名相同的问题;

2)JDK动态代理局限性: 所有目标对象(被织入切面逻辑的对象)都需要实现对应接口; 因为动态代理机制仅针对接口有效(即,若目标对象没有实现接口,则无法使用JDK动态代理)

  • 如 YidongMsgSenderImpl 实现IMsgSender接口, GoogleEmailSenderImpl 实现 IEmailSender接口 ;

3)问题:因为JDK动态代理仅针对接口有效,则当应用系统集成的第三方类库存在没有实现接口的类,则这些类就无法通过JDK动态代理实现AOP, 显然,这是不合理的;

  • 解决方法: 使用动态字节码增强技术实现动态代理,即便目标对象或目标类没有实现接口,也可以实现动态代理;

【java.lang.reflect.Proxy】JDK中newProxyInstance定义:

/**
* Params:
loader – the class loader to define the proxy class 
interfaces – the list of interfaces for the proxy class to implement 代理类需要实现的接口列表 
h – the invocation handler to dispatch method invocations to ()  
*/
@CallerSensitive
    public static Object newProxyInstance(ClassLoader loader,
                                          Class<?>[] interfaces,
                                          InvocationHandler h) {

4)若目标对象不实现接口,则无法使用 JDK动态代理,如下。

【NonInterfaceJdkDynamicProxyMain】目标对象不实现接口导致无法实现JDK动态代理测试main

传入的参数是实现类class数组:new Class[]{GoogleEmailSenderImpl.class} , 而不是接口class数组:new Class[]{IEmailSender.class}

public class NonInterfaceJdkDynamicProxyMain {
    public static void main(String[] args) {
        // 发送邮箱动态代理
        GoogleEmailSenderImpl emailSenderTarget = new GoogleEmailSenderImpl();
        GoogleEmailSenderImpl dynamicProxyEmailSender = (GoogleEmailSenderImpl) Proxy.newProxyInstance(
                emailSenderTarget.getClass().getClassLoader(), new Class[]{GoogleEmailSenderImpl.class}, new SenderInvocationHandler(emailSenderTarget));
        dynamicProxyEmailSender.checkAuth("123@gamil.com");
        dynamicProxyEmailSender.send("您好,您有待办任务需要处理", "123@gamil.com");
    }
}

【报错】

Exception in thread "main" java.lang.IllegalArgumentException: com.tom.springnote.chapter08proxypattern.GoogleEmailSenderImpl is not an interface


【3.3】动态字节码增强(生成)动态代理

1)动态字节码增强(生成)技术: 使用ASM或CGLIB等java工具库,在程序运行期间为目标对象生成子类,修改子类class字节码文件(如新增切面逻辑字节码), 只要修改后的子类class文件满足jvm规范,其就可以正常运行

2)基于动态字节码增强的动态代理: 通过动态字节码增强技术针对目标类生成子类,子类通过覆写扩展父类(目标对象类)方法,即把切面逻辑字节码添加到子类(代理类),应用系统运行时使用子类bean(代理类bean)处理业务逻辑;

【CglibDynamicProxyMain】CGLIB动态代理main

public class CglibDynamicProxyMain {
    public static void main(String[] args) {
        // 传入target
        BusiTarget target = new BusiTarget();
        Enhancer enhancer = new Enhancer();
        enhancer.setSuperclass(target.getClass());
        enhancer.setCallback(new CglibMethodInterceptorImpl(target));
        BusiTarget dynamicByteProxyTaskSender = (BusiTarget) enhancer.create();
        dynamicByteProxyTaskSender.send1("您有1个待办任务需要处理", "task001");

        System.out.println();
        dynamicByteProxyTaskSender.send2("您有2个待办任务需要处理", "task002");
    }
}

【BusiTarget】目标类,目标对象所属类,被代理类 ( 目标类没有实现接口 ,也可以实现动态代理

public class BusiTarget {

    public void send1(String content, String taskId) { 
        System.out.printf("BusiTarget#send1(): 发送任务,内容=[%s], taskId=[%s]\n", content, taskId);
    }

    public void send2(String content, String taskId) {
        System.out.printf("BusiTarget#send2(): 发送任务,内容=[%s], taskId=[%s]\n", content, taskId);
    }
}

【CglibMethodInterceptorImpl】CGLIB方法拦截器实现类

public class CglibMethodInterceptorImpl implements MethodInterceptor {

    private final Object target;

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

    @Override
    public Object intercept(Object object, Method method, Object[] args, MethodProxy methodProxy) throws Throwable {
        System.out.println("send() execute : before");
        Object result = method.invoke(target, args);
        System.out.println("send() execute : after");
        return result;
    }
}

【打印日志】

send() execute : before
BusiTarget#send1(): 发送任务,内容=[您有1个待办任务需要处理], taskId=[task001]
send() execute : after

send() execute : before
BusiTarget#send2(): 发送任务,内容=[您有2个待办任务需要处理], taskId=[task002]
send() execute : after

【3.4】3种代理模式实现小结

1)静态代理模式:通过硬编码方式,传入目标对象构建代理对象; 不灵活,对于每个目标对象(类)都需要新建一个代理类;

2)JDK动态代理模式:针对接口实现代理,目标对象需要实现接口,而代理对象需要实现InvocationHandler接口;与静态代理不同的是,如果多个目标对象的被拦截方法名相同,则仅需要新建一个InvocationHandler实现类; (通过实现接口实现动态代理

3)动态字节码增强动态代理: 动态字节码增强技术针对目标类生成一个子类,子类(代理类)通过覆写扩展父类(目标对象类)方法功能,把子类对象作为代理对象返回给应用系统使用;( 通过继承父类实现动态代理



  • 9
    点赞
  • 20
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值