浅谈Java和SAP ABAP的静态代理和动态代理,以及ABAP面向切面编程的尝试

简介:

文章目录

  • Java的静态代理
  • 静态代理的优缺点
  • ABAP的静态代理
  • Spring AOP的动态代理
  • JDK动态代理的优缺点
  • CGLIB动态代理的优缺点
  • ABAP CGLIB的模拟实现
  • ABAP Pre和Post Exit

Jerry之前一篇文章 SAP产品增强技术回顾,提到基于Java编程语言实现的SAP Commerce,借助Spring框架的支持,能使用面向切面编程的理念(Aspect Orient Programming,以下简称AOP),将业务代码和非业务代码(比如权限检查,日志记录,性能统计等)彻底分离开。

下图是某应用里方法的常规实现:权限检查,日志记录和性能检测的代码一次又一次地侵入到本应只包含业务代码的三个方法中:

下图是应用AOP之后的方法实现:三个方法体内只包含纯粹的业务代码,看起来清爽了很多。权限检查,日志记录和性能检测的代码,作为仍需关注的三个方面,以切面的方式编织到三个方法中。Weave,AOP里的术语,中文材料里经常译成“编织”,描述了被代理类的方法通过非源代码修改层面被增添以新逻辑的动作。

我们说面向对象编程(Object Oriented Programming,简称OOP)是一种理念,不同的编程语言可以有不同的实现。同理,AOP这种理念,不同的编程语言也存在不同的实现。

Java AOP的实现可以分为静态代理和动态代理两种。无论哪种代理方式,一言以蔽之,AOP的核心为,业务逻辑位于原始类中始终保持不变,而编织的非业务逻辑位于代理类中。运行时执行的代码,实际上被调用的是代理类,原始类的业务逻辑通过代理类被间接地调用。

代理模式的UML图:

业务逻辑在编译期间被编织进入代理类的方式,称为静态代理;业务逻辑在运行期间才进行编织的方式,称为动态代理。准确地说,编译期编织还可细分为编译时和编译后编织,而运行期间编织又可细分为载入时编织和运行时编织,但这种细分方式不影响本文接下来的阐述,所以后续仍只按照编译期和运行期两大类来介绍。

看一些具体的例子。

Java静态代理

定义一个IDeveloper的接口,里面包含一个writeCode的方法。创建一个Developer类,实现该方法。

测试:创建一个名为Jerry的Developer实例,调用writeCode方法。

假设我想让Developer在写代码之前,先编写对应的文档,但我不想把写文档这个逻辑,侵入到writeCode方法里。这里“编写文档”,就相当于待编织的非业务逻辑,或者叫做待编织的切面逻辑。

使用静态代理的思路,另外新建一个代理类DeveloperProxy:

注意上图的writeCode方法,首先第8行完成文档编写的任务,然后代理类在第9行调用被代理类Developer的writeCode方法,完成写代码的实际业务逻辑。

测试代码:

Developer和DeveloperProxy都实现了同一个接口IDeveloper,对于消费者代码来说,它完全感知不到也不必要去感知这两个接口实现类的内部差异——这一切对消费者代码来说完全透明。消费者拿到的引入,指向的是类型为IDeveloper接口的变量,然后调用定义在接口上的writeCode方法即可。

静态代理的优缺点

从以上例子可以看出,静态代理工作的基石是接口,如果原始类由于某种原因,无法改造成为某个接口的实现类(比如原始类来自系统遗留代码,无法重构),则静态代理这条路行不通。

针对每个原始类,采用静态代理,都需要创建一个具有持久存储的代理类。这种方式便于理解,并且非业务逻辑(前例中的“写文档”行为)在编译期间植入静态代理类,实际运行时性能优于即将介绍的动态代理。

在Java里如果不想手动创建静态代理类,可以使用工具AspectJ来自动完成。由于本文的读者主要是ABAP开发人员,这里略过其使用方式。

ABAP静态代理类的自动创建

我仿照Java AspectJ的思路,用ABAP写了一个类似的原型。下面是使用方法。

首先我创建一个类CL_HELLOWORLD:

我想自动为该类创建一个静态代理,在代理类的PRINT方法里,除了调用这个原始类的PRINT方法外,再做一些额外的逻辑,比如打印一些输出。

