论坛首页 Java版

Log, Cache, AOP的常用谎言? (嗡嗡作响的AOP系列之二)

浏览 25561 次
该帖已经被评为精华帖
作者 正文
最后更新时间:2004-08-06
大家好, 偶又回来了, 继续嗡嗡作响的AOP之旅, 废话少说, 先来看看AOP号称给可以带给我们的第一个好东东:
1. Modularized implementation of crosscutting concerns
嘿嘿, 一堆buzzword呢: modularity, crosscutting, concern, 偶来用土话解释一下吧, 就是号称可以把原来需要在N处代码里处理的问题, 移到一处地方来处理.

为了解释这个好处呢, AOP的鼓吹者通常会拿一个日志的例子来说明 (因为它最简单, 最容易实现), 给各位举一个简单的银行帐户的例子:

package readonly.aopsucks.test;

import junit.framework.TestCase;
import readonly.aopsucks.bank.Account;
import readonly.aopsucks.bank.impl.AccountImpl;

public class BankTest extends TestCase {
    public void testDeposit() {
        Account a = new AccountImpl(1000);
        a.deposit(100);
        assertEquals(1100, a.getBalance());
    }
}


package readonly.aopsucks.bank;

public interface Account {
    public void deposit(long amount);
    public void withdraw(long amount); 
    public long getBalance();
}


package readonly.aopsucks.bank.impl;

import readonly.aopsucks.bank.Account;

public class AccountImpl implements Account {
    private long balance;

    public AccountImpl(long balance) {
        this.balance = balance;
    }

    public void deposit(long amount) {
        balance += amount;
    }

    public void withdraw(long amount) {
        balance -= amount;
    }

    public long getBalance() {
        return balance;
    }

}


测试通过了, 但是我们想记录每个方法调用的时间怎么办呀? 每个方法开始的时候和结束的时候各加一句? 作为一个AOPer, 一定会大声的嘲笑你, No No 什么年代了还用这样粗劣的方法? 来看看AOP是怎么做的 (以下以aspectwerkz这个AOP实现为例子):

先写个Aspect (又一个buzzword?):
import java.util.Date;

import org.codehaus.aspectwerkz.joinpoint.JoinPoint;

public class LogAspect {

    public void beforeCalled(JoinPoint joinPoint) {
        System.out.println((new Date()) + ": before " + joinPoint.getTargetClass().getName() + "." + joinPoint.getSignature().getName()
                + " called");
    }

    public void afterCalled(JoinPoint joinPoint) {
        System.out.println((new Date()) + ": after " + joinPoint.getTargetClass().getName() + "." + joinPoint.getSignature().getName()
                + " called");
    }
}


组合pointcut, advice (又是2个buzzword?)
<aspectwerkz>
    <system id="aopsucks">
        <package name="readonly.aopsucks.aspect">
            <aspect class="LogAspect">
                <pointcut name="allMethod" expression="execution(* readonly.aopsucks.bank...*(..))"/>
                <advice name="beforeCalled" type="before" bind-to="allMethod"/>
                <advice name="afterCalled" type="after" bind-to="allMethod"/>
            </aspect>
        </package>
    </system>
</aspectwerkz>


运行一下, 哇, 看起来好cool呀, 啧啧称奇

慢着, 回到现实世界来吧, 我们做的是实际应用, 不是这样的简单玩具, 象这种简单日志, 不就是一个触发器或拦截器的概念吗, 远古时代就有的东西, 改头换面, 加了一些buzzword, 又出来糊弄人......

实际应用中AOP可以做复杂上下文环境的日志吗, NO! 别拿玩具出来骗小孩了!



接下去来看看AOP号称给可以带给我们的第2个好东东:
2. Once and only once, more code reuse.
这个倒没有什么buzzword了, 而且也是OOP追求的终极目标, 那么来看例子把, 还是上面那个银行帐户的例子, 加了一个列出每个月的帐务清单的方法.

public interface Account {
    public String generateMonthlyReport(String month);
}


AOPer说这个方法可能比较耗时: 需要从数据库里查询一些记录, 然后封装成String返回, 我们用AOP给它加个Cache来提高性能吧:

package readonly.aopsucks.aspect;

