静态代理
以点外卖为例,外卖App就是商家餐厅的代理,对于餐厅来说只需要完成做外卖这一件事,而接受定单和送出外卖这两件事则是由外卖App代为实现。
首先定义一个Food接口,其中包含了一个代表点单的ordering方法:
public interface Food {
void ordering();
}
然后不同的商家都来实现Food接口,其中ordering方法仅包含做外卖这一件事:
public class KFC implements Food {
@Override
public void ordering() {
System.out.println("KFC做外卖");
}
}
public class McDonald implements Food {
@Override
public void ordering() {
System.out.println("McDonald做外卖");
}
}
public class BurgerKing implements Food {
@Override
public void ordering() {
System.out.println("BurgerKing做外卖");
}
}
如果到此为止的话,往后调用不同商家餐厅的ordering方法就必须实现不同的类的对象。但是由于odering方法是固定的,而且每个商家都有各自的ordering方法,因此想要在ordering中增加接收订单和送出外卖的功能时就必须对每个商家的ordering进行修改,这样后期维护的复杂度就大大提高。
为了解决这个问题我们就需要使用到代理模式,最简单的代理模式就是静态代理。在本例中就是通过创建一个外卖App类完善ordering这个过程,该类同样也实现了Food接口,并维护了一个Food对象而且重新实现了ordering方法,其中做外卖这件事依旧是由商家餐厅对象实现的,但接受点单和送出外卖全都由App这个代理类对象代为实现:
public class App implements Food {
private Food food;
App(Food food) {
this.food = food;
}
@Override
public void ordering() {
System.out.println("接受定单");
this.food.ordering(); // 直接调用实现类中的ordering方法
System.out.println("送出外卖");
}
}
测试一下静态代理的方式:
public class Test {
public static void main(String[] args) {
Food kfc = new KFC();
Food app = new App(kfc);
app.orderring();
}
}

上述例子是通过创建一个代理类的对象,从而实现在被代理对象的基础上对其方法进行补充,这便是静态代理。当然,除了静态代理之外还存在动态代理,不同于静态代理需要编译前手动创建代理类,动态代理则是程序运行时自动创建代理类。此外,静态代理中最大的缺点就是代理类中的代理方法中需要调用实现类中的方法,这会导致代码之间的耦合,而动态代理则无需在程序中以硬编码的方式直接调用便可以达到执行该方法的效果。
常见的动态代理分为JDK动态代理和CGLib动态代理两种方式。
JDK动态代理
JDK动态代理只能为接口创建动态代理,这意味着JDK动态代理需要实现类通过接口定义业务方法,同时创建的代理类可以隐藏具体实现类,有效地降低了具体的实现与调用之间的耦合度,而这也正是JDK动态代理和CGLib动态代理最大的区别。
使用JDK动态代理需要创建一个实现了InvocationHandler接口的动态代理器,这个动态代理器中包含一个私有目标对象以及两个公有方法,其中一个用来返回代理对象的getProxyInstance,另一个运行目标对象被代理的方法(需要传入的参数分别是代理类实例、调用的方法、相关参数)。
public class AppJdkProxy implements InvocationHandler {
//目标对象
private Object target;
public AppJdkProxy(Object target) {
super();
this.target = target;
}
//返回目标对象的代理对象
public Object getProxyInstance() {
return Proxy.newProxyInstance(target.getClass().getClassLoader(),
target.getClass().getInterfaces(), this);
}
@Override
//运行目标对象的方法
public Object invoke(Object proxy, Method method, Object[] args)
throws Throwable {
System.out.println("接受订单");
Object result = method.invoke(target, args);
System.out.println("送出外卖");
return result;
}
}
在测试类中只需要创建一个Food的实现类对象并传入动态代理器的构造函数中,再调用动态代理器的getProxyInstance方法获得代理对象,便可以调用被代理的业务方法。
public class Test {
public static void main(String[] args) {
Food food = new KFC();
Food proxy = (Food) new AppJdkProxy(food).getProxyInstance();
proxy.ordering();
}
}

