AOP原理与实现--反射&代理模式&动态代理

引言

很多关于AOP的blog都是概念居多,不认真研究、对java基础不牢固的看着看着就会莫名其妙。下面我从最开始说起吧。

例子程序入门

下面的例子就是一个简化版的日志打印功能。首先,做一个基本的类UserImpl,实现用户的增删改

public class UserImpl {

    public void createUser() {
        try {
            System.out.println("创建一个用户!执行了方法【createUser】");
            Thread.sleep(1000);
        } catch (InterruptedException ex) {
            Logger.getLogger(UserImpl.class.getName()).log(Level.SEVERE, null, ex);
        }
    }

    public void deleteUser() {
        try {
            System.out.println("删除一个用户!执行了方法【deleteUser】");
            Thread.sleep(1000);
        } catch (InterruptedException ex) {
            Logger.getLogger(UserImpl.class.getName()).log(Level.SEVERE, null, ex);
        }
    }

    public void updateUser() {
        try {
            System.out.println("更新一个用户!执行了方法【updateUser】");
            Thread.sleep(1000);
        } catch (InterruptedException ex) {
            Logger.getLogger(UserImpl.class.getName()).log(Level.SEVERE, null, ex);
        }
    }
}

然后在执行的时候插入日志,但不修改这个方法。我们有两种方式实现,第一种我们利用InvocationHandler这个接口:

/**
 * 实现InvocationHandler接口,重写invoke方法
 * @author Fly
 */
public class LogInvocationHandler implements InvocationHandler {

    private Object obj;
    private static final DateFormat format = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");

    public LogInvocationHandler(Object obj) {
        this.obj = obj;
    }

    @Override
    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
        //记录开始执行的时间
        System.out.println(format.format(new Date()) + " 开始执行:");
        //执行原来的方法
        Object rev = method.invoke(obj, args);
        //记录执行结束的时间
        System.out.println(format.format(new Date()) + " 执行结束:");
        //返回原始执行结果
        return rev;
    }
}

打印日志了。看Test类的调用

/**
 *
 * @author Fly
 */
public class Test {
    public static void main(String[] args) throws ClassNotFoundException, IllegalAccessException,
            IllegalArgumentException, InvocationTargetException, InstantiationException, Throwable {
        //利用反射获取Class对象
        Class clazz = Class.forName("aop.UserImpl");
        //传入Class的实例对象,在这里就是UserImpl这个类
        LogInvocationHandler loghandler = new LogInvocationHandler(clazz.newInstance());
        //利用反射获取非原始的、公共的方法体
        Method[] methods = clazz.getDeclaredMethods();
        //循环方法体,利用LogInvocationHandler的invoke方法执行具体操作。
        for (Method method : methods) {
            loghandler.invoke(null, method, null);
        }
    }
}

执行结果如下

run:
2014-04-10 10:02:37 开始执行:
创建一个用户!执行了方法【createUser】
2014-04-10 10:02:38 执行结束:
2014-04-10 10:02:38 开始执行:
删除一个用户!执行了方法【deleteUser】
2014-04-10 10:02:39 执行结束:
2014-04-10 10:02:39 开始执行:
更新一个用户!执行了方法【updateUser】
2014-04-10 10:02:40 执行结束:

仔细看Test类,是不是有点代理模式的意思。我们的LogInvocationHandler就好像是一个代理类。

第二种方式就是使用代理模式实现日志的切入。研究一下代理模式:

代理模式

代理模式可以说是java中常见的用途很广的模式。简单的说吧,你在国内上网,GFW封锁了一些国外网站,你不能去了,于是你使用代理IP(这个IP是没有被GFW封锁的)就可以了,代理IP其实就是一台(片)服务器,它把你的请求先缓存到它的服务器上,然后再从它的服务器上转发给你。这样你就可以查看你想看的网页了。

所以代理模式的第一个作用就是当你没有权限或者操作很复杂的时候,你使用它达到目的。其实我们常用的中set,get何尝不是一种代理模式呢?

除此之外第三方可以帮你获取数据,当然也能对这部分数据进行修改或者重新包装一些数据。比如如果你使用的是在线WEB代理方式,你访问的网站一般都是嵌套在人家网页之内。所以代理模式的第二个作用就是在原来数据的基础上重新包装或者修改数据,以达到使用目的。

实现一个代理模式,利用上面的UserImpl类,不过我们现在引入一个接口IUser,把UserImpl的方法抽象出一个接口,

/**
 *
 * @author Fly
 */
public interface IUser {

    public void createUser();

    public void deleteUser();

    public void updateUser();
}

然后让UserImpl去实现它

public class UserImpl implements IUser {
    //实现的方法跟上面一样
}

然后我们需要引入一个代理类UserProxy,如下

public class UserProxy implements IUser {

    private final IUser user;
    private static final DateFormat format = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");

    public UserProxy() {
        super();
        this.user = new UserImpl();
    }

    @Override
    public void createUser() {
        //记录开始执行的时间
        System.out.println(format.format(new Date()) + " 开始执行:");
        user.createUser();
        //记录执行结束的时间
        System.out.println(format.format(new Date()) + " 执行结束:");
    }

    @Override
    public void deleteUser() {
        //记录开始执行的时间
        System.out.println(format.format(new Date()) + " 开始执行:");
        user.deleteUser();
        //记录执行结束的时间
        System.out.println(format.format(new Date()) + " 执行结束:");
    }

    @Override
    public void updateUser() {
        //记录开始执行的时间
        System.out.println(format.format(new Date()) + " 开始执行:");
        user.updateUser();
        //记录执行结束的时间
        System.out.println(format.format(new Date()) + " 执行结束:");
    }
}

