前言鸽了很久之后的今天突然心血来潮，准备写一个系列：以太坊黑暗森林，它介绍以太坊生态上的各种奇思妙想和逆天的攻击方式，会从简单的、常见的攻击方式开始介绍。取这个名字是因为我接触以太坊不久后看的一篇文章 [Ethereum is a Dark Forest](https://www.paradigm.xyz/2020/08/ethereum-is-a-dark-forest)，让我想起了《三体》小说中刘慈欣描述的黑暗森林，以太坊是一个弱肉强食的、没有规则的世界，猎人们总是躲在背后监听所有的交易，一旦发现猎物，它们会把它的血给吸干。开盘抢币相信进入以太坊生态的韭菜们，一定有过在 uniswap 上买刚开盘新币的经历，新开盘的币，一般会上涨几倍甚至十几倍，越早买入则越能低价买入。你守着时间，等着项目方添加流动性后第一时间买入代币，但是你发现，无论你的手速多块，总是看到一开盘，价格已经飚了几倍，你骂骂咧咧，开始不断拉高 gas 费用，尝试继续买入，但是你眼睁睁的看着代币涨到十倍，自己的交易却一直失败，你开始怀疑项目方自己抢跑，怀疑项目方捣鬼：肯定是项目方吃相难看，用老鼠仓提前买了。另一些聪明人，研究了以太坊的基本技术，他们在 ethscan 了解了关于“Block”、“Transaction”、“Event”的基本知识，他们聪明的想到：只要我写个程序，不断监听新的区块，只要发现有uniswap添加流动性的事件，我立即买入代币，岂不是比我手动，买入更快？于是他们开发了程序，监听了区块，也成功监听到事件，但是他们遗憾的发现，他们买入时的价格已经上涨十倍了。mempool 内存池韭菜们百思不得其解，正当他们一筹莫展之际，突然发现一个新的名词：mempool（内存池）。什么是 mempool？以太坊的出块时间，是 12 秒一个块，在这 12 秒的间隔中，每笔交易都被存放在一个叫做 mempool 的地方， 它只是这些交易暂存的空间，在矿工打包区块时，从 mempool 拿到所有交易，并对这些交易进行排序：按照 gas 价格进行排序，或者按照某种其它的特殊循序（正常来说都是按照 gas 价格进行排序，但是 flashbot 及其它 MEV 相关技术出现导致某些矿工不会按照这个顺序打包），mempool 存放的都是未决交易，它们还未打包但是即将在下个区块中打包，并且 mempool 是公开的，可以被任何人查询。至此，韭菜们发现为什么他们买入时价格已经是涨了几倍之后：如果在 mempool 中监听流动性事件，并且设置 gas 费用总是低于流动性事件的 gas 费用，然后立即发送交易，这样买入交易就可以与流动性事件的交易在同一个区块中一起被打包，打包上链后，即所谓的开盘，韭菜们才一拥而入，然而抢先的买入交易已经将价格拉高数倍。三明治另一些聪明的韭菜相信，刚开盘剧烈波动的代币不是买入的好时机，他们要等到价格平稳后再视情况买入。他们选择在开盘几个小时后，币价相对平稳，不像开盘时剧烈波动的时机买入。只见他熟练的打开 uniswap，选个 10、20% 滑点（滑点是为了防止频繁的交易失败的参数，因为你的交易会在下个区块打包，在这期间，可能会有别人的交易也会在同一个区块打包，如果别人也买了相同的代币，那么币价会上涨，在这之后的买入交易代币价格会上涨，滑点的设置表示你最多可以接受多大的价格涨幅，超出这个涨幅则交易失败），然后点击 SWAP，几十秒之后，他买到了自己的 “价值币”，心满意足的离开了，他不知道的是，他原本可以得到更多代币。他如果去 ethscan 点开自己的交易，会发现在自己交易的一前一后，刚好有同一个代币的“买入/卖出”交易，这两笔像三明治一样，将他的交易夹在中间：在他交易前先一步买入代币，在他交易后卖出代币，如果他再仔细调查，会发现提前买入代币的交易，刚好将代币价格拉升到他滑点的极限，等他买入后，再卖出获利。这与上文所描述的实际上是使用相同的技术，监听 mempool 的交易，发现目标后，发送一前一后两个交易，这两个交易的 gas 价格都非常讲究，在矿工按照 gas 价格打包时刚好将目标夹在中间然后获利。尾言当然，猎人也面临着各种各样的问题，实际情况没这么简单，原理是这么个原理，但是细节上还有很多问题要解决，例如，如何检测 mempool 的交易内容？mempool 的交易都是未执行的交易，因此没有 Event，也没有执行结果。还有如何保证在与别的竞争对手的竞争中胜出？要知道黑暗森林中盯着猎物的可不止一个猎人。以及如何避免各式各样的钓鱼，蜜獾？有些智能合约专门针对机器人制造了一些陷阱。这些内容以后再开贴细说，现在，我已经没耐心接着说了。
C# 中有两种释放资源的方式：实现 IDisposable 或使用析构函数。通常，必须在特定时间释放资源的场景中，我们实现 IDisposable，像这样：public class ExampleDispose : IDisposable { // 非托管资源 private IntPtr _handle; // 使用的其它托管资源 private readonly Stream _stream; private bool disposed = false; public ExampleDispose(Stream stream, IntPtr handle) { this._stream = stream; this._handle = handle; } public void Dispose() { if (disposed) { return; } disposed = true; // 调用其它托管资源的Dispose _stream.Dispose(); // 释放非托管资源 CloseHandle(_handle); _handle = IntPtr.Zero; } [System.Runtime.InteropServices.DllImport("Kernel32")] private extern static Boolean CloseHandle(IntPtr handle); } 通常来说，如果你在使用完后正确调用了 Dispose() 方法或者使用了 using，就可以正确释放资源。其他情况，则需要依赖 GC 在回收托管对象前先释放引用的非托管资源，一个实现了 IDisposable 接口和具有析构函数的类可能如下所示：public class ExampleDispose : IDisposable { // 非托管资源 private IntPtr _handle; // 使用的其它托管资源 private readonly Stream _stream; private bool disposed = false; public ExampleDispose(Stream stream, IntPtr handle) { this._stream = stream; this._handle = handle; } ~ExampleDispose() { // 此代码有异常情况，不要在生产环境使用 Dispose(); } public void Dispose() { if (disposed) { return; } disposed = true; // 调用其它托管资源的Dispose _stream.Dispose(); // 释放非托管资源 CloseHandle(_handle); _handle = IntPtr.Zero; } [System.Runtime.InteropServices.DllImport("Kernel32")] private extern static Boolean CloseHandle(IntPtr handle); }相比上部分代码，只是多增加了一个析构函数 ~ExampleDispose()，仔细观察它非常有意思，构造函数用于对象的创建，析构函数有点像构造函数，只是没有访问修饰符，且前面多了个 “~”，我们知道这个符号通常用于取反操作，构造函数取反，那就是“销毁函数”，也正如它所隐含的含义那样：GC 会在回收对象前调用析构函数来释放资源。现在，无论用户是否主动调用过 Dispose()，~ExampleDispose() 都会在 GC 回收对象时被调用，如果用户主动释放过资源，那么 disposed 为 true，~ExampleDispose() 实际上调用 Dispose() 后直接返回，没有多于的操作，但是另一种情况，用户从未主动调用过 Dispose()，这时候在 GC 在调用 ~ExampleDispose() 时，会调用到 _stream.Dispose();。注意，这里可能会产问题，因为 GC 在回收对象时，对象内部其它对象可能已经被回收，这里 _stream 是有可能已经被回收的，因此，我们需要稍微更改一下，以实现在 GC 时不引用任何托管代码，为此，我们实现 dispose pattern：public class ExampleDispose : IDisposable { // 非托管资源 private IntPtr _handle; // 使用的其它托管资源 private readonly Stream _stream; private bool disposed = false; public ExampleDispose(Stream stream, IntPtr handle) { this._stream = stream; this._handle = handle; } ~ExampleDispose() { Dispose(false); } public void Dispose() { Dispose(true); } // 如果 disposing 为 true, 则这是用户代码主动释放， // 可以安全的释放托管与非托管对象 // 反之，则为 `GC` 回收对象时调用，这时不可引用任何托管对象 protected virtual void Dispose(bool disposing) { if (disposed) { return; } disposed = true; if (disposing) { // 调用其它托管资源的Dispose _stream.Dispose(); } // 释放非托管资源 CloseHandle(_handle); _handle = IntPtr.Zero; } [System.Runtime.InteropServices.DllImport("Kernel32")] private extern static Boolean CloseHandle(IntPtr handle); } 这样看起来，它已经能够正常工作了，但是还有个小问题：即使用户主动释放了资源，GC 还是会对 ~ExampleDispose() 进行调用。我们再对代码进行一次小更改，实现用户主动释放资源后，使得 GC 回收对象时不再调用 ~ExampleDispose()：public class ExampleDispose : IDisposable { // 非托管资源 private IntPtr _handle; // 使用的其它托管资源 private readonly Stream _stream; private bool disposed = false; public ExampleDispose(Stream stream, IntPtr handle) { this._stream = stream; this._handle = handle; } ~ExampleDispose() { Dispose(false); } public void Dispose() { Dispose(true); // 调用 GC.SuppressFinalize 将该对象从终结队列中移除， // 并防止该对象的终结代码再次执行。 GC.SuppressFinalize(this); } // 如果 disposing 为 true, 则这是用户代码主动释放， // 可以安全的释放托管与非托管对象 // 反之，则为 `GC` 回收对象时调用，这时不可引用任何托管对象 protected virtual void Dispose(bool disposing) { if (disposed) { return; } disposed = true; if (disposing) { // 调用其它托管资源的Dispose _stream.Dispose(); } // 释放非托管资源 CloseHandle(_handle); _handle = IntPtr.Zero; } [System.Runtime.InteropServices.DllImport("Kernel32")] private extern static Boolean CloseHandle(IntPtr handle); }ReferenceImplement a Dispose methodGC ClassObject.Finalize MethodIDisposable Interface
在 Java 里有个称之为线程本地变量的类型叫做 ThreadLocal，它与 ThreadLocal 之于 C# 中是一样的作用，可以在线程范围内设置变量，这个变量只会在当前线程可被访问，但是它们有一点不同的是，在 Java 中，当你设置好变量后，在线程使用完毕回到线程池之前，需要手动调用 ThreadLocal.remove() 方法去清除线程本地变量，否则变量随着线程回到线程池，并且在下次使用此线程时此变量继续存在，而在 C# 中，线程回到线程池时会自动清除本地变量，因此无需手动去清除。我们的业务有这样一个场景：某个业务 UserService 类中，具有多个方法会频繁（甚至循环）调用一个获取用户标签的接口，具体原因是因为某些方法会进行递归，数据结构有个树状结构，因此，为了优化接口响应时间以及看起来不那么蠢，我使用 ThreadLocal 将用户标签接口的返回数据存储到当前线程，因为在单个请求中，多次调用此接口获取数据是不必要的，它看起来像这样： /** * 此静态变量ThreadLocal会为每个线程创建本地副本, 因此USER_TAGS_THREAD_LOCAL的更改只会影响当前线程 注意, * 即使线程被回收到线程池, 此变量副本也不会被清除, 因此, 需要在使用完毕后手动清除, USER_TAGS_THREAD_LOCAL.remove() * 否则可能会导致从线程池拿到的线程带有之前的变量 无论如何, 这不会产生内存泄漏问题 * 此变量在controller返回或controller异常时进行清除操作 */ public final static ThreadLocal<List<Tag>> USER_TAGS_THREAD_LOCAL = ThreadLocal.withInitial(() -> new ArrayList<>()); /** * 获取用户标签 */ private List<Tag> getUserTags(String userId) { if (StringUtil.isNullOrEmpty(userId)) { return new ArrayList<>(); } if (!CollectionUtils.isEmpty(USER_TAGS_THREAD_LOCAL.get())) { // 返回线程本地存储的用户标签 return USER_TAGS_THREAD_LOCAL.get(); } // 调用接口获取用户标签 List<Tag> tags = getUserTags(userId); USER_TAGS_THREAD_LOCAL.get().addAll(tags); return USER_TAGS_THREAD_LOCAL.get(); } // 在多个方法中会用到获取用户标签，这里假设会递归调用 public User queryUserInfo(String userId) { User user = getUser(userId); List<Tag> userTags = getUserTags(userId); user.Parent = queryUserInfo(user.ParentId); ... } 这里有个问题，那就是清除 ThreadLocal 的时机，假设我们在 queryUserInfo 方法中，增加一个 try/catch/finally 块，在每次调用完毕后进行清除，这肯定不合适，这样的递归方法，每次都清除，会失去 ThreadLocal 的意义，那么在 queryUserInfo 外层再增加一个方法，例如： public User queryUserInfoWapper(String userId) { try { return queryUserInfo(userId); } catch { ... } finally { USER_TAGS_THREAD_LOCAL.remove(); } }这看起来确实解决了问题。但是我们在这个 UserService 类中有多个类似 queryUserInfo 的方法，为每个类似 queryUserInfo 的方法都增加 try/catch/finally 块，你得找到所有的点去清除 ThreadLocal，很可能会出现某些点漏掉，或者一次请求线程中多次清除了 ThreadLocal。于是我开始寻找，某种能在线程池回收线程时的钩子，在线程回收时清除线程上的变量，当然最后由于实现难度或开发进度等种种原因，我选择了在控制器入口进行清除，我将 USER_TAGS_THREAD_LOCAL 属性暴露给外层，在控制器入口调用 USER_TAGS_THREAD_LOCAL.remove()： @PostMapping public User queryUserInfo(String userId) { try { User user = userService.queryUserInfo(userId); return user; } catch (Exception exception) { throw exception; } finally { UserService.USER_TAGS_THREAD_LOCAL.remove(); } } @HystrixCommand(fallbackMethod = "fallbackMethod", commandKey = "commandKey", threadPoolKey = "threadPoolKey") @PostMapping public User anotherQueryUserInfo(String userId) { try { User user = userService.queryUserInfo(userId); return user; } catch (Exception exception) { throw exception; } finally { UserService.USER_TAGS_THREAD_LOCAL.remove(); } }实际上这种做法很不好，它隐式的需要你在使用了某些调用过 getUserTags 的方法后手动使用 USER_TAGS_THREAD_LOCAL.remove()，否则线程回到线程池依然带有上次保存的数据。然而这种隐式的操作，只有了解这个 UserService 人才知道，别人可能直接使用业务方法而不知道需要手动调用 USER_TAGS_THREAD_LOCAL.remove()，我们的 BUG 也正因此产生。在某个功能上线后，我们原先运行正常的 queryUserInfo 接口突然数据不正确，查看日志排查问题后发现，是因为 getUserTags 方法的 USER_TAGS_THREAD_LOCAL 数据不正常，在多个不同用户之间数据错乱，A 的标签数据被 B 返回，B 的标签数据被 C 返回。找了一个多小时的原因，这段代码怎么看也没问题，因为无论如何在控制器的 finally 块会清除 USER_TAGS_THREAD_LOCAL， 最终没辙，准备先解决问题（移除 USER_TAGS_THREAD_LOCAL 的使用）再详细查看问题的原因。正改着代码，突然一旁的同事表示，他知道原因了，他在某个其它接口的内部方法调用了 UserService，但是他并不知道需要清除 USER_TAGS_THREAD_LOCAL。那么问题找到了，接口数据不正常是因为其它接口的用户请求后会将 USER_TAGS_THREAD_LOCAL 数据设置到请求线程，线程回到线程池没有被清除 USER_TAGS_THREAD_LOCAL，而后 queryUserInfo 接口再次拿到相同的线程就会有上个用户留下的数据。等等，queryUserInfo 接口虽然数据错误，但是 anotherQueryUserInfo 接口却数据正常，这两调用的是完全相同的业务方法啊？仔细一看，哦，原来 anotherQueryUserInfo 使用了 Hystrix，它使用自己的线程池去分配线程执行，因此，即使 默认线程池的线程带有错误数据，也不影响它。
我在币安智能链的日子-区块链基础
区块和链无论是比特币还是以太坊，都是具有一个个区块（称之为Block）的链式结构，学过<数据结构>的肯定明白链表，区块链就像一个链表，每个区块都存储上一个区块哈希。链（称之为Chain），有非常多的链，他们的协议不同，技术也不尽相同，比特币网络是一个链，以太坊网络是另一个链，每个链都有自己的目标（甚至目标只是为了圈钱），每个链也都有自己的代币，比特币网络的代币是比特币，每次交易都需要比特币作为手续费，以太坊网络代币是以太币，每次在以太坊网络的交易都需要以太币作为手续费。所以，链实际上作为基础设施，非常多的团队喜欢创建新的链，但是一个链光有网络光有代币不行，没有生态，很难成功。币安智能链（Binance Smart Chain：BSC）我的主要操作都是在BSC上，没有其它原因，只因为一个穷字。在BTC网络交易，需要BTC用作手续费，这个我可用不起，在以太坊（Ethereum）网络交易，需要以太币（ETH）作为用作手续费，按照以太币目前（2022-01-01）的价格，单次交易大概要40刀左右（取决于具体操作），因此称为富人链，而BSC网络的代币币安币（BNB）价格要便宜很多，目前单次交易手续费在3刀（取决于具体操作）左右。因此，我实际上没有频繁在BTC与以太坊网络交易，实际经验来自于BSC。前面说过，每个Chain的技术不尽相同，BSC是从以太坊拓展而来，它兼容以太坊虚拟机(EVM)，因此完全兼容以太坊，甚至同一个钱包地址在以太坊与BSC完全一致，你拥有的BSC钱包即是你的以太坊钱包，以太坊钱包地址也可以是你的BSC钱包地址。BSC采用共识机制来降低交易费用和提升交易速度，这个咱不用管，重点是完全兼容以太坊，这有个很大的好处是可以吃一部分以太坊的生态，以前那些在以太坊圈钱的项目现在可以轻松跨链到BSC继续圈一波钱，本来圈不到以太坊大佬钱的项目现在可以上BSC圈一波韭菜的钱，BSC手续费低准入门槛就低，我这种韭菜就喜欢这种链，另外一个好处就是支持智能合约，以太坊的（EVM）能够执行图灵完备的智能合约代码，BSC当然也能。智能合约（Smart Contract）智能合约是以太坊的一项重大创新，它允许开发人员自己编写合约并部署到网络上，由以太坊虚拟机EVM来执行，合约代码是图灵完备的并且它一旦部署就不允许修改，且只能被动的被调用不可以主动，它也像个人账户一样可以存储各种代币的余额。同质化代币与非同质化代币（NFT）ERC20协议 同质化代币简单来说如果你想在以太坊或BSC上发行新的代币（称为Token），必须通过部署合约来操作，部署一个合约，合约指定了发行代币的数量，同时也存储每个地址所持有代币的数量，代币的转移，从一个账户到另一个账户的转移（交易），都需要的调用代币对应的合约来执行，合约内部将某个地址持有的代币数量转移到另一个地址。这套标准的东西既是ERC20，实现ERC20标准的合约必须具有某些特定的方法（例如必须实现代币转移的方法transferFrom等）。因为每个代币都是完全相等可等同置换的，因此是同质化代币。ERC721协议 非同质化代币（NFT）与ERC720一样，它也是一套标准，实现ERC721协议的合约必须实现某些方法，唯一的区别在于，它的每个代币都有独一无二的ID，例如你要发行一个鞋子NFT，每双鞋都是不一样的，不可以等同置换，鞋子也是代币，但是每双鞋子都是一个独一无二的NFT，因此它是非同质化代币。ERC721的代币转移需要指定代币的具体ID进行转移。我发行的新代币有价值吗？因为有ERC20协议的存在，它制定了一套标准，因此可以根据这个标准制定出一套合约模板，你只需要填写代币名称，填写发行数量，就可以发行任意数量的新代币，任何人都可以部署智能合约，因此任何人都可以发行新的代币，但是新的代币是没有价值的，价值是人的共识，股票为什么有价值？因为股票背后是公司，有实体，如果代币发行的团队或个人没有价值，那么这个代币不会有价值。
All about ExecutionContext and SynchronizationContext
前言这篇文章深入探讨了ExecutionContext和SynchronizationContext, 这是大部分开发人员都不需要了解的 .NET 高级领域.SynchronizationContext我最早关于 SynchronizationContext 了解可能是在 WindowsForm 程序, 当我错误的在其它线程去更新 UI 控件时, 总会出现一个异常 Cross-thread operation not valid: Control accessed from a thread other than the thread it was created on 翻译过来即 跨线程操作无效：控件从创建它的线程以外的线程访问, 例如这个代码片段:private void button_Click(object sender, EventArgs e) { Task.Run(() => { //异常: 跨线程操作无效：控件从创建它的线程以外的线程访问 textBox.Text = "异步更新"; }); }我相信如果你写过 WindowsForm 程序, 肯定遇到过这个错误, 这是因为 WindowsForm 程序希望所有的 UI 操作都通过 UI 线程去更新, 当你在单线程中, 无论怎样操作, 都是可以的, 因为不管是按钮事件还是文本框事件, 都是通过 UI 线程触发的, 那么事件内代码也是通过 UI 线程执行的, 因此, 如果你没有其它线程去操作控件的话, 是不会出现此异常的. 我当时通过简单的禁止此检查来避免异常, 实际上正确的方式应当通过我们的主角之一 SynchronizationContext 去更新UI, 像这样:private void button_Click(object sender, EventArgs e) { var sc = SynchronizationContext.Current; Task.Run(() => { sc.Post(delegate { textBox.Text = "异步更新"; }, null); }); }这里发生了什么? 抓住我的手, 我们一步步进行探索. SynchronizationContext 是一个具有抽象功能的类, 主要的两个方法 Send 以及 Post, 这两个方法的动作一致, 仅仅是同步与非同步的区别, Post 接受一个委托, 用来将委托排队到线程上下文, 这里的上下文可能是多个线程的上下文, 也可能是单个线程的上下文, 这取决于具体的 SynchronizationContext 实现. WindowsFormsSynchronizationContext 在 WindowsForm 程序启动时被设置到 UI 线程, 因此在这里, SynchronizationContext.Current, 实际上是 WindowsFormsSynchronizationContext, 它的上下文是单个 UI 线程, WindowsFormsSynchronizationContext 重写了 Post 及 Send, 它将委托传递给基础 Win32 消息循环, 而后 UI 线程接受消息并执行委托方法. 因此这段代码解释为: 在异步线程中将委托方法排队到 UI 线程, 随后 UI 线程更新 textBox.Text.前面说到, SynchronizationContext 并不表示真正的线程同步上下文, 实际情况要看它具体的实现, 例如在 WPF 和 Silverlight 程序中, 它是 DispatcherSynchronizationContext, 它将委托以 Normal 优先级排列到 UI 线程的 Dispatcher. 当线程通过调用 Dispatcher.Run 开始其 Dispatcher 循环时, 此 SynchronizationContext 被设置为当前上下文. DispatcherSynchronizationContext 的上下文是单个 UI 线程. 默认的 SynchronizationContext 是 SynchronizationContext 实例, 如果线程的 SynchronizationContext.Current 为 null, 则认为具有默认的SynchronizationContext. 默认的 SynchronizationContext 将 Post 委托排队到线程池上, 而 Send 委托则使用调用 Send 方法的线程同步执行. 因此从这个角度来说, SynchronizationContext 上下文是整个 ThreadPool.SynchronizationContext 的 Post 方法并不总是异步排队执行, 在 ASP.NET 中, SynchronizationContext 实际为 AspNetSynchronizationContext, 这种情况下即使使用 Post 方法, 也会立即执行委托.SynchronizationContext 实现摘要用于执行委托的特殊线程一次执行一个委托有序(按照排队顺序)Send方法直接调用委托Post 方法直接调用委托WindowsForm是是是仅限于UI线程调用从不WPF/Silverlight是是是仅限于UI线程调用从不Default否否否总是从不ASP.NET否是否总是总是ExecutionContext执行上下文是绝大多数开发人员所不需要关心的, 它像空气一样, 它总是在那, 但是你从不关心, 当你关心它的时候, 那么一定是出了些什么问题. ExecutionContext 就像线程的环境, 可以看成是一个容器, 这个容器里面包含了所有线程的其它上下文信息, 例如 SecurityContext, 它维护诸如当前 主体 之类的信息以及有关代码访问安全性 (CAS) 拒绝和允许的信息. 在许多系统中, 这样的数据通常存储在 ThreadLocalStorage(TLS), 在同步世界中, 这样的存储就够了, 一切都发生在那个线程上, 任何数据都能从线程的本地存储中恢复.但是当我们从同步世界转向异步世界时, 线程的本地存储就无法使用了, 你的代码可能运行在不同的线程中. 例如在同步世界中, 你执行 A 操作, 再执行 B 操作, 它们的线程环境都是相同的, 线程的本地存储数据也是共享的, 但是到了异步, A 操作与 B 操作可能是两个不同的线程执行, 因此, 它们的线程环境不同, 线程的本地存储数据也不会从一个线程流向另一个线程. 线程本地存储是特定于线程的, 而异步操作与特定线程无关. 然而, 通常存在一个逻辑控制流, 以便于将环境数据从一个线程 "流向" 另一个线程, 这就是 ExecutionContext 所存在的意义.ExecutionContext 实际上只是一个状态包, 可用于从一个线程捕获所有这些状态, 然后在逻辑控制流继续时将其恢复到另一个线程. ExecutionContext 是使用静态 Capture 方法捕获的:// 捕获环境状态 ExecutionContext ec = ExecutionContext.Capture();它在通过静态 Run 方法调用委托期间恢复:ExecutionContext.Run(ec, delegate { ... // 此处的代码将 ec 的状态视为环境 }, null);.NET Framework 中的所有异步操作都通过这种方式进行捕获和恢复 (除了那些以Unsafe开头的方法, 这些方法表示为不安全的, 因为它们明确不捕获和恢复环境数据). 例如, 当你使用 Task.Run 时, ExecutionContext首先捕获当前线程的环境数据, 然后将ExecutionContext存储到Task中, 之后当 Task.Run开始执行委托方法时, ExecutionContext.Run 将被调用恢复环境数据. 对于 Task.Run、ThreadPool.QueueUserWorkItem、Delegate.BeginInvoke、Stream.BeginRead、DispatcherSynchronizationContext.Post 以及你能想到的任何其他异步 API, 都是如此. 它们都捕获 ExecutionContext 并存储它, 然后在稍后执行委托的期间恢复环境上下文.async/await 的情况async/await 语法糖背后的框架支持自动捕获和恢复 ExecutionContext 以及 SynchronizationContext.在异步开始等待时, 框架捕获 ExecutionContext 并将引用赋值给 Awaiter, Awaiter 在稍后恢复时, 将还原 ExecutionContext, 这意味着 ExecutionContext 能够流过异步await.上述对 ExecutionContext 的支持内置在表示异步方法的 "构建器" 中 (例如 System.Runtime.CompilerServices.AsyncTaskMethodBuilder), 并且这些构建器确保 ExecutionContext 流过等待点，而不管使用的是哪种类型的等待. 相比之下, 对 SynchronizationContext 的支持内置于对等待 Task 和 Task<TResult> 的支持中. 自定义等待器可以自己添加类似的逻辑，但它们不会自动获取, 这是设计使然, 因为能够自定义调用延续的时间和方式是自定义等待器有用的部分原因.当你等待一个 Task 时, 默认将捕获当前线程的 SynchronizationContext, 如果不为空, 则会将等待完成后的后续代码使用 SynchronizationContext.Post 发送到 SynchronizationContext 上下文执行, 而不是在当前线程执行, 作为样例, 考虑一个常见的 WindowsForm 程序线程死锁代码:static async Task<string> LoadStringAsync() { string firstName = await GetFirstNameAsync();// 线程发生死锁 string lastName = await GetLastNameAsync(); return firstName + ” ” + lastName; } private void button1_Click(object sender, RoutedEventArgs e) { Task<string> s = LoadStringAsync(); textBox1.Text = s.Result; }为什么? 当 await GetFirstNameAsync 时, SynchronizationContext 被捕获, 用来在异步方法完成后使用 SynchronizationContext.Post 发送委托方法给 UI 线程继续执行 GetFirstNameAsync, 然而, 当异步方法完成时, UI 线程正在 button1_Click 方法同步等待异步执行完成, 而异步线程在等待 UI 线程继续执行 GetFirstNameAsync, 因此死锁发生.ExecutionContext中的SynchronizationContext实际上, SynchronizationContext 也是 ExecutionContext 的一员. 当 ExecutionContext.Capture 捕获时, SynchronizationContext 也会被捕获并且设置为 SynchronizationContext.Current, 这会带来一些问题, 一个是 SynchronizationContext.Current 意义开始变得不明确模糊起来, 在线程中拿到的 SynchronizationContext.Current 可能是前面线程的同步上下文, 有可能是当前的线程同步上下文, 这导致了混乱, 作为这可能会出现问题的一个示例, 请考虑以下代码:private void button1_Click(object sender, EventArgs e) { button1.Text = await Task.Run(async delegate { string data = await DownloadAsync(); return Compute(data); }); }这有什么问题? 我期望的是, Task.Run 执行时, ExecutionContext.Capture 捕获当前线程环境, 并在 Task 实际执行是恢复环境信息, 当执行 await DownloadAsync() 时, 如果 SynchronizationContext.Current 为 null, 则 DownloadAsync 之后的 Compute 代码会作为线程池线程执行, 而如果 SynchronizationContext.Current 随着环境信息流动下来, 则 DownloadAsync 之后的 Compute 代码会被 SynchronizationContext.Post 发送到 UI 线程进行执行, 这会可能会导致 UI 界面有些阻塞.幸运的是, ExecutionContext.Capture 实际上有一个重载方法, 但是这个重载方法并不公开 (在mscorlib内部), 它允许可选的阻止 SynchronizationContext 作为环境的一部分向下流动, 与此相对应, 还有一个 ExecutionContext.Run 的重载方法, 它支持忽略存储在 ExecutionContext 中的 SynchronizationContext, 由 mscorlib 公开的大部分异步 API 都是使用的重载方法, 因此 SynchronizationContext 不会随着环境信息向下流动. 但是任何独立于 mscorlib 之外的异步实现 API 都将流动 SynchronizationContext. 上面所说的异步方法构建器(AsyncTaskMethodBuilder), 是实现于 mscorlib 的 API, 且确实使用了重载方法, 因此它不会随着环境信息流动.简单来说, SynchronizationContext.Current 不会随着 await Task 向下流动, ExecutionContext 会随着 await Task 向下流动.ReferenceExecutionContext vs SynchronizationContextAwait, and UI, and deadlocks! Oh my!Parallel Computing - It's All About the SynchronizationContextASP.NET Core SynchronizationContext
业务规则引擎(Business Rule Engine)什么是规则引擎在实际的业务场景中, 会有多个业务规则, 这些政策可能是公司策略, 也可能是法律法规要求. 例如在注册新用户时, 如果年龄小于18岁则需要同意额外的用户协议, 或者在促销时, 满足一定的金额要求则进行部分费用减免. 这些都是业务规则, 而基于这些规则, 给定一个或多个事实, 事实满足规则后可执行一些动作, 这便是规则引擎. 简单来说，你可以看成一堆 if-else 语句.基本上, 规则引擎围绕以下几个点进行操作:规则(Rule), 即是条件.事实(Fact), 存在某种东西是一种事实, 不存在某种东西也是一种事实, 规则引擎将现有的事实带入到规则中, 如果满足则执行动作.动作(Action), 满足某种规则后需要执行的动作.为什么需要规则引擎主要有以下几个原因:业务规则是多变的. 例如今天可能针对22岁以下的用户给予学生优惠, 明天则可能只针对18岁以下的用户给予学生优惠. 你肯定不想天天改这部分代码.业务规则具有种类多的特点. 例如满100元减免50元. 满200元减免110元. 满300元打1折 等等等等很多种规则, 如果将这部分逻辑硬写到代码里, 会有大量的条件判断, 且难以维护. 并且业务反复更改时, 都需要反复变更这部分代码.需要满足时效性要求. 规则硬编码到程序内部意味着规则的更改需要编码和部署, 整个上线流程是需要花费时间的, 如果能够在业务规则发生变化时, 动态重新加载规则, 甚至无需重新部署则新的规则便可应用, 那就极大的减少了变更规则带来的时间成本.让运营人员自己可动态配置规则. 这是很多人使用规则引擎所期望的一件事情, 让运营自己管理业务规则, 不占用开发资源. 但是与往常一样，这听起来似乎有道理，但在实践中却很少行得通. 要满足这个要求, 大部分情况下需要对规则引擎进行二次开发, 否则最终结果很可能便是配置了一堆没有人知道是干什么的规则.业务场景优惠券满减折扣指定商品扣减立减活动全场七折某个日期范围内立减某个品牌七折女用户七折如果现在有如上促销活动逻辑, 你应该怎么做? 将所有的业务逻辑硬编码到代码里? 哼哧哼哧开干, 刚完工, 这时候业务运营大佬来了: 不行啊, 全场七折要亏死了, 得改成全场9折, 另外这个某品牌七折活动他们突然不合作了, 得拿掉这个规则. 那么今天你和运营大佬可能只有一个人能站着走出去.但是, 如果你用了规则引擎, 则可以泰然自若, 喝一口浓茶并花一分钟修改和禁用规则.DroolsDrools是KIE(Knowlegde is Everything)社区的产物, 专门用来做业务规则引擎, 具有基于前向链和后向链推理的规则引擎，可以快速可靠地评估业务规则和复杂事件处理, 还可以与jBMP进行业务流程处理上的配合.运行原理Drools引擎使用以下基本组件运行规则(Rules): 业务规则, 所有规则必须至少包含触发规则的条件和规则规定的操作.事实(Facts): 在Drools引擎中输入或更改的数据, 与规则条件匹配以执行适用的规则.生产内存(Production memory)：规则(Rules)存储在Drools引擎中的位置.工作内存(Working memory)：事实(Facts)存储在Drools引擎中的位置.议程(Agenda): 满足条件的规则准备执行的位置.当业务用户或自动化系统往Drools引擎中添加事实(Facts)时, 这些事实被存储在Drools引擎的工作内存(Working memory)中, Drools引擎将这些事实与存储在生产内存(Production memory)中的规则(Rules)条件进行匹配，以确定符合条件的规则执行(这种将事实与规则匹配的过程通常称为模式匹配(pattern matching)). 当满足规则条件时，Drools引擎激活并在议程中(Agenda)注册规则，然后Drools引擎在议程中排序优先或冲突的规则以准备执行。下图说明了Drools引擎的这些基本组件Drools引擎中的执行逻辑议程评估(Agenda evaluation): 在这个阶段, Drools引擎选择所有可以执行的规则. 如果不存在可执行规则, 则执行周期结束. 如果找到可执行规则，Drools引擎会在议程(Agenda)中注册激活, 然后进入工作记忆操作阶段以执行规则满足后的动作.工作内存动作(Working memory actions): 在此阶段，Drools引擎为之前在议程(Agenda)中注册的所有已激活规则执行Action操作。在所有操作完成或主Java应用程序进程再次调用fireAllRules()后,Drools引擎返回议程评估(Agenda evaluation)阶段重新评估规则.为什么是Drools专业足够灵活与Java的深度互操作性能够与jBMP集成支持业务流程处理能力文档足够全面业内使用者众多开源入门安装及开发环境配置安装请走传送门Installing Drools Tooling.Visual Studio Code 开发环境安装NoneDRL(Drools Rule Language)DRL是一种DLS(Domain Specific Language)语言, 它专门为某个专业的特殊领域编写, 旨在提高领域内的编程效率, DRL即是为了方便我们编写业务规则而产生的, 它的基本语法非常简单, 既通过三个关键字即可编写规则:# import package import com.example.demo.User rule "your first rule" when # conditions User() then # action System.out.println("user exist"); end如上, 你可以像在java里一样导入文件, when关键字定义条件, then关键字定义满足条件后执行的动作, 在then里你可以使用纯java代码. 更多的语法你可以在Drools官方文档中找到.BPMN2NoneDrools业务中心(Business Central)None结尾业务规则是复杂的且变化快速的, 传统在代码里面硬写业务逻辑, 会导致大量的if-else判断, 并且业务的不断变更也会使得代码经常改动变得难以维护. Drools的出现很好的解决了这一难题, 它能够将业务规则的变化部分迁移到规则文件中, 而代码则是编写利于维护的不常变更的部分, 这不仅是业务规则这个领域的问题, 更多的还是一种思想上的东西, 将变化的业务从代码中剥离出来, 从而构造一个稳定的系统.ReferenceDrools IntroductionDrools DocumentationRulesEngineDomain Specific LanguagesProduction Rule SystemWhat is Rule-Engine
前言现代的前端开发，通常使用前后端分离的形式，前端专注页面，数据来源于后端接口。这种开发方式使得职责更加明确且项目文件也更加的清晰起来，但是在本地开发过程中则有一点小问题：我如何调用后端接口？如果直接调用服务器上的接口，或者本地启动后端项目，由于端口不同，都会出现跨域问题，而服务端配置允许跨域也不太合适，那么，能否通过设置代理来绕过浏览器限制呢？声明Create React App以下简称CRA。根据CRA文档，本地配置代理有两种方式：在package.json中新增属性proxy来设置目标地址。通过http-proxy-middleware模块手动配置灵活的代理。两种方式各有好处，通过在package.json文件增加proxy属性的方式非常简单，一行代码即可解决代理问题，但是这个proxy地址没法根据配置进行动态切换，且package.json文件是需要提交到代码仓库的，如果每个人都有不同的目标地址，则package.json一定被大家改来改去且发生合并冲突。另外一种方式则是通过手动配置http-proxy-middleware的方式，根据CRA文档的描述，分为三个步骤：安装http-proxy-middleware模块：$ npm install http-proxy-middleware --save $ # 或者 $ yarn add http-proxy-middleware在src目录下创建setupProxy.js文件：$ mkdir src\setupProxy.js配置setupProxy.js：const { createProxyMiddleware } = require('http-proxy-middleware'); module.exports = function (app) { app.use( "/api", createProxyMiddleware({ target: "http://localhost:5000", changeOrigin: true }) ); }然后启动项目，CRA会自动将src/setupProxy.js注册，这样请求则被代理到http://localhost:5000。可问题还是没有被解决，setupProxy.js依然会被提交到代码仓库，所以target依然不能写死配置。但是setupProxy.js中已经可以使用node环境变量了，因此，我们做如下配置：在项目根目录新建.env.development.local文件：$ mkdir .env.development.local在.env.development.local文件新增一行：REACT_APP_PROXY_ENDPOINT=http://localhost:5000配置setupProxy.js中的target值为process.env.REACT_APP_PROXY_ENDPOINT：... createProxyMiddleware({ target: process.env.REACT_APP_PROXY_ENDPOINT, changeOrigin: true }) ...至此，你已经可以根据环境变量来动态的切换代理的地址，且此文件无需提交到代码仓库。ReferenceCRA Proxying API Requests in DevelopmentCRA Adding Custom Environment Variables
老渔夫在海边哭泣。一个人民富豪走了过了。“先生，你为什么哭泣呀？和我说说吧。我这个人是最爱做慈善的了。”老渔夫擦了擦眼泪，眼中流露出喜悦“老爷，您是个有善心的人。这可真的是太好了，我的苦难对您来说，一定不是什么问题。我现在就和您说一说我的困难吧。请不要嫌弃老头子的啰嗦。”人民富豪点点头“老爷子，您请讲，我这个人还是很有礼貌的。”老渔夫开始讲了起来：“我家祖上一直都靠捕鱼为生。我们靠自己的双手和技术吃饭。但到了我这代，日子越来越难了起来。先是有人说海边的沙滩被人承包了，不让我们住。这也倒不是不能接受，我们习惯了住在船上的。这已经是我年轻时的事了，我就这样在船上活了大半辈子。可最近有人又说这片海被人承包了，不让我们在这儿捕鱼。这可真的是要了我的老命了！”老渔夫叹了口气，忍不住又哭起来。“要是就这样，还能苟活，我们是惯于冒险的，近处的海域不让捕捞，到那远点的海域捕鱼就是了。可近来又有人把那做渔船的材料都给买光了，我们是万万买不起的。没有了土地，海洋，还失去了捕鱼的网和船，这可让我们怎么活？”富豪点了点头，笑着说：“这可真的是太简单了。我这个人是惯于做慈善的。我最近组织了一个渔业公司。不但买了这些沙滩和海域，还买了一大批渔船。老先生，你这样有技术的老渔夫，我是很欢迎的。欢迎你来我的公司工作呀。如果你有能耐，你还能管理其它渔夫，那你可是发达，成为上流人士喽。”老渔夫看了几眼富豪说：“先生，我老头子不是白活了这么久的。“授人以鱼不如授人以渔”的理我还是懂的。如果你不把这海洋和土地承包了，不把渔船垄断了，我们便可以自由的捕鱼。哪里需要您的施舍呢？您要我去，不只是帮你捕鱼那么简单的。没去你那里的时候，我是一个自由人，我的时候，我的生命都是属于我自己的。我可以三天打鱼，两天晒网。签了您那出卖生命的契约，我可就得听您的喽。您让我干几个小时，我就得干几个小时。”人民富豪笑着说：“可你也不得不签不是嘛，不签？不签这份《奋斗者协议》你怎么活得下去？现在干什么不需要钱？”老渔夫叹了口气说：“以前有个老先生说“终身役役而不见其成功，苶然疲役而不知其所归”。我是听不懂的，今天我算是懂了一点了。正因为懂得，我是更不愿咬您那名利的鱼饵。人们嘲笑鱼的贪婪，不经意咬中鱼饵。却不知道人间的饵要危险的多。我是个老渔夫了，我以天为衣，以地为居，找个遮风挡雨的地方就能存世，几块咸鱼就能生存，何必把自己自由的生命交给您呢？”人民富豪面有怒色：“人的需求是无限的，你没有需求只是没被开发出来罢了。”老渔夫转过头不愿理会人民富豪：“先生，您是个有钱人。和我这样的穷人买不起东西，是不同的。既然您的需求是无限。那牛奶何以被倒在河里，钢铁，汽车，房子何以堆积如山。”富豪视线游移不定说：“这···这个···”老渔夫继续说：“你一天只有二十四个小时，你的时间是有限的，你的需求怎么可能是无限的？你能一个人同时住在你所有的房子里面？还是你能喝干一切的牛奶。人都需求水，可即使人喝干了水杯，夸父喝干了黄河，又有谁能喝干这地球，这宇宙的水？您不要太自大了，您的需求根本就不是无限的，您的占有倒是无限的。您占有了无限了大地，您占有了江河胡海，您占有茫茫黎庶，他们都为你打工。可你还不满足，您还能到哪去满足自己呢？”人民富豪挤出个笑容说:“先生，你这可是把我想的太坏了呀。我这个人是要把商业做成慈善的。我会捐出去很多税，养着你们，给你们高福利。”老渔夫瞥了眼说：“先生，请问是我们养着你，还是你养着我们？如果是你养活了我们，我们不求你的饲养。如果是我们是像牛羊一样养活了你，那就请你不要一边说什么高福利，一边咒骂着民众懒惰。我知道您是更愿意养一批为你工作的奴隶的，又施舍一些从指尖露下的面包屑以示你的慈悲。”人民富豪嗤笑道:“你这个愚昧不化的老渔夫。我所创造的福利社会岂不是比你强的多了。只有跟着我才能过好日子，只有努力工作才能有福报。”老渔夫笑道:“我不知道你的日子好不好。但我知道你的日子是过不下去的。你从还能勉强工作的那些人手里把钱夺走，送给那些连工作都没有的人。让人活得下去。而你自己是害怕自己的钱少一分的。你想方设法逃税。哪怕你捐出自己的钱，可这资本还是越来越集中。这就是你说的福利社会吧？”人民富豪说:“所以说你这个老头子坏的狠啊。我可是遵纪守法的纳税大户。你不要污蔑人。”老渔夫笑了笑:“就算你捐了你大部分钱，这个社会也维持不下去。我闲来无事也曾观看天道，那天道损有余以补不足的，而人道则是损不足以奉有余。因为你们有钱。他们没钱。他们要为你们打工。他们的劳动时间是有限的，不能增长，而你们的资本是无限的，可以越来越壮大，让更多人为你们挣钱。”人民富豪纳闷道:“所以为什么维持不下去？”老渔夫说:“你是比我懂得挣钱的，你来说说怎么赚钱。”人民富豪说:“让他们多工作，压低他们的工资，我就能在竞争中取胜了。这样可以挣到钱。”老渔夫说:“多工作商品会多，工资低，你的支出会少。所以你的财富增加了。你的财富的本质不就是对他人时间的占有嘛。”人民富豪说:“这又如何？”老渔夫说:“所以你的商品会堆积起来，出售不出去呀。假如你的劳工生产了五件商品，你会支付多少工资呢？”人民富豪说:“五件商品的原料和地租等等我自然是要除去的。然后支付的工资越少越好呀！剩下的都是我的利润。”老渔夫说:“那你的劳工能买得起这五件商品吗？毕竟他们工资的来源已经被你压低了。”人民富豪说:“所以还是老傻子，没有需求可以创造需求嘛。把钱借给他们，形成贷款不就好了。”老渔夫说:“所以说这就是可悲的地方啊。你不仅在生产侧利用加工，占有了他们的时间。还要占有他们未来的时间。当然，你们还要鼓励消费，利用他们的消费时间赚钱。所以他们的时间被你们榨的一干二净，已经没有时间执行人类生命的本能了。恋爱，结婚，生子，休息的等时间被大幅剥夺，人类这个物种的存续都成了问题，更何况社会呢？所以说这个社会不可持续呀。”人民富豪深思一会儿说:“可持续也是我的梦想啊。我不想把他们都剥夺的干净，想给他们八小时工作时间，不想过分增加自己的财富。只要我能永远的钱比他们多，可以做他们的老板就行了。”老渔夫说:“那你会在市场竞争中落败的。”人民富豪说:“假如我垄断起来呢？”老渔夫说:“那你就是政权的实际统治者了。我们美国是会出台《反垄断法》的。”人民富豪叹了口气说:“这是当然的。可这世界也就这样，人们也就这么过。这是从来如此的。就算我不做资本家，也会有新的资本家。”老渔夫:“只要有阶级特权，就必然会走向崩溃，因此人们若想活下去。只能争取平等的权利和义务，并消灭任何阶级统治； ”“劳动者在经济上受劳动资料即生活源泉的垄断者的支配，是一切形式的奴役即社会贫困、精神屈辱和政治依附的基础。”老渔夫对着怅然若失的人民富豪说:“我个糟老头子是没读过什么书的，唯有上面两段印象深刻。你要是真的想救我，就好好想想吧。”说完老渔夫又哭了起来“呜呜呜呜，东海之日落兮，可以葬我躯；东海之日升兮，可以令我趋！沧浪之水清兮，尧舜满神州。沧浪之水浊兮，窃国为诸侯。岂余身之可悲兮，恐后世之荆棘。岂余身之可悲兮，恐万载之不复。日有落兮，日有升兮。吾有何悲，吾有何苦。观今日之芳草兮，何苗苗而茁壮。妖物其未可久兮，东方之有朝阳。既年岁之未长兮，时又尚且未央。日月不淹兮，春秋代序，草木零落兮，美人迟暮。勿变以为萧艾兮，乘骏马以索路。勿变以为萧艾兮，何不改乎此度？兰芷变而不芳兮，荃蕙化而为茅。何昔日之芳草兮，今直为此萧艾也？此固不以为喜，亦不足以悲。东海之日落兮，可以葬我躯；东海之日升兮，可以令我趋！”-- 非原创，摘抄自<知乎>，若有侵权请联系我
前言目前有很多小伙伴在用IntelliJ IDEA，这东西啥都好，就是贵，再一个就是占用内存很高，打开它也觉得非常的重。如果跟我一样有上面几个问题困扰，可以尝试使用VS Code。它插件众多，能DIY的地方也很多，特别是对于喜欢折腾的人来说，更是福音。当然如果不喜欢折腾想上手即用也可，全家桶插件非常方便。支持的Java版本范围为什么要单开一段说这件事，原因在于目前不少人认为VS Code只能支持Java 11及以上版本，这对于很多使用Java 1.8开发的同学来说简直劝退。但这其实是个误解，目前VS Code是能支持Java 1.5 - Java 15(截止本文发布时的最新版本)，基本能满足目前90%以上的Java使用者了（太老的Java版本这边移步Eclipse），至于说只能支持Java 11这件事，是指插件的运行环境需要安装最低Java 11的SDK，也就是说你如果想用VS Code开发Java程序必须要在本地安装Java 11 SDK才能够使用此插件。具体情况移步：Language Support for Java(TM) by Red Hatvscode-java JDK-Requirementsvscode-java CHANGELOG插件Java Extension Pack这是一个微软整合的Java开发环境所需要的插件集合，相当于一个插件捆绑包，安装之后会自动安装以下几个包：Language Support for Java(TM) by Red Hat提供Java语言支持Debugger for Java提供Java语言的调试支持Java Test Runner提供运行和调试单元测试的支持Maven for Java提供对Maven的支持Project Manager for Java提供对Java项目支持，例如创建Java项目和导出JarVisual Studio IntelliCode提供代码提示、自动补全等功能安装成功后按快捷键F1打开命令面板，可以看到具有一些Java项目的命令，我们先设置运行环境，选择Configure Java Runtime打开之后我们可以看到设置Java运行环境的界面，这里有几个地方需要说明一下，上面半部分的选项卡中三个表示：Project JDKs你项目所需要的JDK版本，例如你可能同时开发Java 1.8丶Java 11和Java 15，下面会介绍如何设置工作使用的JDK版本Java Tooling RuntimeJava工具所运行的环境，也就是此插件运行所需要的Java环境，这个需要最低安装Java 11且要设置到环境变量JAVA_HOME，如果你正确设置了JAVA_HOME这里就会展示你的JDK路径Installed JDKs当前机器上所安装的所有JDK路径在下半部分我们能看到有Install A JDK栏目，这里提供JDK的下载，选择你需要的JDK版本然后点下载即可，这里我们以最新的Java 15版本为例，下载后设置到环境变量JAVA_HOME，然后重新打开VS Code，使用F1快捷键打开命令面板，选择Configure Java Runtime，打开Java Tooling Runtime选项卡后，应当能看到正确设置的JDK路径：如此，第一步便已完成。Spring Boot Extension Pack这个插件也是一个全家桶插件，里面对Spring Boot项目开发所需的插件进行了打包：Spring Boot Tools提供了对Spring Boot项目的支持，例如对application*.properties及application*.yml的支持Spring Initializr Java Support提供了快速生成Spring Boot项目的支持，能够在VS Code中快速创建Spring Boot项目Spring Boot Dashboard提供了对Spring Boot项目快速启动和调试的支持，它能够检测到工作区所有的Spring Boot项目并提供统一管理的入口Cloudfoundry Manifest YML Support基本的YML格式支持Concourse CI Pipeline Editor提供了对**/*pipeline*.yml | **/pipeline/*.yml 和**/tasks/*.yml | **/*task.yml等文件的验证支持安装后我们即可进行Spring Boot的项目创建，像上面一样，先用快捷键F1打开命令面板，然后选择创建 Java 项目(Create Java Project)`：接着选择Spring Boot项目：再选择Maven或Gradle就看个人选择了，我们使用Maven做示例：再会让你选择Spring Boot的版本，我们以2.5.0做示例：再选项目语言，这里选Java：再就是填写Group，这里一般填写公司名称，我们以kirovopensource作为示例：接着按回车，输入项目名称，这里以demo为例：再次回车，选择打包的文件类型，这个也看个人选择，这里使用JAR：选择Java版本，这里出来的版本一般是环境变量进行配置的，如果没出来也没关系，可以在配置文件指定，这里先选择14：然后勾选你需要用到的项目，这里我们勾选Spring Web：回车，选择文件夹后即生成项目完成配置刚才我们生成的Java项目版本为Java 14，如果现在需要运行Java 1.8的项目该怎么办呢？首先肯定需要安装Java 1.8，可以在按照上面的方式打开命令面板在Java Configure Runtime界面安装Java 1.8，也可以自行下载安装，然后转到VS Code的设置界面，搜索java.configuration.runtimes，点击在sesstings.json中编辑：然后我们可以配置多个JDK的位置，其中name属性是固定格式的，每个版本有对应的name值：这样就完成了多个JDK环境的配置，如果项目需要Java 1.8来运行，则pom.xml文件会有<java.version>14</java.version>来标识所需要的Java版本：刚才我们创建出的项目为Java 14版本，现在我们将其改为Java 1.8运行，修改pom.xml中的<java.version>1.8</java.version>：更多配置请查看插件的官方文档，篇幅限制这里不一一展开Hello World创建controller文件夹后再创建HelloworldController.java文件后VS Code会自动将新文件自动创建为class，你也可以选择修改为enum或interface等：当然，ctor这种快速创建构造函数的快捷方式也是必不可少：Hello World推荐插件IntelliJ IDEA Keybindings针对IntelliJ IDEA 玩家，此插件可以将VS Code的编码快捷键更改为你熟悉的IntelliJ IDEA快捷键Chinese (Simplified) Language Pack for Visual Studio Code汉化VS Code所用的插件Java P3C Checker阿里巴巴Java编程规范 (P3C/阿里巴巴Java开发手册) 规约检查 Alibaba Java Coding GuidelinesReferenceJava in Visual Studio Code
real mode protected mode and segmentation
前言在操作系统启动时，会先进入实模式(Real Mode)，然后再转换为保护模式(Protected Mode)，为什么需要这么做？两种模式的寻址方式有何不同？本文通过Intel 8086和Intel 80286分段寻址的不同来解释这两种模式的区别。Intel 80868086被设计为完全的16位处理器，所有的内部寄存器、内部及外部数据总线都是16位宽，不过它具有20位宽的外部地址总线，这意味着它能够寻址2^20=1,048,576个地址，也就是最大能够支持1MB的寻址空间。这里可能会有疑问，刚才不是说8086是16位处理器吗？怎么能够进行20位地址总线寻址呢？这是由于8086处理器支持分段机制(Segmentation)，它能够通过16 * Segment + Offset的方式进行寻址，例如Segment为0xf000，Offset为0xfff0，则进行如下转换：16 * 0xf000 + 0xfff0 = 0xf0000 + 0xfff0 = 0xffff0 Segment为16位的基地址被存储在段寄存器(Segment Registers)，Offset也同样为16位的偏移地址，通过左移位的方式组合得到的结果0xffff0称之为线性地址(Linear Address)，可以理解为Segment为一个内存区域，Offset为相对于这个内存区域起始地址的偏移量。这时有的人可能会问了，如果Segment为0xffff且Offset为0xffff，通过上述公式计算:16 * 0xffff + 0xffff = 0xffff0 + 0xffff = 0x10ffef最终结果为0x10ffef，这已经超过20位地址总线的寻址范围(至少得24位地址总线才能够寻址此地址)，这时CPU会丢弃最高位，实际访问的地址为0xffef(这也是在使用更大的内存地址空间前启用A20总线的原因，既启用高位地址)。这种直接能够访问内存地址的方式称之为实模式(Real Mode)。Intel 80286当80286问世时，外部地址总线被拓宽到24位，使得它的寻址能力进一步提升，但是它所带来的不仅仅是寻址能力的提升，还有一个新东西：保护模式(Protected Mode)。保护模式下的分段机制与实模式下的分段机制有些许不同，它通过以下数据结构进行段翻译(Segment Translation)：描述符(Descriptor) 用来存储分段信息描述符表(Descriptor Table) 有描述符所组成的数组结构段选择器(Selector) 从描述符表中找到具体的某个描述符虽然80286的段寄存器(Segment Registers)依然是16位，但是它保存的不再是段基地址，而是称之为Selector的选择器，这与实模式下的分段机制有所不同。保护模式下每个分段信息是存储在对应的Descriptor中，那么我们要进行地址转换则需要先在Descriptor Table中找到这个Descriptor，这就需要Selector，通过Selector定位到Descriptor，然后翻译Segment成为线性地址(Linear Address)：Descriptor先看下Descriptor，它是由8-byte组成的数据结构：段基地址(Base Address)由三个片段组成占用32位，用来表示地址的开始位置(后续配合Offset来得到一个具体的地址)Segment Limit由两个片段组成占用20位，这两用来表示地址范围G=Granularity如果清除，Segment Limit单位为bytes最大能够表示 2^20 bytes如果设置，表示以4KB(页大小)为单位，最大可表示 2^32 bytesD=Default operand size如果清除，则为16位代码段如果设置，则为32位B=Big如果设置，则数据段的最大偏移量增加到32位0xffffffff否则为最大16位0x0000ffff，与 D 基本相同L=Long如果设置，则为64位段（D 必须为零），并且该段中的代码使用64位指令编码不能将 L 与 D 或 B 同时设置AVL=Available仅供软件使用，硬件不做处理P=Present如果清除，则对此段的任何引用都会生成“段不存在”异常DPL=Descriptor privilege level访问此描述符所需的特权级别Type如果设置，则为代码段描述符。如果清除，则为数据/堆栈段描述符C=Conforming可以从特权级别较低的级别调用此段中的代码E=Expand-Down如果清除，则该段将从基地址扩展到基数+限制如果设置，它将从最大偏移向下扩展到限制，这是通常用于堆栈的行为R=Readable如果清除，则该段可以执行但不能读取W=Writable如果清除，则可以读取但不能写入数据段A=Accessed当访问该段时，该位由硬件设置为1，并由软件清除由描述符的数据结构可以看出，它包含了段信息以及权限等信息，在程序将Selector加载到段寄存器中时，CPU不仅加载段的基地址，而且加载保护信息，根据这些信息对段内存进行保护，所以称之为保护模式(Protected Mode)。未完待续...