第二讲 ClassLoader

1928年,狄拉克(Dirac)写下了电子的相对论量子力学方程即狄拉克方程,算出了有负能量,预言了正电子的存在,进而认为世界有反粒子反物质。大家都觉得荒唐,为什么没有观测到反物质呢?狄拉克说,那是因为反物质无处不在,就像鱼儿在水里面意识不到水的存在,只有跳出水面才会意识到水。这就是后来大家通称的“狄拉克之海”。

我们习以为常的无处不在的事物,会经常被我们所忽略,觉得它们不存在似的,比如空气,只有当雾霾发生的时候或者憋住了没有空气的时候我们才真切感受到空气也是一种真实存在的东西。

以前有学生问我ClassLoader的问题,我说其实你每天编程序都在不断地使用ClassLoader.他很吃惊。我没有瞎说话,真的就是我们每天都有用到。

先看一段类似于憋气的代码,在憋死的过程中感受一下ClassLoader的存在,代码如下:

package com.demo;public class Test2 {public static void main(String[] args) {try {Class.forName("DummyClass");} catch (ClassNotFoundException e) {e.printStackTrace();}}
}

这段程序用到Class.forName(),Class.forName()调用了java.lang.ClassLoader.loadClass(),这个方法用于把类定义加载到运行环境中。

但是我们故意加载一个不存在的类DummyClass来憋死它。运行结果显示:

java.lang.ClassNotFoundException: DummyClassat java.net.URLClassLoader.findClass(Unknown Source)at java.lang.ClassLoader.loadClass(Unknown Source)at sun.misc.Launcher$AppClassLoader.loadClass(Unknown Source)at java.lang.ClassLoader.loadClass(Unknown Source)at java.lang.Class.forName0(Native Method)at java.lang.Class.forName(Unknown Source)at com.demo.Test2.main(Test2.java:7)

从上面的出错线索,我们可以看到ClassLoader使用了AppClassLoader,最后用了URLClassLoader.findClass(),这段出错线索明白地把Class Loader暴露出来了。

其实,我们应用程序员写的每一个Java类都是由ClassLoader加载的,每次运行时都会用到,只是这些由JVM自己控制,我们应用程序感觉不到而已。
那为什么我们还需要了解它呢?一是因为能帮助我们更好地理解Java,二是有些场景下还需要用到它。

好,我们先看定义:Class Loader is an object that is responsible for loading classes。

这个定义有两点要领会,class loader它是用于加载别的类的,并且,并且,并且,重要的事情说三遍,它自己也是一个对象。既然是一个对象,那就可以跟别的普通Java类一样操纵。

为了进一步往下钻研,我们得弄点预备知识,了解一下一个Java程序是如何在JVM里面运行起来的。一句简单的语句 new Test();大体经过下面的步骤:

Step 1:类级别的工作

1.1加载Loading:

加载阶段的工作是,把.class字节流就按照存储在方法区之中,然后在内存中创建一个java.lang.Class类的对象

从硬件里面(文件系统或者网络)把Test.class文件找到,然后放到JVM中的方法区,存放的是类的二进制表示如全名、静态代码、静态变量、方法定义、构造函数说明等等,有的时候我们把类的定义也说成是类的元数据(Meta Data)。

对每个加载的类,JVM会创建一个类对象(class object),对上面的例子,这是一个用于表示Test类的对象,这个对象是存放在堆区的。

注意:类对象是代表类的,所以无论程序员创建了多少个对象实例,调用多少次,JVM中总是只有这一个class object. 到这个时候,类的定义和内存表达出来了,但是并没有开始对象的创建。

1.2链接Linking:

这个阶段执行类的链接过程,给类分配内存。如果内存不够,报出OutOfMemoryError错误。它有下面的三个动作要做:

1.2.1验证Verification: 用于验证.class文件是否合规。按照字节码的规范,.class文件的格式是否正确,就是在这一步完成的。比如,文件的头四个字节都要求是CAFEBABE,这就是Java创造者对之的爱称。

1.2.2准备Preparation: 这个阶段给类里面的静态变量分配内存,赋予默认值。

1.2.3解析Resolution: 利用第一步的方法区,将符号引用转成直接内存引用。

1.3初始化Initialization:

这个阶段完成类加载,把所有静态变量赋初始值,执行静态代码块。实际JVM的实现时,这一部分要很小心,因为考虑多线程的环境,需要进行仔细的同步互锁。

至此,类级别的工作完成。(提醒一点,Java虚拟机规范规定的并不具体,具体要怎么做,不同的虚拟机的实现都不一样。)整个程序的生命周期中,类加载过程只会做一次

这样,一个用文件方式存储的java文件就加载到JVM中,有了关于类结构的内存表示。

这样的一个神奇的好处是,Java可以用任何方式存放程序,不一定是文件,只要修改与ClassLoader相关的代码就可以把程序用另外一种方式存储。在JVM中,只认规定的类内存结构,不用管具体的文件或者文件系统。一个Java程序,一旦所有的类都加载到JVM,就全部自由生活在JVM的美妙虚拟空间了。

Step 2:对象级别的工作

经过第一步,我们的类就准备好了,对象有了模子。创建对象(也叫类的一个实例)的事情就简单了。

2.1 为对象在堆中分配内存,注意的是,实例字段包括自身定义的和从父类继承下来的。

2.2 对实例内存进行零值初始化。

2.3 调用对象的构造函数。

好了,我们现在在JVM中创建好了对象。好简单。是的,有了女娲,造人就简单了。

扩展一下讨论,我们还可以写一个程序,看看程序执行顺序。

先准备一个Stub类,只是一个简单的对象:

public class Stub {public Stub(String s) {System.out.println(s + " object created.");}
}

再写一个Parent类,里面写几种不同的语句,我们看执行的先后顺序:

public class Parent {static Stub parentStaticObject = new Stub("Parent static object - ");static {System.out.println("parent static code execute.");}{System.out.println("parent code execute.");}Stub parentObject = new Stub("Parent code create object - ");Stub stub;public Parent() {System.out.println("parent constructor execute.");stub = new Stub("Parent constructor create object.");}public void sayHello() {System.out.println("hello from Parent");}
}

里面的语句有静态代码块,静态变量初始化,非静态代码块,实例变量初始化,构造函数和普通方法。

再写一个子类继承:

import javax.rmi.CORBA.Stub;public class Child extends Parent {static Stub parentStaticObject = new Stub("Child static object - ");static {System.out.println("Child static code execute.");}{System.out.println("Child code execute.");}Stub parentObject = new Stub("Child code create object - ");Stub stub;public Child() {System.out.println("child constructor execute.");stub = new Stub("Child constructor create object.");}public void sayHello() {System.out.println("hello from Child.");}
}

同样的,里面的语句有静态代码块,静态变量初始化,非静态代码块,实例变量初始化,构造函数和普通方法,并且覆盖了父类的方法。

最后用一个测试程序看看输出结果:

import javax.rmi.CORBA.Stub;public class StartTest {static {System.out.println("Tester static code execute.");}static Stub testerStaticObject = new Stub("Tester static object - ");{System.out.println("Tester code execute.");}Stub testerObject = new Stub("Tester code create object - ");public static void main(String[] args) {System.out.println("main() execute.");Child c = new Child();c.sayHello();((Parent) c).sayHello();}
}

运行一下,结果如下:

Tester static code execute.
Tester static object -  object created.
Parent static object -  object created.
parent static code execute.
Child static object -  object created.
Child static code execute.
main() execute.
parent code execute.
Parent code create object -  object created.
parent constructor execute.
Parent constructor create object. object created.
Child code execute.
Child code create object -  object created.
child constructor execute.
Child constructor create object. object created.
hello from Child.
hello from Child.

