在养猪场日志(二)中,我简单的学习了Mybatis的入门程序,因为无论是在Mybatis中还是Spring中,都有动态代理模式,自己以前也有初步的学习过动态代理模式,过年回家的时候又看了一些书和博文,加深了我对动态代理模式的一些理解,所以这篇博文就先总结一下我对动态代理模式的学习过程,方便以后学习。
所谓代理模式(ProxyPattern),实际就是一个类代表另一个类的功能。光是这一句,不好理解,举个例子说明一下。比如我要买一辆自行车(要啥自行车),我就要去自行车销售店买,而至于我要买的自行车是那个地方的那个工厂生产组装的,我并不关心。这里面有四个对象:我、自行车、销售商、工厂,其中我就就是代理模式中的方法调用者,自行车就是我调用方法想要的结果,销售商是代理类,而工厂就是被代理类。(其实就是我去销售商那里买了工厂生产的自行车)
而至于代理模式的优点,我觉得就是为了代码解耦,使代码的业务逻辑更清晰。
代理模式有好多种,网上一搜好多,这篇博文我就学一下简单的静态代理和动态代理(JDK动态代理和CGLIB动态代理)。
为了更好的理解代理模式,我这里以一个简单的需求来学习。
需求:将10000个字母“A”拼接成字符串,并计算拼接字符串所用的时间。
其实这个实现起来很简单,我直接上代码了:
package com.eboy.nomal;
import java.util.Date;
publicclass AppendString {
publicstaticvoid main(String[] args) {
// TODO Auto-generated method stub
appendString();
}
/**
*
* @Title: appendString
* @Description:字符串拼接并计算耗时TODO
* @param@return
* @return String
* @throws
*/
publicstatic String appendString(){
// TODO Auto-generated method stub
//记录开始时间
longstartTime = new Date().getTime();
String finalString = "";
//拼接字符串
for (inti = 0; i< 10000; i++) {
finalString += "A";
}
//记录结束时间
longendTime = new Date().getTime();
//计算耗时
longtime = endTime - startTime;
System.out.println("拼接10000个'A'耗时" + time + "毫秒!");
//返回结果
returnfinalString;
}
}
执行main方法就可以轻松的计算出拼接字符串所耗的时间。但是如下图所示:
本来appendString这个方法的主要业务是拼接字符串,但是在这个方法中却混杂了计算时间的代码,那么这个方法到底主要业务是什么呢?这样就不好说了。所以就需要想办法将拼接字符串的代码和计算时间的代码分开,最简单的解决办法就是将两部分代码写在两个不同的类中,但是又怎么把他们结合起来呢?用代理模式解决就是一个不错的选择。
个人觉得静态代理模式中最大的特点就是代理类是程序员自己编写的。下面说具体实现过程。
定义一个接口,并在接口中定义拼接字符串的抽象方法,代码如下:
package com.eboy.staticProxy;
/**
*
* @ClassName:AppendString
* @Description:字符串拼接接口TODO
* @authorExceptionalBoy
* @date 2018年2月21日下午7:58:19
* @version V1.0
*/
publicinterface AppendString {
/**
*
* @Title: appendString
* @Description:字符串凭借方法TODO
* @param
* @return String
* @throws
*/
public StringappendString();
}
4.2 被代理类
定义被代理类,实现业务接口中的抽象方法,方法体为拼接字符串的代码。具体如下:
package com.eboy.staticProxy;
/**
*
* @ClassName:AppendStringImpl
* @Description:字符串拼接实现类(也就是被代理类,其中的方法为真正的业务方法) TODO
* @author ExceptionalBoy
* @date 2018年2月21日下午7:59:35
* @version V1.0
*/
publicclassAppendStringImpl implements AppendString{
/*
* 被代理类字符串拼接方法
*(non-Javadoc)
* @seecom.eboy.staticProxy.AppendString#appendString()
*/
public StringappendString() {
// TODO Auto-generated method stub
/*
*将10000个字母'A'拼接为字符串
*/
String finalString = "";
//拼接字符串
for (inti = 0; i< 10000; i++) {
finalString += "A";
}
//为了能清楚地看到这个方法被执行,添加一个打印语句
System.out.println("字符串拼接结束...");
//返回结果
returnfinalString;
}
}
定义代理类,在实现接口抽象方法的同时,将被代理类的实例作为私有属性持有。代码如下:
package com.eboy.staticProxy;
import java.util.Date;
/**
*
* @ClassName:AppendStringProxy
* @Description:字符串拼接代理类TODO
* @authorExceptionalBoy
* @date 2018年2月21日下午8:07:45
* @version V1.0
*/
publicclassAppendStringStaticProxy implements AppendString {
//将接口实现类实例(也就是被代理类的实例)作为代理类的私有属性持有
private AppendString appendString;
//代理类构造方法
publicAppendStringStaticProxy() {
//创建代理类对象的同时,实例化被代理类对象,也就是给私有属性赋值
this.appendString = new AppendStringImpl();
}
/*
* 代理类字符串拼接代理方法
*(non-Javadoc)
* @seecom.eboy.staticProxy.AppendString#appendString()
*/
public StringappendString() {
// TODO Auto-generated method stub
System.out.println("静态代理模式...");
//记录开始时间
longstartTime = new Date().getTime();
//拼接字符串(代理类中的字符串拼接方法中的拼接字符串部分功能代码实际调用的是被代理类中的字符串拼接方法)
String string = this.appendString.appendString();
//记录结束时间
longendTime = new Date().getTime();
//计算耗时
longtime = endTime - startTime;
System.out.println("拼接10000个'A'耗时" + time + "毫秒!");
//返回结果
returnstring;
}
}
其实在代理类中实现的抽象方法还是调用的被代理类的实现方法,只不过代理类又对其进行了扩展,添加了计算时间的代码。
编写测试类如下:
package com.eboy.staticProxy;
/**
*
* @ClassName:StaticProxyTest
* @Description:静态代理测试类TODO
* @authorExceptionalBoy
* @date 2018年2月21日下午8:19:13
* @version V1.0
*/
publicclassStaticProxyTest {
publicstaticvoid main(String[] args) {
//创建代理类对象
AppendString appendString = new AppendStringStaticProxy();
//执行代理类的代理方法
appendString.appendString();
}
}
执行结果如下:
这样就将字符串拼接代码和时间计算代码分离了。
和静态代理模式相比较,我觉得动态代理模式就是代理类由代码动态的生成。Java中有多种动态代理技术,比如JDK、CGLIB、Javassist、ASM,下面就学一下JDK和CGLIB的动态代理技术吧。
JDK动态代理技术值JDK自带的功能,主要由java.lang.reflect.*包提供。一看这个包,想到的一定是Java的反射机制,所以JDK的动态代理一定使用了反射。
在JDK的动态代理中,要实现代理逻辑的类必须实现java.lang.reflect.InvocationHandler接口,它里面定义了一个invoke方法,用于实现代理逻辑,在JDK的API中描述如下图所示:
下面说具体实现。
JDK的动态代理中,业务接口和被代理类代码实现与静态代理模式的业务接口和被代理类代码实现是一致的,所以这里不贴代码了,把上面的复制过来直接用就可以了。
这个类的作用就是绑定代理逻辑,生成代理类,要实现前面提到的InvocationHandler接口,并实现invoke方法,代码如下:
packagecom.eboy.dynamicProxy.jdk;
importjava.lang.reflect.InvocationHandler;
importjava.lang.reflect.Method;
importjava.lang.reflect.Proxy;
importjava.util.Date;
/**
*
* @ClassName: AppendStringDynamicProxy
* @Description:动态代理绑定和代理逻辑实现类TODO
* @author ExceptionalBoy
* @date 2018年2月21日 下午8:35:22
* @version V1.0
*/
public classJDKDynamicProxy implements InvocationHandler{
//将接口实现类实例(也就是被代理类的实例)作为私有属性持有
private AppendString appendString;
//提供创建JDK代理类对象的方法
public AppendStringgetProxyInstance(AppendString appendString){
//被代理类实例赋值
this.appendString = appendString;
//通过反射机制创建代理类对象
/*
* Proxy类的newProxyInstance(...)三个参数的解释:
* 第一个参数:是一个类加载器,这里使用的是被代理类的类加载器
* 第二个参数:是指明生成的动态代理对象挂在那个接口下面(个人理解就是指明创建那个接口的实现类对象)
* 第三个参数:是定义实现方法的逻辑类(个人理解就是实现了InvocationHandler接口的实现类,
* 而且它实现了InvocationHandler接口的invoke方法,这个invoke方法就是代理类的
* 代理逻辑实现方法,相当于代理类中的appendString方法)
*/
Object obj =Proxy.newProxyInstance(appendString.getClass().getClassLoader(),appendString.getClass().getInterfaces(), this);
//返回创建的对象
return (AppendString) obj;
}
/*
* 实现InvocationHandler接口的invoke方法,相当于静态代理中代理类的appendString方法
*(non-Javadoc)
*@see java.lang.reflect.InvocationHandler#invoke(java.lang.Object,java.lang.reflect.Method, java.lang.Object[])
*/
public Object invoke(Object proxy, Methodmethod, Object[] args) throws Throwable {
// TODO Auto-generated method stub
System.out.println("JDK动态代理模式...");
//记录开始时间
long startTime = newDate().getTime();
//拼接字符串(反射)
Object obj =method.invoke(this.appendString, args);//此处相当于调用了appendString()方法
//记录结束时间
long endTime = newDate().getTime();
//计算耗时
long time = endTime - startTime;
System.out.println("拼接10000个'A'耗时" + time +"毫秒!");
return obj;
}
}
JDK动态代理的测试类与静态代理的测试类稍微不同,要先调用方法,动态的生成代理类对象,代码如下:
package com.eboy.dynamicProxy.jdk;
publicclassJDKDynamicProxyTest {
publicstaticvoid main(String[] args) {
JDKDynamicProxy jdkDynamicProxy = new JDKDynamicProxy();
//获取代理对象
AppendString proxy = jdkDynamicProxy.getProxyInstance(new AppendStringImpl());
//执行代理对象的代理方法
proxy.appendString();
}
}
运行结果如下,不再做多的解释了:
CGLIB动态代理技术是第三方提供的代理技术,与JDK的动态代理技术相比,CGLIB的动态代理技术不需要提供接口,只需要一个非抽象类就可以实现动态代理。因为是第三方提供的代理技术,所以要先引入第三方JAR包。由于在Spring框架中就有,这里直接引入Spring框架的源码包,如图:
引入jar包后,就可以继续接下来的学习了。
这里的业务类很简单,就是一个包含一个字符串拼接方法的普通类,代码如下:
package com.eboy.dynamicProxy.cglib;
/**
*
* @ClassName:AppendStringImpl
* @Description:字符串拼接业务类TODO
* @authorExceptionalBoy
* @date 2018年2月21日下午9:24:06
* @version V1.0
*/
publicclass AppendString{
public StringappendString() {
// TODO Auto-generated method stub
/*
*将10000个字母'A'拼接为字符串
*/
String finalString = "";
//拼接字符串
for (inti = 0; i< 10000; i++) {
finalString += "A";
}
//为了能清楚地看到这个方法被执行,添加一个打印语句
System.out.println("字符串拼接结束...");
//返回结果
returnfinalString;
}
}
这个类和JDK的动态代理模式中的作用基本相同,但是要实现的是org.springframework.cglib.proxy.MethodInterceptor接口,并实现intercept方法。这里看到MethodInterceptor这个接口名以及intercept方法,最容易想到的就是拦截器了,其实用动态代理是可以实现一个拦截器的逻辑的,这个留着后面在学习吧。下面说具体代码实现:
package com.eboy.dynamicProxy.cglib;
import java.lang.reflect.Method;
import java.util.Date;
import org.springframework.cglib.proxy.Enhancer;
importorg.springframework.cglib.proxy.MethodInterceptor;
import org.springframework.cglib.proxy.MethodProxy;
publicclassCGLIBDynamicProxy implements MethodInterceptor{
//提供创建cglib代理对象的方法
public ObjectgetCGLIBProxy(Class> cls){
Enhancer enhancer = new Enhancer();
//设置增强类型(个人理解就是设置要生成的代理类继承哪个类)
enhancer.setSuperclass(cls);
//定义代理逻辑对象(个人理解就生成代理类的代理方法,也就是appendString方法)
enhancer.setCallback(this);
//创建并返回代理对象
returnenhancer.create();
}
@Override
public Object intercept(Object proxy, Method method, Object[] args, MethodProxy methodProxy) throws Throwable {
// TODO Auto-generated method stub
System.out.println("CGLIB动态代理模式...");
//记录开始时间
longstartTime = new Date().getTime();
//拼接字符串(反射)
Object obj = methodProxy.invokeSuper(proxy, args);//此处相当于调用了appendString()方法
//记录结束时间
longendTime = new Date().getTime();
//计算耗时
longtime = endTime - startTime;
System.out.println("拼接10000个'A'耗时" + time + "毫秒!");
returnobj;
}
}
测试类的实现逻辑和JDK代理模式的测试类实现逻辑基本一样,还是现获取代理类对象,然后调用代理类的代理方法,代码如下:
package com.eboy.dynamicProxy.cglib;
publicclassCGLIBDynamicProxyTest {
publicstaticvoid main(String[] args) {
CGLIBDynamicProxy cglibDynamicProxy = new CGLIBDynamicProxy();
//获取代理对象
AppendString proxy = (AppendString) cglibDynamicProxy.getCGLIBProxy(AppendString.class);
//执行代理对象的代理方法
proxy.appendString();
}
}
运行结果不多解释,看图:
其实代理模式还有很多中,比如简单的我之前还学过什么虚拟代理、强制代理等等,这里只是简单的总结了一下我自己对JDK和CGLIB两种动态代理技术的学习内容,主要是怕以后忘了,留着方便查看。因为代理模式最主要的还是理解,所以这篇里的源码我都没用图片,方便理解吧。
本来打算这一篇连带着写一下观察者模式呢,后来光一个动态代理就写了很多,今晚是没时间了,后面一篇写吧。