简述: 从这篇文章起,我们将继续Kotlin邂逅设计模式系列篇中的第二篇代理模式。代理模式可以说很多初级中级开发者迷惑的设计模式。但是它确实应用很广,不用多说大家非常熟悉的Retrofit框架,内部使用了动态代理设计模式,以注解的方式简化网络请求参数传递,从而实现更高解耦。然而在Kotlin中有天然支持的属性代理语法特性,可以简化Java中代理模式实现的模板代理。

一、介绍

代理模式(Proxy Pattern),又称委托模式,顾名思义就是一个对象的实现委托给另一个代理对象来实现供外部调用。

二、定义

为其他对象提供一种代理方式来控制对某个对象的访问,从而更好地保证了该对象对外使用的透明性。

三、基本要求

  • 1、委托对象(或者被代理对象)与代理对象需要实现相同的接口。
  • 2、代理对象中保有实际的委托对象引用,外部调用的操作或行为都是代理对象在内部交于实际的委托对象去实现。
  • 3、为了内部隐藏性,外部调用者直接和两者共同的接口通信。

三、使用场景

当无法或不想直接访问某个对象或访问某个对象存在困难时可以通过一个代理对象来间接访问。代理可以实现方法增强,比如常用的日志,缓存等;也可以实现方法拦截,通过代理方法修改原方法的参数和返回值

四、UML类图

代理模式在生活中非常常见,由于最近身边同事都在讨论买房,这里就以买房中介为例来介绍我们今天的代理模式。首先我们需要使用UML类图直观地表示出代理模式思想。

由上面的UML的类图可知,主要涉及到四种角色:

  • 1、Client: 客户类,可以看做代理模式调用的外部者
  • 2、IPurchaseHouse: 抽象买房接口,该接口主要职责是声明HouseOwner(实际房子拥有者)与HouseAgent(房产中介)的共同接口方法,该类可以是一个接口或抽象类
  • 3、HouseOwner: 房子拥有者(房东),也就是代理模式中实际委托对象或被代理对象,外部调用者Client类就是通过代理对象(中介)间接调用实际的委托对象中定义的方法
  • 4、HouseAgent: 房产中介,也就是代理模式中的代理对象,该类持有一个真实HouseOwner引用,在代理类中接口方法中调用HouseOwner方法以此来达到代理作用。

五、静态代理

1、Java实现静态代理

在Java中实现静态代理还是比较简单,只要按照上述UML中分析角色规则来定义就能轻松实现。这里就用Java先去实现上述例子:

//IPurchaseHouse: 抽象买房接口
interface IPurchaseHouse {void inquiryPrice();//询价void visitHouse();//看房void payDeposit();//付定金void signAgreement();//签合同void payMoney();//付钱void getHouse();//拿房
}//HouseOwner: 房子拥有者(房东)
class HouseOwner implements IPurchaseHouse {//实现IPurchaseHouse共同接口@Overridepublic void inquiryPrice() {System.out.println("HouseOwner提出房子价格: 200W RMB");}@Overridepublic void visitHouse() {System.out.println("HouseOwner同意买房者来看房子");}@Overridepublic void payDeposit() {System.out.println("HouseOwner收了买房者1W RMB定金");}@Overridepublic void signAgreement() {System.out.println("HouseOwner与买房者签订合同");}@Overridepublic void payMoney() {System.out.println("买房者付钱给HouseOwner");}@Overridepublic void getHouse() {System.out.println("买房者拿到房子");}
}//HouseAgent: 房产中介
class HouseAgent implements IPurchaseHouse {private IPurchaseHouse mHouseOwner;//具体房东HouseOwner被代理对象引用public HouseAgent(IPurchaseHouse houseOwner) {mHouseOwner = houseOwner;}@Overridepublic void inquiryPrice() {mHouseOwner.inquiryPrice();//通过具体房东HouseOwner引用去调用inquiryPrice}@Overridepublic void visitHouse() {mHouseOwner.visitHouse();//通过具体房东HouseOwner引用去调用visitHouse}@Overridepublic void payDeposit() {mHouseOwner.payDeposit();//通过具体房东HouseOwner引用去调用payDeposit}@Overridepublic void signAgreement() {mHouseOwner.signAgreement();//通过具体房东HouseOwner引用去调用signAgreement}@Overridepublic void payMoney() {mHouseOwner.payMoney();//通过具体房东HouseOwner引用去调用payMoney}@Overridepublic void getHouse() {mHouseOwner.getHouse();//通过具体房东HouseOwner引用去调用getHouse}
}//Client客户类
class Client {public static void main(String[] args) {IPurchaseHouse houseOwner = new HouseOwner();IPurchaseHouse houseAgent = new HouseAgent(houseOwner);//传入具体被代理类实例houseAgent.inquiryPrice();//询问价格houseAgent.visitHouse();//看房houseAgent.payDeposit();//支付定金houseAgent.signAgreement();//签合同houseAgent.payMoney();//付钱houseAgent.getHouse();//拿房}
}

运行结果:

HouseOwner提出房子价格: 200W RMB
HouseOwner同意买房者来看房子
HouseOwner收了买房者1W RMB定金
HouseOwner与买房者签订合同
买房者付钱给HouseOwner
买房者拿到房子Process finished with exit code 0

这就是静态代理具体的实现,可能有些并不能看到代理模式所带来的好处,看上去就像是代理类做了实际转发调用而已。实际上有个很明显优点就是: 可以在HouseAgent类中整个流程插入一些特有的操作或行为,而不会影响内部HouseOwner的实现,保护内部的实现。 还有一个优点就是代理类在保证HouseOwner核心功能同时可以扩展其他行为

上述结论可能有点抽象,假如现在有个不一样需求比如A房产中介,在看房之前首先得签订一个看房协议,但是这个协议只涉及购买用户与中介之间的协议。所以基于代理模式很轻松就实现。

//修改后的HouseAgentA
class HouseAgentA implements IPurchaseHouse {private IPurchaseHouse mHouseOwner;//具体房东HouseOwner被代理对象引用private boolean mIsSigned;public HouseAgentA(IPurchaseHouse houseOwner) {mHouseOwner = houseOwner;}@Overridepublic void inquiryPrice() {mHouseOwner.inquiryPrice();//通过具体房东HouseOwner引用去调用inquiryPrice}@Overridepublic void visitHouse() {if (mIsSigned) {System.out.println("您已经签订了看房协议,可以看房了");mHouseOwner.visitHouse();//通过具体房东HouseOwner引用去调用visitHouse} else {System.out.println("很抱歉,您还没签订了看房协议,暂时不能看房");}}public void signVisitHouseAgreement(boolean isSigned) {mIsSigned = isSigned;}@Overridepublic void payDeposit() {mHouseOwner.payDeposit();//通过具体房东HouseOwner引用去调用payDeposit}@Overridepublic void signAgreement() {mHouseOwner.signAgreement();//通过具体房东HouseOwner引用去调用signAgreement}@Overridepublic void payMoney() {mHouseOwner.payMoney();//通过具体房东HouseOwner引用去调用payMoney}@Overridepublic void getHouse() {mHouseOwner.getHouse();//通过具体房东HouseOwner引用去调用getHouse}
}
//Client客户类
class Client {public static void main(String[] args) {IPurchaseHouse houseOwner = new HouseOwner();IPurchaseHouse houseAgent = new HouseAgentA(houseOwner);//传入具体被代理类实例houseAgent.inquiryPrice();//询问价格((HouseAgentA) houseAgent).signVisitHouseAgreement(true);//签订看房合同houseAgent.visitHouse();//看房houseAgent.payDeposit();//支付定金houseAgent.signAgreement();//签合同houseAgent.payMoney();//付钱houseAgent.getHouse();//拿房}
}

运行结果:

HouseOwner提出房子价格: 200W RMB
您已经签订了看房协议,可以看房了
HouseOwner同意买房者来看房子
HouseOwner收了买房者1W RMB定金
HouseOwner与买房者签订合同
买房者付钱给HouseOwner
买房者拿到房子Process finished with exit code 0

2、Kotlin实现静态代理

看到了Java中的HouseAgent和HouseAgent中代理类中实现转发委托是不是有点无脑啊,有点机械,就像是在写Java中的setter和getter方法一样,太多的样板代码。这时候把它叫给Kotlin吧,它会让你的代理类看起来更加简洁和优雅,因为在Kotlin中实现代理模式有着天然优势,熟悉Kotlin的小伙伴们都知道,在Kotlin中有代理独有语法特性,通过它就能轻松实现代理模式。

//IPurchaseHouseKt: 抽象买房接口
interface IPurchaseHouseKt {fun inquiryPrice() //询价fun visitHouse() //看房fun payDeposit() //付定金fun signAgreement() //签合同fun payMoney() //付钱fun getHouse() //拿房
}
//HouseOwnerKt: 房子拥有者(房东)
class HouseOwnerKt : IPurchaseHouseKt {override fun inquiryPrice() {println("HouseOwner提出房子价格: 200W RMB")}override fun visitHouse() {println("HouseOwner同意买房者来看房子")}override fun payDeposit() {println("HouseOwner收了买房者1W RMB定金")}override fun signAgreement() {println("HouseOwner与买房者签订合同")}override fun payMoney() {println("买房者付钱给HouseOwner")}override fun getHouse() {println("买房者拿到房子")}
}
//HouseAgentKt: 房产中介. 注意了,重点来了,Kotlin只需要简单一行就替代了Java代理类所有样板代码
class HouseAgentKt(houseOwnerKt: IPurchaseHouseKt) : IPurchaseHouseKt by houseOwnerKt//通过by关键字实现代理,省略大量的代理类中的样板代码,这一点需要get
//Client调用处
fun main(args: Array<String>) {val houseOwnerKt = HouseOwnerKt()HouseAgentKt(houseOwnerKt).run {inquiryPrice()//询问价格visitHouse()//看房payDeposit()//支付定金signAgreement()//签合同payMoney()//付钱getHouse()//拿房}
}

运行结果:

HouseOwner提出房子价格: 200W RMB
HouseOwner同意买房者来看房子
HouseOwner收了买房者1W RMB定金
HouseOwner与买房者签订合同
买房者付钱给HouseOwner
买房者拿到房子Process finished with exit code 0

可能有的小伙伴就会问了,你使用by关键字一下把所有的方法都给代理了,可是需要像上面新加的需求一样,需要在某个方法调用时插入一段逻辑。这个也非常方便,只需要重写需要改变的那个方法即可。一起来瞅瞅:

//修改后的HouseAgentAKt
class HouseAgentAKt(houseOwnerAKt: IPurchaseHouseKt) : IPurchaseHouseKt by houseOwnerAKt {private val mHouseOwnerAKt = houseOwnerAKtvar mIsSigned: Boolean = falseoverride fun visitHouse() {//只需要重写visitHouse即可if (mIsSigned) {println("您已经签订了看房协议,可以看房了")mHouseOwnerAKt.visitHouse()} else {println("很抱歉,您还没签订了看房协议,暂时不能看房")}}
}
//Client调用处
fun main(args: Array<String>) {val houseOwnerKt = HouseOwnerKt()HouseAgentAKt(houseOwnerKt).run {mIsSigned = trueinquiryPrice()visitHouse()payDeposit()signAgreement()payMoney()getHouse()}
}

运行结果:

HouseOwner提出房子价格: 200W RMB
您已经签订了看房协议,可以看房了
HouseOwner同意买房者来看房子
HouseOwner收了买房者1W RMB定金
HouseOwner与买房者签订合同
买房者付钱给HouseOwner
买房者拿到房子Process finished with exit code 0

3、揭开Kotlin中使用by代理语法糖衣

可能就会有小伙伴问了,在Kotlin中一个by关键字底层到底做了什么,为什么能减少代理类中样板代码。

实际上,在Kotlin中代理类HouseAgentKt的超类型IPurchaseHouseKt后面的by houseOwnerKt 表示houseOwnerKt将会在HouseAgentKt中内部存储,并且编译器将自动生成委托给houseOwnerKt的所有IPurchaseHouseKt接口方法

我们可以一起来看下反编译后的代码,验证我们的结论:

public final class HouseAgentKt implements IPurchaseHouseKt {// $FF: synthetic fieldprivate final IPurchaseHouseKt $$delegate_0;//houseOwnerKt的内部存储$$delegate_0public HouseAgentKt(@NotNull IPurchaseHouseKt houseOwnerKt) {Intrinsics.checkParameterIsNotNull(houseOwnerKt, "houseOwnerKt");super();this.$$delegate_0 = houseOwnerKt;}public void getHouse() {this.$$delegate_0.getHouse();//委托给$$delegate_0(也即是传入的houseOwnerKt)getHouse方法}public void inquiryPrice() {this.$$delegate_0.inquiryPrice();//委托给$$delegate_0(也即是传入的houseOwnerKt)inquiryPrice方法}public void payDeposit() {this.$$delegate_0.payDeposit();//委托给$$delegate_0(也即是传入的houseOwnerKt)payDeposit方法}public void payMoney() {this.$$delegate_0.payMoney();//委托给$$delegate_0(也即是传入的houseOwnerKt)payMoney方法}public void signAgreement() {this.$$delegate_0.signAgreement();//委托给$$delegate_0(也即是传入的houseOwnerKt)signAgreement方法}public void visitHouse() {this.$$delegate_0.visitHouse();//委托给$$delegate_0(也即是传入的houseOwnerKt)visitHouse方法}
}

六、动态代理

现在我们需求又增加了,现在需要增加多个代理中介,可能有很多小伙伴就说再去按照规则增加几个代理类就可以了。尽管Kotlin能解决Java中需要编写很多样板代码的问题,但是始终还是静态的。所谓静态就是代理者类是需要开发者自己手动编写,在代码运行前代理类的class编译文件就已经存在。甚至,可能不是编译前就能决定的代理类的个数,而是在运行时确定增加代理中介的个数,面对这样场景,静态代理可能就无能为力,那么就引出下面的动态代理

1、Java实现动态代理

在Java中给我们提供了一个非常方便的动态代理接口InvocationHandler,只要实现这个接口然后重写它的抽象方法invoke()

//DynamicProxy类
class DynamicProxy implements InvocationHandler {private Object object;//被代理类的引用DynamicProxy(Object object) {//传入被代理类的实例引用this.object = object;}@Overridepublic Object invoke(Object proxy, Method method, Object[] args) throws Throwable {return method.invoke(object, args);}
}
//Client类
class Client {public static void main(String[] args) {IPurchaseHouse houseOwner = new HouseOwner();DynamicProxy dynamicProxy = new DynamicProxy(houseOwner);//Proxy.newProxyInstance方法动态构造一个代理中介,需要传入被代理类的ClassLoader、共同接口集合和dynamicProxy实例对象IPurchaseHouse agentA = (IPurchaseHouse) Proxy.newProxyInstance(houseOwner.getClass().getClassLoader(), new Class[]{IPurchaseHouse.class}, dynamicProxy);agentA.inquiryPrice();agentA.visitHouse();agentA.payDeposit();agentA.signAgreement();agentA.payMoney();agentA.getHouse();}
}

运行结果:

HouseOwner提出房子价格: 200W RMB
HouseOwner同意买房者来看房子
HouseOwner收了买房者1W RMB定金
HouseOwner与买房者签订合同
买房者付钱给HouseOwner
买房者拿到房子Process finished with exit code 0

2、Kotlin实现动态代理

实际上Java中的动态代理实现已经非常精简了,所以在Kotlin在动态代理实现并没有特别不一样的,它和Java的实现没有不同。所以这里就不再重复实现,只是换了Kotlin语言实现没有什么不一样的。

七、动态代理原理解析

1、原理结论阐述

动态代理与静态代理不同点在于,它可以动态生成任意个代理对象,无需要开发者手动编写代理类代码。动态代理机制在运行时动态生成代理类字节码byte数组,然后通过jvm内部将字节码byte数组反序列化对应代理的Class对象,然后再通过反射机制创建代理类的实例

2、源码分析论证

  • 1、第一步我们先从Proxy.newProxyInstance方法进入探究,通过它在外部更为直观是可以获取代理类对象。
class Client {public static void main(String[] args) {IPurchaseHouse houseOwner = new HouseOwner();DynamicProxy dynamicProxy = new DynamicProxy(houseOwner);//第一步: 从Proxy.newProxyInstance方法入手IPurchaseHouse agentA = (IPurchaseHouse) Proxy.newProxyInstance(houseOwner.getClass().getClassLoader(),new Class[]{IPurchaseHouse.class},dynamicProxy);agentA.inquiryPrice();agentA.visitHouse();agentA.payDeposit();agentA.signAgreement();agentA.payMoney();agentA.getHouse();}
}
  • 2、第二步进入Proxy.newProxyInstance方法的定义

Proxy.newProxyInstance有三个参数:

loader(ClassLoader): 这个参数是实际被代理类的类加载器实例。

interfaces(Class<?>[]): 代理类和被代理类共同实现的接口的Class数组

h(InvocationHandler): 代理拦截器接口,一般需要使用子类去实现该接口或匿名类去实现

    public static Object newProxyInstance(ClassLoader loader,Class<?>[] interfaces,InvocationHandler h)throws IllegalArgumentException{Objects.requireNonNull(h);final Class<?>[] intfs = interfaces.clone();//将interfaces的Class数组clone一份副本,赋值给intfsfinal SecurityManager sm = System.getSecurityManager();if (sm != null) {//检查创建一个新的代理类需要权限checkProxyAccess(Reflection.getCallerClass(), loader, intfs);}/** Look up or generate the designated proxy class.*///注意点1: getProxyClass0方法拿到代理类的Class对象实例cl//注意传入的参数就是从外部传入的loader(被代理类的类加载器)、intfs(被代理类实现所接口的Class[]的副本)Class<?> cl = getProxyClass0(loader, intfs);/** Invoke its constructor with the designated invocation handler.*/try {if (sm != null) {checkNewProxyPermission(Reflection.getCallerClass(), cl);}//注意点2: 拿到cl实例后,就通过反射机制创建代理类实例final Constructor<?> cons = cl.getConstructor(constructorParams);//先拿到代理类的构造器Constructor实例consfinal InvocationHandler ih = h;//检查代理类构造器是否是公有的public权限, 不是就会通过AccessController去修改访问权限以致于可以创建代理类实例if (!Modifier.isPublic(cl.getModifiers())) {AccessController.doPrivileged(new PrivilegedAction<Void>() {public Void run() {cons.setAccessible(true);//将访问权限设置为可访问的return null;}});}//注意点3: 拿到构造器实例cons后,就到了最为关键的也就是最后一步,创建代理类实例。//但是需要注意的是构造器反射传入的参数是h,也就是传入的InvocationHandler的实例,也可以进一步推论生成的代理类中存在以InvocationHandler为参数的构造器。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源码流程:

首先传入loaderinterfacesh三个参数,先将interfacesclone一份副本保存在intfs中,然后检查创建一个新的代理类所需要的权限,接着到了我们第一个注意点1,就是通过getProxyClass0方法(需要传入loaderintfs参数)获得代理类的Class对象实例。拿到了代理类实例后,我们就通过反射的机制创建代理类实例

到了我们的注意点二,通过代理类Class对象cl获得构造器对象cons,并检查构造器对象是否是public,否则就强行修改访问权限

最后到了注意点三,通过cons.newInstance创建代理类对象,并且构造器反射中传入h(InvocationHandler对象),说明我们可以推断一下生成的代理类中存在以InvocationHandler为参数的构造器

  • 3、第三步进入getProxyClass0方法,传入的参数loaderintfs,在该方法内部会委托给proxyClassCache的get方法,如果给定的类加载器中定义的代理类实现了给定的接口,直接返回缓存中的副本,否则它将通过ProxyClassFactory创建代理类.
    private static Class<?> getProxyClass0(ClassLoader loader,Class<?>... interfaces) {if (interfaces.length > 65535) {throw new IllegalArgumentException("interface limit exceeded");}// If the proxy class defined by the given loader implementing// the given interfaces exists, this will simply return the cached copy;// otherwise, it will create the proxy class via the ProxyClassFactory//请注意上面那段英文注释: 如果给定的类加载器中定义的代理类实现了给定的接口,//那么就会直接返回缓存中的副本,否则它将通过ProxyClassFactory创建代理类//注意点1: proxyClassCache;注意点2: ProxyClassFactoryreturn proxyClassCache.get(loader, interfaces);}
  • 4、第四步proxyClassCache的介绍和定义, 请注意创建proxyClassCache传入的构造器两个参数分别是: KeyFactoryProxyClassFactory
    /*** a cache of proxy classes*/private static final WeakCache<ClassLoader, Class<?>[], Class<?>>proxyClassCache = new WeakCache<>(new KeyFactory(), new ProxyClassFactory());

proxyClassCache是一个WeakCache<K,P,V>对象,WeakCache<K,P,V>中的K表示key值,P代表参数,V代表存储的值。此类用于缓存(key, sub-key) -> value键值对。内部具体实现是借助了ConcurentMap<Object, ConcurrentMap<Object, Supplier<V>>>,Supplier是一个接口,就一个get方法用于获得值,不过是泛型V的包装类,第一个Object就是key(这里表达式不用泛型K是因为key值可以为null),第二个就是sub-key,那么它对应着什么呢? 而且具体的缓存中也没有泛型P呢,这就需要引出另外一个函数接口BiFunction<T, U, R>,该接口内部存在R apply(T t, U u)方法,这个方法意思就是根据传入两个泛型TU的值经过一定计算得到泛型R的值。在WeakCache<K,P,V>类中存在两个BiFunction对象:

final class WeakCache<K, P, V> {private final ReferenceQueue<K> refQueue= new ReferenceQueue<>();// the key type is Object for supporting null keyprivate final ConcurrentMap<Object, ConcurrentMap<Object, Supplier<V>>> map= new ConcurrentHashMap<>();private final ConcurrentMap<Supplier<V>, Boolean> reverseMap= new ConcurrentHashMap<>();private final BiFunction<K, P, ?> subKeyFactory;private final BiFunction<K, P, V> valueFactory;public WeakCache(BiFunction<K, P, ?> subKeyFactory, BiFunction<K, P, V> valueFactory) {//根据K,P得到sub-key算法this.subKeyFactory = Objects.requireNonNull(subKeyFactory);//根据K,P得到value算法this.valueFactory = Objects.requireNonNull(valueFactory);}...
}

WeakCahe类中只有一个核心get方法,里面包含了整个缓存的逻辑,注意我们获取代理类Class对象,就是通过proxyClassCache.get(loader, interfaces);实际上就是调用WeakCache中的get方法.

//K泛型是一级map的缓存key, P泛型传入的参数,分别对应外部传入的 loader和 interfaces
public V get(K key, P parameter) {...//通过传入一级map的key,通过CacheKey拿到最终Object cacheKey = CacheKey.valueOf(key, refQueue);// 懒初始化cacheKey对应的二级valuesMap, 如果valuesMap为空,就会新创建一个空的ConcurrentMap的valueMap,put到一级缓存map中ConcurrentMap<Object, Supplier<V>> valuesMap = map.get(cacheKey);if (valuesMap == null) {//如果valuesMap为空,就会新创建一个空的ConcurrentMap的valueMapConcurrentMap<Object, Supplier<V>> oldValuesMap = map.putIfAbsent(cacheKey,valuesMap = new ConcurrentHashMap<>());//如果内部已经存在原来的oldValuesMap直接用它if (oldValuesMap != null) {valuesMap = oldValuesMap;}}//------注意点1: subKeyFactory.apply(key, parameter)-----//根据传入的一级map的key和参数parameter,通过subKeyFactory中的apply方法获得sub-key,Object subKey = Objects.requireNonNull(subKeyFactory.apply(key, parameter));//然后通过我们的sub-key,从二级缓存的valuesMap中取的supplier对象Supplier<V> supplier = valuesMap.get(subKey);Factory factory = null;while (true) {if (supplier != null) {//supplier这个对象可能是Factory或CacheValue<V>对象,//那么也就是supplier.get()方法可能是调用了Factory中的get方法或CacheValue<V>中的get方法
//--------注意点2: supplier.get()-----            V value = supplier.get();//如果value不为空就返回value,结束整个get方法调用if (value != null) {return value;}}//如果缓存中没有supplier对象//或者supplier中get返回是null//或者Factory对象没有在CacheValue中被成功创建//factory为null,就会创建一个新的Factory实例if (factory == null) {factory = new Factory(key, parameter, subKey, valuesMap);}//supplier为nullif (supplier == null) {//根据新创建的factory和subKey拿到supplier对象,如果valuesMap中存在subKey, factory键值对,就返回已经存在的值,没有直接返回nullsupplier = valuesMap.putIfAbsent(subKey, factory);if (supplier == null) {//如果拿到supplier为null,supplier就变为了factory,这就是前面说supplier为一个factory   supplier = factory;}// else retry with winning supplier} else {if (valuesMap.replace(subKey, supplier, factory)) {supplier = factory;} else {//通过valuesMap.get()拿到suppliersupplier = valuesMap.get(subKey);}}}}

我们来一起梳理下WeakCache的逻辑: 首先proxyClassCache就是一个WeakCache实例对象,它有两个构造器参数subKeyFactoryvalueFactory,创建proxyClassCache实例对应传入的是proxyClassCache = new WeakCache<>(new KeyFactory(), new ProxyClassFactory())中的KeyFactoryProxyClassFactory.

然后在WeakCache内部存在二级ConcurrentHashMap, 一级map的key就是get方法传入的key, 通过这个key拿到cacheKey,从而拿到对应的valuesMap二级map

然后又通过根据传入的一级map的key和参数parametersubKeyFactory中的apply方法获得sub-key,通过sub-key拿到二级map中存储的Supplier对象,它可能是一个CacheValue也有可能是一个Factory,

最终通过Factoryget方法拿到实际的值。

对于上述有两个核心注意点

注意点1----->获取subKey过程: 通过subKeyFactory.apply(key,parameter)拿到sub-key

//weakCache调用处: Object subKey = Objects.requireNonNull(subKeyFactory.apply(key, parameter));//KeyFactory的定义private static final class KeyFactoryimplements BiFunction<ClassLoader, Class<?>[], Object>{@Overridepublic Object apply(ClassLoader classLoader, Class<?>[] interfaces) {//可以看到是根据被代理类实现的接口的Class数组长度来决定选用哪一种subKeyswitch (interfaces.length) {//对于被代理类只实现了1个接口情况,也是最频繁一种case 1: return new Key1(interfaces[0]); // the most frequent//对于被代理类只实现了2个接口情况case 2: return new Key2(interfaces[0], interfaces[1]);//对于被代理类只实现了0个接口情况case 0: return key0;//对于被代理类只实现了超过2个接口情况default: return new KeyX(interfaces);}}}

注意点2----> supplier.get()获取value的过程:

我们都知道supplier对应的可以是Factory对象,也就是最后会调用Factory中的get方法。

  @Overridepublic synchronized V get() { // serialize access// 再一次检查supplier,如果传入从valuesMap拿到的不等于当前Factory对象,因为它可能已经变成CacheValue了,那就直接返回nullSupplier<V> supplier = valuesMap.get(subKey);if (supplier != this) {return null;}//创建一个新的valueV value = null;try {//注意点出现,value最终会通过valueFactory.apply(key, parameter)拿到value = Objects.requireNonNull(valueFactory.apply(key, parameter));} finally {if (value == null) { // remove us on failurevaluesMap.remove(subKey, this);}}// 判断value是否为nullassert value != null;// 将拿到的value包装成一个CacheValueCacheValue<V> cacheValue = new CacheValue<>(value);// 尝试把valuesMap中的对应subKey的Factory替换成cacheValue,//这就是为什么前面说过valuesMap中取出的Supplier可能是Factory可能是CacheValue,有没有种偷梁换柱的赶脚if (valuesMap.replace(subKey, this, cacheValue)) {//替换成功后,并把cacheValue put到reverseMap中reverseMap.put(cacheValue, Boolean.TRUE);} else {throw new AssertionError("Should not reach here");}// 成功替换了新的CacheValue,并返回最终的值return value;}

通过上述代码分析,我们知道最终value获取是来自于valueFactoryapply方法,还记得valueFactory是啥吗?没错它就是ProxyClassFactory也就是最终定位到了ProxyClassFactory中的apply方法。这也就是为什么之前说如果缓存中有直接从缓存中返回缓存的副本,没有就在ProxyClassFactory中创建代理对象。

  • 5、第五步进入ProxyClassFactory中的apply方法进行探究,这是创建新的代理类Class对象唯一来源。
    private static final class ProxyClassFactoryimplements BiFunction<ClassLoader, Class<?>[], Class<?>>{// 所有生成的代理类名称统一前缀$Proxyprivate static final String proxyClassNamePrefix = "$Proxy";private static final AtomicLong nextUniqueNumber = new AtomicLong();@Overridepublic Class<?> apply(ClassLoader loader, Class<?>[] interfaces) {Map<Class<?>, Boolean> interfaceSet = new IdentityHashMap<>(interfaces.length);for (Class<?> intf : interfaces) {//验证类加载器是否将此接口的名称解析为同一个Class对象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");}//验证interfaceClass的Class对象是否是一个接口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;     // package to define proxy class inint accessFlags = Modifier.PUBLIC | Modifier.FINAL;//记录非公共代理接口的包,以便proxy类将在同一个包中定义。验证所有非公共代理接口是否在同一个包中for (Class<?> intf : interfaces) {int flags = intf.getModifiers();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");}}}if (proxyPkg == null) {// if no non-public proxy interfaces, use com.sun.proxy packageproxyPkg = ReflectUtil.PROXY_PACKAGE + ".";}//生成唯一的代理类名称标识long num = nextUniqueNumber.getAndIncrement();String proxyName = proxyPkg + proxyClassNamePrefix + num;//生成确定的代理类Class文件的byte数组//------注意点ProxyGenerator.generateProxyClass-----byte[] proxyClassFile = ProxyGenerator.generateProxyClass(proxyName, interfaces, accessFlags);try {//通过defineClass0方法传入被代理类的类加载器、代理类唯一名称、生成的代理类文件反序列化成一个代理类的Class对象return defineClass0(loader, proxyName,proxyClassFile, 0, proxyClassFile.length);} catch (ClassFormatError e) {throw new IllegalArgumentException(e.toString());}}}

再重新梳理一下ProxyClassFactory中的apply中的逻辑,首先做一些接口验证操作,然后通过ProxyGenerator.generateProxyClass生成确定的代理类Class文件的byte数组,最后通过defineClass0方法传入被代理类的类加载器、代理类唯一名称、生成的代理类文件反序列化成一个代理类的Class对象

  • 6、第六步进入ProxyGenerator中的generateProxyClass方法进行探究,主要通过它来生成代理类Class文件。generateProxyClass方法传入的参数主要有: proxyName(唯一代理类名称), interfaces(需要代理的接口Class数组), accessFlags(访问权限标识)
    public static byte[] generateProxyClass(final String var0, Class<?>[] var1, int var2) {ProxyGenerator var3 = new ProxyGenerator(var0, var1, var2);//-----注意点----调用ProxyGenerator中的generateClassFile方法final byte[] var4 = var3.generateClassFile();//是否需要把生成Class文件保存在本地文件中,这个标识可以从外部进行配置//boolean saveGeneratedFiles = (Boolean)AccessController.doPrivileged(new GetBooleanAction("sun.misc.ProxyGenerator.saveGeneratedFiles"))if (saveGeneratedFiles) {AccessController.doPrivileged(new PrivilegedAction<Void>() {public Void run() {try {int var1 = var0.lastIndexOf(46);Path var2;if (var1 > 0) {Path var3 = Paths.get(var0.substring(0, var1).replace('.', File.separatorChar));Files.createDirectories(var3);var2 = var3.resolve(var0.substring(var1 + 1, var0.length()) + ".class");} else {var2 = Paths.get(var0 + ".class");}Files.write(var2, var4, new OpenOption[0]);return null;} catch (IOException var4x) {throw new InternalError("I/O exception saving generated file: " + var4x);}}});}return var4;}
  • 7、第七步进入generateClassFile()方法,该方法主要生成Class文件。
 private byte[] generateClassFile() {//---注意点1----在生成的代理类中加入Object类几个默认方法比如常见的hashCode、equal、toString方法this.addProxyMethod(hashCodeMethod, Object.class);this.addProxyMethod(equalsMethod, Object.class);this.addProxyMethod(toStringMethod, Object.class);//取出代理类的接口的Class数组Class[] var1 = this.interfaces;int var2 = var1.length;int var3;Class var4;//----注意点2---遍历代理类的接口的Class数组,将代理类接口中的方法加入生成的代理类中for(var3 = 0; var3 < var2; ++var3) {var4 = var1[var3];//获得每个接口中定义的所有方法Method对象Method[] var5 = var4.getMethods();int var6 = var5.length;//然后再遍历所有的Method对象,并把它加入到生成的代理类中for(int var7 = 0; var7 < var6; ++var7) {Method var8 = var5[var7];this.addProxyMethod(var8, var4);}}...try {//----注意点3 生成的代理类中加入生成构造器方法generateConstructor----this.methods.add(this.generateConstructor());var11 = this.proxyMethods.values().iterator();...//----注意点4 生成的代理类中加入生成静态初始化块----this.methods.add(this.generateStaticInitializer());} catch (IOException var10) {throw new InternalError("unexpected I/O Exception", var10);}...//创建字节数组输出流ByteArrayOutputStream var13 = new ByteArrayOutputStream();DataOutputStream var14 = new DataOutputStream(var13);try {...var14.writeShort(this.fields.size());var15 = this.fields.iterator();//往输出流写入生成代理类Filed字段相关信息while(var15.hasNext()) {ProxyGenerator.FieldInfo var20 = (ProxyGenerator.FieldInfo)var15.next();var20.write(var14);}var14.writeShort(this.methods.size());var15 = this.methods.iterator();//往输出流写入生成代理类Method方法相关信息while(var15.hasNext()) {ProxyGenerator.MethodInfo var21 = (ProxyGenerator.MethodInfo)var15.next();var21.write(var14);}var14.writeShort(0);//返回最终的Class文件的字节数组return var13.toByteArray();} catch (IOException var9) {throw new InternalError("unexpected I/O Exception", var9);}}}
  • 8、以上就是整个动态代理中代理类生成代码的过程,为了进一步弄明白动态的机制,比如invoke是怎么调用的呢。不妨我们把生成代码保存在本地文件中,然后一起来看下生成的代理类长啥样。
public final class $Proxy1 extends Proxy implements IPurchaseHouse {private static Method m1;private static Method m7;private static Method m8;private static Method m2;private static Method m4;private static Method m3;private static Method m6;private static Method m0;private static Method m5;//----注意点1 生成代理类中的构造器中有个InvocationHandler参数----public $Proxy1(InvocationHandler var1) throws  {super(var1);//并把它传给它的父类Proxy中的h(InvocationHandler)}//生成equals方法public final boolean equals(Object var1) throws  {try {//---注意点出现super.h.invoke---//委托父类`Proxy`中的`h`中的`invoke`方法来实现调用,并把当前生成的代理类实例this、当前方法对应的`Method`对象和参数数组`args`通过`invoke`回调出去,此时`InvocationHandler`子类中的`invoke`方法就会得以触发return (Boolean)super.h.invoke(this, m1, new Object[]{var1});} catch (RuntimeException | Error var3) {throw var3;} catch (Throwable var4) {throw new UndeclaredThrowableException(var4);}}//生成Object类中默认的hashCode方法public final int hashCode() throws  {try {//---注意点出现super.h.invoke---return (Integer)super.h.invoke(this, m0, (Object[])null);} catch (RuntimeException | Error var2) {throw var2;} catch (Throwable var3) {throw new UndeclaredThrowableException(var3);}}//生成Object类中默认的toString方法public final String toString() throws  {try {//---注意点出现super.h.invoke---return (String)super.h.invoke(this, m2, (Object[])null);} catch (RuntimeException | Error var2) {throw var2;} catch (Throwable var3) {throw new UndeclaredThrowableException(var3);}}//生成代理接口中的payDeposit方法public final void payDeposit() throws  {try {//---注意点出现super.h.invoke 同理---super.h.invoke(this, m7, (Object[])null);} catch (RuntimeException | Error var2) {throw var2;} catch (Throwable var3) {throw new UndeclaredThrowableException(var3);}}//生成代理接口中的signAgreement方法public final void signAgreement() throws  {try {//---注意点出现super.h.invoke 同理---super.h.invoke(this, m8, (Object[])null);} catch (RuntimeException | Error var2) {throw var2;} catch (Throwable var3) {throw new UndeclaredThrowableException(var3);}}//生成代理接口中的payMoney方法public final void payMoney() throws  {try {//---注意点出现super.h.invoke 同理---super.h.invoke(this, m4, (Object[])null);} catch (RuntimeException | Error var2) {throw var2;} catch (Throwable var3) {throw new UndeclaredThrowableException(var3);}}//生成代理接口中的getHouse方法public final void getHouse() throws  {try {//---注意点出现super.h.invoke 同理---super.h.invoke(this, m3, (Object[])null);} catch (RuntimeException | Error var2) {throw var2;} catch (Throwable var3) {throw new UndeclaredThrowableException(var3);}}//生成代理接口中的visitHouse方法public final void visitHouse() throws  {try {//---注意点出现super.h.invoke 同理---super.h.invoke(this, m6, (Object[])null);} catch (RuntimeException | Error var2) {throw var2;} catch (Throwable var3) {throw new UndeclaredThrowableException(var3);}}//生成代理接口中的inquiryPrice方法public final void inquiryPrice() throws  {try {//---注意点出现super.h.invoke 同理---super.h.invoke(this, m5, (Object[])null);} catch (RuntimeException | Error var2) {throw var2;} catch (Throwable var3) {throw new UndeclaredThrowableException(var3);}}//生成的静态初始化块中,通过反射拿到对应的方法Method对象,//其中包括了Object中的方法和代理接口中的方法static {try {m1 = Class.forName("java.lang.Object").getMethod("equals", Class.forName("java.lang.Object"));m7 = Class.forName("com.mikyou.design_pattern.delegates.dynamic.IPurchaseHouse").getMethod("payDeposit");m8 = Class.forName("com.mikyou.design_pattern.delegates.dynamic.IPurchaseHouse").getMethod("signAgreement");m2 = Class.forName("java.lang.Object").getMethod("toString");m4 = Class.forName("com.mikyou.design_pattern.delegates.dynamic.IPurchaseHouse").getMethod("payMoney");m3 = Class.forName("com.mikyou.design_pattern.delegates.dynamic.IPurchaseHouse").getMethod("getHouse");m6 = Class.forName("com.mikyou.design_pattern.delegates.dynamic.IPurchaseHouse").getMethod("visitHouse");m0 = Class.forName("java.lang.Object").getMethod("hashCode");m5 = Class.forName("com.mikyou.design_pattern.delegates.dynamic.IPurchaseHouse").getMethod("inquiryPrice");} catch (NoSuchMethodException var2) {throw new NoSuchMethodError(var2.getMessage());} catch (ClassNotFoundException var3) {throw new NoClassDefFoundError(var3.getMessage());}}
}

其实当你看到了生成的代理类的代码后,你就会发现动态代理的机制就非常一目了然。你也就明白了InvocationHandler中的invoke方法什么时候调用了。那我们再来整体梳理下动态代理核心机制,其实最为核心的就是InvocationHandler:

首先,我们需要去实现一个InvocationHandler的子类,重写它的invoke方法,该方法中会回调三个参数: Object proxy, Method method, Object[] args,然后在我们在invoke方法中只需要通过调用methodinvoke方法,并传入args参数。

然后我们去创建一个代理类实例是通过Proxy.newProxyInstance,会传入InvocationHandler子类实例,并把这个InvocationHandler子类实例作为生成新的代理类的构造器函数参数,并把这个参数传给新的代理类的父类Proxy,在Proxy中会维护这个InvocationHandler子类实例h

然后通过上述生成的代理类代码来看,会把所有方法都转成对应的Method对象,并在静态初始化块中通过反射进行初始化,然后每个方法内部调用实现,都会委托父类Proxy中的h中的invoke方法来实现调用,并把当前生成的代理类实例、当前方法对应的Method对象和参数数组args通过invoke回调出去,此时InvocationHandler子类中的invoke方法会得以触发,那么在其内部又转为method调用它的invoke方法,并传入args参数就相当于利用反射去调用这个方法。

最后到这里,有关动态代理内容就算说完了。

欢迎关注Kotlin开发者联盟,这里有最新Kotlin技术文章,每周会不定期翻译一篇Kotlin国外技术文章。如果你也喜欢Kotlin,欢迎加入我们~~~

Kotlin系列文章,欢迎查看:

Kotlin邂逅设计模式系列:

  • 当Kotlin完美邂逅设计模式之单例模式(一)

数据结构与算法系列:

  • 每周一算法之二分查找(Kotlin描述)
  • 每周一数据结构之链表(Kotlin描述)

翻译系列:

  • [译]带你揭开Kotlin中属性代理和懒加载语法糖衣
  • [译] Kotlin中关于Companion Object的那些事
  • [译]记一次Kotlin官方文档翻译的PR(内联类)
  • [译]Kotlin中内联类的自动装箱和高性能探索(二)
  • [译]Kotlin中内联类(inline class)完全解析(一)
  • [译]Kotlin的独门秘籍Reified实化类型参数(上篇)
  • [译]Kotlin泛型中何时该用类型形参约束?
  • [译] 一个简单方式教你记住Kotlin的形参和实参
  • [译]Kotlin中是应该定义函数还是定义属性?
  • [译]如何在你的Kotlin代码中移除所有的!!(非空断言)
  • [译]掌握Kotlin中的标准库函数: run、with、let、also和apply
  • [译]有关Kotlin类型别名(typealias)你需要知道的一切
  • [译]Kotlin中是应该使用序列(Sequences)还是集合(Lists)?
  • [译]Kotlin中的龟(List)兔(Sequence)赛跑

原创系列:

  • 教你如何完全解析Kotlin中的注解
  • 教你如何完全解析Kotlin中的类型系统
  • 如何让你的回调更具Kotlin风味
  • Jetbrains开发者日见闻(三)之Kotlin1.3新特性(inline class篇)
  • JetBrains开发者日见闻(二)之Kotlin1.3的新特性(Contract契约与协程篇)
  • JetBrains开发者日见闻(一)之Kotlin/Native 尝鲜篇
  • 教你如何攻克Kotlin中泛型型变的难点(实践篇)
  • 教你如何攻克Kotlin中泛型型变的难点(下篇)
  • 教你如何攻克Kotlin中泛型型变的难点(上篇)
  • Kotlin的独门秘籍Reified实化类型参数(下篇)
  • 有关Kotlin属性代理你需要知道的一切
  • 浅谈Kotlin中的Sequences源码解析
  • 浅谈Kotlin中集合和函数式API完全解析-上篇
  • 浅谈Kotlin语法篇之lambda编译成字节码过程完全解析
  • 浅谈Kotlin语法篇之Lambda表达式完全解析
  • 浅谈Kotlin语法篇之扩展函数
  • 浅谈Kotlin语法篇之顶层函数、中缀调用、解构声明
  • 浅谈Kotlin语法篇之如何让函数更好地调用
  • 浅谈Kotlin语法篇之变量和常量
  • 浅谈Kotlin语法篇之基础语法

Effective Kotlin翻译系列

  • [译]Effective Kotlin系列之考虑使用原始类型的数组优化性能(五)
  • [译]Effective Kotlin系列之使用Sequence来优化集合的操作(四)
  • [译]Effective Kotlin系列之探索高阶函数中inline修饰符(三)
  • [译]Effective Kotlin系列之遇到多个构造器参数要考虑使用构建器(二)
  • [译]Effective Kotlin系列之考虑使用静态工厂方法替代构造器(一)

实战系列:

  • 用Kotlin撸一个图片压缩插件ImageSlimming-导学篇(一)
  • 用Kotlin撸一个图片压缩插件-插件基础篇(二)
  • 用Kotlin撸一个图片压缩插件-实战篇(三)
  • 浅谈Kotlin实战篇之自定义View图片圆角简单应用

当Kotlin邂逅设计模式之代理模式(二)相关推荐

  1. JavaScipt设计模式初探-代理模式(三) 虚拟代理

    文章目录 前言 一.举例_图片懒加载 二.虚拟代理在示例中的体现 总结 前言 虚拟代理是代理模式在性能方面的分支, 个人认为有点像人工异步操作. 把累活和细活从真实对象拆出来丢给代理对象, " ...

  2. 【设计模式】代理模式 ( 动态代理 | 模拟 Java 虚拟机生成对应的 代理对象 类 )

    文章目录 前言 一.模拟 JVM 生成对应的 代理对象 二.模拟 JVM 生成对应的 代理对象 完整流程展示 1.目标对象接口 2.被代理对象 3.调用处理程序 4.模拟 JVM 生成的代理对象类 5 ...

  3. 「设计模式(五) - 代理模式」

    「设计模式(五) - 代理模式」 一.处处可见的"代理" "代理"在平常生活司空见惯,点外卖,租房子找中介,买飞机票等等.基本上用手机就能完成,也就是不直接接触 ...

  4. 23种设计模式7_代理模式之一静态代理

    23种设计模式7_代理模式之一静态代理 1 基本介绍 代理模式:为其他对象提供一种代理以控制对这个对象的访问 代理模式也叫委托模式,它是一项基本设计技巧.许多其他的模式,如状态模式.策略模式.访问者模 ...

  5. C++设计模式之代理模式

    这篇文章主要介绍了C++设计模式之代理模式,本文讲解了什么是代理模式.代理模式的使用场合.代理模式的实现代码等内容,需要的朋友可以参考下 前言 青春总是那样,逝去了才开始回味:大学生活也是在不经意间就 ...

  6. python中代理模式分为几种_Python设计模式之代理模式实例详解

    本文实例讲述了Python设计模式之代理模式.分享给大家供大家参考,具体如下: 代理模式(Proxy Pattern):为其他对象提供一种代理以控制对这个对象的访问 #!/usr/bin/env py ...

  7. 设计模式笔记——代理模式

    设计模式笔记--代理模式 代理模式介绍 代理模式通常是介于请求方和提供方的一个中介系统,请求方是发送请求的一方,提供方是根据请求提供相应资源的一方 Web中的代理服务器就是一个例子,客户端向代理服务器 ...

  8. Javascript 设计模式之代理模式【讲师辅导】-曾亮-专题视频课程

    Javascript 设计模式之代理模式[讲师辅导]-969人已学习 课程介绍         随着 javascript ES6/7 的发布,很多老版本的设计模式的实现,今天来看是错误的,将被彻底. ...

  9. 设计模式之一代理模式

    代理模式(代理设计模式) 代理模式的定义与特点 代理模式的结构与实现 代理模式(代理设计模式) 在有些情况下,一个客户不能或者不想直接访问另一个对象,这时需要找一个中介帮忙完成某项任务,这个中介就是代 ...

最新文章

  1. 利用最小二乘法求解仿射变换参数
  2. Java设计模式(备忘录模式-解释器模式-状态模式-策略模式-职责链模式)
  3. 网上图书商城项目学习笔记-035工具类之JdbcUtils及TxQueryRunner及C3P0配置
  4. VTK修炼之道31:图像二值化_阈值法
  5. windows和linux的内存管理
  6. 持续更新的Zookeeper知识总结
  7. class触发后让另一个class加样式_Bootstrap的按钮组样式
  8. java导入数据 neo4j_java-neo4j-使用neo4j剩余图数据库进行批量插...
  9. 在Python中写入文件时,权限被拒绝错误
  10. NYOJ-71-独木舟上的旅行
  11. Linux安装jdk、删除Open jdk
  12. 前端实现序列帧_Html5 序列帧动画
  13. 如何编辑PDF文件?分享几种编辑PDF文件方法
  14. 小程序开发的流程简介
  15. 网站搭建niushop系统,全面搭建,打包app,h5详细教程
  16. RTEC机器人_广东省中小学电脑机器人活动获奖结果公布
  17. 西部数码网站备案幕布及核验单填写规范
  18. 安知我不知死之乐”的意境
  19. Spring Cloud Loadbalancer
  20. STP的端口状态,BPDU,计时器

热门文章

  1. win7wifi共享
  2. uboot显示logo的方式
  3. 李克平教授讲座——《城市道路交叉口规划规范》解读与绿灯间隔问题分析
  4. 研发部门管理的三驾马车
  5. 软件工程专业女生测试方向,女生学软件工程好吗 就业方向有哪些
  6. Flask web开发实战之基础篇 Flask-数据库
  7. redis6 入门级教程,有整合案例,可以直接看整合案例,简单入门,直接上手
  8. 共享面部识别,让罪犯在阿sir面前无所遁形!
  9. Cloudera Manager 5 Overview
  10. jQuery取得列表控件选中的option对象