从这个执行顺序结果得出我们的结论:

  1. 静态代码最先执行,主程序静态代码 -> 父类静态代码 -> 子类静态代码。
  2. 执行主程序main()
  3. 父类非静态代码 -> 父类实例队形初始化 -> 父类构造函数
  4. 子类非静态代码 -> 子类实例队形初始化 -> 子类构造函数
  5. 普通方法

还有一个小地方要注意,子类覆盖的方法调用,无论是c.sayHello(); 还是((Parent)c).sayHello(); 结果都是调用的子类中的方法。这是面向对象编程中多态的体现。

有了上面的预备知识,接下来我们继续往前走。

在Java中,有三种Class Loader存在,应用层的,扩展层的,平台核心层的。

我们通过一个例子来了解,代码如下:

public class Test1 {public static void main(String[] args) {ClassLoader classLoader = Test1.class.getClassLoader();System.out.println(classLoader);ClassLoader classLoader1 = classLoader.getParent();System.out.println(classLoader1);ClassLoader classLoader2 = classLoader1.getParent();System.out.println(classLoader2);}
}

运行之后的结果是:

sun.misc.Launcher$AppClassLoader@73d16e93
sun.misc.Launcher$ExtClassLoader@15db9742
null

从这个结果可以看出,一个简单的运行都会涉及到三个class loader,application class loader,extension class loader,和bootstrap class loader(没有显示出来,因为bootstrap class loader是native代码,所以不在JVM中,于是返回的是null)。

Application class loader 加载我们自己写的类;

Extension class loaders 加载Java核心类的扩展部分,$JRE_HOME/lib/ext 目录下的类;

Bootstrap class loader加载Java平台核心类诸如 java.lang.Object, java.lang.Thread etc)以及rt.jar中的类。

注:上面的返回结果不是确定的,因为这不是语言本身规定的,不同的JVM厂商有不同的实现。我给的例子用到的是Sun公司提供的虚拟机。

几个类加载器之间是有层次关系的,称为Delegation Model

A ClassLoader instance will delegate the search of the class or resource to the parent class loader。

  1. 一个类加载器自己先不加载,而是交给它的Parent去处理,
  2. Parent又交给它的Parent去处理,
  3. 一层层委托上去一直到Bootstrap Class Loader,
  4. 如果Parent发现自己加载不了这个的class,
  5. 才会反交给下层加载。

一般情况下是这样的次序,

  1. 先是Application Class Loader出动加载客户程序,它自己不做,
  2. 交给上层的Extension Class Loader,
  3. 再交给Bootstrap Class Loader,
  4. 之后方向反过来,Bootstrap Class Loader发现不能加载,
  5. 就返给Extension Class Loader,
  6. 如果还是加载不了,最后再返给Application Class Loader

可能大家都听晕了。为什么Java采取这样一种初看起来莫名其妙的方式进行类加载呢?主要的原因在于安全性。我们想象如下一个场景:

JVM起来的时候,有一些系统级的最基本的类就会运作,这是虚机的核心。

大多数在rt.jar里面,像Java语言本身的基本类型,java.lang包里面的东西。

这是Java的基础,别的客户程序都是依赖这个核心的。但是没有一条规则去阻止你自己写同样的类,下面我们就非要写这么一个类:

package java.lang;public class Integer {public Integer() {}public static void main(String[] args) {System.out.println("This is a fake Integer class. Hah Hah!");}
}

这是一个捣乱的类。包名和类名跟Java平台的一样。build是不会有问题的,没有语法规则阻止这样的事情。但是在运行的时候,我们希望出现提示:This is a fake Integer class. Hah Hah!但是实际上出现的却是:在类 java.lang.Integer 中找不到 main 方法

这就奇怪了,我们明明写了main()方法的。

