24.4 自定义ClassLoader

Java类加载机制的强大之处在于,我们可以创建自定义的ClassLoader,自定义Class-Loader是Tomcat实现应用隔离、支持JSP、OSGI实现动态模块化的基础。

怎么自定义呢?一般而言,继承类ClassLoader,重写findClass就可以了。怎么实现findClass呢?使用自己的逻辑寻找class文件字节码的字节形式,找到后,使用如下方法转换为Class对象:

1
protected final Class<? > defineClass(String name, byte[] b, int off, int len)

name表示类名,b是存放字节码数据的字节数组,有效数据从off开始,长度为len。看个例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
public class MyClassLoader extends ClassLoader {
private static final String BASE_DIR = "data/c87/";
@Override
protected Class<? > findClass(String name) throws ClassNotFoundException {
String fileName = name.replaceAll("\\.", "/");
fileName = BASE_DIR + fileName + ".class";
try {
byte[] bytes = BinaryFileUtils.readFileToByteArray(fileName);
return defineClass(name, bytes, 0, bytes.length);
} catch (IOException ex) {
throw new ClassNotFoundException("failed to load class " + name, ex);
}
}
}

MyClassLoader从BASE_DIR下的路径中加载类,它使用了我们在第13章介绍的readFileToByteArray方法读取文件,转换为byte数组。MyClassLoader没有指定父Class-Loader,默认是系统类加载器,即ClassLoader.getSystemClassLoader()的返回值,不过,Class-Loader有一个可重写的构造方法,可以指定父ClassLoader:

1
protected ClassLoader(ClassLoader parent)

MyClassLoader有什么用呢?将BASE_DIR加到classpath中不就行了,确实可以,这里主要是演示基本用法,实际中,可以从Web服务器、数据库或缓存服务器获取bytes数组,这就不是系统类加载器能做到的了。

不过,不把BASE_DIR放到classpath中,而是使用MyClassLoader加载,还有一个很大的好处,那就是可以创建多个MyClassLoader,对同一个类,每个MyClassLoader都可以加载一次,得到同一个类的不同Class对象,比如:

1
2
3
4
5
6
7
8
MyClassLoader cl1 = new MyClassLoader();
String className = "shuo.laoma.dynamic.c87.HelloService";
Class<? > class1 = cl1.loadClass(className);
MyClassLoader cl2 = new MyClassLoader();
Class<? > class2 = cl2.loadClass(className);
if(class1 ! = class2) {
System.out.println("different classes");
}

cl1和cl2是两个不同的ClassLoader, class1和class2对应的类名一样,但它们是不同的对象。

但,这到底有什么用呢?

1)可以实现隔离。一个复杂的程序,内部可能按模块组织,不同模块可能使用同一个类,但使用的是不同版本,如果使用同一个类加载器,它们是无法共存的,不同模块使用不同的类加载器就可以实现隔离,Tomcat使用它隔离不同的Web应用,OSGI使用它隔离不同模块。
2)可以实现热部署。使用同一个ClassLoader,类只会被加载一次,加载后,即使class文件已经变了,再次加载,得到的也还是原来的Class对象,而使用MyClassLoader,则可以先创建一个新的ClassLoader,再用它加载Class,得到的Class对象就是新的,从而实现动态更新。

下面,我们来具体看热部署的示例。

24.3 类加载的应用:可配置的策略

可以通过ClassLoader的loadClass或Class.forName自己加载类,但什么情况需要自己加载类呢?很多应用使用面向接口的编程,接口具体的实现类可能有很多,适用于不同的场合,具体使用哪个实现类在配置文件中配置,通过更改配置,不用改变代码,就可以改变程序的行为,在设计模式中,这是一种策略模式。我们看个简单的示例,定义一个服务接口IService:

1
2
3
public interface IService {
public void action();
}

客户端通过该接口访问其方法,怎么获得IService实例呢?查看配置文件,根据配置的实现类,自己加载,使用反射创建实例对象,示例代码为:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
public class ConfigurableStrategyDemo {
public static IService createService() {
try {
Properties prop = new Properties();
String fileName = "data/c87/config.properties";
prop.load(new FileInputStream(fileName));
String className = prop.getProperty("service");
Class<? > cls = Class.forName(className);
return (IService) cls.newInstance();
} catch (Exception e) {
throw new RuntimeException(e);
}
}
public static void main(String[] args) {
IService service = createService();
service.action();
}
}

config.properties的内容示例为:

1
service=shuo.laoma.dynamic.c87.ServiceB

代码比较简单,就不赘述了。完整代码可参看https://github.com/swiftma/program-logic ,位于包shuo.laoma.dynamic.c87下。

24.2 理解ClassLoader

类ClassLoader是一个抽象类,Application ClassLoader和ExtensionClassLoader的具体实现类分别是sun.misc.Launcher$AppClassLoader和sun.misc.Launcher$ExtClassLoader, Bootstrap ClassLoader不是由Java实现的,没有对应的类。

每个Class对象都有一个方法,可以获取实际加载它的ClassLoader,方法是:

1
public ClassLoader getClassLoader()

ClassLoader有一个方法,可以获取它的父ClassLoader:

1
public final ClassLoader getParent()

如果ClassLoader是Bootstrap ClassLoader,返回值为null。比如:

1
2
3
4
5
6
7
8
9
10
public class ClassLoaderDemo {
public static void main(String[] args) {
ClassLoader cl = ClassLoaderDemo.class.getClassLoader();
while(cl ! = null) {
System.out.println(cl.getClass().getName());
cl = cl.getParent();
}
System.out.println(String.class.getClassLoader());
}
}

输出为:

1
2
3
sun.misc.Launcher$AppClassLoader
sun.misc.Launcher$ExtClassLoader
null

ClassLoader有一个静态方法,可以获取默认的系统类加载器:

1
public static ClassLoader getSystemClassLoader()

ClassLoader中有一个主要方法,用于加载类:

