Quantcast
Channel: 涂0实验室 »思考
Viewing all articles
Browse latest Browse all 14

AOP简介,理解AOP

$
0
0

AOP简介

一. AOP字面意思

AOP = Aspect Oriented Programming 中文意为: 面向切面编程

要掌握AOP就得从aspect,即切面,入手,理解什么是切面,为什么要切面,如何使用切面。

二. 没有AOP的日子

OOP = Object Oriented Programming 面向对象的编程。

兴起于上个世纪九十年代的面向对象编程理念,仍旧是现在编程思想的主流。面向对象的核心思想是抽像问题域的模型成对象,用程序去模拟对象以解决问题。但是在软件系统中还存在着一些问题,它们不易于,或者不适合于用面向对象地方式来解决,比如事务,安全,日志等。看下面:

public class Tool1 {
	public void doSomething() {
		System.out.println("I am doing...");
	}
}
public class Tool2 {
	public void doSomething () {
		System.out.println("I am do some other things...");
	}
}
public class Main() {
	public static void main(String[] args) {
		Tool1 t1 = new Tool1();
		Tool2 t2 = new Tool2();
		t1.doSomething();
		t2. doSomething ();
	}
}

例1

这个程序很简单,容易看得懂。现在新的要求来了,要求在执行Tool1和Tool2的两个方法前后说一句话。如果用过程式的编程方法,我们只要改一下main方法,让它变成这样,看例2:

public class Main() {
	public static void main(String[] args) {
		Tool1 t1 = new Tool1();
		Tool2 t2 = new Tool2();
		System.out.println("Going to do something...");
		t1.doSomething();
		System.out.println("Did something...");
		System.out.println("Going to do something...");
		t2. doSomething ();
		System.out.println("Did something...");
	}
}

例2

很好,实现目标了,但这存在以下问题:

  • 如果还有其它不是main的调用者使用Tool1和Tool2,我们需要改动所有调用者代码
  • 完成同样的事情却将代码散落在多个地方,复制粘贴不是一个编程好习惯

下面我们来解决这两个问题。为了解决第一个问题,我们可以把代码改动由调用者转移到被调用者里,如:

public class Tool1 {
	public void doSomething() {
		System.out.println("Going to do something...");
		System.out.println("I am doing...");
		System.out.println("Did something...");
	}
}
public class Tool2 {
	public void doSomething () {
		System.out.println("Going to do something...");
		System.out.println("I am do some other things...");
		System.out.println("Did something...");
	}
}
public class Main() {
	public static void main(String[] args) {
		Tool1 t1 = new Tool1();
		Tool2 t2 = new Tool2();
		t1.doSomething();
		t2. doSomething ();
	}
}

例3

好,这样就不需要改变第个调用者的代码了。但是为了实现同样的功能,还是用了太多的代码,好,我们利用伟大的面向对向的能力吧!

public abstract class abstractTool {
	public void doSomething() {
		System.out.println("Going to do something...");
		doSomethingReally();
		System.out.println("Did something...");
	}
	protected abstract void doSomethingReally();
}
public class Tool1 extends abstractTool {
	public void doSomethingReally() {
		System.out.println("I am doing...");
	}
}
public class Tool2 extends abstractTool {
	public void doSomethingReally() {
		System.out.println("I am do some other things...");
	}
}
public class Main() {
	public static void main(String[] args) {
		Tool1 t1 = new Tool1();
		Tool2 t2 = new Tool2();
		t1.doSomething();
		t2.doSomething ();
	}
}

例4

好了,这样实现新功能的代码被集在了一个抽象类的方法里了,没有在散落开来,易于维护了。这样的方案是不是就是完美的了呢?不,还不够,它存在着一个严重问题,

  • 必须得修改被调用者的代码
  • 强迫被调用者继承于某个类,这样它将不能再继承其它类

这样做显然很不灵活,假如还有Tool3 Tool4 Tooln…,它们有一些需要这样的功能,有一些不需要,甚至于有时有一些需要,有时有一些不需要,怎么办?我们只好每次变动时都做这样的修改,那太痛苦了。好吧,还有更好的方案吗?也许一开始设计得就有些问题吧?嗯,是的,我们采用工厂模式吧,面向接口编程吧!于是就有了下面的代码:

public interface Tool {
	public void doSomeThing();
}
public class Tool1 implements Tool {
	public void doSomethingReally() {
		System.out.println("I am doing...");
	}
}
public class Tool2 implements Tool {
	public void doSomethingReally() {
		System.out.println("I am do some other things...");
	}
}
public class ToolProxy implements Tool{
	private Tool tool;
	public ToolProxy(tool) {
		this.tool =tool;
	}
	public void doSomething() {
		System.out.println("Going to do something...");
		this.tool.doSomething();
		System.out.println("Did something...");
	}
}
public class ToolFactory {
	public static Tool getTool(String type) {
		if("Tool1".equals(type)) {
			return new ToolProxy(new Tool1());
		} else if("Tool2".equals(type)) {
			return new ToolProxy(new Tool2());
		}
		return null;
	}
}
public class Main() {
	public static void main(String[] args) {
		Tool t1 = ToolFactory.getTool("Tool1");
		Tool t2 = ToolFactory.getTool("Tool2");
		t1.doSomething();
		t2.doSomething ();
	}
}

例5

接口使被调用者不再需要继承于某个类,也使得调用者和被调用者之间解耦。工厂负责组装类,我们可以在工厂里实现通过xml来配置等功能。ToolProxy代理类把新功能性代码集中在一起,便于管理。可惜这个方案还是有些问题的,假如我们不能改动被调用代码,不能强制人家实现某个接口呢?好吧,我们再使用一点花招吧,看下面代码:

public interface Tool {
	public void doSomeThing();
}
public class Tool1Wrapper implements Tool {
	public Tool1 tool1;
	public Tool1Wrapper() {
		tool1 = new Tool1();
	}
	public void doSomething() {
		tool1.doSomething();
	}
}
public class Tool2Wrapper implements Tool {
	public Tool2 tool2;
	public Tool2Wrapper() {
		tool2 = new Tool2();
	}
	public void doSomething() {
		tool2.doSomething();
	}
}
public class Tool1{
	public void doSomething() {
		System.out.println("I am doing...");
	}
}
public class Tool2{
	public void doSomething() {
		System.out.println("I am do some other things...");
	}
}
public class ToolProxy implements Tool{
	private Tool tool;
	public ToolProxy(tool) {
		this.tool =tool;
	}
	public void doSomething() {
		System.out.println("Going to do something...");
		this.tool.doSomething();
		System.out.println("Did something...");
	}
}
public class ToolFactory {
	public static Tool getTool(String type) {
		if("Tool1".equals(type)) {
			return new ToolProxy(new Tool1Wrapper());
		} else if("Tool2".equals(type)) {
			return new ToolProxy(new Tool2Wrapper());
		}
		return null;
	}
}
public class Main() {
	public static void main(String[] args) {
		Tool t1 = ToolFactory.getTool("Tool1");
		Tool t2 = ToolFactory.getTool("Tool2");
		t1.doSomething();
		t2.doSomething ();
	}
}

例6

当你看完这段代码后,如果你想要抓狂,那就抓吧,我能够理解。为了实现这点新功能,我们做了如此大的努力!是不是这样就结束了呢?这就是最终的完美方案了吗?显然还不够:

  • 代码太长
  • 如果有多个类,就需要写多个Wrapper类
  • 如果有多个接口,就需要写多个Proxy类

如果这样的代码就能够让你满意,看来你不是足够懒惰的程序员,你很勤快,愿意为实现这样的功能敲打出一堆代码。但是伟大的懒惰程序员们并不没有停下来。

三. AOP是对OOP的补充

当写完上面的代码后,我们该静下来想一想,我们到底想要对程序做什么,为什么会这样麻烦呢?

我们想要什么?我们想要就是能在程序的调用者和被调用者之间切开一个面,加入新的功能!上面的例子,我们就是想在调用Tool1和Tool2的doSomething()方法时,将调用过程切开一个面,分别在进入和出来时放入两句代码而已。而现在的程序语言恰恰缺少这样把调用过程切开的能力,所以我们需要新的语言,新的能力!这个编程的思想就是AOP,这种能力就是AOP的能力,能够这样做的语言就是AOP的语言。

AOP是对OOP的一个补充,正是OOP缺少了这样的能力,而人们又有这样的需要,所以才会有AOP。AOP并不能代替OOP,正如OOP不能代替过程式编程一样。

四. 理解AOP的语义

AOP中最关键的就是aspect,即切面。切面是以前的语言没有定义的新的概念。要理解切面,就要理解切面几个要素:

1. 切谁 即,对谁的调用会被切开,我们想要加入的新的功能正是针对于这个被切的对象的。

2. 切什么地方 被调用者会可能有多个被外部访问的出入口,为了实现我们的功能,需要被切的那个出入口。

3. 添加什么样的功能 将程序切开,目的是在被切开的地方加入新的功能。

理想状况下,所有的对象都能被切,不管这个对象是什么类型的,是不是final的,是不是nested的等;对象的所有地方都能切,不管是在构建时,调用方法时,调用属性时,调用前,调用后等;任意功能都能添加,切开之后,做什么都可以。

