JAVA实现代码热更新

news2025/5/26 11:27:36

JAVA实现代码热更新


引言

本文将带领大家利用Java的类加载器加SPI服务发现机制实现一个简易的代码热更新工具。

类加载相关知识可以参考: 深入理解JVM虚拟机第三版, 深入理解JVM虚拟机(第二版)—国外的,自己动手写JVM


类加载器

JVM通过ClassLoader将.class二进制流读取到内存中,然后为其建立对应的数据结构:

/*
ClassFile {
    u4             magic;
    u2             minor_version;
    u2             major_version;
    u2             constant_pool_count;
    cp_info        constant_pool[constant_pool_count-1];
    u2             access_flags;
    u2             this_class;
    u2             super_class;
    u2             interfaces_count;
    u2             interfaces[interfaces_count];
    u2             fields_count;
    field_info     fields[fields_count];
    u2             methods_count;
    method_info    methods[methods_count];
    u2             attributes_count;
    attribute_info attributes[attributes_count];
}
*/
//伪代码,不全
type Class struct {
	accessFlags       uint16
	name              string // thisClassName
	superClassName    string
	interfaceNames    []string
	constantPool      *ConstantPool
	fields            []*Field
	methods           []*Method
	sourceFile        string
	loader            *ClassLoader
	superClass        *Class
	interfaces        []*Class
	instanceSlotCount uint
	staticSlotCount   uint
	staticVars        Slots
	initStarted       bool
	jClass            *Object
	...
}

接着对Class执行验证,准备和解析,当然将符号引用解析为直接引用的过程一般用到的时候才会去解析,这也说明了为什么类只会在用到的时候才会进行初始化。

如果想要在内存中唯一确定一个类,需要通过加载该类的类加载实例和当前类本身来唯一确定,因为每个类加载器都有自己的命名空间:

//伪代码
type ClassLoader struct {
	//负责从哪些路径下加载class文件
	cp          *classpath.Classpath
	//简易版本命令空间隔离实现
	classMap    map[string]*Class // loaded classes
}

对于由不同类加载实例对象加载的类而言,他们是不相等的,这里的不相等包括Class对象的equals方法,isAssignableFrom方法,isInstance方法,Instanceof关键字,包括checkcast类型转换指令。

同一个类加载实例不能重复加载同一个类两次,否则会抛出连接异常。


实现热更新思路

  • 自定义类加载器,重写loadClass,findClass方法
/**
 * @author 大忽悠
 * @create 2023/1/10 10:31
 */
public class DynamicClassLoader extends ClassLoader{
    ...
    @Override
    protected Class<?> loadClass(String name, boolean resolve) throws ClassNotFoundException {
        synchronized (getClassLoadingLock(name)) {
            Class<?> c=null;
            //0.确保当前类加载不会重复加载已经加载过的类
            if((c=findLoadedClass(name))!=null){
                return c;
            }
            //1.父类加载
            if (getParent() != null) {
                try{
                    c = getParent().loadClass(name);
                }catch (ClassNotFoundException e){
                }
            }
            //2.自己加载
            if(c==null){
                c = findClass(name);
            }
            //3.是否对当前class进行连接
            if (resolve) {
                resolveClass(c);
            }
            return c;
        }
    }


    @Override
    public Class<?> findClass(String name) throws ClassNotFoundException {
        byte[] classBytes=getClassBytes(name);
        return defineClass(name,classBytes, 0, classBytes.length);
    }
    
        /**
     * @param name 全类名
     * @param resolve 是否需要对加载得到类进行link过程--验证,准备,解析(一般都是懒解析)
     */
    public static Class<?> dynamicLoadClass(String name,Boolean resolve) throws ClassNotFoundException {
        DynamicClassLoader dynamicClassLoader = new DynamicClassLoader();
        return dynamicClassLoader.loadClass(name,resolve);
    }

    /**
     * @param name 全类名
     */
    public static Class<?> dynamicLoadClass(String name) throws ClassNotFoundException {
        return dynamicLoadClass(name,false);
    }
    
    ...
}

