延迟加载时代理的典型应用之一,在分析Spring代码时,我们梳理了反射和代理模式模式的原理,这里,我们重点看CGLIB和JAVASSIST的用法和如何落地的。
“延迟加载”的含义是:暂时不用的对象不会真正载入到内存中, 直到真正需要使用该对象时,才去执行数据库查询操作,将该对象加载到内存中 。在 MyBatis 中,如果一个对象的某个属性需要延迟加载,那么在映射该属性时,会为该属性创建相应的代理对象并返回; 当真正要使用延迟加载的属性时,会通过代理对象执行数据库加载操作,得到真正的数据。所以我们就看一下代理模式,以及如何落地的。
一个属性是否能够延时加载,主要看两个地方的配置:
(1)如果属性在<resultMap>中的相应节点明确地配置了 fetchType 属性,则按照 fetchType属性决定是否延迟加载。
(2)如果未配 置 fetchType 属性,则需要根据 mybatis-config.xml 配置文件中的lazyLoadingEnabled 配置决定是否延时加载,具体配置如下 :
<setting name= ” lazyLoadingEnabled” value= "true ” />
<setting name="aggressiveLazyLoading” value=” false " />
与延时加载相关的另一个配置项是 aggressiveLazyLoading , 当该配置项为 true 时,表示有延迟加载属性的对象在被调用,将完全加载其属性,否则属性将按需要加载属性 。
MyBatis 中的延迟加载是通过动态代理实现的,可能读者第一反应就是使用前面介绍的 JDK动态代理实现该功能。但是正如前面的介绍所述,要使用 JDK 动态代理的方式为一个对象生成代理对象, 要求该目标类必须实现了(任意〉接口,而 MyBatis 映射的结果对象大多是普通的JavaBean , 并没有实现任何接口,所以无法使用 JDK 动态代理。 MyBatis 中提供了另外两种可以为普通 JavaBean 动态生成代理对象的方式,分别是 CGLIB 方式和 JAVASSIST 方式 。
1.CGlib
cglib 采用字节码技术实现动态代理功能,其原理是通过字节码技术为目标类生成一个子类,并在该子类中采用方法拦截的方式拦截所有父类方法的调用,从而实现代理的功能。因为 cglib使用生成子类的方式实现动态代理,所以无法代理 final 关键宇修饰的方法。 cglib 与 JDK 动态代理之间可以相互补充 : 在目标类实现接口时 ,使用 JDK 动态代理创建代理对象,但当目标类没有实现接口时,使用 cglib 实现动态代理的功能。在 Spring 、 MyBatis 等多种开源框架中,都可以看到 JDK 动态代理与 cglib 结合使用的场景。
下面通过一个示例简单介绍 cglib 的使用 。在使用 cglib 创建动态代理类时,首先需要定义
一个 Callback 接口的实现, cglib 中也提供了 多个 Callback 接口的子接口:
MethodInterceptor是Callback的子接口,我们基于该接口实现一个CGlib的例子:
代理类:
public class CglibProxy implements MethodInterceptor {
private Enhancer enhancer = new Enhancer(); // cglib中的Enhancer对象
@Override
public Object intercept(Object obj, Method method, Object[] args, MethodProxy proxy) throws Throwable {
System.out.println("前置处理");
Object result = proxy.invokeSuper(obj, args);
System.out.println("后置处理");
return result;
}
public Object getProxy(Class clazz) {
enhancer.setSuperclass(clazz);
enhancer.setCallback(this);
return enhancer.create();
}
}
测试类:
public class CGLibTest {
public String method(String str) {
System.out.println("CGLib method():" + str);
return str;
}
public static void main(String[] args) {
CglibProxy proxy = new CglibProxy();
CGLibTest proxyImp = (CGLibTest) proxy.getProxy(CGLibTest.class);
String result = proxyImp.method("test");
System.out.println(result);
}
}
打印结果
2.Javassist
Javassist 是一个开源的生成 Java 字节码的类库,其主要优点在于简单、快速,直接使用
Javassist 提供的 JavaAPI 就能动态修改类的结构,或是动态的生成类。
Javassist 的使用 比较简单,首先来看如何使用 Javassist 提供的 Java API 动态创建类
代理接口:
/**
* 可以唱歌的
*/
public interface Singable {
/**
* 唱歌
*/
void sing();
}
被代理类:
/**
* 歌手
*/
public class Singer implements Singable {
@Override
public void sing() {
System.out.println("I am singing...");
}
}
创建代理接口:
public class Client {
public static void main(String[] args) throws Exception {
Singable proxy = createJavassistDynamicProxy();
proxy.sing();
}
private static Singable createJavassistDynamicProxy()
throws Exception {
ProxyFactory proxyFactory = new ProxyFactory();
// 设置实现的接口
proxyFactory.setInterfaces(new Class[]{Singable.class});
Class<?> proxyClass = proxyFactory.createClass();
Singable javassistProxy = (Singable) proxyClass.getDeclaredConstructor().newInstance();
((ProxyObject) javassistProxy).setHandler(new JavassistInterceptor(new Singer()));
return javassistProxy;
}
private static class JavassistInterceptor implements MethodHandler {
// 被代理对象
private Object delegate;
private JavassistInterceptor(Object delegate) {
this.delegate = delegate;
}
/**
* @param self 创建的代理对象
* @param m 被代理方法
* @param proceed 如果代理接口,此参数为null,如果代理类,此参数为父类的方法
* @param args 方法参数
*/
public Object invoke(Object self, Method m, Method proceed,
Object[] args) throws Throwable {
System.out.println("javassist proxy before sing");
Object ret = m.invoke(delegate, args);
System.out.println("javassist proxy after sing");
return ret;
}
}
}
和JDK的动态代理创建方式类似,但Javassist也可以代理类。
public class Client {
public static void main(String[] args) throws Exception {
Singable proxy = createJavassistDynamicProxy();
proxy.sing();
}
private static Singable createJavassistDynamicProxy()
throws Exception {
ProxyFactory proxyFactory = new ProxyFactory();
// 设置父类
proxyFactory.setSuperclass(Singer.class);
Class<?> proxyClass = proxyFactory.createClass();
Singable javassistProxy = (Singable) proxyClass.getDeclaredConstructor().newInstance();
((ProxyObject) javassistProxy).setHandler(new JavassistInterceptor());
return javassistProxy;
}
private static class JavassistInterceptor implements MethodHandler {
/**
* @param self 创建的代理对象
* @param m 被代理方法
* @param proceed 如果代理接口,此参数为null,如果代理类,此参数为父类的方法
* @param args 方法参数
*/
public Object invoke(Object self, Method m, Method proceed,
Object[] args) throws Throwable {
System.out.println("javassist proxy before sing");
// 调用父类的sing方法
Object ret = proceed.invoke(self, args);
System.out.println("javassist proxy after sing");
return ret;
}
}
}
还可以使用Javassist提供的字节码API实现:
public class Client {
public static void main(String[] args) throws Exception {
Singable proxy = createJavassistBytecodeDynamicProxy(new Singer());
proxy.sing();
}
private static Singable createJavassistBytecodeDynamicProxy(Singable delegate) throws Exception {
ClassPool mPool = new ClassPool(true);
CtClass mCtc = mPool.makeClass(Singable.class.getName() + "JavaassistProxy");
mCtc.addInterface(mPool.get(Singable.class.getName()));
mCtc.addConstructor(CtNewConstructor.defaultConstructor(mCtc));
mCtc.addField(CtField.make("public " + Singable.class.getName() + " delegate;", mCtc));
String src = "public void sing() { "
+ "System.out.println(\"javassist bytecode proxy before sing\");"
+ "delegate.sing();"
+ "System.out.println(\"javassist bytecode proxy after sing\"); "
+ "}";
mCtc.addMethod(CtNewMethod.make(src, mCtc));
Class<?> pc = mCtc.toClass();
Singable bytecodeProxy = (Singable) pc.getDeclaredConstructor().newInstance();
Field filed = bytecodeProxy.getClass().getField("delegate");
filed.set(bytecodeProxy, delegate);
return bytecodeProxy;
}
}
Javassist可以直接拼接java源码生成字节码,这是比ASM易用的地方,但也会造成一定的性能损失。
3.延迟加载实现的过程
MyBatis 中与延迟加载相关的类有 ResultLoader、 ResultLoaderMap 、 ProxyFactoy接口及实现类。ResultLoader 主要负责保存一次延迟加载操作所需的全部信息,ResultLoader 的核心是 loadResult()方法,该方法会通过 Executor 执行ResultLoader中记录的SQL语句井返回相应的延迟加载对象。
public Object loadResult() throws SQLException {
List<Object> list = selectList();
resultObject = resultExtractor.extractObjectFromList(list, targetType);
return resultObject;
}
其中, selectList()方法才是完成延迟加载操作的地方,具体实现如下:
private <E> List<E> selectList() throws SQLException {
Executor localExecutor = executor;
if (Thread.currentThread().getId() != this.creatorThreadId || localExecutor.isClosed()) {
localExecutor = newExecutor();
}
try {
return localExecutor.query(mappedStatement, parameterObject, RowBounds.DEFAULT, Executor.NO_RESULT_HANDLER, cacheKey, boundSql);
} finally {
if (localExecutor != executor) {
localExecutor.close(false);
}
}
}
延迟加载得到的是 List类型的对象 , ResultEx位actor.extractObjectFromList()方法负责将其转换为 targetType 类型的对象,大致逻辑如下:
- 如果目标对象类型为 List,则无须转换。
- 如果目标对象类型是 Collection 子类、数组类型(其中项可以是基本类型 ,也可以是对象类型),则创建 targetType 类型的集合对象,并复制 List<O均ect>中的项。
- 如果目标对象是普通 Java 对象且延迟加载得到的 List 大小为 1 ,则认为将其中唯一的项作为转换后的对象返回。
- ResultLoaderMap 与 ResultLoader 之 间的关系非常密切,在 ResultLoaderMap 中使用 loadMap字段保存对象中延迟加载属性及其对应的 ResultLoader 对象之间的关系。ResultLoaderMap 中提供了 load()和 loadAll()两个执行延迟加载的入口方法,前者负责加载指定名称的属性,后者则是加载该对象中全部的延迟加载属性。ResultLoaderMap.load()方法和 loadAll()方法最终都是通过调用 LoadPair.load()方法实现的,
LoadPair.load()方法的具体代码如下:
public boolean load(String property) throws SQLException {
LoadPair pair = loaderMap.remove(property.toUpperCase(Locale.ENGLISH));
if (pair != null) {
pair.load();
return true;
}
return false;
}
这里的loaderMap存的就是代理对象。javaassist和CGlib都使用该map作为代理对象的缓存。
之后就是javaassist和CGlib分别执行相关的代理逻辑了。