C#-反射

一、概念

  1. 反射(Reflection)在C#中是一种非常重要的特性,它为开发者提供了在运行时获取和操作关于类型、成员、属性、方法等的详细信息的能力。通过反射,开发者可以在程序运行期间动态地创建对象、调用方法、设置属性值以及进行其他多种操作,而不需要事先在代码中硬编码这些操作。
  2. C#中的反射API主要集中在System.Reflection命名空间下,该命名空间包含了多个用于反射的类和接口。

1.1常用的反射类型:

  1. Assembly(程序集):Assembly类用于表示一个程序集,可以加载和卸载程序集,以及获取程序集中的类型、资源等信息。
  2. Type(类型):Type类表示程序集中的任意类型,可以是类、接口、枚举、结构等。通过Type类可以获取类型的成员信息,如方法和属性等。
  3. MethodInfo(方法信息):MethodInfo类提供了关于特定方法的信息,包括方法的名称、参数、返回类型等,还可以调用该方法。
  4. PropertyInfo(属性信息):PropertyInfo类用于表示属性,可以获取或设置属性的值。
  5. FieldInfo(字段信息):FieldInfo类用于表示字段,可以获取或设置字段的值。

1.2常用场景

  1. 动态加载程序集:在运行时根据需要加载.dll或.exe文件,并访问其中的类型和成员。
  2. 晚期绑定和早期绑定:晚期绑定是指在程序运行时才确定要调用的方法或访问的属性,而早期绑定是在编译时就确定。反射可以实现晚期绑定。
  3. 对象序列化和反序列化:在序列化和反序列化过程中,反射可以动态地读取和写入对象的各个属性。
  4. 实现插件系统:通过反射可以加载和调用插件中的功能,而无需预先知道插件的具体实现细节。
  5. 单元测试和ORM(对象关系映射)技术:在单元测试中,反射可用于动态调用待测试的方法;在ORM技术中,反射则可用于动态访问和映射对象的属性至数据库表的列。

1.3程序集概念

  1. 程序集(Assembly)是.NET框架下代码编译的一个逻辑单元,用于组合相关的代码和类型,最终生成PE文件
  2. 程序集是.NET应用程序的基本组成单元,它以可执行文件(.exe) 动态链接库文件(.dll)的形式存在。一个程序集可以包含一个或多个模块,每个模块又可以包括多个类型(如类、接口、枚举等)。程序集不仅包含编译后的IL(中间语言)代码 ,还包含描述程序集自身的元数据、资源文件等信息。
  3. 在软件开发过程中,一个项目通常对应一个程序集。例如,在Visual Studio中,一个解决方案可以包含多个项目,每个项目编译后都会生成一个程序集(一般是一个.dll文件)。这些程序集可以被其他项目引用,从而实现功能的复用和模块化开发。

二、Type类

2.1、概念

Type类是C#反射中的一个核心类,用于表示所有.NET类型的公共基类。通过Type类,可以访问类型的元数据,获取类型的成员信息,并进行实例化等操作。以下将详细探讨Type类的各个方面:

定义:Type类位于System.Reflection命名空间下,它提供了很多方法来获取类型信息、创建实例、执行方法等。

2.2、功能

通过Type类,可以检查类型是否为泛型类型、接口、委托等。同时,还可以获取类型的属性、方法、事件等成员信息。

2.3、使用

  1. 获取Type实例
    静态方法:可以使用Type的静态方法如GetType、GetTypes等来获取类型信息。例如,typeof(int)返回表示int类型的Type对象。
    动态实例:通过对象实例调用GetType()方法获取其类型。例如,一个字符串实例myString.GetType()将返回String类型的Type对象。
  2. 获取类型信息
    属性和方法:使用Type对象的GetProperties和GetMethods方法可以分别获取类型的所有属性和方法。这些返回的PropertyInfo和MethodInfo对象提供了更详细的成员信息。
    基类和接口:可以通过Type对象的BaseType属性获取其基类的类型,IsClass属性判断是否为类,Implements接口方法检查实现的接口。
  3. 创建类型实例
    使用Activator类:Activator类提供了基于Type创建实例的方法,如CreateInstance。这允许在运行时动态创建对象。
    使用ConstructorInfo:通过Type对象的GetConstructors方法获取构造函数,然后使用ConstructorInfo对象的Invoke方法创建实例。
  4. 调用类型方法
    MethodInfo对象:通过Type对象的GetMethods获取方法列表,然后用MethodInfo对象的Invoke方法动态调用这些方法。
    参数处理:MethodInfo的Invoke方法需要参数数组。如果方法需要的参数类型与传递的参数类型不匹配,需要进行适当的类型转换。
  5. 类型的兼容性和转换
    类型兼容检查:IsAssignableFrom方法可以用来检查一个类型是否可以从另一个类型派生。例如,typeof(object).IsAssignableFrom(typeof(string))将返回true。
    类型转换:Type类提供了几个方法来检查和执行类型转换,如IsInstanceOfType方法和ChangeType方法。