dynamicLoadClass作为新增的静态方法,每次都会重新创建一个DynamicClassLoader自定义类加载器实例,并利用该实例去加载我们指定的类:

    public static void main(String[] args) throws InterruptedException, ClassNotFoundException, InstantiationException, IllegalAccessException, NoSuchMethodException, InvocationTargetException {
        invokeSay();
        Thread.sleep(15000);
        invokeSay();
    }

    private static void invokeSay() throws ClassNotFoundException, InstantiationException, IllegalAccessException, NoSuchMethodException, InvocationTargetException {
        Class<?> aClass = DynamicClassLoader.dynamicLoadClass("com.exm.A");
        Object newInstance = aClass.newInstance();
        Method method = aClass.getMethod("say");
        method.invoke(newInstance);
    }

我们只需要在休眠的这15秒内,替换掉对应的class文件实现,即可完成代码的热更新,并且同时确保父类加载器不能够找到同类路径的类,否则就不能让自定义加载器得到机会重新读取二进制流到内存并建立相应的数据结构了。

默认的父类加载器是类路径加载器,也被称作系统类路径加载器
在这里插入图片描述
该系统类加载器就是默认创建用来加载启动类的加载器,因为我们在启动类中通过方法调用引用了DynamicClassLoader,因此我们自定义的类加载器也是通过加载启动类的加载器进行加载的。
在本类中引用到的类都会使用加载本类的加载器进行加载


多种多样的加载来源

class二进制流数据可以来自于文件,网络,数据库或者其他地方,因此为了支持多种多样的加载来源,我们可以定义一个ClassDataLoader接口:

/**
 * @author 大忽悠
 * @create 2023/1/10 11:37
 */
public interface ClassDataLoader {
    /**
     * @param name 全类名
     * @return 加载得到的二进制文件流
     */
    byte[] loadClassData(String name);
}
  • 这里给出一个从文件中加载classData的实现案例:
package com;

import java.io.*;

/**
 * @author 大忽悠
 * @create 2023/1/10 11:48
 */
public class FileClassDataLoader implements ClassDataLoader{
    /**
     * 默认从当前项目路径找起
     */
    private String basePath="";

    /**
     * @param name 全类名
     * @return 加载得到的二进制文件流
     */
    @Override
    public byte[] loadClassData(String name) {
        return getClassData(new File(basePath+name.replace(".","/")+".class"));
    }

    private static byte[] getClassData(File file) {
        try (InputStream ins = new FileInputStream(file); ByteArrayOutputStream baos = new
                ByteArrayOutputStream()) {
            byte[] buffer = new byte[4096];
            int bytesNumRead = 0;
            while ((bytesNumRead = ins.read(buffer)) != -1) {
                baos.write(buffer, 0, bytesNumRead);
            }
            return baos.toByteArray();
        } catch (IOException e) {
            e.printStackTrace();
        }
        return new byte[] {};
    }
}


DynamicClassLoader自定义加载器内部新增两个属性:

    /**
     * 负责根据全类名加载class二进制流
     */
    private final static List<ClassDataLoader> classDataLoaderList=new ArrayList<>();
    /**
     * 所有DynamicClassLoader加载器共享一个缓存
     */
    private final static Map<String,byte[]> classBytesCache =new HashMap<>();

    public static void registerClasDataLoader(ClassDataLoader classDataLoader){
          classDataLoaderList.add(classDataLoader);
    }

    public static void cacheUpdateHook(String name,byte[] classData){
         classBytesCache.put(name,classData);
    }

对应的loadClass方法被修改为如下:

    @Override
    protected Class<?> loadClass(String name, boolean resolve) throws ClassNotFoundException {
        synchronized (getClassLoadingLock(name)) {
            Class<?> c=null;
            //0.确保当前类加载不会重复加载已经加载过的类
            if((c=findLoadedClass(name))!=null){
                return c;
            }
            //1.父类加载--如果缓存中存在,那么父类也就无需再次寻找了
            if (classBytesCache.get(name)==null && getParent() != null) {
                try{
                    c = getParent().loadClass(name);
                }catch (ClassNotFoundException e){
                }
            }
            //2.自己加载
            if(c==null){
                c = findClass(name);
            }
            //3.是否对当前class进行连接
            if (resolve) {
                resolveClass(c);
            }
            return c;
        }
    }


    @Override
    public Class<?> findClass(String name) throws ClassNotFoundException {
        byte[] classBytes = classBytesCache.get(name);
        if(classBytes==null){
            for (ClassDataLoader classDataLoader : classDataLoaderList) {
                      if((classBytes=classDataLoader.loadClassData(name))!=null){
                               break;
                      }
            }
        }
        if (classBytes==null || classBytes.length == 0) {
            throw new ClassNotFoundException();
        }
        classBytesCache.put(name,classBytes);
        return defineClass(name,classBytes, 0, classBytes.length);
    }