1
public Class<? > loadClass(String name) throws ClassNotFoundException

比如:

1
2
3
4
5
6
7
8
ClassLoader cl = ClassLoader.getSystemClassLoader();
try {
Class<? > cls = cl.loadClass("java.util.ArrayList");
ClassLoader actualLoader = cls.getClassLoader();
System.out.println(actualLoader);
} catch (ClassNotFoundException e) {
e.printStackTrace();
}

需要说明的是,由于委派机制,Class的getClassLoader方法返回的不一定是调用load-Class的ClassLoader,比如,上面代码中,java.util.ArrayList实际由BootStrap ClassLoader加载,所以返回值就是null。

在反射一章,我们介绍过Class的两个静态方法forName:

1
2
3
public static Class<? > forName(String className)
public static Class<? > forName(String name,
boolean initialize, ClassLoader loader)

第一个方法使用系统类加载器加载,第二个方法指定ClassLoader,参数initialize表示加载后是否执行类的初始化代码(如static语句块),没有指定默认为true。

ClassLoader的loadClass方法与Class的forName方法都可以加载类,它们有什么不同呢?基本是一样的,不过,ClassLoader的loadClass不会执行类的初始化代码,看个例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
public class CLInitDemo {
public static class Hello {
static {
System.out.println("hello");
}
};
public static void main(String[] args) {
ClassLoader cl = ClassLoader.getSystemClassLoader();
String className = CLInitDemo.class.getName() + "$Hello";
try {
Class<? > cls = cl.loadClass(className);
}
catch (ClassNotFoundException e) {
e.printStackTrace();
}
}
}

使用ClassLoader加载静态内部类Hello, Hello有一个static语句块,输出”hello”,运行该程序,类被加载了,但没有任何输出,即static语句块没有被执行。如果将loadClass的语句换为:

1
Class<? > cls = Class.forName(className);

则static语句块会被执行,屏幕将输出”hello”。

我们来看下ClassLoader的loadClass代码,以进一步理解其行为:

1
2
3
public Class<? > loadClass(String name) throws ClassNotFoundException {
return loadClass(name, false);
}

它调用了另一个loadClass方法,其主要代码为(省略了一些代码,加了注释,以便于理解):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
protected Class<? > loadClass(String name, boolean resolve)
throws ClassNotFoundException {
synchronized (getClassLoadingLock(name)) {
//首先,检查类是否已经被加载了
Class c = findLoadedClass(name);
if(c == null) {
//没被加载,先委派父ClassLoader或BootStrap ClassLoader去加载
try {
if(parent ! = null) {
//委派父ClassLoader, resolve参数固定为false
c = parent.loadClass(name, false);
} else {
c = findBootstrapClassOrNull(name);
}
} catch (ClassNotFoundException e) {
//没找到,捕获异常,以便尝试自己加载
}
if(c == null) {
//自己去加载,findClass才是当前ClassLoader的真正加载方法
c = findClass(name);
}
}
if(resolve) {
//链接,执行static语句块
resolveClass(c);
}
return c;
}
}

参数resolve类似Class.forName中的参数initialize,可以看出,其默认值为false,即使通过自定义ClassLoader重写loadClass,设置resolve为true,它调用父ClassLoader的时候,传递的也是固定的false。findClass是一个protected方法,类ClassLoader的默认实现就是抛出ClassNotFoundException,子类应该重写该方法,实现自己的加载逻辑,后文我们会给出具体例子。

第24章 类加载机制

在前几章中,我们多次提到了类加载器ClassLoader,本章就来详细讨论Java中的类加载机制与ClassLoader。

类加载器ClassLoader就是加载其他类的类,它负责将字节码文件加载到内存,创建Class对象。与之前介绍的反射、注解和动态代理一样,在大部分的应用编程中,我们需要自己实现ClassLoader。

不过,理解类加载的机制和过程,有助于我们更好地理解之前介绍的内容。在反射一章,我们介绍过Class的静态方法Class.forName,理解类加载器有助于我们更好地理解该方法。

ClassLoader一般是系统提供的,不需要自己实现,不过,通过创建自定义的ClassLoader,可以实现一些强大灵活的功能,比如:
1)热部署。在不重启Java程序的情况下,动态替换类的实现,比如Java Web开发中的JSP技术就利用自定义的ClassLoader实现修改JSP代码即生效,OSGI(OpenService Gateway Initiative)框架使用自定义ClassLoader实现动态更新。
2)应用的模块化和相互隔离。不同的ClassLoader可以加载相同的类但互相隔离、互不影响。Web应用服务器如Tomcat利用这一点在一个程序中管理多个Web应用程序,每个Web应用使用自己的ClassLoader,这些Web应用互不干扰。OSGI和Java 9利用这一点实现了一个动态模块化架构,每个模块有自己的ClassLoader,不同模块可以互不干扰。
3)从不同地方灵活加载。系统默认的ClassLoader一般从本地的.class文件或jar文件中加载字节码文件,通过自定义的ClassLoader,我们可以从共享的Web服务器、数据库、缓存服务器等其他地方加载字节码文件。

理解自定义ClassLoader有助于我们理解这些系统程序和框架,如Tomat、JSP、OSGI,在业务需要的时候,也可以借助自定义ClassLoader实现动态灵活的功能。

下面,我们首先来进一步理解Java加载类的过程,理解类ClassLoader和Class.for-Name,介绍一个简单的应用,然后探讨如何实现自定义ClassLoader,演示如何利用它实现热部署。

24.1 类加载的基本机制和过程

运行Java程序,就是执行java这个命令,指定包含main方法的完整类名,以及一个classpath,即类路径。类路径可以有多个,对于直接的class文件,路径是class文件的根目录,对于jar包,路径是jar包的完整名称(包括路径和jar包名)。

Java运行时,会根据类的完全限定名寻找并加载类,寻找的方式基本就是在系统类和指定的类路径中寻找,如果是class文件的根目录,则直接查看是否有对应的子目录及文件;如果是jar文件,则首先在内存中解压文件,然后再查看是否有对应的类。