调用下图的GET_PROXY方法,将自动为CL_HELLOWORLD创建一个静态代理类,将第7行和第8行指定的额外逻辑编织到静态代理类的PRINT方法里:

测试:调用静态代理类的PRINT方法,得到下图的输出,能观察到编织到静态代理类的两行WRITE语句,分别在原始类PRINT方法之前和之后被调用了:

SE24可以观察到,通过我写的工具自动创建的ABAP静态类,及编织到代理类方法PRINT里的额外逻辑:


这个工具的核心是调用ABAP Class API生成新的ABAP类,源代码可以在文末Jerry提供的链接里获得:

Spring AOP的动态代理

所谓动态代理,即AOP框架在编译期不会对原始类做任何处理,而是直到应用运行期间,在内存中临时为需要被代理的类生成一个AOP对象,该对象包含了原始类的全部方法,并且在被代理的方法处做了增强处理,编织入新的逻辑,并回调原始类的方法。

Spring AOP动态代理有两种实现方式:JDK动态代理和CGLIB动态代理。

JDK动态代理

JDK动态代理的原理是基于Java反射机制实现的方法拦截器机制。

我们在第一个例子的基础上,增添一个新的ITester接口,代表测试人员这个岗位:

现在的需求是给测试人员的doTesting方法内也植入编写文档的逻辑。如果采用静态代理的方式,我们得又创建一个TesterProxy的静态代理类。随着开发小组里人员岗位类型的增加,这些静态代理类的个数也随之增加。

那么用动态代理如何优雅地避免这个问题呢?

创建一个新的代理类,取名为EnginnerProxy,名字暗示了这个实现了JDK标准接口InnovationHandler的类,在运行时能统一代理一个软件开发团队里所有角色的工程师类的方法。

第七行的bind方法,接收一个被代理类的实例,在运行时动态为该实例创建一个临时的代理类实例。所谓临时,指该代理实例的生命周期只存在于当前会话中,应用运行结束后即销毁,不会像静态代理类那样会持久化存储。

运行时代理类的方法一旦执行,无论是Developer的writeCode, 还是Tester的doTesting方法,均会被EnginnerProxy的invoke方法拦截,在invoke方法内统一执行第17行的文档撰写逻辑,然后再调用18行包含了业务逻辑的原始类方法。

下图是测试代码及运行结果,现在无论是Developer还是Tester,在写代码和做测试之前,都会自动执行文档撰写的任务了:

基于JDK动态代理的优缺点

显而易见,在需要代理多个类时,动态代理只需创建一个统一的代理类,而不必像静态代理那样,需要为每个包含业务逻辑的类单独创建代理类。而代理类“用后即焚”,也避免了在工程文件夹里生成太多代理类。

另一方面,因为动态代理在运行时通过Java反射机制实现,运行时的性能劣于在编译期间进行代理逻辑编织的静态代理。此外,JDK动态代理工作的前提条件同静态代理一样,也需要被代理的类实现某个接口。

看个反例,假设产品经理类ProductOwner未实现任何接口:

使用JDK动态代理,在运行时会抛ClassCastException异常:

正因为JDK动态代理的这种局限性,存在另一种动态代理的实现方式:基于CGLIB的动态代理。

CGLIB(Code Generation Library)是一个Java字节码生成库,可以在运行时对Java类的字节码进行处理和增强,底层基于字节码处理框架ASM实现。

基于CGLIB的动态代理可以绕过JDK动态代理的限制,即使一个需要被代理的类没有实现任何接口,也能使用CGLIB动态代理。

注意这次使用CGLIB创建的统一代理类,导入的开发包来自net.sf.cglib.proxy, 而非JDK动态代理解决方案中的java.lang.reflect:

消费代码的风格同JDK动态代理类似:

CGLIB动态代理的优缺点

CGLIB克服了JDK动态代理需要被代理类必须实现某个接口才能工作的限制,然而其本身也有局限性。CGLIB本质上是运行时用API操作Java类的字节码的方式,直接创建一个继承自被代理类的子类,然后将切面逻辑编织到这个子类方法中去。显而易见,如果被代理类被定义成无法继承,比如被Java和ABAP里的final关键字修饰,则CGLIB动态代理这种方式也无法工作。