仔细看跟我们上面实现的LogInvocationHandler大同小异,当然LogInvocationHandler看起来简洁一些,因为我们引入了反射嘛。

然后看看我们怎么调用这个代理类呢?

public class ProxyTest {

    public static void main(String[] args) throws ClassNotFoundException {
        //构造代理类处理数据(附加操作)
        IUser proxyUser = new UserProxy();
        //执行各个方法
        proxyUser.createUser();
        proxyUser.deleteUser();
        proxyUser.updateUser();
    }
}

执行结果如下:

run:
2014-04-10 10:36:22 开始执行:
创建一个用户!执行了方法【createUser】
2014-04-10 10:36:23 执行结束:
2014-04-10 10:36:23 开始执行:
删除一个用户!执行了方法【deleteUser】
2014-04-10 10:36:24 执行结束:
2014-04-10 10:36:24 开始执行:
更新一个用户!执行了方法【updateUser】
2014-04-10 10:36:25 执行结束:

可以看出我们现在的执行结果跟上面完全一样,在我们不修改UserImpl这个业务逻辑的情况下,我们插入了日志记录。这里有点AOP的影子了。

当然AOP不是这么简单,但最重要的两点,就是代理模式和反射机制。

AOP深入
1、基本的思考

既然我们知道了代理模式,也知道反射机制,那我们怎么切入我们的代理点呢(在哪里实现代理)?那些方法需要拦截(比如上面,只想在createUser方法上拦截添加日志),怎么设置拦截(怎么去设置这拦截,比如是否有异常)?是在方法前切入(createUser执行前切入,打印开始时间的操作)?还是方法后切入(createUser执行后切入,打印结束时间的操作)?切入逻辑的怎么实现(上面的逻辑是打印执行时间)?其实这些都是AOP很重要的一些逻辑。查看一些重要的切入逻辑

Joinpoint:拦截点,如某个业务方法。
Pointcut:Joinpoint的表达式,表示拦截哪些方法。一个Pointcut对应多个Joinpoint。
Advice:  要切入的逻辑。
Before Advice 在方法前切入。
After Advice 在方法后切入,抛出异常时也会切入。
After Returning Advice 在方法返回后切入,抛出异常则不会切入。
After Throwing Advice 在方法抛出异常时切入。
Around Advice 在方法执行前后切入,可以中断或忽略原有流程的执行。

2、InvocationHandler是什么?本段摘自(http://www.iteye.com/topic/1116696)

 Java在JDK1.3后引入的动态代理机制,使我们可以在运行期动态的创建代理类。使用动态代理实现AOP需要有四个角色:被代理的类,被代理类的接口,织入器,和InvocationHandler,而织入器使用接口反射机制生成一个代理类,然后在这个代理类中织入代码。被代理的类是AOP里所说的目标,InvocationHandler是切面,它包含了Advice和Pointcut。

3、使用Proxy来实现动态代理

在上面代理模式中,我们写了一个自己的代理类UserProxy,这是一种“静态代理”的方式,这种方式的有个很明显的缺点,灵活性不够,每增加一个业务方法,就需要重新增加代理的逻辑。所以我们需要动态生成代理类。动态代理的核心其实就是代理对象的生成,即Proxy.newProxyInstance(classLoader, proxyInterface, handler)。针对我们最开始的例子,我们修改Test测试类的方法。在这里我们生成了一个代理类对象proxy。然后我们使用proxy来调用对应的方法:

/**
 *
 * @author Fly
 */
public class Test {
    public static void main(String[] args) throws ClassNotFoundException, IllegalAccessException,
            IllegalArgumentException, InvocationTargetException, InstantiationException, Throwable {
        //利用反射获取接口的Class对象
        Class clazz = Class.forName("aop.IUser");
        //利用反射获取实现类的Class对象,他就是切入点
        Class clazzImpl = Class.forName("aop.UserImpl");
        //传入Class的实例对象,在这里就是UserImpl这个类,
        LogInvocationHandler loghandler = new LogInvocationHandler(clazzImpl.newInstance());
        //获取类加载器
        ClassLoader classLoader = Test.class.getClassLoader();
        //获取所有的接口数组
        Class[] iClazzs = new Class[]{clazz};
        //动态生成代理类
        IUser proxy = (IUser) Proxy.newProxyInstance(classLoader, iClazzs, loghandler);
        //利用代理类实现业务逻辑
        proxy.createUser();
        proxy.deleteUser();
        proxy.updateUser();
    }
}

这样是不是真正的实现AOP,从一个切面来实现日志的增加,而调用方式还是跟以前一样。

关于动态代理的原理,我们可以看http://www.iteye.com/topic/1116696

看了原理我们知道,如果要使用动态代理,必须要使用接口。然后动态代理会使用反射机制,这就会在性能有有很大的影响,最大的问题在于java的字节码文件加载后是存放在持久代上的,如果持久代溢出,就会导致Full GC,所以大量使用反射机制的话,会对系统造成很大影响。

其他动态加载机制

详细请看http://www.iteye.com/topic/1116696 ,特别感谢fantasy!

发散思维

在延时加载机制中,基本原理就是动态加载机制。网上有个面试题目问“hibernate的动态加载原理是什么?”,其实说到底就是动态加载机制。下一节我们来实现动态代理。

    原文作者:AOP
    原文地址: https://blog.csdn.net/Fly_Sky520/article/details/23333897
    本文转自网络文章,转载此文章仅为分享知识,如有侵权,请联系博主进行删除。
点赞