网上搜CGLIB动态代理,几乎所有的博文都只给了示例代码而缺少对代码的解释说明(特别是关键的intercept函数),看完实在是云里雾里。所以,这篇博文将带你从源码的角度来理解intercept函数。

前言

关于如何使用CGLIB创建动态代理,网上已经有很多资料,这里就不再赘述。本文将使用如下代码进行分析,如果你还看不懂下面的代码,请先自行搜索资料看懂后再继续后面的内容。

import net.sf.cglib.core.DebuggingClassWriter;
import net.sf.cglib.proxy.Enhancer;
import net.sf.cglib.proxy.MethodInterceptor;
import net.sf.cglib.proxy.MethodProxy;
import java.lang.reflect.Method;class Human {public void say() {System.out.println("I am super man~~~");}
}class ProxyWithCglib {public static Object newProxy(Object object) {Enhancer enhancer = new Enhancer();enhancer.setSuperclass(object.getClass());MyMethodInterceptor myMethodInterceptor = new MyMethodInterceptor();myMethodInterceptor.bind(object);enhancer.setCallback(myMethodInterceptor);return enhancer.create();}
}class MyMethodInterceptor implements MethodInterceptor {private Object target;public void bind(Object o) {this.target = o;}@Overridepublic Object intercept(Object o, Method method, Object[] objects, MethodProxy methodProxy) throws Throwable {System.out.println("pre-function logic.");Object ret = methodProxy.invokeSuper(o, objects);System.out.println("post-function logic.");return ret;}
}public class Test {public static void main(String[] args) {System.setProperty(DebuggingClassWriter.DEBUG_LOCATION_PROPERTY, "./debug_info"); // 用于输出CGLIB产生的类Human humanProxy = (Human) ProxyWithCglib.newProxy(new Human());System.out.println(humanProxy.getClass());humanProxy.say();}
}

简要对代码进行一些说明:

  • Human类为被代理对象;
  • MyMethodInterceptor中的target属性用于保存被代理对象的实例;
  • ProxyWithCglib类的静态方法newProxy用于创建代理对象实例;
  • 运行代码即可在debug_info目录下看到运行时生成的类,这将用于我们后续的分析

理解intercept的四个参数

要理解intercept方法的参数的含义,一个不错的办法就是看看运行时调用intercept方法的时候传入的参数是什么。

为了找到在哪里调用了intercept方法,我们从main方法的最后一行:

humanProxy.say();

开始跟。

根据

System.out.println(humanProxy.getClass());

这一行我们可以知道运行时生成的类为:

Human$$EnhancerByCGLIB$$xxxxxx

这个类的代码为(为了便于阅读,只放出我们关注的部分):

public class Human$$EnhancerByCGLIB$$1a29a813 extends Human implements Factory {private boolean CGLIB$BOUND;public static Object CGLIB$FACTORY_DATA;private static final ThreadLocal CGLIB$THREAD_CALLBACKS;private static final Callback[] CGLIB$STATIC_CALLBACKS;private MethodInterceptor CGLIB$CALLBACK_0;private static Object CGLIB$CALLBACK_FILTER;private static final Method CGLIB$say$0$Method;private static final MethodProxy CGLIB$say$0$Proxy;private static final Object[] CGLIB$emptyArgs;private static final Method CGLIB$equals$1$Method;private static final MethodProxy CGLIB$equals$1$Proxy;private static final Method CGLIB$toString$2$Method;private static final MethodProxy CGLIB$toString$2$Proxy;private static final Method CGLIB$hashCode$3$Method;private static final MethodProxy CGLIB$hashCode$3$Proxy;private static final Method CGLIB$clone$4$Method;private static final MethodProxy CGLIB$clone$4$Proxy;static void CGLIB$STATICHOOK1() {CGLIB$THREAD_CALLBACKS = new ThreadLocal();CGLIB$emptyArgs = new Object[0];Class var0 = Class.forName("Human$$EnhancerByCGLIB$$1a29a813");Class var1;Method[] var10000 = ReflectUtils.findMethods(new String[]{"equals", "(Ljava/lang/Object;)Z", "toString", "()Ljava/lang/String;", "hashCode", "()I", "clone", "()Ljava/lang/Object;"}, (var1 = Class.forName("java.lang.Object")).getDeclaredMethods());CGLIB$say$0$Method = ReflectUtils.findMethods(new String[]{"say", "()V"}, (var1 = Class.forName("Human")).getDeclaredMethods())[0];CGLIB$say$0$Proxy = MethodProxy.create(var1, var0, "()V", "say", "CGLIB$say$0");// .....}final void CGLIB$say$0() {super.say();}public final void say() {MethodInterceptor var10000 = this.CGLIB$CALLBACK_0;if (var10000 == null) {CGLIB$BIND_CALLBACKS(this);var10000 = this.CGLIB$CALLBACK_0;}if (var10000 != null) {var10000.intercept(this, CGLIB$say$0$Method, CGLIB$emptyArgs, CGLIB$say$0$Proxy);} else {super.say();}}//.....
}

通过这段代码,我们可以获得如下信息:

  • CGLIB为我们生成的代理类继承了Human,验证了CGLIB是基于继承来实现代理的。

  • 当我们调用humanProxy.say()方法时,如果我们设置了MethodInterceptor的话,执行的逻辑实际是:

    var10000.intercept(this, CGLIB$say$0$Method, CGLIB$emptyArgs, CGLIB$say$0$Proxy);
    

    所以,也就是在这里,调用了intercept方法!

    再多说一句,这里的MethodInterceptor对象就是我们用enhancer的setCallback方法传进来的,即如下代码:

    MyMethodInterceptor myMethodInterceptor = new MyMethodInterceptor();
    myMethodInterceptor.bind(object);
    enhancer.setCallback(myMethodInterceptor);
    

好了,找到intercept方法的调用后,我们就可以开始理解他的四个参数了,为了便于阅读,我把intercept的声明也列出来:

// 声明
public Object intercept(Object o, Method method, Object[] objects, MethodProxy methodProxy) throws Throwable {...}// 调用 (在say()方法的上下文中)
var10000.intercept(this, CGLIB$say$0$Method, CGLIB$emptyArgs, CGLIB$say$0$Proxy);// 相关的变量(在CGLIB$STATICHOOK1()中初始化)
CGLIB$emptyArgs = new Object[0];CGLIB$say$0$Method = ReflectUtils.findMethods(new String[]{"say", "()V"}, (var1 = Class.forName("Human")).getDeclaredMethods())[0];CGLIB$say$0$Proxy = MethodProxy.create(var1, var0, "()V", "say", "CGLIB$say$0");

这么一看,intercept方法的四个参数的含义也就比较清楚了:

  • Object o:代理对象本身

    通过调用时传入this很容易看出。

  • Method method: 被代理对象的方法

    通过CGLIB$say 0 0 0Method的初始化过程我们可以知道,他实际就指向了被代理类(Human)中对应的方法(say())。也就是说,你在代理对象上调用了方法fun,当进入到intercept函数时,method参数就是指向了被代理对象的fun方法。

  • Object[] objects:函数调用的参数

    通过CGLIB$emptyArgs我们也可以很容易的猜出这个参数实际就是函数调用时要传递的参数列表。在本例中,由于say()不需要任何参数,所以传个空参即可。

  • MethodProxy methodProxy:方法的代理

    这个是四个参数中最难理解的一个,下面对这个参数展开说明。

理解MethodProxy(重点!)

同样,我们还是从源码的角度进行分析,源码中创建MethodProxy的代码为:

Class var0 = Class.forName("Human$$EnhancerByCGLIB$$1a29a813");
var1 = Class.forName("Human");
CGLIB$say$0$Proxy = MethodProxy.create(var1, var0, "()V", "say", "CGLIB$say$0");

这里,var1就是我们的被代理对象,而var0则是代理对象本身,然后通过MethodProxy.create()方法创建一个MethodProxy,MethodProxy类的源码为:

//
// Source code recreated from a .class file by IntelliJ IDEA
// (powered by FernFlower decompiler)
//package net.sf.cglib.proxy;import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import net.sf.cglib.core.AbstractClassGenerator;
import net.sf.cglib.core.CodeGenerationException;
import net.sf.cglib.core.GeneratorStrategy;
import net.sf.cglib.core.NamingPolicy;
import net.sf.cglib.core.Signature;
import net.sf.cglib.reflect.FastClass;
import net.sf.cglib.reflect.FastClass.Generator;public class MethodProxy {private Signature sig1;private Signature sig2;private MethodProxy.CreateInfo createInfo;private final Object initLock = new Object();private volatile MethodProxy.FastClassInfo fastClassInfo;public static MethodProxy create(Class c1, Class c2, String desc, String name1, String name2) {MethodProxy proxy = new MethodProxy();proxy.sig1 = new Signature(name1, desc);proxy.sig2 = new Signature(name2, desc);proxy.createInfo = new MethodProxy.CreateInfo(c1, c2);return proxy;}private void init() {if (this.fastClassInfo == null) {synchronized(this.initLock) {if (this.fastClassInfo == null) {MethodProxy.CreateInfo ci = this.createInfo;MethodProxy.FastClassInfo fci = new MethodProxy.FastClassInfo();fci.f1 = helper(ci, ci.c1);fci.f2 = helper(ci, ci.c2);fci.i1 = fci.f1.getIndex(this.sig1);fci.i2 = fci.f2.getIndex(this.sig2);this.fastClassInfo = fci;this.createInfo = null;}}}}private static FastClass helper(MethodProxy.CreateInfo ci, Class type) {Generator g = new Generator();g.setType(type);g.setClassLoader(ci.c2.getClassLoader());g.setNamingPolicy(ci.namingPolicy);g.setStrategy(ci.strategy);g.setAttemptLoad(ci.attemptLoad);return g.create();}// 为了便于观看,省略部分非关键代码public static MethodProxy find(Class type, Signature sig) {try {Method m = type.getDeclaredMethod("CGLIB$findMethodProxy", MethodInterceptorGenerator.FIND_PROXY_TYPES);return (MethodProxy)m.invoke((Object)null, sig);} catch (NoSuchMethodException var3) {throw new IllegalArgumentException("Class " + type + " does not use a MethodInterceptor");} catch (IllegalAccessException var4) {throw new CodeGenerationException(var4);} catch (InvocationTargetException var5) {throw new CodeGenerationException(var5);}}public Object invoke(Object obj, Object[] args) throws Throwable {try {this.init();MethodProxy.FastClassInfo fci = this.fastClassInfo;return fci.f1.invoke(fci.i1, obj, args);} catch (InvocationTargetException var4) {throw var4.getTargetException();} catch (IllegalArgumentException var5) {if (this.fastClassInfo.i1 < 0) {throw new IllegalArgumentException("Protected method: " + this.sig1);} else {throw var5;}}}public Object invokeSuper(Object obj, Object[] args) throws Throwable {try {this.init();MethodProxy.FastClassInfo fci = this.fastClassInfo;return fci.f2.invoke(fci.i2, obj, args);} catch (InvocationTargetException var4) {throw var4.getTargetException();}}private static class CreateInfo {// 省略}private static class FastClassInfo {// 省略}
}

友情提示:看下面的文字时,一定要特别关注是代理类,还是被代理类!

代码不算长,有一定编程经验的人应该都能看个大概(建议不要在这里看,最好能用IDE看,会方便很多),下面说一下看完代码后应得到的一些信息:

  • 代码里实际维护了两个东西,一个指向被代理的对象,一个指向代理对象。且和被代理对象相关的变量以1结尾,而与代理对象相关的变量则以2结尾

    这个通过create方法的源码即可知道。我们在调用create方法时,第一个参数var1,对应create方法中的形参class1,而var1指向的是被代理类,所以在MethodProxy类中,xxx1就是和被代理类有关;类似的,xxx2就是和代理类相关。这是最关键的一点!

  • 函数调用过程中涉及到了FastClass

    MethodProxy的两个重要方法就是invoke和invokeSuper,我们可以看到在他们的源码中,其实最后都是通过FastClass来完成了函数的调用。FastClass的原理就属于另外一个内容了,就不放在这篇文章里讲了。你现在只需要知道FastClass可以加速调用过程即可

我们在使用过程中,最主要的还是调用MethodProxy的invoke或者invokeSuper方法。所以我们再对这两个方法挖的深入一点:

public Object invoke(Object obj, Object[] args) throws Throwable {try {this.init();MethodProxy.FastClassInfo fci = this.fastClassInfo;return fci.f1.invoke(fci.i1, obj, args);} catch (InvocationTargetException var4) {throw var4.getTargetException();} catch (IllegalArgumentException var5) {if (this.fastClassInfo.i1 < 0) {throw new IllegalArgumentException("Protected method: " + this.sig1);} else {throw var5;}}}public Object invokeSuper(Object obj, Object[] args) throws Throwable {try {this.init();MethodProxy.FastClassInfo fci = this.fastClassInfo;return fci.f2.invoke(fci.i2, obj, args);} catch (InvocationTargetException var4) {throw var4.getTargetException();}
}

从代码中可以看出,invoke和invokeSuper最主要的区别在于他们是用不同的FastClass完成了最终的调用,即:

// inivoke
return fci.f1.invoke(fci.i1, obj, args);
// invokeSuper
return fci.f2.invoke(fci.i2, obj, args);

回想我们前面说的,1就是和被代理类相关,2就是和代理类相关。所以,套用到这里,我们就可以对这两个语句进行解释:

  • invoke中的fci.f1.invoke(fci.i1, obj, args);表示用对象obj以参数args调用被代理类中函数描述为desc的name1方法

    desc和name1是什么呢?再回过头看看MethodPeoxy的create()函数:

    // 声明
    public static MethodProxy create(Class c1, Class c2, String desc, String name1, String name2) {MethodProxy proxy = new MethodProxy();proxy.sig1 = new Signature(name1, desc);proxy.sig2 = new Signature(name2, desc);proxy.createInfo = new MethodProxy.CreateInfo(c1, c2);return proxy;
    }
    // 调用
    CGLIB$say$0$Proxy = MethodProxy.create(var1, var0, "()V", "say", "CGLIB$say$0");
    

    可以发现,desc描述了这个函数的参数列表以及返回值,由于我们这里的say方法没有返回值且不接收参数,所以是()V。而name1则是函数在被代理类中的名字。在本文的例子中就是say。

  • invokeSuper中的fci.f2.invoke(fci.i2, obj, args);表示用对象obj以参数args调用代理类中函数描述为desc的name2方法

    和上面一样的分析方式,可以知道这里调用的是代理类的CGLIB$say$0方法,那这个方法是什么呢?回到源码中一看,发现这个方法是:

    final void CGLIB$say$0() {super.say();
    }
    

    这个方法很简单,只有一行,直接super.say();相当于直接调用了父类(也就是被代理类)的say方法。

好了,分析到这,我们对MethodProxy应该有个比较清晰的认识了,总结起来就是(一定要理解!):

  • MethodProxy本质上是对一个方法的代理。通过create函数,我们可以创建一个MethodProxy,并且指定被代理类(class1),代理类(class2),被代理方法的描述(desc),被代理方法在被代理类中的名字(name1),被代理方法在代理类中的名字(name2)

    这里对name2的解释可能不是非常准确。对于一个被代理类中的方法(如say()),代理类其实会为他生成两个方法(say()和CGLIB$say$0()):

    final void CGLIB$say$0() {super.say();
    }public final void say() {MethodInterceptor var10000 = this.CGLIB$CALLBACK_0;if (var10000 == null) {CGLIB$BIND_CALLBACKS(this);var10000 = this.CGLIB$CALLBACK_0;}if (var10000 != null) {var10000.intercept(this, CGLIB$say$0$Method, CGLIB$emptyArgs, CGLIB$say$0$Proxy);} else {super.say();}
    }
    

    其中,和被代理类中的方法完全重名的方法是用来给外部调用的,这个方法实际重写了被代理类(也就是父类)的方法。逻辑就是要把函数调用转发到intercept函数中,从而实现对被代理对象的代理!还有一个函数带有CGLIB前缀和标号(CGLIB$say$0()),这个函数逻辑简单,就是直接调用被代理类的函数。我们这里说的name2就是指带有CGLIB前缀和标号的这个函数!

  • invoke(Object obj, Object[] args)函数的语义是用对象obj以参数args调用被代理类中函数描述为desc的name1方法

  • invokeSuper(Object obj, Object[] args)函数的语义是用对象obj以参数args调用代理类中函数描述为desc的name2方法

编写intercept逻辑

经过以上分析,我们已经对intercept的四个参数有了清晰的认识了。那最终的目的当然还是应用啦。所以最后我们来说一下如何编写intercept方法的逻辑。intercept方法的逻辑模板就是:

@Override
public Object intercept(Object o, Method method, Object[] objects, MethodProxy methodProxy) throws Throwable {// 1. pre-function logic// 2. invoke actual function// 3. post-function logic// return return ret;
}

简单来说就是:

  1. 执行函数前的代理逻辑
  2. 调用被代理类的方法,执行函数
  3. 执行函数后的代理逻辑
  4. 将返回值返回

这里,第1,3步根据业务逻辑来写就行,第4步返回也没什么好说的。最关键的在于我们怎么调用被代理类的方法呢(即第2步)?

那这里就要用到我们前面对intercept参数的理解啦。我们可以用method来完成,也可以用methodProxy来完成。下面依次讲解全部可行的方法:

  1. 通过method.invoke(target, objects)

    这里target是我们维护的被代理对象,所以这句话相当于是说在target上用objects作为参数调用函数method,根据我们前面的分析,这个method实际就是Human类中的say方法,所以可以。

  2. 通过methodProxy.invoke(target, objects)

    根据前面的分析,这里的语义为“用target以objects为参数调用被代理类上的函数”,实际就是用target去调用了say,所以没问题。

  3. 通过methodProxy.invokeSuper(o, objects)

    同样的道理,根据前面的分析,这句话相当于是用o调用了CGLIB$say$0方法,也没问题。

虽然三种方式都可以,但还是推荐使用第2,3种方式,因为他们使用了FastClass,可以提升效率。第2种方式要求你在MyMethodInterceptor中维护一个被代理对象的实例target,而第3种方式则没有这个要求。


以上是三种可行的方式,还有三种不可行的方式也需要注意:

  1. method.invoke(o, objects)

    死循环。这句话相当于用o调用say方法,o中的say方法会一直调用intercept方法,intercept方法又调用say方法,从而导致死循环,stack overflow!

  2. methodProxy.invokeSuper(target, objects);

    运行报错!target是被代理类,也就是父类,这句话的语义相当于你要用target去调用CGLIB$say$0方法,而这个方法是在子类中才有的!所以会报错!

  3. methodProxy.invoke(o, objects);

    死循环。这句话引起死循环的原因和1非常类似。相当于你要在代理类上调用被代理类的say方法,所以最终会分配到intercept,那死循环就出现了!say—>intercept—>say---->intercept,最后stack overflow!

好了,到此为止,我们已经分析了所有可能的调用方式以及他们的正确性。实际上,这些方式完全不用背,只要你理解了本文所分析的内容,看到某一种调用方式,你就能分析出他的语义,从而推断出他的正确性。

最后,再放一下完整的测试代码,以下代码可以用于测试所有6种方式。

import net.sf.cglib.core.DebuggingClassWriter;
import net.sf.cglib.proxy.Enhancer;
import net.sf.cglib.proxy.MethodInterceptor;
import net.sf.cglib.proxy.MethodProxy;
import java.lang.reflect.Method;class Human {public void say() {System.out.println("I am super man~~~");}
}class ProxyWithCglib {public static Object newProxy(Object object) {Enhancer enhancer = new Enhancer();enhancer.setSuperclass(object.getClass());MyMethodInterceptor myMethodInterceptor = new MyMethodInterceptor();myMethodInterceptor.bind(object);enhancer.setCallback(myMethodInterceptor);return enhancer.create();}
}class MyMethodInterceptor implements MethodInterceptor {private Object target;public void bind(Object o) {this.target = o;}@Overridepublic Object intercept(Object o, Method method, Object[] objects, MethodProxy methodProxy) throws Throwable {System.out.println("pre-function logic.");//        Object ret = method.invoke(target, objects); // 可以
//        Object ret = method.invoke(o, objects); // 死循环
//        Object ret = methodProxy.invokeSuper(o, objects); // 可以Object ret = methodProxy.invokeSuper(target, objects); // 运行时报错
//        Object ret = methodProxy.invoke(target, objects); // 可以
//        Object ret = methodProxy.invoke(o, objects); // 死循环System.out.println("post-function logic.");return ret;}
}public class Test {public static void main(String[] args) {System.setProperty(DebuggingClassWriter.DEBUG_LOCATION_PROPERTY, "./debug_info");Human humanProxy = (Human) ProxyWithCglib.newProxy(new Human());System.out.println(humanProxy.getClass());humanProxy.say();}
}

写在最后

总结一下,本文旨在通过源码层面的分析来更好的理解CGLIB动态代理中的关键方法intercept,回答了如何在intercept中完成对被代理方法的调用这一绝大多数博客都没有明确说明的问题。其中,对于MethodProxy的理解是本文的关键。最后,本文对所有可能的六种调用方式进行了分析,说明了他们的语义及正确性。希望本文能对你有所帮助。


最后的最后:源码分析类文章不太会写,欢迎批评指正!

CGLIB动态代理之intercept函数刨析相关推荐

  1. 你必须会的 JDK 动态代理和 CGLIB 动态代理

    来自:ytao 我们在阅读一些 Java 框架的源码时,基本上常会看到使用动态代理机制,它可以无感的对既有代码进行方法的增强,使得代码拥有更好的拓展性.通过从静态代理.JDK 动态代理.CGLIB 动 ...

  2. spring框架中JDK和CGLIB动态代理区别

    转载:https://blog.csdn.net/yhl_jxy/article/details/80635012 前言 JDK动态代理实现原理(jdk8):https://blog.csdn.net ...

  3. JDK和CGLIB动态代理区别

    前些天发现了一个巨牛的人工智能学习网站,通俗易懂,风趣幽默,忍不住分享一下给大家.点击跳转到教程. 前言 Github:https://github.com/yihonglei/thinking-in ...

  4. CGLib动态代理原理

    CGLib动态代理原理 CGLib动态代理是代理类去继承目标类,然后重写其中目标类的方法啊,这样也可以保证代理类拥有目标类的同名方法: 看一下CGLib的基本结构,下图所示,代理类去继承目标类,每次调 ...

  5. JDK 动态代理与 CGLIB 动态代理,它俩真的不一样

    摘要:一文带你搞懂JDK 动态代理与 CGLIB 动态代理 本文分享自华为云社区<一文带你搞懂JDK 动态代理与 CGLIB 动态代理>,作者: Code皮皮虾 . 两者有何区别 1.Jd ...

  6. java CGLIB动态代理

    CGLIB动态代理 一:CGLIB(Code Generation Library)是一个基于ASM的字节码生成库,它允许我们在运行时对字节码进行修改和动态生成.CGLIB通过继承方式实现代理. 二: ...

  7. Cglib动态代理实现及原理

    JDK实现动态代理需要实现类通过接口定义业务方法,对于没有接口的类,如何实现动态代理呢,这就需要Cglib了.Cglib采用了非常底层的字节码技术,其原理是通过字节码技术为一个类创建子类,并在子类中采 ...

  8. CGLIB 动态代理用例及源码解析

    CGLIB 动态代理 参考链接:https://blog.csdn.net/yhl_jxy/article/details/80633194 参考链接:https://www.jianshu.com/ ...

  9. 设计模式之代理模式(静态代理、Java动态代理、Cglib动态代理)

    代理模式的定义:由于某些原因需要给某对象提供一个代理以控制对该对象的访问.这时,访问对象不适合或者不能直接引用目标对象,代理对象作为访问对象和目标对象之间的中介. 提醒:动态代理中涉及到以前的一些知识 ...

最新文章

  1. javascript优化_如何通过使用服务人员来优化JavaScript应用
  2. iOS高级教程:处理1000张图片的内存优化
  3. 提交请求输出XML文件的时候出错.解决方法
  4. hdu 5092 线裁剪(纵向连线最小和+输出路径)
  5. Rsync+inotify搭建使用
  6. 【转】shell学习笔记(一)——学习目的性、特殊字符、运算符等
  7. 全国计算机等级考试题库二级C操作题100套(第95套)
  8. Redis 和 memcached 区别(二)
  9. 如何下载有效的Flash离线安装包
  10. PDF虚拟打印机是如何打印文件的
  11. 聚类之详解FCM算法原理及应用
  12. python基础教程视频优酷_Python快速入门视频
  13. HTTP常见状态码及常见错误
  14. Nginx的rewrite地址重写
  15. vue cli js css压缩方案
  16. 解决WPS公式上浮问题
  17. 玩客云安装mysql_玩客云的使用经验总结
  18. 『教师节』程序猿用文心大模型带你一键加速祝福,祝老师们节日快乐
  19. 棋牌游戏开发的风险有哪些?
  20. 《论文写作》心得体会

热门文章

  1. 电商日常数据处理 - 人生苦短,我用Python!
  2. ffmpeg云服务器推流
  3. Jenkins配置git通过http下载资源到节点服务器上
  4. [unity实现在游戏暂停的时候播放动画不受影响]
  5. 每秒处理10万订单的支付架构 乐视集团
  6. PHP设计模式之实体属性值模式(EAV 模式)代码实例大全(35)
  7. 社群营销的3个常见方法,让客户自动成交?
  8. 微信漂流瓶点击屏幕任何地方 title隐藏||显示代码
  9. Linux(Centos-7 64位)的的详细安装及配置和Xshell远程控制
  10. intel各cpu型号标识