做一个测试,我将ProductOwner类标志为final,即无法被继承,这时在运行之前的测试代码,会遇到异常和错误消息:Cannot subclass final class

ABAP动态代理

因为ABAP无法在语言层面精确做到像Java JDK InnovationHandler那样能够用一个代理类统一拦截多个被代理类方法执行的效果,因此Jerry选择对另一种动态代理,即CGLIB代理方式,用ABAP进行模拟。

首先创建一个需要被代理的类,业务逻辑写在GREET方法里。

接着使用Jerry自己实现的ABAP CGLIB工具类,通过其方法GET_PPROXY得到这个类的代理类,并调用代理类的GREET方法:

上图第8行和第9行是包含了两个切面逻辑的类,我期望其方法分别在被代理类的GREET调用之前和调用之后被执行。

ABAP CGLIB的核心在GET_PROXY方法里的generate_proxy方法内:

这里使用了ABAP动态生成类的关键字GENERATE SUBROUTINE POOL, 根据内表mt_source里包含的预先拼凑好的源代码,生成新的临时类。这个类不会在SE24或者SE80里存储,仅仅存活在当前应用的会话里。

第17行动态生成新的代理类之后,第21行生成一个该代理类的实例,然后在第23和26行分别植入切面逻辑。

最后调用这个代理类实例的GREET方法,打印输出如下:

其中Hello World是原始被代理类即ZCL_JAVA_CGLIB的GREET方法的输出,而它的前后两行为调用ABAP CGLIB生成代理类时传入的切面逻辑。

到目前为止,尽管我们意识到静态代理和动态代理都各自存在一些缺陷,但从这些缺陷出现的原因,也再次提醒我们,在编写新的代码时,要尽量面向接口编程,尽量避免直接面向实现编程,从而降低程序的耦合性,提高应用的可维护性,可复用性和可扩展性。

以上介绍的ABAP CGLIB工具只是Jerry开发的一个原型,在ABAP里如果仅仅想将切面逻辑(比如权限检查,日志记录,性能分析)彻底地同业务逻辑隔离开,可以使用ABAP Netweaver提供的对类方法增强的标准方式:Pre-Exit和Post-Exit.

选中要增强的类,点击Enhance菜单:

这种增强和被代理的类是分开存储的:

创建新的Pre-Exit:

点击Pre-Exit的面板,就可以进去编写代码了:

在运行时,被代理类ZCL_JAVA_CGLIB的GREET方法执行之前,Pre-Exit里的代码会自动触发:

Jerry之前在SAP Business By Design这个产品工作的时候,在不修改产品标准代码的前提下,用这种Exit技术实现了很多的客户需求。典型的客户需求是,在SAP标准UI增添扩展字段,其值通过后台复杂的逻辑计算出来。于是我们首先把后台API的Response结构体做增强,新建一个扩展字段;然后给后台API取数方法创建一个Post-Exit,将扩展字段的填充逻辑实现在Exit里。

采用Pre和Post-Exit,虽然使用方式上和Java Spring AOP基于注解(Annotation)的工作方式相比有所差异,但从效果上看,也能实现Spring AOP将业务逻辑和非业务逻辑严格分开的需求。

本文介绍的Java和ABAP的静态和动态代理,以及ABAP模拟Java CGLIB的实现,在Jerry发布的SAP社区博客上有详细叙述:

本文提到的Jerry开发的所有ABAP原型和工具,在这个链接里有源代码。

今后如果有人聊到关于ABAP能否进行面向切面编程的话题,您或许可以提到Jerry这篇文章。感谢阅读。

本文来自云栖社区合作伙伴“汪子熙”,了解相关信息可以关注微信公众号"汪子熙"。

ABAP专题