DynamicClassLoader内部内置了多个ClassData数据源,我们通过遍历数据源列表,只要其中一个返回结果不为空,我们就立刻返回。

为了避免每次都需要重新从数据源中读取数据,我们可以将从数据源中获取到的二进制字节码缓存起来,然后让ClassDataLoader通过cacheUpdateHook钩子函数更新缓存达到动态更新的效果。


我们自定义的FileClassDataLoader通过回调registerClassDataLoader接口,将自身注册到DynamicClassLoader的数据源列表中去:

    static {
        DynamicClassLoader.registerClasDataLoader(new FileClassDataLoader());
    }

但是如何让FileClassDataLoader静态代码块能够执行,也就是FileClassDataLoader类需要被初始化,如何做到?


SPI服务发现机制

在不通过new指令,不调用类里面的方法和访问类中字段的情况下,想要类能够被初始化,我们可以通过Class.forName完成:
在这里插入图片描述
forName的重载方法有一个Initialize参数,表明加载了当前类后,是否需要初始化该类,如果我们调用单参数的forName,那么默认为true。

所以,现在,我们只需要通过一种方式获取到ClassDataLoader的所有实现类类名,然后挨个使用Class.forName方法,完成实现类的初始化,就可以让实现类都注册到DynamicClassLoader中去。

SPI可以使用Java提供的serviceLoader,或者参考Spring的spring.factories实现,这里我给出一个简单的实现方案:

/**
 * @author 大忽悠
 * @create 2023/1/10 12:03
 */
public class SPIService {
    /**
     * 服务文件地址
     */
    private static final String SERVICE_PATH = "META-INF" + File.separator + "SPI.properties";
    /**
     * 服务信息存储
     */
    private static Properties SERVICE_MAP;

    static {
        try {
            SERVICE_MAP = new Properties();
            SERVICE_MAP.load(SPIService.class.getClassLoader().getResourceAsStream(SERVICE_PATH));
        } catch (IOException e) {
            throw new RuntimeException(e);
        }
    }

    /**
     * @param name 需要寻找的服务实现的接口的全类名
     * @return 找寻到的所有服务实现类
     */
    public List<Class<?>> loadService(String name) {
        if (SERVICE_MAP == null) {
            return null;
        }
        String[] classNameList = SERVICE_MAP.getProperty(name).split(",");
        ArrayList<Class<?>> classList = new ArrayList<>(classNameList.length);
        for (String classDataClassName : classNameList) {
            try {
                classList.add(Class.forName(classDataClassName));
            } catch (ClassNotFoundException e) {
                //忽略不可被解析的服务实现类
                e.printStackTrace();
            }
        }
        return classList;
    }

}

DynamicClassLoader新增代码:

    /**
     * 负责提供SPI服务发现机制
     */
    private final static SPIService spiService=new SPIService();

    static {
        //通过SPI机制寻找classDataLoader
        spiService.loadService(ClassDataLoader.class.getName());
    }

在这里插入图片描述


完整代码

package com;

import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

/**
 * @author 大忽悠
 * @create 2023/1/10 10:31
 */
public class DynamicClassLoader extends ClassLoader{
    /**
     * 负责根据全类名加载class二进制流
     */
    private final static List<ClassDataLoader> classDataLoaderList=new ArrayList<>();
    /**
     * 所有DynamicClassLoader加载器共享一个缓存
     */
    private final static Map<String,byte[]> classBytesCache =new HashMap<>();
    /**
     * 负责提供SPI服务发现机制
     */
    private final static SPIService spiService=new SPIService();

    static {
        //通过SPI机制寻找classDataLoader
        spiService.loadService(ClassDataLoader.class.getName());
    }

