<?xml version="1.0" encoding="utf-8" standalone="yes"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom">
  <channel>
    <title>Java on 元闰子的邀请</title>
    <link>https://yrunz.com/tags/java/</link>
    <description>Recent content in Java on 元闰子的邀请</description>
    <generator>Hugo -- gohugo.io</generator>
    <language>en-us</language>
    <lastBuildDate>Sat, 26 Feb 2022 00:00:00 +0000</lastBuildDate><atom:link href="https://yrunz.com/tags/java/index.xml" rel="self" type="application/rss+xml" />
    <item>
      <title>【Java实现】实践GoF的23种设计模式：SOLID原则</title>
      <link>https://yrunz.com/p/java%E5%AE%9E%E7%8E%B0%E5%AE%9E%E8%B7%B5gof%E7%9A%8423%E7%A7%8D%E8%AE%BE%E8%AE%A1%E6%A8%A1%E5%BC%8Fsolid%E5%8E%9F%E5%88%99/</link>
      <pubDate>Sat, 26 Feb 2022 00:00:00 +0000</pubDate>
      
      <guid>https://yrunz.com/p/java%E5%AE%9E%E7%8E%B0%E5%AE%9E%E8%B7%B5gof%E7%9A%8423%E7%A7%8D%E8%AE%BE%E8%AE%A1%E6%A8%A1%E5%BC%8Fsolid%E5%8E%9F%E5%88%99/</guid>
      <description>之前也有写过关于设计模式的文章《使用Go实现GoF的23种设计模式》，但是那个系列写了3篇文章就没在继续了，主要的原因是找不到合适的示例代码。考虑到，如果以类似于“鸭子是否会飞”、“烘培的制作流程”等贴近生活的事情举例，很难在我们日常的开发中产生联系。（目前应该很少有这些逻辑的软件系统吧）
《实践GoF的23种设计模式》可以看成是《使用Go实现GoF的23种设计模式》系列的重启，吸取了上次烂尾的教训，本次在写文章之前就已经完成了23种设计模式的示例代码实现。和上次不同，本次示例代码使用Java实现，以我们日常开发中经常碰到的一些技术/问题/场景作为切入点，示范如何运用设计模式来完成相关的实现。
 前言 从1995年GoF提出23种设计模式到现在，25年过去了，设计模式依旧是软件领域的热门话题。设计模式通常被定义为：
 设计模式（Design Pattern）是一套被反复使用、多数人知晓的、经过分类编目的、代码设计经验的总结，使用设计模式是为了可重用代码、让代码更容易被他人理解并且保证代码可靠性。
 从定义上看，设计模式其实是一种经验的总结，是针对特定问题的简洁而优雅的解决方案。既然是经验总结，那么学习设计模式最直接的好处就在于可以站在巨人的肩膀上解决软件开发过程中的一些特定问题。
学习设计模式的最高境界是吃透它们本质思想，可以做到即使已经忘掉某个设计模式的名称和结构，也能在解决特定问题时信手拈来。设计模式背后的本质思想，就是我们熟知的SOLID原则。如果把设计模式类比为武侠世界里的武功招式，那么SOLID原则就是内功内力。通常来说，先把内功练好，再来学习招式，会达到事半功倍的效果。因此，在介绍设计模式之前，很有必要先介绍一下SOLID原则。
本文首先会介绍本系列文章中用到的示例代码demo的整体结构，然后开始逐一介绍SOLID原则，也即单一职责原则、开闭原则、里氏替换原则、接口隔离原则和依赖倒置原则。
一个简单的分布式应用系统  本系列示例代码demo获取地址：https://github.com/ruanrunxue/Practice-Design-Pattern&amp;ndash;Java-Implementation
 示例代码demo工程实现了一个简单的分布式应用系统（单机版），该系统主要由以下几个模块组成：
 网络 Network，网络功能模块，模拟实现了报文转发、socket通信、http通信等功能。 数据库 Db，数据库功能模块，模拟实现了表、事务、dsl等功能。 消息队列 Mq，消息队列模块，模拟实现了基于topic的生产者/消费者的消息队列。 监控系统 Monitor，监控系统模块，模拟实现了服务日志的收集、分析、存储等功能。 边车 Sidecar，边车模块，模拟对网络报文进行拦截，实现access log上报、消息流控等功能。 服务 Service，运行服务，当前模拟实现了服务注册中心、在线商城服务集群、服务消息中介等服务。  示例代码demo工程的主要目录结构如下：
