一、前言
笔者平时开发使用“动态代理”不多,最近在看设计模式的时候,“动态代理”又在面前晃了几次,所以这次想从源码的角度去分析动态代理的实现原理,以窥探其精妙~
二、正文
2.1 静态代理
本文源码基于:jdk1.6.0_33
在正式剖析动态代理的源码之前,我们可以先来看看“静态代理”(就是我们普通的代理模式)的UML图:
从上图可以看出,代理类“ProxySubject”和被代理类“RealSubject”实现相同接口,并且“ProxySubject”关联“RealSubject”,客户端调用的时候,实际是调用“ProxySubject”,然后“ProxySubject”调用“RealSubject”同名方法,方法的最终实现是由“RealSubject”提供,“ProxySubject”可以实现对“RealSubject”的访问控制,也可以对对应的方法进行“增强”。下面举个简单的例子说明一下:
// Subject public interface Subject { public void doSomething(); } //ProxySubject public class ProxySubject implements Subject { private Subject realSubject = new RealSubject(); @Override public void doSomething() { System.out.println("before doSomething~"); realSubject.doSomething(); System.out.println("after doSomething~"); } } //RealSubject public class RealSubject implements Subject { @Override public void doSomething() { System.out.println("小杜比亚在写博客"); } } //Client public class Client { public static void main(String[] args) { Subject subject = new ProxySubject(); subject.doSomething(); } } //输出 before doSomething~ 小杜比亚在写博客 after doSomething~
那么动态代理和这个静态代理有什么区别?这就体现在一个“动”字,从上面的代码我们知道,静态代理的“代理类”是程序员写好的,而动态代理的“代理类”是在程序运行期间动态生成的~有些刚开始接触Java编程的读者可能会很好奇,Java的类可以动态生成?当然可以,我之前有篇关于“类”加载的博文:深入探讨Java类加载机制里面有写到,程序运行期间,类加载器会将java字节码文件加载进内存,这边的“加载”是从磁盘上加载,java程序只要字节码文件,至于你的来源(网络、或者按照字节码文件格式进行动态生成),人家是不管的。
直接切入正题,“动态代理”中牵扯到两个类:java.lang.reflect.InvocationHandler和java.lang.reflect.Proxy,我们先看下一个简单的动态代理例子:
1 //Subject 2 package com.proxy.main; 3 public interface Subject { 4 public void doSomething(); 5 } 6 7 //RealSubject 8 package com.proxy.main; 9 public class RealSubject implements Subject { 10 11 @Override 12 public void doSomething() { 13 System.out.println("小杜比亚还在写博客,有点儿想玩游戏了,坚持~"); 14 } 15 } 16 17 //ProxyHandler 18 package com.proxy.main; 19 import java.lang.reflect.InvocationHandler; 20 import java.lang.reflect.Method; 21 22 public class ProxyHandler implements InvocationHandler { 23 private Object proxiedObj; 24 public ProxyHandler(Object proxiedObj){ 25 this.proxiedObj = proxiedObj; 26 } 27 /** 28 * @author caoyg 29 * @date 2017-05-06 30 * @description设置“被代理”对象 31 * @param proxiedObj 32 */ 33 public void setProxiedObject(Object proxiedObj){ 34 this.proxiedObj = proxiedObj; 35 } 36 @Override 37 public Object invoke(Object proxy, Method method, Object[] args)throws Throwable { 38 return method.invoke(proxiedObj, args); 39 } 40 41 } 42 43 //Client 44 package com.proxy.main; 45 import java.lang.reflect.Proxy; 46 47 public class Client { 48 49 public static void main(String[] args) { 50 Subject sb = (Subject) Proxy.newProxyInstance(Subject.class.getClassLoader(), 51 new Class[]{Subject.class} , 52 new ProxyHandler(new RealSubject())); 53 sb.doSomething(); 54 } 55 56 } 57 58 //输出 59 小杜比亚还在写博客,有点儿想玩游戏了,坚持~
上面代码第37行,invoke方法三个参数含义如下:
1 /** 2 该方法负责集中处理动态代理类上的所有方法调用。 3 第一个参数是动态生成的代理类实例, 4 第二个参数是被调用的方法对象 5 第三个方法是方法调用参数。 6 调用处理器根据这三个参数进行预处理或分派到委托类实例上执行 7 */ 8 public Object invoke(Object proxy, Method method, Object[] args) 9 throws Throwable;
2.2 动态代理机制
1、先创建一个InvocationHandler实现类,并且将需要“被代理”的对象传递给它(类似下面代码)
2、Proxy通过方法newProxyInstance接收classLoader和一组interfaces(“被代理”对象实现的接口)以及InvocationHandler的实现类实例(以下简称“h”),通过classLoader和interfaces构造动态代理类的Class(下面代码是Proxy的源码部分)
1 Class cl = getProxyClass(loader, interfaces); 2 ............ 3 public static Class<?> getProxyClass(ClassLoader loader,Class<?>... interfaces)throws IllegalArgumentException 4 { 5 //......省略 6 byte[] proxyClassFile = ProxyGenerator.generateProxyClass(proxyName, interfaces); 7 try { 8 proxyClass = defineClass0(loader, proxyName, 9 proxyClassFile, 0, proxyClassFile.length); 10 } catch (ClassFormatError e) { 11 /* 12 * A ClassFormatError here means that (barring bugs in the 13 * proxy class generation code) there was some other 14 * invalid aspect of the arguments supplied to the proxy 15 * class creation (such as virtual machine limitations 16 * exceeded). 17 */ 18 throw new IllegalArgumentException(e.toString()); 19 } 20 //......省略21 return proxyClass; 22 }
3、Proxy通过反射机制,将传入的h作为参数,调用动态代理对应的构造函数,返回最终的动态代理实例
在调用动态代理类的方法时,动态代理将方法的调用分派转发给InvocationHandler的invoke方法,InvocationHandler的invoke方法又将方法调用分派给它本身引用的“被代理”类对应的方法。
2.3 动态代理注意点
1、包:代理接口都是public修饰的,则代理类被定义在顶层包(package为空),如果访问修饰符是默认(default),那么代理类被定义在该接口所在包下面
1 /* 2 * Record the package of a non-public proxy interface so that the 3 * proxy class will be defined in the same package. Verify that 4 * all non-public proxy interfaces are in the same package. 5 */ 6 for (int i = 0; i < interfaces.length; i++) { 7 int flags = interfaces[i].getModifiers(); 8 if (!Modifier.isPublic(flags)) { 9 String name = interfaces[i].getName(); 10 int n = name.lastIndexOf(‘.‘); 11 String pkg = ((n == -1) ? "" : name.substring(0, n + 1)); 12 if (proxyPkg == null) { 13 proxyPkg = pkg; 14 } else if (!pkg.equals(proxyPkg)) { 15 throw new IllegalArgumentException("non-public interfaces from different packages"); 16 } 17 } 18 } 19 20 if (proxyPkg == null) { // if no non-public proxy interfaces, 21 proxyPkg = ""; // use the unnamed package 22 }
2、生成的代理类为public final,不能被继承
3、类名:格式是“$ProxyN”,N是逐一递增的数字,代表Proxy被第N次动态生成的代理类,要注意,对于同一组接口(接口的排列顺序也相同),不会重复创建动态代理类,而是返回一个先前已经创建并缓存了的代理类对象。提高了效率
1 /* 2 * Choose a name for the proxy class to generate.代理类类名生成规则 3 */ 4 long num; 5 synchronized (nextUniqueNumberLock) { 6 num = nextUniqueNumber++; 7 } 8 String proxyName = proxyPkg + proxyClassNamePrefix + num;
1 synchronized (cache) { 2 /* 3 * Note that we need not worry about reaping the cache for 4 * entries with cleared weak references because if a proxy class 5 * has been garbage collected, its class loader will have been 6 * garbage collected as well, so the entire cache will be reaped 7 * from the loaderToCache map. 8 */ 9 do { 10 Object value = cache.get(key); 11 if (value instanceof Reference) { 12 proxyClass = (Class) ((Reference) value).get(); 13 } 14 if (proxyClass != null) { 15 // proxy class already generated: return it 16 return proxyClass; 17 } else if (value == pendingGenerationMarker) { 18 // proxy class being generated: wait for it 19 try { 20 cache.wait(); 21 } catch (InterruptedException e) { 22 /* 23 * The class generation that we are waiting for should 24 * take a small, bounded time, so we can safely ignore 25 * thread interrupts here. 26 */ 27 } 28 continue; 29 } else { 30 /* 31 * No proxy class for this list of interfaces has been 32 * generated or is being generated, so we will go and 33 * generate it now. Mark it as pending generation. 34 */ 35 cache.put(key, pendingGenerationMarker); 36 break; 37 } 38 } while (true); 39 }
4、类继承关系
Proxy 类是它的父类,这个规则适用于所有由 Proxy 创建的动态代理类。(也算是java动态代理的一处缺陷,java不支持多继承,所以无法实现对class的动态代理,只能对于Interface的代理,cglib解决了这个问题)而且该类还实现了其所代理的一组接口,这就是为什么它能够被安全地类型转换到其所代理的某接口的根本原因。
5、代理类的根类 java.lang.Object 中有三个方法也同样会被分派到调用处理器的 invoke 方法执行,它们是 hashCode,equals 和 toString,代码在反编译中
1 //ProxyGenerator.class 2 private byte[] generateClassFile() 3 { 4 addProxyMethod(hashCodeMethod, Object.class); 5 addProxyMethod(equalsMethod, Object.class); 6 addProxyMethod(toStringMethod, Object.class); 7 ..................... 8 }
2.4 Proxy源码分析
主要关注一些静态变量和两个方法,我们先来看看Proxy中的静态变量:
1 /** prefix for all proxy class names */ 2 //类名前缀 3 private final static String proxyClassNamePrefix = "$Proxy"; 4 5 /** parameter types of a proxy class constructor */ 6 //proxy类构造函数的参数类型,这个在通过反射生成动态代理类的时候用到 7 private final static Class[] constructorParams = { InvocationHandler.class }; 8 9 /** maps a class loader to the proxy class cache for that loader */ 10 private static Map loaderToCache = new WeakHashMap(); 11 12 /** marks that a particular proxy class is currently being generated */ 13 private static Object pendingGenerationMarker = new Object(); 14 15 /** next number to use for generation of unique proxy class names */ 16 //下个proxy的序号 17 private static long nextUniqueNumber = 0; 18 private static Object nextUniqueNumberLock = new Object(); 19 20 /** set of all generated proxy classes, for isProxyClass implementation */ 21 private static Map proxyClasses = Collections.synchronizedMap(new WeakHashMap()); 22 23 /** 24 * the invocation handler for this proxy instance. 25 * @serial 26 */ 27 protected InvocationHandler h;
接下来是两个重要方法:getProxyClass和newProxyInstance,这个newProxyInstance就是我们在前面的动态代理实例里面直接使用的方法,这个方法调用getProxyClass获取动态代理类的Class对象,然后通过反射生成动态代理类实例并返回:
1 public static Object newProxyInstance(ClassLoader loader,Class<?>[] interfaces,InvocationHandler h)throws IllegalArgumentException 2 { 3 if (h == null) { 4 throw new NullPointerException(); 5 } 6 7 /* 8 * Look up or generate the designated proxy class. 9 */ 10 //生成动态代理Class 11 Class cl = getProxyClass(loader, interfaces); 12 13 /* 14 * Invoke its constructor with the designated invocation handler. 15 */ 16 try { 17 Constructor cons = cl.getConstructor(constructorParams); 18 //通过反射生成动态代理类实例对象,并返回 19 return (Object) cons.newInstance(new Object[] { h }); 20 } catch (NoSuchMethodException e) { 21 throw new InternalError(e.toString()); 22 } catch (IllegalAccessException e) { 23 throw new InternalError(e.toString()); 24 } catch (InstantiationException e) { 25 throw new InternalError(e.toString()); 26 } catch (InvocationTargetException e) { 27 throw new InternalError(e.toString()); 28 } 29 }
在Proxy整个源码中,主要工作都集中在方法getProxyClass中:
1 public static Class<?> getProxyClass(ClassLoader loader, Class<?>... interfaces)throws IllegalArgumentException 2 { 3 //接口数的限制 4 if (interfaces.length > 65535) { 5 throw new IllegalArgumentException("interface limit exceeded"); 6 } 7 8 Class proxyClass = null; 9 10 /* collect interface names to use as key for proxy class cache */ 11 String[] interfaceNames = new String[interfaces.length]; 12 13 Set interfaceSet = new HashSet(); // for detecting duplicates 14 /*下面这个for循环做三项安全验证: 15 *1、传入的所有接口是否对loader可见,如果不可见,抛出异常 16 *2、传入的所有”接口“是否真的都是interface,如果不是,抛出异常 17 *3、传入的所有接口是否有重复,如果有,抛出异常 18 */ 19 for (int i = 0; i < interfaces.length; i++) { 20 /* 21 * Verify that the class loader resolves the name of this 22 * interface to the same Class object. 23 */ 24 String interfaceName = interfaces[i].getName(); 25 Class interfaceClass = null; 26 try { 27 interfaceClass = Class.forName(interfaceName, false, loader); 28 } catch (ClassNotFoundException e) { 29 } 30 if (interfaceClass != interfaces[i]) { 31 throw new IllegalArgumentException( 32 interfaces[i] + " is not visible from class loader"); 33 } 34 35 /* 36 * Verify that the Class object actually represents an 37 * interface. 38 */ 39 if (!interfaceClass.isInterface()) { 40 throw new IllegalArgumentException( 41 interfaceClass.getName() + " is not an interface"); 42 } 43 44 /* 45 * Verify that this interface is not a duplicate. 46 */ 47 if (interfaceSet.contains(interfaceClass)) { 48 throw new IllegalArgumentException( 49 "repeated interface: " + interfaceClass.getName()); 50 } 51 interfaceSet.add(interfaceClass); 52 53 interfaceNames[i] = interfaceName; 54 } 55 56 /* 57 * Using string representations of the proxy interfaces as 58 * keys in the proxy class cache (instead of their Class 59 * objects) is sufficient because we require the proxy 60 * interfaces to be resolvable by name through the supplied 61 * class loader, and it has the advantage that using a string 62 * representation of a class makes for an implicit weak 63 * reference to the class. 64 */ 65 //接口名称列表(List类型) 66 Object key = Arrays.asList(interfaceNames); 67 68 /* 69 * Find or create the proxy class cache for the class loader.(查询或者创建代理类的缓存,缓存未找到或者已经失效,那么重新创建一个) 70 */ 71 Map cache; 72 synchronized (loaderToCache) { 73 cache = (Map) loaderToCache.get(loader); 74 if (cache == null) { 75 cache = new HashMap(); 76 loaderToCache.put(loader, cache); 77 } 78 /* 79 * This mapping will remain valid for the duration of this 80 * method, without further synchronization, because the mapping 81 * will only be removed if the class loader becomes unreachable. 82 */ 83 } 84 85 /* 86 * Look up the list of interfaces in the proxy class cache using 87 * the key. This lookup will result in one of three possible 88 * kinds of values: 89 * null, if there is currently no proxy class for the list of 90 * interfaces in the class loader, 91 * the pendingGenerationMarker object, if a proxy class for the 92 * list of interfaces is currently being generated, 93 * or a weak reference to a Class object, if a proxy class for 94 * the list of interfaces has already been generated. 95 */ 96 synchronized (cache) { 97 /* 98 * Note that we need not worry about reaping the cache for 99 * entries with cleared weak references because if a proxy class 100 * has been garbage collected, its class loader will have been 101 * garbage collected as well, so the entire cache will be reaped 102 * from the loaderToCache map. 103 */ 104 do { 105 //接口名称列表作为key,查找代理类缓存中是否已经存在创建好的动态代理类 106 Object value = cache.get(key); 107 if (value instanceof Reference) { 108 proxyClass = (Class) ((Reference) value).get(); 109 } 110 //代理类如果已经创建,直接返回 111 if (proxyClass != null) { 112 // proxy class already generated: return it 113 return proxyClass; 114 } else if (value == pendingGenerationMarker) {//动态代理类还处于创建状态,那么当前线程进入等待 115 // proxy class being generated: wait for it 116 try { 117 cache.wait(); 118 } catch (InterruptedException e) { 119 /* 120 * The class generation that we are waiting for should 121 * take a small, bounded time, so we can safely ignore 122 * thread interrupts here. 123 */ 124 } 125 continue;//被唤醒之后,进行第二次检查 126 } else { 127 /* 128 * No proxy class for this list of interfaces has been 129 * generated or is being generated, so we will go and 130 * generate it now. Mark it as pending generation. 131 */ 132 /*如果接口名称列表映射的动态代理类还未创建,那么将pendingGenerationMarker先设置到动态代理类的缓存中, 133 *作为key的映射,表示接下来准备要开始动态代理类的创建了,并且退出当前循环 134 */ 135 cache.put(key, pendingGenerationMarker); 136 break; 137 } 138 } while (true); 139 } 140 141 try { 142 String proxyPkg = null; // package to define proxy class in 143 144 /* 145 * Record the package of a non-public proxy interface so that the 146 * proxy class will be defined in the same package. Verify that 147 * all non-public proxy interfaces are in the same package. 148 */ 149 /*接下来这个循环,就是生成动态代理类的包名。首先校验所有interface是否都是public类型,如果声明为 150 *public的,那么动态代理类的包名为"",如果存在不止一个interface的接口声明为非public类型(不写修饰符的情况), 151 *那么,所有这些非public访问声明的接口必须在同一个包中,否则抛出错误;如果这些非public访问声明的接口都在同 152 *一个包中,那么动态代理类的包名和这些非public访问声明的接口的包名一致 153 */ 154 for (int i = 0; i < interfaces.length; i++) { 155 int flags = interfaces[i].getModifiers(); 156 if (!Modifier.isPublic(flags)) { 157 String name = interfaces[i].getName(); 158 int n = name.lastIndexOf(‘.‘); 159 String pkg = ((n == -1) ? "" : name.substring(0, n + 1)); 160 if (proxyPkg == null) { 161 proxyPkg = pkg; 162 } else if (!pkg.equals(proxyPkg)) { 163 throw new IllegalArgumentException( 164 "non-public interfaces from different packages"); 165 } 166 } 167 } 168 169 if (proxyPkg == null) { // if no non-public proxy interfaces, 170 proxyPkg = ""; // use the unnamed package 171 } 172 173 { 174 /* 175 * Choose a name for the proxy class to generate. 176 */ 177 long num; 178 //生成下一个动态代理类的序号 179 synchronized (nextUniqueNumberLock) { 180 num = nextUniqueNumber++; 181 } 182 //动态代理类的类名:包名+proxyClassNamePrefix + num; 183 String proxyName = proxyPkg + proxyClassNamePrefix + num; 184 /* 185 * Verify that the class loader hasn‘t already 186 * defined a class with the chosen name. 187 */ 188 189 /* 190 * Generate the specified proxy class. 191 */ 192 //这个方法别人说有点儿复杂,反编译进去看确实挺复杂,这边笔者解释不了,这边就是生成动态代理类的字节 193 byte[] proxyClassFile = ProxyGenerator.generateProxyClass( 194 proxyName, interfaces); 195 try { 196 proxyClass = defineClass0(loader, proxyName, 197 proxyClassFile, 0, proxyClassFile.length); 198 } catch (ClassFormatError e) { 199 /* 200 * A ClassFormatError here means that (barring bugs in the 201 * proxy class generation code) there was some other 202 * invalid aspect of the arguments supplied to the proxy 203 * class creation (such as virtual machine limitations 204 * exceeded). 205 */ 206 throw new IllegalArgumentException(e.toString()); 207 } 208 } 209 // add to set of all generated proxy classes, for isProxyClass 210 proxyClasses.put(proxyClass, null); 211 212 } finally { 213 /* 214 * We must clean up the "pending generation" state of the proxy 215 * class cache entry somehow. If a proxy class was successfully 216 * generated, store it in the cache (with a weak reference); 217 * otherwise, remove the reserved entry. In all cases, notify 218 * all waiters on reserved entries in this cache. 219 */ 220 synchronized (cache) { 221 if (proxyClass != null) { 222 /*如果动态代理类创建成功,那么将动态代理类缓存中key对应的值更新成new WeakReference(proxyClass),原先 223 *设置的是pendingGenerationMarker 224 */ 225 cache.put(key, new WeakReference(proxyClass)); 226 } else { 227 //如果创建不成功,那么将key对应的映射移除 228 cache.remove(key); 229 } 230 //不管创建成功不成功,将阻塞在代理类缓存上面的线程唤醒 231 cache.notifyAll(); 232 } 233 } 234 return proxyClass; 235 }
以上内容已经大致将动态代理的内部实现交代清楚,下面的程序将动态代理的实例写入class文件,然后反编译出来看下,动态代理类的实例到底长什么样:
生成动态代理类的Class文件代码:
1 package com.proxy.main; 2 3 import java.io.FileOutputStream; 4 import java.lang.reflect.Proxy; 5 6 import sun.misc.ProxyGenerator; 7 8 public class Client { 9 10 public static void main(String[] args) { 11 Subject sb = (Subject) Proxy.newProxyInstance(Subject.class.getClassLoader(), 12 new Class[]{Subject.class} , 13 new ProxyHandler(new RealSubject())); 14 sb.doSomething(); 15 createProxyClassFile(); 16 } 17 18 public static void createProxyClassFile(){ 19 try{ 20 String proxyName = "MyProxy"; 21 byte[] data = ProxyGenerator.generateProxyClass(proxyName, new Class[]{Subject.class}); 22 FileOutputStream out = new FileOutputStream( proxyName + ".class" ); 23 out.write(data); 24 out.close(); 25 } 26 catch(Exception e){ 27 e.printStackTrace(); 28 } 29 } 30 31 }
动态代理类反编译出来的代码:
1 //因为传进去的interface数据中,所有interface的声明都是public,所以,动态代理类的包名为"" 2 import com.proxy.main.Subject; 3 import java.lang.reflect.InvocationHandler; 4 import java.lang.reflect.Method; 5 import java.lang.reflect.Proxy; 6 import java.lang.reflect.UndeclaredThrowableException; 7 //继承自Proxy 8 public final class MyProxy extends Proxy 9 implements Subject 10 { 11 private static Method m1; 12 private static Method m0; 13 private static Method m3; 14 private static Method m2; 15 16 public MyProxy(InvocationHandler paramInvocationHandler) 17 throws 18 { 19 super(paramInvocationHandler); 20 } 21 22 public final boolean equals(Object paramObject) 23 throws 24 { 25 try 26 { 27 return ((Boolean)this.h.invoke(this, m1, new Object[] { paramObject })).booleanValue(); 28 } 29 catch (RuntimeException localRuntimeException) 30 { 31 throw localRuntimeException; 32 } 33 catch (Throwable localThrowable) 34 { 35 } 36 throw new UndeclaredThrowableException(localThrowable); 37 } 38 39 public final int hashCode() 40 throws 41 { 42 try 43 { 44 return ((Integer)this.h.invoke(this, m0, null)).intValue(); 45 } 46 catch (RuntimeException localRuntimeException) 47 { 48 throw localRuntimeException; 49 } 50 catch (Throwable localThrowable) 51 { 52 } 53 throw new UndeclaredThrowableException(localThrowable); 54 } 55 56 public final void doSomething() 57 throws 58 { 59 try 60 { 61 this.h.invoke(this, m3, null); 62 return; 63 } 64 catch (RuntimeException localRuntimeException) 65 { 66 throw localRuntimeException; 67 } 68 catch (Throwable localThrowable) 69 { 70 } 71 throw new UndeclaredThrowableException(localThrowable); 72 } 73 74 public final String toString() 75 throws 76 { 77 try 78 { 79 return (String)this.h.invoke(this, m2, null); 80 } 81 catch (RuntimeException localRuntimeException) 82 { 83 throw localRuntimeException; 84 } 85 catch (Throwable localThrowable) 86 { 87 } 88 throw new UndeclaredThrowableException(localThrowable); 89 } 90 91 static 92 { 93 try 94 { 95 m1 = Class.forName("java.lang.Object").getMethod("equals", new Class[] { Class.forName("java.lang.Object") }); 96 m0 = Class.forName("java.lang.Object").getMethod("hashCode", new Class[0]); 97 m3 = Class.forName("com.proxy.main.Subject").getMethod("doSomething", new Class[0]); 98 m2 = Class.forName("java.lang.Object").getMethod("toString", new Class[0]); 99 return; 100 } 101 catch (NoSuchMethodException localNoSuchMethodException) 102 { 103 throw new NoSuchMethodError(localNoSuchMethodException.getMessage()); 104 } 105 catch (ClassNotFoundException localClassNotFoundException) 106 { 107 } 108 throw new NoClassDefFoundError(localClassNotFoundException.getMessage()); 109 } 110 }
三、链接
http://blog.csdn.net/scplove/article/details/52451899
http://www.cnblogs.com/flyoung2008/archive/2013/08/11/3251148.html
四、联系本人
为方便没有博客园账号的读者交流,特意建立一个企鹅群,读者如果有对博文不明之处,欢迎加群交流:261746360,小杜比亚-博客园