网上由很多关于AOP的解释,但是,它最大的好处是什么,它的作用是什么?
众说纷纭,有的解释甚至很扯淡。例如,百度百科上有这样一段:
这给我的感觉就是——人类的主要功能:吃、喝、玩、乐。 -_-!
要解释这个问题,得先回答,为什么要有AOP。
AOP的历史
有人说AOP(Aspect Oriented Program)是基于OOP (Object Oriented Program)来的,这个,我感觉貌似无法考究。而且即便是传统的Structured Program也未可不能进化出AOP的用法。因为它只是一个思想。我们来看,这个思想具体是什么。
有如下代码:
package com.edi.poc;
public interface Hall {
void open();
void close();
void visit(Entrant e);
}
package com.edi.poc.ifc;
public enum HALL_NAME {
DINOSAUR, CAT;
}
package com.edi.poc.ifc;
import com.edi.poc.Zoo;
public interface Entrant {
String getName();
void visit(Zoo zoo, HALL_NAME hallName);
}
package com.edi.poc;
import com.edi.poc.ifc.Entrant;
import com.edi.poc.ifc.HALL_NAME;
public class Tourer implements Entrant {
private String name;
public Tourer(String name)
{
this.name = name;
}
public boolean isCharged() {
return false;
}
public String getName() {
return name;
}
public void visit(Zoo zoo, HALL_NAME hallName) {
zoo.enter(this, hallName).visit(this);
}
}
package com.edi.poc;
import java.util.HashMap;
import java.util.Map;
import com.edi.poc.ifc.HALL_NAME;
import com.edi.poc.ifc.Hall;
import com.edi.poc.ifc.Entrant;
public class Zoo {
private String name;
private Map<HALL_NAME, Hall> halls;
public Zoo(String name)
{
this.name = name;
this.halls = new HashMap<HALL_NAME, Hall>();
halls.put(HALL_NAME.DINOSAUR, new DinoHall());
}
public void open()
{
for(Hall h:halls.values())
{
h.open();
}
System.out.println("The "+ name + " Zoo " + "is opened.");
}
public void close()
{
for(Hall h:halls.values())
{
h.close();
}
System.out.println("The "+ name + " Zoo " + "is closed.");
}
public Hall enter(Entrant e, HALL_NAME hallName)
{
return this.halls.get(hallName);
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public static void main(String[] args) {
Entrant jack = new Tourer("jack");
Zoo zoo = new Zoo("People");
zoo.open();
jack.visit(zoo, HALL_NAME.DINOSAUR);
zoo.close();
}
}
典型的OOP。实现了一个动物园开放和关门,以及一个游客Jack到恐龙馆晃了一圈。
输出
Dinosaur hall is opened.
The People Zoo is opened.
jack visited diano hall.
Dinosaur hall is closed.
The People Zoo is closed.
现在,新的需求来了,动物园得赚钱才能维持,得收费。怎么办?有人说,那就在开放时,加一个收费站收门票费。
游客不干了,我来是看恐龙的,不是看猫的。我家就有猫,就算没你这品种多,也不能跟恐龙一个价啊。
场馆也不干了,恐龙馆说,我养只恐龙喂的东西,跟猫馆不一个价啊。恩,所以,只能让每个馆自己定价,进门收门票费,进馆收馆费(为了简单起见,我就不再封装一个类去抽象票了)
public interface Hall {
….
void charge(Entrant e);
}
public interface Entrant {
….
boolean isCharged();
public void setCharged(boolean isCharged);
}
public class DinoHall implements Hall {
public void visit(Entrant e) {
if(!e.isCharged())
{
System.out.println(e.getName() + " needs to be charged first.");
charge(e);
}
System.out.println(e.getName() + " visited diano hall.");
}
}
public void charge(Entrant e) {
e.setCharged(true);
System.out.println("Dianosaur hall charges " + e.getName() + " $2.00");
}
}
为了加入这个功能,我们修改了Hall 和 Entrant的接口,并且所有Hall的实现类都需要实现charge()方法。显然,这里出现了代码冗余。
假如一个zoo有20个馆,20个馆的Hall实现类就有20个charge()方法,里面除了金额不同外,完全一样。
如果解决呢?一种方式是剥离charge,在zoo.enter()的时候去收费,然后封装一个ticket类,里面用Collection来表示哪些馆已经买了票。这样在进馆时就只检查该Collection中是否包含该馆就行了。
这是什么?封装! OOP做法。显然这样可以解决这个需求了,但是,wait,这样就没问题了吗?
mmm… 动物园引入了IBM的咨询(可参看IBM关于企业咨询的广告),想借此提高利润。IBM的咨询师们需要搜集数据,想获得整个动物园当天、实时的人流量,每个场馆的人流量,每个场馆的实时income等。怎么办?
有人说,简单,把这些东西封装成一个属性加到几个BEAN中去呗。跟收费一样。
但是,客流量、实时收入,这些东西是一个动物园或者场馆的有效属性吗?我们说抽象一个对象,需要考虑它的有效属性。否则,人这个抽象对象有可能就包含身份证编号。但其实这个东西是属于一个外部系统的。
好,那么,我们就可以把这些数据封装成另外一个对象,例如ConsultingData,在Zoo和Hall里面调用下这个封装类的更新方法。
OK了吗?永远不要低估一个咨询师对于信息的需求,当你辛苦改完代码后,他们会又要求你搜集更多的数据!
怎么办?继续封装?那么这个Zoo和Hall的实现类就会变得无法maintain了,被强制加入了很多它自己完全不是非常有关系的方法调用。
这个时候,有经验的工程师会自然想到——设计模式。可以用代理模式来把它做的很好。
典型代码:
package com.edi.poc;
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;
import java.lang.reflect.Proxy;
import com.edi.poc.ifc.Hall;
public class JdkProxy implements InvocationHandler{
private Object target;
public Object getProxy(Object target)
{
this.target = target;
return Proxy.newProxyInstance(
target.getClass().getClassLoader(),
target.getClass().getInterfaces(),
this);
}
public Object invoke(Object proxy, Method method, Object[] args)
throws Throwable {
System.out.println("Before ...");
// Add those operations whatever you want before it really goes
Object result = method.invoke(target, args);
// Add those operations whatever you want before it really leave
System.out.println("After ...");
return result;
}
public static void main(String[] args) {
Hall dinoHall = new DinoHall();
JdkProxy proxy = new JdkProxy();
Hall dinoHallProxy = (Hall)proxy.getProxy(dinoHall);
dinoHallProxy.open();
}
}
输出:
Before ...
Dinosaur hall is opened.
After ...
这样,我们就可以把统计的那些需求跟原始代码剥离开来。该例子使用的是JDK提供的动态代理。但它必须要求被代理类要有接口!
这种该法,其实就是AOP的本质——将流程操作截开,以拦截器的方式,实现切片组装,以灵活加入更多逻辑。
说白了——代理调用。
正常调用:
拦截调用:
回头来再看看,有了AOP,或者根本上说,代理模式调用,为我们带来了什么好处:
- 原始代码简洁,没有因为各种集成造成的复杂冗余。(想想这时候的zoo和hall是多么的纯粹)
- 根据需要灵活插入各种业务逻辑 (IBM咨询师一走,就不用搜集数据了,果断把这些插件类去掉?)
- 被插入的逻辑处理类也显得很纯粹而易于维护
凡事有利必有弊,我们再看看(可能)坏处:
- 原始流程被打断,在阅读时,增加理解难度
- 调用代理类开销可能比直接调用原始类要大 (取决于实现)
第一个显然只能通过各种文档和友好的注释来尽量解决。第二个的话,已经开发出相应工具解决掉了。
目前代理的方法有三类:
- 运行时动、静态代理 (jdk代理类,cglib等)
- 加载类时代理
- 编译时代理
后两者,b会造成加载缓慢,c会造成编译缓慢。通常,我们会选择c,但实际上两者差距并不是很大。可参见一篇专业performance比对文章 (需要翻墙),或者直接看下面图。
但其本质是一样的,都是帮我们把这些拦截插入的代码和原始代码拼接到一起,然后加载入JVM。
本文源码:
https://github.com/EdisonXu/POC/tree/master/intro-aop