├── db # 数据库模块，定义Db、Table、TableVisitor等抽象接口 【@单例模式】 │ ├── cache # 数据库缓存代理，为Db新增缓存功能 【@代理模式】 │ ├── console # 数据库控制台实现，支持dsl语句查询和结果显示 【@适配器模式】 │ ├── dsl # 实现数据库dsl语句查询能力，当前只支持select语句查询 【@解释器模式】 │ ├── exception # 数据库模块相关异常定义 │ ├── iterator # 遍历表迭代器，包含按序遍历和随机遍历 【@迭代器模式】 │ └── transaction # 实现数据库的事务功能，包括执行、提交、回滚等 【@命令模式】【@备忘录模式】 ├── monitor # 监控系统模块，采用插件式的架构风格，当前实现access log日志etl功能 │ ├── config # 监控系统插件配置模块 【@抽象工厂模式】【@组合模式】 │ │ ├── json # 实现基于json格式文件的配置加载功能 │ │ └── yaml # 实现基于yaml格式文件的配置加载功能 │ ├── entity # 监控系统实体对象定义 │ ├── exception # 监控系统相关异常 │ ├── filter # Filter插件的实现定义 【@责任链模式】 │ ├── input # Input插件的实现定义 【@策略模式】 │ ├── output # Output插件的实现定义 │ ├── pipeline # Pipeline插件的实现定义，一个pipeline表示一个ETL处理流程 【@桥接模式】 │ ├── plugin # 插件抽象接口定义 │ └── schema # 监控系统相关的数据表定义  ├── mq # 消息队列模块 ├── network # 网络模块，模拟网络通信，定义了socket、packet等通用类型/接口 【@观察者模式】 │ └── http # 模拟实现了http通信等服务端、客户端能力 ├── service # 服务模块，定义了服务的基本接口 │ ├── mediator # 服务消息中介，作为服务通信的中转方，实现了服务发现，消息转发的能力 【@中介者模式】 │ ├── registry # 服务注册中心，提供服务注册、去注册、更新、 发现、订阅、去订阅、通知等功能 │ │ ├── entity # 服务注册/发现相关的实体定义 【@原型模式】【@建造者模式】 │ │ └── schema # 服务注册中心相关的数据表定义 【@访问者模式】【@享元模式】 │ └── shopping # 模拟在线商城服务群的定义，包含订单服务、库存服务、支付服务、发货服务 【@外观模式】 └── sidecar # 边车模块，对socket进行拦截，提供http access log、流控功能 【@装饰者模式】【@工厂模式】 └── flowctrl # 流控模块，基于消息速率进行随机流控 【@模板方法模式】【@状态模式】 SRP：单一职责原则 单一职责原则（The Single Responsibility Principle，SRP）应该是SOLID原则中，最容易被理解的一个，但同时也是最容易被误解的一个。很多人会把“将大函数重构成一个个职责单一的小函数”这一重构手法等价为SRP，这是不对的，小函数固然体现了职责单一，但这并不是SRP。</description>
    </item>
    
    <item>
      <title>从Hash索引到LSM树（二）</title>
      <link>https://yrunz.com/p/%E4%BB%8Ehash%E7%B4%A2%E5%BC%95%E5%88%B0lsm%E6%A0%91%E4%BA%8C/</link>
      <pubDate>Tue, 28 Jul 2020 00:00:00 +0000</pubDate>
      
      <guid>https://yrunz.com/p/%E4%BB%8Ehash%E7%B4%A2%E5%BC%95%E5%88%B0lsm%E6%A0%91%E4%BA%8C/</guid>
      <description>前言 在上一篇文章《从Hash索引到LSM树（一）》中，我们通过append-only log的数据结构，实现了一个具备高写入性能的key-value数据库。append-only log之所以有很高的写入性能，主要得益于磁盘的顺序写入。这可能违反了我们对磁盘的认知，因为在我们的印象中，写磁盘总是很慢。其实不然，准确地说应该是随机写磁盘很慢，因为在写之前可能会进行多次寻址。如果只是顺序写磁盘，性能是非常的高，如下的一个ACM报告中显示，顺序写磁盘甚至比随机写内存的性能还要高！
 举个例子，Kafka是一个高性能的消息队列，它的厉害之处就在于极致地利用磁盘的顺序写入性能，如果生产者和消费者的速率相当，消息甚至可以在操作系统的Page Cache层面就完成了传递。所以，以后别再认为写磁盘很慢了！
 append-only log大幅提升了数据写入性能，但是随之而来的是，非常低的数据读取性能。针对这一点，我们采用Hash索引进行了优化，优化的效果也非常的显著。然而，Hash索引有两个明显的限制：（1）当key的数量很多时，维护Hash索引会给内存带来很大的压力；（2）区间查询很低效。如何对这两个限制进行优化呢？这就轮到本文介绍的主角，LSM树，出场了。
