【Java设计模式】:代理模式

版权声明:本文为博主原创文章,未经博主允许不得转载。 https://blog.csdn.net/hxcaifly/article/details/85061330

写在前面
1.本文重点阐述三种代理模式的区别和应用案例。
2. 结合AOP编程,讲解代理模式。

什么是代理模式

代理(Proxy)是一种设计模式,提供了对目标对象另外的访问方式;即通过代理对象访问目标对象。这样做的好处是:可以在目标对象实现的基础上,增强额外的功能操作,即扩展目标对象的功能。

这里使用到编程中的一个思想: 不要随意去修改别人已经写好的代码或者方法,如果需改修改,可以通过代理的方式来扩展该方法

举个例子来说明代理的作用:假设我们想邀请一位明星,那么并不是直接连接明星,而是联系明星的经纪人,来达到同样的目的。明星就是一个目标对象,他只要负责活动中的节目,而其他琐碎的事情就交给他的代理人(经纪人)来解决。这就是代理思想在现实中的一个例子。用图表示如下:
在这里插入图片描述

代理模式的关键点是: 代理对象与目标对象。代理对象是对目标对象的扩展,并会调用目标对象。

Java中实现代理的方法对比

在Java中,常用的代理方法有三种,即:静态代理,动态代理,CGLIB代理。另外还有两种不怎么常用的实现AOP机制:自定义类加载器和字节码转换。这5种方法的对比如下表:

类别 机制 原理 优点 缺点
静态AOP 静态代理 在编译期,切面直接以字节码的形式编译到目标字节码文件中 对系统无性能影响 灵活性不够
动态AOP 动态代理 在运行期,目标类加载后,为接口动态生成代理类,类切面织入到代理类中 相当于静态AOP更加灵活 切入的关注点需要实现接口,对系统有一定性能影响
动态字节码生成 CGLIB 在运行期,目标类加载后,动态构建字节码文件生成目标类的子类,将切面逻辑加入到子类中 没有接口也可以织入 扩展类的实例方法为final,那么子类就不能继承,则无法进行织入
自定义类加载器 在运行期,目标加载前,将切面逻辑加到目标字节码里 可以绝大部分类进行织入 代码中如果使用了其他类加载器,则这些类将不会被织入
字节码转换 在运行期,所有类加载器字节码前进行拦截 可以对所有类进行织入

这5中代理方法,根据它们织入的时期,可以用下图表示:
在这里插入图片描述

静态代理

静态代理在使用时,需要定义接口或者父类,被代理对象与代理对象一起实现相同的接口或者是继承相同父类。

下面举个案例来解释:
模拟保存动作,定义一个保存动作的接口:IUserDao.java,然后目标对象实现这个接口的方法UserDao.java,此时如果使用静态代理方式,就需要在代理对象(UserDaoProxy.java)中也实现IUserDao接口。调用的时候通过调用代理对象的方法来调用目标对象。

代码示例:
接口:IUserDao.java

/**
 * 接口
 */
public interface IUserDao {

    void save();
}

目标对象:UserDao.java

/**
 * 接口实现
 * 目标对象
 */
public class UserDao implements IUserDao {
    public void save() {
        System.out.println("----已经保存数据!----");
    }
}

代理对象:UserDaoProxy.java

/**
 * 代理对象,静态代理
 */
public class UserDaoProxy implements IUserDao{
    //接收保存目标对象
    private IUserDao target;
    public UserDaoProxy(IUserDao target){
        this.target=target;
    }

    public void save() {
        System.out.println("开始事务...");
        target.save();//执行目标对象的方法
        System.out.println("提交事务...");
    }
}

测试类:App.java

/**
 * 测试类
 */
public class App {
    public static void main(String[] args) {
        //目标对象
        UserDao target = new UserDao();

        //代理对象,把目标对象传给代理对象,建立代理关系
        UserDaoProxy proxy = new UserDaoProxy(target);

        proxy.save();//执行的是代理的方法
    }
}

静态代理总结:

  1. 可以做到在不修改目标对象的功能前提下,对目标功能扩展。
  2. 缺点: 因为代理对象需要与目标对象实现一样的接口,所以会有很多代理类,类太多。同时,一旦接口增加方法,目标对象与代理对象都要维护。

如何解决静态代理中的缺点呢?答案是可以使用动态代理方式。

动态代理

JDK中生成代理对象的API
代理类所在包:java.lang.reflect.Proxy
JDK实现代理只需要使用newProxyInstance方法,但是该方法需要接收三个参数,完整的写法是:

static Object newProxyInstance(ClassLoader loader, Class<?>[] interfaces,InvocationHandler h )

注意该方法是在Proxy类中是静态方法,且接收的三个参数依次为:

  • ClassLoader loader:指定当前目标对象使用类加载器,获取加载器的方法是固定的
  • Class<?>[] interfaces:目标对象实现的接口的类型,使用泛型方式确认类型
  • InvocationHandler h:事件处理,执行目标对象的方法时,会触发事件处理器的方法,会把当前执行目标对象的方法作为参数传入

具体示例:
定义接口:

public interface IBusiness {
    void doSomeThing();
}

