参考文章

oracle 官方文档
Class Loaders In Java - Baeldung
Demystifying class loading problems
浅谈双亲委派模型
tomcat classloader violates delegating policy
What is JAR Hell?
Spring Boot Classloader and Class Overriding

致命的翻译(双亲委派模型)

JVM 类加载机制也是一道常见的暖场题, 令人感到厌烦的是, 这个类加载机制的翻译就和“套接字”一样令人感到窒息。

大部分的计算机英文术语在命名时, 都会尽可能做到直白易懂, 体现技术概念的本质。 但是中文翻译中往往因为翻译者水平有限,导致这种信息的丢失, 使得原本直白的概念变得晦涩难懂,容易误解。

双亲委派模型 就是一个典型的例子。

大部分程序员第一眼看到这个术语, 脑子中必定会浮现这样一种画面:

上面这个第一印象, 再加上百度 “双亲委派模型” 最常见的如下配图, 基本上就足以误导 80 % 80\% 80% 的读者

  • 误解1: 双亲是指最下面的那两个UserClassLoader。
  • 误解2: 双亲是指两层父结点。

正确的翻译(委派模型 或 父委派模型)

oracle 官方文档关于 jvm 类加载机制所用的描述是:

The Java platform uses a delegation model for loading classes. The basic idea is that every class loader has a "parent" class loader. When loading a class, a class loader first "delegates" the search for the class to its parent class loader before attempting to find the class itself.

翻译过来就是:

java 平台使用 委派模型来加载类。 基本思想就是, 每一个类加载器都有一个父加载器, 当需要加载一个 class
时, 首先把该 class 的查询和加载优先委派给父加载器进行, 如果父加载器无法加载该 class, 再去尝试自行加载这个 class

委派模型的代码实现

下面是 jdk 中 java.lang.ClassLoaderloadClass 方法具体代码逻辑, 较为清晰的展现了父加载模型的逻辑。

父委派模型

找到
未找到
不为空
为空
未成功加载
查找是父引用 parent 是否为空
parent.loadClass(name)
用 parent 去加载
调用 findBootstrapClassOrNull(name)
其实就是用 BootStrapClassLoader 去加载
调用 findClass( myClassName )
寻找待加载的 class 物理文件,并解析加载为 class 予以返回
loadClass( myClassName )
loadClass( myClassName,false)
findLoadedClass( myClassName)
寻找是否已经加载过该类
直接返回
该名称的类

下面是源码摘录 java.lang.ClassLoader.java( jdk1.8.0_101)