LSM树（Log-Structured Merge Tree）并不是一种数据结构，准确来说是一种存储模型，由MemTable、Immutable MemTable、SSTable等部分组成。它也是利用了append-only log的优势，大幅提升了写入性能。同时，因为key的存储有序性，所以具备了不错的读取性能，也克服了上文所述Hash索引的两个限制。下面，本文将一步步分析LSM树是如何做到这一点的。
SSTable 在最简单的数据库例子中，因为数据是无序存储的，所以在读取某个key的值时，就需要遍历整个数据文件，算法复杂度是O(n)。为了提升读性能，我们不得不在内存中维护所有key的Hash索引。
假如存储数据时，对记录按照key进行排序的会怎样？
对于key有序存储这种情况，即使不用Hash索引，也能得到很好的查询效率，因为我们可以使用二分查找法（Binary Search）来快速找到key所在的位置，算法复杂度是O(logn)。LSM树正是采用key有序这种方式来组织数据存储的，并称之为SSTable。
SSTable（Sorted String Table）是LSM树最基础的一个存储结构，存储在磁盘中，并且数据按照key进行排序的。数据保持key有序的好处是可以在O(logn)的时间下，快速找到一个key值，相比于纯粹的append-only log有了很大的提升。但是，如果所有的数据都存储在一个SSTable上，数据量一大，查询效率也会下降。因此，LSM树通常会将数据分散存储在多个SSTable中，并且记录每个SSTable的最大key和最小key，这样就能快速定位到一个key存储在哪个SSTable上了。
// SSTable，数据保存到SSTable后只读不写 public class SSTable { ... // 数据存储路径  private final LogFile logFile; // 该SStable中存储的最小Key  private String minKey; // 该SStable中存储的最大Key  private String maxKey; // 使用二分查找法获取key值  public String get(String key) { // step1：先判断是否在SSTable的范围内  if (key.compareTo(minKey) &amp;lt; 0 || key.compareTo(maxKey) &amp;gt; 0) { return &amp;#34;&amp;#34;; } // step2：二分查找  long start = 0; long end = logFile.</description>
    </item>
    
    <item>
      <title>从Hash索引到LSM树（一）</title>
      <link>https://yrunz.com/p/%E4%BB%8Ehash%E7%B4%A2%E5%BC%95%E5%88%B0lsm%E6%A0%91%E4%B8%80/</link>
      <pubDate>Fri, 26 Jun 2020 00:00:00 +0000</pubDate>
      
      <guid>https://yrunz.com/p/%E4%BB%8Ehash%E7%B4%A2%E5%BC%95%E5%88%B0lsm%E6%A0%91%E4%B8%80/</guid>
      <description>前言 数据库算是软件应用系统中最常用的一类组件了，不管是一个庞大而复杂的电商系统，还是一个简单的个人博客，多多少少都会用到数据库，或是存储海量的数据，或是存储简单的状态信息。一般地，我们都喜欢将数据库划分为关系型数据库和非关系型数据库（又称NoSQL数据库），前者的典型代表是MySQL数据库，后者的典型代表是HBase数据库。不管是关系型，还是非关系型，数据库都离不开两个最基本的功能：（1）数据存储；（2）数据查询。简单来说就是，当你把数据丢给数据库时，它能够保持下来，并在稍后你想获取的时候，把数据返回给你。