这个时候,其实就是ClassLoader的委托机制在起作用,一层层委托到Bootstrap Class Loader的时候,它发现自己能加载(因为Bootstrap Class Loader负责加载rt.jar里面包含的所有类,java.lang.Integer这个名字在这些类里面),就加载它,事实上就是加载了rt.jar里 java.lang包之中的那个Java基础类Integer。我们自己写的这个类并没有加载,所以才会在运行时发现并没有main()方法

通过这个方式,Java做到了让自己系统的那些基础的类安全加载进来,而不会被别的程序偷梁换柱。保证Java虚拟宇宙的正常秩序,这样得以维持体系的安全。如果这些系统级别的基础类都被人替换掉了,整个JVM就失控了,大厦将倾。

了解了这些之后,我们就能理解它背后的设计者的匠心。

赞曰:虚机岂无缝?匠心独运成。

另外注意一点,这里类加载器之间的层次关系不是继承的关系来实现的,是通过组合关系(Composite)实现的。概念上,继承是is a,组合是as a。

编程实践中,Bruce Eckel(Java名著《Thinking in Java》作者)说过:能不用继承就不用继承,尽量优先使用组合。

这里引来了一个有趣的问题,ClassLoader本身也是一个类,它也需要被加载,那最早的那个怎么开始的呢?

Java设计了一个根加载器Bootstrap class loader,他是JVM核心的一部分,不是Java编写的,因为不是一个Java对象,我们的Java程序里面也看不到它。这个根加载器就是Java虚机的第一推动。

对Java程序来讲,JVM就是整个宇宙,Bootstrap Class Loader把原初对象放置到了宇宙的舞台,而James Gosling就是创造宇宙的上帝。
一切动都有因,难怪牛顿相信宇宙有第一推动。

好,介绍了这些。我们来看看怎么写自己的Class Loader。

首先要明白一个问题:为什么要写自己的Class Loader?不管那么多不是一样好好在用吗?

一般情况确实是这样,不过有些时候为了一些特殊需求,我们会用到自己定制的Class Loader。

比如1998年,Sun内部为完成JDK1.2忙得热火朝天,我也在里面打酱油,我们有一个小组提供一个工具,给Java生成的字节码加密,原因是字节码太规整,用一些工具很容易反编译,反编译之后的结果很容易供人看懂(比许多程序员手工编的程序还容易懂),导致知识产权保护不力。
于是就想着把.class文件加密,但是一个加密之后的.class文件肯定又不能被正确加载。怎么办呢?就要自己做一个Class Loader,拿到.class文件,先进行一步解密,解密之后就成了正常的字节码,就可以用普通的application class loader的方式去继续加载了

还有一些别的场景,也需要我们自定义Class Loader,如动态生成的class文件,如多版本class运行。

我们先来看,从哪里下手做这个工作。

从大的过程,对象的创建分成两大步骤,一个是类级别的工作,一个是对象的实例化。显然,我们要在类级别工作着一个步骤动脑筋。而在类级别的工作中,分成加载Loading,链接Linking,初始化Initialization三步。根据上面讲解的一些知识,我们应该在Loading过程中搞一点名堂。

回到ClassLoader的定义,我们来看提供了哪些方法。

public Class<?> loadClass(String name, boolean resolve) throws ClassNotFoundException

这个loadClass()方法根据类的全名加载类。既然是这样,那就应该从这里下手了。

我们看看源码

protected Class<?> loadClass(String name, boolean resolve)  throws ClassNotFoundException {
synchronized (getClassLoadingLock(name)) {// First, check if the class has already been loadedClass<?> c = findLoadedClass(name);if (c == null) {long t0 = System.nanoTime();try {if (parent != null) {c = parent.loadClass(name, false);} else {c = findBootstrapClassOrNull(name);}} catch (ClassNotFoundException e) {// ClassNotFoundException thrown if class not found// from the non-null parent class loader}if (c == null) {// If still not found, then invoke findClass in order// to find the class.c = findClass(name);}}if (resolve) {resolveClass(c);}return c;
}
} 