import java.util.HashMap;
import java.util.Map;

import org.codehaus.aspectwerkz.joinpoint.JoinPoint;
import org.codehaus.aspectwerkz.joinpoint.MethodRtti;

public class CacheAspect {
    private Map cache = new HashMap();
    
    public Object cache(JoinPoint joinPoint) throws Throwable {
        MethodRtti rtti = (MethodRtti)joinPoint.getRtti();
        final Long key = new Long(calculateHash(rtti));
        Object cachedValue = cache.get(key);
        if (cachedValue == null) {
            cachedValue = joinPoint.proceed();
            cache.put(key, cachedValue);
        }else{
            System.out.println("using cache: " + cachedValue);
        }
        return cachedValue;
    }
    
    private long calculateHash(MethodRtti rtti) {
        int result = 17;
        result = 37 * result + rtti.getName().hashCode();
        Object[] parameters = rtti.getParameterValues();
        for (int i = 0, j = parameters.length; i < j; i++) {
            result = 37 * result + parameters[i].hashCode();
        }
        return result;
    }    
}


    <aspect class="CacheAspect" deployment-model="perInstance">
        <pointcut name="cacheAble" expression="execution(* readonly.aopsucks.bank...generateMonthlyReport(..))"/>
        <advice name="cache" type="around" bind-to="cacheAble"/>
    </aspect>


AOPer: 看到没? 如果需要cache其他的方法, 那么我们修改一下pontcut的匹配符, 不用修改, 增加其他任何代码, 真正的reuse吧!
哇, 又看起来很cool?

再次回到现实生活来吧, 难道Cache的东西难道不需要刷新吗? 偶今天又往这个帐户里面存了一笔钱, 你的AOP Cache如何得知要清除对应的Key, 从而列出正确的当月清单来? 不同的Instance需要不同的Cache策略, 你的AOP Cache能做到? 实际应用的复杂性不是AOP这种简单的code reuse所能消除的!

AOPer, 找些有实际意义的例子吧, 别老是拿这些幼稚的东东给偶们嘲笑了!