public Class<?> loadClass(String name) throws ClassNotFoundException {return loadClass(name, false);
}protected Class<?> loadClass(String name, boolean resolve)throws ClassNotFoundException{synchronized (getClassLoadingLock(name)) {// First, check if the class has already been loadCedClass<?> 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.long t1 = System.nanoTime();c = findClass(name);// this is the defining class loader; record the statssun.misc.PerfCounter.getParentDelegationTime().addTime(t1 - t0);sun.misc.PerfCounter.getFindClassTime().addElapsedTimeFrom(t1);sun.misc.PerfCounter.getFindClasses().increment();}}if (resolve) {resolveClass(c);}return c;}}

类加载过程的细分流程

细心的同学应该会有疑问, 加载一个类时, 发现该类还继承其他的类,或者方法定义中用到了别的类作为参数或者返回值, 会发生什么。

这就涉及到了类的具体加载过程, 如下图, 类的加载过程被从左到右划分为 3 大阶段:

  • 装载 (Loading)

    • 该阶段负责找到待加载类的二进制 class 文件, 并把它以 bytecode 的形式装载到虚拟机。 在这个过程中, JVM 会给这个类分配一个基本的内存结构, 但是方法, 变量域, 和它引用到的其他类在这个阶段都还没有处理, 也就是说, 这个类在目前阶段还不可用
  • 链接 (Linking)
    • 这个步骤又可细分为3个阶段
    • 字节码验证
      • 验证字节码是否是一个正确,符合规范的类字节码
    • 类准备
      • 为这个类定义好必须的数据结构以表示成员变量域, 方法, 以及实现的接口等等
    • 解析
      • 把这个类锁引用的其他类全部加载进来 , 引用的方式有如下几种:

        • 继承
        • 实现接口
        • 域变量
        • 方法定义
        • 方法中定义的本地变量
  • 初始化(Initializing)
    • 执行类中定义的静态代码块, 初始化静态变量为默认值

隐式加载 vs 显示加载

从上文类加载的详细过程可以看出, 类有两种方式被加载进来

  • 显式加载

    • 程序主动调用下列类型的方法去主动加载一个类

      • classloader.loadClass( className)
      • Class.forName( className)
  • 隐式加载
    • 被显式加载的类对其他类可能存在如下引用:

      • 继承
      • 实现接口
      • 域变量
      • 方法定义
      • 方法中定义的本地变量
    • 被引用的类会被动地一并加载至虚拟机, 这种加载方式属于隐式加载

不要把父加载器误解为父类

看过 ClassLoader 的源码以后, 会意识到所谓的父加载器, 只是一个简单的成员变量引用 parent, 该引用在构造 ClassLoader 时, 由外部传递

正如先前所展示的, jdk 默认提供了三类内建的类加载器。


下面代码的输出了不同类的加载器

public void printClassLoaders() throws ClassNotFoundException {System.out.println("Classloader of this class:"+ PrintClassLoader.class.getClassLoader());System.out.println("Classloader of Logging:"+ Logging.class.getClassLoader());System.out.println("Classloader of ArrayList:"+ ArrayList.class.getClassLoader());
}

输出内容:

Class loader of this class:sun.misc.Launcher$AppClassLoader@18b4aac2
Class loader of Logging:sun.misc.Launcher$ExtClassLoader@3caeaf62
Class loader of ArrayList:null

上面输出了三种结果 AppClassLoader, ExtClassLoader, null(其实是 BootStrapClassLoader)

  • AppClassLoader 负责加载 classpath 下的文件
  • ExtClassLoader 负责加载 java 核心类的扩展类, 通常是搜索 $JAVA_HOME/lib/ext 中的文件或是任意定义在 java.ext.dirs 属性中的文件夹下的文件予以加载
  • BootStrapClassLoader 负责加载 java 核心类, 例如 ArrayList.

但是, 我们看到, ArrayList 加载类的输出内容为 null , 这是因为 BootStrapClassLoader 是用平台原生语言( 可能是 C,C++ 或其他平台相关语言) , 而 getClassLoader() 返回的是 java 类, 所以这项输出只能为空

如何看出三种内置加载器的父子关系

AppClassLoader, ExtClassLoader 是由 sun.misc.Launcher 初始化的, 查看源码中的构造方法可以发现

源码为 IDE 反编译获得, 所以变量名可读性较弱, 但不影响理解

public Launcher() {Launcher.ExtClassLoader var1;try {// var1 是 ExtClassLoader 引用变量var1 = Launcher.ExtClassLoader.getExtClassLoader();} catch (IOException var10) {throw new InternalError("Could not create extension class loader", var10);}try {// var1 作为入参, 传入了 getAppClassLoader this.loader = Launcher.AppClassLoader.getAppClassLoader(var1);  } catch (IOException var9) {throw new InternalError("Could not create application class loader", var9);}Thread.currentThread().setContextClassLoader(this.loader);// ... 省略部分代码

沿着 getAppClassLoader() 方法, 最后可以追踪到 ClassLoader 的构造方法中, 可以看到 getAppClassLoader(var1) 中传入的参数 var1 最终被保存在 parent 成员变量中

private ClassLoader(Void unused, ClassLoader parent) {this.parent = parent;// 省略部分代码}

综上, 可以看到 AppClassLoaderExtClassLoader 的父子关系由 Launcher 保证

BootStrapClassLoader 是如何成为 ExtClassLoader 的父加载器呢?

其实上文提到过的 ClassLoader 的源码逻辑提供了答案

父委派模型

不为空
为空
未成功加载
查找是父引用 parent 是否为空
parent.loadClass(name)
用 parent 去加载
调用 findBootstrapClassOrNull(name)
其实就是用 BootStrapClassLoader 去加载
调用 findClass( myClassName )
寻找待加载的 class 物理文件,并解析加载为 class 予以返回

注意到, 在加载类的过程中, 找不到 parent 的时候, 会首先调用 findBootStrapClassOrNull(name) 去尝试返回由 BootStrapClassLoader 加载的 java 核心类。 这种机制便保证了 bootStrapClassLoader 是所有 ClassLoader 的父加载器

为什么使用父委派模型

原因一: 层级关系

使用了父委派模型后, **类随着它的类加载器, 一起具备了一种层级关系

如果将父加载器的层级视为更高层级的加载器(如上图所示), 那么由于父加载器总是拥有优先加载一个类的机会, 那么当不同的 child class loader 试图加载一个属于更高层级的parent class loader 加载范围的 class 时, 该请求总会被转发给对应的最高层级的父加载器, 返回一致的结果。

例如应用层级 classpath 中的代码, 是由 AppClassLoader 负责加载的, 但是如果有懵懂或邪恶的程序员定义了与 jdk 中的核心类同名的类, 如 sun.applet.Main 会发生什么呢, 是否会导致项目里面其他使用了这个sun.applet.Main 错误访问到这个由程序员自行定义的类,导致行为异常呢

答案是不会

可以尝试运行下面自定义的这个类的 main 方法

package sun.applet;public class Main {public Main() {}public static void main(String[] args) {System.out.println("this sun.applet.Main class defined by an ignorant programmer");}
}

得到输出

用法: appletviewer <options> url其中, <options> 包括:-debug                  在 Java 调试器中启动小应用程序查看器-encoding <encoding>    指定 HTML 文件使用的字符编码-J<runtime flag>        将参数传递到 java 解释器-J 选项是非标准选项, 如有更改, 恕不另行通知。

这说明 jvm 并没有加载运行我们自行定义的 sun.applet.Main , 这也是父委派模型的好处, 当 AppClassLoader 试图加载我们自行定义的 sun.applet.Main 时, 最终将这个请求委派给了 Bootstrap Class Loader, 并执行了 jdk 中所定义的 sun.applet.Main 类的 main 方法。

原因二: 类可见性

使用了父委派模型的另一个影响是, 一个类加载器只能看到由他自己或是由其父辈加载的类, 它自己是看不到更低层级加载器所负责加载的类。

例如, 如果父加载器(ExtClassLoader)需要加载的类 $JAVA_HOME/jre/ext/xx.jar#Class A 引用了存在于更低层级加载器AppClassLoader负责范围($class_path)中才存在的类, 那么在加载过程就会报错。

当这种需求出现的时候, 可以使用 JDK 提供的另一种类加载器 ContextClassLoader 予以解决, 这里不做展开描述, 有兴趣的同学请自行查阅资料

如何自定义符合父委派模型的类加载器

  • 首先, 自定义 CustomClassLoader 继承自 ClassLoader
public class CustomClassLoader extends ClassLoader {@Overridepublic Class findClass(String name) throws ClassNotFoundException {byte[] b = loadClassFromFile(name);return defineClass(name, b, 0, b.length);}private byte[] loadClassFromFile(String fileName)  {InputStream inputStream = getClass().getClassLoader().getResourceAsStream(fileName.replace('.', File.separatorChar) + ".class");byte[] buffer;ByteArrayOutputStream byteStream = new ByteArrayOutputStream();int nextValue = 0;try {while ( (nextValue = inputStream.read()) != -1 ) {byteStream.write(nextValue);}} catch (IOException e) {e.printStackTrace();}buffer = byteStream.toByteArray();return buffer;}
}

注意到我们重写了 ClassLoader 中的 findClass(String name) 方法, 里面自行实现了读取 class 文件为 byte 数组, 调用 defineClass 方法将 byte 数组解析加载为类。

由于我们并未重写 loadClass(String name) 方法 , 所以 CustomClassLoader 依旧会遵从 loadClass(String name) 中定义的父委派模型加载方法。

如何自定义一个违背父加载模型的类加载器

以为我们之前自行定义的 sun.applet.Main 为例, 如果我们就是想让这个自定义的类加载到 JVM 中, 并得以执行自定义 main 方法, 该如何自定义一个类加载器完成该操作?

package sun.applet;public class Main {public Main() {}public static void main(String[] args) {System.out.println("this sun.applet.Main class defined by an ignorant programmer");}
}

把 ide 编译出的 Main.class 文件放到 ./out/production/classes/sun/applet/ 目录下

然后自定义类加载器如下

public class UnDelegationClassLoader extends ClassLoader {private String classpath;public UnDelegationClassLoader(String classpath) {super(null);this.classpath = classpath;}@Overridepublic Class<?> loadClass(String name) throws ClassNotFoundException {Class<?> clz = findLoadedClass(name);if (clz != null) {return clz;}    // jdk 目前对"java."开头的包增加了权限保护,这些包我们仍然交给 jdk 加载if (name.startsWith("java.")) {return ClassLoader.getSystemClassLoader().loadClass(name);}return findClass(name);}@Overrideprotected Class<?> findClass(String name) throws ClassNotFoundException {InputStream is = null;try {String classFilePath = this.classpath + name.replace(".", "/") + ".class";is = new FileInputStream(classFilePath);byte[] buf = new byte[is.available()];is.read(buf);return defineClass(name, buf, 0, buf.length);} catch (IOException e) {throw new ClassNotFoundException(name);} finally {if (is != null) {try {is.close();} catch (IOException e) {throw new IOError(e);}}}}public static void main(String[] args)throws ClassNotFoundException, IllegalAccessException, InstantiationException,MalformedURLException, FileNotFoundException, NoSuchMethodException, InvocationTargetException {sun.applet.Main main1 = new sun.applet.Main();FileInputStream file = new FileInputStream("./out/production/classes/sun/applet/Main.class");UnDelegationClassLoader cl = new UnDelegationClassLoader("./out/production/classes/");String name = "sun.applet.Main";Class<?> clz = cl.loadClass(name);Object main2 = clz.newInstance();Method mainMehthod = clz.getMethod("main",String[].class);String params[] = null;mainMehthod.invoke(null,(Object)params);System.out.println("main1 class: " + main1.getClass());System.out.println("main2 class: " + main2.getClass());System.out.println("main1 classloader: " + main1.getClass().getClassLoader());System.out.println("main2 classloader: " + main2.getClass().getClassLoader());System.out.println( );}
}

输出:

this sun.applet.Main class defined by an ignorant programmer
main1 class: class sun.applet.Main
main2 class: class sun.applet.Main
main1 classloader: null
main2 classloader: sun.applet.UnDelegationClassLoader@36baf30c

注意到为了打破父委派模型, 我们重写 loadClass(String name) 方法, 在该方法中, java. 开头的类, 我们还是调用 jdk 提供的加载器去加载。因为这些核心类 jdk 做了权限保护, 如果直接尝试加载一个自定义的 java. 开头的核心类, 例如 java.lang.Object 的话, 在执行 defineClass 时会得到如下报错

Exception in thread "main" java.lang.SecurityException: Prohibited package name: java.langat java.lang.ClassLoader.preDefineClass(ClassLoader.java:662)at java.lang.ClassLoader.defineClass(ClassLoader.java:761)at java.lang.ClassLoader.defineClass(ClassLoader.java:642)at sun.applet.UnDelegationClassLoader.findClass(UnDelegationClassLoader.java:36)at sun.applet.UnDelegationClassLoader.loadClass(UnDelegationClassLoader.java:24)at sun.applet.UnDelegationClassLoader.main(UnDelegationClassLoader.java:58)

这是 jdk 对于 java. 开头的类加载的一种权限保护, 确保用户没办法错误或恶意的加载自定义的核心 java 类 。

哪些场景下需要违背父委派模型

目前有不少框架都会自行实现 classLoader 满足一些特定需求, 其中就有一些框架会在一定程度上违背父加载模型, 例如 Tomcat, JNDI、OSGi .

这里分析一下 Tomcat 什么如何违背父委派模型, 以及为什么违背

首先 tomcat 文档中描述了其自定义的类加载器层级关系:

When Tomcat is started, it creates a set of class loaders that are organized into the following parent-child relationships, where the parent class loader is above the child class loader:

                 Bootstrap ( $JAVA_HOME/jre/lib/ ;  $JAVA_HOME/jre/lib/ext )|System   ( $CATALINA_HOME/bin/bootstrap.jar ;$CATALINA_BASE/bin/tomcat-juli.jar or $CATALINA_HOME/bin/tomcat-juli.jar ; $CATALINA_BASE/bin/tomcat-juli.jar ;CATALINA_HOME/bin/commons-daemon.jar|Common    (deafult: $CATALINA_BASE/lib)/     \webapp1     webapp2

Tomcat 作为一个服务器容器, 需要有能力同时运行多个 war 包, 而每个 war 包中都拥有各自的依赖 lib 库(WEB-INF/lib) 以及各自的项目代码(WEB-INF/classes), 为了保证每个 web 项目可以共同运行, 互不干扰, Tomcat 为每个项目都创建一个单独 webapp classloader, 它会负责加载对应的 web 项目下 WEB-INF/classes 的 class 文件和资源以及 WEB-INF/lib 下的jar 包中所包含的 class 文件和资源文件, 使得这些被加载的内容仅对该 web 项目可见, 对其他 web 项目不可见。

webapp class loader 违背父加载模型

在上述过程中, 每一个 webapp classloader 在加载类时, 会优先在 WEB-INF/classesWEB-INF/lib 中搜索并尝试加载, 而不是优先委托给父加载器尝试加载,

这样做的好处是它允许不同的 web 项目去重载 Tomcat 提供的 lib 包(如$CATALINA_HOME/lib/ 中的 jar 包)。

这极大程度上保证了不同 web 项目的独立性和自由度。

理解类加载机制有什么用

应该很多人会疑问, 作为普通程序员, 为什么有必要理解类的加载过程 ? 我平时又没有需求要开发自己的类加载器

这里简单举几个用处

好处一: 理解 JAR Hell / Classpath Hell 是什么

JAR Hell 是一个术语, 用于描述由 java 类加载机制特性而引发的一系列问题。

  • 问题一: jar 包对于自身依赖的表达能力缺失

    • 一个 jar 包并没有途径向 JVM 表达它自己依赖哪些其他的 jar 包。 必须有一个外部的实体负责主动的把相互依赖的 jar 包都添加到类路径下, 让 jvm 统一予以加载。 在没有构建工具的时候, 程序员需要人工根据文档, 找到相互依赖的 jar 包, 将其下载好, 并把他们添加到项目中
    • JVM 运行时, 并不会检查 jar 包中缺失的依赖, 只有当那些依赖需要被访问时, 才会直接抛出 NoClassDefFoundError
  • 问题二: 传递依赖 ( Transitive Dependency )
    • jar A 依赖 jar B , jar B 又依赖 jar C , 这种延伸可以指数级地进行, 导致项目的依赖变得庞大且难以管理
  • 问题三: 同名类的相互遮蔽(Shadowing
    • 回顾前文提到的类的父委派加载机制, 可以发现一个重要的特点, 当一个类加载器被要求加载一个类时, 它首先会查询这个类是否已经被加载过findLoadedClass( myClassName), 如果加载过, 就会直接返回
    • 这一特性的好处时提升了效率,避免重复加载。 坏处是当 classpath 中不同的 jar 包含有相同的类全局限定名时, 只有一个类文件会得到加载。 例如: 当一个项目中意外引入了多个版本的同一个类库以后, JVM 中具体加载哪一个类就取决于类加载器会优先访问到哪一个 jar 包。 这显然会导致难以排查的异常, 因为 IDE 和 生产环境的类加载顺序很有可能产生不一致。
  • 问题四: 日趋复杂的类加载模型
    • 由于程序员可以自行实现各种类加载器, 框架也可以自定义类加载器, 当一个项目中引入了很多会自行创建类加载器的框架以后, 整个项目的类加载就会混乱而难以管理

好处二: 利用类加载机制, 实现对第三方库的低侵入式 bug fix

上文提到, 全局限定名相同的类只会被同一个类加载器加载一次。 这容易引发问题, 但也可以用来实现对第三方类库的修改。

想象你引用了一个第三方 jar 包, 但是发现有一点小问题, 你希望简单地修改这个 jar 包中的某一个类。 但是这个 jar 包其他项目也在引用, 你无权或不便修改。 但你的项目又确实需要进行这种修改。

此时最为便捷的方式, 是把 jar 包中的类拷贝到你的项目中,包路径及类名和 jar 中的完全相同, 然后直接进行修改,如果类加载能直接优先加载项目源码中, 你所定义的 class 文件,而不再使用 jar 包中的那个类文件, 不是极好的吗?

以笔者使用的 gradle 构建的 spring boot 单体 jar 包为例, 由于 gradle 构建出的 spring boot 单体 jar 包中,在 BOOT-INF 文件夹下, 将项目文件 classes 目录放置与 lib 目录之前, 而 spring boot 应用启动时, 又会按照 BOOT-INF 中文件夹组织顺序去加载类文件, 这就确保了笔者可以方便的对 lib 中所引用的第三方 jar 包进行类的替换 --》 想替换或修改哪个类, 就在项目下面自定义一个包路径相同的同名类, 自由修改。 jvm 运行时, 只会加载这个我们自定义的类, 忽略 jar 包中原始的那个类。

面试题之窒息翻译:类加载机制的双亲委派(正解:父委派模型)相关推荐

  1. Java类加载机制:双亲委托模型

    Java类加载机制:双亲委托模型 前言(废话) 一如既往,这篇博客是我极为浅显的理解,仅仅是我记录我自己成长的一环而已.我以前听我老师说过,什么是进步,进步就是当你三个月后重新再看自己的代码,发现那就 ...

  2. 阿里面试题,说说Java 类加载机制

    点击上方"Java后端技术栈"关注 持续推送技术干货 面试时,阿里面试官问了两个问题: 1.可以不可以自己写个String类? 答:不可以,因为根据类加载的双亲委派机制,会去加载父 ...

  3. Java虚拟机:对象创建过程与类加载机制、双亲委派模型

    一.对象的创建过程: 1.对象的创建过程: 对象的创建过程一般是从 new 指令(JVM层面)开始的,整个创建过程如下: (1)首先检查 new 指令的参数是否能在常量池中定位到一个类的符号引用: ( ...

  4. 36.JVM内存分哪几个区,每个区的作用是什么、如和判断一个对象是否存活、java垃圾回收机制、垃圾收集的方法有哪些、java类加载过程、类加载机制、双亲委派、Minor GC和Major GC

    36.JVM内存分哪几个区,每个区的作用是什么? 37.如和判断一个对象是否存活?(或者GC对象的判定方法) 38.简述java垃圾回收机制? 39.java中垃圾收集的方法有哪些? 40.java类 ...

  5. JVM类加载机制_字节码执行引擎_Java内存模型

    类加载机制: 类加载生命期:加载(Loading),验证(Verification),准备(Preparation),解析(Resolution),初始化(Initialization),使用(Usi ...

  6. 类加载机制、双亲委派机制深度解析以及如何自定义类加载器

    文章目录 1.类加载运行的全过程 2. JVM类加载器的初始化 3.双亲委派机制 4.编写自定义类加载器 5.(Tomcat)如何打破双亲委派机制 当我们运行一个类的时候,首先要通过类加载机制把类加载 ...

  7. 双亲委托类加载机制_图解JVM类加载机制和双亲委派模型

    我们都知道以 .java 结尾的 Java 源文件,经过编译之后会变成 .class 结尾的字节码文件.JVM 通过类加载器来加载字节码文件,然后再执行程序. 什么时候加载一个类 那么,什么时候类加载 ...

  8. java类加载机制为什么双亲委派_[五]类加载机制双亲委派机制 底层代码实现原理 源码分析 java类加载双亲委派机制是如何实现的...

    Launcher启动类 本文是双亲委派机制的源码分析部分,类加载机制中的双亲委派模型对于jvm的稳定运行是非常重要的不过源码其实比较简单,接下来简单介绍一下我们先从启动类说起有一个Launcher类 ...

  9. JVM 类加载机制及双亲委派模型

    一 .整体的流程 Java 中的所有类,必须被装载到 jvm 中才能运行,这个装载工作是由 jvm 中的类加载器完成的,类加载器所做的工作实质是把类文件从硬盘读取到内存中,JVM 在加载类的时候,都是 ...

最新文章

  1. 清明是品茗的好时节,那么你了解quot;明前茶quot;吗?
  2. 鸿海拟收购韩国家电企业东洋美吉 价格或达4.5亿美元
  3. 1.5 Map集合:HashMap 和TreeMap 类
  4. 计算机科学与技术专家报告内容,计算机科学与技术专业设计报告大学内容.docx...
  5. 随便写写有关微软2004软件架构师峰会的个人感想(一)
  6. 武汉加油!武大本科生用Python敲出樱花绽放,满屏春天太浪漫
  7. LODOP不同电脑打印效果不同排查
  8. HttpServletRequest 和 HttpServletResponse
  9. 散粉在哪个步骤用_如何正确的使用散粉
  10. TreeNMS redis/memcached可视化客户端工具的使用
  11. word批量转pdf,word批量转pdf步骤
  12. C1. Simple Polygon Embedding(计算几何)
  13. 模拟电视原理(笔记)
  14. nvidia驱动缓存能清理吗?
  15. USDT暴涨背后:溢价、套利和竞合
  16. 微信发红包案例测试场景
  17. Google Chrome显示粉红色屏幕
  18. JAVA初学习(三)(数组和面向对象)
  19. 地图学的基础知识_天文坐标系_大地坐标系_地心坐标系及其相关概念
  20. 30000台苹果电脑遭恶意软件入侵,包括最新的M1系列!

热门文章

  1. 信息安全--三:BLP模型(Bell-La Padula模型)
  2. 应用商店安装ubantu_谷歌宣布打击针对Netflix和Spotify的应用内结算
  3. uniapp实现微信扫二维码进行核销
  4. php验证码实现代码(3种)验证类
  5. 《神雕瞎驴》,金庸看后哭了
  6. windows下配置Tomcat为系统服务
  7. 57-HTML转义符
  8. 浅析互联网消费金融的发展与风险防范
  9. Tableau学习笔记⑥(多边形地图、设置地理信息、背景图地图)
  10. 2010.02.23——google map api----五岳剑派 拖拽的标注