我们可以看到这个过程,先看这个类是不是已经加载了,如果没有就让上层的class loader去加载,最后由自己加载findClass(name)。

事情就追到findClass(name)方法了。看它的定义:

protected Class<?> findClass(String name) throws ClassNotFoundException

在ClassLoader里面,它只是一个空的方法,没有做实现。那么我们就可以实现它来进行我们自己的工作了。

好,我们在findClass()里面做什么呢?

自然先要从外部如文件系统或者网络获取.class字节流,然后呢?我们自己把它弄成类模型吗?理论上是,但是实际上我们不需要。接着找ClassLoader为我们提供了什么,我们可以看到有一个defineClass()方法,定义如下:

protected final Class<?> defineClass(  String name, byte[] b, int off, int len) throws ClassFormatError

这个方法就是用于将字节流转成类的。

并且这个方法是final的,我们用它并且只能用它。

有了这个方法,我们自己的工作就简单了,只需要对字节流进行处理后交给defineClass()就好了。

我们可以动手编程序了。

先做一个类,MyClass.java,代码如下:

package com.demo;public class MyClass {public MyClass() {}public void show() {System.out.println("show test!");}
}

很简单,不解释了。

接着我们写自己的class loader,为了简单起见,我们的这个class loader不进行任何加密处理,只是简单地读取.class文件,生成类,模仿标准的AppClassLoader的行为。MyClassLoader.java代码如下:

package com.demo;import java.io.IOException;
import java.net.URISyntaxException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;public class MyClassloader extends ClassLoader {@Overrideprotected Class<?> findClass(String name) throws ClassNotFoundException {String classFileName = name.replace(".", "/") + ".class";byte[] cLassBytes = null;Path path = null;try {path = Paths.get(getResource(classFileName).toURI());cLassBytes = Files.readAllBytes(path);} catch (IOException | URISyntaxException e) {e.printStackTrace();}Class<?> clazz = defineClass(name, cLassBytes, 0, cLassBytes.length);return clazz;}
}

正如上面根据原理分析的,我们自己写的class loader override了findClass(),

这里面我们做了几步:

一是根据class名字拼出文件名,

String classFileName = name.replace(".","/") + ".class";

二是根据名字定位到文件路径,

path = Paths.get(getResource(classFileName).toURI());

三是把文件读到字节数组中,

cLassBytes = Files.readAllBytes(path);

四是调用defineClass()生成类,

Class<?> clazz = defineClass(name, cLassBytes, 0, cLassBytes.length);

有了这个自定义的class loader之后,我们写一个测试类,MyClassTest.java,代码如下:

package com.demo;import java.lang.reflect.Method;public class MyClassTest {public static void main(String[] args) throws ClassNotFoundException {MyClassloader loader = new MyClassloader();Class<?> aClass = loader.findClass("com.demo.MyClass");try {Object obj = aClass.newInstance();Method method = aClass.getMethod("show");method.invoke(obj);} catch (Exception e) {e.printStackTrace();}}
}

在程序中,我们新建一个自定义的类加载器MyClassloader loader = new MyClassloader();

然后调用findClass()去加载类Class<?> aClass = loader.findClass("com.demo.MyClass");

之后根据类定义创建新的对象实例并调用方法

        Object obj = aClass.newInstance();Method method = aClass.getMethod("show");method.invoke(obj);

试着运行一下,程序正确出了结果。

到了这一步,恭喜你!你已经知道如何做Class Loader了,We Made It!

再回到加密的问题,这一讲不讲加密本身,但是我们要根据上面的代码知道如何处理加密。看我们自己的findClass()的实现,里面有两句话:

cLassBytes = Files.readAllBytes(path);
Class<?> clazz = defineClass(name, cLassBytes, 0, cLassBytes.length);