围绕着这两个基本功能，各类数据库都运用了很多技术手段对其进行了优化，其中最广为人知的当属数据库索引技术。索引是一种数据结构，它在牺牲少量数据存储（写）性能的情况下，可以大幅提升数据查询（读）性能。索引也有很多种类型，Hash索引算是最简单高效的一种了，但是由于它自身的限制，在数据库系统中并不被广泛使用。当今最常用的索引技术是B/B+树索引，被广泛地应用在关系型数据库中，主要应用于读多写少的场景。随着NoSQL数据库的兴起，LSM（Log-Structured Merged-Tree）树也逐渐流行，并被Google的BigTable论文所发扬光大。严格来说，LSM树并不算一种传统意义上的索引，它更像是一种设计思想，主要应用于写多读少的场景。
本系列文章，将从实现最简单的Key-Value数据库讲起，然后针对实现过程中遇到的一些瓶颈，采用上述的索引技术，对数据库进行优化，以此达到对数据库的索引技术有一个较为深刻的理解。
最简单的数据库 Martin Kleppmann在《Designing Data-Intensive Applications》一书中给出了一个最简单数据库的实现：
#!/bin/bash db_set() { echo &amp;#34;$1,$2&amp;#34; &amp;gt;&amp;gt; database } db_get() { grep &amp;#34;^$1,&amp;#34; database | sed -e &amp;#34;s/^$1,//&amp;#34; | tail -n 1 } 这不到10行的shell代码实现了一个简单的Key-Value数据库。它一共有两个函数，db_set和db_get，前者对应数据存储功能，后者对应数据查询功能。该数据库采用简单的文本格式（database文件）进行数据存储，每条记录包含了一个键值对，key和value之间通过逗号（,）进行分隔。
数据库的使用方法也很简单，通过调用db_set key value可以将key及其对应的value存储到数据库中；通过db_get key可以得到该key对应的value：
$ db_set 123456 &amp;#39;{&amp;#34;name&amp;#34;:&amp;#34;London&amp;#34;,&amp;#34;attractions&amp;#34;:[&amp;#34;Big Ben&amp;#34;,&amp;#34;London Eye&amp;#34;]}&amp;#39; $ db_set 42 &amp;#39;{&amp;#34;name&amp;#34;:&amp;#34;San Francisco&amp;#34;,&amp;#34;attractions&amp;#34;:[&amp;#34;Golden Gate Bridge&amp;#34;]}&amp;#39; $ db_get 42 {&amp;#34;name&amp;#34;:&amp;#34;San Francisco&amp;#34;,&amp;#34;attractions&amp;#34;:[&amp;#34;Golden Gate Bridge&amp;#34;]} 透过db_set的实现我们发现，该数据库每次写都是直接往database文件中追加记录，鉴于文件系统顺序写的高效，因此该数据库的数据写入具有较高的性能。但是追加写也意味着，对同一个key进行更新时，其对应的旧的value并不会被覆盖，这也使得每次调用db_get获取某个key的value时，总是需要遍历所有记录，找到所有符合条件的value，并取其中最新的一个。因此，该数据库的读性能是非常低的。
下面我们采用Java对这个最简单的数据库进行重写：
/** * SimpleKvDb.java * 追加写 * 全文件扫描读 */ public class SimpleKvDb implements KvDb { .</description>
    </item>
    
    <item>
      <title>Java的对象模型——Oop-Klass模型（二）</title>
      <link>https://yrunz.com/p/java%E7%9A%84%E5%AF%B9%E8%B1%A1%E6%A8%A1%E5%9E%8Boop-klass%E6%A8%A1%E5%9E%8B%E4%BA%8C/</link>
      <pubDate>Sun, 02 Feb 2020 00:00:00 +0000</pubDate>
      
      <guid>https://yrunz.com/p/java%E7%9A%84%E5%AF%B9%E8%B1%A1%E6%A8%A1%E5%9E%8Boop-klass%E6%A8%A1%E5%9E%8B%E4%BA%8C/</guid>
      <description>前言 在《Java对象表示——Oop-Klass模型（一）》一文的最后讲到，为了实现Java方法调用的动态绑定，HotSpot使用了与C++虚函数类似的机制，同时为了避免每个对象都维护一个虚函数表，于是就设计了Klass类。
如下为HotSpot源码中对Klass的功能介绍：
 A Klass provides:
​ 1: language level class object (method dictionary etc.)
​ 2: provide vm dispatch behavior for the object
Both functions are combined into one C++ class.
 可见，Klass主要提供了两个功能：
（1）用于表示Java类。Klass中保存了一个Java对象的类型信息，包括类名、限定符、常量池、方法字典等。一个class文件被JVM加载之后，就会被解析成一个Klass对象存储在内存中。
（2）实现对象的虚分派（virtual dispatch）。所谓的虚分派，是JVM用来实现多态的一种机制。
class A { void callMe() { System.out.println(&amp;#34;This is A.&amp;#34;); } } class B extends A { @Override public void callMe() { System.out.println(&amp;#34;This is B.&amp;#34;); } } class C extends A { @Override public void callMe() { System.</description>
    </item>
    
    <item>
      <title>Java的对象模型——Oop-Klass模型（一）</title>
      <link>https://yrunz.com/p/java%E7%9A%84%E5%AF%B9%E8%B1%A1%E6%A8%A1%E5%9E%8Boop-klass%E6%A8%A1%E5%9E%8B%E4%B8%80/</link>
      <pubDate>Fri, 31 Jan 2020 00:00:00 +0000</pubDate>
      
      <guid>https://yrunz.com/p/java%E7%9A%84%E5%AF%B9%E8%B1%A1%E6%A8%A1%E5%9E%8Boop-klass%E6%A8%A1%E5%9E%8B%E4%B8%80/</guid>
      <description>前言 谈起Java对象，笔者的第一反应是在：Java中的每一个对象（不包括基础类型）都继承于Object对象。相信这也是大多数程序员对Java对象的初次印象，Object可以表示所有的Java对象。但是，这种理解仅仅是停留在语言层面，至于更深的JVM层面，对象还是用Object来表示吗？显然不是。JVM通常使用非Java语言实现，是用来解析并运行Java程序的，它有自己的模型来表示Java语言的各种特性，包括Object。下面我们以HotSpot为例，一起来探讨Java对象在JVM层面的Java对象模型。
 HotSpot采用C++语言实现，下文中的JVM如无特殊说明，指的都是HotSpot。
 Java程序通过new操作符来创建一个对象，在深入探讨HotSpot的Java对象模型前，我们先看下new操作符的具体实现。
// hotspot/src/share/vm/interpreter/interpreterRuntime.cpp ... // HotSpot中new操作符的实现函数 IRT_ENTRY(void, InterpreterRuntime::_new(JavaThread* thread, ConstantPool* pool, int index)) Klass* k_oop = pool-&amp;gt;klass_at(index, CHECK); instanceKlassHandle klass (THREAD, k_oop); // Make sure we are not instantiating an abstract klass  klass-&amp;gt;check_valid_for_instantiation(true, CHECK); // Make sure klass is initialized  klass-&amp;gt;initialize(CHECK); // At this point the class may not be fully initialized  // ...  oop obj = klass-&amp;gt;allocate_instance(CHECK); thread-&amp;gt;set_vm_result(obj); IRT_END ... 上述代码片段来自HotSpot源码中new操作符的实现函数，先不深入分析每一行的具体含义，这段代码给我们最直观的功能就是：先对klass对象进行初始化工作，然后再用它来创建出oop对象。到这里我们大致就能猜出，oop表示的就是一个Java对象。而这里的klass和Java中的Class之间似乎有着紧密的联系，一是两者的名字非常类似，另外也可通过第16行代码得到进一步的肯定。对Java的反射机制稍微有所了解的人，看着第16行代码一定很熟悉，因为它与使用Class.</description>
    </item>
    
    <item>
      <title>彻底弄懂Java的移位操作符</title>
      <link>https://yrunz.com/p/%E5%BD%BB%E5%BA%95%E5%BC%84%E6%87%82java%E7%9A%84%E7%A7%BB%E4%BD%8D%E6%93%8D%E4%BD%9C%E7%AC%A6/</link>
      <pubDate>Sun, 22 Dec 2019 00:00:00 +0000</pubDate>
      
      <guid>https://yrunz.com/p/%E5%BD%BB%E5%BA%95%E5%BC%84%E6%87%82java%E7%9A%84%E7%A7%BB%E4%BD%8D%E6%93%8D%E4%BD%9C%E7%AC%A6/</guid>
      <description>前言 对于移位操作符，很多人既感到熟悉，又感到陌生。熟悉是因为移位操作符是最基本的操作符之一，几乎每种编程语言都包含这一操作符；陌生是因为除非是追求极致性能等罕见场景，否则也很难用得上它。打开JDK源码，你会发现移位操作符的身影极为常见，弄清楚它的用法，对阅读源码很有帮助。
移位操作是把数据看作是二进制数，然后将其向左或向右移动若干位的运算。在Java编程语言中，移位操作符包含三种，分别是 &amp;lt;&amp;lt;（左移）、 &amp;gt;&amp;gt;（带符号右移）和 &amp;gt;&amp;gt;&amp;gt;（无符号右移），这三种操作符都只能作用于long、int、short、byte、char这四种基本的整型类型上。
左移操作符 &amp;laquo; 左移操作符 &amp;lt;&amp;lt; 是将数据转换成二进制数后，向左移若干位，高位丢弃，低位补零。看如下例子：
public static void main(String[] args) { int i = -1; System.out.println(&amp;#34;Before &amp;lt;&amp;lt; , i&amp;#39;s value is &amp;#34; + i); System.out.println(&amp;#34;i&amp;#39;s binary string is &amp;#34; + Integer.toBinaryString(i)); i &amp;lt;&amp;lt;= 10; System.out.println(&amp;#34;After &amp;lt;&amp;lt; , i&amp;#39;s value is &amp;#34; + i); System.out.println(&amp;#34;i&amp;#39;s binary string is &amp;#34; + Integer.toBinaryString(i)); } Java的int占32位，因此对i = -1转换成二进制数，然后左移10位，其结果是左边高10位丢弃，右边低10位补0，再转换为十进制，得到i = -1024的结果。
因此，上述例子的输出结果为：
Before &amp;lt;&amp;lt; , i&#39;s value is -1 i&#39;s binary string is 11111111111111111111111111111111 After &amp;lt;&amp;lt; , i&#39;s value is -1024 i&#39;s binary string is 11111111111111111111110000000000 带符号右移操作符 &amp;raquo; 众所周知，Java中整型表示负数时，最高位为符号位，正数为0，负数为1。&amp;gt;&amp;gt; 是带符号的右移操作符，将数据转换成二进制数后，向右移若干位，高位补符号位，低位丢弃。对于正数作右移操作时，具体体现为高位补0；负数则补1。看如下例子：</description>
    </item>
    
  </channel>
</rss>
