6.1 涉及的技术难点简介
来到原理章,我们有必要对一些常用的、基础的技术难点进行简介,否则读者可能难以理解本章的内容。
正如我们第 2 章描述的那样,Mapper 仅仅是一个接口,而不是一个包含逻辑的实现类。我们知道一个接口是没有办法去执行的,那么它是怎么运行的呢?这不是违反了教科书所说的接口不能运行的道理吗?相信不少的读者会对此产生极大的疑惑。
答案就是动态代理,我们不妨看看 Mapper 到底是一个什么东西,如图 6-1 所示。
图 6-1 Mapper 动态代理
很显然 Mapper 产生了代理类,这个代理类是由 MyBatis 为我们创建的,为此我们不妨先来学习一下动态代理,这有利于后续的学习。
首先,什么是代理模式?所谓的代理模式就是在原有的服务上多加一个占位,通过这个占位去控制服务的访问。这句话不太容易理解,举例而言,假设你是一个公司的工程师,能提供一些技术服务,公司的客服是一个美女,她不懂技术。而我是一个客户,需要你们公司提供技术服务。显然,我只会找到你们公司的客服,和客服沟通,而不是找你沟通。客服会根据公司的规章制度和业务规则来决定找不找你服务。那么这个时候客服就等同于你的一个代理,她通过和我的交流来控制对你的访问,当然她也可以提供一些你们公司对外的服务。而我只能通过她的代理访问你。对我而言,根本不需要认识你,只需要认识客服就可以了。事实上,站在我的角度,我会认为客服就代表你们公司,而不管真正为我服务的你是怎么样的。
其次,为什么要使用代理模式?通过代理,一方面可以控制如何访问真正的服务对象,提供额外服务。另外一方面有机会通过重写一些类来满足特定的需要,正如客服也可以根据公司的业务规则,提供一些服务,这个时候就不需要劳你大驾了。下面给出动态代理示意图,如图 6-2 所示。
图 6-2 动态代理示意图
一般而言,动态代理分为两种,一种是 JDK 反射机制提供的代理,另一种是 CGLIB 代理。在 JDK 提供的代理,我们必须要提供接口,而 CGLIB 则不需要提供接口,在 MyBatis 里面两种动态代理技术都已经使用了。但是在此之前我们需要学习的技术就是反射,让我们开始基础技术的学习。
6.1.1 反射技术
在 Java 中,反射技术已经大行其道,并且通过不断优化,Java 的可配置性等性能得到了巨大的提高。让我们来写一个服务打印 hello + 姓名 ,如代码清单 6-1 所示。
代码清单 6-1:ReflectService.java 反射示例
import java.lang.reflect.InvocationTargetException; import java.lang.reflect.Method; public class ReflectService { /** * 服务方法 * @param name -- 姓名 */ public void sayHello(String name) { System.err.println("hello" + name); } /** * 测试入口 * @param args */ public static void main(String[] args) throws ClassNotFoundException, NoSuchMethodException, InstantiationException, IllegalAccessException, IllegalArgumentException, InvocationTargetException { //通过反射创建 ReflectService 对象 Object service = Class.forName(ReflectService.class.getName()). newInstance(); //获取服务方法 - sayHello Method method=service.getClass().getMethod("sayHello", String. class); //反射调用方法 method.invoke(service, "zhangsan"); } }
这段代码通过反射技术去创建 ReflectService 对象,获取方法后通过反射调用。
反射调用的最大好处是配置性大大提高,就如同 Spring IOC 容器一样,我们可以给很多配置设置参数,使得 Java 应用程序能够顺利运行起来,大大提高了 Java 的灵活性和可配置性,降低模块之间的耦合。
6.1.2 JDK 动态代理
JDK 的动态代理,是由 JDK 的 java.lang.reflect.*包提供支持的,我们需要完成这么几个步骤。
编写服务类和接口,这个是真正的服务提供者,在 JDK 代理中接口是必须的。
编写代理类,提供绑定和代理方法。
JDK 的代理最大的缺点是需要提供接口,而 MyBatis 的 Mapper 就是一个接口,它采用的就是 JDK 的动态代理。我们先给一个服务接口,如代码清单 6-2 所示。
代码清单 6-2:HelloService.java
public interface HelloService { public void sayHello(String name); }
然后,写一个实现类,如代码清单 6-3 所示。
代码清单 6-3:HelloServiceImpl.java
public class HelloServiceImpl implements HelloService{ @Override public void sayHello(String name) { System.err.println("hello " + name); } }
现在我们写一个代理类,提供真实对象的绑定和代理方法。代理类的要求是实现 InvocationHandler 接口的代理方法,当一个对象被绑定后,执行其方法的时候就会进入到代理方法里,如代码清单 6-4 所示。
代码清单 6-4:HelloServiceProxy.java
public class HelloServiceProxy implements InvocationHandler { /** * 真实服务对象 */ private Object target; /** * 绑定委托对象并返回一个代理类 * @param target * @return */ public Object bind(Object target) { this.target = target; //取得代理对象 return Proxy.newProxyInstance(target.getClass().getClassLoader(), target.getClass().getInterfaces(), this); //jdk 代理需要提供接口 } @Override /** * 通过代理对象调用方法首先进入这个方法 * @param proxy --代理对象 * @param method -- 被调用方法 * @param args -- 方法的参数 */ public Object invoke(Object proxy, Method method, Object[] args) throws Throwable { System.err.println("############我是 JDK 动态代理################"); Object result = null; //反射方法前调用 System.err.println("我准备说 hello。"); //执行方法,相当于调用 HelloServiceImpl 类的 sayHello 方法 result=method.invoke(target, args); //反射方法后调用 System.err.println("我说过 hello 了"); return result; } } Proxy.newProxyInstance(target.getClass().getClassLoader(), target.getClass().getInterfaces(), this);
上面这段代码让 JDK 产生一个代理对象。这个代理对象有三个参数:第一个参数 target.getClass().getClassLoader() 是类加载器,第二个参数 target.getClass().getInterfaces() 是接口(代理对象挂在哪个接口下),第三个参数 this 代表当前 HelloServiceProxy 类,换句话说是使用 HelloServiceProxy 的代理方法作为对象的代理执行者。
一旦绑定后,在进入代理对象方法调用的时候就会到 HelloServiceProxy 的代理方法上,代理方法有三个参数:第一个 proxy 是代理对象,第二个是当前调用的那个方法,第三个是方法的参数。比方说,现在 HelloServiceImpl 对象(obj)用 bind 方法绑定后,返回其占位,我们再调用 proxy.sayHello("张三"),那么它就会进入到 HelloServiceProxy 的 invoke() 方法。而 invoke 参数中第一个便是代理对象 proxy,方法便是 sayHello,参数是张三。
我们已经用 HelloServiceProxy 类的属性 target 保存了真实的服务对象,那么我们可以通过反射技术调度真实对象的方法。
result=method.invoke(target, args);
这里我们演示了 JDK 动态代理的实现,并且在调用方法前后都可以加入我们想要的东西。MyBatis 在使用 Mapper 的时候也是这样做的。
让我们测试一下动态代理,如代码清单 6-5 所示。
代码清单 6-5:HelloServiceMain.java
public class HelloServiceMain { public static void main(String[] args) { HelloServiceProxy HelloHandler = new HelloServiceProxy(); HelloService proxy = (HelloService)HelloHandler.bind(new HelloServiceImpl()); proxy.sayHello("张三"); } }
我们运行它,就可以看到如下运行结果。
############我是 JDK 动态代理################ 我准备说 hello hello 张三 我说过 hello 了
6.1.3 CGLIB 动态代理
JDK 提供的动态代理存在一个缺陷,就是你必须提供接口才可以使用,为了克服这个缺陷,我们可以使用开源框架 - CGLIB,它是一种流行的动态代理。
让我们看看如何使用 CGLIB 动态代理。HelloService.java 和 HelloServiceImpl.java 都不需要改变,但是我们要实现 CGLIB 的代理类。它的实现 MethodInterceptor 的代理方法如代码清单 6-6 所示。
代码清单 6-6:HelloServiceCgLib.java
public class HelloServiceCgLib 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 proxy) throws Throwable { System.err.println("##############我是 CGLIB 的动态代理###### ######"); //反射方法前调用 System.err.println("我准备说 hello"); Object returnObj = proxy.invokeSuper(obj, args); //反射方法后调用 System.err.println("我说过 hello 了"); return returnObj; } }
这样便能够实现 CGLIB 的动态代理。在 MyBatis 中通常在延迟加载的时候才会用到 CGLIB 的动态代理。有了这些基础,我们就可以更好地论述 MyBatis 的解析和运行过程了。
绑定邮箱获取回复消息
由于您还没有绑定你的真实邮箱,如果其他用户或者作者回复了您的评论,将不能在第一时间通知您!
发布评论