定义具体实现类:

public class Business implements IBusiness{
    @Override
    public void doSomeThing() {
        System.out.println("执行业务逻辑");
    }
}

定义事件处理:

public class LogInvocationHandler implements InvocationHandler {

    private Object target;//目标对象

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

    @Override
    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
        //执行织入的日志,你可以控制哪些方法执行切入逻辑
        System.out.println("逻辑执行前");
        //执行原有逻辑
        Object recv = method.invoke(target, args);
        System.out.println("逻辑执行后");
        return recv;
    }
}

测试逻辑:

public class Main {
    public static void main(String[] args) {
        //需要代理的类接口,被代理类实现的多个接口都必须在这这里定义
        Class[] proxyInterface = new Class[] {IBusiness.class};
        //构建AOP的Advice,这里需要传入业务类的实例
        LogInvocationHandler handler = new LogInvocationHandler(new Business());
        //生成代理类的字节码加载器
        ClassLoader classLoader = Business.class.getClassLoader();
        //织入器,织入代码并生成代理类
        IBusiness proxyBusiness = (IBusiness) Proxy.newProxyInstance(classLoader, proxyInterface, handler);
        proxyBusiness.doSomeThing();
    }
}

执行结果:

逻辑执行前
执行业务逻辑
逻辑执行后

总结
动态代理在运行期通过接口动态生成代理类,这为其带来了一定的灵活性,但这个灵活性却带来了两个问题:
第一,代理类必须实现一个接口,如果没实现接口会抛出一个异常
第二,性能影响,因为动态代理是使用反射机制实现的,首先反射肯定比直接调用要慢,其次使用反射大量生成类文件可能引起full gc,因为字节码文件加载后会存放在JVM运行时方法区(或者叫永久代、元空间)中,当方法区满时会引起full gc,所以当你大量使用动态代理时,可以将永久代设置大一些,减少full gc的次数

CGLib实现代理

使用动态字节码生成技术实现AOP原理是在运行期间目标字节码加载后,生成目标类的子类,将切面逻辑加入到子类中,所以cglib实现AOP不需要基于接口。

在这里插入图片描述

本节介绍如何使用cglib来实现动态字节码技术。

cglib是一个强大的、高性能的Code生成类库,它可以在运行期间扩展Java类和实现Java接口,它封装了Asm,所以使用cglib前需要引入Asm的jar。

下面具体示例
具体实现类:

/**
 * 这个是没有实现接口的实现类
 */
public class BookFacadeImpl {
    public void addBook() {
         System.out.println("增加图书的普通方法。。。");
     }
 
    public void deleteBook() {
         System.out.println("删除图书的普通方法。。。");
     }
}

代理类实现:

import net.sf.cglib.proxy.Enhancer;
import net.sf.cglib.proxy.MethodInterceptor;
import net.sf.cglib.proxy.MethodProxy;
 
import java.lang.reflect.Method;

/**
 * 使用cglib动态代理
*/
 public class BookFacadeCglib implements MethodInterceptor {
 
    private Object target;
 
     /**
      * 创建代理对象
      *
      * @param target
      * @return
      */
     public Object getInstance(Object target) {
         this.target = target;
         Enhancer enhancer = new Enhancer();
         enhancer.setSuperclass(this.target.getClass());
         //回调方法
         enhancer.setCallback(this);
         //创建代理
         return enhancer.create();
     }
 
     //回调方法
     @Override
     public Object intercept(Object obj, Method method, Object[] args, MethodProxy methodProxy) throws Throwable {
         if (method.getName().equals("addBook")) {
            System.out.println("记录增加图书的日志");
         }
         methodProxy.invokeSuper(obj, args);
         return null;
     }
 }

具体测试:

public class TestCglib {
    public static void main(String[] args) {
        BookFacadeCglib cglib = new BookFacadeCglib();
        BookFacadeImpl bookFacade = (BookFacadeImpl) cglib.getInstance(new BookFacadeImpl());
        bookFacade.addBook();
        bookFacade.deleteBook();
    }
}

执行结果:

记录增加图书的日志
增加图书的普通方法。。。
删除图书的普通方法。。。

自定义类加载器

如果我们实现了一个自定义类加载器,在类加载到JVM之前直接修改某些类的方法,并将切入逻辑织入到这个方法里,然后将修改后的字节码文件交给虚拟机运行,那岂不是更直接。

在这里插入图片描述

Javassist是一个编辑字节码的框架,可以让你很简单地操作字节码。它可以在运行期定义或修改Class。使用Javassist实现AOP的原理是在字节码加载前直接修改需要切入的方法。

这比使用cglib实现AOP更加高效,并且没有太多限制,实现原理如下图:
在这里插入图片描述

我们使用类加载器启动我们自定义的类加载器,在这个类加载器里加一个类加载监听器,监听器发现目标类被加载时就织入切入逻辑。

具体示例:Javassist实现AOP的代码。

启动自定义的类加载器:

//获取存放CtClass的容器ClassPool 
ClassPool cp = ClassPool.getDefault(); 
//创建一个类加载器 
Loader cl = new Loader(); 
//增加一个转换器 
cl.addTranslator(cp, new MyTranslator()); 
//启动MyTranslator的main函数 
cl.run("jsvassist.JavassistAopDemo$MyTranslator", args); 

类加载监听器:

public static class MyTranslator implements Translator { 

        public void start(ClassPool pool) throws NotFoundException, CannotCompileException { 
        } 

        /* * 
         * 类装载到JVM前进行代码织入 
         */ 
        public void onLoad(ClassPool pool, String classname) { 
            if (!"model$Business".equals(classname)) { 
                return; 
            } 
            //通过获取类文件 
            try { 
                CtClass  cc = pool.get(classname); 
                //获得指定方法名的方法 
                CtMethod m = cc.getDeclaredMethod("doSomeThing"); 
                //在方法执行前插入代码 
                m.insertBefore("{ System.out.println(\"记录日志\"); }"); 
            } catch (NotFoundException e) { 
            } catch (CannotCompileException e) { 
            } 
        } 

        public static void main(String[] args) { 
            Business b = new Business(); 
            b.doSomeThing2(); 
            b.doSomeThing(); 
        } 
    } 

输出:

执行业务逻辑2
记录日志
执行业务逻辑

总结: 从本节中可知,使用自定义的类加载器实现AOP在性能上有优于动态代理和cglib,因为它不会产生新类,但是它仍人存在一个问题,就是如果其他的类加载器来加载类的话,这些类就不会被拦截。

字节码转换

自定义类加载器实现AOP只能拦截自己加载的字节码,那么有一种方式能够监控所有类加载器加载的字节码吗?

有,使用Instrumentation,它是Java5的新特性,使用Instrument,开发者可以构建一个字节码转换器,在字节码加载前进行转换

本节使用Instrumentation和javassist来实现AOP。

构建字节码转换器

首先需要创建字节码转换器,该转换器负责拦截Business类,并在Business类的doSomeThing方法前使用javassist加入记录日志的代码

public class MyClassFileTransformer implements ClassFileTransformer { 

    /** 
     * 字节码加载到虚拟机前会进入这个方法 
     */ 
    @Override 
     public byte[] transform(ClassLoader loader, String className, Class<?> classBeingRedefined, 
                             ProtectionDomain protectionDomain, byte[] classfileBuffer) 
             throws IllegalClassFormatException { 
         System.out.println(className); 
         //如果加载Business类才拦截 
         if (!"model/Business".equals(className)) { 
             return null; 
         } 
 
         //javassist的包名是用点分割的,需要转换下 
         if (className.indexOf("/") != -1) { 
             className = className.replaceAll("/", "."); 
         } 
         try { 
             //通过包名获取类文件 
             CtClass cc = ClassPool.getDefault().get(className); 
             //获得指定方法名的方法 
             CtMethod m = cc.getDeclaredMethod("doSomeThing"); 
             //在方法执行前插入代码 
             m.insertBefore("{ System.out.println(\"记录日志\"); }"); 
             return cc.toBytecode(); 
         } catch (NotFoundException e) { 
         } catch (CannotCompileException e) { 
         } catch (IOException e) { 
             //忽略异常处理 
         } 
         return null; 
} 

注册转换器

使用premain函数注册字节码转换器,该方法在main函数之前执行

public class MyClassFileTransformer implements ClassFileTransformer { 
    public static void premain(String options, Instrumentation ins) { 
        //注册我自己的字节码转换器 
        ins.addTransformer(new MyClassFileTransformer()); 
} 
} 

配置和执行
需要告诉JVM在启动main函数之前,需要先执行premain函数。

首先,需要将premain函数所在的类打成jar包,并修改jar包里的META-INF\MANIFEST.MF文件

Manifest-Version: 1.0 
Premain-Class: bci. MyClassFileTransformer

其次,在JVM的启动参数里加上-javaagent:D:\java\projects\opencometProject\Aop\lib\aop.jar

输出:
执行main函数,你会发现切入的代码无侵入性的织入进去了。

public static void main(String[] args) { 
    new Business().doSomeThing(); 
    new Business().doSomeThing2(); 
 }   

输出:

model/Business 
sun/misc/Cleaner 
java/lang/Enum 
model/IBusiness 
model/IBusiness2 
记录日志 
执行业务逻辑 
执行业务逻辑2 
java/lang/Shutdown 
java/lang/Shutdown$Lock 

从输出中可以看到系统类加载器加载的类也经过了这里

Spring的AOP

Spring默认采取动态代理机制实现AOP,当动态代理不可用时(代理类无接口)会使用cglib机制。

但Spring的AOP有一定的缺点:

第一,只能对方法进行切入,不能对接口、字段、静态代码块进行切入(切入接口的某个方法,则该接口下所有实现类的该方法都将被切入)。
第二,同类中的互相调用方法将不会使用代理类。因为要使用代理类必须从Spring容器中获取Bean。
第三,性能不是最好的。从前面几节得知,我们自定义的类加载器,性能优于动态代理和cglib。

猜你喜欢

转载自blog.csdn.net/hxcaifly/article/details/85061330
今日推荐