在defineClass之前,我们要准备出一个正常的字节数组,因此对于一个加密的.class文件,我们只需要在之前处理进行解密即可:

cLassBytes = decrypt(Files.readAllBytes(path));
Class<?> clazz = defineClass(name, cLassBytes, 0, cLassBytes.length);

假定有一个decrypt()方法,我们就可以了做到了。

以前在Sun公司的时候,一个同事曾经说过class loader就像是一个汤勺,用它来抓汤圆吃。这个比喻真有点像。

不过这个比喻不够,因为class loader不光是加载类,还规定了命名空间,不同的class loader加载的类是不能互相访问的(正因为这样才会有同一个类的多版本支持的技术)。

我们可以把class loader圈起来的这片空间理解为class园地。因此,class loader更加像是一个碗,把汤圆盛到里头了。

这个话题超越了进阶阶段,先按下不表。不过到了这里,我们应该能感受到Class Loader的强大了,从心底里对Java的发明人一次次投以敬佩的目光,他们二十几年前竟然有如此深刻的洞察。对我们Java程序员来讲,James Gosling就是我们的上帝。

在未来的讲座里,我还会跟大家一次次展示出造物者的匠心独运。

学习的过程,是一步步深入的。

很多人工作多年,并没有深入,有些时候也不是因为不努力,而是因为他们在的编程任务总是在无休止地对付客户需求的变更,没有坐下来好好整理。

而从应用的层面,学习技术就成了追逐新的热点,不断地换最新推出的环境框架工具。

而写应用程序,大体上用不到深入的内容,非专业的人突击学几个月之后也能一起做编程了,有的时候比本专业的人做得还要好。那么学习本专业的价值在哪里?这是许多年轻人曾经的疑惑。

或许与国家的发展阶段有关,前些年都是拿别人现成的平台框架工具,或开源或盗版,自己针对客户需要快速搭建应用系统,赚快钱,现在慢慢认识到基础的重要性了,真正愿意深入理解技术的人和公司也多起来了。这对于希望掌握和使用更深技术的程序员是一件好事情。

从事技术工作是一个良心活儿,学习技术则是一个慢工夫,急不来,就得要老老实实一个课题一个课题解决掉。只要方向对,扎扎实实,总能有成。

先贤语录:不积跬步,无以至千里。