负责加载类的类就是类加载器,它的输入是完全限定的类名,输出是Class对象。类加载器不是只有一个,一般程序运行时,都会有三个(适用于Java 9之前,Java9引入了模块化,基本概念是类似的,但有一些变化,限于篇幅,就不探讨了)。

1)启动类加载器(Bootstrap ClassLoader):这个加载器是Java虚拟机实现的一部分,不是Java语言实现的,一般是C++实现的,它负责加载Java的基础类,主要是<JAVA_HOME>/lib/rt.jar,我们日常用的Java类库比如String、ArrayList等都位于该包内。
2)扩展类加载器(Extension ClassLoader):这个加载器的实现类是sun.misc.Laun-cher$ExtClassLoader,它负责加载Java的一些扩展类,一般是<JAVA_HOME>/lib/ext目录中的jar包。
3)应用程序类加载器(Application ClassLoader):这个加载器的实现类是sun.misc. Launcher$AppClassLoader,它负责加载应用程序的类,包括自己写的和引入的第三方法类库,即所有在类路径中指定的类。

这三个类加载器有一定的关系,可以认为是父子关系,Application ClassLoader的父亲是Extension ClassLoader, Extension的父亲是Bootstrap ClassLoader。注意不是父子继承关系,而是父子委派关系,子ClassLoader有一个变量parent指向父ClassLoader,在子Class-Loader加载类时,一般会首先通过父ClassLoader加载,具体来说,在加载一个类时,基本过程是:
1)判断是否已经加载过了,加载过了,直接返回Class对象,一个类只会被一个Class-Loader加载一次。
2)如果没有被加载,先让父ClassLoader去加载,如果加载成功,返回得到的Class对象。
3)在父ClassLoader没有加载成功的前提下,自己尝试加载类。

这个过程一般被称为“双亲委派”模型,即优先让父ClassLoader去加载。为什么要先让父ClassLoader去加载呢?这样,可以避免Java类库被覆盖的问题。比如,用户程序也定义了一个类java.lang.String,通过双亲委派,java.lang.String只会被Bootstrap ClassLoader加载,避免自定义的String覆盖Java类库的定义。

需要了解的是,“双亲委派”虽然是一般模型,但也有一些例外,比如:
1)自定义的加载顺序:尽管不被建议,自定义的ClassLoader可以不遵从“双亲委派”这个约定,不过,即使不遵从,以java开头的类也不能被自定义类加载器加载,这是由Java的安全机制保证的,以避免混乱。
2)网状加载顺序:在OSGI框架和Java 9模块化系统中,类加载器之间的关系是一个网,每个模块有一个类加载器,不同模块之间可能有依赖关系,在一个模块加载一个类时,可能是从自己模块加载,也可能是委派给其他模块的类加载器加载。
3)父加载器委派给子加载器加载:典型的例子有JNDI服务(Java Naming andDirectoryInterface),它是Java企业级应用中的一项服务,具体我们就不介绍了。

一个程序运行时,会创建一个Application ClassLoader,在程序中用到ClassLoader的地方,如果没有指定,一般用的都是这个ClassLoader,所以,这个ClassLoader也被称为系统类加载器(System ClassLoader)。下面,我们来具体看下表示类加载器的类ClassLoader。

23.5 动态代理的应用:AOP

利用cglib动态代理,我们实现一个极简的AOP框架,演示AOP的基本思路和技术,先来看这个框架的用法,然后分析其实现原理。

23.5.1 用法

我们添加一个新的注解@Aspect,其定义为:

1
2
3
4
5
@Retention(RUNTIME)
@Target(TYPE)
public @interface Aspect {
Class<? >[] value();
}

它用于注解切面类,它有一个参数,可以指定要增强的类,比如:

1
2
@Aspect({ServiceA.class, ServiceB.class})
public class ServiceLogAspect

ServiceLogAspect就是一个切面,它负责类ServiceA和ServiceB的日志切面,即为这两个类增加日志功能。再如:

1
2
@Aspect({ServiceB.class})
public class ExceptionAspect

ExceptionAspect也是一个切面,它负责类ServiceB的异常切面。

这些切面类与主体类怎么协作呢?我们约定,切面类可以声明三个方法before/after/exception,在主体类的方法调用前/调用后/出现异常时分别调用这三个方法,这三个方法的声明需符合如下签名:

1
2
3
4
5
public static void before(Object object, Method method, Object[] args)
public static void after(Object object, Method method,
Object[] args, Object result)
public static void exception(Object object, Method method,
Object[] args, Throwable e)

object、method和args与cglib MethodInterceptor中 的invoke参 数 一 样,after中 的result表示方法执行的结果,exception中的e表示发生的异常类型。

ServiceLogAspect实现了before和after方法,加了一些日志,如代码清单23-6所示。

代码清单23-6 日志切面类
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
@Retention(RUNTIME)
@Target(TYPE)
public @interface Aspect {
Class<? >[] value();
}
@Aspect({ServiceA.class, ServiceB.class})
public class ServiceLogAspect
@Aspect({ServiceB.class})
public class ExceptionAspect
public static void before(Object object, Method method, Object[] args)
public static void after(Object object, Method method,
Object[] args, Object result)
public static void exception(Object object, Method method,
Object[] args, Throwable e)
@Aspect({ ServiceA.class, ServiceB.class })
public class ServiceLogAspect {
public static void before(Object object, Method method, Object[] args) {
System.out.println("entering " + method.getDeclaringClass()
.getSimpleName() + "::" + method.getName()
+ ", args: " + Arrays.toString(args));
}
public static void after(Object object, Method method, Object[] args,
Object result) {
System.out.println("leaving " + method.getDeclaringClass()
.getSimpleName() + "::" + method.getName()
+ ", result: " + result);
}
}

