Java 面向切面编程(Aspect Oriented Programming,AOP)
本文内容
- 实例
- 引入
- 原始方法
- 装饰者模式
- JDK 动态代理和 cglib 代理
- 直接使用 AOP 框架——AspectWerkz
最近跳槽了,新公司使用了 AOP 相关的技术,于是查点资料,复习一下。之前,多少知道点,但没怎么在实际项目中使用过~
下载 demo
实例
引入
package com.cap.aop;
public interface ICalculator {
public double add(double num1, double num2) throws Exception;
public double sub(double num1, double num2) throws Exception;
public double div(double num1, double num2) throws Exception;
public double mul(double num1, double num2) throws Exception;
}
package com.cap.aop;
/**
* 加减乘除
* */
public class Calculator implements ICalculator {
@Override
public double add(double num1, double num2) {
double result = num1 + num2;
return result;
}
@Override
public double sub(double num1, double num2) {
double result = num1 - num2;
return result;
}
@Override
public double div(double num1, double num2) {
double result = num1 / num2;
return result;
}
@Override
public double mul(double num1, double num2) {
double result = num1 * num2;
return result;
}
}
定义 ICalculator 接口和 Calculator 类,并且 Calculator 也继承 ICalculator。
若要为这个类添加“日志”功能该如何做?日志在实际项目中很有必要,比如数据库日志,业务日志等等,通过日志就能知道数据库和业务存在的问题,这要比调试程序容易多了,此外还有性能统计,安全控制,事务处理,异常处理等等都是类似问题。
我们最可能想到的是,在类的每个方法内都写日志相关的代码,或是在该类的基类中写,在其子类中继承。
原始方法
package com.cap.aop;
/**
* 加减乘除,原始方式
* */
public class CalculatorOriginalWay implements ICalculator {
@Override
public double add(double num1, double num2) {
System.out.println("the method [add()]" + "begin with args (" + num1
+ "," + num2 + ")");
double result = num1 + num2;
System.out.println("the method [add()]" + "end with result (" + result
+ ")");
return result;
}
@Override
public double sub(double num1, double num2) {
System.out.println("the method [sub()]" + "begin with args (" + num1
+ "," + num2 + ")");
double result = num1 - num2;
System.out.println("the method [sub()]" + "end with result (" + result
+ ")");
return result;
}
@Override
public double div(double num1, double num2) {
System.out.println("the method [div()]" + "begin with args (" + num1
+ "," + num2 + ")");
double result = num1 / num2;
System.out.println("the method [div()]" + "end with result (" + result
+ ")");
return result;
}
@Override
public double mul(double num1, double num2) {
System.out.println("the method [mul()]" + "begin with args (" + num1
+ "," + num2 + ")");
double result = num1 * num2;
System.out.println("the method [mul()]" + "end with result (" + result
+ ")");
return result;
}
}
这样做的缺点显而易见,重复代码太多,耦合也不好。要是该类只针对正数运算呢,还要对变量做检查,代码如下所示:
package com.cap.aop;
/**
* 加减乘除,只对正数
* */
public class CalculatorForPositiveNumber implements ICalculator {
@Override
public double add(double num1, double num2) throws Exception {
this.argsValidatior(num1);
this.argsValidatior(num2);
System.out.println("the method [add()]" + "begin with args (" + num1
+ "," + num2 + ")");
double result = num1 + num2;
System.out.println("the method [add()]" + "end with result (" + result
+ ")");
return result;
}
@Override
public double sub(double num1, double num2) throws Exception {
this.argsValidatior(num1);
this.argsValidatior(num2);
System.out.println("the method [sub()]" + "begin with args (" + num1
+ "," + num2 + ")");
double result = num1 - num2;
System.out.println("the method [sub()]" + "end with result (" + result
+ ")");
return result;
}
@Override
public double div(double num1, double num2) throws Exception {
this.argsValidatior(num1);
this.argsValidatior(num2);
System.out.println("the method [div()]" + "begin with args (" + num1
+ "," + num2 + ")");
double result = num1 / num2;
System.out.println("the method [div()]" + "end with result (" + result
+ ")");
return result;
}
@Override
public double mul(double num1, double num2) throws Exception {
this.argsValidatior(num1);
this.argsValidatior(num2);
System.out.println("the method [mul()]" + "begin with args (" + num1
+ "," + num2 + ")");
double result = num1 * num2;
System.out.println("the method [mul()]" + "end with result (" + result
+ ")");
return result;
}
private void argsValidatior(double arg) throws Exception {
if (arg < 0)
throw new Exception("参数不能为负数");
}
}
这也仅仅是一个类而已,实际项目中那么多类,要是都这么干,显然不现实,那么如何改进?——设计模式“装饰者模式”,在不必改变原类文件和继承的情况下,动态地扩展一个对象的功能。
装饰者方法
package com.cap.aop;
/**
* 加减乘除,装饰者模式
*/
public class CalculatorDecorator implements ICalculator {
private ICalculator cal;
public CalculatorDecorator(ICalculator iCalculator) {
cal = iCalculator;
}
@Override
public double add(double num1, double num2) throws Exception {
System.out.println("the method [add()]" + "begin with args (" + num1
+ "," + num2 + ")");
double result = cal.add(num1, num2);
System.out.println("the method [add()]" + "end with result (" + result
+ ")");
return result;
}
@Override
public double sub(double num1, double num2) throws Exception {
System.out.println("the method [sub()]" + "begin with args (" + num1
+ "," + num2 + ")");
double result = cal.sub(num1, num2);
System.out.println("the method [sub()]" + "end with result (" + result
+ ")");
return result;
}
@Override
public double div(double num1, double num2) throws Exception {
System.out.println("the method [div()]" + "begin with args (" + num1
+ "," + num2 + ")");
double result = cal.div(num1, num2);
System.out.println("the method [div()]" + "end with result (" + result
+ ")");
return result;
}
@Override
public double mul(double num1, double num2) throws Exception {
System.out.println("the method [mul()]" + "begin with args (" + num1
+ "," + num2 + ")");
double result = cal.mul(num1, num2);
System.out.println("the method [mul()]" + "end with result (" + result
+ ")");
return result;
}
}
这个方法比上面的强点,用原来的类 Calculator 创建一个新类 CalculatorDecorator,也继承 ICalculator 接口,这样,在对原来的类不做任何修改的情况下,在新类中添加日志功能。但该方法也有弱点,需要为每个类都应用“装饰者模式”,工作量也不小。如何解决?——代理。
JDK 动态代理和 cglib 代理
JDK 从 1.3 版本开始,引入了动态代理。JDK 动态代理非常简单,但动态代理的对象必须是一个或多个接口。
若想代理类,就需要使用 cglib 包。cglib 是一个强大的、高性能的代码生成包,cglib 包的底层是通过使用一个小而快的字节码处理框架 ASM,来转换字节码并生成新的类,cglib 被许多 AOP 框架使用,例如 Spring 的 AOP;Hibernate 使用 cglib 来代理单端 single-ended(多对一和一对一)关联;EasyMock 通过使用模仿(moke)对象来测试 java 包……它们都通过 cglib 来为那些没有接口的类创建模仿(moke)对象。
JDK 动态代理
package com.cap.aop;
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;
import java.lang.reflect.Proxy;
import java.util.Arrays;
/**
* 加减乘除,JDK 代理<br/>
* 只能代理接口,不能代理类
*
* */
public class CalculatorInvocationHandler implements InvocationHandler {
// 动态代理只有在运行时才知道代理谁,所以定义为Object类型
private Object target = null;
/**
* 通过构造函数传入原对象
*
* @param target
* 要代理的对象
*/
public CalculatorInvocationHandler(Object target) {
this.target = target;
}
/**
* InvocationHandler 接口的 invoke 方法,调用代理的方法
*
* @param proxy在其上调用方法的代理实例
* @param method拦截的方法
* @param args拦截的参数
* */
@Override
public Object invoke(Object proxy, Method method, Object[] args)
throws Throwable {
System.out.println("JDK proxy...");
// 日志开始
System.out.println("the method [" + method.getName() + "]"
+ "begin with args (" + Arrays.toString(args) + ")");
Object result = method.invoke(this.target, args);
// 日志结束
System.out.println("the method [" + method.getName() + "]"
+ "end with result (" + result + ")");
return result;
}
/**
* 获取代理类
*/
public Object getProxy() {
return Proxy.newProxyInstance(target.getClass().getClassLoader(),
target.getClass().getInterfaces(),
new CalculatorInvocationHandler(target));
}
}
cglib 代理
package com.cap.aop;
import java.lang.reflect.Method;
import java.util.Arrays;
import net.sf.cglib.proxy.Enhancer;
import net.sf.cglib.proxy.MethodInterceptor;
import net.sf.cglib.proxy.MethodProxy;
/**
* 加减乘除,cglib 代理<br/>
* 能代理接口和类,不能代理final类
*/
public class CalculatorInterceptor implements MethodInterceptor {
private Object target;
public CalculatorInterceptor(Object target) {
this.target = target;
}
@Override
public Object intercept(Object proxy, Method method, Object[] args,
MethodProxy invocation) throws Throwable {
System.out.println("cglib proxy...");
// 日志开始
System.out.println("the method [" + method.getName() + "]"
+ "begin with args (" + Arrays.toString(args) + ")");
Object result = invocation.invoke(target, args);
// 日志结束
System.out.println("the method [" + method.getName() + "]"
+ "end with result (" + result + ")");
return result;
}
public Object proxy() {
return Enhancer.create(target.getClass(), new CalculatorInterceptor(
target));
}
}
package com.cap.aop;
public class Client {
public static void main(String[] args) throws Exception {
ICalculator calculatorProxy = (ICalculator) new CalculatorInvocationHandler(
new Calculator()).getProxy();
calculatorProxy.add(10, 10);
Calculator calculator = (Calculator) new CalculatorInterceptor(
new Calculator()).proxy();
calculator.add(20, 20);
}
}
运行结果:
JDK proxy...
the method [add]begin with args ([10.0, 10.0])
the method [add]end with result (20.0)
cglib proxy...
the method [add]begin with args ([20.0, 20.0])
the method [add]end with result (40.0)
直接使用 AOP 框架——AspectWerkz
利用 AOP 框架,你只需要利用注释和 Aspect 就可以完成上面的所有工作。
AOP,Aspect Oriented Programming,称为“面向切面编程”,AOP 是 OOD/OOP 和 GoF 的延续,GoF 孜孜不倦的追求是调用者和被调用者之间的解耦,提高代码的灵活性和可扩展性,AOP 的目标也是一样。
AOP 是一种通过预编译和运行时动态代理实现在不修改源代码的情况下给程序动态统一添加功能的技术。利用 AOP 可以对业务逻辑的各个部分进行隔离,从而使得业务逻辑各部分之间的耦合度降低,提高程序的可重用性,同时提高了开发效率。在 Spring 中,通过分离应用的业务逻辑与系统级服务,应用对象只完成业务逻辑,不负责(甚至是意识)其它的系统级关注点,例如日志、事务、审计等等。
本文最开始的“日志功能”,就是一个切面。
AOP 主要应用在日志记录,性能统计,安全控制,事务处理,异常处理等等,将它们从业务逻辑代码中分离,将它们独立到非业务逻辑的方法中,进而在改变这些行为时不影响业务逻辑的代码。
AOP 与 OOP/OOD
AOP(面向切面编程)与 OOP(面向对象编程)字面上虽然类似,但却是面向不同领域的两种设计思想。
OOP 针对业务中的实体及其属性和行为进行抽象封装,以便划分逻辑单元。而 AOP 则是针对业务中的“切面”进行提取,它面对的是处理过程中的某个步骤或阶段,以获得各部分之间低耦合性的隔离效果。因此,这两种设计思想有着本质的差异。
简单来说,对“雇员”这个业务实体进行封装,是 OOP 的任务,我们可以建立一个“Employee”类,并将“雇员”相关的属性和行为封装其中。而用 AOP 对“雇员”进行封装将无从谈起;权限检查也是如此,它是 AOP 的领域。
换而言之,OOD/OOP 面向名词领域,AOP 面向动词领域。
很多人在初次接触 AOP 的时候可能会说,AOP 能做到的,一个定义良好的 OOP 接口也能,这个观点是值得商榷。AOP 和定义良好的 OOP 可以说都是用来解决并且实现需求中的横切问题。但对于 OOP 中的接口来说,它仍然需要我们在相应的模块中去调用该接口中相关的方法,这是 OOP 所无法回避的,并且一旦接口不得不进行修改的时候,所有事情会变得一团糟;AOP 则不会,你只需要修改相应的 Aspect,再重新 weave(编织)即可。 AOP 也绝对不会取代 OOP。核心的需求仍然会由 OOP 来加以实现,而 AOP 将会和 OOP 整合起来,扬长避短。
AOP 涉及的概念
下面概念在 AspectWerkz 的注释中都有所体现。
- Aspect: Aspect 声明类似于 Java 中的类声明,在 Aspect 中会包含着一些 Pointcut 以及相应的 Advice。
- Joint point:表示在程序中明确定义的点,典型的包括方法调用,对类成员的访问以及异常处理程序块的执行等等,它自身还可以嵌套其它 joint point。
- Pointcut:表示一组 joint point,这些 joint point 或是通过逻辑关系组合起来,或是通过通配、正则表达式等方式集中起来,它定义了相应的 Advice 将要发生的地方。
- Advice:Advice 定义了在 pointcut 里面定义的程序点具体要做的操作,它通过 before、after 和 around 来区别是在每个 joint point 之前、之后还是代替执行的代码。
Java 的 AOP 框架
- AspectWerkz 是简单、动态、轻量级、强大的 AOP 框架,更容易的集成 AOP 的项目中。
- JBoss AOP 是 JBoss 4.0 带了一个 AOP 框架,但也能够在你的应用中单独的运行它。
- Nanning。Nanning Aspects is a simple yet scaleable aspect-oriented framework for Java. Nanning is also nice "little" town in Guanxi province in southern China. It's about 1-2 million inhabitants which barely qualifies it as a town by chinese standards. It is not to be confused with the much larger Nanking/Nanjing.
- JAC,Java Aspect Components 是一个应用服务器。它为 Java 2 平台、J2EE和基于 Web 的分布式应用,提供开放式资源的又一个选择。JAC 包括统一模型语言(UML)IDE,该 IDE 模块化应用商业逻辑,并自动生成和编译纯商业逻辑 Java 类。这些类,在 JAC 容器内执行,可从一组技术和/或商业的横切关系 (crosscutting concerns),如数据持久性、认证、配置文件管理、访问权限检测、演示和负载平衡中无缝地受益。基于 AOP 的 JAC 将这些关系(concerns)从应用程序的核心商业逻辑中分离出来。
- DynamicAspects。This project is no longer maintained! DynamicAspects enables you to do aspect-oriented programming in pure Java. Using the "instrumentation" and "agent" features introduced with Sun JDK 1.5, aspects can be installed and deinstalled during runtime!
- CAESAR。CaesarJ is a new Java based programming language, which facilitates better modularity and development of reusable components. The components are collaborations of classes, but they can modularize crosscutting features or non-functional concerns. Caesar language features help to implement, abstract and integrate such components. Caesar can be used in combination with plain Java. Tool support is available in the form of an Eclipse plugin.
- PROSE 是一个动态编排(weaving)工具,允许在运行时插入或抽取 aspects。PROSE aspects 是规则的 Java 对象能够被发送到或从网络上的计算机接收。签名可被用于保证它们的完整性。一旦一个 aspect 插入到 JVM 中,任何事件的发生将影响在相应 aspect advice 执行的结果。假如一个 aspect 从 JVM 中撤消,aspect 代码将被丢弃并且相应的拦截也将不会再发生。
- FastAOP。FastAOP is an very high performant AOP (Aspect Oriented Programming) framework for java. The framework was initially
developped to support performance profiling and monitoring for large J2EE applications with nearly no runntime overhad.
.Net 的 AOP 框架
- Encase 是 C# 编写开发的为 .NET 平台提供的 AOP 框架。Encase 独特的提供了把方面(aspects)部署到运行时代码,其它 AOP 框架依赖配置文件的方式。这种部署方面(aspects)的方法帮助缺少经验的开发人员提高开发效率。
- NKalore 是这款编程语言,扩展了 C#,允许在 .NET 平台使用 AOP。NKalore 的语法简单、直观,编译器是基于 Mono C#编译器(MCS)。NKalore 目前只能在命令行或 #Develop 内部使用。NKalore 兼容公共语言规范(Common Language Specification,CLS),可以在任何 .NET 开发环境中使用,包括微软的 Visual Studio .NET。
- PostSharp 读取 .NET 字节模块,转换成对象模型。让插件分析和转换这个模型并写回到MSIL。PostSharp 使开发程序分析应用程序容易得像分析代码规则和设计模式,它使程序开发的思想变革为面向方面软件开发(AOSD/AOD)思想。
- AspectDNG 的目标是为 .NET 开发人员提供简单而功能强大的 AOP-GAOP 实现。它效仿 java 下的开源工具 AspectJ 和 Spoon,成熟程度也很接近它们。
- RAIL,Runtime Assembly Instrumentation Library,开源项目可以在 C# 程序集加载和运行前进行处理控制调整和重新构建。C#在 CLR 中,我们已经能够动态加载程序集并且获得程序集中的类和方法,RAIL(Runtime Assembly Instrumentation Library)的出现填补了CLR处理过程中的一些空白。
- SetPoint 是一款 .NET 框架下的全功能 AOP 引擎,它着重为称为语义切点(semantic pointcuts)的定义依赖 RDF/OWL 的使用,它的功能为一个 IL-level,highly dynamic weaver&LENDL,,一个引人注目的定义语言 DotNetAOP 为 CLR language 提供 AOP 框架基础属性。
- NAop 是一个 .NET 下的 AOP 框架。
- AspectSharp 是 .NET 下的免费 AOP框架,它以 Dynamic Proxies 和 XML 作为配置文件。