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

2024-08-25 02:28

本文主要是介绍spring揭秘07-aop01-aop基本要素及代理模式3种实现,希望对大家解决编程问题提供一定的参考价值,需要的开发者们随着小编来一起学习吧!

文章目录

  • 【README】
  • 【1】AOP思想演进
    • 【1.1】AOL:面向切面语言
    • 【1.2】AOP实现设想
    • 【1.3】java平台的AOP实现机制
      • 【1.3.1】动态代理(需要目标类实现接口)
      • 【1.3.2】动态字节码增强(不需要目标类实现接口)
      • 【1.3.3】自定义加载器
  • 【2】AOP基本要素
    • 【2.1】Joinpoint切点
    • 【2.2】Pointcut切点表达式
    • 【2.3】Advice通知(横切逻辑)
    • 【2.4】Aspect切面
    • 【2.5】织入器
    • 【2.6】目标对象
  • 【3】代理模式
    • 【3.1】静态代理模式
      • 【3.1.1】静态代理模式的问题
    • 【3.2】JDK动态代理模式
      • 【3.2.1】JDK动态代理模式的问题
    • 【3.3】动态字节码增强(生成)动态代理
    • 【3.4】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;}@Overridepublic 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 {@Overridepublic 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;}@Overridepublic 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 {@Overridepublic 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 ()  
*/
@CallerSensitivepublic 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) {// 传入targetBusiTarget 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;}@Overridepublic 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 : aftersend() execute : before
BusiTarget#send2(): 发送任务,内容=[您有2个待办任务需要处理], taskId=[task002]
send() execute : after

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

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

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

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



这篇关于spring揭秘07-aop01-aop基本要素及代理模式3种实现的文章就介绍到这儿,希望我们推荐的文章对编程师们有所帮助!



http://www.chinasem.cn/article/1104287

相关文章

JVM 的类初始化机制

前言 当你在 Java 程序中new对象时,有没有考虑过 JVM 是如何把静态的字节码(byte code)转化为运行时对象的呢,这个问题看似简单,但清楚的同学相信也不会太多,这篇文章首先介绍 JVM 类初始化的机制,然后给出几个易出错的实例来分析,帮助大家更好理解这个知识点。 JVM 将字节码转化为运行时对象分为三个阶段,分别是:loading 、Linking、initialization

Spring Security 基于表达式的权限控制

前言 spring security 3.0已经可以使用spring el表达式来控制授权,允许在表达式中使用复杂的布尔逻辑来控制访问的权限。 常见的表达式 Spring Security可用表达式对象的基类是SecurityExpressionRoot。 表达式描述hasRole([role])用户拥有制定的角色时返回true (Spring security默认会带有ROLE_前缀),去

浅析Spring Security认证过程

类图 为了方便理解Spring Security认证流程,特意画了如下的类图,包含相关的核心认证类 概述 核心验证器 AuthenticationManager 该对象提供了认证方法的入口,接收一个Authentiaton对象作为参数; public interface AuthenticationManager {Authentication authenticate(Authenti

Spring Security--Architecture Overview

1 核心组件 这一节主要介绍一些在Spring Security中常见且核心的Java类,它们之间的依赖,构建起了整个框架。想要理解整个架构,最起码得对这些类眼熟。 1.1 SecurityContextHolder SecurityContextHolder用于存储安全上下文(security context)的信息。当前操作的用户是谁,该用户是否已经被认证,他拥有哪些角色权限…这些都被保

Spring Security基于数据库验证流程详解

Spring Security 校验流程图 相关解释说明(认真看哦) AbstractAuthenticationProcessingFilter 抽象类 /*** 调用 #requiresAuthentication(HttpServletRequest, HttpServletResponse) 决定是否需要进行验证操作。* 如果需要验证,则会调用 #attemptAuthentica

Spring Security 从入门到进阶系列教程

Spring Security 入门系列 《保护 Web 应用的安全》 《Spring-Security-入门(一):登录与退出》 《Spring-Security-入门(二):基于数据库验证》 《Spring-Security-入门(三):密码加密》 《Spring-Security-入门(四):自定义-Filter》 《Spring-Security-入门(五):在 Sprin

Java架构师知识体认识

源码分析 常用设计模式 Proxy代理模式Factory工厂模式Singleton单例模式Delegate委派模式Strategy策略模式Prototype原型模式Template模板模式 Spring5 beans 接口实例化代理Bean操作 Context Ioc容器设计原理及高级特性Aop设计原理Factorybean与Beanfactory Transaction 声明式事物

大模型研发全揭秘:客服工单数据标注的完整攻略

在人工智能(AI)领域,数据标注是模型训练过程中至关重要的一步。无论你是新手还是有经验的从业者,掌握数据标注的技术细节和常见问题的解决方案都能为你的AI项目增添不少价值。在电信运营商的客服系统中,工单数据是客户问题和解决方案的重要记录。通过对这些工单数据进行有效标注,不仅能够帮助提升客服自动化系统的智能化水平,还能优化客户服务流程,提高客户满意度。本文将详细介绍如何在电信运营商客服工单的背景下进行

Java进阶13讲__第12讲_1/2

多线程、线程池 1.  线程概念 1.1  什么是线程 1.2  线程的好处 2.   创建线程的三种方式 注意事项 2.1  继承Thread类 2.1.1 认识  2.1.2  编码实现  package cn.hdc.oop10.Thread;import org.slf4j.Logger;import org.slf4j.LoggerFactory

hdu1043(八数码问题,广搜 + hash(实现状态压缩) )

利用康拓展开将一个排列映射成一个自然数,然后就变成了普通的广搜题。 #include<iostream>#include<algorithm>#include<string>#include<stack>#include<queue>#include<map>#include<stdio.h>#include<stdlib.h>#include<ctype.h>#inclu