ExceptionAspect只实现exception方法,在异常发生时,输出一些信息,如代码清单23-7所示。

代码清单23-7 异常切面类
1
2
3
4
5
6
7
8
@Aspect({ ServiceB.class })
public class ExceptionAspect {
public static void exception(Object object,
Method method, Object[] args, Throwable e) {
System.err.println("exception when calling: "
+ method.getName() + ", " + Arrays.toString(args));
}
}

ServiceLogAspect的目的是在类ServiceA和ServiceB所有方法的执行前后加一些日志,而ExceptionAspect的目的是在类ServiceB的方法执行出现异常时收到通知并输出一些信息。它们都没有修改类ServiceA和ServiceB本身,本身做的事是比较通用的,与ServiceA和ServiceB的具体逻辑关系也不密切,但又想改变ServiceA/ServiceB的行为,这就是AOP的思维。

只是声明一个切面类是不起作用的,我们需要与第22章介绍的DI容器结合起来。我们实现一个新的容器CGLibContainer,它有一个方法:

1
public static <T> T getInstance(Class<T> cls)

通过该方法获取ServiceA或ServiceB,它们的行为就会被改变,ServiceA和ServiceB的定义与第22章一样,如代码清单22-1所示,这里就不重复了。

通过CGLibContainer获取ServiceA,会自动应用ServiceLogAspect,比如:

1
2
ServiceA a = CGLibContainer.getInstance(ServiceA.class);
a.callB();

输出为:

1
2
3
4
5
entering ServiceA::callB, args: []
entering ServiceB::action, args: []
I'm B
leaving ServiceB::action, result: null
leaving ServiceA::callB, result: null

23.5.2 实现原理

这是怎么做到的呢?CGLibContainer在初始化的时候,会分析带有@Aspect注解的类,分析出每个类的方法在调用前/调用后/出现异常时应该调用哪些方法,在创建该类的对象时,如果有需要被调用的方法,则创建一个动态代理对象,下面我们具体来看下代码。

为简化起见,我们基于第22章介绍的DI容器的第一个版本,即每次获取对象时都创建一个,不支持单例。我们定义一个枚举InterceptPoint,表示切点(调用前/调用后/出现异常):

1
2
3
public static enum InterceptPoint {
BEFORE, AFTER, EXCEPTION
}

在CGLibContainer中定义一个静态变量,表示每个类的每个切点的方法列表,定义如下:

1
2
static Map<Class<? >, Map<InterceptPoint, List<Method>>> interceptMethodsMap
= new HashMap<>();

我们在CGLibContainer的类初始化过程中初始化该对象,方法是分析每个带有@Aspect注解的类,这些类一般可以通过扫描所有的类得到,为简化起见,我们将它们写在代码中,如下所示:

1
2
static Class<? >[] aspects = new Class<? >[] {
ServiceLogAspect.class, ExceptionAspect.class };

分析这些带@Aspect注解的类,并初始化interceptMethodsMap的代码如下所示:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
static {
init();
}
private static void init() {
for(Class<? > cls : aspects) {
Aspect aspect = cls.getAnnotation(Aspect.class);
if(aspect ! = null) {
Method before = getMethod(cls, "before", new Class<? >[] {
Object.class, Method.class, Object[].class });
Method after = getMethod(cls, "after", new Class<? >[] {
Object.class, Method.class, Object[].class, Object.class });
Method exception = getMethod(cls, "exception", new Class<? >[] {
Object.class, Method.class, Object[].class, Throwable.class });
Class<? >[] intercepttedArr = aspect.value();
for(Class<? > interceptted : intercepttedArr) {
addInterceptMethod(interceptted,
InterceptPoint.BEFORE, before);
addInterceptMethod(interceptted, InterceptPoint.AFTER, after);
addInterceptMethod(interceptted,
InterceptPoint.EXCEPTION, exception);
}
}
}
}

对每个切面,即带有@Aspect注解的类cls,查找其before/after/exception方法,调用方法addInterceptMethod将其加入目标类的切点方法列表中,addInterceptMethod的代码为:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
private static void addInterceptMethod(Class<? > cls,
InterceptPoint point, Method method) {
if(method == null) {
return;
}
Map<InterceptPoint, List<Method>> map = interceptMethodsMap.get(cls);
if(map == null) {
map = new HashMap<>();
interceptMethodsMap.put(cls, map);
}
List<Method> methods = map.get(point);
if(methods == null) {
methods = new ArrayList<>();
map.put(point, methods);
}
methods.add(method);
}

准备好了每个类的每个切点的方法列表,我们来看根据类型创建实例的代码:

1
2
3
4
5
6
7
8
9
10
private static <T> T createInstance(Class<T> cls)
throws InstantiationException, IllegalAccessException {
if(! interceptMethodsMap.containsKey(cls)) {
return (T) cls.newInstance();
}
Enhancer enhancer = new Enhancer();
enhancer.setSuperclass(cls);
enhancer.setCallback(new AspectInterceptor());
return (T) enhancer.create();
}

如果类型cls不需要增强,则直接调用cls.newInstance(),否则使用cglib创建动态代理,callback为AspectInterceptor,其代码为:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
static class AspectInterceptor implements MethodInterceptor {
@Overridepublic
Object intercept(Object object, Method method,Object[] args, MethodProxy proxy) throws Throwable {
//执行before方法List<Method>
beforeMethods = getInterceptMethods(object.getClass().getSuperclass(), InterceptPoint.BEFORE);
for(Method m : beforeMethods) {
m.invoke(null, new Object[] {object, method, args });
}
try {
//调用原始方法Object
result = proxy.invokeSuper(object, args);
//执行after方法List<Method>
afterMethods = getInterceptMethods(object.getClass().getSuperclass(), InterceptPoint.AFTER);
for(Method m : afterMethods) {
m.invoke(null, new Object[] {object, method, args, result });
}
return result;
}
catch (Throwable e) {
//执行exception方法List<Method>
exceptionMethods = getInterceptMethods(object.getClass().getSuperclass(), InterceptPoint.EXCEPTION);
for(Method m : exceptionMethods) {
m.invoke(null, new Object[] {object, method, args, e });
}
throw e;
}
}
}

