代理模式
设计模式最初由 GOF 在《设计模式》一书中总结,将其划分为创建型、结构型、行为型三大类。其中结构型模式总结了一些将类或对象组合在一起的经典结构。代理模式正是结构型模式的一种,它在不改变原始类的情况下通过引入代理类来给原始类附加新的功能。代理通常应用于业务系统的非功能性需求,如日志、鉴权、统计等。根据是否在运行时创建代理对象可以分为静态代理和动态代理两大类。
静态代理
静态代理需要为每个接口或类事先创建代理对象。下面通过代码的形式说明静态代理的使用。
假定业务代码如下。
public interface IAService { void method1(); } // 目标类:版本一 public class AServiceImpl implements IAService { @Override public void method1() { System.out.println("execute AServiceImpl#method1"); } }
现在需要统计方法的执行时间,使用最原始的方式解决,修改方法如下。
// 目标类:版本二 public class AServiceImpl implements IAService { @Override public void method1() { long start = System.currentTimeMillis(); System.out.println("execute AServiceImpl#method1"); long cost = System.currentTimeMillis() - start; System.out.println("cost time: " + cost); } }
上面的代理有两个问题。第一,业务代码和框架代码耦合,如果以后想要替换框架代码,成本会相对比较高。第二,统计方法执行时间的代码和方法本身的功能无关,使类的职责不够单一。为了解决上面的问题,使用静态代理的方式解决如下。
// 代理类:版本一,实现目标类实现的接口 public class AServiceProxy implements IAService { private IAService service; public ServiceProxy(IAService service) { this.service = service; } @Override public void method1() { long start = System.currentTimeMillis(); this.service.method1(); long cost = System.currentTimeMillis() - start; System.out.println("cost time: " + cost); } } // 调用方式如下 public class App { public static void main(String[] args) throws SQLException { // IAService service = new AServiceImpl(); IAService service = new AServiceProxy(new AServiceImpl()); service.method1(); } }
在基于接口而非实现编程的时候,通过将代理类实现和目标类相同的接口,运用组合的方式可以将目标类直接替换为代理类。然而,如果目标类并非我们开发,而且目标类并未实现接口,这种方式则无法处理,这种情况下我们可以通过继承目标类的方式进行处理,代码如下。
// 代理类,版本二,继承目标类 public class AServiceProxy extends AServiceImpl { @Override public void method1() { long start = System.currentTimeMillis(); super.method1(); long cost = System.currentTimeMillis() - start; System.out.println("cost time: " + cost); } } // 调用方式如下 public class App { public static void main(String[] args) throws SQLException { IAService service = new AServiceProxy(); service.method1(); } }
小结如下,静态代理通过组合或继承的方式创建代理类,然后将目标类替换为代理类。
使用组合的方式相对灵活,可以代理接口的所有子类,然而必须实现接口中的所有方法。
使用继承的方式可以只重写需要处理的方法,但是由于 Java 中继承的限制,只能继承一个类,并且对于 final 修饰的类也是无法继承的。
动态代理
静态代理必须事先创建目标类的代理类,如果目标类或者目标类的方法比较多,会使项目中类的数量成倍的增加,并且代理类的方法中会存在大量的相似代码。动态代理则是在运行时动态的创建代理类,减少了代理类的数量。Java 作为一种静态强类型语言,需要在开发时定义类,我们写的类都会被编译为 class 文件,然后被类加载器加载到内存。事实上类加载器并不关心字节码来自于哪里,最终只要能转换为字节数组即可,因此只要我们在运行时生成符合规范的字节码数组,就能够动态的生成新的类。Java 中各种生成动态代理方法的原理都是如此。目前 Java 中有以下几种生成动态代理的方式,分别是 JDK 动态代理、Cglib 动态代理、Javassist 动态代理,下面分别加以介绍。
JDK 动态代理
JDK 动态代理是 Java 类库自带创建或实例化代理类的一种方式。利用 JDK 动态代理生成上面统计方法执行事件的代理类,代码如下。
// 代理 public class AServiceJdkProxy implements InvocationHandler { // 目标类 private Object service; public AServiceJdkProxy(Object service) { this.service = service; } // 创建目标类的代理对象 public Object createProxy() { return Proxy.newProxyInstance(Thread.currentThread().getContextClassLoader(), service.getClass().getInterfaces(), this); } // 调用目标方法时会调用该方法 @Override public Object invoke(Object proxy, Method method, Object[] args) throws Throwable { long start = System.currentTimeMillis(); Object result = method.invoke(this.service, args); long cost = System.currentTimeMillis() - start; System.out.println("cost time: " + cost); return result; } }
JDK 动态代理只能创建实现了指定接口的代理对象,如果目标对象没有实现接口,则无法创建代理。可以通过反射查看创建代理对象的信息。
public class App { public static void main(String[] args) throws SQLException { Class<?> proxyCls = new AServiceJdkProxy(new AServiceImpl()).createProxy().getClass(); String modifier = Modifier.toString(proxyCls.getModifiers()); String clsName = proxyCls.getName(); String superCls = proxyCls.getSuperclass().getName(); String interfaces = Arrays.stream(proxyCls.getInterfaces()).map(Class::getName).collect(Collectors.joining(",")); // public final com.sun.proxy.$Proxy0 extend java.lang.reflect.Proxy implements com.zzuhkp.blog.IAService System.out.println(modifier + " " + clsName + " extend " + superCls + " implements " + interfaces); } }
可以看到生成代理类继承了 Proxy,并且实现了我们提供的接口。运用 JDK 动态代理可以减少对外部的依赖,并且 JDK 的底层也会对其优化,随着 JDK 的升级而升级,创建代理时应优先使用 JDK 提供的代理方式。
CGLIB 动态代理
ASM 是一种小而快的字节码操作和分析框架,通过 ASM 可以不用加载类而读取类的信息,也可以动态的创建或修改类,因此直接使用 ASM 也可以在运行时创建代理类,然而由于需要对底层的字节码指令具有深入理解才能使用 ASM,因此推荐直接使用 CGLIB,CGLIB 通过对 ASM 的封装简化了创建代理的操作。在目标类没有实现接口的时候可以通过 CGLIB 实现目标类,进而创建目标类的代理。CGLIB 依赖坐标如下。
<dependency> <groupId>cglib</groupId> <artifactId>cglib</artifactId> <version>3.3.0</version> </dependency>
下面使用 CGLIB 创建代理来对方法的执行时长进行统计。
// 使用 CGLIB 创建代理 public class AServiceCglibProxy { private Object service; public AServiceCglibProxy(Object service) { this.service = service; } public Object createProxy() { Enhancer enhancer = new Enhancer(); enhancer.setInterfaces(this.service.getClass().getInterfaces()); enhancer.setCallback(new MethodInterceptor() { @Override public Object intercept(Object obj, Method method, Object[] args, MethodProxy proxy) throws Throwable { long start = System.currentTimeMillis(); Object result = method.invoke(AServiceCglibProxy.this.service, args); long cost = System.currentTimeMillis() - start; System.out.println("cost time: " + cost); return result; } }); return enhancer.create(); } } // 使用示例如下 public class App { public static void main(String[] args) throws SQLException { IAService service = (IAService) new AServiceCglibProxy(new AServiceImpl()).createProxy(); service.method1(); } }
Javassist 动态代理
Javassist 同样是一个分析,编辑,创建字节码的类库,和其他类库有所不同的是,Javassist 同时提供了源码级别和字节码级别的 API,点击查看 Javassist 的文档 。Javassist 依赖坐标如下。
<dependency> <groupId>org.javassist</groupId> <artifactId>javassist</artifactId> <version>3.27.0-GA</version> </dependency>
下面通过 Javassist 创建代理对象来分析 Java 方法的执行时长。
// 使用 Javassist 创建代理 public class AServiceJavassistProxy { private Object service; public AServiceJavassistProxy(Object service) { this.service = service; } public Object createProxy() throws InvocationTargetException, NoSuchMethodException, InstantiationException, IllegalAccessException { ProxyFactory enhancer = new ProxyFactory(); enhancer.setInterfaces(this.service.getClass().getInterfaces()); Object proxy = enhancer.create(null, null); ((Proxy) proxy).setHandler(new MethodHandler() { @Override public Object invoke(Object self, Method thisMethod, Method proceed, Object[] args) throws Throwable { long start = System.currentTimeMillis(); Object result = thisMethod.invoke(AServiceJavassistProxy.this.service, args); long cost = System.currentTimeMillis() - start; System.out.println("cost time: " + cost); return result; } }); return proxy; } } // 使用示例如下 public class App { public static void main(String[] args) throws SQLException, NoSuchMethodException, InstantiationException, IllegalAccessException, InvocationTargetException { IAService service = (IAService) new AServiceJavassistProxy(new AServiceImpl()).createProxy(); service.method1(); } }
总结
Java 中的代理分为静态代理和动态代理,由于静态代理需要事先确定要代理的对象,因此相对动态代理来说不够灵活,在明确目标类的情况下可以使用,后面文章将对 Mybatis 日志模块使用的静态代理进行分析。动态代理底层操作字节码生成新的代理类,减少了重复的代码,相对简洁。代理是实现 AOP 的基础,后面也会对 AOP 加以介绍。