2024年4月

(适用于.NET/.NET Core/.NET Framework)

【目录】
0.前言
1.第一个AOP程序
2.Aspect横切面编程
3.一个横切面程序拦截多个主程序
4.多个横切面程序拦截一个主程序
5.AOP的泛型处理
(扩充)
6.AOP的异步处理
(扩充)
7.优势总结
8.展望

0.前言

AOP(Aspect Oriented Programming)是“面向横切面编程”,主要是用来对程序/模块进行解耦。怎么理解??

我们可以把一般的编程理解为“纵向编程”(主程序),比如如下的一个示例代码:

        public string GetInfo(inti)
{
string s = "";if (i == 1)
s
= "A";else if (i == 2)
s
= "B";else if (i == 3)
s
= "C";elses= "Z";returns;
}

试想一下,上述软件实际使用后,

  • 如果条件变量i有更多的判断值,我们是不是要在GetInfo()方法内部修改代码+重新编译?
  • 如果后续需要加个日志记录功能,我们是不是也要在GetInfo()方法内部加上日志函数+重新编译?
  • 如果...
  • 更多如果...

为了避免上述的这些麻烦并增加软件的灵活性,“横向编程”,也就是AOP被创造了出来,它就像是“横切一刀”,把相关功能塞进了主程序。

现行AOP的实现,主要是通过拦截方法(即拦截主程序),并修改其参数+返回值来完成。

网上有很多相关方案,比如:特性注释拦截、动态代码生成、派遣代理模式、等。但这些方案要么实现的很复杂、要么耦合度没完全切断、逻辑有变化时还是需要修改代码重新编译。均不够理想。

而今天要隆重登场的主角-DeveloperSharp平台中的AOP技术,则提供了一种简便、快捷、彻底解耦的AOP实现。使用它,在程序逻辑有变化时,你只需要修改配置文件就行,而不再需要对主程序进行一丁丁点的代码修改!!

1.第一个AOP程序

制作一个AOP程序需要四个步骤:

(1)制作主程序

(2)制作横切面程序

(3)制作配置文件,让横切面程序拦截主程序

(4)调用主程序

下面,我们一步一步来实现上述四个步骤。

【第一步】:制作主程序

我们在Visual Studio中新建一个名为“School.Logic”的类库工程,并在该工程中新建一个名为PersonManage的类,该类中有一个名为GetInfo1的方法,代码如下:

//从NuGet引用DeveloperSharp包
usingDeveloperSharp.Structure.Model;namespaceSchool.Logic
{
//主程序必须继承自LogicLayer类 public classPersonManage : LogicLayer
{
public string GetInfo1(string Name, intNum)
{
return $"共有{Name}{Num}人";
}
}
}

以上,编写了一个非常简单的主程序。

【第二步】:制作横切面程序

我们再在Visual Studio中新建一个名为“School.Aspect”的类库工程,并在该工程中新建一个名为Interceptor1的类,代码如下:

//从NuGet引用DeveloperSharp包
usingDeveloperSharp.Structure.Model.Aspect;namespaceSchool.Aspect
{
//横切面程序必须继承自AspectModel类 public classInterceptor1 : AspectModel
{
//PreProcess方法先于主程序执行 public override void PreProcess(objectsender, AspectEventArgs e)
{
//把主程序的两个参数值改掉 e.MethodInfo.ParameterValues[0] = "老师";
e.MethodInfo.ParameterValues[
1] = 20;
}
//PostProcess方法后于主程序执行 public override void PostProcess(objectsender, AspectEventArgs e)
{

}
}
}

以上,编写了一个横切面程序。它的主要功能是把主程序方法的两个参数值给改掉。
AspectModel基类中的PreProcess方法会在主程序方法执行之前被执行,而PostProcess方法会在主程序方法执行之后被执行。它两就是AOP横向拦截的核心要素。它两均需要被override重写覆盖掉。

【第三步】:制作配置文件,让横切面程序拦截主程序

若是在.Net Core环境下,我们创建一个名为DeveloperSharp.json的配置文件,设置让Interceptor1拦截PersonManage中的GetInfo1方法。文件内容如下:

{"DeveloperSharp":
{
"AspectObject":
[
{
"name":"School.Aspect.Interceptor1", //横切面拦截器类 "scope":"School.Logic.PersonManage", //被拦截的主程序类 "method":"GetInfo1" //被拦截的方法 }
]
}
}

若是在.Net Framework环境下,我们创建一个名为DeveloperSharp.xml的配置文件,设置让Interceptor1拦截PersonManage中的GetInfo1方法。文件内容如下:

<?xml version="1.0" encoding="utf-8"?>
<DeveloperSharp>
  <AspectObject>
    <Aoname="School.Aspect.Interceptor1"scope="School.Logic.PersonManage"method="GetInfo1"/>
  </AspectObject>
</DeveloperSharp>

注意:以上配置中所有的类名,都要用完全限定名。

【第四步】:调用主程序

最后,我们再在Visual Studio中创建一个控制台工程,让它来调用主程序中的GetInfo1方法,代码如下:

        //需要引用School.Aspect、School.Logic、DeveloperSharp三项
        static void Main(string[] args)
{
var pm = newSchool.Logic.PersonManage();//要用这种形式调用主程序中的方法,AOP功能才会生效 var str = pm.InvokeMethod("GetInfo1", "学生", 200);
Console.WriteLine(str);

Console.ReadLine();
}

附注:有人会觉得上述InvokeMethod这种调用方法不够优雅,但事实上ASP.NET Web Api也是被类似InvokeMethod这种方式包裹调用才实现了各种Filter拦截器的拦截(本质也是AOP),只不过它的这个InvokeMethod动作是在.NET自身的CLR管道运行时中进行的。而且,那些Filter拦截器还只能用于ASP.NET Web Api环境,而不能像本方案这样用于一般程序。

现在,为了让前面第三步创建的配置文件生效,我们此时还需要在此主调项目中对它进行链接:
若是在.Net Core环境下,我们只需要把DeveloperSharp.json文件放到程序执行目录中(即bin目录下与dll、exe等文件的同一目录中,放错了位置会报错)(注意:有些.Net Core版本在Visual Studio“调试”时,不会在bin目录下生成全部的dll、exe,此时需要把此配置文件放在应用程序的“根目录”下)。

若是在.Net Framework环境下,我们需要在工程配置文件App.config/Web.config中添加appSettings节点,节点内容如下:

  <appSettings>
    <addkey="ConfigFile"value="D:\Test\Assist\DeveloperSharp.xml" />
  </appSettings>

此处需要设置为配置文件的“绝对路径”(使用“绝对路径”而不是“相对路径”,一是有利于安全性,二是有利于分布式部署)

一切准备完毕,运行,结果如下:

【控制台显示出】:
共有老师20人

可见AOP已经拦截成功。

若此时,我们在配置文件DeveloperSharp.json/DeveloperSharp.xml中稍做修改,比如:把“GetInfo1”这个方法名改为“ABC”这样一个不存在的方法名,再运行,结果如下:

【控制台显示出】:
共有学生200人

2.Aspect横切面编程

上面,第二步,制作的横切面程序,是通过修改主程序方法的参数值,而最终改变了主程序的返回值。

其实,我们也有办法直接修改主程序方法的返回值,比如把上面Interceptor1类的代码修改为如下:

//从NuGet引用DeveloperSharp包
usingDeveloperSharp.Structure.Model.Aspect;namespaceSchool.Aspect
{
//横切面程序必须继承自AspectModel类 public classInterceptor1 : AspectModel
{
//PreProcess方法先于主程序执行 public override void PreProcess(objectsender, AspectEventArgs e)
{

}
//PostProcess方法后于主程序执行 public override void PostProcess(objectsender, AspectEventArgs e)
{
//把主程序的返回值改掉 e.MethodInfo.ReturnValue = $"共有校长2人";
}
}
}

运行,结果如下:

【控制台显示出】:
共有校长2人

到目前为止,我们已经知道了如何通过“Aspect横切面程序”修改主程序方法的参数值、返回值。

如果我们想进一步获取主程序的“命名空间”、“类名”、“方法名”、“参数名”、“参数类型”、“返回值类型”,则可以通过如下代码获取:

e.MethodInfo.NamespaceName                       //命名空间
e.MethodInfo.ClassName                           //类名
e.MethodInfo.MethodName                          //方法名
e.MethodInfo.ParameterInfos[0].Name              //参数名(第一个参数)
e.MethodInfo.ParameterInfos[0].ParameterType     //参数类型(第一个参数)
e.MethodInfo.ReturnValue.GetType()               //返回值类型

有时候,在某些特殊情况下,我们希望主程序方法不运行,此时则可以通过在PreProcess方法里把e.Continue设置为false来完成。

接前面的“第一个AOP程序”,比如:我们希望当人数大于10000时,主程序方法就不再运行,则可以通过把Interceptor1类的代码修改为如下样式来实现:

//从NuGet引用DeveloperSharp包
usingDeveloperSharp.Structure.Model.Aspect;namespaceSchool.Aspect
{
//横切面程序必须继承自AspectModel类 public classInterceptor1 : AspectModel
{
//PreProcess方法先于主程序执行 public override void PreProcess(objectsender, AspectEventArgs e)
{
//当人数大于10000时,主程序方法就不再运行 if (Convert.ToInt32(e.MethodInfo.ParameterValues[1]) > 10000)
e.Continue
= false;
}
//PostProcess方法后于主程序执行 public override void PostProcess(objectsender, AspectEventArgs e)
{

}
}
}

现在的这个示例是一个Aspect横切面程序拦截一个主程序。在后续将要讲解的“多个Aspect横切面程序拦截一个主程序”的情况中,只要有一个e.Continue=false被设置,主程序方法就不会运行(在此事先提点)。

3.一个横切面程序拦截多个主程序

为了演示这部分的内容,我们首先在前面“第一个AOP程序”的基础上,把主程序进行扩充。采取的动作是:

(1)在PersonManage类中增加一个GetInfo2方法

(2)再新增一个主程序类SystemManage,该类中有一个名为GetMessage1的方法。代码如下:

PersonManage类:

//从NuGet引用DeveloperSharp包
usingDeveloperSharp.Structure.Model;namespaceSchool.Logic
{
//主程序必须继承自LogicLayer类 public classPersonManage : LogicLayer
{
public string GetInfo1(string Name, intNum)
{
return $"共有{Name}{Num}人";
}
public string GetInfo2(string Name, intNum)
{
return $"学校共有{Name}{Num}人";
}
}
}

SystemManage类:

//从NuGet引用DeveloperSharp包
usingDeveloperSharp.Structure.Model;namespaceSchool.Logic
{
//主程序必须继承自LogicLayer类 public classSystemManage : LogicLayer
{
public string GetMessage1(string Name1, int Num1, string Name2, intNum2)
{
return $"第一组共有{Name1}{Num1}人,第二组共有{Name2}{Num2}人";
}
}
}

如此一来,现在就有了3个主程序方法。

接下来,我们修改配置文件,让Interceptor1去拦截上述的3个主程序方法。

若是在.Net Core环境下,DeveloperSharp.json文件的内容修改为如下:

{"DeveloperSharp":
{
"AspectObject":
[
{
"name":"School.Aspect.Interceptor1","scope":"School.Logic.PersonManage","method":"*" //星号*代表该作用域下的全部方法 },
{
"name":"School.Aspect.Interceptor1","scope":"School.Logic.SystemManage","method":"GetMessage1"}
]
}
}

若是在.Net Framework环境下,DeveloperSharp.xml文件的内容修改为如下:

<?xml version="1.0" encoding="utf-8"?>
<DeveloperSharp>
  <AspectObject>
    <Aoname="School.Aspect.Interceptor1"scope="School.Logic.PersonManage"method="*"/>
    <Aoname="School.Aspect.Interceptor1"scope="School.Logic.SystemManage"method="GetMessage1"/>
  </AspectObject>
</DeveloperSharp>

最后,我们把控制台启动程序修改为如下:

        //需要引用School.Aspect、School.Logic、DeveloperSharp三项
        static void Main(string[] args)
{
var pm = newSchool.Logic.PersonManage();var sm = newSchool.Logic.SystemManage();//要用这种形式调用主程序中的方法,AOP功能才会生效 var str1 = pm.InvokeMethod("GetInfo1", "学生", 200);var str2 = pm.InvokeMethod("GetInfo2", "学生", 200);var str3 = sm.InvokeMethod("GetMessage1", "学生", 200, "院士", 10);
Console.WriteLine(str1);
Console.WriteLine(str2);
Console.WriteLine(str3);

Console.ReadLine();
}

运行结果如下:

【控制台显示出】:

共有老师20人
学校共有老师20人
第一组共有老师20人,第二组共有院士10人

可见AOP所有拦截均已成功!

4.多个横切面程序拦截一个主程序

为了演示这部分的内容,我们还是要先回到前面的“第一个AOP程序”,在它的基础上,我们新增一个名为Interceptor2的Aspect横切面类,代码如下:

//从NuGet引用DeveloperSharp包
usingDeveloperSharp.Structure.Model.Aspect;namespaceSchool.Aspect
{
//横切面程序必须继承自AspectModel类 public classInterceptor2 : AspectModel
{
//PreProcess方法先于主程序执行 public override void PreProcess(objectsender, AspectEventArgs e)
{
//把主程序的两个参数值改掉 e.MethodInfo.ParameterValues[0] = "辅导员";
e.MethodInfo.ParameterValues[
1] = 40;
}
//PostProcess方法后于主程序执行 public override void PostProcess(objectsender, AspectEventArgs e)
{

}
}
}

如此一来,我们就有了2个Aspect横切面程序Interceptor1与Interceptor2。

接下来,我们修改配置文件,让Interceptor1、Interceptor2都去拦截主程序方法GetInfo1。

若是在.Net Core环境下,DeveloperSharp.json文件的内容修改为如下:

{"DeveloperSharp":
{
"AspectObject":
[
{
"name":"School.Aspect.Interceptor1","scope":"School.Logic.PersonManage","method":"GetInfo1"},
{
"name":"School.Aspect.Interceptor2","scope":"School.Logic.PersonManage","method":"GetInfo1"}
]
}
}

若是在.Net Framework环境下,DeveloperSharp.xml文件的内容修改为如下:

<?xml version="1.0" encoding="utf-8"?>
<DeveloperSharp>
  <AspectObject>
    <Aoname="School.Aspect.Interceptor1"scope="School.Logic.PersonManage"method="GetInfo1"/>
    <Aoname="School.Aspect.Interceptor2"scope="School.Logic.PersonManage"method="GetInfo1"/>
  </AspectObject>
</DeveloperSharp>

上述修改完毕,运行控制台主调程序,结果如下:

【控制台显示出】:
共有辅导员40人

从上述运行结果,我们大致可以推断出:Interceptor1、Interceptor2这两个Aspect横切面拦截器是按配置顺序执行的。其中,Interceptor1先把GetInfo1方法的两个参数值改为了("老师",20),接着,Interceptor2又把GetInfo1方法的两个参数值改为了("辅导员",40),所以最终GetInfo1方法的参数值变为了("辅导员",40)。

5.AOP的泛型处理
如果我们的主程序是泛型方法,则需要用InvokeMethod<T>这种方式来进行调用。

比如,现有如下的主程序代码:

//从NuGet引用DeveloperSharp包
usingDeveloperSharp.Structure.Model;namespaceTest4Logic
{
//主程序必须继承自LogicLayer类 public classCalculate : LogicLayer
{
public int add(int i, intj)
{
return i +j;
}
public int add(int i, int j, intk)
{
return i + j +k;
}
public string add<T>(T i, T j, T k)
{
return "T" + i.ToString() + j.ToString() +k.ToString();
}
public string add<T, V>(T i, T j, V k)
{
return "TTV" + i.ToString() + j.ToString() +k.ToString();
}
public string add<T, V>(T i, V j, V k)
{
return "TVV" + i.ToString() + j.ToString() +k.ToString();
}
}
}

对应的Aspect横切面类代码如下:

//从NuGet引用DeveloperSharp包
usingDeveloperSharp.Structure.Model.Aspect;namespaceTest4Aspect
{
//横切面程序必须继承自AspectModel类 public classInterceptor : AspectModel
{
//PreProcess方法先于主程序执行 public override void PreProcess(objectsender, AspectEventArgs e)
{
//把主程序的第一个参数值改掉 e.MethodInfo.ParameterValues[0] = 8;

}
//PostProcess方法后于主程序执行 public override void PostProcess(objectsender, AspectEventArgs e)
{

}
}
}

对应的配置文件如下:
若是在.Net Core环境下,DeveloperSharp.json文件的内容如下:

{"DeveloperSharp":
{
"AspectObject":
[
{
"name":"Test4Aspect.Interceptor", //横切面拦截器类 "scope":"Test4Logic.Calculate", //被拦截的主程序类 "method":"add" //被拦截的方法 }
]
}
}

若是在.Net Framework环境下,DeveloperSharp.xml文件的内容如下:

<?xml version="1.0" encoding="utf-8"?>
<DeveloperSharp>
  <AspectObject>
    <Aoname="Test4Aspect.Interceptor"scope="Test4Logic.Calculate"method="add"/>
  </AspectObject>
</DeveloperSharp>

控制台主调程序代码如下:

        //需要引用Test4Aspect、Test4Logic、DeveloperSharp三项
        static void Main(string[] args)
{
var cal = newTest4Logic.Calculate();//要用这种形式调用主程序中的方法,AOP功能才会生效 var r1 = cal.InvokeMethod("add", 1, 2);var r2 = cal.InvokeMethod("add", 1, 2, 3);var r3 = cal.InvokeMethod<int>("add", 1, 2, 3);var r4 = cal.InvokeMethod<int, float>("add", 1, 2, (float)3);var r5 = cal.InvokeMethod<int, float>("add", 1, (float)2, (float)3);

Console.WriteLine(r1);
Console.WriteLine(r2);
Console.WriteLine(r3);
Console.WriteLine(r4);
Console.WriteLine(r5);

Console.ReadLine();
}

运行上述控制台主调程序,结果如下:

【控制台显示出】:
10
13
T823
TTV823
TVV823

主程序中每个泛型方法的对应调用一目了然。

6.AOP的异步处理
如果我们的主程序是异步方法,还是使用InvokeMethod来进行调用。下面给出一个代码样式示例(代码做了简化处理):

//主程序//主程序必须继承自LogicLayer类
    public classUserService : LogicLayer
{
public async Task<Worker> GetUser(string Id, int Age, stringName)
{
//...相关代码... }public async Task<T> GetUser<T>(string Id, int Age, string Name) where T : User, new()
{
//...相关代码... }
}
---------------------------------------------------------------------- //主调程序 var us = newUserService();//要用这种形式调用主程序中的方法,AOP功能才会生效 var worker = await us.InvokeMethod("GetUser", "C007", 26, "alex");var user = await us.InvokeMethod<Manager>("GetUser", "A002", 46, "kevin");

Console.WriteLine(worker.Name);
Console.WriteLine(user
?.Name);

即然主程序可以是异步的,那Aspect横截面拦截程序能不能也是异步的了?答案是肯定的。你可以把PreProcess与PostProcess中的至少一个改为异步方法,实现单个Aspect类的同步异步混用,其代码结构与原先的同步Aspect类一致,这点连.NET/微软自身都还没有实现...
下面给出一个示例代码:

    //横切面程序必须继承自AspectModel类
    public classUserInterceptor : AspectModel
{
//PreProcess方法先于主程序执行 public override void PreProcess(objectsender, AspectEventArgs e)
{

}
//PostProcess方法后于主程序执行 public override async void PostProcess(objectsender, AspectEventArgs e)
{
await Task.Run(() =>{
Thread.Sleep(
10000);
File.AppendAllText(
"D:/zzz.txt", "耗时操作");
});
}
}

7.优势总结

本文所讲述的,是全网唯一实现AOP彻底解耦的技术方案。使用它,当你需要给主程序增加Aspect横切面拦截器时,无论是增加一个还是多个,都不再需要修改&重新编译主程序。这实现了不同功能构件之间的0依赖拼装/拆卸开发方式,随之而来的也会对研发团队的管理模式产生重大影响,意义深远...

8.展望

AOP对于程序代码的解耦、业务模块的拆分与拼装组合,有着巨大的作用。正确的使用AOP,甚至能对传统的软件架构设计,产生颠覆性的影响,如超级战士出场一般,让所有人刮目相看,完全耳目一新!!

为了让读者能直观感知AOP的上述神奇魅力,下面给出一个业务案例:
有一批货品要录入数据库,货品包含长、宽、高、颜色、类型等属性。现在有业务需求如下,
(1)当货品长度大于10厘米时,它在数据库中标记为A类;当货品长度大于20厘米时,标记为B类。
(2)当货品颜色无法分辨时,统一在数据库中默认标记为白色。
(3)每个货品录入数据库后,还要在另一个财务数据库中录入该货品的价格信息,同时把该货品的操作员名字记入日志文件。

这样的一个业务案例,你以前会怎么设计这个程序?今天学了AOP后你又会怎么设计程序?你会创建几个Aspect横切面了...?


原文首发于下方公众号,请关注!

向大佬学习,探行业内幕,享时代机遇。

Ubuntu是一个基于Linux的开源操作系统,它遵循GNU通用公共许可证,用户可以自由使用、复制、分发和修改。它提供直观易用的桌面环境,适合新手和有经验用户。Ubuntu有强大的软件中心,支持多硬件架构,注重安全和稳定,并有庞大的用户社区提供支持。它适用于桌面、笔记本和服务器等多种设备,被广泛应用于教育、开发和科学等领域。

接下来就为大家介绍一下Ubuntu操作系统的安装与配置

一、安装前准备

请确保电脑中已经安装了VMware和Electerm,如果没有安装的小伙伴可以点击下面的链接查看安装教程:

需要安装的软件名称 链接
虚拟机软件VMware Workstation 点我查看
远程连接软件Electerm 点我查看
下载器Neat Download Manager(推荐安装,可以加快下载速度) 点我查看

建议电脑预留50G的存储空间。

二、下载Ubuntu镜像

截止到2024年2月,Ubuntu有两个长期支持的版本:22.04版本和20.04版本。这里我选择安装的是22.04版本的Ubuntu服务器。

1. 点击右侧连接进入清华大学镜像站:
点我查看

2. 这里我选择的是22.04版本下载,点击进入:

3. 下载服务器端的Ubuntu,如下图所示:

三、创建Ubuntu虚拟机

1. 打开VMware,按
Ctrl

N
键,新建虚拟机。

2. 进入新建虚拟机向导以后,选择第二个
自定义(高级)
,完成后点击
下一步

3. 点击
下一步

4. 安装客户机操作系统选择
稍后安装操作系统
,完成后点击
下一步

5. 客户机操作系统选择
Linux
,版本选择
Ubuntu 64位
,完成后点击
下一步

6. 自定义虚拟机名称和安装位置。安装位置建议安装在一个空间比较大的盘,这里我安装在了J盘:

7. 处理器配置时处理器数量和内核数量不能超过电脑自身的数量,否则虚拟机无法正常运行,这里我设置的
处理器内核总数
为2:

如何检查电脑本机的CPU信息:按
Ctrl
Shift
Esc
,打开任务管理器,找到性能,即可查看到CPU信息:

8. 设置虚拟机内存,内存大小按照VMware的要求设置在一定范围之内。这里我设置内存大小为4GB(4096M),完成后点击
下一步

9. 网络类型选择
网络地址转换(NAT)
,完成后点击
下一步

10. I/O控制器类型按照系统默认选择即可,然后点击
下一步

11. 虚拟磁盘类型按照默认选择即可,完成后点击
下一步

12. 选择磁盘按照系统默认选择即可,然后点击
下一步

13. 最大磁盘大小建议设置在20GB及以上,这里我设置了50GB,磁盘分配按照默认勾选即可。完成后点击
下一步

14. 指定磁盘文件位置可以自定义。这里需要设置的小伙伴点击
浏览
可以更改。不需要更改的小伙伴点击
下一步

15. 点击
完成
,虚拟机创建完成:

16. 点击
编辑虚拟机设置

17. 进入虚拟机设置后,左侧设备选择
CD/DVD
,设备状态勾选
启动时连接
,连接选择
使用ISO映像文件
,点击
浏览

18. 找到前面我们下载的Ubuntu镜像并选中,完成后点击右下角
打开

19. 镜像配置成功,点击
确定

四、开启虚拟化

1. 开启刚刚创建好的虚拟机,此时VMware会弹出一个错误信息:

2. 此时按
Ctrl
Shift
Esc
,打开任务管理器,找到性能,虚拟化并未开启:

3. 重新启动电脑,具体进入BIOS可以根据自身电脑品牌型号进入。这里我的电脑使用的是华硕,开机过程中一直按
F2
键即可进入BIOS,再点击右下角
Advanced Mode
,进入高级模式:

4. 按照下图所示操作,点击
高级
,将
Intel Virtualization Technology
配置项开启:

5. 按
F10
键保存上述配置并重启:

6. 按
Ctrl
Shift
Esc
,打开任务管理器,左上角找到
性能
,发现虚拟化成功开启:

五、安装Ubuntu操作系统

1. 开启刚刚创建好的虚拟机:

2. 进入安装界面,选择第一个
Try or Install Ubuntu Server
,然后按一下回车/:

3. 此时会加载Ubuntu的安装界面,请耐心等待。

4. 加载完成后 ,进入Ubuntu安装界面,安装语言选择
English
,完成后按一下回车:

5. (无下图内容可以跳转到下一步)此时弹出安装器可更新提示,下方选项选择第二个
Continue without updating
(不更新,继续安装),完成后按一下回车:

6. 键盘布局按照系统默认使用英文(美国)布局即可,选择
Done
,按一下回车:

7. 安装类型选择第一个
Ubuntu Server
即可,完成后选择
Done
,按一下回车:

8. 网络连接:如果电脑本机已经连接网络,此时虚拟机为我们提供了一个IP地址,按照默认配置选择即可。选择
Done
,按一下回车:

9. 这里我们不需要配置代理。选择
Done
,按一下回车:

10. 配置镜像地址,这里我们将原有的镜像地址替换为清华大学镜像地址:

http://mirrors.tuna.tsinghua.edu.cn/ubuntu

完成后,选择Done,按一下回车:

11. 配置存储位置,按照默认选择即可,按向上/下键可以选择,选择
Done
,按一下回车:

12. 存储大小配置,按照默认配置即可,选择
Done
,按一下回车:

13. 此时会弹出一个确认提示,选择Continue会开始安装,会替换掉原有的磁盘空间,这里我们选择
Continue
,按一下回车:

14. 配置用户名和密码,如下图所示,完成后选择
Done
,按一下回车:

个人名称、服务器名称、用户名可以自定义(要求是:英文小写或者英文小写+数字)

15. 是否升级到Ubuntu Pro,这里我们选择
Skip for now
(不升级),完成后选择
Continue
,按一下回车:

16. 这里我们将光标移动到Install OpenSSH server,按空格键勾选安装OpenSSH。然后选择
Done
,按一下回车:

17. 上述组件不需要安装,选择Done,按一下回车:

18. 安装完成,选择第二个
Cancel Update And Reboot
(取消更新并重启):

19. 重启中,请耐心等待(预计需要5分钟以上)。出现下面界面以后,按一下回车,继续重启:

20. 等待了3~5分钟以后,会出现登录页面,输入以下用户名(这里我设置的是
icode504
)和密码(这里我设置的是
123456
,密码不会在命令行中显示)。输入完成后会出现欢迎界面,此时我们可以在命令行中输入命令了:

六、为root用户分配密码

作为最高权限的root用户,我们在安装过程中并没有给root用户分配密码。

执行如下命令,为root用户分配密码:

sudo passwd

此时会输入两次密码(不会在控制台显示)。为了方便记忆,我将root密码设置成123456:

切换到root用户,执行如下命令:

su root

此时Ubuntu会提醒我们输入密码,完成后按一下回车,此时成功切换到root用户:

七、使用SSH工具(Electerm)远程连接Ubuntu

请保证当前宿主机(电脑本机)处于联网状态:

1. 开启SSH服务:

service ssh start

2. 检查SSH服务是否开启,执行如下命令:

systemctl status sshd

下图状态说明SSH服务已经成功开启:

3. 由于
root
用户默认是不能使用SSH的方式登录,因此我们需要更改一下对配置文件
/etc/ssh/sshd_config
进行修改。执行如下命令:

sudo vim /etc/ssh/sshd_config

4. 此时会进入
sshd_config
配置文件中,我们依次执行如下命令:

:set nu
/PermitRootLogin

此时我们在33行可以看到如下信息
PermitRootLogin prohibit-password
,这段配置的含义是使用SSH方式登录root用户是禁止的,因此我们需要对这段代码进行修改:

5. 按
i
键进入编辑模式,需要对32-34行代码修改成下图所示的效果:

6. 完成编辑后,先按一下
Esc
键,再输入
:wq
保存并退出编辑。

7. 重启SSH服务,执行如下命令:

systemctl restart sshd

8. 在Ubuntu命令行中查看防火墙状态,在命令行中输入如下命令:

sudo ufw status

此时会提示输入密码(这里我已经输入过了)后即可查看防火墙状态是
inactive
(未开启):

9. 为了保障系统安全,我们需要开启防火墙,只开放特定的端口。输入如下命令开启防火墙:

sudo ufw enable

再次查看防火墙状态,此时防火墙已经开启:

sudo ufw status

10. SSH默认是22号端口,此时我们需要开启22号端口。执行如下命令:

sudo ufw allow 22/tcp

执行成功,规则已添加:

11. 输入如下命令,查看Ubuntu的IP地址:

ip addr

12. 打开Electerm,点击左侧的书签:

13. 按照下图操作填写连接信息:

14. 向下找,点击
测试连接

等待一段时间后,如果上方出现一个
connection is ok
,说明前面填写内容没有问题:

如果出现的时
connection is failed
,说明填写的内容有问题,需要更改后再次测试连接。

15. 测试连接成功后,点击
保存并连接
后,此时我们就可以在Electerm中登录root用户并执行命令了:

昨天我们聊到KG在RAG中如何发挥作用,今天我们来看一个具体的例子。 我们找到一篇论文:
https://arxiv.org/abs/2311.17330
,论文的研究人员开发了一种名为知识图谱增强的提示生成(KG-RAG)框架(
https://github.com/BaranziniLab/KG_RAG),该框架利用生物医学知识图谱SPOKE与大型语言模型相结合,有效的提升了LLM在医疗领域的问答效果。

KG-RAG框架介绍

KG-RAG框架,较好的结合了生物医学知识图谱SPOKE和LLM的优势。SPOKE是一个开放知识图谱,提供数据下载和开放API,整合了超过40个公开可用的生物医学知识源,涵盖了基因、蛋白质、药物、化合物、疾病等概念和概念之间的关系,可以为LLM提供一个强大的医疗领域知识。

研究人员对KG-RAG框架进行了广泛的测试,包括单跳和双跳提示、药物再利用查询、生物医学真假问题和多项选择题。结果表明,KG-RAG显著提高了LLMs的性能,特别是在具有挑战性的多项选择题数据集上,LLMs都取得了较大的提升。此外,KG-RAG还能够提供有意义的药物再利用建议,并在回答中体现出对临床试验必要性的谨慎态度。

使用KG-RAG与不使用,蓝框是不使用,绿框是使用

相关测试结果

LLM测试结果

工作原理

KG-RAG框架的工作原理包括以下步骤:

  1. 实体识别
    :从用户输入的query中识别出疾病实体,然后在SPOKE知识图谱中找到相应的节点。
  2. 上下文提取
    :从SPOKE知识图谱中提取与疾病节点相关的上下文信息,并将其转换为自然语言。
  3. 提示组装
    :将提取的上下文与原始prompt结合。
  4. 文本生成
    :使用LLM(如Llama-2-13b、GPT-3.5-Turbo或GPT-4)生成有意义的生物医学文本。

KG-RAG框架原理

实体识别

区别于用小模型去做NER,KG-RAG里使用LLM识别实体。

1. 实体抽取(Disease Entity Extraction)

在KG-RAG框架中,这一过程是通过零样本提示(zero-shot prompting)实现的。研究人员设计了一个高效的抽取prompt,引导大型语言模型(如GPT-3.5-Turbo)从输入文本中提取疾病实体,并将结果以JSON格式返回。

def disease_entity_extractor_v2(text):  
    chat_model_id, chat_deployment_id = get_gpt35()  
    prompt_updated = system_prompts["DISEASE_ENTITY_EXTRACTION"] + "\n" + "Sentence : " + text  
    resp = get_GPT_response(prompt_updated, system_prompts["DISEASE_ENTITY_EXTRACTION"], chat_model_id, chat_deployment_id, temperature=0)  
    try:  
        entity_dict = json.loads(resp)  
        return entity_dict["Diseases"]  
    except:  
        return None

这里的
DISEASE_ENTITY_EXTRACTION
:

  You are an expert disease entity extractor from a sentence and report it as JSON in the following format:  
  Diseases: <List of extracted entities>  
  Please report only Diseases. Do not report any other entities like Genes, Proteins, Enzymes etc.

2. 实体链接(Entity Matching to SPOKE)

疾病实体抽取出来后,下一步就是将这些实体与SPOKE知识图谱中的疾病实体进行匹配,也就是传统NLP任务中的实体链接,KG-RAG这个框架中采用的方法是,用语义相似度的方式来做。

  • 实体embedding计算:首先,使用Embedding模型(如'all-MiniLM-L6-v2')为SPOKE知识图谱中的所有疾病概念节点计算embedding向量
  • 将计算出的疾病embedding存储在向量数据库(如'Chroma')中,以便快速检索。
  • 语义搜索匹配:将LLM提取的疾病实体与向量数据库中的疾病实体进行比较,选择最相似的

当然,如果零样本方法未能识别出疾病实体,采取的办法是直接拿原始query去匹配,取top 5。

最终,实体匹配过程会输出与输入文本提示中的疾病实体最相关的SPOKE知识图谱节点。这些节点及其相关信息将用于后续的上下文提取和文本生成步骤。通过这种方法,KG-RAG框架能够有效地从专业文本中提取和识别疾病实体,并将其与丰富的生物医学知识库相连接,从而生成准确、可靠的生物医学相关信息。

子图查询与剪枝

子图查询

在得到具体的实体后,紧接着就是从KG中去查询这个实体关联的子图,这些信息通常以三元组(Subject, Predicate, Object)的形式存在,表示不同的生物医学关系。通常情况下,可以查询1~3跳内的三元组信息,这里借助图数据库可以比较容易的实现。

得到的三元组信息,LLM可能不太能比较好的理解,这里就需要将三元组转换成自然语言,以便与输入提示结合并用于后续的文本生成。举个例子:

(Disease hypertension, ASSOCIATES_DaG, Gene VHL) → `Disease hypertension associates Gene VHL`

上下文剪枝

在KG-RAG框架中,Context Pruning(上下文剪枝)是一个关键步骤,就和dfs遍历时,需要剪枝来减少遍历时间一样,这里的剪枝可以减少给LLM的信息,减少token数量,同时过滤掉一些无用信心,还能提升LLM回答的精确性。

Context Pruning的具体做法还是会基于embedding来计算语义相似度,大概就是使用embedding模型计算三元组和query的cos相似度,最后选择策略:

  • 条件一
    :上下文关联的余弦相似度必须大于所有提取上下文关联的相似度分布的75%分位
  • 条件二
    :余弦相似度的最小值必须达到0.5

通过这个0.5 和 75%,可以有效减少给LLM的无效信息,有助于提高后续文本生成的准确性和相关性。

提示组装与文本生成

这里就简单了,就是和question一起,组合为propmt,再加上SYSTEM_PROMPT,送给LLM回答:

question = row["text"]  
#检索
context = retrieve_context(question, vectorstore, embedding_function_for_context_retrieval, node_context_df, context_volume, QUESTION_VS_CONTEXT_SIMILARITY_PERCENTILE_THRESHOLD, QUESTION_VS_CONTEXT_MINIMUM_SIMILARITY, edge_evidence)  
# 
enriched_prompt = "Context: "+ context + "\n" + "Question: " + question  
output = get_GPT_response(enriched_prompt, SYSTEM_PROMPT, CHAT_MODEL_ID, CHAT_DEPLOYMENT_ID, temperature=TEMPERATURE)  
if not output:
enriched_prompt = "Context: "+ context + "\n" + "Question: "+ question

这里的SYSTEM_PROMPT:

# One-Hop Validation  
SINGLE_DISEASE_ENTITY_VALIDATION: |  
  You are an expert biomedical researcher. For answering the Question at the end, you need to first read the Context provided.  
  Then give your final answer by considering the context and your inherent knowledge on the topic. Give your answer in the following JSON format:  
    {Compounds: <list of compounds>, Diseases: <list of diseases>}  
  
# Two-Hop Validation  
TWO_DISEASE_ENTITY_VALIDATION: |  
  You are an expert biomedical researcher. For answering the Question at the end, you need to first read the Context provided.  
  Then give your final answer by considering the context and your inherent knowledge on the topic. Give your answer in the following JSON format:  
    {Nodes: <list of nodes>}

运行的结果举例:

question = 'Does drug dependence have any genetic factors? Do you have any statistical evidence from trustworthy sources for this?'
KG_RAG_FLAG = True  
EDGE_EVIDENCE_FLAG = True   
generate_response(question, LLM_TO_USE, KG_RAG_FLAG, evidence_flag=EDGE_EVIDENCE_FLAG, temperature=TEMPERATURE)

Yes, drug dependence does have genetic factors. The genes KAT2B and SLC25A16 have been associated with drug dependence. This information is backed by statistical evidence from the GWAS Catalog, with p-values of 4e-10 and 1e-09 respectively.

KG-RAG 在应用中落地思考

KG-RAG 给出了如何结合KG来做RAG的一个有效方案,但这里再工业场景中落地,还有很多是我们细致去思考的。比如NER实体识别这里,通过LLM来抽取,再来做entity link,这里的效率肯定是感人的,其实这里传统的bert模型就可以了,成本可以忽略不计。

再则,剪枝这里,原始的实现效率是很低的,这里的embedding模型也需要专门去微调训练。三元组转换成自然语言,这里也是有讲究,如何生成更通顺的自然语言,更好的做法LLM+人工,确定好模版,通过模版生成。另外,是先是被实体,然后去查询实体的关联子图,还是全图查询,通过实体来过滤,都是可以考虑的点。

总结

KG-RAG框架通过结合生物医学知识图谱和LLM,为生物医学领域的问题提供了通用的解决方案。不仅提高了模型的性能,而且简化了流程,使其更具成本效益和时间效率。

在其他领域如何去应用KG做RAG,一方面可以扩展该框架,另外一方面,也要结合自己的实际场景去定制具体的策略。

本文示例代码已上传至我的
Github
仓库
https://github.com/CNFeffery/DataScienceStudyNotes

1 简介

大家好我是费老师,在日常编写
Python
代码的过程中,由于个人经验及编程习惯上的差异,有些人写出的代码可读性很高,一眼看上去就非常整洁易懂,而有些人写出的代码则十分“潦草随意”,读起来颇为费劲。

想要写出格式工整、可读性强的
Python
代码,除了需要在编写大量代码的过程中逐渐养成良好习惯外,还可以结合
代码格式化
工具实现代码的自动格式美化,经典的
Python
代码格式化工具有
autopep8

black

yapf
等,均可在
vscode

pycharm
等主流
ide
中安装相关插件进行快捷使用。


而去年大火的代码静态分析工具
ruff
,主要功能中也包含了代码格式化功能,由于其底层基于
rust
编写,因此执行相关功能时的运算速度超快,据官方称可达到其他同类型工具的10~100倍之多⚡,今天的文章中,费老师我就将为大家介绍基于
ruff
的代码格式化常用功能

引言

随着技术的进步,许多企业开始考虑将他们的数据从Oracle迁移到更现代、成本效益更高的数据库系统如MySQL或PostgreSQL。本文将详细描述我们如何进行这样的数据迁移过程。

一、前期准备工作

1.搭建新的MySQL数据库

​ 首先,我们需要设置一个新的MySQL数据库环境,这将作为我们的新数据源。这包括安装MySQL服务器,创建数据库,以及配置适当的用户权限。

2 .建立相应的数据表

​ 我们可以使用PowerDesigner等数据表模型设计工具,将Oracle的模型转换成MySQL模型,然后根据这个模型生成DDL脚本。这些脚本可能需要根据实际情况进行一些修改。例如,我们可能需要调整字段类型以适应MySQL的特性,或者修改索引和约束的定义。

2.1 数据库兼容性分析

2.1.1 字段类型兼容性分析

以下是常用的oracle字段类型和和mysql字段类型的对应关系 ,如果使用特殊的字段类型,需要检查确认字段转换是否符合真实需求。

oracle字段类型 mysql字段类型
varchar2 varchar
number(1,0))->number(2,0) tinyint
number(3,0)->number(4,0) smallint
number(5,0)->number(6,0) mediumint
number(7,0)->number(9,0) int
number(10,0) -> number(18,0) bigint
number(x,y) decimal(x,y)
date datetime
timestamp(6) datetime
char varchar
clob Text 或 Midiumtext 或 longtext

2.1.2 函数兼容性分析

Oracle和MySQL的函数有一定对的相似性也要有一定的区别,下面表格列出了Oracle和MySQL常用函数的对比和区别。

功能 oracle函数 mysql函数 备注
舍入函数 round round 一样
取绝对值 abs abs 一样
返回 expr 的最小或最大值 Max(expr)/Min(expr) Max(expr)/Min(expr) 一样
在字符串 str 中所有出现的字符串 from_str 均被 to_str 替换 REPLACE(str,from_str,to_str) REPLACE(str,from_str,to_str) 一样
截取函数 SUBSTR('abcd',2,2) substring('abcd',2,2) 函数名称不同
获取长度 length(str) char_length() 函数名称不同
转大写 UPPER(str) UPPER(str) 一样
转小写 LOWER(str) LOWER(str) 一样
转字符 TO_CHAR(SQLCODE) date_format/ time_format 函数名称不同
转时间 to_date(str,format) STR_TO_DATE(str,format) 函数名称不同
获取当前时间 SYSDATE now() / SYSDATE() 函数名称不同
求和 SUM(num) SUM(num) 一样
返回两个日期之间的天数 (D1-D2) DATEDIFF(date1,date2)

2.1.3 是否使用存储过程?存储过程的个数?复杂度?

在这次的案例中,没有使用存储过程,因此不需要进行这方面的分析。

2.1.4 是否使用触发器?个数?使用的场景?

公司的数据库使用规范里禁止使用触发器,因此这次也不需要进行这方面的分析。

2.2 建表过程中其他需要注意的事项

  • 自增主键
    mysql默认需要有自增主键,而oracle的表可以不加主键
  • 编码格式
    :oracle的编码格式utf8在mysql需要修改成utf8mb4 要确保所有的表都有一个自增的主键列。
  • 时间字段
    :时间字段需要精确到时分秒的需要修改为datatime类型。这是因为MySQL的DATETIME类型可以存储到秒级别的时间信息,而Oracle的DATE类型只能存储到天级别的时间信息。
  • 索引格式
    :索引格式需要按照规范重新定义,最好在测试环境中进行检查和校验。这是因为Oracle和MySQL的索引实现方式有所不同,直接复制索引可能会导致性能问题。

3.为项目配置Oracle和MySQL双数据源

在项目的数据源配置里添加刚刚新建的MySQL数据源配置,并配置双数据源和Mapper的匹配规则。

4.对项目进行改造添加MySQL数据CRUD代码

添加一套针对MuSQL数据库CRUD的Dao和Mapper代码,同时我们写了一个注解以切面的方式实现根据配置实例化Oracle的Dao、MySQl的Dao、同时调用Oracle和MySQLDao的功能。

改造方式:
请添加图片描述

数据库迁移切换流程:
请添加图片描述

二、数据迁移操作步骤

数据迁移操作的答题步骤如下图所示:
请添加图片描述

1、配置初始化

​ 数据库写入的配置设置为只写Oracle数据库。数据库读取的配置设置为从OracleL数据库读取。

2、同步数据检查

​ 查询待迁移的几张表的数据量:

​ select count(1) from table;

3、全量数据迁移

​ 在迁移工具上执行数据迁移脚本SQL

4、检查全量迁移的数据

​ 查询迁移后的数据量,检查是否和需要迁移的数据量能匹配:

5、开启双写

数据库写入的配置设置为Oracle数据库和MYSQl数据库双写

6、获取迁移过程中oracle数据库的增量数据

查询updated_time在全量数据迁移开始时间之后的数据

select * from table whereupdated_time>to_Date('2022/12/16 04:00:00', 'yyyy/mm/dd hh24:mi:ss')

7、增量数据脚本准备

根据监控的增量数据对比,找出需要新增和修改的数据,准备脚本

8、数据补偿

在迁移工具上执行数据补偿脚本SQL

9、核对整体数据

我们有额外的数据核对方案,通过应用读Oracle,再异步读取MySQL并进行对比的方式进行业务表的数据核对。这样可以确保数据的一致性。

10、在灰度环境里验证数据的正确性

将灰度机器的数据库读取的配置设置为从MySQL数据库读取。并在灰度环境验证数据的正确性

11、数据库读取的配置设置为从MySQL数据库读取。

将正式环境的机器的数据库读取的配置设置为从MySQL数据库读取。

12、数据库写入的配置设置为只写MySQL

在生产环境运行一段时间,如果运行平稳的话,就可以关闭数据库双写,将数据库写入配置改为只写MySQL数据库了。

三、数据迁移的经验教训

1. 遇到的坑

在迁移过程中,我们发现了一些Oracle语法与MySQL语法不兼容的地方,有些写法在Oracle中可行,在MySQL中会报错:

(1)子查询语句要取别名

(2)字段别名需要注意,AS后是否为空

(3)条件语句中判断需要注意

(4)oracle转mysql条件语句is null需格外注意,在Oracle中null和空串是一个含义,在mysql中是两个含(只针对字段类型为varchar类型的字段)

Oracle中:
IS_LIMIT_SUCESS is null
Mysql替换为:
(IS_LIMIT_SUCESS is null or IS_LIMIT_SUCESS = '')