这段代码也容易理解,它根据原始类的实际类型查找应该执行的before/after/exception方法列表,在调用原始方法前执行before方法,执行后执行after方法,出现异常时执行exception方法。getInterceptMethods方法的代码为:

1
2
3
4
5
6
7
8
9
10
11
12
static List<Method> getInterceptMethods(Class<? > cls,
InterceptPoint point) {
Map<InterceptPoint, List<Method>> map = interceptMethodsMap.get(cls);
if(map == null) {
return Collections.emptyList();
}
List<Method> methods = map.get(point);
if(methods == null) {
return Collections.emptyList();
}
return methods;
}

这段代码也容易理解。CGLibContainer最终的getInstance方法就简单了,它调用create-Instance创建实例,代码如下所示:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
public static <T> T getInstance(Class<T> cls) {
try {
T obj = createInstance(cls);
Field[] fields = cls.getDeclaredFields();
for(Field f : fields) {
if(f.isAnnotationPresent(SimpleInject.class)) {
if(! f.isAccessible()) {
f.setAccessible(true);
}
Class<? > fieldCls = f.getType();
f.set(obj, getInstance(fieldCls));
}
}
return obj;
} catch (Exception e) {
throw new RuntimeException(e);
}
}

相比完整的AOP框架,这个AOP的实现是非常粗糙的,主要用于解释动态代理的应用和AOP的一些基本思路和原理。完整的代码在github上,地址为https://github.com/swift-ma/program-logic ,位于包shuo.laoma.dynamic.c86下。

本章探讨了Java中的代理,从静态代理到两种动态代理。动态代理广泛应用于各种系统程序、框架和库中,用于为应用程序员提供易用的支持、实现AOP,以及其他灵活通用的功能,理解了动态代理,我们就能更好地利用这些系统程序、框架和库,在需要的时候,也可以自己创建动态代理。

下一章,我们来进一步理解Java中的类加载过程,探讨如何利用自定义的类加载器实现更为动态强大的功能。

23.4 Java SDK代理与cglib代理比较

Java SDK代理面向的是一组接口,它为这些接口动态创建了一个实现类。接口的具体实现逻辑是通过自定义的InvocationHandler实现的,这个实现是自定义的,也就是说,其背后都不一定有真正被代理的对象,也可能有多个实际对象,根据情况动态选择。cglib代理面向的是一个具体的类,它动态创建了一个新类,继承了该类,重写了其方法

从代理的角度看,Java SDK代理的是对象,需要先有一个实际对象,自定义的InvocationHandler引用该对象,然后创建一个代理类和代理对象,客户端访问的是代理对象,代理对象最后再调用实际对象的方法;cglib代理的是类,创建的对象只有一个。

如果目的都是为一个类的方法增强功能,Java SDK要求该类必须有接口,且只能处理接口中的方法,cglib没有这个限制。

23.3 cglib动态代理

Java SDK动态代理的局限在于,它只能为接口创建代理,返回的代理对象也只能转换到某个接口类型,如果一个类没有接口,或者希望代理非接口中定义的方法,那就没有办法了。有一个第三方的类库cglib(https://github.com/cglib/cglib),可以做到这一点,Spring、Hibernate等都使用该类库。我们看个简单的例子,如代码清单23-5所示。

代码清单23-5 cglib动态代理示例
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
public class SimpleCGLibDemo {
static class RealService {
public void sayHello() {
System.out.println("hello");
}
}
static class SimpleInterceptor implements MethodInterceptor {
@Overridepublic
Object intercept(Object object, Method method,Object[] args, MethodProxy proxy) throws Throwable {
System.out.println("entering " + method.getName());
Object result = proxy.invokeSuper(object, args);
System.out.println("leaving " + method.getName());
return result;
}
}
private static <T> T getProxy(Class<T> cls) {
Enhancer enhancer = new Enhancer();
enhancer.setSuperclass(cls);
enhancer.setCallback(new SimpleInterceptor());
return (T) enhancer.create();
}
public static void main(String[] args) throws Exception {
RealService proxyService = getProxy(RealService.class);
proxyService.sayHello();
}
}

RealService表示被代理的类,它没有接口。getProxy()为一个类生成代理对象,这个代理对象可以安全地转换为被代理类的类型,它使用了cglib的Enhancer类。Enhancer类的setSuperclass设置被代理的类,setCallback设置被代理类的public非final方法被调用时的处理类。Enhancer支持多种类型,这里使用的类实现了MethodInterceptor接口,它与Java SDK中的InvocationHandler有点类似,方法名称变成了intercept,多了一个MethodProxy类型的参数。

与前面的InvocationHandler不同,SimpleInterceptor中没有被代理的对象,它通过MethodProxy的invokeSuper方法调用被代理类的方法:

1
Object result = proxy.invokeSuper(object, args);

注意,它不能这样调用被代理类的方法:

1
Object result = method.invoke(object, args);

object是代理对象,调用这个方法还会调用到SimpleInterceptor的intercept方法,造成死循环。

在main方法中,我们也没有创建被代理的对象,创建的对象直接就是代理对象。

cglib的实现机制与Java SDK不同,它是通过继承实现的,它也是动态创建了一个类,但这个类的父类是被代理的类,代理类重写了父类的所有public非final方法,改为调用Callback中的相关方法,在上例中,调用SimpleInterceptor的intercept方法。

23.1 静态代理

我们先介绍它的用法,然后介绍实现原理,最后分析它的优点。

23.2.1 用法

在静态代理中,代理类是直接定义在代码中的,在动态代理中,代理类是动态生成的,怎么动态生成呢?我们用动态代理实现前面的例子,如代码清单23-2所示。

代码清单23-2 使用Java SDK实现动态代理示例
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
public class SimpleJDKDynamicProxyDemo {
static interface IService {
public void sayHello();
}
static class RealService implements IService {
@Overridepublic
void sayHello() {
System.out.println("hello");
}
}
static class SimpleInvocationHandler implements InvocationHandler {
private Object realObj;
public SimpleInvocationHandler(Object realObj) {
this.realObj = realObj;
}
@Overridepublic
Object invoke(Object proxy, Method method,Object[] args) throws Throwable {
System.out.println("entering " + method.getName());
Object result = method.invoke(realObj, args);
System.out.println("leaving " + method.getName());
return result;
}
}
public static void main(String[] args) {
IService realService = new RealService();
IService proxyService = (IService) Proxy.newProxyInstance(IService.class.getClassLoader(), new Class<? >[] {IService.class },new SimpleInvocationHandler(realService));
proxyService.sayHello();
}
}

代码看起来更为复杂了,这有什么用呢?别着急,我们慢慢解释。IService和Real-Service的定义不变,程序的输出也没变,但代理对象proxyService的创建方式变了,它使用java.lang.reflect包中的Proxy类的静态方法newProxyInstance来创建代理对象,这个方法的声明如下:

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

它有三个参数,具体如下。
1)loader表示类加载器,下一章我们会单独探讨,例子使用和IService一样的类加载器。
2)interfaces表示代理类要实现的接口列表,是一个数组,元素的类型只能是接口,不能是普通的类,例子中只有一个IService。
3)h的类型为InvocationHandler,它是一个接口,也定义在java.lang.reflect包中,它只定义了一个方法invoke,对代理接口所有方法的调用都会转给该方法。