那么getProxyInstance方法是如何返回一个代理类实例的?实际上它调用了newProxyInstance方法,该方法参数中的loader是目标对象使用的类加载器,interfaces是目标对象实现的接口类型,h是实现了InvocationHandler接口的动态处理器。
public static Object newProxyInstance(ClassLoader loader,
Class<?>[] interfaces,
InvocationHandler h)
throws IllegalArgumentException
{
Objects.requireNonNull(h);
//对传入的接口类型进行安全检查
final Class<?>[] intfs = interfaces.clone();
final SecurityManager sm = System.getSecurityManager();
if (sm != null) {
checkProxyAccess(Reflection.getCallerClass(), loader, intfs);
}
// 查找或生成指定的代理类
Class<?> cl = getProxyClass0(loader, intfs);
// 根据生成的Class对象通过反射的方式获取构造函数,生成代理类实例
try {
if (sm != null) {
checkNewProxyPermission(Reflection.getCallerClass(), cl);
}
final Constructor<?> cons = cl.getConstructor(constructorParams);
final InvocationHandler ih = h;
if (!Modifier.isPublic(cl.getModifiers())) {
AccessController.doPrivileged(new PrivilegedAction<Void>() {
public Void run() {
cons.setAccessible(true);
return null;
}
});
}
//将动态处理器h传给代理类的构造方法,生成并返回代理类的实例
return cons.newInstance(new Object[]{h});
} catch (IllegalAccessException|InstantiationException e) {
throw new InternalError(e.toString(), e);
} catch (InvocationTargetException e) {
Throwable t = e.getCause();
if (t instanceof RuntimeException) {
throw (RuntimeException) t;
} else {
throw new InternalError(t.toString(), t);
}
} catch (NoSuchMethodException e) {
throw new InternalError(e.toString(), e);
}
}
从以上代码中可以看到,newProxyInstance方法主要的功能就是通过传入的类加载器和接口类型,调用getProxyClass0方法获得指定的代理类,并通过反射的方式调用其构造函数生成代理类实例。
再来看一下getProxyClass0方法的源代码,其中proxyClassCache是代理类的缓存变量,调用它的get方法时,如果该缓存中存在指定的代理类,则由返回缓存副本,否则将创建该代理类。
private static Class<?> getProxyClass0(ClassLoader loader,
Class<?>... interfaces) {
if (interfaces.length > 65535) {
throw new IllegalArgumentException("interface limit exceeded");
}
return proxyClassCache.get(loader, interfaces);
}
那么proxyClassCache是怎么通过get获得代理类的呢?首先了解一下proxyClassCache的定义。
private static final WeakCache<ClassLoader, Class<?>[], Class<?>>
proxyClassCache = new WeakCache<>(new KeyFactory(), new ProxyClassFactory());
很明显,proxyClassCache是一个WeakCache对象,因此再来解一下WeakCache类的属性。
// 引用队列,配合垃圾回收时使用
private final ReferenceQueue<K> refQueue = new ReferenceQueue<>();
// 缓存的底层实现,key为一级缓存,value为二级缓存;由于key的类型为Object,因此key可以为null
private final ConcurrentMap<Object, ConcurrentMap<Object, Supplier<V>>> map = new ConcurrentHashMap<>();
// 记录了缓存中所有的CacheValue,实现了缓存的过期机制
private final ConcurrentMap<Supplier<V>, Boolean> reverseMap = new ConcurrentHashMap<>();
// 生成二级缓存中的key
private final BiFunction<K, P, ?> subKeyFactory;
// 生成二级缓存中的value
private final BiFunction<K, P, V> valueFactory;
// 构造函数中传入subKeyFactory和valueFactory
public WeakCache(BiFunction<K, P, ?> subKeyFactory,
BiFunction<K, P, V> valueFactory) {
this.subKeyFactory = Objects.requireNonNull(subKeyFactory);
this.valueFactory = Objects.requireNonNull(valueFactory);
}
可以看出,WeakCache的缓存机制底层是靠ConcurrentHashMap实现的(使用ConcurrentHashMap的目的是为了线程安全),其中一个对象map的作用是实现两级缓存机制,另一个对象reverseMap的作用是实现缓存的过期机制。对象subKeyFactory的作用是产生二级缓存中的key,对象valueFactory的作用是产生二级缓存中的值。而proxyClassCache作为一个WeakCache对象,其中对应subKeyFactory和valueFactory的分别是Proxy类中的KeyFactory对象和ProxyClassFactory对象。
接着再回到proxyClassCache调用get方法是如何返回代理类这个问题上。看一下get方法的代码,其中Factory是Supplier接口的一个实现类,而二级缓存的值实际上是一个Factory实例。此外,在之前的代码中我们知道getProxyClass0方法中proxyClassCache调用get方法时,传入的参数分别是类加载器和实现的接口类型。
public V get(K key, P parameter) {
// 实现的接口不能为空
Objects.requireNonNull(parameter);
// 清除过期缓存
expungeStaleEntries();
// 将类加载器作为一级缓存的key
Object cacheKey = CacheKey.valueOf(key, refQueue);
// 通过一级缓存的key获得的值即为二级缓存
ConcurrentMap<Object, Supplier<V>> valuesMap = map.get(cacheKey);
// 如果二级缓存不存在
if (valuesMap == null) {
// 则创建二级缓存
ConcurrentMap<Object, Supplier<V>> oldValuesMap
= map.putIfAbsent(cacheKey,
valuesMap = new ConcurrentHashMap<>());
// 如果oldValuesMap有值则不改变原值
if (oldValuesMap != null) {
valuesMap = oldValuesMap;
}
}
// 根据类加载器和代理类实现的接口生成二级缓存的key
Object subKey = Objects.requireNonNull(subKeyFactory.apply(key, parameter));
// 通过二级缓存的key获得二级缓存的值
Supplier<V> supplier = valuesMap.get(subKey);
Factory factory = null;
// 通过轮询机制
while (true) {
// 当二级缓存的值不为空时则返回
if (supplier != null) {
V value = supplier.get();
if (value != null) {
return value;
}
}
// 如果factory为空则创建一个新的Factory实例
if (factory == null) {
factory = new Factory(key, parameter, subKey, valuesMap);
}
// 当二级缓存的值为空时
if (supplier == null) {
// 将factory存入二级缓存
supplier = valuesMap.putIfAbsent(subKey, factory);
// 双重检测保证factory放入二级缓存
if (supplier == null) {
supplier = factory;
}
} else {
// 如果其他线程也修改了二级缓存的值时,则替换原值
if (valuesMap.replace(subKey, supplier, factory)) {
// 将factory替换成新值
supplier = factory;
} else {
// 替换失败则获取原值
supplier = valuesMap.get(subKey);
}
}
}
}
由此可知,WeakCache的get方法,先根据类加载器获得一级缓存的key,再由这个key获得二级缓存。接着根据类加载器和实现的接口类型获得二级缓存的key,以轮询的方式获取二级缓存中存放的值(即Factory实例),若该缓存值不为空则调用get方法返回代理类,否则创建一个Factory实例后存入二级缓存并重新轮询。其中二级缓存的key分为key0,key1,key2,keyX,这是由于被代理的类实现的接口可能不止一个,因此用不同的key表示实现的接口数量。
那么Factory的get方法是如何获得代理类的呢?首先来看看Factory的代码。
private final class Factory implements Supplier<V> {
// 一级缓存的key
private final K key;
// 代理类实现的接口数组
private final P parameter;
// 二级缓存的key
private final Object subKey;
// 二级缓存
private final ConcurrentMap<Object, Supplier<V>> valuesMap;
Factory(K key, P parameter, Object subKey,
ConcurrentMap<Object, Supplier<V>> valuesMap) {
this.key = key;
this.parameter = parameter;
this.subKey = subKey;
this.valuesMap = valuesMap;
}
@Override
public synchronized V get() {
// 通过二级缓存的key获得二级缓存的value
Supplier<V> supplier = valuesMap.get(subKey);
// 如果取出的二级缓存的supplier不是Factory对象
if (supplier != this) {
// 当supplier替换为CacheValue或由于生成代理类失败而从二级缓存中移除时
// 返回null从而让WeakCache轮询并重新调用get
return null;
}
V value = null;
try {
// 通过valueFactory来生成代理类
value = Objects.requireNonNull(valueFactory.apply(key, parameter));
} finally {
// 当代理类生成失败时,删除这个二级缓存
if (value == null) {
valuesMap.remove(subKey, this);
}
}
// 当value的值不为null时才能到达这里
assert value != null;
// 使用弱引用将生成的代理类包装成cacheValue
CacheValue<V> cacheValue = new CacheValue<>(value);
// 在reverseMap中记录这个cacheValue,用于以后的过期机制
reverseMap.put(cacheValue, Boolean.TRUE);
// 将cacheValue放入二级缓存中,若失败则报错
if (!valuesMap.replace(subKey, this, cacheValue)) {
throw new AssertionError("Should not reach here");
}
// 返回没有被弱引用包装过的代理类
return value;
}
}
很明显,Factory的get方法主要通过调用valueFactory的apply方法来创建代理类。前文提到过,proxyClassCache作为一个WeakCache对象,其valueFactory对应着ProxyClassFactory实例。此外,get方法使用了synchronized关键字进行同步处理,用来验证二级缓存中的supplier是否是工厂本身,如果是就让valueFactory创建代理类并使用弱引用进行包装后放入reverseMap中,否则就返回null并让WeakCache的get方法重新轮询。
接着来看一下ProxyClassFactory类的代码,这个工厂类是Proxy中的一个内部类:
private static final class ProxyClassFactory
implements BiFunction<ClassLoader, Class<?>[], Class<?>>
{
// 所有代理类的名称前缀都是$Proxy
private static final String proxyClassNamePrefix = "$Proxy";
// 下一位数字用来确定唯一代理类名称
private static final AtomicLong nextUniqueNumber = new AtomicLong();
@Override
public Class<?> apply(ClassLoader loader, Class<?>[] interfaces) {
Map<Class<?>, Boolean> interfaceSet = new IdentityHashMap<>(interfaces.length);
for (Class<?> intf : interfaces) {
// 通过遍历interfaces数组来判断intf是否可以由指定的类加载器所加载、
// intf是否是一个接口以及intf在数组中是否有重复
Class<?> interfaceClass = null;
try {
interfaceClass = Class.forName(intf.getName(), false, loader);
} catch (ClassNotFoundException e) {
}
if (interfaceClass != intf) {
throw new IllegalArgumentException(
intf + " is not visible from class loader");
}
if (!interfaceClass.isInterface()) {
throw new IllegalArgumentException(
interfaceClass.getName() + " is not an interface");
}
if (interfaceSet.put(interfaceClass, Boolean.TRUE) != null) {
throw new IllegalArgumentException(
"repeated interface: " + interfaceClass.getName());
}
}
//代理类包名
String proxyPkg = null;
//代理类访问标志,默认为public final
int accessFlags = Modifier.PUBLIC | Modifier.FINAL;
//记录非公共代理接口的包,以便在同一个包中定义代理类。验证所有非公共代理接口都在同一个包中
for (Class<?> intf : interfaces) {
//获取接口的访问标志
int flags = intf.getModifiers();
//如果接口的访问标志不是public
if (!Modifier.isPublic(flags)) {
//将代理类的访问标志设置为final
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; //如果代理类包名为null,则将代理类包名设置为和接口包名相同
} else if (!pkg.equals(proxyPkg)) { //如果代理类包名不为null,且和接口包名不同,则抛出异常
throw new IllegalArgumentException(
"non-public interfaces from different packages");
}
}
}
//如果接口访问标志是public,且没有非公共代理接口时,生成的代理类都默认放在com.sum.proxy包下
if (proxyPkg == null) {
// if no non-public proxy interfaces, use com.sun.proxy package
proxyPkg = ReflectUtil.PROXY_PACKAGE + ".";
}
// 给生成的代理类命名
long num = nextUniqueNumber.getAndIncrement();
String proxyName = proxyPkg + proxyClassNamePrefix + num;
//调用ProxyGenerator的generateProxyClass方法来生成代理类的二进制字节
byte[] proxyClassFile = ProxyGenerator.generateProxyClass(
proxyName, interfaces, accessFlags);
try {
// 根据二进制文件生成Class实例
return defineClass0(loader, proxyName,
proxyClassFile, 0, proxyClassFile.length);
} catch (ClassFormatError e) {
throw new IllegalArgumentException(e.toString());
}
}
}
ProxyClassFactory的核心是apply方法,该方法主要功能是通过传入的类加载器和实现的接口类型,调用ProxyGenerator类的generateProxyClass方法生成代理类的Class实例,此外apply方法还规范了代理类的类名、访问标志和位置等信息。
由于sun.misc包下的ProxyGenerator类不能直接访问,因此通过OpenJDK来查看源代码,首先其中的generateProxyClass方法:
public static byte[] generateProxyClass(final String name,
Class<?>[] interfaces,
int accessFlags)
{
ProxyGenerator gen = new ProxyGenerator(name, interfaces, accessFlags);
// 调用generateClassFile方法
final byte[] classFile = gen.generateClassFile();
if (saveGeneratedFiles) {
java.security.AccessController.doPrivileged(
new java.security.PrivilegedAction<Void>() {
public Void run() {
// 获得代理类的路径,并将生成的class文件写入该路径
try {
int i = name.lastIndexOf('.');
Path path;
if (i > 0) {
Path dir = Paths.get(name.substring(0, i).replace('.', File.separatorChar));
Files.createDirectories(dir);
path = dir.resolve(name.substring(i+1, name.length()) + ".class");
} else {
path = Paths.get(name + ".class");
}
Files.write(path, classFile);
return null;
} catch (IOException e) {
throw new InternalError(
"I/O exception saving generated file: " + e);
}
}
});
}
return classFile;
}
再看一下generateProxyClass方法中调用的generateClassFile方法,该方法的主要功能是按照一定的结构生成代理类的class文件:
private byte[] generateClassFile() {
/*
* 第一步:将所有方法组装成ProxyMethod对象
*/
// 为代理类生成hashCode、equals、toString方法
addProxyMethod(hashCodeMethod, Object.class);
addProxyMethod(equalsMethod, Object.class);
addProxyMethod(toStringMethod, Object.class);
// 遍历每一个接口中的每一个方法, 并且为其生成ProxyMethod对象
for (Class<?> intf : interfaces) {
for (Method m : intf.getMethods()) {
addProxyMethod(m, intf);
}
}
// 对于具有相同签名的代理方法, 检验方法的返回值是否兼容
for (List<ProxyMethod> sigmethods : proxyMethods.values()) {
checkReturnTypes(sigmethods);
}
/*
* 第二步:为生成的class文件组装字段和方法信息
*/
try {
// 添加构造器
methods.add(generateConstructor());
// 遍历缓存中的代理方法
for (List<ProxyMethod> sigmethods : proxyMethods.values()) {
for (ProxyMethod pm : sigmethods) {
// 添加代理类的静态字段
fields.add(new FieldInfo(pm.methodFieldName,
"Ljava/lang/reflect/Method;",
ACC_PRIVATE | ACC_STATIC));
// 添加代理类的代理方法
methods.add(pm.generateMethod());
}
}
// //添加代理类的静态字段初始化方法
methods.add(generateStaticInitializer());
} catch (IOException e) {
throw new InternalError("unexpected I/O Exception", e);
}
if (methods.size() > 65535) {
throw new IllegalArgumentException("method limit exceeded");
}
if (fields.size() > 65535) {
throw new IllegalArgumentException("field limit exceeded");
}
/*
* 第三步:写入最终的class文件
*/
// 验证常量池中存在代理类的全限定名
cp.getClass(dotToSlash(className));
// 验证常量池中存在代理类父类的全限定名
cp.getClass(superclassName);
// //验证常量池存在代理类接口的全限定名
for (Class<?> intf: interfaces) {
cp.getClass(dotToSlash(intf.getName()));
}
// 开始写入文件,设置常量池只读
cp.setReadOnly();
ByteArrayOutputStream bout = new ByteArrayOutputStream();
DataOutputStream dout = new DataOutputStream(bout);
// 以下为具体写入的内容
try {
dout.writeInt(0xCAFEBABE);
dout.writeShort(CLASSFILE_MINOR_VERSION);
dout.writeShort(CLASSFILE_MAJOR_VERSION);
cp.write(dout);
dout.writeShort(accessFlags);
dout.writeShort(cp.getClass(dotToSlash(className)));
dout.writeShort(cp.getClass(superclassName));
dout.writeShort(interfaces.length);
for (Class<?> intf : interfaces) {
dout.writeShort(cp.getClass(
dotToSlash(intf.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);
}
// 转换为二进制数组返回
return bout.toByteArray();
}
可以看到,generateClassFile方法生成class文件的具体过程为:首先收集所有要生成的代理方法,将其包装成ProxyMethod对象并添加到一个集合中,接着收集所有组成class文件的字段和方法信息,最后生成class文件。
通过反编译工具查看一下生成的代理类。很明显,代理对象的代理方法实际上调用了动态代理器中的invoke方法,从而实现代理逻辑。
public final class $Proxy0 extends Proxy implements Food {
private static Method m1;
private static Method m3;
private static Method m2;
private static Method m0;
// 构造方法传入了InvocationHandler实例
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})).booleanValue();
} catch (RuntimeException | Error var3) {
throw var3;
} catch (Throwable var4) {
throw new UndeclaredThrowableException(var4);
}
}
public final void ordering() 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)).intValue();
} 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("Food").getMethod("ordering");
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());
}
}
}
至此,我们了解完了整个JDK动态代理的过程。
再回来谈谈JDK动态代理的特点。前面提到过,JDK动态代理只能为接口创建动态代理,这是因为创建出来的代理类继承了Proxy类,其构造函数中传入的参数正是使用JDK动态代理时需要创建的InvocationHandler实例。为了让代理类通过反射机制获取被代理的方法,因此代理类必须继承或实现包含这个方法的类或接口,但是由于java中不允许继承多个类,因此只能通过实现接口的方式来获得该方法。
但是在这还是有一个疑问的,那就是JDK动态代理只能为接口创建动态代理的前提是因为生成的代理类已经继承了Proxy类,若还想要通过反射获取被代理的方法就只剩下实现接口这一种方式。但是问题在于生成的代理类为什么一定要继承Proxy类呢,有人肯定会说是因为要继承Proxy类中参数为InvocationHandler h的构造函数,但是这个Proxy中的构造函数实际上代码仅仅是this.h = h而已,完全没有必要通过继承Proxy来实现这个功能,而是直接在代理类中创建这个方法就可以了,但正是由于继承了Proxy类导致出现了JDK动态代理只能为接口实现代理这一局限性。
参考文章链接:
1、https://www.jianshu.com/p/e9aa4d5952fe
2、https://blog.csdn.net/lovejj1994/article/details/78080124
3、https://rejoy.iteye.com/blog/1627405
4、https://mp.weixin.qq.com/s?__biz=MjM5MjAwODM4MA==&mid=2650711062&idx=2&sn=941ad450538f1590bf21a446e6b818e9&chksm=bea6d5c589d15cd3b2b1fde45f8172062074699bfaab39baadf2584591e3026bfc279d61bce8&mpshare=1&scene=1&srcid=1224QlTZcPGlddTI9lCVGsBH#rd
5、https://www.cnblogs.com/liuyun1995/
6、https://blog.csdn.net/zxysshgood/article/details/78684229
7、https://www.cnblogs.com/V1haoge/p/5860749.html
8、https://www.jianshu.com/u/2946c4d3899d
网友评论