前言
最近,看了一下对于 RMI(Remote Method Invocation) 相干的常识,遇到了一个动静代理的问题,而后就决定探索一下动静代理。
这里先科普一下 RMI。
RMI
像咱们平时写的程序,对象之间相互调用办法都是在同一个 JVM 中进行,而 RMI 能够实现一个 JVM 上的对象调用另一个 JVM 上对象的办法,即近程调用。
接口定义
定义一个近程对象接口,实现 Remote 接口来进行标记。
public interface UserInterface extends Remote {void sayHello() throws RemoteException;
}
近程对象定义
定义一个近程对象类,继承 UnicastRemoteObject 来实现 Serializable 和 Remote 接口,并实现接口办法。
public class User extends UnicastRemoteObject implements UserInterface {public User() throws RemoteException {}
@Override
public void sayHello() {System.out.println("Hello World");
}
}
服务端
启动服务端,将 user 对象在注册表上进行注册。
public class RmiServer {public static void main(String[] args) throws RemoteException, AlreadyBoundException, MalformedURLException {User user = new User();
LocateRegistry.createRegistry(8888);
Naming.bind("rmi://127.0.0.1:8888/user", user);
System.out.println("rmi server is starting...");
}
}
启动服务端:
客户端
从服务端注册表获取近程对象,在服务端调用 sayHello() 办法。
public class RmiClient {public static void main(String[] args) throws RemoteException, NotBoundException, MalformedURLException {UserInterface user = (UserInterface) Naming.lookup("rmi://127.0.0.1:8888/user");
user.sayHello();}
}
服务端运行后果:至此,一个简略的 RMI demo 实现。
动静代理
提出问题
看了看 RMI 代码,感觉 UserInterface 这个接口有点多余,如果客户端应用 Naming.lookup() 获取的对象不强转成 UserInterface,间接强转成 User 是不是也能够,于是试了一下,就报了以下谬误:似曾相识又有点生疏的 $Proxy0,翻了翻尘封的笔记找到了是动静代理的知识点,寥寥几笔带过,所以决定梳理一下动静代理,重新整理一份笔记。
动静代理 Demo
接口定义
public interface UserInterface {void sayHello();
}
实在角色定义
public class User implements UserInterface {
@Override
public void sayHello() {System.out.println("Hello World");
}
}
调用解决类定义
代理类调用实在角色的办法时,其实是调用与实在角色绑定的解决类对象的 invoke() 办法,而 invoke() 调用的是实在角色的办法。
这里须要实现 InvocationHandler 接口以及 invoke() 办法。
public class UserHandler implements InvocationHandler {
private User user;
public UserProxy(User user) {this.user = user;}
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {System.out.println("invoking start....");
method.invoke(user);
System.out.println("invoking stop....");
return user;
}
}
执行类
public class Main {public static void main(String[] args) {User user = new User();
// 解决类和实在角色绑定
UserHandler userHandler = new UserHandler(user);
// 开启将代理类 class 文件保留到本地模式,平时能够省略
System.getProperties().put("sun.misc.ProxyGenerator.saveGeneratedFiles", "true");
// 动静代理生成代理对象 $Proxy0
Object o = Proxy.newProxyInstance(Main.class.getClassLoader(), new Class[]{UserInterface.class}, userHandler);
// 调用的其实是 invoke()
((UserInterface)o).sayHello();}
运行后果:这样动静代理的根本用法就学完了,可是还有好多问题不明确。
- 动静代理是怎么调用的 invoke() 办法?
- 解决类 UserHandler 有什么作用?
- 为什么要将类加载器和接口类数组当作参数传入 newProxyInstance?
如果让你去实现动静代理,你有什么设计思路?
猜测
动静代理,是不是和动态代理,即设计模式的代理模式有相同之处呢?
简略捋一捋代理模式实现原理:实在角色和代理角色独特实现一个接口并实现形象办法 A,代理类持有实在角色对象,代理类在 A 办法中调用实在角色对象的 A 办法。在 Main 中实例化代理对象,调用其 A 办法,间接调用了实在角色的 A 办法。
「实现代码」
// 接口和实在角色对象就用下面代码
// 代理类,实现 UserInterface 接口
public class UserProxy implements UserInterface {
// 持有实在角色对象
private User user = new User();
@Override
public void sayHello() {System.out.println("invoking start....");
// 在代理对象的 sayHello() 里调用实在角色的 sayHello()
user.sayHello();
System.out.println("invoking stop....");
}
}
// 运行类
public class Main {public static void main(String[] args) {
// 实例化代理角色对象
UserInterface userProxy = new UserProxy();
// 调用了代理对象的 sayHello(),其实是调用了实在角色的 sayHello()
userProxy.sayHello();}
拿开始的动静代理代码和动态代理比拟,接口、实在角色都有了,区别就是多了一个 UserHandler 解决类,少了一个 UserProxy 代理类。
接着比照一下两者的解决类和代理类,发现 UserHandler 的 invoke() 和 UserProxy 的 sayHello() 这两个办法的代码都是一样的。那么,是不是新建一个 UserProxy 类,而后实现 UserInterface 接口并持有 UserHandler 的对象,在 sayHello() 办法中调用 UserHandler 的 invoke() 办法,就能够动静代理了。
「代码大略就是这样的」
// 猜测的代理类构造,动静代理生成的代理是 com.sun.proxy.$Proxy0
public class UserProxy implements UserInterface{
// 持有解决类的对象
private InvocationHandler handler;
public UserProxy(InvocationHandler handler) {this.handler = handler;}
// 实现 sayHello() 办法,并调用 invoke()
@Override
public void sayHello() {
try {handler.invoke(this, UserInterface.class.getMethod("sayHello"), null);
} catch (Throwable throwable) {throwable.printStackTrace();
}
}
}
// 执行类
public static void main(String[] args) {User user = new User();
UserHandler userHandler = new UserHandler(user);
UserProxy proxy = new UserProxy(userHandler);
proxy.sayHello();}
输入后果:
下面的代理类代码是写死的,而动静代理是当你调用 Proxy.newProxyInstance() 时,会依据你传入的参数来动静生成这个代理类代码,如果让我实现,会是以下这个流程。
- 依据你传入的 Class[] 接口数组,代理类会来实现这些接口及其办法 ( 这里就是 sayHello()),并且持有你传入的 userHandler 对象,应用文件流将事后设定的包名、类名、办法名等一行行代码写到本地磁盘,生成 $Proxy0.java 文件
- 应用编译器将编译成 Proxy0.class
- 依据你传入的 ClassLoader 将 $Proxy0.class 加载到 JMV 中
- 调用 Proxy.newProxyInstance() 就会返回一个 $Proxy0 的对象,而后调用 sayHello(),就执行了外面 userHandler 的 invoke()
以上就是对动静代理的一个猜测过程,上面就通过 debug 看看源码是怎么实现的。
在困惑的日子里学会拥抱源码
拥抱源码
调用流程图
这里先用 PPT 画一个流程图,能够跟着流程图来看前面的源码。
流程图
「从 newProxyInstance() 设置断点」
newProxyInstance()
newProxyInstance() 代码分为高低两局部,上局部是获取类,下局部是通过反射构建 Proxy0 对象。
「上局部代码」
newProxyInstance()
从名字看就晓得 getProxyClass0() 是外围办法,step into
getProxyClass0()
getProxyClass()
外面调用了 WeakCache 对象的 get() 办法,这里暂停一下 debug,先讲讲 WeakCache 类。
WeakCache
顾名思义,它是一个弱援用缓存。那什么是是弱援用呢,是不是还有强援用呢?
弱援用
WeakReference 就是弱援用类,作为包装类来包装其余对象,在进行 GC 时,其中的包装对象会被回收,而 WeakReference 对象会被放到援用队列中。
举个栗子:
// 这就是强援用,只有不写 str1 = null,str1 指向的这个字符串不就会被垃圾回收
String str1 = new String("hello");
ReferenceQueue referenceQueue = new ReferenceQueue();
// 只有垃圾回收,这个 str2 外面包装的对象就会被回收,然而这个弱援用对象不会被回收,即 word 会被回收,然而 str2 指向的弱援用对象不会
// 每个弱援用关联一个 ReferenceQueue,当包装的对象被回收,这个弱援用对象会被放入援用队列中
WeakReference<String> str2 = new WeakReference<>(new String("world"), referenceQueue);
// 执行 gc
System.gc();
Thread.sleep(3);
// 输入被回收包装对象的弱援用对象:java.lang.ref.WeakReference@2077d4de
// 能够 debug 看一下,弱援用对象的 referent 变量指向的包装对象曾经为 null
System.out.println(referenceQueue.poll());
WeakCache 的构造
其实整个 WeakCache 的都是围绕着成员变量 map 来工作的,构建了一个一个 <K,<K,V>> 格局的二级缓存,在动静代理中对应的类型是 < 类加载器, < 接口 Class, 代理 Class>>,它们都应用了弱援用进行包装,这样在垃圾回收的时候就能够间接回收,缩小了堆内存占用。
// 寄存已回收弱援用的队列
private final ReferenceQueue<K> refQueue = new ReferenceQueue<>();
// 应用 ConcurrentMap 实现的二级缓存构造
private final ConcurrentMap<Object, ConcurrentMap<Object, Supplier<V>>> map = new ConcurrentHashMap<>();
// 能够不关注这个,这个是用来标识二级缓存中的 value 是否存在的,即 Supplier 是否被回收
private final ConcurrentMap<Supplier<V>, Boolean> reverseMap = new ConcurrentHashMap<>();
// 包装传入的接口 class,生成二级缓存的 Key
private final BiFunction<K, P, ?> subKeyFactory = new KeyFactory();
// 包装 $Proxy0,生成二级缓存的 Value
private final BiFunction<K, P, V> valueFactory = new ProxyClassFactory();
WeakCache 的 get()
回到 debug,接着进入 get() 办法,看看 map 二级缓存是怎么生成 KV 的。
public V get(K key, P parameter) {Objects.requireNonNull(parameter);
// 遍历 refQueue,而后将缓存 map 中对应的生效 value 删除
expungeStaleEntries();
// 以 ClassLoader 为 key,构建 map 的一级缓存的 Key,是 CacheKey 对象
Object cacheKey = CacheK.valueOf(key, refQueue);
// 通过 Key 从 map 中获取一级缓存的 value,即 ConcurrentMap
ConcurrentMap<Object, Supplier<V>> valuesMap = map.get(cacheKey);
if (valuesMap == null) {
// 如果 Key 不存在,就新建一个 ConCurrentMap 放入 map,这里应用的是 putIfAbsent
// 如果 key 曾经存在了,就不笼罩并返回外面的 value,不存在就返回 null 并放入 Key
// 当初缓存 map 的构造就是 ConCurrentMap<CacheKey, ConCurrentMap<Object, Supplier>>
ConcurrentMap<Object, Supplier<V>> oldValuesMap = map.putIfAbsent(cacheKey, valuesMap = new ConcurrentHashMap<>());
// 如果其余线程曾经创立了这个 Key 并放入就能够复用了
if (oldValuesMap != null) {valuesMap = oldValuesMap;}
}
// 生成二级缓存的 subKey,当初缓存 map 的构造就是 ConCurrentMap<CacheKey, ConCurrentMap<Key1, Supplier>>
// 看前面的 < 生成二级缓存 Key>!!!Object subKey = Objects.requireNonNull(subKeyFactory.apply(key, parameter));
// 依据二级缓存的 subKey 获取 value
Supplier<V> supplier = valuesMap.get(subKey);
Factory factory = null;
//!!!直到实现二级缓存 Value 的构建才完结,Value 是弱援用的 $Proxy0.class!!!while (true) {
// 第一次循环:suppiler 必定是 null,因为还没有将放入二级缓存的 KV 值
// 第二次循环:这里 suppiler 不为 null 了!!!进入 if
if (supplier != null) {
// 第二次循环:真正生成代理对象,// 往后翻,看 < 生成二级缓存 Value>,外围!!!!!
// 看完前面回到这里:value 就是弱援用后的 $Proxy0.class
V value = supplier.get();
if (value != null) {
// 本办法及上局部的最初一行代码,跳转最初的 < 构建 $Proxy 对象 >
return value;
}
}
// 第一次循环:factory 必定为 null,生成二级缓存的 Value
if (factory == null) {factory = new Factory(key, parameter, subKey, valuesMap);
}
// 第一次循环:将 subKey 和 factory 作为 KV 放入二级缓存
if (supplier == null) {supplier = valuesMap.putIfAbsent(subKey, factory);
if (supplier == null) {
// 第一次循环:赋值之后 suppiler 就不为空了,记住!!!!!supplier = factory;
}
}
}
}
}
生成二级缓存 Key
在 get() 中调用 subKeyFactory.apply(key, parameter),依据你 newProxyInstance() 传入的接口 Class[] 的个数来生成二级缓存的 Key,这里咱们就传入了一个 UserInterface.class,所以就返回了 Key1 对象。
KeyFactory.apply()
不论是 Key1、Key2 还是 KeyX,他们都继承了 WeakReference,都是包装对象是 Class 的弱援用类。这里看看 Key1 的代码。
Key1
生成二级缓存 Value
在下面的 while 循环中,第一次循环只是生成了一个空的 Factory 对象放入了二级缓存的 ConcurrentMap 中。
在第二次循环中,才开始通过 get() 办法来真正的构建 value。
别回头,接着往下看。
Factory.get() 生成弱援用 value
「CacheValue」 类是一个弱援用,是二级缓存的 Value 值,包装的是 class,在这里就是 $Proxy0.class,至于这个类如何生成的,依据上面代码正文始终看完 Class 文件的生成
public synchronized V get() {
// 查看是否被回收,如果被回收,会继续执行下面的 while 循环,从新生成 Factory
Supplier<V> supplier = valuesMap.get(subKey);
if (supplier != this) {return null;}
// 这里的 V 的类型是 Class
V value = null;
// 这行是外围代码,看前面 <class 文件的生成 >,记住这里返回的是 Class
value = Objects.requireNonNull(valueFactory.apply(key, parameter));
// 将 Class 对象包装成弱援用
CacheValue<V> cacheValue = new CacheValue<>(value);
// 回到下面 <WeakCache 的 get() 办法 >V value = supplier.get();
return value;
}
}
CacheValue
Class 文件的生成
包名类名的定义与验证
进入 valueFactory.apply(key, parameter) 办法,看看 class 文件是怎么生成的。
private static final String proxyClassNamePrefix = "$Proxy";
public Class<?> apply(ClassLoader loader, Class<?>[] interfaces) {Map<Class<?>, Boolean> interfaceSet = new IdentityHashMap<>(interfaces.length);
// 遍历你传入的 Class[],咱们只传入了 UserInterface.class
for (Class<?> intf : interfaces) {
Class<?> interfaceClass = null;
// 获取接口类
interfaceClass = Class.forName(intf.getName(), false, loader);
// 这里就很明确为什么只能传入接口类,不是接口类会报错
if (!interfaceClass.isInterface()) {
throw new IllegalArgumentException(interfaceClass.getName() + "is not an interface");
}
String proxyPkg = null;
int accessFlags = Modifier.PUBLIC | Modifier.FINAL;
for (Class<?> intf : interfaces) {int flags = intf.getModifiers();
// 验证接口是否是 public,不是 public 代理类会用接口的 package,因为只有在同一包内能力继承
// 咱们的 UserInterface 是 public,所以跳过
if (!Modifier.isPublic(flags)) {
accessFlags = Modifier.FINAL;
String name = intf.getName();
int n = name.lastIndexOf('.');
String pkg = ((n == -1) ? "" : name.substring(0, n + 1));
if (proxyPkg == null) {proxyPkg = pkg;} else if (!pkg.equals(proxyPkg)) {
throw new IllegalArgumentException("non-public interfaces from different packages");
}
}
}
// 如果接口类是 public,则用默认的包
if (proxyPkg == null) {
// PROXY_PACKAGE = "com.sun.proxy";
proxyPkg = ReflectUtil.PROXY_PACKAGE + ".";
}
// 原子 Int,此时 num = 0
long num = nextUniqueNumber.getAndIncrement();
// com.sun.proxy.$Proxy0,这里包名和类名就呈现了!!!String proxyName = proxyPkg + proxyClassNamePrefix + num;
//!!!!生成 class 文件,查看前面 <class 文件写入本地 > 外围!!!!byte[] proxyClassFile = ProxyGenerator.generateProxyClass(proxyName, interfaces, accessFlags);
//!!!看完上面再回来看这行!!!!// 获取了字节数组之后,获取了 class 的二进制流将类加载到了 JVM 中
// 并且返回了 $Proxy0.class,返回给 Factory.get() 来包装
return defineClass0(loader, proxyName,proxyClassFile, 0, proxyClassFile.length);
}
}
}
defineClass0() 是 Proxy 类自定义的类加载的 native 办法,会获取 class 文件的二进制流加载到 JVM 中,以获取对应的 Class 对象,这一块能够参考 JVM 类加载器。
class 文件写入本地
generateProxyClass() 办法会将 class 二进制文件写入本地目录,并返回 class 文件的二进制流,应用你传入的类加载器加载,「这里你晓得类加载器的作用了么」。
public static byte[] generateProxyClass(final String name,
Class[] interfaces)
{ProxyGenerator gen = new ProxyGenerator(name, interfaces);
// 生成 class 文件的二进制,查看前面 < 生成 class 文件二进制 >
final byte[] classFile = gen.generateClassFile();
// 将 class 文件写入本地
if (saveGeneratedFiles) {
java.security.AccessController.doPrivileged(new java.security.PrivilegedAction<Void>() {public Void run() {
try {
FileOutputStream file =
new FileOutputStream(dotToSlash(name) + ".class");
file.write(classFile);
file.close();
return null;
} catch (IOException e) {
throw new InternalError("I/O exception saving generated file:" + e);
}
}
});
}
// 返回 $Proxy0.class 字节数组,回到下面 <class 文件生成 >
return classFile;
}
生成 class 文件二进制流
generateClassFile() 生成 class 文件,并存放到字节数组,「能够顺便学一下 class 构造,这里也体现了你传入的 class[] 的作用」。
private byte[] generateClassFile() {
// 将 hashcode、equals、toString 是三个办法放入代理类中
addProxyMethod(hashCodeMethod, Object.class);
addProxyMethod(equalsMethod, Object.class);
addProxyMethod(toStringMethod, Object.class);
for (int i = 0; i < interfaces.length; i++) {Method[] methods = interfaces[i].getMethods();
for (int j = 0; j < methods.length; j++) {// 将接口类的办法放入新建的代理类中,这里就是 sayHello()
addProxyMethod(methods[j], interfaces[i]);
}
}
for (List<ProxyMethod> sigmethods : proxyMethods.values()) {checkReturnTypes(sigmethods);
}
// 给代理类减少构造方法
methods.add(generateConstructor());
for (List<ProxyMethod> sigmethods : proxyMethods.values()) {for (ProxyMethod pm : sigmethods) {
// 将下面的四个办法都封装成 Method 类型成员变量
fields.add(new FieldInfo(pm.methodFieldName,
"Ljava/lang/reflect/Method;",
ACC_PRIVATE | ACC_STATIC));
// generate code for proxy method and add it
methods.add(pm.generateMethod());
}
}
// static 动态块结构
methods.add(generateStaticInitializer());
cp.getClass(dotToSlash(className));
cp.getClass(superclassName);
for (int i = 0; i < interfaces.length; i++) {cp.getClass(dotToSlash(interfaces[i].getName()));
}
cp.setReadOnly();
ByteArrayOutputStream bout = new ByteArrayOutputStream();
DataOutputStream dout = new DataOutputStream(bout);
// !!! 外围点来了!这里就开始构建 class 文件了,以下都是 class 的构造,只写一部分
try {
// u4 magic,class 文件的魔数,确认是否为一个能被 JVM 承受的 class
dout.writeInt(0xCAFEBABE);
// u2 minor_version,0
dout.writeShort(CLASSFILE_MINOR_VERSION);
// u2 major_version,主版本号,Java8 对应的是 52;
dout.writeShort(CLASSFILE_MAJOR_VERSION);
// 常量池
cp.write(dout);
// 其余构造,可参考 class 文件构造
dout.writeShort(ACC_PUBLIC | ACC_FINAL | ACC_SUPER);
dout.writeShort(cp.getClass(dotToSlash(className)));
dout.writeShort(cp.getClass(superclassName));
dout.writeShort(interfaces.length);
for (int i = 0; i < interfaces.length; i++) {
dout.writeShort(cp.getClass(dotToSlash(interfaces[i].getName())));
}
dout.writeShort(fields.size());
for (FieldInfo f : fields) {f.write(dout);
}
dout.writeShort(methods.size());
for (MethodInfo m : methods) {m.write(dout);
}
dout.writeShort(0);
} catch (IOException e) {throw new InternalError("unexpected I/O Exception", e);
}
// 将 class 文件字节数组返回
return bout.toByteArray();}
构建 $Proxy 对象
newProxyInstance() 上半局部通过下面层层代码调用,获取了 $Proxy0.class,接下来看下局部代码:
newInstance
cl 就是下面获取的 Proxy0.class,h 就是下面传入的 userHandler,被当做结构参数来创立 $Proxy0 对象。而后获取这个动静代理对象,调用 sayHello() 办法,相当于调用了 UserHandler 的 invoke(),「这里就是 UserHandler 的作用」!
$Proxy.class 文件
咱们开启了将代理 class 写到本地目录的性能,在我的项目下的 com/sum/proxy 目录下找到了 $Proxy0 的 class 文件。
「看一下反编译的 class」
package com.sun.proxy;
import com.test.proxy.UserInterface;
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;
import java.lang.reflect.Proxy;
import java.lang.reflect.UndeclaredThrowableException;
public final class $Proxy0 extends Proxy implements UserInterface {
private static Method m1;
private static Method m3;
private static Method m2;
private static Method m0;
public $Proxy0(InvocationHandler var1) throws {super(var1);
}
public final boolean equals(Object var1) throws {
try {return (Boolean)super.h.invoke(this, m1, new Object[]{var1});
} catch (RuntimeException | Error var3) {throw var3;} catch (Throwable var4) {throw new UndeclaredThrowableException(var4);
}
}
public final void sayHello() throws {
try {super.h.invoke(this, m3, (Object[])null);
} catch (RuntimeException | Error var2) {throw var2;} catch (Throwable var3) {throw new UndeclaredThrowableException(var3);
}
}
public final String toString() throws {
try {return (String)super.h.invoke(this, m2, (Object[])null);
} catch (RuntimeException | Error var2) {throw var2;} catch (Throwable var3) {throw new UndeclaredThrowableException(var3);
}
}
public final int hashCode() throws {
try {return (Integer)super.h.invoke(this, m0, (Object[])null);
} catch (RuntimeException | Error var2) {throw var2;} catch (Throwable var3) {throw new UndeclaredThrowableException(var3);
}
}
static {
try {m1 = Class.forName("java.lang.Object").getMethod("equals", Class.forName("java.lang.Object"));
m3 = Class.forName("com.test.proxy.UserInterface").getMethod("sayHello");
m2 = Class.forName("java.lang.Object").getMethod("toString");
m0 = Class.forName("java.lang.Object").getMethod("hashCode");
} catch (NoSuchMethodException var2) {throw new NoSuchMethodError(var2.getMessage());
} catch (ClassNotFoundException var3) {throw new NoClassDefFoundError(var3.getMessage());
}
}
}
结语
下面就是动静代理源码的调试过程,与之前的猜测的代理类的生成过程比拟,动静代理是间接生成 class 文件,省去了 java 文件和编译这一块。
刚开始看可能比拟绕,跟着正文及跳转指引,急躁多看两遍就明确了。动静代理波及的知识点比拟多,我本人看的时候,在 WeakCache 这一块纠结了一阵,其实把它当成一个两层的 map 看待即可,只不过外面所有的 KV 都被弱援用包装。
心愿看到这篇文章的每个程序员最终都能成为头发繁茂的码农;
举荐浏览
为什么阿里巴巴的程序员成长速度这么快,看完他们的内部资料我懂了
字节跳动总结的设计模式 PDF 火了,完整版凋谢下载
刷 Github 时发现了一本阿里大神的算法笔记!标星 70.5K
程序员 50W 年薪的常识体系与成长路线。
月薪在 30K 以下的 Java 程序员,可能听不懂这个我的项目;
字节跳动总结的设计模式 PDF 火了,完整版凋谢分享
对于【暴力递归算法】你所不晓得的思路
开拓鸿蒙,谁做零碎,聊聊华为微内核
=
看完三件事❤️
如果你感觉这篇内容对你还蛮有帮忙,我想邀请你帮我三个小忙:
点赞,转发,有你们的『点赞和评论』,才是我发明的能源。
关注公众号『Java 斗帝』,不定期分享原创常识。
同时能够期待后续文章 ing????