    @Override
    protected Class<?> loadClass(String name, boolean resolve) throws ClassNotFoundException {
        synchronized (getClassLoadingLock(name)) {
            Class<?> c=null;
            //0.确保当前类加载不会重复加载已经加载过的类
            if((c=findLoadedClass(name))!=null){
                return c;
            }
            //1.父类加载--如果缓存中存在,那么父类也就无需再次寻找了
            if (classBytesCache.get(name)==null && getParent() != null) {
                try{
                    c = getParent().loadClass(name);
                }catch (ClassNotFoundException e){
                }
            }
            //2.自己加载
            if(c==null){
                c = findClass(name);
            }
            //3.是否对当前class进行连接
            if (resolve) {
                resolveClass(c);
            }
            return c;
        }
    }


    @Override
    public Class<?> findClass(String name) throws ClassNotFoundException {
        byte[] classBytes = classBytesCache.get(name);
        if(classBytes==null){
            for (ClassDataLoader classDataLoader : classDataLoaderList) {
                      if((classBytes=classDataLoader.loadClassData(name))!=null){
                               break;
                      }
            }
        }

        if (classBytes==null || classBytes.length == 0) {
            throw new ClassNotFoundException();
        }
        classBytesCache.put(name,classBytes);
        return defineClass(name,classBytes, 0, classBytes.length);
    }

    /**
     * @param name 全类名
     * @param resolve 是否需要对加载得到类进行link过程--验证,准备,解析(一般都是懒解析)
     */
    public static Class<?> dynamicLoadClass(String name,Boolean resolve) throws ClassNotFoundException {
        DynamicClassLoader dynamicClassLoader = new DynamicClassLoader();
        return dynamicClassLoader.loadClass(name,resolve);
    }

    /**
     * @param name 全类名
     */
    public static Class<?> dynamicLoadClass(String name) throws ClassNotFoundException {
        return dynamicLoadClass(name,false);
    }

    public static void registerClasDataLoader(ClassDataLoader classDataLoader){
          classDataLoaderList.add(classDataLoader);
    }

    public static void cacheUpdateHook(String name,byte[] classData){
         classBytesCache.put(name,classData);
    }
}

/**
 * @author 大忽悠
 * @create 2023/1/10 11:37
 */
public interface ClassDataLoader {
    /**
     * @param name 全类名
     * @return 加载得到的二进制文件流
     */
    byte[] loadClassData(String name);
}
package com;

import java.io.*;

/**
 * @author 大忽悠
 * @create 2023/1/10 11:48
 */
public class FileClassDataLoader implements ClassDataLoader{
    /**
     * 默认从当前项目路径找起
     */
    private String basePath="";

    static {
        DynamicClassLoader.registerClasDataLoader(new FileClassDataLoader());
    }

    /**
     * @param name 全类名
     * @return 加载得到的二进制文件流
     */
    @Override
    public byte[] loadClassData(String name) {
        return getClassData(new File(basePath+name.replace(".","/")+".class"));
    }

    private static byte[] getClassData(File file) {
        try (InputStream ins = new FileInputStream(file); ByteArrayOutputStream baos = new
                ByteArrayOutputStream()) {
            byte[] buffer = new byte[4096];
            int bytesNumRead = 0;
            while ((bytesNumRead = ins.read(buffer)) != -1) {
                baos.write(buffer, 0, bytesNumRead);
            }
            return baos.toByteArray();
        } catch (IOException e) {
            e.printStackTrace();
        }
        return new byte[] {};
    }
}

package com;

import java.io.File;
import java.io.IOException;
import java.util.ArrayList;
import java.util.List;
import java.util.Properties;

/**
 * @author 大忽悠
 * @create 2023/1/10 12:03
 */
public class SPIService {
    /**
     * 服务文件地址
     */
    private static final String SERVICE_PATH = "META-INF" + File.separator + "SPI.properties";
    /**
     * 服务信息存储
     */
    private static Properties SERVICE_MAP;

    static {
        try {
            SERVICE_MAP = new Properties();
            SERVICE_MAP.load(SPIService.class.getClassLoader().getResourceAsStream(SERVICE_PATH));
        } catch (IOException e) {
            throw new RuntimeException(e);
        }
    }

    /**
     * @param name 需要寻找的服务实现的接口的全类名
     * @return 找寻到的所有服务实现类
     */
    public List<Class<?>> loadService(String name) {
        if (SERVICE_MAP == null) {
            return null;
        }
        String[] classNameList = SERVICE_MAP.getProperty(name).split(",");
        ArrayList<Class<?>> classList = new ArrayList<>(classNameList.length);
        for (String classDataClassName : classNameList) {
            try {
                classList.add(Class.forName(classDataClassName));
            } catch (ClassNotFoundException e) {
                //忽略不可被解析的服务实现类
                e.printStackTrace();
            }
        }
        return classList;
    }

}