newProxyInstance的返回值类型为Object,可以强制转换为interfaces数组中的某个接口类型。这里我们强制转换为了IService类型,需要注意的是,它不能强制转换为某个类类型,比如RealService,即使它实际代理的对象类型为RealService。

SimpleInvocationHandler实现了InvocationHandler,它的构造方法接受一个参数realObj表示被代理的对象,invoke方法处理所有的接口调用,它有三个参数:
1)proxy表示代理对象本身,需要注意,它不是被代理的对象,这个参数一般用处不大。
2)method表示正在被调用的方法。
3)args表示方法的参数。

在SimpleInvocationHandler的invoke实现中,我们调用了method的invoke方法,传递了实际对象realObj作为参数,达到了调用实际对象对应方法的目的,在调用任何方法前后,我们输出了跟踪调试语句。需要注意的是,不能将proxy作为参数传递给method. invoke,比如:

1
Object result = method.invoke(proxy, args);

上面的语句会出现死循环,因为proxy表示当前代理对象,这又会调用到SimpleIn-vocationHandler的invoke方法。

23.2.2 基本原理

看了上面的介绍是不是更晕了,没关系,看下Proxy.newProxyInstance的内部就理解了。代码清单23-2中创建proxyService的代码可以用如下代码代替:

1
2
3
4
5
6
Class<? > proxyCls = Proxy.getProxyClass(IService.class.getClassLoader(),
new Class<? >[] { IService.class });
Constructor<? > ctor = proxyCls.getConstructor(
new Class<? >[] { InvocationHandler.class });
InvocationHandler handler = new SimpleInvocationHandler(realService);
IService proxyService = (IService) ctor.newInstance(handler);

分为三步:
1)通过Proxy.getProxyClass创建代理类定义,类定义会被缓存;
2)获取代理类的构造方法,构造方法有一个InvocationHandler类型的参数;
3)创建InvocationHandler对象,创建代理类对象。

Proxy.getProxyClass需要两个参数:一个是ClassLoader;另一个是接口数组。它会动态生成一个类,类名以$Proxy开头,后跟一个数字。对于上面的例子,动态生成的类定义如代码清单23-3所示,为简化起见,我们忽略了异常处理的代码。

代码清单23-3 Java SDK动态生成的代理类示例
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
final class $Proxy0 extends Proxy implementsSimpleJDKDynamicProxyDemo.IService {
private static Method m1;
private static Method m3;
private static Method m2;
private static Method m0;
public $Proxy0(InvocationHandler paramInvocationHandler) {
super(paramInvocationHandler);
}
public final boolean equals(Object paramObject) {
return((Boolean) this.h.invoke(this, m1,new Object[] {paramObject })).booleanValue();
}
public final void sayHello() {
this.h.invoke(this, m3, null);
}
public final String toString() {
return (String) this.h.invoke(this, m2, null);
}
public final int hashCode() {
return ((Integer) this.h.invoke(this, m0, null)).intValue();
}
static {
m1 = Class.forName("java.lang.Object").getMethod("equals",new Class[] {Class.forName("java.lang.Object") });
m3 = Class.forName("laoma.demo.proxy.SimpleJDKDynamicProxyDemo$IService").getMethod("sayHello", new Class[0]);
m2 = Class.forName("java.lang.Object").getMethod("toString", new Class[0]);
m0 = Class.forName("java.lang.Object").getMethod("hashCode", new Class[0]);
}
}

$Proxy0的父类是Proxy,它有一个构造方法,接受一个InvocationHandler类型的参数,保存为了实例变量h, h定义在父类Proxy中,它实现了接口IService,对于每个方法,如sayHello,它调用InvocationHandler的invoke方法,对于Object中的方法,如hash-Code、equals和toString, $Proxy0同样转发给了InvocationHandler。

可以看出,这个类定义本身与被代理的对象没有关系,与InvocationHandler的具体实现也没有关系,而主要与接口数组有关,给定这个接口数组,它动态创建每个接口的实现代码,实现就是转发给InvocationHandler,与被代理对象的关系以及对它的调用由InvocationHandler的实现管理

我们是怎么知道$Proxy0的定义的呢?对于Oracle的JVM,可以配置java的一个属性得到,比如:

1
2
java  -Dsun.misc.ProxyGenerator.saveGeneratedFiles=true  shuo.laoma.dynamic.c86.
SimpleJDKDynamicProxyDemo