2.4、代码实例

csharp 复制代码
// See https://aka.ms/new-console-template for more information
//Console.WriteLine("Hello, World!");
//反射
//Type类实例化

using System.Reflection;

Type type = typeof(Person);
Console.WriteLine("--------------------获取属性信息------------------------------");
Console.WriteLine(type.ToString());
//01、获取类中属性信息GetProperties(),返回的是PropertyInfo[]类型数组
PropertyInfo[] propertyInfos=type.GetProperties();
foreach (var item in propertyInfos)
{
    Console.WriteLine(item.Name);
    Console.WriteLine(item.Attributes);//还有其他特性可以获取
}
Console.WriteLine("-----------------------获取字段信息---------------------------");

//02、获取所有字段,只能获取共有字段,属性的第二种定义方式也属于私有字段
FieldInfo[] fieldInfos=type.GetFields();
foreach (var field in fieldInfos) { 

    Console.WriteLine(field.Name);
    Console.WriteLine(field.FieldType);
}

/*结果:
iD
System.Int32
name
System.String*/

Console.WriteLine("-----------------------获取类的信息---------------------------");
//直接使用type调用相关方法
Console.WriteLine(type.Name);
Console.WriteLine(type.Attributes);

Console.WriteLine("-----------------------获取构造方法的信息---------------------------");
ConstructorInfo[] methodInfo =type.GetConstructors();
foreach (var item in methodInfo)
{
    Console.WriteLine(item);//返回的是构造方法的签名,返回值与参数
}
/* 
Void.ctor()
Void.ctor(Int32, System.String)*/
Console.WriteLine("-----------------------获取方法的信息---------------------------");
MethodInfo[] info = type.GetMethods();

foreach (var item in info)
{
    Console.WriteLine(item.Name);
}



/// <summary>
/// 定义反射测试类
/// </summary>
class  Person{

    //01、定义公有字段
    public int iD;
    public string name;
    //02、定义私有字段
    private string Sex;
    //03、定义属性,属性有两种定义方式
    //方式1
    public int Age { get; set; }
    //方式2
    private string address;
    //04无参构造
    public Person()
    {
        Console.WriteLine("无参构造");
    }
    //05有参构造
    public Person(int iD, string name)
    {
        this.iD = iD;
        this.name = name;
    }
    public string Address { 
        get { return this.address; }   
        set { this.address = value; }
    }
}

三、Assembly类

3.1概念及使用

Assembly类在C#反射中是用于操作程序集的一个重要工具,它能够加载、枚举、和操纵程序集。以下是对Assembly类的详细解析:

  1. 加载程序集
    • 静态加载方法 :Assembly.Load方法可以加载一个程序集,通常用于加载同一目录下或全局程序集中的其它程序集[^1^]。例如,Assembly assembly = Assembly.Load("AssemblyName");
    • 动态加载方法 :Assembly.LoadFrom和Assembly.LoadFile方法可以指定文件路径来加载程序集。例如,Assembly assembly = Assembly.LoadFrom("包含程序集清单的文件的名称或路径");[^2^]。
  2. 枚举程序集中的类型
    • 获取类型信息 :通过Assembly实例的GetTypes方法可以得到程序集中定义的所有类型的Type对象数组[^3^]。例如,Type[] types = assembly.GetTypes();
    • 创建类型实例 :结合Activator.CreateInstance方法,可以用枚举到的Type动态创建类型的实例。例如,object obj = Activator.CreateInstance(type);[^4^]。
  3. 调用方法与访问属性
    • 调用方法 :MethodInfo类可以用于获取方法和动态调用方法。例如,使用MethodInfo method = type.GetMethod("MethodName");来获取方法信息,然后使用method.Invoke(obj, parameters);来调用它[^4^]。
    • 访问属性 :PropertyInfo类可以用来获取属性信息和动态设置或获取属性值。例如,PropertyInfo property = type.GetProperty("PropertyName");用来获取属性信息,然后property.SetValue(obj, value, index);用于设置属性值[^4^]。
  4. 获取程序集信息
    • 获取程序集名称和版本 :Assembly实例的GetName方法可以获取程序集的完整名称、版本等信息。例如,AssemblyName name = assembly.GetName();[^4^]。
    • 获取程序集元数据 :可以使用Assembly实例的其他方法如GetCustomAttributes来检查程序集的特性和元数据[^4^]。
  5. 操纵程序集
    • 创建新实例 :除了加载已存在的程序集外,Assembly类还提供了CreateInstance方法用于创建程序集中特定类型的新实例[^3^]。例如,object instance = assembly.CreateInstance("TypeName");
    • 获取当前程序集 :Assembly.GetExecutingAssembly方法可以获取当前正在执行的程序集,这在需要访问当前程序集中的资源或类型时非常有用[^2^]。例如,Assembly currentAssembly = Assembly.GetExecutingAssembly();