完整项目架构:
在这里插入图片描述

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/152587.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

从零开始搭建一个vue demo工程

查询了不少文章&#xff0c;有知乎、CSDN、简书、思否等&#xff0c;发现如下操作性比较好&#xff0c;特此记录 目录 使用vue-cli创建 使用vite创建 Vue2和Vue3的代表作 参考文章 使用vue-cli创建 Vue3-使用vue/cli搭建项目 - 个人文章 - SegmentFault 思否 此处使用了v…

一文看懂基站无源交调

众所周知&#xff0c;有源器件会在系统中产生非线性效应。虽然已开发出多种技术来改善此类器件在设计和运行阶段的性能&#xff0c;但容易忽视的是&#xff0c;无源器件也可能引入非线性效应&#xff1b;虽然有时相对较小&#xff0c;但若不加以校正&#xff0c;这些非线性效应…

将vue项目打包成电脑端应用.exe

目录 第一步:下载模板electron-quick-start 第二步&#xff1a;进入到下载好的模板文件当中&#xff08;electron-quick-start&#xff09; 第三步&#xff1a;打包自己的项目&#xff08;npm run build&#xff09; 第四步&#xff1a;删掉官方demo下的index.html文件 …

Framework入门

一入门简介Android系统建构分为四层由上到下依次是应用层&#xff0c;应用框架层&#xff0c;依赖库层&#xff0c;内核层&#xff0c;framework处于第二层&#xff0c;它为应用层的开发者提供基本功能&#xff0c;帮助开发快速构建应用程序。FrameWork框架采用c/s架构&#xf…

Java之反射爆破操作

一些方法 首先说一下-getField/Method什么的这种不带Declared只能获取到对应public的属性/方法 只有带Declared可以获取到非public的属性/方法 再看不带s和带s区别 不带s只获取对应的构造器/方法 比如说构造器 getDeclaredConstructor(int.class,String class) 就会获取不管什么…

【数据分析】【Pandas】(一)如何制作频率分布直方图

文章目录概述1. 直方图2. 密度图概述 计算一组数据的分布有助于我们更好的了解数据构成&#xff0c;我们可以通过直方图或密度图&#xff0c;将离散的数据通过连续的方式展现出来。 数据分布&#xff08;频数分布&#xff09;&#xff1a;在各组按顺序排列的基础上&#xff0c…

小伍说,商业发展均是顺势而为,【字节跳动】之所以成功是因为顺应趋势,成功是必然结果!

昨天看完2021年 刘润【进化的力量】年终演讲&#xff0c;让我深刻感受到两点&#xff1a; 1、所有商业的变化&#xff0c;都是顺势而为&#xff01; 2、所有理所当然的现在&#xff0c;都是曾经不可思议的未来&#xff0c;所有现在不可思议的未来&#xff0c;可能都是明天理所…

JavaSE笔记——函数式编程(高级集合类和收集器)

文章目录前言一、方法引用二、元素顺序三、使用收集器1.转换成其他集合2.转换成值3.数据分块4.数据分组5.字符串6.组合收集器总结前言 前面介绍了集合类的部分变化&#xff0c;事实上&#xff0c;Java 8 对集合类的改进不止这些。现在是时候介绍一些高级主题了&#xff0c;包括…

LInkedList的模拟实现

在之前的文章笔者介绍了链表的实现&#xff1a;无头单向非循环链表的实现&#xff01;感兴趣的各位老铁可以点进来看看&#xff1a;https://blog.csdn.net/weixin_64308540/article/details/128397961?spm1001.2014.3001.5502对于此篇博客&#xff0c;在一写出来&#xff0c;便…

java学习day72(乐友商城)微信支付实现

今日目标&#xff1a; 会调用订单系统接口 实现订单结算功能 实现微信支付功能 1.订单系统接口 我们不做开发&#xff0c;只讲解 1.1.导入订单服务 把课前资料提供的leyou-order复制到D:\heima\code\leyou目录。 然后在工程内导入&#xff1a; 然后导入module&#xff1a…

java:责任链设计模式配合Spring@Order注解使用场景