相关实践学习
日志服务之使用Nginx模式采集日志
本文介绍如何通过日志服务控制台创建Nginx模式的Logtail配置快速采集Nginx日志并进行多维度分析。
相关文章
|
12天前
|
安全 算法 Java
深入理解Java并发编程:线程安全与性能优化
【4月更文挑战第11天】 在Java中,高效的并发编程是提升应用性能和响应能力的关键。本文将探讨Java并发的核心概念,包括线程安全、锁机制、线程池以及并发集合等,同时提供实用的编程技巧和最佳实践,帮助开发者在保证线程安全的前提下,优化程序性能。我们将通过分析常见的并发问题,如竞态条件、死锁,以及如何利用现代Java并发工具来避免这些问题,从而构建更加健壮和高效的多线程应用程序。
|
4天前
|
存储 Java
Java动态转发代理IP的实现方法
Java动态转发代理IP的实现方法
20 11
|
4天前
|
IDE Java 物联网
《Java 简易速速上手小册》第1章:Java 编程基础(2024 最新版)
《Java 简易速速上手小册》第1章:Java 编程基础(2024 最新版)
13 0
|
4天前
|
监控 Java 开发者
掌握 Java 反射和动态代理
【4月更文挑战第19天】Java反射和动态代理提供强大功能和灵活性。反射允许运行时检查和操作类,获取类信息、动态调用方法,但可能带来性能损失和降低代码可读性。动态代理则用于创建代理对象,实现透明性和横切关注点分离,常用于日志、权限检查等。两者结合能实现更复杂功能。掌握这些技术能提升代码的灵活性和可扩展性,但也需注意性能和可读性。通过学习和实践,能更好地构建高效软件系统。
|
5天前
|
安全 Java 开发者
Java并发编程:深入理解Synchronized关键字
【4月更文挑战第19天】 在Java多线程编程中,为了确保数据的一致性和线程安全,我们经常需要使用到同步机制。其中,`synchronized`关键字是最为常见的一种方式,它能够保证在同一时刻只有一个线程可以访问某个对象的特定代码段。本文将深入探讨`synchronized`关键字的原理、用法以及性能影响,并通过具体示例来展示如何在Java程序中有效地应用这一技术。
|
5天前
|
安全 Java API
java借助代理ip,解决访问api频繁导致ip被禁的问题
java借助代理ip,解决访问api频繁导致ip被禁的问题
|
5天前
|
安全 Java 调度
Java并发编程:深入理解线程与锁
【4月更文挑战第18天】本文探讨了Java中的线程和锁机制,包括线程的创建(通过Thread类、Runnable接口或Callable/Future)及其生命周期。Java提供多种锁机制,如`synchronized`关键字、ReentrantLock和ReadWriteLock,以确保并发访问共享资源的安全。此外,文章还介绍了高级并发工具,如Semaphore(控制并发线程数)、CountDownLatch(线程间等待)和CyclicBarrier(同步多个线程)。掌握这些知识对于编写高效、正确的并发程序至关重要。
|
6天前
|
缓存 分布式计算 监控
Java并发编程:深入理解线程池
【4月更文挑战第17天】在Java并发编程中,线程池是一种非常重要的技术,它可以有效地管理和控制线程的执行,提高系统的性能和稳定性。本文将深入探讨Java线程池的工作原理,使用方法以及在实际开发中的应用场景,帮助读者更好地理解和使用Java线程池。
|
6天前
|
Java API 数据库
深研Java异步编程:CompletableFuture与反应式编程范式的融合实践
【4月更文挑战第17天】本文探讨了Java中的CompletableFuture和反应式编程在提升异步编程体验上的作用。CompletableFuture作为Java 8引入的Future扩展,提供了一套流畅的链式API,简化异步操作,如示例所示的非阻塞数据库查询。反应式编程则关注数据流和变化传播,通过Reactor等框架实现高度响应的异步处理。两者结合,如将CompletableFuture转换为Mono或Flux,可以兼顾灵活性和资源管理,适应现代高并发环境的需求。开发者可按需选择和整合这两种技术,优化系统性能和响应能力。
|
7天前
|
缓存 监控 Java
Java并发编程:线程池与任务调度
【4月更文挑战第16天】Java并发编程中,线程池和任务调度是核心概念,能提升系统性能和响应速度。线程池通过重用线程减少创建销毁开销,如`ThreadPoolExecutor`和`ScheduledThreadPoolExecutor`。任务调度允许立即或延迟执行任务,具有灵活性。最佳实践包括合理配置线程池大小、避免过度使用线程、及时关闭线程池和处理异常。掌握这些能有效管理并发任务,避免性能瓶颈。