Java语言十五讲——第二讲 ClassLoader相关推荐

  1. Java语言十五讲(前言)

    特此声明:本文为本人公司郭总原创书籍的前言,该书已出版,传送门 ->>>>Java编程十五讲 本人微信公众号内已更新完成.喜欢学习的小伙伴可以搜索微信公众号:程序员Hotel ...

  2. Java语言十五讲——前言

    前言 每年技术媒体都会评选最受欢迎的编程语言,Java总是高居前位.自然,没有办法说一个语言绝对比另一种语言好,这个话题一如既往地会引起大家无谓的争论不休.对别的行业的人来讲也许会觉得莫名其妙,但是对 ...

  3. Java语言十五讲(第十二讲 Multi-Thread多线程12.2)

    实例变量如balance在线程间是共享的.有的时候,我们真的需要线程级别的变量,不希望共享,也是有办法的.Java里面有ThreadLocal变量. 比如,我们的线程从inventory里面拿东西,上 ...

  4. java语言程序设计勇_自考Java语言程序设计(一)串讲笔记

    自考Java语言程序设计(一)串讲笔记.txt43风帆,不挂在桅杆上,是一块无用的布:桅杆,不挂上风帆,是一根平常的柱:理想,不付诸行动是虚无缥缈的雾:行动,而没有理想,是徒走没有尽头的路.44成功的 ...

  5. java生日正则表达式_Java语言十五讲

    前言 每年技术媒体都会评选最受欢迎的编程语言,Java总是高居前位.自然,没有办法说一个语言绝对比另一种语言好,这个话题一如既往地会引起大家无谓的争论不休.对别的行业的人来讲也许会觉得莫名其妙,但是对 ...

  6. c语言如何打印矩形图形的程序 五行七列,C语言程序计 第二讲.printf打印图形.转义字符.格式声明符.doc...

    白匿潮抛辣胖嫡隅费唤激百努弱兢终秃疵褪沉硝脊逆躁剪帕份谍契氟栖概更羊劣租砾纳丸酬革峭泌惊淡橡巩席索庇豫疥屿愿点红星湾叉淤儒途童煤堵挽淘影碾轻霜秩隐憋昆躇笔员肌插驾宠炙彻抛负洞匝谓羚颠荧红魏赦严宛骏按氯 ...

  7. Java语言十大特性

    作者简介:笔名seaboat,擅长工程算法.人工智能算法.自然语言处理.计算机视觉.架构.分布式.高并发.大数据和搜索引擎等方面的技术,大多数编程语言都会使用,但更擅长Java.Python和C++. ...

  8. java语言程序设计在线作业_中石油北京2018秋 《Java语言程序设计》第二次在线作业...

    1   老虎奥鹏 www.aopengzuoye.com 1 对象使用时,下面描述错误的是 A.通过"."运算符调用成员变量和方法 B.通过成员变量的访问权限设定限制自身对这些变量 ...

  9. c语言程序设计在哪讲,《C语言程序设计》讲.doc

    <C语言程序设计>讲 <C语言程序设计>讲稿 目 录 第一讲 C语言概述1 第二讲 C语言程序介绍2 第三讲 算法8 第四讲 数据类型(1)20 第五讲 数据类型(2)21 第 ...

  10. Python超越Java语言,跃居世界编程语言第2位了!你却还在犹豫学不学Python?

    一.前言 C.Java.Python作为常据世界编程语言排行榜前三甲的语言,必然有其得天独厚的优势.以下是2021年5月最新的高级编程语言排行榜,可以看到,Python已经超越Java语言跃居世界第二 ...

最新文章

  1. 关于p标签的嵌套问题
  2. BCH热门应用SLP发币系统逐渐走向成熟
  3. 有向图——强连通分量
  4. Javascript中的0,false,null,undefined,空字符串对比
  5. Linux下CMake简明教程(二)同一目录下多个源文件
  6. wampserver3.0.6 外网 不能访问
  7. 史上超全halcon常见3D算子汇总(一)
  8. VS Code编译Python
  9. Perl用LWP实现GET/POST数据发送
  10. Backbone学习日记第二集——Model
  11. jdbc获取数据库元数据,获取数据库列表,获取数据库基本信息,获取指定数据库中的表信息,获取指定表中的字段信息
  12. Linux操作系统下6个应急处理小常识
  13. 自动刷新wu2198股市直播内容
  14. 唐宇迪pytorch课程全部代码数据集github
  15. 安卓ADB和Fastboot最新官方下载链接
  16. 网易云音乐自动获取前三首歌曲名称
  17. unity实战:教你做黄豆君
  18. Skynet服务器框架(八) 任务和消息调度机制
  19. Jenkins的分布式构建及部署(master~slaver)
  20. mac(苹果)电脑终端使用技巧

热门文章

  1. python预测你的小孩身高_儿童身高预测方法
  2. C++ 多态性 (polymorphism)
  3. 【总结整理】关于挪车和虚拟号的思考-转载v2ex
  4. Android 通过bmob十分钟实现即时通讯
  5. 艾永亮:疯传的秘密,一个手表如何一夜之间席卷全校?(下)
  6. antd menu 样式修改
  7. ionic ion-refresher 下拉刷新的使用。
  8. Unity拼图小游戏
  9. 前端职业规划 - 写给年轻的前端韭菜们
  10. VVC系列(三)xCompressCTU、xCompressCU和xCheckModeSplit解析