java&#xff1a;责任链设计模式配合SpringOrder注解使用场景 1 前言 java的责任链模式&#xff0c;经典使用的场景为SpringMVC的doDispatch下&#xff0c;针对请求的过滤链式行为。实际开发场景中&#xff0c;可配合Spring的Order注解&#xff0c;定义1个有顺序的链式Compon…

我在深圳的三次工厂旅程 (一)

2019年末加入一家人工智能AI创业公司&#xff0c;由于公司涉及到智能硬件产品&#xff0c;所以有机会参与到硬件产品的整个研发测试端、产品迭代流、工厂生产序等方面的这些事情。相对于研发测试、产品迭代这些在过往的工作中经历中相对比较熟悉和接触频繁&#xff0c;在软件产…

鉴定完毕!来看看跨年晚会谁假唱了…欧莱雅智能画眉设备;首个AI律师下月出庭;推特又裁员;GitHub今日热榜 | ShowMeAI资讯日报

&#x1f440;日报合辑 | &#x1f3a1;AI应用与工具大全 | &#x1f514;公众号资料下载 | &#x1f369;韩信子 &#x1f3a1; 『谁在假唱』技术手段分析跨年演唱会上的歌手们 各大卫视的跨年演唱会&#xff0c;你追了几场&#xff1f;看出来了谁在对口型&#xff1f;B站Up主…

【北京理工大学-Python 数据分析-2.2Matplotlib绘制饼图、直方图、极坐标、散点图】

pyplot的基础图标函数 函数说明plt.plot(x,y,fmt,…)绘制一个坐标图plt.boxplot(data,notch,position)绘制一个箱型图plt.bar(left,height,width,bottom)绘制一个条形图plt.barh(width,bottom,left,height)绘制一个横向条形图plt.polar(theta,r)绘制极坐标图plt.psd(x,NFFT256…

Dokcer14_5:Docker Compose volumes解析、Docker Compose volumes目录路径生成规则

Dokcer14_5&#xff1a;Docker Compose volumes解析、Docker Compose volumes目录路径生成规则docker-compose volumes语法语法格式及其三种变体1.无来源 &#xff1a;匿名挂载主机系统上的目录路径2.非路径源&#xff1a;具名挂载&#xff08;常用&#xff09;主机系统上的目录…

mysql idb,frm文件复制恢复

idb是innodb数据文件frm是innodb表结构文件在数据库的data目录下可以看到 data下的目录名就是数据库名&#xff0c;打开该数据库文件夹对于一个表有2个文件&#xff0c;一个以idb结尾&#xff0c;一个以frm结尾直接复制粘贴是不识别的&#xff0c;提示找不到该表解决方法&#…

在线实习项目|Python爬虫助力疫情数据追踪在线实习项目

项目介绍 项目背景&#xff1a;2019-NCOV新型冠状病毒引发的肺炎牵动全国人民的心&#xff0c;本项目希望通过大数据技术为抗击新冠肺炎贡献一份力量。 项目目标&#xff1a;使用PYTHON爬虫技术爬取疫情数据&#xff0c;从不同维度分析数据&#xff0c;并用 MATPLOT…

ubuntu18.04部署DXSLAM,CNN+VSLAM,CPU实时运行

一、下载源代码 打开终端&#xff0c;输入命令克隆仓库 git clone https://github.com/raulmur/DXSLAM.git DXSLAM二、配置环境 We have tested the library in Ubuntu 16.04 and Ubuntu 18.04, but it should be easy to compile in other platforms. C11 or C0x CompilerPa…

虚拟化技术学习笔记8

添加网卡&#xff1a; 1、virt-manager: 选择虚拟机直接添加网卡操作。 2、virsh命令&#xff1a; virsh list virsh domiflist centos7-1 virsh attach-interface centos7-1 \ --type network \ --source default \ --model virtio \ --config 虚拟机查看网卡的添加情况&…

分享65个NET源码,总有一款适合您

NET源码 分享65个NET源码&#xff0c;总有一款适合您 65个NET源码链接&#xff1a;https://pan.baidu.com/s/19yFm_9K_L0xfykMP1hdP5A?pwdn2p7 提取码&#xff1a;n2p7 下面是文件的名字&#xff0c;我放了一些图片&#xff0c;文章里不是所有的图主要是放不下...&#x…