3.2应用-Assembly拿到成员-Type操作成员

3.2.1、Assembly获取程序集中的类/接口

  1. VS中定义类库文件:demo49反射_数据,作为程序集文件
  2. 定义控制台应用程序:demo49反射_Assembly使用,通过反射 去获取指定程序集里面的程序 操作成员(注意:此文将并没有对程序集文件添加依赖,不可直接操作程序集文件中内容,需要使用反射相关技术获取程序集文件
  3. 通过LoadFile获取程序集文件:通过loadFile+程序集的绝对路径的方式 获取到了要反射的程序集
csharp 复制代码
//路径不知道的情况下:如何获取路径
//获取当前路径的完全限定路径
Console.WriteLine("当前文件夹路径:" + Environment.CurrentDirectory);
//找到net7.0文件夹,自己手动进入找到.dll文件
Assembly ass= Assembly.LoadFile(@"E:\c#体系课\04c#高级\代码\demo49反射_数据\bin\Debug\net7.0\demo49反射_数据.dll");
  1. 通过Assembly对象 自己程序的GetType获取程序集中指定的类型 成员 GetType()+类型全名称 [命名空间.类型名字]
csharp 复制代码
//通过GetTypes() 获取程序集中 所以的成员   获取到的是  pubic + internal
Type type = ass.GetType("demo49反射_数据.Person");//获取单个
Type[] types = ass.GetTypes();//获取所有

但是有时候我们并不希望获取内部internal成员

csharp 复制代码
//只获取程序集中被public修饰的成员,不要internal成员  Exported:出口
Type[] types = ass.GetExportedTypes();

foreach (var item in types)
{
    Console.WriteLine(item.Name);
}


3.2.2、Type获取类中方法

  1. 添加数据,对Person类中添加需要测试的属性,方法等数据
csharp 复制代码
public class  Person
{
    //01、定义属性
    public string Name { get; set; }
    public int Age { get; set; }
    public char Gender { get; set; }
    //02、定义方法
    public static void SayHello()
    {
        Console.WriteLine("我是静态方法");
    }

    public void Sayhi()
    {
        Console.WriteLine("我是实例方法");
    }

    public int Add(int n1, int n2) {

        return n1 + n2;
    }

    public Person()
    {
        
    }
    //03、有参构造
    public Person(string name,int age,char gender)
    {
        this.Name = name;
        this.Age = age;
        this.Gender = gender;
    }

    public void Test() {
        Console.WriteLine("我是无参方法");
    }

    public void Test(string name) {
        Console.WriteLine(name);
    }

    public void Test(int n1,int n2)
    {
        Console.WriteLine(n1+n2);
    }

}
  1. 获取类中方法
csharp 复制代码
//01、Assembly操作
Assembly ass= Assembly.LoadFile(@"D:\C#_Project\05C#高级代码\demo49反射_数据\bin\Debug\net7.0\demo49反射_数据.dll");

//02、Type操作
Type type = ass.GetType("demo49反射_数据.Person");
//获取所有方法
MethodInfo[] methodInfos = type.GetMethods();
foreach (var item in methodInfos)
{
    Console.WriteLine(item.Name);
}

添加限定条件,只要静态方法

csharp 复制代码
//如果我们要用BindingFlags.Static这个枚举 需要再加上一个public 枚举进行配合
MethodInfo[] methodInfos = type.GetMethods(BindingFlags.Static | BindingFlags.Public);
foreach (var item in methodInfos)
{
    Console.WriteLine(item.Name);
}

//如果只要其中一个方法就可以用下面这种方式
MethodInfo method = type.GetMethod("SayHello");

3.2.3、执行获取的方法

如果执行静态方法 注意:不可以像调用委托或者事件一样通过加括号直接调用

间接调用 通过 invoke调用

//第一个参数 在调用成员的是很好 是否需要传入对象

//1.静态方法:类.

//2.实例方法:对象.

//第二个参数 调用方法的时候是否需要传入参数 如果需要就传入参数 不需要传入null就可以了

csharp 复制代码
 method.Invoke(null,null);

3.2.4、Activator创建对象(方法一)

对象创建唯一途径就是构造方法被调用

csharp 复制代码
//01、获取程序集
Assembly ass= Assembly.LoadFile(@"D:\C#_Project\05C#高级代码\demo49反射_数据\bin\Debug\net7.0\demo49反射_数据.dll");

//02、获取程序集中需要的类
Type type = ass.GetType("demo49反射_数据.Person");

//03、创建person对象实例  在反射中 一般有2中方式创建对象 
//1.调用构造函数
//2.通过Activator 动态创建实例对象
//Activator的底层 就是调用无参构造函数  
object person = Activator.CreateInstance(type);
Console.WriteLine(person.ToString());

//调用实例方法,参数1:实例对象
method.Invoke(person,null);

注意:SayHello是静态方法直接通过类就可获取

3.2.5、构造方法创建对象(方法二)

csharp 复制代码
//LoadFile传入的路径是绝对路径    不是相对路径
//1.通过loadFile+程序集的绝对路径的方式 获取到了要反射的程序集,从文件中找,不要直接属性复制
Assembly ass= Assembly.LoadFile(@"D:\C#_Project\05C#高级代码\demo49反射_数据\bin\Debug\net7.0\demo49反射_数据.dll");
Type type = ass.GetType("demo49反射_数据.Person");
//获取单个构造函数
//参数:string name,int age,char gender
ConstructorInfo constructor= type.GetConstructor(new Type[] { typeof(string),typeof(int),typeof(char)});
object person= constructor.Invoke(new object[] {"张三",10,'男'});
//间接调用  person返回的就是你创建的对象

PropertyInfo property= type.GetProperty("Name");

//通过GetValue()调用这个方法可以获取属性的值
object propertys= property.GetValue(person);
Console.WriteLine(propertys);//输出:张三

3.2.6、获取有参有返回值方法

csharp 复制代码
//01、获取程序集
Assembly ass= Assembly.LoadFile(@"D:\C#_Project\05C#高级代码\demo49反射_数据\bin\Debug\net7.0\demo49反射_数据.dll");
//02、通过Assembly对象 自己程序的GetType获取程序集中指定的类型 成员  GetType()+类型全名称 [命名空间.类型名字]
Type type = ass.GetType("demo49反射_数据.Person");
//3.1、创建类对象
object person = Activator.CreateInstance(type);
//3.2、通过Type数组 判断调用方法的参数个数和参数类型  依次告诉我们编译器我们想要哪个方法
MethodInfo method = type.GetMethod("Test", new Type[] { });

MethodInfo method1 = type.GetMethod("Test", new Type[] { typeof(string) });

MethodInfo method2= type.GetMethod("Test", new Type[] { typeof(int), typeof(int) });

//04、调用重载方法 编译器无法直接从是否传递的参数来潘顿我们调用的是哪个方法
Console.WriteLine(method.Name);
method.Invoke(person,new object[] { });

Console.WriteLine(method1.Name);
method1.Invoke(person,new object[] {10 });

Console.WriteLine(method2.Name);
method2.Invoke(person,new object[] { 10,20 });

四、反射常用API

  1. IsAssignableFrom:后面的值 能不能直接赋值给前面的类型
  2. IsSubclassOf :判断后面的类型 是否是前面的类型的父类 必须是严格的父子类继承关系 才会返回true
  3. IsInstanceOfType:判断的不是继承关系 而是单纯的判断 是否是某个类型的对象
  4. IsAbstrcat; 是否是抽象类 True
  5. //IsInterface:是否是接口
csharp 复制代码
// See https://aka.ms/new-console-template for more information
//Console.WriteLine("Hello, World!");

//判断类之间的关系

//IsAssignableFrom 返回bool类型   后面的值 能不能直接赋值给前面的类型
// 判断继承  子类-----》父类
bool b1 = typeof(Person).IsAssignableFrom(typeof(Student));
Console.WriteLine(b1);

bool b2 = typeof(Student).IsAssignableFrom(typeof(Teacher));
Console.WriteLine(b2);

bool b3 = typeof(IinterFace).IsAssignableFrom(typeof(TestInterFace));
Console.WriteLine(b3);

Console.WriteLine("--------------------------------------------------");
//IsSubclassOf :判断后面的类型 是否是前面的类型的父类 必须是严格的父子类继承关系 才会返回true
bool b4 = typeof(Student).IsSubclassOf(typeof(Person));
Console.WriteLine(b4);
bool b5 = typeof(Teacher).IsSubclassOf(typeof(Person));
Console.WriteLine(b5);
//接口严格意义上来说 不是严格父子类的关系 二期实现的关系
bool b6 = typeof(TestInterFace).IsSubclassOf(typeof(IinterFace));
Console.WriteLine(b6);


//IsInstanceOfType
//判断的不是继承关系  而是单纯的判断 是否是某个类型的对象
bool b = typeof(Student).IsInstanceOfType(new Person());
Console.WriteLine(b1);


//IsAbstrcat; 是否是抽象类 True
//但凡不能创建对象的  都认识是抽象的
Console.WriteLine(typeof(IinterFace).IsAbstract);
Console.WriteLine(typeof(Stest).IsAbstract);
Console.WriteLine(typeof(Person).IsAbstract);


//IsInterface:是否是接口
Console.WriteLine(typeof(Person).IsInterface);
Console.WriteLine(typeof(IinterFace).IsInterface);
Console.WriteLine(typeof(Stest).IsInterface);


class Person
{

}

class Student : Person
{

}

class Teacher : Person
{

}

interface IinterFace
{

}

class TestInterFace : IinterFace
{

}

abstract class Test
{

}
static class Stest
{
}


/*
Console.WriteLine("a");*/

五、案例:使用反射开发记事本插件

插件时dll文件

5.1、记事本简单功能实现


csharp 复制代码
private void 保存ToolStripMenuItem_Click(object sender, EventArgs e)
{
    //保存对话框
    saveFileDialog1 = new SaveFileDialog();
    //当前选择的是ok的话
    if (saveFileDialog1.ShowDialog()==DialogResult.OK)
    {
        string path = saveFileDialog1.FileName;

        File.WriteAllText(path,textBox1.Text.Trim());
    }
}


5.2、插件功能开发




5.2.1步骤一、记事本基础功能开发


csharp 复制代码
 private void 保存ToolStripMenuItem_Click(object sender, EventArgs e)
 {
     //保存对话框
     saveFileDialog1 = new SaveFileDialog();
     //当前选择的是ok的话
     if (saveFileDialog1.ShowDialog()==DialogResult.OK)
     {
         string path = saveFileDialog1.FileName;

         File.WriteAllText(path,textBox1.Text.Trim());
     }
 }

5.2.2步骤二、接口定义规范

添加引用,使得Form中的控件可以作为参数传递:引用的窗体类 system.windows.form

csharp 复制代码
namespace NotePad_Interface
{
    public interface DLLInterface
    {
        //规范:   规范程序员应该安装如何标准对我们的记事本程序进行二次开发

        //写什么

        //1.属性   用来定义 我们扩展功能的名称
        //只能获取不能设置
        string Name { get; }//.转大写   实现功能


        //2.方法 用来定义插件扩展的功能  object 泛型  委托.....(都不对)
        //必须要明确出来 Run方法的返回值和参数
        //具体写啥  一定要理解我们的业务需求
        //参数:1.首先拿到文本框  我们做修改 
        //步骤:1. 引用的窗体类 system.windows.form  拿到才可以把文本框 做参数传进来
        void Run(TextBox textbox);


    }
}

5.2.3步骤三、加载程序集

在Form窗体的Load事件中加载即可

csharp 复制代码
 private void Form1_Load(object sender, EventArgs e)
 {
     //1.允许我们的程序加载DLL程序集  进行功能的扩展
     //2.当我们加载的时候 Debug / Plug -in-Componets目录下面的相关的DLL
     //3.我们拿到了不同的开发入门扩展的DLL之后 我们应该如何去操作
     //第一:DLL文件应该去写 第二:拿到DLL之后我们应该做什么

     //当我们程序加载的时候 我们应该去读取Debug/Plugs-in-Components目录下的DLL
     //全路径: 如果写全路径 当我们的程序发布后给了别人之后怎么办  他可能项目不会放在e盘
     //相对路径  :获取

     //Assembly.GetExecutingAssembly().Location获取当前运行程序的程序集
     //MessageBox.Show(Assembly.GetExecutingAssembly().Location);

     //获取我们的程序集目录GetDirectoryName
     //MessageBox.Show(Path.GetDirectoryName(Assembly.GetExecutingAssembly().Location));

     //1.拼接我们要加载的程序集的路径
     string path = Path.Combine(Path.GetDirectoryName(Assembly.GetExecutingAssembly().Location), "Plug-in-Componets");

     //2.获取指定路径下 所以插件的全路径
     string[] strPath= Directory.GetFiles(path);
     //3.通过循环 获取每一个插件
     foreach (var item in strPath)
     {
         Assembly ass= Assembly.LoadFile(item);  //加载我们的程序集
         //每一个程序集都有属性方法字段等一大堆东西  我们需要实现DLLInterface的
         Type[] types= ass.GetExportedTypes();

         //判断实现DLLInterface的
         foreach (var item2 in types)
         {
             //两种情况  第一种 就是我们想要的  实现列接口的类的对象
             //第二种:继续了接口的接口和抽象类
             if (typeof(DLLInterface).IsAssignableFrom(item2)&& !item2.IsAbstract )
             {
                 //我们拿到对象  创建对象  Activator  执行构造函数
                 //执行构造函数 比较麻烦     (如果必须执行 请在开发接口文档里面写好)

                 DLLInterface o =(DLLInterface) Activator.CreateInstance(item2);
                 //获取对象的Name属性 赋值 给窗体的MenuStrip控件对象
                 //toolStripItem 添加完成对象
                 ToolStripItem toolStripItem =  menuStrip1.Items.Add(o.Name);
                 //给按钮注册应该单击事件
                 toolStripItem.Click += ToolStripItem_Click;

                 //创建插件对象   Tag :存放不重要的内容  id
                 toolStripItem.Tag = o;
             }
         }
     }


 }

主要代码解释:

csharp 复制代码
string path = Path.Combine(Path.GetDirectoryName(Assembly.GetExecutingAssembly().Location), "Plug-in-Componets");
  1. Assembly.GetExecutingAssembly():这个方法用于获取当前正在执行的程序集(Assembly)。程序集是包含程序代码和元数据的集合,通常是一个DLL或EXE文件。

  2. Location:这个属性返回程序集的文件路径。例如,如果程序集位于C:\MyApp\bin\Debug\MyApp.exe,那么.Location将返回"C:\MyApp\bin\Debug\MyApp.exe"。

  3. Path.GetDirectoryName(...):这个方法接收一个文件路径作为参数,并返回该文件所在目录的路径。在这个例子中,它将返回"C:\MyApp\bin\Debug"。

找到插件功能类的dll文件并复制到主应用程序中

相关推荐
黄油饼卷咖喱鸡就味增汤拌孜然羊肉炒饭13 分钟前
SpringBoot如何实现缓存预热?
java·spring boot·spring·缓存·程序员
暮湫30 分钟前
泛型(2)
java
超爱吃士力架34 分钟前
邀请逻辑
java·linux·后端
南宫生39 分钟前
力扣-图论-17【算法学习day.67】
java·学习·算法·leetcode·图论
转码的小石1 小时前
12/21java基础
java
李小白661 小时前
Spring MVC(上)
java·spring·mvc
GoodStudyAndDayDayUp1 小时前
IDEA能够从mapper跳转到xml的插件
xml·java·intellij-idea
百度智能云技术站2 小时前
广告投放系统成本降低 70%+,基于 Redis 容量型数据库 PegaDB 的方案设计和业务实践
数据库·redis·oracle
装不满的克莱因瓶2 小时前
【Redis经典面试题六】Redis的持久化机制是怎样的?
java·数据库·redis·持久化·aof·rdb
n北斗2 小时前
常用类晨考day15
java