Jekyll2023-04-28T09:31:19+00:00https://yangjava.github.io/feed.xmlyangjavayangjava's personal page杨京京Torrent文件实现2022-11-13T00:00:00+00:002022-11-13T00:00:00+00:00https://yangjava.github.io/2022/11/13/Torrent%E6%96%87%E4%BB%B6%E5%AE%9E%E7%8E%B0<h1 id="torrent文件实现">Torrent文件实现</h1> <p>种子文件本质上是文本文件,包含Tracker信息和文件信息两部分。</p> <h2 id="种子定义">种子定义</h2> <p>BitTorrent 协议的种子文件可以保存一组文件的元数据。这种格式的文件被 BitTorrent 协议所定义。扩展名一般为“.torrent”。</p> <h2 id="种子结构">种子结构</h2> <p>.torrent 种子文件本质上是文本文件,包含 Tracker 信息和文件信息两部分。Tracker 信息主要是 BT 下载中需要用到的 Tracker 服务器的地址和针对 Tracker 服务器的设置,文件信息是根据对目标文件的计算生成的,计算结果根据 BitTorrent 协议内的 Bencode 规则进行编码。它的主要原理是需要把提供下载的文件虚拟分成大小相等的块,块大小必须为2k的整数次方(由于是虚拟分块,硬盘上并不产生各个块文件),并把每个块的索引信息和 Hash 验证码写入种子文件中;所以,种子文件就是被下载文件的“索引”。</p> <p>Tracker信息主要是BT下载中需要用到的Tracker服务器的地址和针对Tracker服务器的设置;</p> <p>文件信息是根据对目标文件的计算生成的,包括以下字段:</p> <ul> <li>announce - tracker的URL</li> <li>info - 该条映射到一个字典,该字典的键将取决于共享的一个或多个文件:</li> <li>name - 建议保存到的文件和目录名称</li> <li>piece length - 每个文件块的字节数。通常为28 = 256KB = 262144B</li> <li>pieces - 每个文件块的SHA-1的整合Hash。因为SHA-1会返回160-bit的Hash,所以pieces将会得到1个160-bit的整数倍的字符串。和一个length(相当于只有一个文件正在共享)或files(相当于当多个文件被共享):</li> <li>length - 文件的大小(以字节为单位)</li> <li>files - 一个字典的列表(每个字典对应一个文件)与以下的键:</li> <li>path - 一个对应子目录名的字符串行表,最后一项是实际的文件名称</li> <li>length - 文件的大小(以字节为单位)</li> </ul> <p>它的主要原理是需要把提供下载的文件虚拟分成大小相等的块,块大小必须为2k的整数次方(由于是虚拟分块,硬盘上并不产生各个块文件),并把每个块(是块,不是整个文件)的索引信息和Hash验证码写入种子文件中;所以,种子文件就是被下载文件的“索引”。</p> <h3 id="下载启动">下载启动</h3> <p>BT客户端首先解析种子文件得到Tracker地址,然后连接Tracker服务器。 Tracker服务器回应下载者的请求,提供下载者其他下载者(包括发布者)的IP。下载者再连接其他下载者,根据种子文件,两者分别告知对方自己已经有的块,然后交换对方所没有的数据。 此时不需要其他服务器参与,分散了单个线路上的数据流量,因此减轻了服务器负担。</p> <h3 id="防止重复下载">防止重复下载</h3> <p>下载者每得到一个块,需要算出下载块的Hash验证码与种子文件中的对比,如果一样则说明块正确,不一样则需要重新下载这个块。这种规定是为了解决下载内容准确性的问题。</p> <h3 id="下载过程中的变化">下载过程中的变化</h3> <p>用户下载的文件内容越来越完整,对应的,拥有完整种子文件的用户也会越来越多,使文件的“寿命”不断延长。</p> <h3 id="算法扩展">算法扩展</h3> <p>为了解决某些用户“下完就跑”的现象,在非官方BitTorrent协议中还存在一种慢慢开放下载内容的超级种子的算法。</p> <h3 id="网络技术扩展">网络技术扩展</h3> <p>DHT技术可以在无Tracker的情况下下载。 DHT全称为分布式哈希表(Distributed Hash Table),是一种分布式存储方法。在不需要服务器的情况下,每个客户端负责一个小范围的路由,并负责存储一小部分数据,从而实现整个DHT网络的寻址和存储。使用支持该技术的BT下载软件,用户无需连上Tracker就可以下载,因为软件会在DHT网络中寻找下载同一文件的其他用户并与之通讯,开始下载任务。 有些软件(如比特精灵)还会自动通过DHT搜索种子资源,构成种子市场。</p> <h2 id="磁力链接">磁力链接</h2> <p>现在我们使用迅雷等工具下载资源的时候,基本上都只需要一个叫做磁力链接的东西就可以了,非常方便。</p> <p>磁力链接格式类似于 magnet:?xt=urn:btih:E7FC73D9E20697C6C440203F5884EF52F9E4BD28</p> <p>分解一下这个链接</p> <ul> <li>magnet:协议名。</li> <li>xt:exact topic 的缩写,表示资源定位点。BTIH(BitTorrent Info Hash)表示哈希方法名,这里还可以使用 SHA1 和 MD5。这个值是文件的标识符,是不可缺少的。 一般来讲,一个磁力链接只需要上面两个参数即可找到唯一对应的资源。也有其他的可选参数提供更加详细的信息。</li> <li>dn:display name 的缩写,表示向用户显示的文件名。</li> <li>tr:tracker 的缩写,表示 tracker 服务器的地址。</li> <li>kt: 关键字,更笼统的搜索,指定搜索关键字而不是特定文件。</li> <li>mt:文件列表,链接到一个包含磁力链接的元文件 (MAGMA - MAGnet MAnifest)。</li> </ul> <h2 id="种子-磁力联系">种子-磁力联系</h2> <p>磁力链接的唯一标识符就是 40 个 16 进制字符码,也就是 magnet:?xt=urn:btih:E7FC73D9E20697C6C440203F5884EF52F9E4BD28中的 E7FC73D9E20697C6C440203F5884EF52F9E4BD28。这个同时也是种子文件的 info_hash,是每个种子的唯一标识码。根据它就能将磁力链接于种子联系起来,得到资源的详细信息,进而下载资源。</p> <h2 id="tracker简介">Tracker简介</h2> <p>在BT下载中,有一个非常重要的角色,那就是Tracker服务器。Tracker会追踪有多少人在下载同一文件,并把这些名单发送到BT软件上。BT软件再尝试连接这些用户,以此来给你提供下载速度,同时你也会给他们贡献速度。</p> <p>简单来说,Tracker服务器起到的,就是牵线搭桥的作用,而这正是BT下载的核心。越热门、越优质的Tracker,资源解析速度及下载速度就越快。普通BT软件速度不稳定,就是因为内置的Tracker太少。</p> <p>opentracker是一个linux中开源和免费的BitTorrent Tracker ,旨在实现一个最小化资源使用,并且可以在无线路由器中使用的轻量级tracker服务器。</p> <p>opentracker官网地址:https://erdgeist.org/arts/software/opentracker/</p> <h2 id="dht">DHT</h2> <p>BitTorrent 使用”分布式哈希表”(DHT)来为无 tracker 的种子(torrents)存储 peer 之间的联系信息。这样每个 peer 都成了 tracker。这个协议基于 Kademila 网络并且在 UDP 上实现。DHT 由节点组成,它存储了 peer 的位置。BitTorrent 客户端包含一个 DHT 节点,这个节点用来联系 DHT 中其他节点,从而得到 peer 的位置,进而通过 BitTorrent 协议下载。 peer: 一个 TCP 端口上监听的客户端/服务器,它实现了 BitTorrent 协议。 节点: 一个 UDP 端口上监听的客户端/服务器,它实现了 DHT(分布式哈希表) 协议。</p> <h2 id="python实现下载">Python实现下载</h2> <p>libtorrent是功能齐全的p2p协议开源C ++ bittorrent源码实现,专注于效率和可伸缩性。它可以在嵌入式设备和台式机上运行。它拥有完善的文档库,易于使用。 它提供了client_test可以用于解析torrent种子和磁力链接,可根据磁力链接直接下载文件,对于正在下载的视频文件,用其他播放器可边播放边下载。常见使用libtorrent库的项目有qBittorrent,deluge,Free download manager等。</p> <div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>import libtorrent as lt # 创建 session 对象 ses = lt.session() ses.listen_on(6881, 6891) # 添加磁力链接到下载队列 params = {'save_path': 'D:/demo'} link = 'magnet:?xt=urn:btih:...' handle = lt.add_magnet_uri(ses, link, params) # 等待下载完成 print('downloading metadata...') while not handle.has_metadata(): s = handle.status() progress = s.progress * 100 print('%.2f%% complete (down: %.1f kb/s up: %.1f kB/s peers: %d)' % (progress, s.download_rate / 1000, s.upload_rate / 1000, s.num_peers)) # 开始下载 print('starting', handle.name()) while not handle.is_seed(): s = handle.status() progress = s.progress * 100 print('%.2f%% complete (down: %.1f kb/s up: %.1f kB/s peers: %d)' % (progress, s.download_rate / 1000, s.upload_rate / 1000, s.num_peers)) </code></pre></div></div> <h2 id="libtorrent源码">libtorrent源码</h2> <p>bt协议有早起的tracker版本(俗称bt1.0)和现在常用的基于DHT的版本(俗称bt2.0),下文为整理的协议相关资料。</p>杨京京Torrent文件实现 种子文件本质上是文本文件,包含Tracker信息和文件信息两部分。Janusgraph图数据库2022-11-12T00:00:00+00:002022-11-12T00:00:00+00:00https://yangjava.github.io/2022/11/12/JanusGraph%E5%9B%BE%E6%95%B0%E6%8D%AE%E5%BA%93<h1 id="janusgraph">JanusGraph</h1> <p>JanusGraph是一个可扩展的图形数据库,用于存储和查询分布在多机集群中的包含数千亿顶点和边的图形。该项目由Titan出资,并于2017年由Linux基金会(Linux Foundation)开放管理。</p> <h2 id="常见图库对比">常见图库对比</h2> <p>目前主流数据库分为两个派系:Neo4j派 和 Tinkerpop派</p> <h2 id="janusgraph-与-tinkerpop-与-gremlin">JanusGraph 与 Tinkerpop 与 Gremlin</h2> <p>Tinkerpop 是Apache基金会下的一个开源的图数据库与图计算框架(OLTP与OLAP)。</p> <p>Gremlin 是Tinkerpop的一个组件,它是一门路径导向语言,用于图操作和图遍历(也称查询语言)。Gremlin Console 和 Gremlin Server 分别提供了控制台和远程执行Gremlin查询语言的方式。Gremlin Server 在 JanusGraph 中被成为 JanusGraph Server。</p> <p>JanusGraph是基于Tinkerpop这个框架来开发的,使用的查询语言也是Gremlin。</p> <h2 id="janusgraph概述">JanusGraph概述</h2> <p>JanusGraph是一个图形数据库引擎。其本身专注于紧凑图序列化、丰富图数据建模、高效的查询执行。另外,JanusGraph利用Hadoop进行图分析和批处理图处理。JanusGraph为数据持久性、数据索引、客户端访问实现了强大的模块化接口。JanusGraph的模块化体系结构使其可以与多种存储、索引、客户端技术进行互操作。它还简化了扩展JanusGraph以支持新的过程。</p> <h2 id="交互">交互</h2> <p>应用程序可以通过两种方式与JanusGraph交互:</p> <ul> <li> <p>嵌入式: 将JanusGraph嵌入到自己的图Gremlin查询应用中,与自己的应用公用同一JVM。 查询执行时,JanusGraph的缓存和事务处理都在与应用程序相同的JVM中进行,当数据模型大时,很容易OOM,并且耦合性太高,生产上一般不这么搞。</p> </li> <li> <p>服务式: JanusGraph单独运行在一个或一组服务器上,对外提供服务。(推荐的模式) 客户端通过向服务器提交Gremlin查询,与远程JanusGraph实例进行交互。</p> </li> </ul> <h2 id="docker安装janusgraph">Docker安装JanusGraph</h2> <p>JanusGraph提供Docker image。 Docker使得在单台机器上安装和运行Janusgraph服务和客户端变得简单容易。有关Janusgraph的安装和使用说明请参照 docker guide。 下面就举例如何使用Docker技术来安装和运行JanusGraph:</p> <div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>$ docker run -it -p 8182:8182 janusgraph/janusgraph </code></pre></div></div> <p>运行docker命令,获取janusgraph的Dockerimage并运行于Docker容器,8182端口作为服务端口暴露对外。启动日志如下:</p> <p>等待服务启动完成后,就可以启动Gremlin Console来连接新安装的janusgraph server: Gremlin Console是一个交互式shell,允许您访问JanusGraph图数据。你可以通过运行位于项目的bin目录中的gremlin.sh脚本来运行。</p> <div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>$ bin/gremlin.sh \,,,/ (o o) -----oOOo-(3)-oOOo----- gremlin&gt; :remote connect tinkerpop.server conf/remote.yaml ==&gt;Configured localhost/127.0.0.1:8182 </code></pre></div></div> <p>Gremlin Console命令采用的是Apache Groovy,。Gremlin-Groovy集成自Groovy提供了许多基本或高级的图遍历方法。</p> <p>为了方便起见,这里在同一台机器同时安装JanusGraph Server和Gremlin Console Client,首先在机器上创建Docker容器来运行JanusGraph Server:</p> <div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>$ docker run --name janusgraph-default janusgraph/janusgraph:latest </code></pre></div></div> <p>然后在这台机器上再另启一个容器来运行Gremlin Console Client并连接运行中的Janusgraph Server。</p> <div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>$ docker run --rm --link janusgraph-default:janusgraph -e GREMLIN_REMOTE_HOSTS=janusgraph \ -it janusgraph/janusgraph:latest ./bin/gremlin.sh \,,,/ (o o) -----oOOo-(3)-oOOo----- gremlin&gt; :remote connect tinkerpop.server conf/remote.yaml ==&gt;Configured janusgraph/172.17.0.2:8182 </code></pre></div></div> <p>Gremlin server启动完成后,默认端口是8182。下面我们启动Gremlin Console使用如下:remote的远程连接命令,将Gremlin Console连接到Server上。</p> <div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>gremlin&gt; :remote connect tinkerpop.server conf/remote.yaml ==&gt;Configured localhost/127.0.0.1:8182 </code></pre></div></div> <p>从日志中可以看出,在本例中,客户机和服务器运行在同一台机器上。当使用不同的设置时,您所要做的就是修改conf/remote.yaml文件。</p> <h2 id="将图加载到janusgraph">将图加载到JanusGraph</h2> <p>将一个图数据加载到Janusgraph主要通过JanusGraphFactory和JanusGraphFactory两个类。首先利用JanusGraphFactory提供的静态open方法,以配置文件路径作为输入参数,运行并返回graph实例(示例中的配置参考配置参考章节中介绍)。然后利用GraphOfTheGodsFactory的load方法并将返回的graph实例作为参数运行,从而完成图加载到JanusGraph中。</p> <p>将图加载到带索引后端的Janusgraph 下面的conf/janusgraph-berkeleyje-es.properties配置文件配置的是以BerkeleyDB作为存储后端,Elasticsearch作为索引后端的Janusgraph。</p> <div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>gremlin&gt; graph = JanusGraphFactory.open('conf/janusgraph-berkeleyje-es.properties') ==&gt;standardjanusgraph[berkeleyje:../db/berkeley] gremlin&gt; GraphOfTheGodsFactory.load(graph) ==&gt;null gremlin&gt; g = graph.traversal() ==&gt;graphtraversalsource[standardjanusgraph[berkeleyje:../db/berkeley], standard] </code></pre></div></div> <p>JanusGraphFactory.open()和GraphOfTheGodsFactory.load()完成以下操作</p> <ul> <li>为图创建全局和vertex-centric的索引集合。</li> <li>加载所有的顶点及其属性。</li> <li>加载所有的边及其属性。</li> </ul>杨京京JanusGraph JanusGraph是一个可扩展的图形数据库,用于存储和查询分布在多机集群中的包含数千亿顶点和边的图形。该项目由Titan出资,并于2017年由Linux基金会(Linux Foundation)开放管理。Windwo子系统实战2022-11-11T00:00:00+00:002022-11-11T00:00:00+00:00https://yangjava.github.io/2022/11/11/Windwo%E5%AD%90%E7%B3%BB%E7%BB%9F%E5%AE%9E%E6%88%98<h1 id="window子系统实战">Window子系统实战</h1> <p>Windows Subsystem for Linux(WSL)是一个用于在本地运行linux二进制可执行文件(ELF格式)的兼容层。</p> <h2 id="wsl简介">WSL简介</h2> <p>Windows Subsystem for Linux(WSL)是一个用于在本地运行linux二进制可执行文件(ELF格式)的兼容层。与虚拟机相比,wsl没有虚拟硬件的过程,而是直接在windows上虚拟一个linux内核,模拟linux系统调用,以运行linux执行文件。 因此效率要比虚拟机高,但是它使用的是自己实现的init进程而不是发行版的init进程,并且几乎没有实现任何系统服务,因此只适用于软件的开发,而不是作为桌面环境或生产性的服务器。</p> <p>并且wsl的目的也是如此,可以简单的将它理解为可以运行linux可执行文件的、类似于powershell的shell,具有互操作性(在linux中执行windows命令,在windows中执行linux命令)。</p> <h2 id="wsl组件">WSL组件</h2> <p>wsl实现的组件涉及到了用户和内核模式。在Windows NT内核模式中,LXCore,LXSS这两个驱动提供了linux内核调用的实现,即将linux调用转化为对应的windows NT内核调用;还提供了两种文件系统:VolFs(挂载在/目录上,支持linux文件系统所有特性)和DriverFs(挂载在/mnt/c,/mnt/d等等windows分区,主要为了支持系统间的互操作性);驱动还会模拟内核的行为,对linux进程进行调度。</p> <p>在用户模式下,windows提供了一种特殊的进程类型:Pico进程,来支持linux进程的运行。windows会 “放松” 对该类型进程的控制,主要交由linux虚拟内核调用和管理,即隔离性(因此需要系统的支持,低版本的系统不能使用wls的功能)。pico会将ELF二进制可执行文件装入到自己的地址空间,然后执行在linux虚拟内核提供的兼容层上。一个pico对应一个linux进程,并且pico进程也是windows的一种特殊进程,因此你可以在任务管理器上看到linux进程。</p> <p>无论exe还是elf格式的二进制文件,原理上都可以在同架构的cpu上执行,只是结构不同操作系统不能解析罢了。而Pico能够解析ELF格式的二进制文件,只需要linux虚拟内核能够提供正确的系统调用,就能够运行大部分linux命令。</p> <p>LXSS管理服务主要用于协调windows和linux进程之间的关系,和给于Bash.exe(并不是shell,只是我们访问wsl的入口)调用linux命令的接口。所有的运行的linux进程都会被加入到叫Linux实例(应该有LXSS记录的)中,只有第一次请求访问linux进程时才会创建Linux实例,才会创建init进程;当window关机时,会自动关闭linux实例,即关闭linux所有进程。</p> <p>也就是wsl不会随windows系统自启而自启,同时没有使用发行版的init进程,导致wsl中没有服务的存在。</p> <h2 id="wsl运行过程">WSL运行过程</h2> <p>linux中正常的启动过程是引导程序载入内核,内核初始化后载入init进程,init进程开启各项服务,将系统配置到用户可用的状态,如多用户登录、图形界面登录等。此时linux自启完成,接着等待用户的登录,并且登录方式多种,如控制台登录、ssh登录、虚拟终端登录等等。</p> <p>而wsl并不是一个真正的系统,而只是windows的一部分,可以执行linux二进制可执行文件。内核也是虚拟出来的,但是具有一定隔离性,如linux进程由虚拟内核调度,也具有互操作性。</p> <p>因此当windows自启结束并加载了上述LxCore、LXSS两个驱动后,已经能够提供linux进程系统调用了。此时wsl的init进程(windows自己实现的)并不会立刻运行,即暂时没有一个linux实例。只有当第一次访问wsl时(执行Bash.exe),才会创建linux实例,执行init服务进程。这个init进程会伴随linux实例,直到实例结束。然后再创建bash shell 和 另一个init 进程,在本次会话结束时(关闭Bash.exe窗口)这两个进程结束。之后再通过Bash.exe连接wsl,都只会创建bash和右边的init进程。</p> <h2 id="文件系统">文件系统</h2> <p>wsl只有两种windows设计的文件系统:VolFs和DriverFs。其中VolFs文件系统主要是为了支持linux文件系统的全部特性,如linux的文件权限、符号链接、不同于windows的文件名、文件名大小写敏感等等。</p> <p>而DriveFs主要是为了挂载windows的分区,并且实现互操作性而存在。实际上就是NTFS文件系统的包装,能够让NTFS在linux中使用,即使也提供了大部分linux文件系统特性,但是限制很多,如:</p> <ul> <li>文件目录权限全为777,实际上就算是root用户,在windows分区中也只有打开Base.exe命令拥有者的权限。说明普通用户使用root权限也不能修改c盘中大部分文件。</li> <li>最好不要在windows下创建文件名只有大小写不同的文件,尽管NTFS支持了。</li> <li>支持linux符号链接,为windows可执行文件创建符号链接时,注意添加后缀.exe。不要与windows的快捷方式混淆,它们目的一致,但结构不一致,不能在linux中使用。</li> </ul> <h2 id="禁忌">禁忌</h2> <p>不要在任何情况下,使用windows工具访问、创建、修改linux发行版的文件。再安装了WSL后,可以在C盘中找到它的根目录,含有linux标准目录结构,但千万不要修改它。</p> <p>linux和windows文件系统的数据存放形式是不同的,为了支持linux文件系统,wsl实现了VolFs。VolFs本质上还是使用了windows的NTFS的文件格式(因此你能够找到它),只是将Linux文件的元数据放入NTFS文件的扩展属性中。因此linux文件系统可以正常执行,而在windows看来,它的文件不存在或者空;而使用windows修改它的文件时,不会保留它的扩展属性,导致在linux中看起来不正常。</p> <p>因此,如果想要实现两个系统的互操作,应该将工作目录置于windows分区内,linux文件夹外,在windows的文件夹中操作。</p> <h2 id="wsl使用">WSL使用</h2> <p>wsl需要系统支持,安装之前最好将系统更新到最新版。</p> <ul> <li>开启linux子系统(WSL),使用管理员权限打开PowerShell,运行: <div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>Enable-WindowsOptionalFeature -Online -FeatureName Microsoft-Windows-Subsystem-Linux </code></pre></div> </div> </li> <li>重启</li> <li>安装发行版,这里使用Ubuntu。打开应用商店,然后下载</li> </ul> <p>按win键,找到ubuntu,点击运行,它会进行初始化。然后提示输入普通用户名和密码,这是默认登录用户。ubuntu中默认root用户不能登录。</p> <h2 id="用户账户和权限">用户账户和权限</h2> <p>wsl与windows有一定隔离性,wsl的用户对linux文件、进程的权限与linux一致。但对于/mnt/c下windows的最高访问权限取决于运行Base.exe(即使使用Ubuntu.exe,最终一致)拥有者的权限。即普通windows用户以root身份运行wsl也不能随意删除、访问、修改c盘文件。</p> <h2 id="wsl管理">wsl管理</h2> <p>有三种运行wsl的方法:</p> <ul> <li>发行版提供的app,如ubuntu:进入到linux家目录</li> <li>wsl.exe或bash.exe:linux置当前目录为工作目录</li> <li><code class="language-plaintext highlighter-rouge">wsl [command]</code> 或 <code class="language-plaintext highlighter-rouge">bash -c [command]</code>:运行linux命令,当前目录作为工作目录</li> </ul> <p>wsl中可以存在多个发行版,通过wslconfig管理,wslconfig /?可以查看具体功能:</p> <div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>wslconfig /list [/all]:列出已注册的发行版,/all额外列出正在安装和卸载的发行版。 wslconfig /setdefault:设置运行wsl时的默认发行版 wslconfig /unregister:卸载发行版 </code></pre></div></div> <p>具体的发行版提供的入口,如ubuntu,可以设置默认用户,详细参考ubuntu /?</p> <h2 id="互操作">互操作</h2> <p>上面多次提到互操作性,也就是可以在windows中运行linux进程,在linux中运行windows进程,并且支持两个不同进程间的输入输出重定向。</p> <p>在windows中使用<code class="language-plaintext highlighter-rouge">wsl [command]</code>运行linux命令,需要注意:</p> <p>linux命令使用当前目录作为工作目录 对于windows文件,拥有运行WSL的windows用户权限;对于linux文件拥有登录用户权限 文件路径使用linux格式 例子:</p> <div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>C:\temp&gt; wsl ls -la &lt;- contents of C:\temp -&gt; C:\temp&gt; wsl sudo apt-get update [sudo] password for username: Hit:1 https://archive.ubuntu.com/ubuntu xenial InRelease Get:2 https://security.ubuntu.com/ubuntu xenial-security InRelease [94.5 kB] C:\temp&gt; wsl ls -la | findstr "foo" -rwxrwxrwx 1 root root 14 Sep 27 14:26 foo.bat C:\temp&gt; dir | wsl grep foo 09/27/2016 02:26 PM 14 foo.bat C:\temp&gt; wsl ls -la &gt; out.txt C:\temp&gt; wsl ls -la /proc/cpuinfo -r--r--r-- 1 root root 0 Sep 28 11:28 /proc/cpuinfo C:\temp&gt; wsl ls -la "/mnt/c/Program Files" &lt;- contents of C:\Program Files -&gt; </code></pre></div></div> <p>在linux中运行windows命令,使用[binary name].exe(后缀不要省略),linux可以直接访问windows命令的原因在于linux共享了windows的PATH环境变量(我猜是linux中唯一的守护进程init做的)。需要注意:</p> <p>一般windows命令的工作目录为当前linux的工作目录。如果linux工作目录位于linux文件系统内,则windows的工作目录会改为Base.exe的工作目录(原因见2.4小节)。 拥有与运行WSL拥有者一致的权限 需要注意路径,看下面的例子</p> <div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>$ notepad.exe $ ipconfig.exe | grep IPv4 | cut -d: -f2 172.21.240.1 10.159.21.24 $ ls -la | findstr.exe foo.txt $ cmd.exe /c dir &lt;- contents of C:\ -&gt; $#cmd 原生命名需要通过cmd.exe运行 $ cmd.exe /C dir &lt;- contents of C:\ -&gt; $ PING.EXE www.microsoft.com Pinging e1863.dspb.akamaiedge.net [2600:1409:a:5a2::747] with 32 bytes of data: Reply from 2600:1409:a:5a2::747: time=2ms $ notepad.exe "C:\temp\foo.txt" $ notepad.exe C:\\temp\\foo.txt </code></pre></div></div> <h2 id="wsl默认账号">WSL默认账号</h2> <p>wsl创建完成后,是通过自定义的用户和密码进行登录的,而并没有设置root的密码,但很多情况下,都需要切换到root来操作,每次都sudo也不很方便。 那,如果想登录wsl的时候就以root登录该如何设置呢?</p> <p>首先,我们找到ubuntu.exe的路径,比如我这里是“C:\Program Files\WindowsApps\CanonicalGroupLimited.Ubuntu20.04onWindows_2004.2021.222.0_x64__79rhkp1fndgsc\ubuntu2004.exe”,切换到该路径后,</p> <div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>ubuntu.exe config --default-user root </code></pre></div></div> <p>重新打开wsl后发现,登录即为root用户。</p>杨京京Window子系统实战 Windows Subsystem for Linux(WSL)是一个用于在本地运行linux二进制可执行文件(ELF格式)的兼容层。Vscode实战2022-11-10T00:00:00+00:002022-11-10T00:00:00+00:00https://yangjava.github.io/2022/11/10/Vscode%E5%AE%9E%E6%88%98<h1 id="vscode实战">VsCode实战</h1> <h2 id="vscodewsldocker-开发环境构建指南">VsCode+WSL+Docker 开发环境构建指南</h2> <h3 id="why为什么整这种活">Why——为什么整这种活</h3> <p>在实际开发中,我们遇到了以下类型的问题:</p> <ul> <li> <p>不同工程对nodejs版本要求不一样 比如我们的工程A是基于angularjs1.5的,它要求nodejs版本为11.0.0, 而工程B是基于vue+vite的,vite要求nodejs版本需要大于12.0.0。</p> </li> <li> <p>某些npm包需要linux编译环境 典型地,如node-sass这个包,所以需要本地具有可以编译它的环境,比如python,gyp等等,但是一般来说,我们的Windows上并没有这些环境,所以就会报一堆错,而Windows如果想要安装这种环境,又需要安装一些很冷门还很难装的软件与库,搞得及其烦人。</p> </li> <li> <p>需要适应未来DevOps转型 现在很多大厂已经转型DevOps了,云计算和容器化是一种大趋势,随着业务量与项目数量的增长,开发和运维都面临着向DevOps的转型,所以提前进行容器化开发,可以为未来容器化开发——容器化部署模式铺平道路。</p> </li> </ul> <h2 id="方案简述">方案简述</h2> <p>我们采用Vscode + Docker + WSL来解决上述问题。</p> <ul> <li>VsCode :不必多介绍,一款流行的编辑器,在本方案中,它只需要安装两个插件即可:</li> <li>Remote-Containers :用于连接远程容器</li> <li>Remote-WSL :用于连接本地的WSL</li> <li>WSL(windows subsystem linux):Windows系统下的Linux子系统,它在这里主要起的作用是用来提供一个linux环境,一是提供给docker作为运行环境,二是用来解决上述某些npm包需要linux编译环境的问题。</li> <li>Docker :一款容器应用,它可以提供比虚拟机更加快捷和对硬件资源消耗更少的虚拟化方案,为我们的提供一个虚拟的服务器和开发环境。</li> </ul> <h2 id="安装配置wsl-20版本">安装配置WSL 2.0版本</h2> <p>最新的Windws10默认都是直接支持WSL2的,我们只需要在windows应用商店搜索linux,就能看到支持的Linux子系统列表,选择其中一个安装就可以了。</p> <p>当然,推荐的话,还是ubuntu比较流行,遇到问题也比较容易搜索到相关解决方案。 为了更好地操作,推荐在应用商店中搜索windows terminal安装。</p> <p>另外,由于WSL是基于Hype-V技术的,需要Windows开启相关功能,并且保证BIOS中的虚拟化选项开启,由于不同电脑BIOS的设置方式不同,请自行在网络上搜索相关设置方法。</p> <p>如何判断电脑是否开启了虚拟化呢? 打开任务管理器,切换到【性能】一栏,可以看到【CPU】下面的信息中标识了是否开启了虚拟化:</p> <p>输入以下命令:</p> <div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>wsl -l --verbose </code></pre></div></div> <p>会列出本机现在安装的所有WSL发行版以及它的版本</p> <div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code> NAME STATE VERSION * Ubuntu Running 2 </code></pre></div></div> <p>系统默认的那个发行版前会有一个*号,也就是,当你在windows Terminal中直接输入wsl时,打开的那个发行版:</p> <h2 id="安装配置docker">安装配置Docker</h2> <p>Windows需要去官网下载Docker desktop最新版本,下载安装文件后直接安装即可。 https://docs.docker.com/desktop/windows/wsl/ 安装好之后,我们要进行以下配置,运行docker-desktop程序</p> <p>修改一下docker镜像源和配置:</p> <div class="language-json highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="p">{</span><span class="w"> </span><span class="nl">"registry-mirrors"</span><span class="p">:</span><span class="w"> </span><span class="p">[</span><span class="w"> </span><span class="s2">"http://hub-mirror.c.163.com"</span><span class="p">,</span><span class="w"> </span><span class="s2">"https://docker.mirrors.ustc.edu.cn"</span><span class="w"> </span><span class="p">],</span><span class="w"> </span><span class="nl">"insecure-registries"</span><span class="p">:</span><span class="w"> </span><span class="p">[],</span><span class="w"> </span><span class="nl">"debug"</span><span class="p">:</span><span class="w"> </span><span class="kc">false</span><span class="p">,</span><span class="w"> </span><span class="nl">"experimental"</span><span class="p">:</span><span class="w"> </span><span class="kc">false</span><span class="p">,</span><span class="w"> </span><span class="nl">"features"</span><span class="p">:</span><span class="w"> </span><span class="p">{</span><span class="w"> </span><span class="nl">"buildkit"</span><span class="p">:</span><span class="w"> </span><span class="kc">true</span><span class="w"> </span><span class="p">},</span><span class="w"> </span><span class="nl">"builder"</span><span class="p">:</span><span class="w"> </span><span class="p">{</span><span class="w"> </span><span class="nl">"gc"</span><span class="p">:</span><span class="w"> </span><span class="p">{</span><span class="w"> </span><span class="nl">"enabled"</span><span class="p">:</span><span class="w"> </span><span class="kc">true</span><span class="p">,</span><span class="w"> </span><span class="nl">"defaultKeepStorage"</span><span class="p">:</span><span class="w"> </span><span class="s2">"20GB"</span><span class="w"> </span><span class="p">}</span><span class="w"> </span><span class="p">}</span><span class="w"> </span><span class="p">}</span><span class="w"> </span></code></pre></div></div> <h2 id="安装配置vscode相关插件">安装配置VsCode相关插件</h2> <h2 id="在wsl容器中开发调试应用">在WSL容器中开发调试应用</h2> <p>自动创建容器 插件安装成功后,在VsCode中打开工程目录,会看到左下角有两个相对的箭头的标识,点击它,就会弹出命令选项,让我们选择在哪里打开:</p> <p>在确保docker desktop 正常运行的前提下,我们可以选择在WSL中开发目录,也可以选择在容器内打开当前工程目录,这里我们选择在容器内打开。它会让我们选择要打开的目录,默认为当前目录:</p> <p>VsCode在配置好相关环境后,会在当前目录下生成一个一个名为.devcontainer的文件夹,里面存放着容器的环境配置,有了这个配置,下次再在容器中打开工程目录时,就不会再让我们选择工程环境类型了。</p> <p>当然,为了不污染项目文件,可以把这个目录加入到.gitignore文件中。</p> <p>之后,我们就可以像在本地开发一样,在容器内进行开发了。</p> <p>手动创建容器 我们发现,在选择NodeJS版本那一步,只有少数几个版本,比如我们需要10.17.0的话,这里就没有,怎么办呢,我们有另一种方式。</p> <p>在工程根目录下新建Dockerfile文件,</p> <div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>FROM node:10.17.0 # 这里写上需要的nodejs版本 WORKDIR /usr/src/app COPY package*.json ./ RUN npm install COPY . . EXPOSE 8090 # 这里是应用在开发时默认运行的端口 CMD [ "npm", "run", "serve"] # 拆解的开发命令 npm run serve </code></pre></div></div> <p>然后新建docker忽略文件.dockerignore :</p> <p>node_modules npm-debug.log 新建后,点击左下角远程容器按钮,选择【在WSL中打开】:</p> <h2 id="软硬件要求">软硬件要求</h2> <p>官方System requirements文档</p> <p>BIOS开启虚拟化 添加Hyper-V(win10家庭版需要安装) WSL安装(控制台输入WSL -l检查是否安装,下载地址:WSL安装更新) 将默认安装的Linux子系统版本设置为WSL2(控制台输入wsl.exe –set-default-version 2) 4GB系统内存 64位处理器带二级地址翻译(SLAT)</p> <h2 id="下载安装">下载安装</h2> <p>下载Docker Desktop(各版本下载地址:docker desktop release-notes),按正常的客户端程序安装即可,4.9+版本目前有不少小问题,可到官方的issue页面检索相关问题(https://github.com/docker/for-win/issues);</p> <h1 id="参考资料">参考资料</h1> <p>https://docs.docker.com/desktop/windows/wsl/</p>杨京京VsCode实战Python实战hive2022-11-09T00:00:00+00:002022-11-09T00:00:00+00:00https://yangjava.github.io/2022/11/09/Python%E5%AE%9E%E6%88%98Hive<h1 id="python实战hive">Python实战Hive</h1> <p>PyHive 是 Python 语言编写的用于操作 Hive 的简便工具库。</p> <h2 id="pyhive安装">PyHive安装</h2> <div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code># Liunx系统 pip install sasl pip install thrift pip install thrift-sasl pip install PyHive # Windows系统会出现莫名其妙的报错,sasl需要选择对应的版本号 pip install sasl‑0.3.1‑cp310‑cp310‑win_amd64.whl 或者下载到本地手动导入包 pip install D:\sasl-0.3.1-cp310-cp310-win_amd64.whl 优化后导入如下: pip install sasl-0.2.1-cp36-cp36m-win_amd64.whl pip install thrift -i https://pypi.tuna.tsinghua.edu.cn/simple pip install thrift_sasl==0.3.0 -i https://pypi.tuna.tsinghua.edu.cn/simple pip install pyhive -i https://pypi.tuna.tsinghua.edu.cn/simple </code></pre></div></div> <p>下载一个对应你所使用的Python版本和Windows版本的sasl文件:https://www.lfd.uci.edu/~gohlke/pythonlibs/#sasl。 例如, sasl‑0.2.1‑cp36‑cp36m‑win_amd64.whl,它对应的Python 版本为3.6,对应的Windows系统为64位。 安装执行pip install sasl-0.2.1-cp37-cp37m-win_amd64.whl</p> <h2 id="简介">简介</h2> <p>pyhive通过与HiveServer2通讯来操作Hive数据。当hiveserver2服务启动后,会开启10000的端口,对外提供服务,此时pyhive客户端通过JDBC连接hiveserver2进行Hive sql操作。</p> <p>Pyhive Client通过JDBC与hiveserver2建立通信,hiveserver2服务端发送HQL语句到Driver端,Driver端将HQL发送至Compiler组件进行语法树解析,此时需在metastore获取HQL相关的database和table等信息,在对HQL完成解析后,Compiler组件发送执行计划至Driver端等待处理,Driver端发送执行计划至Executor端,再由Executor端发送MapReduce任务至Hadoop集群执行Job,Job完成后最终将HQL查询数据发送Driver端,再由hive server2返回数据至pyhive Client。</p> <h2 id="访问">访问</h2> <p>PyHive 连接 Hive 一般流程:</p> <ul> <li>创建连接</li> <li>获取游标</li> <li>执行SQL语句</li> <li>获取结果</li> <li>关闭连接</li> </ul> <div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1"># 加载包 </span><span class="kn">from</span> <span class="nn">pyhive</span> <span class="kn">import</span> <span class="n">hive</span> <span class="c1"># 建立连接 </span><span class="n">conn</span> <span class="o">=</span> <span class="n">hive</span><span class="p">.</span><span class="n">connect</span><span class="p">(</span><span class="n">host</span> <span class="o">=</span> <span class="s">'10.8.1.2'</span><span class="p">,</span> <span class="c1"># 主机 </span> <span class="n">port</span> <span class="o">=</span> <span class="mi">10000</span><span class="p">,</span> <span class="c1"># 端口 </span> <span class="n">username</span> <span class="o">=</span> <span class="s">'hdfs'</span><span class="p">,</span> <span class="c1"># 用户 </span> <span class="p">)</span> <span class="c1"># 查询 </span><span class="n">cursor</span> <span class="o">=</span> <span class="n">conn</span><span class="p">.</span><span class="n">cursor</span><span class="p">()</span> <span class="c1"># 获取一个游标 </span><span class="n">cursor</span><span class="p">.</span><span class="n">execute</span><span class="p">(</span><span class="s">'show databases'</span><span class="p">)</span> <span class="c1"># 执行sql语句 # sql = 'show tables' # 操作语句 </span><span class="k">for</span> <span class="n">result</span> <span class="ow">in</span> <span class="n">cursor</span><span class="p">.</span><span class="n">fetchall</span><span class="p">():</span> <span class="c1"># 输出获取结果的所有行 </span> <span class="k">print</span><span class="p">(</span><span class="n">result</span><span class="p">)</span> <span class="c1"># 关闭连接 </span><span class="n">cursor</span><span class="p">.</span><span class="n">close</span><span class="p">()</span> <span class="n">conn</span><span class="p">.</span><span class="n">close</span><span class="p">()</span> </code></pre></div></div> <p>其中,cursor.fetchall() 返回的是一个 list 对象,并且每一个元素都是一个 tuple 对象。 需要对其进行一定的处理,才能转换为建模需要的 DataFrame。</p> <h2 id="函数封装">函数封装</h2> <div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1"># 函数封装 </span><span class="k">def</span> <span class="nf">get_data</span><span class="p">(</span><span class="n">params</span><span class="p">,</span> <span class="n">sql_text</span><span class="p">,</span> <span class="n">is_all_col</span><span class="o">=</span><span class="mi">1</span><span class="p">):</span> <span class="s">''' is_all_col: 是否选取所有的列 select * '''</span> <span class="c1"># 建立连接 </span> <span class="n">con</span> <span class="o">=</span> <span class="n">hive</span><span class="p">.</span><span class="n">connect</span><span class="p">(</span><span class="n">host</span> <span class="o">=</span> <span class="n">params</span><span class="p">.</span><span class="n">get</span><span class="p">(</span><span class="s">'ip'</span><span class="p">),</span> <span class="n">port</span> <span class="o">=</span> <span class="n">params</span><span class="p">.</span><span class="n">get</span><span class="p">(</span><span class="s">'port'</span><span class="p">),</span> <span class="n">auth</span> <span class="o">=</span> <span class="n">params</span><span class="p">.</span><span class="n">get</span><span class="p">(</span><span class="s">'auth'</span><span class="p">),</span> <span class="n">kerberos_service_name</span> <span class="o">=</span> <span class="n">params</span><span class="p">.</span><span class="n">get</span><span class="p">(</span><span class="s">'kerberos_service_name'</span><span class="p">),</span> <span class="n">database</span> <span class="o">=</span> <span class="n">params</span><span class="p">.</span><span class="n">get</span><span class="p">(</span><span class="s">'database'</span><span class="p">),</span> <span class="n">password</span> <span class="o">=</span> <span class="n">params</span><span class="p">.</span><span class="n">get</span><span class="p">(</span><span class="s">'password'</span><span class="p">))</span> <span class="n">cursor</span> <span class="o">=</span> <span class="n">con</span><span class="p">.</span><span class="n">cursor</span><span class="p">()</span> <span class="n">cursor</span><span class="p">.</span><span class="n">execute</span><span class="p">(</span><span class="n">sql_text</span><span class="p">)</span> <span class="c1"># 列名 </span> <span class="n">col_tmp</span> <span class="o">=</span> <span class="n">cursor</span><span class="p">.</span><span class="n">description</span> <span class="n">col</span> <span class="o">=</span> <span class="nb">list</span><span class="p">()</span> <span class="k">if</span> <span class="n">is_all_col</span> <span class="o">==</span> <span class="mi">1</span><span class="p">:</span> <span class="k">for</span> <span class="n">i</span> <span class="ow">in</span> <span class="nb">range</span><span class="p">(</span><span class="nb">len</span><span class="p">(</span><span class="n">col_tmp</span><span class="p">)):</span> <span class="n">col</span><span class="p">.</span><span class="n">append</span><span class="p">(</span><span class="n">col_tmp</span><span class="p">[</span><span class="n">i</span><span class="p">][</span><span class="mi">0</span><span class="p">].</span><span class="n">split</span><span class="p">(</span><span class="s">'.'</span><span class="p">)[</span><span class="mi">1</span><span class="p">])</span> <span class="k">else</span><span class="p">:</span> <span class="k">for</span> <span class="n">i</span> <span class="ow">in</span> <span class="nb">range</span><span class="p">(</span><span class="nb">len</span><span class="p">(</span><span class="n">col_tmp</span><span class="p">)):</span> <span class="n">col</span><span class="p">.</span><span class="n">append</span><span class="p">(</span><span class="n">col_tmp</span><span class="p">[</span><span class="n">i</span><span class="p">][</span><span class="mi">0</span><span class="p">])</span> <span class="c1"># 数据 </span> <span class="n">data</span> <span class="o">=</span> <span class="n">cursor</span><span class="p">.</span><span class="n">fetchall</span><span class="p">()</span> <span class="n">result</span> <span class="o">=</span> <span class="n">pd</span><span class="p">.</span><span class="n">DataFrame</span><span class="p">(</span><span class="nb">list</span><span class="p">(</span><span class="n">data</span><span class="p">),</span> <span class="n">columns</span><span class="o">=</span><span class="n">col</span><span class="p">)</span> <span class="c1"># 关闭连接 释放资源 </span> <span class="n">cursor</span><span class="p">.</span><span class="n">close</span><span class="p">()</span> <span class="n">con</span><span class="p">.</span><span class="n">close</span><span class="p">()</span> <span class="k">return</span> <span class="n">result</span> <span class="k">if</span> <span class="n">__name__</span> <span class="o">==</span> <span class="s">'__main__'</span><span class="p">:</span> <span class="kn">import</span> <span class="nn">pandas</span> <span class="k">as</span> <span class="n">pd</span> <span class="kn">import</span> <span class="nn">numpy</span> <span class="k">as</span> <span class="n">np</span> <span class="n">params</span> <span class="o">=</span> <span class="p">{</span> <span class="s">'ip'</span><span class="p">:</span> <span class="s">'xxx.xxx.xxx.xxx'</span><span class="p">,</span> <span class="s">'port'</span><span class="p">:</span> <span class="s">'1000'</span><span class="p">,</span> <span class="s">'auth'</span><span class="p">:</span> <span class="s">'hider'</span><span class="p">,</span> <span class="s">'kerberos_service_name'</span><span class="p">:</span> <span class="s">'hive'</span><span class="p">,</span> <span class="s">'database'</span><span class="p">:</span> <span class="s">'hive'</span><span class="p">,</span> <span class="s">'password'</span><span class="p">:</span> <span class="s">'100'</span><span class="p">,</span> <span class="p">}</span> <span class="n">sql_text1</span> <span class="o">=</span> <span class="s">'select * from table limit 5'</span> <span class="n">sql_text2</span> <span class="o">=</span> <span class="s">'select a, b, c from table limit 5'</span> <span class="c1"># 所有列 </span> <span class="n">data1</span> <span class="o">=</span> <span class="n">get_data</span><span class="p">(</span><span class="n">params</span><span class="p">,</span> <span class="n">sql_text1</span><span class="p">,</span> <span class="n">is_all_col</span><span class="o">=</span><span class="mi">1</span><span class="p">)</span> <span class="c1"># 指定列 </span> <span class="n">data2</span> <span class="o">=</span> <span class="n">get_data</span><span class="p">(</span><span class="n">params</span><span class="p">,</span> <span class="n">sql_text2</span><span class="p">,</span> <span class="n">is_all_col</span><span class="o">=</span><span class="mi">0</span><span class="p">)</span> </code></pre></div></div> <h2 id="hive配置">Hive配置</h2> <p>Hive 有许多必要的参数设置,通过 Connection 类的 configuration 参数可进行配置。</p> <div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>hive_config = { 'mapreduce.job.queuename': 'my_hive', 'hive.exec.compress.output': 'false', 'hive.exec.compress.intermediate': 'true', 'mapred.min.split.size.per.node': '1', 'mapred.min.split.size.per.rack': '1', 'hive.map.aggr': 'true', 'hive.groupby.skewindata': 'true', 'hive.exec.dynamic.partition.mode': 'nonstrict' } conn = hive.connect(host = '', port = '', ... configuration = hive_config) </code></pre></div></div> <h2 id="列名">列名</h2> <p>Cursor 类中有一个 description 方法,可以获取数据表中的列名、数据类型等信息。</p> <div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>col = cursor.description col_names = list() for column in col: col_names.append(column[0]) # 提取第一个元素:列名 </code></pre></div></div> <h2 id="执行脚本带参数">执行脚本带参数</h2> <p>游标所执行的脚本可以不写死,通过参数的方式进行配置。</p> <div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>month = 202205 # %占位符 sql_text = 'select * from table where month = %d limit 5' % month print(sql_text) # format sql_text = 'select * from table where month = {} limit 5'.format(month) # f-string sql_text = f'select * from table where month = {month} limit 5' print(sql_text) </code></pre></div></div>杨京京Python实战Hive PyHive 是 Python 语言编写的用于操作 Hive 的简便工具库。Pyspark实战2022-11-08T00:00:00+00:002022-11-08T00:00:00+00:00https://yangjava.github.io/2022/11/08/Pyspark%E5%AE%9E%E6%88%98<h1 id="pyspark实战">PySpark实战</h1> <p>Python PySpark是Spark官方提供的一个Python类库,其中内置了完全的Spark API,使得Python用户在导入这个类库后,可以使用自己熟悉的Python语言来编写Spark应用程序,并最终将程序提交到Spark集群运行。</p> <h1 id="window本地环境pyspark安装">window本地环境PySpark安装</h1> <p>PySpark是基于Python语言开发的类库,仅支持在单机环境下供Python用户开发调试使用,需要将程序提交到Spark集群上才能使用Spark集群分布式的能力处理大规模的数据处理任务。</p> <p>PySpark就是官方为了让Python用户更方便地使用Spark而开发出来的类库。Python用户不需要编写复杂的底层逻辑,只需要调用PySpark的API即可。</p> <p>本文基于Windows 64位操作系统进行PySpark的安装演示。 现在主流的方式都是通过Anaconda来管理自己的Python环境了。安装PySpark</p> <div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>pip pyspark </code></pre></div></div> <p>搭建Spark环境。我们从官网或者国内镜像下载最新版本的Spark安装包,这里下载的是spark-3.3.2-bin-hadoop3.tgz,然后将其解压缩到合适的位置,这里比如是D:\spark。</p> <div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>https://mirrors.tuna.tsinghua.edu.cn/apache/spark/spark-3.3.2/ </code></pre></div></div> <p>然后我们需要新增一个环境变量:</p> <div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>SPARK_HOME=D:\spark\spark-3.3.2-bin-hadoop3 </code></pre></div></div> <p>再将其添加到Path环境变量中:</p> <div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>%SPARK_HOME%\bin </code></pre></div></div> <p>然后我们在命令行尝试运行spark\bin下面的pyspark:</p> <div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>&gt; pyspark Python 3.10.9 | packaged by Anaconda, Inc. | (main, Mar 1 2023, 18:18:15) [MSC v.1916 64 bit (AMD64)] on win32 Type "help", "copyright", "credits" or "license" for more information. 23/04/19 15:31:10 WARN Shell: Did not find winutils.exe: java.io.FileNotFoundException: java.io.FileNotFoundException: HADOOP_HOME and hadoop.home.dir are unset. -see https://wiki.apache.org/hadoop/WindowsProblems Setting default log level to "WARN". To adjust logging level use sc.setLogLevel(newLevel). For SparkR, use setLogLevel(newLevel). 23/04/19 15:31:10 WARN NativeCodeLoader: Unable to load native-hadoop library for your platform... using builtin-java classes where applicable Welcome to ____ __ / __/__ ___ _____/ /__ _\ \/ _ \/ _ `/ __/ '_/ /__ / .__/\_,_/_/ /_/\_\ version 3.3.2 /_/ Using Python version 3.10.9 (main, Mar 1 2023 18:18:15) Spark context Web UI available at http://DESKTOP-DFA24F11:4040 Spark context available as 'sc' (master = local[*], app id = local-1681889472795). SparkSession available as 'spark'. &gt;&gt;&gt; 23/04/19 15:31:28 WARN ProcfsMetricsGetter: Exception when trying to compute pagesize, as a result reporting of ProcessTree metrics is stopped </code></pre></div></div> <p>Hadoop环境搭建。由于Spark的运行需要Hadoop作为底层支持,所以我们还需要从官网或者国内镜像下载最新的Hadoop安装包,这里现在的是hadoop-3.3.4.tar.gz,然后将其加压缩到合适的目录,假设这里为D:\hadoop。</p> <div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code> </code></pre></div></div> <p>然后我们需要配置Hadoop的环境变量,新建:</p> <div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>HADOOP_HOME=D:\hadoop\hadoop-3.3.4 </code></pre></div></div> <p>然后将其添加到Path环境变量中:</p> <div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>%HADOOP_HOME%\bin %HADOOP_HOME%\sbin </code></pre></div></div> <p>然后我们进入到D:\hadoop\hadoop-3.3.4\etc\hadoop目录下,修改hadoop-env.cmd,设置Java Home:</p> <div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>set JAVA_HOME=C:\PROGRA~1\Java\jdk1.8.0_341 </code></pre></div></div> <p>我本机的Java环境在C:\Program Files\Java,因为Program Files中间有空格,按照这样填写的话Hadoop启动会报错,无法找到Java的地址,因此我们使用PROGRA~1来代替Program Files就能解决这个问题。</p> <p>然后我们从github的winutils仓库中下载hadoop-3.0.0的压缩包,将其中的内容全部覆盖复制到D:\hadoop\hadoop-3.3.4\bin下面,然后将hadoop.dll拷贝到C:\Windows\System32下面,然后我们就可以验证Hadoop了:</p> <div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>(base)PS C:\Users\zhangxun&gt; hadoop version Hadoop 3.3.4 Source code repository https://github.com/apache/hadoop.git -r a585a73c3e02ac62350c136643a5e7f6095a3dbb Compiled by stevel on 2022-07-29T12:32Z Compiled with protoc 3.7.1 From source with checksum fb9dd8918a7b8a5b430d61af858f6ec This command was run using /D:/hadoop/hadoop-3.3.4/share/hadoop/common/hadoop-common-3.3.4.jar </code></pre></div></div> <p>至此,Hadoop配置完成。</p> <h2 id="helloworld测试">HelloWorld测试</h2> <p>我们在合适的地方新建一个文件夹,然后用vscode打开这个文件夹,后面的示例代码程序都在这个文件夹里面完成了。</p> <div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kn">from</span> <span class="nn">pyspark</span> <span class="kn">import</span> <span class="n">SparkConf</span> <span class="kn">from</span> <span class="nn">pyspark</span> <span class="kn">import</span> <span class="n">SparkContext</span> <span class="k">if</span> <span class="n">__name__</span> <span class="o">==</span> <span class="s">'__main__'</span><span class="p">:</span> <span class="n">conf</span><span class="o">=</span><span class="n">SparkConf</span><span class="p">().</span><span class="n">setAppName</span><span class="p">(</span><span class="s">"test1"</span><span class="p">).</span><span class="n">setMaster</span><span class="p">(</span><span class="s">"local"</span><span class="p">)</span> <span class="n">sc</span><span class="o">=</span><span class="n">SparkContext</span><span class="p">(</span><span class="n">conf</span><span class="o">=</span><span class="n">conf</span><span class="p">)</span> <span class="n">rdd</span><span class="o">=</span><span class="n">sc</span><span class="p">.</span><span class="n">parallelize</span><span class="p">([</span><span class="mi">1</span><span class="p">,</span><span class="mi">2</span><span class="p">,</span><span class="mi">3</span><span class="p">,</span><span class="mi">4</span><span class="p">,</span><span class="mi">5</span><span class="p">])</span> <span class="k">print</span><span class="p">(</span><span class="s">"rdd是:"</span><span class="p">,</span><span class="n">rdd</span><span class="p">)</span> <span class="k">print</span><span class="p">(</span><span class="s">"rdd.collect是:"</span><span class="p">,</span><span class="n">rdd</span><span class="p">.</span><span class="n">collect</span><span class="p">())</span> </code></pre></div></div> <p>执行该程序:</p> <div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>&gt;python hello.py Setting default log level to "WARN". To adjust logging level use sc.setLogLevel(newLevel). For SparkR, use setLogLevel(newLevel). rdd是: ParallelCollectionRDD[0] at readRDDFromFile at PythonRDD.scala:274 rdd.collect是: [1, 2, 3, 4, 5] </code></pre></div></div> <p>如此证明pyspark程序可以提交到本地的spark环境正常运行。</p> <h2 id="pyspark程序运行机制分析">PySpark程序运行机制分析</h2> <p>在没有学习PySpark执行原理之前,很多人可能会认为PySpark编写的程序会被翻译为JVM能识别的字节码,然后提交给Spark运行,其实不是这样的。</p> <p>Spark工作原理如下: Driver负责总体的调度,Executor负责具体Task的运行,它们都是运行在JVM进程之中的,而这些JVM进程则是可以部署在多种的资源管理系统中的,比如Yarn、Mesos或者是K8s等;用户提交的Spark程序交给Driver进行管理,Driver将程序分解为一个个的Task交给Executor执行。</p> <p>为了不影响现有Spark的工作架构,Spark在外围包装了一层Python的API,借助Py4j实现Python和Java的交互,进而实现通过Python代码来编写Spark应用程序并提交给Spark集群运行。</p> <p>在Driver端,Python通过Py4j来调用Java方法,将用户使用Python写的程序映射到JVM中,比如,用户在PySpark中实例化一个Python的SparkContext对象,最终会在JVM中实例化Scala的SparkContext对象。</p> <p>在Executor端,都启动一个Python守护进程,当Task收到任务请求后,交给底层的Python进程去执行。</p> <p>所以,Pyspark的运行机制和我们预想的并不一样,这种方式可以不破坏现有的Spark执行架构,同时也方便多种语言支持的扩展,但是也很明显,使用PySpark运行Spark任务肯定比使用Java或者Scala要有一些额外的性能损耗。</p> <h2 id="docker下安装pyspark">Docker下安装PySpark</h2>杨京京PySpark实战 Python PySpark是Spark官方提供的一个Python类库,其中内置了完全的Spark API,使得Python用户在导入这个类库后,可以使用自己熟悉的Python语言来编写Spark应用程序,并最终将程序提交到Spark集群运行。微电影大导演2022-11-07T00:00:00+00:002022-11-07T00:00:00+00:00https://yangjava.github.io/2022/11/07/%E5%BE%AE%E7%94%B5%E5%BD%B1%E5%A4%A7%E5%AF%BC%E6%BC%94<p>#</p>杨京京#Pr剪辑视频2022-11-06T00:00:00+00:002022-11-06T00:00:00+00:00https://yangjava.github.io/2022/11/06/PR%E5%89%AA%E8%BE%91%E8%A7%86%E9%A2%91<h1 id="pr剪辑视频">PR剪辑视频</h1> <p>Pr全称Adobe Premiere,是Adobe公司开发的一款视频剪辑&amp;编辑软件。主要用于视频采集、视频剪辑、调色、添加字幕、编辑音频、渲染输出。Pr是一款是功能强大、简单易学、应用面广、专业稳定的视频剪辑软件。</p> <p>## 帧:构成动画的最小单位,即组成动画的每一幅静态画面,一帧就是一幅静态画面。 帧速率:是视频中每秒包含的帧数,即每秒钟所播放的画面达到的数量。PAL制式影片(中国、德国、英国等采用的电视制式)的帧速率是25帧/秒,NTSC制式影片(美国、日本等采用的电视制式)的帧速度是29.97帧/秒,电影的帧速率是24帧/秒,二维动画的帧速率是12帧/秒。 场:在电视上,每一帧都有两个画面,电视机通过隔行扫描技术,把电视中的每个帧画面隔行抽掉一半,然后交错合成为一个帧的大小。由隔行扫描技术产生的两个画面被称为“场”。</p> <h2 id="常用视频尺寸">常用视频尺寸:</h2> <ul> <li>标清:标清是指标准清晰度视频,就制式而言,PAL制式标清视频尺寸为720像素×576像素。</li> <li>高清:高于标清标准的视频被称为“高清视频”,如1280像素×720像素(也称“小高清”)、1920像素×1080像素(标准的高清视频尺寸,也称“全高清”)等。</li> <li>2K:2K分辨率是指屏幕或者内容的水平分辨率达约2000像素的分辨率等级,标准的2K分辨率为2048像素×1080像素。</li> <li>4K:K分辨率是指屏幕的物理分辨率达3840像素×2160像素,且能接收、解码、显示相应分辨率视频信号的电视。</li> </ul> <h2 id="景别">景别</h2> <p>景别是指由于在焦距一定时,摄影机与被摄体的距离不同,而造成被摄体在摄影机录像器中所呈现出的范围大小的区别,大致分为:远景、全景、中景、近景、特写。 景别组接类型:</p> <ul> <li>前进式:全景+中景+近景+特写</li> <li>后退式:特写+近景+中景+全景</li> <li>同等式:相同景别组接</li> <li>两级式:跨度大的景别组接到一起</li> <li>循环式:前进式和后退式的结合</li> </ul> <h2 id="剪辑工作流程">剪辑工作流程</h2> <p>剪辑工作流程:</p> <ul> <li>素材导入。提前将准备好的素材(视频、音频、图片)分类放在文件夹中,然后导入pr。</li> <li>素材编辑。素材编辑就是设置素材的入点与出点,以选择最合适的部分,然后按脚本组接不同素材的过程。</li> <li>特技处理。对于视频素材,特技处理包括转场、特效、合成、叠加。对于音频素材,特技处理包括转场、特效。令人震撼的画面效果,就是经过特技处理产生的。</li> <li>字幕制作。字幕是视频中非常重要的部分,它包括文字和图形两个方面。</li> <li>输出与生成。视频编辑完成后,就可以生成视频文件,发布到网上。</li> </ul> <h2 id="pr界面">pr界面</h2> <ul> <li>项目面板 :素材文件的管理者,将素材导入“项目”面板后,会在“项目”面板中显示素材的名称、帧速率、长度等信息。</li> <li>素材预览窗口 :用来查看时间线上的当前序列。</li> <li>工具箱 :其中的工具都是用来编辑素材文件的。</li> <li>预设面板 :包含很多种关于工作界面各个区域分布的预设,初期学习时,建议选择编辑模式,它是一种常用的预设面板。</li> <li>效果控件面板 :用来编辑各类效果的属性。</li> <li>时间线 :即时间轴,默认有3个视频轨道+3个音频轨道+1个主声道。视频轨道与ps一样有层级覆盖的特点。</li> </ul> <h2 id="pr工具">pr工具</h2> <ul> <li>选择工具 :快捷键为V,可以用来选择素材、移动素材、调整素材等。+shift键可选择或者取消选择时间线上不连续的多个素材。+alt键可以单独选择在链接状态下的视频或音频,进行操作。+alt键拖曳素材,可以在时间线上复制该素材。先拖曳素材再按住Ctrl键,鼠标指针会变成向右的箭头,被拖曳的素材前端会有一排小三角,松开鼠标左键,素材将插入时间线上,原有素材向右顺延。</li> <li>向前选择轨道工具组 :向前选择轨道工具的快捷键为A,向后选择轨道工具的快捷键为Shift+A。当时间线上素材量较多、剪辑时长较长的时候,这两个工具可以选择所有的素材并进行整体调整。</li> <li>波纹编辑工具 :快捷键为B,单击素材的出点或者入点,鼠标指针会变成黄色的箭头,左右拖动素材增加或减少画面。</li> <li>滚动编辑工具 :快捷键为N,它与波纹编辑工具的区别是会同时选择编辑点左侧素材的出点和右侧素材的入点,在调节两个素材的长短,修改画面内容的时候,会使时间线的整个时长保持不变,也不会改变其他素材的时长。</li> <li>比率拉伸工具 :快捷键为R,它可以调整素材的速度以匹配时间线上空出来的波纹,相当于给素材做变速效果,当然它更多的时候是用于填补波纹空隙或者让画面与音乐节奏点相匹配,和“剪辑速度/持续时间”对话框的作用是一样的,不过在“剪辑速度/持续时间”对话框中,是要修改参数才能更改素材速度的,当不知道要修改多少参数才能填补空隙的时候,还是用比率拉伸工具比较合适。</li> <li>剃刀工具 :快捷键为C,选择剃刀工具后,鼠标指针会变成小刀片的形状,可以把素材一分为二,为素材添加编辑点。使用快捷键Ctrl+K可以根据播放头指针位置切割素材,与剃刀工具的作用是一样的。</li> <li>外滑/内滑工具 :外滑快捷键为Y,内滑快捷键为U。当使用外滑工具单击素材,并左右拖动画面时,可以改变素材的入点和出点,而不改变该素材在轨道中的位置和长度。外滑工具改变的是该素材的画面内容,相当于重新定义素材的入点和出点。</li> <li>钢笔工具 :快捷键为P,钢笔工具可以调整画面的运动路径、制作蒙版和添加关键帧。</li> <li>手形工具 :快捷键为H,选择手形工具,在时间线上单击轨道并左右拖动,可以左右移动时间线。</li> </ul> <h2 id="项目创建及素材基本处理">项目创建及素材基本处理</h2> <p>启动软件,出现欢迎使用界面,点击新建项目:修改名称和文件保存位置,其他不用修改,即创建好了一个项目。</p> <p>素材编辑的前提是必须要有序列,序列的大小决定了输出视频的尺寸大小。常用预设是HDV 720p25,其中720p25 指的是分辨率 1280×720,每秒播放25帧画面。</p>杨京京PR剪辑视频 Pr全称Adobe Premiere,是Adobe公司开发的一款视频剪辑&amp;编辑软件。主要用于视频采集、视频剪辑、调色、添加字幕、编辑音频、渲染输出。Pr是一款是功能强大、简单易学、应用面广、专业稳定的视频剪辑软件。Vue基础入门2022-11-05T00:00:00+00:002022-11-05T00:00:00+00:00https://yangjava.github.io/2022/11/05/Vue%E5%9F%BA%E7%A1%80%E5%85%A5%E9%97%A8<h1 id="vue基础入门">Vue基础入门</h1>杨京京Vue基础入门规则引擎实战2022-11-02T00:00:00+00:002022-11-02T00:00:00+00:00https://yangjava.github.io/2022/11/02/%E8%A7%84%E5%88%99%E5%BC%95%E6%93%8E%E5%AE%9E%E6%88%98<h1 id="drools-规则引擎实战">Drools 规则引擎实战</h1> <h2 id="drools常见的使用场景">Drools常见的使用场景</h2> <p>Drools是一个规则引擎,通常用于实现业务规则管理和自动决策。下面是Drools常见的使用场景:</p> <ul> <li>客户端决策:Drools可以被用来处理客户端决策问题,如客户经理决策、客户评分卡、授信决策、风险评估等。</li> <li>规则管理:Drools可以作为一个独立的规则管理平台,帮助企业管理规则和流程。</li> <li>基于规则的业务流程管理:Drools可以集成到业务流程管理系统中,帮助企业管理规则驱动的业务流程。</li> <li>数据验证和清洗:Drools可以被用来对数据进行验证和清洗,以确保数据的准确性和一致性。</li> <li>基于事件的决策:Drools可以被用来实现基于事件的决策,如基于实时事件的促销决策、实时交易监测等。</li> </ul> <h2 id="数据验证和清洗">数据验证和清洗</h2> <p>Drools可以在数据验证和清洗场景中使用,通过使用规则来验证数据的有效性和一致性。具体来说,Drools可以用于:</p> <ul> <li>数据格式验证:可以使用Drools验证数据是否符合特定的格式要求,如邮件地址、电话号码等。</li> <li>数据范围验证:可以使用Drools验证数据是否在允许的范围内,如年龄范围、工资范围等。</li> <li>数据一致性验证:可以使用Drools验证不同数据项之间的一致性,如邮寄地址和电话号码的一致性等。</li> <li>数据清洗:可以使用Drools对数据进行清洗,如删除重复的数据、修正错误的数据等。</li> </ul> <p>通过使用Drools进行数据验证和清洗,可以保证数据的准确性和一致性,从而提高数据的可靠性和可用性。</p> <p>如下几个规则是关于Drools在数据清洗场景中的示例</p> <p>删除重复的数据</p> <div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>rule "Remove duplicate records" when $record1: Record( $id1: id, $name1: name, $age1: age, $email1: email ) $record2: Record( id == $id1, name == $name1, age == $age1, email == $email1 ) $record1 != $record2 then retract( $record2 ); end </code></pre></div></div> <p>修正错误的数据</p> <div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>rule "Correct invalid email addresses" when $record: Record( email not matches "(\\w+\\.)*\\w+@\\w+\\.\\w+" ) then modify( $record ) { setEmail( "invalid" ) }; end </code></pre></div></div> <p>数据格式转换 使用Drools对数据进行格式转换,如将日期字段从 一种格式转换为另一种格式等。</p> <div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>package com.example.drools.rules; import com.example.drools.DataCleaningExample.Record; rule "Remove Records with Invalid Age" when $record : Record(age &lt; 0 || age &gt; 120) then retract($record); end </code></pre></div></div> <p>上面的规则演示了标准化地址的功能(将所有地址字段转换为大写字母)。</p> <p>去除噪声数据 使用Drools对数据进行预处理,如去除数据中的噪声等。</p> <div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>package com.example.drools.rules; import com.example.drools.DataCleaningExample.Record; rule "Remove Outliers - Age" when $record : Record(age &lt; 18 || age &gt; 65) then retract($record); end rule "Remove Outliers - Address" when $record : Record(address matches ".*[0-9].*") then retract($record); end </code></pre></div></div> <p>这是两个Drools规则,分别演示了如何删除年龄的离群值(年龄小于18或大于65)和地址的离群值(地址中包含数字)。</p> <p>填补缺失的数据 Drools对数据进行预处理,如填补缺失的数据等。</p> <div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>package com.example.drools.rules; import com.example.drools.DataCleaningExample.Record; rule "Fill Missing Name" when $record : Record(name == null || name.isEmpty()) then $record.setName("N/A"); end rule "Fill Missing Age" when $record : Record(age == 0) then $record.setAge(30); end rule "Fill Missing Address" when $record : Record(address == null || address.isEmpty()) then $record.setAddress("N/A"); end </code></pre></div></div> <p>这是三个Drools规则,分别演示了如何填补缺失的名称,年龄和地址。</p> <h2 id="客户评分卡">客户评分卡</h2> <p>Drools可以用于客户评分卡场景,以评估客户的信用评分。它可以通过评估客户的财务历史、信用历史和个人信息等因素来评分客户。</p> <div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>package com.example.drools.rules; import com.example.drools.CreditScoringExample.Customer; rule "Good Credit History" when $customer : Customer(creditHistory == "Good") then $customer.setScore($customer.getScore() + 20); end rule "High Income" when $customer : Customer(income &gt; 75000) then $customer.setScore($customer.getScore() + 10); end rule "Long Employment" when $customer : Customer(employmentLength &gt; 5) then $customer.setScore($customer.getScore() + 15); end </code></pre></div></div> <h2 id="基于规则的业务流程管理">基于规则的业务流程管理</h2> <p>Drools可以用于帮助企业管理规则和流程。它可以作为一种途径,帮助企业管理和自动化各种业务规则,如:</p> <ul> <li>权限管理:判断用户是否具有某种特定的权限,以访问特定的功能或数据。</li> <li>工作流管理:帮助企业管理复杂的业务流程,如请求审批或订单处理。</li> <li>报价管理:帮助企业根据特定的规则,计算客户的报价。</li> <li>优惠券管理:帮助企业判断特定的优惠券是否适用于特定的客户。</li> </ul> <p>如下是一个简单的Drools规则示例,演示了如何管理报价:</p> <div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>package com.example.drools.rules; import com.example.drools.PricingManagementExample.Order; rule "Discount for High Volume Orders" when $order : Order(quantity &gt; 100) then $order.setDiscount(0.10); end rule "Discount for Repeat Customers" when $order : Order(customer.numberOfOrders &gt; 10) then $order.setDiscount(0.05); end rule "Discount for Large Orders" when $order : Order(total &gt; 1000) then $order.setDiscount(0.15); end </code></pre></div></div> <p>这是三个Drools规则,分别演示了如何为大量订单、重复顾客和大订单提供折扣。</p> <h2 id="基于事件的决策">基于事件的决策</h2> <p>Drools可以通过使用事件驱动的决策来实现基于事件的决策。它通过监听事件并触发相应的规则来实现事件驱动的决策。规则可以根据事件中包含的信息执行特定的操作,如:</p> <ul> <li>审批请求:当客户发起请求时,根据请求类型和客户信息执行相应的审批操作。</li> <li>发送通知:当特定事件发生时,发送通知到相关的人员或系统。</li> <li>执行操作:当特定事件发生时,执行特定的操作,如修改数据或发送请求。</li> </ul> <p>如下是一个简单的Drools规则示例,演示了如何执行基于事件的决策:</p> <div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>package com.example.drools.rules; import com.example.drools.EventDrivenDecisionMakingExample.OrderEvent; rule "Approve High Volume Orders" when $orderEvent : OrderEvent(order.quantity &gt; 100) then $orderEvent.getOrder().setApproved(true); end rule "Send Notification for Large Orders" when $orderEvent : OrderEvent(order.total &gt; 1000) then // Send notification to relevant personnel or system end rule "Update Inventory for Approved Orders" when $orderEvent : OrderEvent(order.isApproved() == true) then // Update inventory information end </code></pre></div></div> <p>这是三个Drools规则,分别演示了如何批准大量订单、发送通知和更新库存信息。</p> <h2 id="银行业务审批">银行业务审批</h2> <p>在银行业务审批中,Drools可以应用于贷款审批和信用卡审批等场景。通过规则引擎,银行可以根据客户的贷款申请和信用历史等信息,自动化地判断是否批准贷款或信用卡。</p> <p>下面是一个简单的银行业务审批案例,演示了Drools如何在银行业务审批中使用:</p> <p>规则文件(loan-approval.drl)</p> <div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>package rules; import org.dtt.entity.*; rule "Reject Loan for Applicant with Bad Credit Score" when $applicant:Applicant(creditScore &lt; 600) $loanApplication:LoanApplication(applicant == $applicant) then $loanApplication.setApproved(false); $loanApplication.setReason("Rejected due to low credit score"); System.out.println("Rejected due to low credit score"); retract($loanApplication); end rule "Approve Loan for Applicant with Good Credit Score" when $applicant:Applicant(creditScore &gt;= 600) $loanApplication:LoanApplication(applicant == $applicant) then $loanApplication.setApproved(true); $loanApplication.setReason("Approved"); System.out.println("Approve Loan for Applicant with Good Credit Score --&gt; approved"); end rule "Reject Loan for Applicant with Low Income" when $applicant:Applicant(income &lt; 5000) $loanApplication:LoanApplication(applicant == $applicant) then $loanApplication.setApproved(false); $loanApplication.setReason("Rejected due to low income"); retract($loanApplication); System.out.println("Reject Loan for Applicant with Low Income --&gt; reject"); end rule "print" when $loanApplication: LoanApplication() then System.out.println($loanApplication.getApplicant().getName()); end </code></pre></div></div> <p>Fact类</p> <div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>import java.io.Serializable; public class Applicant implements Serializable { private String name; private int age; private int creditScore; private double income; public LoanApplication(Applicant applicant, Integer loanAmount) { this.applicant = applicant; this.loanAmount = loanAmount; } //省略Getters/Setters方法 } </code></pre></div></div> <div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>public class LoanApplication { private Applicant applicant; private Integer loanAmount; private boolean approved; private String reason; //省略Getters/Setters方法 } </code></pre></div></div> <p>测试示例 在我们运行TestLoanApproval代码时,它会触发Drools规则引擎,并使用LoanApplication和Applicant对象作为规则的输入。Drools规则引擎将按照定义的规则来评估这些对象,并决定是否批准贷款申请。</p> <p>如果所有的规则都被评估为true,那么贷款申请将被批准。在这种情况下,LoanApprovalTest中的断言语句将不会抛出任何异常,代表测试成功。</p> <p>如果任何一个规则评估为false,那么贷款申请将不会被批准。在这种情况下,LoanApprovalTest中的断言语句将抛出异常,代表测试失败。</p> <div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code> @Test public void testLoanApproval() { KieServices kieServices = KieServices.Factory.get(); KieContainer kieContainer = kieServices.newKieClasspathContainer(); KieSession kieSession = kieContainer.newKieSession(); Applicant applicant = new Applicant("Joe", 23, 900, 10000); LoanApplication loanApplication = new LoanApplication(applicant); loanApplication.setLoanAmount(100000); kieSession.insert(applicant); kieSession.insert(loanApplication); kieSession.fireAllRules(); kieSession.dispose(); assertTrue(loanApplication.isApproved()); } </code></pre></div></div> <h2 id="小明喝汽水问题">小明喝汽水问题</h2> <p>1元钱一瓶汽水,喝完后两个空瓶换一瓶汽水,问:小明有20元钱,最多可以喝到几瓶汽水?</p> <p>规则拆分 规则1:1元钱一瓶汽水–&gt; 有钱就买水,空瓶+1,钱-1,喝水+1 规则2:两个空瓶换一瓶汽水–&gt;有空瓶就换钱,空瓶-2,钱+1</p> <div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>//规则1:1元钱一瓶汽水。有钱就买水,空瓶+1,钱-1,喝水+1; rule "rule1" salience 3 when $m:XiaoMing(money&gt;0); then System.out.println("有钱即可喝水,钱:"+$m.getMoney()); $m.setBottle($m.getBottle()+1); $m.setMoney($m.getMoney()-1); $m.setDrink($m.getDrink()+1); update($m) end </code></pre></div></div> <p>需要注意的是,如果想要换水,需要再编写一个规则,描述空瓶的变化情况。</p> <p>该规则描述了:当XiaoMing实例的bottle属性大于等于2时,空瓶减少2瓶,喝水量增加1瓶,并且更新XiaoMing实例。</p> <div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>//规则2:两个空瓶换一瓶汽水。有空瓶就换钱,空瓶-2,钱+1; rule "rule2" salience 2 when $m:XiaoMing(bottle&gt;=2); then System.out.println("有瓶子就换钱,瓶子:"+$m.getBottle()); $m.setBottle($m.getBottle()-2); $m.setMoney($m.getMoney()+1); update($m) end </code></pre></div></div> <p>如果想要在Drools规则中打印喝水数量,可以在合适的地方添加一行代码,例如在最终状态打印喝水数量:</p> <div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>//规则3,当XiaoMing实例的money属性小于等于0时,打印喝水数量 rule "rule3" salience 1 when $m:XiaoMing(money&lt;=0); then System.out.println("总共喝掉:"+$m.getDrink()); end </code></pre></div></div> <p>Fact类</p> <div class="language-java highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="cm">/** * Fact定义 */</span> <span class="kd">public</span> <span class="kd">class</span> <span class="nc">XiaoMing</span> <span class="o">{</span> <span class="c1">//总共的钱</span> <span class="kd">private</span> <span class="kt">int</span> <span class="n">money</span><span class="o">;</span> <span class="c1">//空瓶子数目</span> <span class="kd">private</span> <span class="kt">int</span> <span class="n">bottle</span><span class="o">;</span> <span class="c1">//已经喝掉的汽水</span> <span class="kd">private</span> <span class="kt">int</span> <span class="n">drink</span><span class="o">;</span> <span class="c1">//省略getters、setters方法.....</span> <span class="o">}</span> </code></pre></div></div> <p>测试方法</p> <div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>@Test public void test01() { KieContainer kc = KieServices.Factory.get().getKieClasspathContainer(); System.out.println(kc.verify().getMessages().toString()); KieSession ksession = kc.newKieSession("mingKS"); XiaoMing xiaoMing=new XiaoMing(); xiaoMing.setMoney(20); ksession.insert(xiaoMing); ksession.fireAllRules(); ksession.dispose(); } </code></pre></div></div> <h2 id="高尔夫球员站位问题">高尔夫球员站位问题</h2> <p>问题分析 已知有四个高尔夫球员,他们的名字是Fred,Joe,Bob,Tom;今天他们分别穿着红色,蓝色,橙色,以及格子衣服,并且他们按照从左往右的顺序站成一排。我们将最左边的位置定为1,最右边的位置定为4,中间依次是2,3位置。</p> <p>现在我们了解的情况是:</p> <ul> <li>高尔夫球员Fred,目前不知道他的位置和衣服颜色</li> <li>Fred右边紧挨着的球员穿蓝色衣服</li> <li>Joe排在第2个位置</li> <li>Bob穿着格子短裤</li> <li>Tom没有排在第1位或第4位,也没有穿橙色衣服 请问,这四名球员的位置和衣服颜色。</li> </ul> <p>规则</p> <div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>package com.xshe.drools import com.xshe.drools.bean.Golfer; rule "find solution" when //1.高尔夫球员Fred,目前不知道他的位置和衣服颜色 $fred : Golfer( name == "Fred" ) //3.Joe排在第2个位置 $joe : Golfer( name == "Joe", position == 2, position != $fred.position, color != $fred.color ) //4.Bob穿着格子短裤 $bob : Golfer( name == "Bob", position != $fred.position, position != $joe.position, color == "plaid", color != $fred.color, color != $joe.color ) //5.Tom没有排在第1位或第4位,也没有穿橙色衣服 $tom : Golfer( name == "Tom", position != 1, position != 4, position != $fred.position, position != $joe.position, position != $bob.position, color != "orange", color != $fred.color, color != $joe.color, color != $bob.color ) //2.Fred右边紧挨着的球员穿蓝色衣服 Golfer( position == ( $fred.position + 1 ), color == "blue", this in ( $joe, $bob, $tom ) ) then System.out.println( "Fred " + $fred.getPosition() + " " + $fred.getColor() ); System.out.println( "Joe " + $joe.getPosition() + " " + $joe.getColor() ); System.out.println( "Bob " + $bob.getPosition() + " " + $bob.getColor() ); System.out.println( "Tom " + $tom.getPosition() + " " + $tom.getColor() ); end </code></pre></div></div> <h2 id="测试">测试</h2> <div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>public static void main(final String[] args) { KieContainer kc = KieServices.Factory.get().getKieClasspathContainer(); System.out.println(kc.verify().getMessages().toString()); KieSession ksession = kc.newKieSession("golfKS"); String[] names = new String[]{"Fred", "Joe", "Bob", "Tom"}; String[] colors = new String[]{"red", "blue", "plaid", "orange"}; int[] positions = new int[]{1, 2, 3, 4}; for (String name : names) { for (String color : colors) { for (int position : positions) { ksession.insert(new Golfer(name, color, position)); } } } ksession.fireAllRules(); ksession.dispose(); } </code></pre></div></div> <h2 id="springboot规则引擎来实现打折">SpringBoot规则引擎来实现打折</h2> <p>现在有这么个需求,网上购物,需要根据不同的规则计算商品折扣,比如VIP客户增加5%的折扣,购买金额超过1000元的增加10%的折扣等,而且这些规则可能随时发生变化,甚至增加新的规则。面对这个需求,你该怎么实现呢?难道是计算规则一变,就要修改业务代码,重新测试,上线吗。</p> <h3 id="引入依赖">引入依赖</h3> <p>我们创建一个spring boot应用程序,pom中添加drools相关的依赖,如下:</p> <div class="language-xml highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nt">&lt;dependency&gt;</span> <span class="nt">&lt;groupId&gt;</span>org.drools<span class="nt">&lt;/groupId&gt;</span> <span class="nt">&lt;artifactId&gt;</span>drools-core<span class="nt">&lt;/artifactId&gt;</span> <span class="nt">&lt;version&gt;</span>7.59.0.Final<span class="nt">&lt;/version&gt;</span> <span class="nt">&lt;/dependency&gt;</span> <span class="nt">&lt;dependency&gt;</span> <span class="nt">&lt;groupId&gt;</span>org.drools<span class="nt">&lt;/groupId&gt;</span> <span class="nt">&lt;artifactId&gt;</span>drools-compiler<span class="nt">&lt;/artifactId&gt;</span> <span class="nt">&lt;version&gt;</span>7.59.0.Final<span class="nt">&lt;/version&gt;</span> <span class="nt">&lt;/dependency&gt;</span> <span class="nt">&lt;dependency&gt;</span> <span class="nt">&lt;groupId&gt;</span>org.drools<span class="nt">&lt;/groupId&gt;</span> <span class="nt">&lt;artifactId&gt;</span>drools-decisiontables<span class="nt">&lt;/artifactId&gt;</span> <span class="nt">&lt;version&gt;</span>7.59.0.Final<span class="nt">&lt;/version&gt;</span> <span class="nt">&lt;/dependency&gt;</span> </code></pre></div></div> <h3 id="drools配置类">Drools配置类</h3> <p>创建一个名为DroolsConfig的配置 java 类。</p> <div class="language-java highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nd">@Configuration</span> <span class="kd">public</span> <span class="kd">class</span> <span class="nc">DroolsConfig</span> <span class="o">{</span> <span class="c1">// 制定规则文件的路径 </span> <span class="kd">private</span> <span class="kd">static</span> <span class="kd">final</span> <span class="nc">String</span> <span class="no">RULES_CUSTOMER_RULES_DRL</span> <span class="o">=</span> <span class="s">"rules/customer-discount.drl"</span><span class="o">;</span> <span class="kd">private</span> <span class="kd">static</span> <span class="kd">final</span> <span class="nc">KieServices</span> <span class="n">kieServices</span> <span class="o">=</span> <span class="nc">KieServices</span><span class="o">.</span><span class="na">Factory</span><span class="o">.</span><span class="na">get</span><span class="o">();</span> <span class="nd">@Bean</span> <span class="kd">public</span> <span class="nc">KieContainer</span> <span class="nf">kieContainer</span><span class="o">()</span> <span class="o">{</span> <span class="nc">KieFileSystem</span> <span class="n">kieFileSystem</span> <span class="o">=</span> <span class="n">kieServices</span><span class="o">.</span><span class="na">newKieFileSystem</span><span class="o">();</span> <span class="n">kieFileSystem</span><span class="o">.</span><span class="na">write</span><span class="o">(</span><span class="nc">ResourceFactory</span><span class="o">.</span><span class="na">newClassPathResource</span><span class="o">(</span><span class="no">RULES_CUSTOMER_RULES_DRL</span><span class="o">));</span> <span class="nc">KieBuilder</span> <span class="n">kb</span> <span class="o">=</span> <span class="n">kieServices</span><span class="o">.</span><span class="na">newKieBuilder</span><span class="o">(</span><span class="n">kieFileSystem</span><span class="o">);</span> <span class="n">kb</span><span class="o">.</span><span class="na">buildAll</span><span class="o">();</span> <span class="nc">KieModule</span> <span class="n">kieModule</span> <span class="o">=</span> <span class="n">kb</span><span class="o">.</span><span class="na">getKieModule</span><span class="o">();</span> <span class="nc">KieContainer</span> <span class="n">kieContainer</span> <span class="o">=</span> <span class="n">kieServices</span><span class="o">.</span><span class="na">newKieContainer</span><span class="o">(</span><span class="n">kieModule</span><span class="o">.</span><span class="na">getReleaseId</span><span class="o">());</span> <span class="k">return</span> <span class="n">kieContainer</span><span class="o">;</span> <span class="o">}</span> <span class="o">}</span> </code></pre></div></div> <ul> <li>定义了一个 KieContainer的Spring Bean ,KieContainer用于通过加载应用程序的/resources文件夹下的规则文件来构建规则引擎。</li> <li>创建KieFileSystem实例并配置规则引擎并从应用程序的资源目录加载规则的 DRL 文件。</li> <li>使用KieBuilder实例来构建 drools 模块。我们可以使用KieSerive单例实例来创建 KieBuilder 实例。</li> <li>最后,使用 KieService 创建一个 KieContainer 并将其配置为 spring bean。</li> </ul> <h2 id="添加业务model">添加业务Model</h2> <p>创建一个订单对象OrderRequest,这个类中的字段后续回作为输入信息发送给定义的drools规则中,用来计算给定客户订单的折扣金额。</p> <div class="language-java highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nd">@Getter</span> <span class="nd">@Setter</span> <span class="kd">public</span> <span class="kd">class</span> <span class="nc">OrderRequest</span> <span class="o">{</span> <span class="cm">/** * 客户号 */</span> <span class="kd">private</span> <span class="nc">String</span> <span class="n">customerNumber</span><span class="o">;</span> <span class="cm">/** * 年龄 */</span> <span class="kd">private</span> <span class="nc">Integer</span> <span class="n">age</span><span class="o">;</span> <span class="cm">/** * 订单金额 */</span> <span class="kd">private</span> <span class="nc">Integer</span> <span class="n">amount</span><span class="o">;</span> <span class="cm">/** * 客户类型 */</span> <span class="kd">private</span> <span class="nc">CustomerType</span> <span class="n">customerType</span><span class="o">;</span> <span class="o">}</span> </code></pre></div></div> <p>此外,定义一个客户类型CustomerType 的枚举,规则引擎会根据该值计算客户订单折扣百分比,如下所示。</p> <div class="language-java highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kd">public</span> <span class="kd">enum</span> <span class="nc">CustomerType</span> <span class="o">{</span> <span class="no">LOYAL</span><span class="o">,</span> <span class="no">NEW</span><span class="o">,</span> <span class="no">DISSATISFIED</span><span class="o">;</span> <span class="kd">public</span> <span class="nc">String</span> <span class="nf">getValue</span><span class="o">()</span> <span class="o">{</span> <span class="k">return</span> <span class="k">this</span><span class="o">.</span><span class="na">toString</span><span class="o">();</span> <span class="o">}</span> <span class="o">}</span> </code></pre></div></div> <p>最后,创建一个订单折扣类 OrderDiscount ,用来表示计算得到的最终的折扣,如下所示。</p> <div class="language-java highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nd">@Getter</span> <span class="nd">@Setter</span> <span class="kd">public</span> <span class="kd">class</span> <span class="nc">OrderDiscount</span> <span class="o">{</span> <span class="cm">/** * 折扣 */</span> <span class="kd">private</span> <span class="nc">Integer</span> <span class="n">discount</span> <span class="o">=</span> <span class="mi">0</span><span class="o">;</span> <span class="o">}</span> </code></pre></div></div> <p>我们将使用上述响应对象返回计算出的折扣。</p> <h3 id="定义drools-规则">定义drools 规则</h3> <p>前面的DroolsConfig类中指定drools规则的目录,现在我们在/src/main/resources/rules目录下添加customer- discount.drl文件,在里面定义对应的规则。</p> <div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code> import com.alvin.drools.model.OrderRequest; import com.alvin.drools.model.CustomerType; global com.alvin.drools.model.OrderDiscount orderDiscount; dialect "mvel" // 规则1: 根据年龄判断 rule "Age based discount" when // 当客户年龄在20岁以下或者50岁以上 OrderRequest(age &lt; 20 || age &gt; 50) then // 则添加10%的折扣 System.out.println("==========Adding 10% discount for Kids/ senior customer============="); orderDiscount.setDiscount(orderDiscount.getDiscount() + 10); end // 规则2: 根据客户类型的规则 rule "Customer type based discount - Loyal customer" when // 当客户类型是LOYAL OrderRequest(customerType.getValue == "LOYAL") then // 则增加5%的折扣 System.out.println("==========Adding 5% discount for LOYAL customer============="); orderDiscount.setDiscount(orderDiscount.getDiscount() + 5); end rule "Customer type based discount - others" when OrderRequest(customerType.getValue != "LOYAL") then System.out.println("==========Adding 3% discount for NEW or DISSATISFIED customer============="); orderDiscount.setDiscount(orderDiscount.getDiscount() + 3); end rule "Amount based discount" when OrderRequest(amount &gt; 1000L) then System.out.println("==========Adding 5% discount for amount more than 1000$============="); orderDiscount.setDiscount(orderDiscount.getDiscount() + 5); end </code></pre></div></div> <p>这个drl文件虽然不是java文件,但还是很容易看懂的。</p> <ul> <li>我们使用了一个名为orderDiscount 的全局参数,可以在多个规则之间共享。</li> <li>drl 文件可以包含一个或多个规则。我们可以使用mvel语法来指定规则。此外,每个规则使用rule关键字进行描述。</li> <li>每个规则when-then语法来定义规则的条件。</li> <li>根据订单请求的输入值,我们正在为结果添加折扣。如果规则表达式匹配,每个规则都会向全局结果变量添加额外的折扣。</li> </ul> <h2 id="添加service层">添加Service层</h2> <p>创建一个名为OrderDiscountService 的服务类,如下:</p> <div class="language-java highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nd">@Service</span> <span class="kd">public</span> <span class="kd">class</span> <span class="nc">OrderDiscountService</span> <span class="o">{</span> <span class="nd">@Autowired</span> <span class="kd">private</span> <span class="nc">KieContainer</span> <span class="n">kieContainer</span><span class="o">;</span> <span class="kd">public</span> <span class="nc">OrderDiscount</span> <span class="nf">getDiscount</span><span class="o">(</span><span class="nc">OrderRequest</span> <span class="n">orderRequest</span><span class="o">)</span> <span class="o">{</span> <span class="nc">OrderDiscount</span> <span class="n">orderDiscount</span> <span class="o">=</span> <span class="k">new</span> <span class="nc">OrderDiscount</span><span class="o">();</span> <span class="c1">// 开启会话 </span> <span class="nc">KieSession</span> <span class="n">kieSession</span> <span class="o">=</span> <span class="n">kieContainer</span><span class="o">.</span><span class="na">newKieSession</span><span class="o">();</span> <span class="c1">// 设置折扣对象 </span> <span class="n">kieSession</span><span class="o">.</span><span class="na">setGlobal</span><span class="o">(</span><span class="s">"orderDiscount"</span><span class="o">,</span> <span class="n">orderDiscount</span><span class="o">);</span> <span class="c1">// 设置订单对象 </span> <span class="n">kieSession</span><span class="o">.</span><span class="na">insert</span><span class="o">(</span><span class="n">orderRequest</span><span class="o">);</span> <span class="c1">// 触发规则 </span> <span class="n">kieSession</span><span class="o">.</span><span class="na">fireAllRules</span><span class="o">();</span> <span class="c1">// 中止会话 </span> <span class="n">kieSession</span><span class="o">.</span><span class="na">dispose</span><span class="o">();</span> <span class="k">return</span> <span class="n">orderDiscount</span><span class="o">;</span> <span class="o">}</span> <span class="o">}</span> </code></pre></div></div> <ul> <li>注入KieContainer实例并创建一个KieSession实例。</li> <li>设置了一个OrderDiscount类型的全局参数,它将保存规则执行结果。</li> <li>使用insert()方法将请求对象传递给 drl 文件。</li> <li>调用fireAllRules()方法触发所有规则。</li> <li>最后通过调用KieSession 的dispose()方法终止会话。</li> </ul>杨京京Drools 规则引擎实战