以上命令会把动态生成的代理类$Proxy0保存到文件$Proxy0.class中,通过一些反编译器工具比如JD-GUI(http://jd.benow.ca/ )就可以得到源码。

理解了代理类的定义,后面的代码就比较容易理解了,就是获取构造方法,创建代理对象。

23.2.3 动态代理的优点

相比静态代理,动态代理看起来麻烦了很多,它有什么好处呢?使用动态代理,可以编写通用的代理逻辑,用于各种类型的被代理对象,而不需要为每个被代理的类型都创建一个静态代理类。看个简单的示例,如代码清单23-4所示。

代码清单23-4 通用的动态代理类示例
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
public class GeneralProxyDemo {
static interface IServiceA {
public void sayHello();
}
static class ServiceAImpl implements IServiceA {
@Override
public void sayHello() {
System.out.println("hello");
}
}
static interface IServiceB {
public void fly();
}
static class ServiceBImpl implements IServiceB {
@Override
public void fly() {
System.out.println("flying");
}
}
static class SimpleInvocationHandler implements InvocationHandler {
private Object realObj;
public SimpleInvocationHandler(Object realObj) {
this.realObj = realObj;
}
@Override
public Object invoke(Object proxy, Method method, Object[] args)
throws Throwable {
System.out.println("entering " + realObj.getClass()
.getSimpleName() + "::" + method.getName());
Object result = method.invoke(realObj, args);
System.out.println("leaving " + realObj.getClass()
.getSimpleName() + "::" + method.getName());
return result;
}
}
private static <T> T getProxy(Class<T> intf, T realObj) {
return (T) Proxy.newProxyInstance(intf.getClassLoader(),
new Class<? >[] { intf }, new SimpleInvocationHandler(realObj));
}
public static void main(String[] args) throws Exception {
IServiceA a = new ServiceAImpl();
IServiceA aProxy = getProxy(IServiceA.class, a);
aProxy.sayHello();
IServiceB b = new ServiceBImpl();
IServiceB bProxy = getProxy(IServiceB.class, b);
bProxy.fly();
}
}

在这个例子中,有两个接口IServiceA和IServiceB,它们对应的实现类是Service-AImpl和ServiceBImpl,虽然它们的接口和实现不同,但利用动态代理,它们可以调用同样的方法getProxy获取代理对象,共享同样的代理逻辑SimpleInvocationHandler,即在每个方法调用前后输出一条跟踪调试语句。程序输出为:

1
2
3
4
5
6
entering ServiceAImpl::sayHello
hello
leaving ServiceAImpl::sayHello
entering ServiceBImpl::fly
flying
leaving ServiceBImpl::fly

第23章 动态代理

本章,我们来探讨Java中另外一个动态特性:动态代理。动态代理是一种强大的功能,它可以在运行时动态创建一个类,实现一个或多个接口,可以在不修改原有类的基础上动态为通过该类获取的对象添加方法、修改行为,这么描述比较抽象,下文会具体介绍。这些特性使得它广泛应用于各种系统程序、框架和库中,比如Spring、Hibernate、MyBatis、Guice等。

动态代理是实现面向切面的编程AOP(Aspect Oriented Programming)的基础。切面的例子有日志、性能监控、权限检查、数据库事务等,它们在程序的很多地方都会用到,代码都差不多,但与某个具体的业务逻辑关系也不太密切,如果在每个用到的地方都写,代码会很冗余,也难以维护,AOP将这些切面与主体逻辑相分离,代码简单优雅得多。

和注解类似,在大部分的应用编程中,我们不需要自己实现动态代理,而只需要按照框架和库的文档说明进行使用就可以了。不过,理解动态代理有助于我们更为深刻地理解这些框架和库,也能更好地应用它们,在自己的业务需要时,也能自己实现。

要理解动态代理,我们首先要了解静态代理,了解了静态代理后,我们再来看动态代理。动态代理有两种实现方式:一种是Java SDK提供的;另外一种是第三方库(如cglib)提供的。我们会分别介绍这两种方式,包括其用法和基本实现原理,理解了基本概念和原理后,我们来看一个简单的应用,实现一个极简的AOP框架。

23.1 静态代理

我们首先介绍代理。代理是一个比较通用的词,作为一个软件设计模式,它在《设计模式》一书中被提出,基本概念和日常生活中的概念是类似的。代理背后一般至少有一个实际对象,代理的外部功能和实际对象一般是一样的,用户与代理打交道,不直接接触实际对象。虽然外部功能和实际对象一样,但代理有它存在的价值,比如:
1)节省成本比较高的实际对象的创建开销,按需延迟加载,创建代理时并不真正创建实际对象,而只是保存实际对象的地址,在需要时再加载或创建。
2)执行权限检查,代理检查权限后,再调用实际对象。
3)屏蔽网络差异和复杂性,代理在本地,而实际对象在其他服务器上,调用本地代理时,本地代理请求其他服务器。

代理模式的代码结构也比较简单,我们看个简单的例子,如代码清单代码23-1所示。

代码清单23-1 静态代理示例
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
public class SimpleStaticProxyDemo {
static interface IService {
public void sayHello();
}
static class RealService implements IService {
@Overridepublic
void sayHello() {
System.out.println("hello");
}
}
static class TraceProxy implements IService {
private IService realService;
public TraceProxy(IService realService) {
this.realService = realService;
}
@Overridepublic
void sayHello() {
System.out.println("entering sayHello");
this.realService.sayHello();
System.out.println("leaving sayHello");
}
}
public static void main(String[] args) {
IService realService = new RealService();
IService proxyService = new TraceProxy(realService);
proxyService.sayHello();
}
}

代理和实际对象一般有相同的接口,在这个例子中,共同的接口是IService,实际对象是RealService,代理是TraceProxy。TraceProxy内部有一个IService的成员变量,指向实际对象,在构造方法中被初始化,对于方法sayHello的调用,它转发给了实际对象,在调用前后输出了一些跟踪调试信息,程序输出为:

1
2
3
entering sayHello
hello
leaving sayHello

我们在第12章介绍过两种设计模式:适配器和装饰器,它们与代理模式有点类似,它们的背后都有一个别的实际对象,都是通过组合的方式指向该对象,不同之处在于,适配器是提供了一个不一样的新接口,装饰器是对原接口起到了“装饰”作用,可能是增加了新接口、修改了原有的行为等,代理一般不改变接口。不过,我们并不想强调它们的差别,可以将它们看作代理的变体,统一看待。

在上面的例子中,我们想达到的目的是在实际对象的方法调用前后加一些调试语句。为了在不修改原类的情况下达到这个目的,我们在代码中创建了一个代理类TraceProxy,它的代码是在写程序时固定的,所以称为静态代理。

输出跟踪调试信息是一个通用需求,可以想象,如果每个类都需要,而又不希望修改类定义,我们需要为每个类创建代理,实现所有接口,这个工作就太烦琐了,如果再有其他的切面需求,整个工作可能又要重来一遍。这时,就需要动态代理了,主要有两种方式实现动态代理:Java SDK和第三方库cglib,我们先来介绍JavaSDK。

22.6 注解的应用:DI容器

我们再来看一个简单的DI容器的例子。我们引入两个注解:一个是@SimpleInject;另一个是@SimpleSingleton,先来看@SimpleInject。

1. @SimpleInject

引入一个注解@SimpleInject,修饰类中字段,表达依赖关系,定义为:

1
2
3
4
@Retention(RUNTIME)
@Target(FIELD)
public @interface SimpleInject {
}

我们看两个简单的服务ServiceA和ServiceB, ServiceA依赖于ServiceB,它们的定义如代码清单22-1所示。

代码清单22-1 两个简单的服务ServiceA和ServiceB
1
2
3
4
5
6
7
8
9
10
11
12
public class ServiceA {
@SimpleInject
ServiceB b;
public void callB(){
b.action();
}
}
public class ServiceB {
public void action(){
System.out.println("I'm B");
}
}

ServiceA使用@SimpleInject表达对ServiceB的依赖。

DI容器的类为SimpleContainer,提供一个方法:

1
public static <T> T getInstance(Class<T> cls)

应用程序使用该方法获取对象实例,而不是自己new,使用方法如下所示:

1
2
ServiceA a = SimpleContainer.getInstance(ServiceA.class);
a.callB();

SimpleContainer.getInstance会创建需要的对象,并配置依赖关系,其代码为:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
public static <T> T getInstance(Class<T> cls) {
try {
T obj = cls.newInstance();
Field[] fields = cls.getDeclaredFields();
for(Field f : fields) {
if(f.isAnnotationPresent(SimpleInject.class)) {
if(! f.isAccessible()) {
f.setAccessible(true);
}
Class<? > fieldCls = f.getType();
f.set(obj, getInstance(fieldCls));
}
}
return obj;
} catch (Exception e) {
throw new RuntimeException(e);
}
}

代码假定每个类型都有一个public默认构造方法,使用它创建对象,然后查看每个字段,如果有SimpleInject注解,就根据字段类型获取该类型的实例,并设置字段的值。

2. @SimpleSingleton

在上面的代码中,每次获取一个类型的对象,都会新创建一个对象,实际开发中,这可能不是期望的结果,期望的模式可能是单例,即每个类型只创建一个对象,该对象被所有访问的代码共享,怎么满足这种需求呢?我们增加一个注解@SimpleSingleton,用于修饰类,表示类型是单例,定义如下:

1
2
3
4
@Retention(RUNTIME)
@Target(TYPE)
public @interface SimpleSingleton {
}

我们可以这样修饰ServiceB:

1
2
3
4
5
6
@SimpleSingleton
public class ServiceB {
public void action(){
System.out.println("I'm B");
}
}

SimpleContainer也需要做修改,首先增加一个静态变量,缓存创建过的单例对象:

1
private static Map<Class<? >, Object> instances = new ConcurrentHashMap<>();

getInstance也需要做修改,如下所示:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
public static <T> T getInstance(Class<T> cls) {
try {
boolean singleton = cls.isAnnotationPresent(SimpleSingleton.class);
if(! singleton) {
return createInstance(cls);
}
Object obj = instances.get(cls);
if(obj ! = null) {
return (T) obj;
}
synchronized (cls) {
obj = instances.get(cls);
if(obj == null) {
obj = createInstance(cls);
instances.put(cls, obj);
}
}
return (T) obj;
} catch (Exception e) {
throw new RuntimeException(e);
}
}

首先检查类型是否是单例,如果不是,就直接调用createInstance创建对象。否则,检查缓存,如果有,直接返回,如果没有,则调用createInstance创建对象,并放入缓存中。

createInstance与第一版的getInstance类似,代码为:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
private static <T> T createInstance(Class<T> cls) throws Exception {
T obj = cls.newInstance();
Field[] fields = cls.getDeclaredFields();
for(Field f : fields) {
if(f.isAnnotationPresent(SimpleInject.class)) {
if(! f.isAccessible()) {
f.setAccessible(true);
}
Class<? > fieldCls = f.getType();
f.set(obj, getInstance(fieldCls));
}
}
return obj;
}

本章介绍了Java中的注解,包括注解的使用、自定义注解和应用示例,示例的完整代码在github上,地址为https://github.com/swiftma/program-logic ,位于包shuo.laoma.dynamic. c85下。

注解提升了Java语言的表达能力,有效地实现了应用功能和底层功能的分离,框架/库的程序员可以专注于底层实现,借助反射实现通用功能,提供注解给应用程序员使用,应用程序员可以专注于应用功能,通过简单的声明式注解与框架/库进行协作

下一章,我们来探讨Java中一种更为动态灵活的机制:动态代理。