AOP有各种不同的实现,不同实现的能力之间的区别就可以通过上面的三条来衡量。功能最强的AspectJ,可以对任意对象的创建,方法调用,属性调用时做切面,加入任意功能。但是一般情况下我们不需要那么强的能力,能够实现对象的方法调用的切入,就已经够用了,spring里大量使用了这样的能力。

为了使用的方便,一般对方法的切入,分为以下几种:

1. Before 在方法调用前执行,不能控制返回值

2. After 在方法调用后执行,可以控制返回值

3. Throw 在方法异常退出时执行

4. Around 兼有以上三种的能力

五. AOP 实现原理举例

实现AOP有很多种不同方法,大体上可以分为三类:

1. 修改由源程序生的class文件,加入新功能

这一类的代表是AspectJ项目。它在java语言的基础上添加的新的语言和语法。AspectJ有自己的编译器,它会在调用java的编译器生成class文件后,修改class文件。这种方法实现的AOP是最高效的,因为功能的添加是在编译器就加入了,也是功能最强的,所有的地方都可以切入,缺点是需要学习新的语法和需要额外的编译步骤。

2. 动态生成类的子类,通过方法覆盖来添加新的功能

Spring使用cglib在运行时生成类的子类来实现一部分AOP。比如对于上面例子中的Tool1类,spring可以在运行时动态生成一个新的类,类似于:

public class Tool1$4535354 extends Tool1{
	public void doSomething {
		System.out.println("Going to do something...");
		super.doSomething();
		System.out.println("Did something...");
	}
}

例7

这种方法需要被调用对象的类是可以被继承的,方法是可以被覆盖的,还好这一条基本上都能满足,因为大部情况下我们面对的都是普通的java bean。

3. 借助于接口和动态代理类,添加新功能。

Spring在处理带有接口的类的时候采用这种方式。JDK里有一个类java.lang.reflect.Proxy 可以动态地生
成一个实现了某个接口的代理类。例如,spring实现了一个类似于下面的类:
public class ProxyFactory {
	public static Object getProxy(Class[] interfaces,
		Object target) {
		Object proxy = Proxy.newProxyInstance(
			target.getClass().getClassLoader(),
			interfaces,
			new MyInvocationHandler(target);
		);
		return proxy;
	}
	static class MyInvocationHandler {
		private Object target;
		public MyInvocationHandler(Object target) {
			this.target = target;
		}
		public Object invoke(
			Object proxy, Method method, Object[] args)
				throws Throwable {
			System.out.println("Going to do something...");
			Object o = method.invoke(target, args);
			System.out.println("Did something...");
			return o;
		}
	}
}

进一步的,我们还可以修改MyInvocationHanlder使其动态化,在invoke方法里执行抽像化的before after around throw 等建议,这里就写了。有了这个动态代理类,我们就可以不用实现很多的Proxy类了。

六. AOP的标准

为了能够统一对AOP使用的术语和API,国际上出现了AOP相关的组织,比如项目aopalliance等。Spring在使用的aop时就采用这个项目制定的api。关于标准的内容,可以自行去查看相关资料。

七. 在spring中使用AOP

Spring里大量使用AOP,为了能更简单地表达AOP,可以通过xml或annotation对AOP进行描述。详细请看spring的手册。

<tx:advice id="daoTxAdvice" transaction-manager="transactionManager">
	<tx:attributes>
		<tx:method name="find*" read-only="true" propagation="REQUIRED" />
		<tx:method name="check*" read-only="true" propagation="REQUIRED" />
		<tx:method name="*" propagation="REQUIRED" />
	</tx:attributes>
</tx:advice >
<aop:config>
	<aop:pointcut id="daoOperation" expression="execution(public * com.**.dao.*Dao.*(..))" />
	<aop:advisor advice-ref="daoTxAdvice" pointcut-ref="daoOperation" />
	<aop:aspect ref="permissionCheckBean" order="0">
		<aop:pointcut id="permissionCheckPointCut" expression="execution(public String com.**.action.*Action.execute*())" />
                <aop:around method="checkPermission" arg-names="pjp " pointcut-ref="permissionCheckPointCut"/>
	</aop:aspect>
</aop:config>

例9

上面的配置中<aop:pointcut>标签定义了一个bean,它能够说明切什么对象的什么地方。比如,expression=”execution(public String com.**.action.*Action.execute*()) and @annotation(requirePermission) 说明在对方公开的反回值为String的,包名符使表达式com.liba.**.action的,类名符合*Action的,方法名以execute开头的,无参数的,这样的方法进行切入。然后<aop:around>标签则表达了,对于上面那样的方法以around方式切入,切入后,在切开的地方执行permissionCheckBean的 checkPermission方法。


Viewing all articles
Browse latest Browse all 14

Trending Articles