后记: 一个设计良好的OOP架构, 完全可以更简洁的实现上面这些幼稚的功能 ((比如xwork的拦截器), 更重要的是不需要你去了解一堆的buzzword
   
最后更新时间:2004-08-06
哈哈!很有意思!
说白了,就是看似解决了问题,其实根本没有解决问题。
说白了,只是提出一个问题解决方案,没有提出一个思路,一个框架,或者一个新技术所具有的底层理论!
AOP只是分离关注点,但并没有减少关注点!不是吗?它的提出并不是说你可以不用考虑问题的解决!恰恰相反,为了整体框架的清晰,你还要理出更多的buzzword,定义更多的xml配置,然后将这些关注点“简单”的分离到独立的
部分。具体实现的复杂都一点都没有减少,该考虑的还是要考虑,该复杂的还是要复杂,一切都没有简单。相反,为了更好的分离关注点,有可能将本来较为简单的实现变得更为复杂。(难道大家没有受够传递多个参数的折磨吗?)
感觉,AOP不是让你更简单,而是更复杂。而这一切的一切都是为了
清晰的构架,可扩展的服务,简便的维护(建立在清楚了解AOP的基础上)。
投入与产出值得我们好好比较
   
0 请登录后投票
最后更新时间:2004-08-06
这次我觉得readonly的论据不够有力度。
log和cache,你上面举的例子的确类似于hello world,拿它来嘲笑AOP,是不公平的。
aspect 和 pointcut也不能认为是buzzword,这就如同class, instance也会被不习惯oop的人认为是buzzword一样?这只是一些便于对话而引入的术语。


你的第一个log的例子,点出的是aspect中无法处理上下文的问题,这一点是正确的。这个观点gigix在4月份的javaeye交流上已经讲过。涉及到上下文的log的确有问题。

potian在上面一个帖子中说明,假若不考虑业务上下文, log的例子可以引申为profiling.
一个例子在这里:
http://www.onjava.com/pub/a/onjava/2004/05/12/aop.html


AOP 的目的,按照我的理解应该是透明的,便于分离用户。同样拿Cache的例子来说,涉及到业务层的cache,拿来做这个例子并不妥当,因为业务层cache往往会互相交织,难以处理(当然不是不可处理)。如果你把Cache应用在一些底层设施上,比如集群模式下的request session 复制,页面cache等等。


换句话说,你的枪头有点歪,只能说AOP不适合做某些事情,而这样的讨论,正好可以廓清一些误解,AOP不是万金油,就如同OOP一样,不是胡乱一切OO化就可以搞定一切的。

所以说Readonly不是反对的AOP,而是“嗡嗡作响的”AOP,不知道我这么理解对不对?

JBOSS 4内部,已经大量使用了AOP,包括cluster 里面的状态复制,request session 复制等等。
   
0 请登录后投票
最后更新时间:2004-08-06
谁要是认为AOP是buzzyword,你不玩就是了。反正我玩得很开心,我才不关心你怎么看。
   
0 请登录后投票
最后更新时间:2004-08-06
曹晓钢 写道
这次我觉得readonly的论据不够有力度。
log和cache,你上面举的例子的确类似于hello world,拿它来嘲笑AOP,是不公平的。

不懂OOP的人会去学AOP? AOP鼓吹者既然号称其是OOP的进阶, 就不要拿Hello World这种东西出来嗡嗡作响, 找点有实际用途的出来反击我的愚蠢吧.


曹晓钢 写道
potian在上面一个帖子中说明,假若不考虑业务上下文, log的例子可以引申为profiling.
一个例子在这里:
http://www.onjava.com/pub/a/onjava/2004/05/12/aop.html

按照那个文章里的例子, 那么AOP可以做得事情太多了: profiling, asynch, exception handle, lock ......
但是, 到底有鬼东西阻止你用OOP不能优雅地实现这一类拦截器性质的东东?


曹晓钢 写道
所以说Readonly不是反对的AOP,而是“嗡嗡作响的”AOP,不知道我这么理解对不对?

bingo! 更准确的说是“嗡嗡作响的”AOPer, 在发出噪声以前, 先掌握好OOP吧.
   
0 请登录后投票
最后更新时间:2004-08-06
Readonly 写道
曹晓钢 写道
这次我觉得readonly的论据不够有力度。
log和cache,你上面举的例子的确类似于hello world,拿它来嘲笑AOP,是不公平的。

不懂OOP的人会去学AOP? AOP鼓吹者既然号称其是OOP的进阶, 就不要拿Hello World这种东西出来嗡嗡作响, 找点有实际用途的出来反击我的愚蠢吧.


这话也对,也不对。

Hello World并不就直接等于“初级”、“简单”、“弱智”。

哪怕是最复杂的技术,一般都会有一个类似Hello World的入门,以显示其容易掌握。这样也是引诱他人上钩的无奈之举,否则人家根本不感兴趣,你怎么办?

但是,这样的Hello World往往是一种误导,让学习它的人,以为这个技术就该这样使用,其实不然。

SUN为了说明EJB,搞了一个其大无比的Hello World,也就是petstore。结果他那样的使用EJB的方式,其蠢无比,后来被微软一脚踢爆,当然了,其实这只能说明petstore愚蠢,并不能说明EJB有问题。

而那些运行了Hello World就认为已经掌握了这种技术的人,早晚吃亏,也是意料中的事情。

我对AOP的看法是,这东西是超级牛刀,千万不要用它来杀鸡。在使用之前,要慎之又慎。

至于gigix的回复,我觉得与技术无关,没有必要。
   
0 请登录后投票
最后更新时间:2004-08-06
Readonly 写道

曹晓钢 写道
potian在上面一个帖子中说明,假若不考虑业务上下文, log的例子可以引申为profiling.
一个例子在这里:
http://www.onjava.com/pub/a/onjava/2004/05/12/aop.html

按照那个文章里的例子, 那么AOP可以做得事情太多了: profiling, asynch, exception handle, lock ......
但是, 到底有鬼东西阻止你用OOP不能优雅地实现这一类拦截器性质的东东?



比如说,你要实现这样一个profiling 的tool,请问你会怎么做呢?
这个例子是一个非侵入性的好例子。面对一个你一无所知的老式系统,你如何使用拦截器?
   
0 请登录后投票
最后更新时间:2004-08-06
引用
比如说,你要实现这样一个profiling 的tool,请问你会怎么做呢?

很巧, 偶以前用过P6Spy, 看过一点它的代码, 以它里面讲的P6LogQuery为例子, 如果是一个良好的OO设计:
1. 应该Separate interfaces from implementation
2. 调用P6LogQuery的对象, 应该Dependency injection
3. 不应该用static方法

如果是这样的设计, 你要为P6LogQuery扩展一个profiling, 3分钟就写完了, 还有必要用AOP来捣鼓么?

再来看看它用AOP实现的InstrumentSQLAdvice, 需要对于JoinPoint了解的一清二楚, 这样丑陋的Advice还能re-use么, 比前面那个被我嘲笑的CacheAdvice的例子还要差N倍 (起码人家的Cache Key的生成策略还能重用).

引用
这个例子是一个非侵入性的好例子。面对一个你一无所知的老式系统,你如何使用拦截器?

OK, 如果你遇到的鬼东西是不可改动的旧系统, 如果你遇到的鬼东西是不想改动的腐烂代码, 如果你遇到的鬼东西是难以改动的垃圾设计, 那么偶承认错了, 对于Java而言, 除了修改ClassLoader, 偶无能为力......
   
0 请登录后投票
最后更新时间:2004-08-06
我一直认为aop的最大用武之地在于遗留系统的处理上。只有对于这类系统,我们已经很难在代码级别上干些什么事情,只能靠外挂。
至于代码现成的情况,横切的那些东西如果对被切物有太多了解,耦合度那么大,就应当放在一起。而如果不需要对被切物有了解,或者了解较少,看来看去只有事务管理这一个看起来还比较方便,其他的都是toy级别的东西,对于企业级应用没有直接的帮助。
当然间接的也有,比如说那个所谓log能够起到一个profiling的作用,不过这类现成工具就很多,何必用aop来做。但aop的log也就仅限于此,难道在实际应用中我只用来记录函数进出时间?我要记的是函数里面干事的情况,如果用aop的方式来做,显然是个紧密耦合的东西。
Cache也一样。不是说拿来一个东西我就可以去cache,容量有限的情况下cache的乱七八糟的东西越多,命中率也越低。而根据cache命中和不命中时的访问代价以及cache策略,可以计算出一个阈值,如果命中率低于这个阈值,cache就没有意义。因此,在这里aop要做的不是说我把所有经手的东西都cache一下,而是要把经手的有cache价值的数据cache下来。而哪些数据有价值,这些信息则是紧耦合于应用逻辑的。

.............................
另外,我一直在猜,还是没踩中。
   
0 请登录后投票
最后更新时间:2004-08-06
只计一点,不计其它,眼光是不是应该看得更远一点。

说到cache的例子,我就借花献佛,举一个Adrian Colyer的例子:

一个DataProvider对象:
[code:1]
public class DataProvider {

private int multiplicationFactor = 0;

public DataProvider(int seed) {
multiplicationFactor = seed;
}

public int expensiveOperation(int x) {
try {
Thread.sleep(1000);
} catch (InterruptedException ex) {}
return x * multiplicationFactor;
}
}
[/code:1]

一个DataProvider的TestCase:
[code:1]
public class CachingTest extends TestCase {

private DataProvider provider;

public void testExpensiveOperationCache() {
long start100 = System.currentTimeMillis();
int op100 = provider.expensiveOperation(100);
long stop100 = System.currentTimeMillis();

long start100v2 = System.currentTimeMillis();
int op100v2 = provider.expensiveOperation(100);
long stop100v2 = System.currentTimeMillis();

long expectedSpeedUp = 500; // expect at least 0.5s quicker with cache

assertTrue("caching speeds up return (100)",
    ((stop100 - start100) - (stop100v2 - start100v2))
>= expectedSpeedUp);
assertEquals("cache returns correct value(100)",op100,op100v2);

         }


protected void setUp() throws Exception {
super.setUp();
provider = new DataProvider(100);
}

}
[/code:1]

不知道如何让这个TestCase green?
   
0 请登录后投票
论坛首页 Java版

跳转论坛:
JavaEye推荐