SegmentFault java 工具最新的文章
2024-01-30T00:06:52+08:00
https://segmentfault.com/feeds/blogs
https://creativecommons.org/licenses/by-nc-nd/4.0/
sensitive-word-admin 敏感词控台 v1.2.0 版本开源
https://segmentfault.com/a/1190000044599929
2024-01-30T00:06:52+08:00
2024-01-30T00:06:52+08:00
老马啸西风
https://segmentfault.com/u/houbinbin
0
<h2>开源目的</h2><p>丰富 <a href="https://link.segmentfault.com/?enc=%2FPu1aG1%2FvTI2VnEblx2qSw%3D%3D.KhVlHLeEvqC4Ifj1%2BoHvXconuqtNzTYHd3t1TADVSgV7VGbBAQs3%2Frt0Y%2FoQZo0o" rel="nofollow">sensitive-word</a> 的生态。</p><p>提供最基本的操作例子,便于在此基础上丰富实现自己的敏感词控台。</p><p>避免重复开发的成本,让更多的精力专注于业务。</p><h3>拓展阅读</h3><blockquote><p><a href="https://link.segmentfault.com/?enc=REDPKp5JihnrBVbB2tkDAA%3D%3D.NI6Bilqb%2BIu5OCMM1OTnizeGZUjLmxufdzJX9gYkII8A9n5o7n9ERs5Az5cF800SKgN0hE5IyjlC6vuVdsjpaQ%3D%3D" rel="nofollow">sensitive-word-admin 敏感词控台 v1.2.0 版本开源</a></p><p><a href="https://link.segmentfault.com/?enc=0JJ8Y0Y%2Bc6%2FvRn6SzS06%2Bg%3D%3D.fOC1jdqfXH8SUBHoczYOe2KFTYsOYUTlRLpcpnLydzrHKs3zhJCVEbKPpZQgAg3TNJHSOClK6m9BsBOMWZV%2F3Q%3D%3D" rel="nofollow">sensitive-word 基于 DFA 算法实现的高性能敏感词工具介绍</a></p></blockquote><p><img src="/img/remote/1460000044599931" alt="view" title="view"></p><h2>v1.2.0 版本特性</h2><p>优化对应的组件依赖,<a href="https://link.segmentfault.com/?enc=svdKXIZCFfIe9mvsvD3S1Q%3D%3D.XVkY8sgrVYxFYpbwHlOwzj0IyerQY2I9UQdxwhfNgGBTLM3Ig0AUag4t%2BoVEYRRx" rel="nofollow">sensitive-word</a> 同步升级到最新版本。</p><p>修复已知问题。</p><p>模板已包含敏感词基础的操作,后续将持续优化。</p><h2>核心 api</h2><p>提供了核心的 api,可以让用户自行调用。在这个基础上封装自己的脱敏服务。</p><h3>接口列表</h3><table><thead><tr><th align="left">api</th><th align="left">入参</th><th align="left">出参</th><th align="left">说明</th></tr></thead><tbody><tr><td align="left">/api/sensitiveWord/contains</td><td align="left">string</td><td align="left">boolean</td><td align="left">是否包含敏感词</td></tr><tr><td align="left">/api/sensitiveWord/findAll</td><td align="left">string</td><td align="left"><code>List<String></code></td><td align="left">获取所有的敏感词</td></tr><tr><td align="left">/api/sensitiveWord/findFist</td><td align="left">string</td><td align="left">string</td><td align="left">获取第一个的敏感词</td></tr><tr><td align="left">/api/sensitiveWord/replace</td><td align="left">string</td><td align="left">string</td><td align="left">获取替换后的结果</td></tr><tr><td align="left">/api/sensitiveWord/tags</td><td align="left">string</td><td align="left"><code>Set<String></code></td><td align="left">获取敏感词的标签列表</td></tr></tbody></table><h2>核心实现</h2><h3>spring 配置</h3><pre><code class="java">@Configuration
public class SensitiveWordConfig {
@Autowired
private MyDdWordAllow myDdWordAllow;
@Autowired
private MyDdWordDeny myDdWordDeny;
/**
* 初始化引导类
* @return 初始化引导类
* @since 1.0.0
*/
@Bean
public SensitiveWordBs sensitiveWordBs() {
return SensitiveWordBs.newInstance()
.wordAllow(WordAllows.chains(WordAllows.defaults(), myDdWordAllow))
.wordDeny(WordDenys.chains(WordDenys.defaults(), myDdWordDeny))
.ignoreRepeat(false)
// 各种其他配置
.init();
}
}</code></pre><p>最核心的代码部分,引入初默认配置之外的数据库的 MyDdWordAllow + MyDdWordDeny,通过数据库层面的</p><h3>refresh</h3><p>每次敏感词变更,会触发对应的敏感词 refresh 操作。</p><p>所以不用重启,就可以实现敏感词的实时刷新。</p><p>后续会做进一步的优化,让敏感词精确到单个词,刷新性能更好。</p><h2>开源地址</h2><blockquote><a href="https://link.segmentfault.com/?enc=1Ph6JB7ddtSerGfqFE7JEg%3D%3D.vROh5RA0qx%2FW6DFZ8YbeJIDzrtNK%2Bl%2FMB9Slia3tdF%2BEtockBg8akPbfR2vHPdVR" rel="nofollow">https://github.com/houbb/sensitive-word-admin</a></blockquote><h2>后续 ROAD-MAP</h2><ul><li>[ ] 登录/登出</li><li>[ ] 页面操作的权限管理</li><li>[ ] 调用方系统 token 注册管理</li><li>[ ] 敏感词的数据大盘</li><li>[ ] 调用信息数据大盘</li><li>[ ] 操作审计日志</li></ul><h2>参考资料</h2><p><a href="https://link.segmentfault.com/?enc=JH7eQgYoaiVXZA1pyCYPwg%3D%3D.QfljNNptNF8%2BygHHarLITvCjpvIeUwD52fOPZZb%2Fc2YPCjm2et4WOj0K53W395sj" rel="nofollow">https://github.com/houbb/sensitive-word-admin</a></p>
logstash4j-用于日志的输入、转换处理、输出, java 开发者自己的 logstash
https://segmentfault.com/a/1190000044558061
2024-01-16T11:36:32+08:00
2024-01-16T11:36:32+08:00
老马啸西风
https://segmentfault.com/u/houbinbin
0
<h2>项目简介</h2><p>用于日志的输入、转换处理、输出, java 开发者自己的 logstash</p><h3>特性</h3><ul><li>input</li><li>output</li><li>filter</li><li>metric</li></ul><h2>开源地址</h2><blockquote><a href="https://link.segmentfault.com/?enc=29AKnF8Qvf%2BzL4KuT%2B8R8A%3D%3D.3dy790tIV70qzQ00LT%2FA7mpc%2BxFFejnj2%2BmNZEyrXjsUgOp%2FpELstMGkK%2FysA10R" rel="nofollow">logstash4j</a></blockquote><h2>快速开始</h2><h3>需要</h3><p>jdk1.8+</p><p>maven 3.x+</p><h3>maven 引入</h3><pre><code class="xml"><dependency>
<groupId>com.github.houbb</groupId>
<artifactId>logstash4j-core</artifactId>
<version>0.9.0</version>
</dependency></code></pre><h3>例子</h3><h4>启动程序</h4><pre><code class="java">import com.github.houbb.logstash4j.core.api.ILogstashConfigClassName;
import com.github.houbb.logstash4j.core.api.ILogstashManageConfig;
import com.github.houbb.logstash4j.core.bs.LogstashManageCoreBs;
import com.github.houbb.logstash4j.core.support.config.LogstashConfigClassName;
import java.util.*;
public class LogstashManageCoreBsTest {
public static void main(String[] args) {
ILogstashManageConfig config = new ILogstashManageConfig() {
@Override
public List<ILogstashConfigClassName> getInputConfigList() {
List<ILogstashConfigClassName> list = new ArrayList<>();
LogstashConfigClassName logstashConfigClassName = new LogstashConfigClassName(new HashMap<String, Object>());
logstashConfigClassName.setClassName("com.github.houbb.logstash4j.plugins.input.StdIn");
list.add(logstashConfigClassName);
return list;
}
@Override
public List<ILogstashConfigClassName> getFilterConfigList() {
//1. add
List<ILogstashConfigClassName> list = new ArrayList<>();
Map<String, Object> configMap = new HashMap<>();
configMap.put("addField1", "value1");
configMap.put("addField2", "value2");
LogstashConfigClassName logstashConfigClassName = new LogstashConfigClassName(configMap);
logstashConfigClassName.setClassName("com.github.houbb.logstash4j.plugins.filter.Add");
list.add(logstashConfigClassName);
return list;
}
@Override
public List<ILogstashConfigClassName> getOutConfigList() {
List<ILogstashConfigClassName> list = new ArrayList<>();
LogstashConfigClassName logstashConfigClassName = new LogstashConfigClassName(new HashMap<String, Object>());
logstashConfigClassName.setClassName("com.github.houbb.logstash4j.plugins.output.StdOut");
list.add(logstashConfigClassName);
return list;
}
};
//run
LogstashManageCoreBs.newInstance()
.manageConfig(config)
.init()
.run();
}
}</code></pre><h4>测试日志</h4><pre><code>11111111111
Stdout: {addField2=value2, addField1=value1, message=11111111111}
3333333333333333
Stdout: {addField2=value2, addField1=value1, message=3333333333333333}</code></pre><h3>yaml 例子</h3><h4>yaml 配置</h4><ul><li>gen_test.yaml</li></ul><pre><code class="yaml">---
outputs:
- StdOut: {}
inputs:
- StdIn: {}
filters:
- Add:
addField2: "value2"
addField1: "value1"</code></pre><h4>java 代码</h4><pre><code class="java">ILogstashManageConfig config = new YamlLogstashManageConfig("gen_test.yaml");
LogstashManageCoreBs.newInstance()
.manageConfig(config)
.init()
.run();</code></pre><p>效果和上面类似,只是换了一种配置方式。</p><h2>ROAD-MAP</h2><p>满足下面的话,可以让组件更加的强大。</p><ul><li>[ ] 如何可以让不同的 event 进行关联?</li><li>[ ] 如何让 input/filter/ouput 之间也可以关联?</li></ul><h2>日志矩阵</h2><p><a href="https://link.segmentfault.com/?enc=oZ8DKDAZj%2F7jnuYFWyqaJA%3D%3D.HTkLb%2BpMJDhbYnC%2F29T%2FX3iKq9XJapWheT%2BMxL93ojDmyRuYjNd2sj3hmudKt2c5" rel="nofollow">日志采集-logcollect-go</a></p><p><a href="https://link.segmentfault.com/?enc=WJDNTLK5Mx3LSThq%2BTs4RQ%3D%3D.VHeMtn7RKtlJuwUCWkaMmwgJyU02mn0SSglMB3%2Fbne7Q%2FO0gC3rkl28qwAWIelVu" rel="nofollow">日志处理-logstash4j</a></p><p><a href="https://link.segmentfault.com/?enc=aWtpnBFBnNMvlJQ6k4tIAg%3D%3D.BxnuJsXxm8WaCkt%2BC3CdLRB97Qsluj%2FaATOQFlmMv125sVnM1cbaKExoNu3BHfPo" rel="nofollow">日志处理插件-logstash4j-plugins</a></p><p><a href="https://link.segmentfault.com/?enc=O5rMlGbP5hzTPaebaHrdOg%3D%3D.zeDeKqZZS5RVcq%2B7RrkE71E9UnNSp1Z7H2ivel96MHdg8aY0%2BGFuqrfFiFsz%2FXuu" rel="nofollow">日志整合框架-log-integration</a></p><p><a href="https://link.segmentfault.com/?enc=LCoRlCMFqcCJZuQQimgyDg%3D%3D.7RLaFuH%2FYQ%2BYT2zmYKNcDlpLNwJZo0%2FdNbIU1mjyxD0rI%2FcZnkEB6UeVQFaDhmCs" rel="nofollow">切面日志-auto-log</a></p><p><a href="https://link.segmentfault.com/?enc=y2N4o2E9FG232krLGXMW2Q%3D%3D.lk9293wdiQ0qiPUaq8BC%2FYo6DVANyJ9dQcYjBh%2FftzaHYh49j8vmKHIXsZ%2Fp7YHY" rel="nofollow">日志脱敏-sensitive</a></p><h3>日志的全链路</h3><p><a href="https://link.segmentfault.com/?enc=ICWDWQesIb9L2qfVXjxWew%3D%3D.QxXugpa0xw%2BT3jPzio7SGqicdamgLSHT8sB8uIS77GlVuMdQXqr9q8Ev9LjOEZKa" rel="nofollow">全链路 auto-trace</a></p><p><a href="https://link.segmentfault.com/?enc=p46dHLKsEKv9DTZyhIhuww%3D%3D.ZamNYLAaDUHUxsrBjdaH%2F0kAKUPfM0ZXN32Xl8aLmwVC%2BRp71FH4K8c9Y3hfBCT1" rel="nofollow">唯一的跟踪号-trace-id</a></p><p>[包内链路信息]()</p><p>[应用间链路]()</p><h3>日志检索</h3><p><a href="https://link.segmentfault.com/?enc=VpTLtXKuxyckFQ08D050MA%3D%3D.FM5iMVIzwIz0QjHgyEJ89Fg7V0HWqXXRXF2VXRUP%2FOuVFi3oI9g%2BBqwFMCXEPXbX" rel="nofollow">分词-segment</a></p><p><a href="https://link.segmentfault.com/?enc=C3EvDvdVXdOLDDEactTV8g%3D%3D.joVaowfXcZMNnd1xhJy%2BbM4XCTwF7GWjPNDZpVqwKTU%3D" rel="nofollow">拼音-pinyin</a></p><p><a href="https://link.segmentfault.com/?enc=PG0h34vCpAhfhyU4PkqKFA%3D%3D.%2FvK%2BPkesAHWmXp5qoIwwkn%2FeqiRRKnhh2dvvvhiMX9ZWV81RVeQMS86aTbB%2F1JZG" rel="nofollow">中文繁简体转换-opencc4j</a></p><p>[日志存储+检索 类似ES:TODO]()</p><p>[日志的可视化 类似kibana:TODO]()</p><p>[日志分析-错误+慢日志+报警平台:TODO]()</p>
test-02-java 单元测试框架 junit5 入门介绍
https://segmentfault.com/a/1190000044540542
2024-01-09T22:57:21+08:00
2024-01-09T22:57:21+08:00
老马啸西风
https://segmentfault.com/u/houbinbin
0
<h2>拓展阅读</h2><p><a href="https://link.segmentfault.com/?enc=vsuk7GQuzrQUxzjPKanG3g%3D%3D.WyWDSPPcrt1SN3%2F3iaJbmqVbM2RXng8HGFy9jKxL9U6i4vkOOBEQERw9lkRkS6QxMNt4VFRwsGGmgp6p%2FhPAfg%3D%3D" rel="nofollow">junit5 系列</a></p><p><a href="https://link.segmentfault.com/?enc=xbXNAUluC9BCnVQiIGA4nA%3D%3D.ISOXLs3VoAYeOA8H8mNNjslx7gz%2FoeejnxA8t8UVfxU50HNTh2EcGd5ZWaq66zWUlq5p98PKIJ7ySXlOBqpSbg%3D%3D" rel="nofollow">基于 junit5 实现 junitperf 源码分析</a></p><p><a href="https://link.segmentfault.com/?enc=QASWPJLBUOMhBZx%2FabOmDA%3D%3D.aOVFX6lGMnKqzmqQQa7X8ki%2BQxS1lgfrzAGEWsHbbVSFSoBOxCgaKis9kEwtWNWn" rel="nofollow">Auto generate mock data for java test.(便于 Java 测试自动生成对象信息)</a></p><p><a href="https://link.segmentfault.com/?enc=sWQ8JAXnc1b70IK5bhwSrw%3D%3D.q34rzJEIa68ZjNJasO8%2F3xJLpqnmD38W1WmWcMlOSoObVKviPgkjWbMrTxlgZfxG" rel="nofollow">Junit performance rely on junit5 and jdk8+.(java 性能测试框架。性能测试。压测。测试报告生成。)</a></p><h2>junit5 的入门例子</h2><h3>maven 引入</h3><pre><code class="xml"><dependencies>
<!-- 添加 JUnit 5 依赖项 -->
<dependency>
<groupId>org.junit.jupiter</groupId>
<artifactId>junit-jupiter-api</artifactId>
<version>5.8.2</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.junit.jupiter</groupId>
<artifactId>junit-jupiter-engine</artifactId>
<version>5.8.2</version>
<scope>test</scope>
</dependency>
</dependencies></code></pre><h3>方法</h3><pre><code class="java">public class Calculator {
public int add(int a, int b) {
return a + b;
}
}</code></pre><h3>测试类</h3><pre><code class="java">import org.junit.jupiter.api.Test;
import static org.junit.jupiter.api.Assertions.*;
public class CalculatorTest {
@Test
void testAdd() {
// Arrange
Calculator calculator = new Calculator();
// Act
int result = calculator.add(3, 7);
// Assert
assertEquals(10, result, "3 + 7 should equal 10");
}
}</code></pre><h2>小结</h2><p>junit5 对比 junit4 可以说是划时代的提升。</p><p>这么多年过去了,就算是再顽固的系统,现在也开始使用 jdk1.8 了,这一点令人欣慰。</p><p>junit5 是新时代的单元测试,所以忘掉你的 junit4 吧。</p><p><img width="723" height="305" src="/img/bVcVhsC" alt="" title=""></p>
test-01-java 单元测试框架 junit 入门介绍
https://segmentfault.com/a/1190000044540536
2024-01-09T22:56:15+08:00
2024-01-09T22:56:15+08:00
老马啸西风
https://segmentfault.com/u/houbinbin
0
<h2>拓展阅读</h2><p><a href="https://link.segmentfault.com/?enc=WsxONgakfxdO6B2ki4zytw%3D%3D.qoquTp50APaWKEPpWzqxmDcXcP55hJCh3fP%2F2twO9UTJ3cVfM0tyoUSvYZjQs7r2zXk5xVPkK%2B%2F3qnM4l752BA%3D%3D" rel="nofollow">junit5 系列</a></p><p><a href="https://link.segmentfault.com/?enc=pmQCEpbAfUkSDN7DkDizXA%3D%3D.%2BrNtfVQ99vKy3OEehh3kCJlkZHu1l0%2Bdi8bDIICpnZSvG3zfSmtg8BTP6yaSknyokX4dtukbJp1GtgUPAId1qQ%3D%3D" rel="nofollow">基于 junit5 实现 junitperf 源码分析</a></p><p><a href="https://link.segmentfault.com/?enc=%2BLTRO4pnMQaNPdG9TTWO9g%3D%3D.HF67Cy9U79976RoNZzwn4cxaCekrmydqNPoJFxRwK6EFbmnCV24MlENMvs9gS29Y" rel="nofollow">Auto generate mock data for java test.(便于 Java 测试自动生成对象信息)</a></p><p><a href="https://link.segmentfault.com/?enc=TfT2pY274quMyduX1H%2FpWA%3D%3D.8YsUkgw7quMvXVpAUk9izvxyIJMzShi9H1vUt%2BKPuieFm8vpGBSjw7mjVdJO3VfH" rel="nofollow">Junit performance rely on junit5 and jdk8+.(java 性能测试框架。性能测试。压测。测试报告生成。)</a></p><h2>JUnit</h2><p><a href="https://link.segmentfault.com/?enc=feYNxyeXVoI990bAit3wxw%3D%3D.wt3nUxJKvN4abBeIGqqlzQTZplqkwUb%2Bn7BK3XgfX%2Bs%3D" rel="nofollow">JUnit</a> 是一个用于编写可重复测试的简单框架。</p><p>它是 xUnit 架构的一种实例,专门用于单元测试框架。</p><blockquote>What to test?</blockquote><table><thead><tr><th align="left">Need</th><th align="left">Desc</th></tr></thead><tbody><tr><td align="left">Right</td><td align="left">结果是否正确</td></tr><tr><td align="left">B</td><td align="left">边界条件是否满足</td></tr><tr><td align="left">I</td><td align="left">能反向关联吗</td></tr><tr><td align="left">C</td><td align="left">有其他手段交叉检查吗</td></tr><tr><td align="left">E</td><td align="left">是否可以强制异常发生</td></tr><tr><td align="left">P</td><td align="left">性能问题</td></tr></tbody></table><h2>maven 入门例子</h2><h3>maven 引入</h3><pre><code class="xml"><dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<version>4.13.2</version>
<scope>test</scope>
</dependency></code></pre><h3>方法</h3><pre><code class="java">public class Calculator {
public int add(int a, int b) {
return a + b;
}
}</code></pre><h3>测试方法</h3><pre><code class="java">import org.junit.Test;
import static org.junit.Assert.*;
public class CalculatorTest {
@Test
public void testAdd() {
// Arrange
Calculator calculator = new Calculator();
// Act
int result = calculator.add(3, 7);
// Assert
assertEquals(10, result);
}
}</code></pre><p>CalculatorTest 类包含了一个测试方法 testAdd,用于测试 Calculator 类的 add 方法。</p><p>@Test 注解表示这是一个测试方法。</p><p>在测试方法中,我们首先创建了一个 Calculator 对象,然后调用 add 方法进行加法操作。</p><p>最后,使用 assertEquals 断言来验证计算的结果是否符合预期值。</p><h3>运行测试类</h3><p>在 IDE 中,通常有一个 "Run" 或 "Debug" 按钮,可以直接运行测试类。</p><p>也可以通过</p><pre><code>mvn test</code></pre><p>统一执行测试用例</p><h3>验证结果</h3><p>测试运行后,IDE 会显示测试结果。</p><p>如果测试通过,你将看到一个绿色的标志;如果测试失败,将会显示红色的标志,并且会提供详细的失败信息。</p><h2>我们自己的测试例子</h2><ul><li>我们创建一个用于学生的测试类;</li></ul><pre><code class="java">public class StudentTest extends TestCase {
public void testCreate() {
Student student = new Student("Mike");
}
}</code></pre><ul><li>Student class</li></ul><pre><code class="java">public class Student {
private String name;
public Student(String name) {
this.name = name;
}
public String getName() {
return "ryo";
}
public void setName(String name) {
this.name = name;
}
}</code></pre><p>当我们运行 <code>StudentTest</code> 时,</p><p><img src="/img/remote/1460000044540538" alt="success" title="success"></p><p>接着,我们修改测试代码。</p><pre><code class="java">public class StudentTest extends TestCase {
public void testCreate() {
Student student = new Student("Mike");
String name = student.getName();
assertEquals("Mike", name);
}
}</code></pre><p>result</p><p><img src="/img/remote/1460000044540539" alt="failed" title="failed"></p><h2>Usage</h2><ul><li>Add jars in IDEA</li></ul><pre><code>File --> Project Structure [crtl+alt+shift+s] --> Libraries --> "+"---> "Attach Files or Directories"</code></pre><ul><li>setUp()</li></ul><p>Now we add a new class Course.</p><pre><code class="java">public class Course {
private String name;
private int num;
public Course(String name, int num) {
this.name = name;
this.num = num;
}
public String getName() {
return name;
}
public int getNum() {
return num;
}
}</code></pre><p>test class like this...</p><pre><code class="java">public class CourseTest extends TestCase {
public void testCreateNum() {
Course course = new Course("Math", 1);
assertEquals(1, course.getNum());
}
public void testCreateName() {
Course course = new Course("Math", 1);
assertEquals("Helo", course.getName());
}
}</code></pre><pre><code class="java">Course course = new Course("Math", 1);</code></pre><p>我们已经写了两次,有没有更简单的方法?</p><p>现在,我们可以使用 <code>setUp()</code> 来帮助我们更轻松地进行测试;<code>setUp()</code> 中的内容将在每个测试方法执行<strong>之前</strong>调用。</p><pre><code class="java">public class CourseTest extends TestCase {
private Course course;
public void setUp() {
course = new Course("Math", 1);
}
public void testCreateNum() {
assertEquals(1, course.getNum());
}
public void testCreateName() {
assertEquals("Helo", course.getName());
}
}</code></pre><ul><li><p><code>tearDown()</code></p><p>此外,<code>tearDown()</code> 将在每个测试方法执行<strong>之后</strong>调用。</p></li><li><p><code>@Before</code></p><p>用 <code>@Before</code> 注解的方法在每次测试之前执行;同样,<code>@After</code> 在每次测试之后执行。</p></li><li><p><code>@BeforeClass</code></p><p>只运行一次,并且是唯一的。</p></li></ul><p><img width="723" height="305" src="/img/bVcVhsC" alt="" title=""></p>
让 sdk 包静默升级的 SAO 操作,你见过几种?
https://segmentfault.com/a/1190000044528178
2024-01-04T20:38:34+08:00
2024-01-04T20:38:34+08:00
老马啸西风
https://segmentfault.com/u/houbinbin
0
<h2>拓展阅读</h2><p><a href="https://link.segmentfault.com/?enc=cJ%2Fv4UEs5ZNGEuoYjFS0Lw%3D%3D.vJHjexQ1hx%2FgCJAbPq2LWT2U9xW6dBTv7SE%2B3SHUx2ysKY9L4az1fYbGTK3FOkyo56JkbNLjiIIRcGjOEbo1jA%3D%3D" rel="nofollow">让 sdk 包静默升级的 SAO 操作,你见过几种?</a></p><h2>业务背景</h2><p>有时候为业务方提供了基础的 sdk 包,为了保证稳定性,一般都是 release 包。</p><p>但是每一次升级都非常痛苦,也不可能写一个一步到位的 jar 包,因为业务一直在变化。</p><p>那有什么方式,让 sdk 包静默升级呢?</p><p>今天学习到一个骚操作,和大家分享一下。</p><p><img src="/img/remote/1460000044528180" alt="骚操作" title="骚操作"></p><h2>方式1-snapshot</h2><p>以 java 的 maven 包管理为例,如果使用 snapshot,那么就可以随时方便的升级包内容。</p><h3>优点</h3><p>非常简单,maven 天然支持、</p><h3>缺点</h3><ul><li>包信息不够稳定,一般为了追求生产的稳定性,都会要求去 snapshot。</li><li>升级失败,不兼容等,没有回滚的余地。</li></ul><h3>方式2-nexus 等内部仓库直接替换</h3><p>方式:直接替换指定版本的仓库中的包。这种方式的核心和上面类似。只不过是看起来不是 snapshot,但是依然无法保障安全。</p><h3>方式3-shell 脚本结合 CI</h3><p>方式:使用 shell 脚本,在升级的时候,先备份,再替换。</p><p>流程:一般结合 ci 流水线使用,部署打包的时候替换包版本到指定版本,相比较而言更加灵活,也有回旋的余地。</p><p>缺点:要求比较多,比较麻烦。且要求应用必须通过 ci 流水线部署,否则就会被绕过。</p><h4>注意点</h4><p>1)需要保证 jar 版本之间的向前兼容性,避免升级导致问题。</p><p>2)应用环境比较复杂,可能会出问题。所以一定要逐步的升级,让用户测试环境经过验证。</p><p>3)提前通知用户,让用户知道这个事情。而且允许用户不做升级操作。</p><h2>小结</h2><p>这种方式给我的感觉是无可奈何,但是又非常巧妙。</p><p>上一次有这种感觉的,还是运维怕磁盘爆,预留 500MB 放一个垃圾文件,磁盘满了直接移除,快速解决问题。</p><p>你在日常工作中,见到过哪些 SAO 操作?</p>
sensitive word 敏感词(脏词) 如何忽略无意义的字符?达到更好的过滤效果?
https://segmentfault.com/a/1190000044458149
2023-12-09T15:01:34+08:00
2023-12-09T15:01:34+08:00
老马啸西风
https://segmentfault.com/u/houbinbin
0
<h2>忽略字符</h2><h3>说明</h3><p>我们的敏感词一般都是比较连续的,比如 傻帽</p><p>那就有大聪明发现,可以在中间加一些字符,比如【傻!@#$帽】跳过检测,但是骂人等攻击力不减。</p><p>那么,如何应对这些类似的场景呢?</p><p>我们可以指定特殊字符的跳过集合,忽略掉这些无意义的字符即可。</p><p>v0.11.0 开始支持</p><p><img src="/img/remote/1460000044458151" alt="无意义的字符.jpg" title="无意义的字符.jpg"></p><h3>例子</h3><p>其中 charIgnore 对应的字符策略,用户可以自行灵活定义。</p><pre><code class="java">final String text = "傻@冒,狗+东西";
//默认因为有特殊字符分割,无法识别
List<String> wordList = SensitiveWordBs.newInstance().init().findAll(text);
Assert.assertEquals("[]", wordList.toString());
// 指定忽略的字符策略,可自行实现。
List<String> wordList2 = SensitiveWordBs.newInstance()
.charIgnore(SensitiveWordCharIgnores.specialChars())
.init()
.findAll(text);
Assert.assertEquals("[傻@冒, 狗+东西]", wordList2.toString());</code></pre><h2>敏感词标签</h2><h3>说明</h3><p>有时候我们希望对敏感词加一个分类标签:比如社情、暴/力等等。</p><p>这样后续可以按照标签等进行更多特性操作,比如只处理某一类的标签。</p><p>支持版本:v0.10.0</p><h3>入门例子</h3><h4>接口</h4><p>这里只是一个抽象的接口,用户可以自行定义实现。比如从数据库查询等。</p><pre><code class="java">public interface IWordTag {
/**
* 查询标签列表
* @param word 脏词
* @return 结果
*/
Set<String> getTag(String word);
}</code></pre><h4>配置文件</h4><p>我们可以自定义 dict 标签文件,通过 WordTags.file() 创建一个 WordTag 实现。</p><ul><li>dict_tag_test.txt</li></ul><pre><code>五星红旗 政-治,国家</code></pre><p>格式如下:</p><pre><code>敏感词 tag1,tag2</code></pre><h4>实现</h4><p>具体的效果如下,在引导类设置一下即可。</p><p>默认的 wordTag 是空的。</p><pre><code class="java">String filePath = "dict_tag_test.txt";
IWordTag wordTag = WordTags.file(filePath);
SensitiveWordBs sensitiveWordBs = SensitiveWordBs.newInstance()
.wordTag(wordTag)
.init();
Assert.assertEquals("[政-治, 国家]", sensitiveWordBs.tags("五星红旗").toString());;</code></pre><p>后续会考虑引入一个内置的标签文件策略。</p><h2>更多资料</h2><h3>敏感词控台</h3><p>有时候敏感词有一个控台,配置起来会更加灵活方便。</p><blockquote><a href="https://link.segmentfault.com/?enc=DWWJUH5pJAkkvqHVrpQHuQ%3D%3D.tMuq5pa7D%2Fpzf35cRLeuG5GFvCoDQGTHj6mBgcHy3pIXgmc2cJfFspV1ghao6wfuB8yvsMi3QnwbN4I4xSgXEw%3D%3D" rel="nofollow">java 如何实现开箱即用的敏感词控台服务?</a></blockquote><h3>敏感词标签文件</h3><p>梳理了大量的敏感词标签文件,可以让我们的敏感词更加方便。</p><p>这两个资料阅读可在下方文章获取:</p><blockquote><a href="https://link.segmentfault.com/?enc=OaGvf5On6Wk5pSAnGVAbbg%3D%3D.8%2Bujnbe33I3w%2BmFFg1FjHJrWRSEF1kBeEuvGbBvSgl1dtEebW1AwkTOImp%2BjAp6aDLe3GPAnsL3nN3PGV9lDRQ%3D%3D" rel="nofollow">v0.11.0-敏感词新特性</a></blockquote><h2>拓展阅读</h2><p><a href="https://link.segmentfault.com/?enc=0eeimWoLizyan9CcNl1VJw%3D%3D.ssX3QeITw0Vd4nlyPuocfwsf5SggYJL4tFIl0Y80qViIo7v8LdPYnQ8ALVsYUgS5k2rosdD9W9KimA9Fe%2BLOVw%3D%3D" rel="nofollow">敏感词工具实现思路</a></p><p><a href="https://link.segmentfault.com/?enc=H1p9jJJ1s0R1JLTdeTwYEw%3D%3D.omChRb9MSRhA6%2BcQPxnfIf8Un03ONdy7YosdMwPxi48zOB4cy84rmJgkJpCMQIvCFJdkRd6BTXgP6KMsPVjpdQ%3D%3D" rel="nofollow">DFA 算法讲解</a></p><p><a href="https://link.segmentfault.com/?enc=6euoboepj%2F7a0xAaTBGnlA%3D%3D.JdHY6tniaS1IjVl4eTHmMHlA8W4Y9bVTv1DbMWUbc%2FqByzREfIM%2B44cluXlS9jqhpsWPPhMgBaJ0rTNK%2Fu8x%2Bg%3D%3D" rel="nofollow">敏感词库优化流程</a></p><p><a href="https://link.segmentfault.com/?enc=C8GBvFrLPb927MDS7JWnUg%3D%3D.BIyDQCiVy1TF%2FakzfAwd8gAF9Sw6lFdDZgfzXr5lTaKcEpacU7TMVFlqnV3dw3iEimgHT%2FIqX0UlxvcX4ua4jA%3D%3D" rel="nofollow">java 如何实现开箱即用的敏感词控台服务?</a></p><p><a href="https://link.segmentfault.com/?enc=yeL84Lod0N4BuHfvXu5cQg%3D%3D.QuXeWsH6jamBQNQUfK3Ec7cfQsdlDcOgNX4svygx8R6bvyTeXzgSCcj5IIh7jmCg3buYf9C9aTkp3AkGq4FekWIZMFxNXIXSV4S0TnE%2FarFVCTmgLyZNVl9mJJAtq7myDZGtDXazawl0xPYr41rcj9YVqJk0DlG00Fn%2BZXShRBAtMinBtXitpEu7ivBW8qOljbnOEB0fltOrWUY3%2BHOM5Z%2B%2BFJRIqFkjOGhbJ%2B4SAoqrXZWeGA3kKUMYSSoz4%2Bqwyw8QLH2xe8bWTUzbHD1PTAvf%2BT5XwB0Ldw991yxVUmk%3D" rel="nofollow">各大平台连敏感词库都没有的吗?</a></p><p><a href="https://link.segmentfault.com/?enc=ejh9sX78trbtPlHyloGDyQ%3D%3D.83jQGcUz%2FufRa%2BCcH033LZ63QTDnWfdJma4w5rt1nWTYk%2Bm1%2BTtsvINqCOBYqFKexxXWjDPiF2oxQVBSKNGrfPYuc1Vgnqj3Cj38WP22I1JFLIonMZHZ7iKJMddOdl%2BM" rel="nofollow">sensitive-word 敏感词/脏词开源工具-v.0.10.0-脏词分类标签支持</a></p><h2>开源框架</h2><blockquote><a href="https://link.segmentfault.com/?enc=sQge%2BeCNmA2XTc6GtOajAQ%3D%3D.MaU2csBnIk%2FDLTOWhBO7jWc%2FfP89jLudgeT6Uq3nFwkZn70UqKQMOU5IdpO%2FSQph" rel="nofollow">sensitive-word</a></blockquote>
sensitive-word 敏感词/脏词开源工具-v.0.10.0-脏词分类标签支持
https://segmentfault.com/a/1190000044446609
2023-12-05T19:04:52+08:00
2023-12-05T19:04:52+08:00
老马啸西风
https://segmentfault.com/u/houbinbin
0
<h2>sensitive-word</h2><p><a href="https://link.segmentfault.com/?enc=vNgSj1Ui%2BrEK41LovBsnLA%3D%3D.OAuN59Ne7Tt2GwUcHjdfJK4cOqHEU2xVDecLMwwT5waJueKELm2CqNrTBlyCVSbC" rel="nofollow">sensitive-word</a> 基于 DFA 算法实现的高性能敏感词工具。</p><h3>创作目的</h3><p>实现一款好用敏感词工具。</p><p>基于 DFA 算法实现,目前敏感词库内容收录 6W+(源文件 18W+,经过一次删减)。</p><p>后期将进行持续优化和补充敏感词库,并进一步提升算法的性能。</p><p>希望可以细化敏感词的分类,感觉工作量比较大,暂时没有进行。</p><p>v0.10.0 先添加对应的脏词分类接口,后续有时间完善对应的实现和词典。</p><h2>敏感词标签</h2><h3>说明</h3><p>有时候我们希望对敏感词加一个分类标签:比如社情、暴/力等等。</p><p>这样后续可以按照标签等进行更多特性操作,比如只处理某一类的标签。</p><p>支持版本:v0.10.0</p><h3>入门例子</h3><h4>接口</h4><p>这里只是一个抽象的接口,用户可以自行定义实现。比如从数据库查询等。</p><pre><code class="java">public interface IWordTag {
/**
* 查询标签列表
* @param word 脏词
* @return 结果
*/
Set<String> getTag(String word);
}</code></pre><h4>配置文件</h4><p>我们可以自定义 dict 标签文件,通过 WordTags.file() 创建一个 WordTag 实现。</p><ul><li>dict_tag_test.txt</li></ul><pre><code>五星红旗 政治,国家</code></pre><p>格式如下:</p><pre><code>敏感词 tag1,tag2</code></pre><h4>实现</h4><p>具体的效果如下,在引导类设置一下即可。</p><p>默认的 wordTag 是空的。</p><pre><code class="java">String filePath = "dict_tag_test.txt";
IWordTag wordTag = WordTags.file(filePath);
SensitiveWordBs sensitiveWordBs = SensitiveWordBs.newInstance()
.wordTag(wordTag)
.init();
Assert.assertEquals("[政治, 国家]", sensitiveWordBs.tags("五星红旗").toString());;</code></pre><p>后续会考虑引入一个内置的标签文件策略。</p><h2>拓展阅读</h2><p><a href="https://link.segmentfault.com/?enc=nz5desHKL%2BWByYR8W7i50w%3D%3D.4WOWa9w0JU%2BkwUItGnt7vn6RqCgt7AZOWAaV%2BkGPY7X6cnuzz%2FMuIavSoZiubVZGINeVDacNTeUsFgnh07U5Yw%3D%3D" rel="nofollow">敏感词工具实现思路</a></p><p><a href="https://link.segmentfault.com/?enc=ICld9c2Kx3%2FUavcHRBxDIQ%3D%3D.prD8X6wH8KKWhmzxmN9OTlH1%2B6unSZhkWC7TfUM4KVAyEvMaxAYQxj0j8H7O4coR0Fyd%2BlL4hWK%2BUhoPTe2JGA%3D%3D" rel="nofollow">DFA 算法讲解</a></p><p><a href="https://link.segmentfault.com/?enc=5kjzwyYRBsQqNz8lVCmOKQ%3D%3D.bRJOvrFhblMgp9adzlGmXpLFexZE6%2FoeW9qElTasZHS7DOc7H1ipO4BW4TQhG8A0ZcuCqyMppi8kKb4Wclgj1w%3D%3D" rel="nofollow">敏感词库优化流程</a></p><p><a href="https://link.segmentfault.com/?enc=5rbJ2xUQ6SSGbuZ2SNNnIg%3D%3D.mH6jTBe9GkxVrF%2BV%2BbCOnQoqNBHorFArvx3Ju%2FpAZWwA69%2FuZl8fWL5BWPQHX%2FmXDK88QbNBSUzx7Juyk8W6qQ%3D%3D" rel="nofollow">java 如何实现开箱即用的敏感词控台服务?</a></p><h2>开源地址</h2><p>其他很多特性以前介绍过,不再赘述。</p><p>感兴趣的话,前往开源地址:</p><blockquote><a href="https://link.segmentfault.com/?enc=o5I%2Br%2BrJmOtMLiLPH2fXwg%3D%3D.PqUdZV6GkeOIioq67hPnVGMJrhkw7Ev2tzF6wZjdPD3ufWRwGA6J22d9cx6t%2Ftvt" rel="nofollow">https://github.com/houbb/sensitive-word</a></blockquote>
高性能日志脱敏组件:已支持 log4j2 和 logback 插件
https://segmentfault.com/a/1190000044316879
2023-10-19T00:10:25+08:00
2023-10-19T00:10:25+08:00
老马啸西风
https://segmentfault.com/u/houbinbin
0
<h2>项目介绍</h2><p>日志脱敏是常见的安全需求。普通的基于工具类方法的方式,对代码的入侵性太强,编写起来又特别麻烦。</p><p><a href="https://link.segmentfault.com/?enc=3mI7NJJe7xA7K5SA8BGV4w%3D%3D.fcdA25QeMqDlWZxqW38GQdyUqv4lxcprWZeEjwbCp5n8BYBkkdmLXe5%2BfDzRm821" rel="nofollow">sensitive</a>提供基于注解的方式,并且内置了常见的脱敏方式,便于开发。</p><p>同时支持 logback 和 log4j2 等常见的日志脱敏插件。</p><h2>拓展阅读</h2><p><a href="https://link.segmentfault.com/?enc=bvhS%2Fm89g335H7GBZn3EvA%3D%3D.u%2Bz9hme3dYGPbwurb2XRrAgsLzeVt8XpJ4SP10LcDFIuN9GZXS7W3IVa0MhYlTkOn6Ki4sbY0B83c9NiUPbsgQ%3D%3D" rel="nofollow">金融用户敏感数据如何优雅地实现脱敏?</a></p><p><a href="https://link.segmentfault.com/?enc=eknHvTEaVi9GQBrGsUYBcw%3D%3D.%2FO3c9bTegtl6U%2BavFxbLN%2BMdu4ws1Y64aJU4Gwwy0JdJ9K158e5NtUp%2FeoqY9gzEshtkfk1MwaGfhMaIyhJoCQ%3D%3D" rel="nofollow">日志脱敏之后,无法根据信息快速定位怎么办?</a></p><h2>log4j2 插件统一脱敏</h2><h3>说明</h3><p>上面的方法非常适用于新的项目,按照响应的规范进行推广。</p><p>但是很多金融公司都有很多历史遗留项目,或者使用不规范,比如使用 map 等,导致上面的方法在脱敏技改时需要耗费大量的时间,而且回溯成本很高。</p><p>有没有什么方法,可以直接在日志层统一处理呢?</p><h3>log4j2 Rewrite</h3><p>我们可以基于 log4j2 RewritePolicy 统一使用脱敏策略。</p><p>说明:如果使用 slf4j 接口,实现为 log4j2 时也是支持的。</p><h3>使用入门</h3><h4>maven 引入</h4><p>引入核心脱敏包。</p><pre><code class="xml"><dependency>
<groupId>com.github.houbb</groupId>
<artifactId>sensitive-log4j2</artifactId>
<version>1.6.1</version>
</dependency></code></pre><p>其他的一般项目中也有,如 log4j2 包:</p><pre><code class="xml"><dependency>
<groupId>org.apache.logging.log4j</groupId>
<artifactId>log4j-api</artifactId>
<version>${log4j2.version}</version>
</dependency>
<dependency>
<groupId>org.apache.logging.log4j</groupId>
<artifactId>log4j-core</artifactId>
<version>${log4j2.version}</version>
</dependency></code></pre><h4>log4j2.xml 配置</h4><p>例子如下:</p><pre><code class="xml"><?xml version="1.0" encoding="UTF-8"?>
<Configuration status="WARN" packages = "com.github.houbb.sensitive.log4j2.layout">
<Properties>
<Property name="DEFAULT_PATTERN">%d{HH:mm:ss.SSS} [%t] %-5level %logger{36} - %msg%n</Property>
<Property name="DEFAULT_CHARSET">UTF-8</Property>
</Properties>
<Appenders>
<Console name="Console" target="SYSTEM_OUT">
<SensitivePatternLayout/>
</Console>
</Appenders>
<Loggers>
<Root level="DEBUG">
<AppenderRef ref="Console"/>
</Root>
</Loggers>
</Configuration></code></pre><p>几个步骤:</p><ol><li>指定 package 为 <code>packages = "com.github.houbb.sensitive.log4j2.layout"</code></li><li>按照 log4j2 layout 规范,指定 Layout 策略为 <code>SensitivePatternLayout</code></li></ol><h4>测试</h4><p>正常的日志打印:</p><pre><code class="java">private static final String TEST_LOG = "mobile:13088887777; bankCard:6217004470007335024, email:mahuateng@qq.com, amount:123.00, " +
"IdNo:340110199801016666, name1:李明, name2:李晓明, name3:李泽明天, name4:山东小栗旬" +
", birthday:20220517, GPS:120.882222, IPV4:127.0.0.1, address:中国上海市徐汇区888号;";
logger.info(TEST_LOG);</code></pre><p>自动脱敏效果如下:</p><pre><code>01:37:28.010 [main] INFO com.github.houbb.sensitive.test.log4j2.Log4j2AndSlf4jLayoutTest - mobile:130****7777|9FC4D36D63D2B6DC5AE1297544FBC5A2; bankCard:6217***********5024|444F49289B30944AB8C6C856AEA21180, email:mahu*****@qq.com|897915594C94D981BA86C9E83ADD449C, amount:123.00, IdNo:340110199801016666, name1:李明, name2:李晓明, name3:李泽明天, name4:山东小栗旬, birthday:20220517, GPS:120.882222, IPV4:127.0.0.1, address:中国上海市徐******|821A601949B1BD18DCBAAE27F2E27147;</code></pre><p>ps: 这里是为了演示各种效果,实际默认对应为 1,2,3,4,9 这几种策略。</p><h3>log4j2 配置定制化</h3><p>为了满足各种用户的场景,在 V1.6.0 引入了 SensitivePatternLayout 策略的可配置化。</p><p>用户可以在应用 resources 下通过 <code>chars-scan-config.properties</code> 配置文件指定。</p><h4>默认配置</h4><p>log4j2 配置中,<code>SensitivePatternLayout</code> 配置默认为:</p><pre><code class="properties">chars.scan.prefix=::,,'"‘“=| +()()
chars.scan.scanList=1,2,3,4,9
chars.scan.replaceList=1,2,3,4,9
chars.scan.defaultReplace=12
chars.scan.replaceHash=md5
chars.scan.whiteList=""</code></pre><h4>属性说明</h4><p>SensitivePatternLayout 策略的属性说明。</p><table><thead><tr><th align="left">属性</th><th align="left">说明</th><th align="left">默认值</th><th align="left">备注</th></tr></thead><tbody><tr><td align="left">prefix</td><td align="left">需要脱敏信息的匹配前缀</td><td align="left"><code>::,,'"‘“= +()()</code> 和英文竖线</td><td align="left">降低误判率</td></tr><tr><td align="left">replaceHash</td><td align="left">哈希策略模式</td><td align="left"><code>md5</code></td><td align="left">支持 md5/none 两种模式</td></tr><tr><td align="left">scanList</td><td align="left">敏感扫描策略列表</td><td align="left"><code>1,2,3,4</code></td><td align="left">1~10 内置的10种敏感信息扫描策略,多个用逗号隔开</td></tr><tr><td align="left">replaceList</td><td align="left">敏感替换策略列表</td><td align="left"><code>1,2,3,4</code></td><td align="left">1~10 内置的10种敏感信息替换策略,多个用逗号隔开</td></tr><tr><td align="left">defaultReplace</td><td align="left">敏感替换默认策略</td><td align="left"><code>12</code></td><td align="left">1~13 内置的13种敏感信息替换策略,指定一个。当列表没有匹配时,默认使用这个</td></tr><tr><td align="left">whiteList</td><td align="left">白名单</td><td align="left">``</td><td align="left">希望跳过处理的白名单信息</td></tr></tbody></table><p>其中 1-13 的内置策略说明如下:</p><table><thead><tr><th align="left">策略标识</th><th align="left">说明</th></tr></thead><tbody><tr><td align="left">1</td><td align="left">手机号</td></tr><tr><td align="left">2</td><td align="left">身份证</td></tr><tr><td align="left">3</td><td align="left">银行卡</td></tr><tr><td align="left">4</td><td align="left">邮箱</td></tr><tr><td align="left">5</td><td align="left">中国人名</td></tr><tr><td align="left">6</td><td align="left">出生日期</td></tr><tr><td align="left">7</td><td align="left">GPS</td></tr><tr><td align="left">8</td><td align="left">IPV4</td></tr><tr><td align="left">9</td><td align="left">地址</td></tr><tr><td align="left">10</td><td align="left">护照</td></tr><tr><td align="left">11</td><td align="left">匹配任意不掩盖</td></tr><tr><td align="left">12</td><td align="left">匹配任意半掩盖</td></tr><tr><td align="left">13</td><td align="left">匹配任意全掩盖</td></tr></tbody></table><h4>不足之处</h4><p>这里的策略自定义和 log4j2 的插件化比起来,确实算不上强大,但是可以满足 99% 的脱敏场景。</p><p>后续有时间考虑类似 log4j2 的 plugins 思想,实现更加灵活的自定义策略。</p><h2>logback 脱敏插件</h2><h3>说明</h3><p>为了便于用户使用,v1.6.0 开始支持 logback 插件模式。</p><h3>使用入门</h3><h4>maven 引入</h4><p>引入 logback 依赖包</p><pre><code class="xml"><dependency>
<groupId>ch.qos.logback</groupId>
<artifactId>logback-classic</artifactId>
<version>${logback.version}</version>
</dependency></code></pre><h4>指定 logback.xml 配置</h4><pre><code class="xml"><configuration>
<!-- 基于 converter -->
<conversionRule conversionWord="sensitive" converterClass="com.github.houbb.sensitive.logback.converter.SensitiveLogbackConverter" />
<!-- 使用 converter -->
<appender name="STDOUTConverter" class="ch.qos.logback.core.ConsoleAppender">
<encoder>
<pattern>%d{HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %sensitive%n</pattern>
</encoder>
</appender>
<!-- 使用 layout -->
<appender name="STDOUTLayout" class="ch.qos.logback.core.ConsoleAppender">
<layout class="com.github.houbb.sensitive.logback.layout.SensitiveLogbackLayout">
<pattern>%d{HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n</pattern>
</layout>
</appender>
<!-- 设置根日志级别为DEBUG,并将日志输出到控制台 -->
<root level="DEBUG">
<appender-ref ref="STDOUTConverter"/>
<appender-ref ref="STDOUTLayout"/>
</root>
</configuration></code></pre><p>这里共计支持 Converter 和 Layout 两种模式,任选一个即可。</p><p>建议使用 SensitiveLogbackConverter,脱敏日志内容。</p><h3>日志效果</h3><p>脱密效果和 log4j2 类似,如下:</p><pre><code>01:42:32.579 [main] INFO c.g.h.sensitive.test2.LogbackMain - mobile:130****7777|9FC4D36D63D2B6DC5AE1297544FBC5A2; bankCard:6217***********5024|444F49289B30944AB8C6C856AEA21180, email:mahu*****@qq.com|897915594C94D981BA86C9E83ADD449C, amount:123.00, " + "IdNo:340110199801016666, name1:李明, name2:李晓明, name3:李泽明天, name4:山东小栗旬" + ", birthday:20220517, GPS:120.882222, IPV4:127.0.0.1, address:中国上海市徐******|821A601949B1BD18DCBAAE27F2E27147;</code></pre><h3>配置属性</h3><p>同 log4j2,此处不再赘述。</p><h2>小结</h2><p>基于日志插件的方式更加方便,便于项目的推广使用,实用性更强。</p><p>项目的开源地址</p><blockquote><a href="https://link.segmentfault.com/?enc=cwPgO8kA14pnvWZVZrOHGQ%3D%3D.NlKQRZpMTf%2Fl5%2BdZ80BEVFj8kFF6a1fTVIdW4nbLy4kcAwoIB%2F9ZQ8zAVX4oUXk%2F" rel="nofollow">https://github.com/houbb/sensitive</a></blockquote>
日志开源组件(六)Adaptive Sampling 自适应采样
https://segmentfault.com/a/1190000044158689
2023-08-28T23:52:22+08:00
2023-08-28T23:52:22+08:00
老马啸西风
https://segmentfault.com/u/houbinbin
0
<h2>业务背景</h2><p>有时候日志的信息比较多,怎么样才可以让系统做到自适应采样呢?</p><h3>拓展阅读</h3><p><a href="https://link.segmentfault.com/?enc=SixAI8Fa70XFfXWLBq3sXA%3D%3D.5Uj8SdOjOvR5ncFoKg9hG13wRdx%2BladpLy1cTZW8eb6KEg1eucEgcnuQOAPhpWPAb3WzOVs%2FWBhuMH63RmqWRw%3D%3D" rel="nofollow">日志开源组件(一)java 注解结合 spring aop 实现自动输出日志</a></p><p><a href="https://link.segmentfault.com/?enc=oYVefBooBXSnMz6GpKXzaw%3D%3D.aZHLqrQ03sOcW4116Oo7qjFoDELhlBBZFYXc%2BdpvViaINddrzfOCDMxJNz0VGVn8FDprny12aR%2BExJ5JuRoxQA%3D%3D" rel="nofollow">日志开源组件(二)java 注解结合 spring aop 实现日志traceId唯一标识</a></p><p><a href="https://link.segmentfault.com/?enc=tluxBwoc9yMBZCIXcLWylQ%3D%3D.%2ByGmbF8aU1nerIjn0ViedbwRtuuTMihqE%2F9t00AhlwWbXTN5wseEKs29dRYDHH1o84WLS27Wl2VE4jxzZe%2BrJg%3D%3D" rel="nofollow">日志开源组件(三)java 注解结合 spring aop 自动输出日志新增拦截器与过滤器</a></p><p><a href="https://link.segmentfault.com/?enc=%2BjeIMDZkh1FEZSGs39rYSA%3D%3D.1nzi5fNb%2F8sGSKW4bIY%2FVNh2JpxZwb2iEXhC4K8IowOU2IF2wXSoHxcMQYePLAXiGkXo4zvpvO%2BtGw%2FRoT8XVA%3D%3D" rel="nofollow">日志开源组件(四)如何动态修改 spring aop 切面信息?让自动日志输出框架更好用</a></p><p><a href="https://link.segmentfault.com/?enc=Z6KUHOOaxXURzIPpGuJFSw%3D%3D.udIHSqRNGTTXx3B0XByVr8CqPcXPJrOZX%2Buzj%2BFXAh9aW3pq8jKAzV1wxKUM4IxN4RAFJ8L4JaY4jZxIKBa2av%2BdKx6eE9SwoypZi4O0L%2F8%3D" rel="nofollow">日志开源组件(五)如何将 dubbo filter 拦截器原理运用到日志拦截器中?</a></p><h2>自适应采样</h2><h3>是什么?</h3><p>系统生成的日志可以包含大量信息,包括错误、警告、性能指标等,但在实际应用中,处理和分析所有的日志数据可能会对系统性能和资源产生负担。</p><p>自适应采样在这种情况下发挥作用,它能够根据当前系统状态和日志信息的重要性,智能地决定哪些日志需要被采样记录,从而有效地管理和分析日志数据。</p><h3>采样的必要性</h3><p>日志采样系统会给业务系统额外增加消耗,很多系统在接入的时候会比较排斥。</p><p>给他们一个百分比的选择,或许是一个不错的开始,然后根据实际需要选择合适的比例。</p><p>自适应采样是一个对用户透明,同时又非常优雅的方案。</p><p><img src="/img/remote/1460000044158691" alt="自适应" title="自适应"></p><h2>如何通过 java 实现自适应采样?</h2><h3>接口定义</h3><p>首先我们定义一个接口,返回 boolean。</p><p>根据是否为 true 来决定是否输出日志。</p><pre><code class="java">/**
* 采样条件
* @author binbin.hou
* @since 0.5.0
*/
public interface IAutoLogSampleCondition {
/**
* 条件
*
* @param context 上下文
* @return 结果
* @since 0.5.0
*/
boolean sampleCondition(IAutoLogContext context);
}</code></pre><h3>百分比概率采样</h3><p>我们先实现一个简单的概率采样。</p><p>0-100 的值,让用户指定,按照百分比决定是否采样。</p><pre><code class="java">public class InnerRandomUtil {
/**
* 1. 计算一个 1-100 的随机数 randomVal
* 2. targetRatePercent 值越大,则返回 true 的概率越高
* @param targetRatePercent 目标百分比
* @return 结果
*/
public static boolean randomRateCondition(int targetRatePercent) {
if(targetRatePercent <= 0) {
return false;
}
if(targetRatePercent >= 100) {
return true;
}
// 随机
ThreadLocalRandom threadLocalRandom = ThreadLocalRandom.current();
int value = threadLocalRandom.nextInt(1, 100);
// 随机概率
return targetRatePercent >= value;
}
}</code></pre><p>实现起来也非常简单,直接一个随机数,然后比较大小即可。</p><h3>自适应采样</h3><h4>思路</h4><p>我们计算一下当前日志的 QPS,让输出的概率和 QPS 称反比。</p><pre><code class="java">/**
* 自适应采样
*
* 1. 初始化采样率为 100%,全部采样
*
* 2. QPS 如果越来越高,那么采样率应该越来越低。这样避免 cpu 等资源的损耗。最低为 1%
* 如果 QPS 越来越低,采样率应该越来越高。增加样本,最高为 100%
*
* 3. QPS 如何计算问题
*
* 直接设置大小为 100 的队列,每一次在里面放入时间戳。
* 当大小等于 100 的时候,计算首尾的时间差,currentQps = 100 / (endTime - startTime) * 1000
*
* 触发 rate 重新计算。
*
* 3.1 rate 计算逻辑
*
* 这里我们存储一下 preRate = 100, preQPS = ?
*
* newRate = (preQps / currentQps) * rate
*
* 范围限制:
* newRate = Math.min(100, newRate);
* newRate = Math.max(1, newRate);
*
* 3.2 时间队列的清空
*
* 更新完 rate 之后,对应的队列可以清空?
*
* 如果额外使用一个 count,好像也可以。
* 可以调整为 atomicLong 的计算器,和 preTime。
*</code></pre><h4>代码实现</h4><pre><code class="java">public class AutoLogSampleConditionAdaptive implements IAutoLogSampleCondition {
private static final AutoLogSampleConditionAdaptive INSTANCE = new AutoLogSampleConditionAdaptive();
/**
* 单例的方式获取实例
* @return 结果
*/
public static AutoLogSampleConditionAdaptive getInstance() {
return INSTANCE;
}
/**
* 次数大小限制,即接收到多少次请求更新一次 adaptive 计算
*
* TODO: 这个如何可以让用户可以自定义呢?后续考虑配置从默认的配置文件中读取。
*/
private static final int COUNT_LIMIT = 1000;
/**
* 自适应比率,初始化为 100.全部采集
*/
private volatile int adaptiveRate = 100;
/**
* 上一次的 QPS
*
* TODO: 这个如何可以让用户可以自定义呢?后续考虑配置从默认的配置文件中读取。
*/
private volatile double preQps = 100.0;
/**
* 上一次的时间
*/
private volatile long preTime;
/**
* 总数,请求计数器
*/
private final AtomicInteger counter;
public AutoLogSampleConditionAdaptive() {
preTime = System.currentTimeMillis();
counter = new AtomicInteger(0);
}
@Override
public boolean sampleCondition(IAutoLogContext context) {
int count = counter.incrementAndGet();
// 触发一次重新计算
if(count >= COUNT_LIMIT) {
updateAdaptiveRate();
}
// 直接计算是否满足
return InnerRandomUtil.randomRateCondition(adaptiveRate);
}
}</code></pre><p>每次累加次数超过限定次数之后,我们就更新一下对应的日志概率。</p><p>最后的概率计算和上面的百分比类似,不再赘述。</p><pre><code class="java">/**
* 更新自适应的概率
*
* 100 计算一次,其实还好。实际应该可以适当调大这个阈值,本身不会经常变化的东西。
*/
private synchronized void updateAdaptiveRate() {
//消耗的毫秒数
long costTimeMs = System.currentTimeMillis() - preTime;
//qps 的计算,时间差是毫秒。所以次数需要乘以 1000
double currentQps = COUNT_LIMIT*1000.0 / costTimeMs;
// preRate * preQps = currentRate * currentQps; 保障采样均衡,服务器压力均衡
// currentRate = (preRate * preQps) / currentQps;
// 更新比率
int newRate = 100;
if(currentQps > 0) {
newRate = (int) ((adaptiveRate * preQps) / currentQps);
newRate = Math.min(100, newRate);
newRate = Math.max(1, newRate);
}
// 更新 rate
adaptiveRate = newRate;
// 更新 QPS
preQps = currentQps;
// 更新上一次的时间内戳
preTime = System.currentTimeMillis();
// 归零
counter.set(0);
}</code></pre><h3>自适应代码-改良</h3><h4>问题</h4><p>上面的自适应算法一般情况下都可以运行的很好。</p><p>但是有一种情况会不太好,那就是流量从高峰期到低峰期。</p><p>比如凌晨11点是请求高峰期,我们的输出日志概率很低。深夜之后请求数会很少,想达到累计值就会很慢,这个时间段就会导致日志输出很少。</p><p>如何解决这个问题呢?</p><h4>思路</h4><p>我们可以通过固定时间窗口的方式,来定时调整流量概率。</p><h4>java 实现</h4><p>我们初始化一个定时任务,1min 定时更新一次。</p><pre><code class="java">public class AutoLogSampleConditionAdaptiveSchedule implements IAutoLogSampleCondition {
private static final ScheduledExecutorService EXECUTOR_SERVICE = Executors.newSingleThreadScheduledExecutor();
/**
* 时间分钟间隔
*/
private static final int TIME_INTERVAL_MINUTES = 5;
/**
* 自适应比率,初始化为 100.全部采集
*/
private volatile int adaptiveRate = 100;
/**
* 上一次的总数
*
* TODO: 这个如何可以让用户可以自定义呢?后续考虑配置从默认的配置文件中读取。
*/
private volatile long preCount;
/**
* 总数,请求计数器
*/
private final AtomicLong counter;
public AutoLogSampleConditionAdaptiveSchedule() {
counter = new AtomicLong(0);
preCount = TIME_INTERVAL_MINUTES * 60 * 100;
//1. 1min 后开始执行
//2. 中间默认 5 分钟更新一次
EXECUTOR_SERVICE.scheduleAtFixedRate(new Runnable() {
@Override
public void run() {
updateAdaptiveRate();
}
}, 60, TIME_INTERVAL_MINUTES * 60, TimeUnit.SECONDS);
}
@Override
public boolean sampleCondition(IAutoLogContext context) {
counter.incrementAndGet();
// 直接计算是否满足
return InnerRandomUtil.randomRateCondition(adaptiveRate);
}
}</code></pre><p>其中更新概率的逻辑和上面类似:</p><pre><code class="java">/**
* 更新自适应的概率
*
* QPS = count / time_interval
*
* 其中时间维度是固定的,所以可以不用考虑时间。
*/
private synchronized void updateAdaptiveRate() {
// preRate * preCount = currentRate * currentCount; 保障采样均衡,服务器压力均衡
// currentRate = (preRate * preCount) / currentCount;
// 更新比率
long currentCount = counter.get();
int newRate = 100;
if(currentCount != 0) {
newRate = (int) ((adaptiveRate * preCount) / currentCount);
newRate = Math.min(100, newRate);
newRate = Math.max(1, newRate);
}
// 更新自适应频率
adaptiveRate = newRate;
// 更新 QPS
preCount = currentCount;
// 归零
counter.set(0);
}</code></pre><h2>小结</h2><p>让系统自动化分配资源,是一种非常好的思路,可以让资源利用最大化。</p><p>实现起来也不是很困难,实际要根据我们的业务量进行观察和调整。</p><h2>开源地址</h2><blockquote>auto-log <a href="https://link.segmentfault.com/?enc=lPxZVAbvVqmfdcF4HzgKlA%3D%3D.Lk3JaJPSF8NDAQi%2FE8Jzok2gQQtin1hlgFpyWxFk1F6DIIyVnh6iBHraoEdDqlOE" rel="nofollow">https://github.com/houbb/auto-log</a></blockquote>
如何将 dubbo filter 拦截器原理运用到日志拦截器中?
https://segmentfault.com/a/1190000044085612
2023-08-06T20:45:13+08:00
2023-08-06T20:45:13+08:00
老马啸西风
https://segmentfault.com/u/houbinbin
0
<h2>业务背景</h2><p>我们希望可以在使用日志拦截器时,定义属于自己的拦截器方法。</p><p>实现的方式有很多种,我们分别来看一下。</p><h3>拓展阅读</h3><p><a href="https://link.segmentfault.com/?enc=7IbvaBSxZoGKTv9NmBTDkQ%3D%3D.sTO7ZJ%2B5LNFQZezZPrkh82IJDgthYbXYdlTIUv68KcWAAokMjRbgqLNUxLaBwJbTbLqFgtrUiJj0cv5PUXhZqw%3D%3D" rel="nofollow">java 注解结合 spring aop 实现自动输出日志</a></p><p><a href="https://link.segmentfault.com/?enc=ru9csQdEpM%2BFGGqJveIegA%3D%3D.JPwmR9hx5qhNDr14%2FkJ7Emv0N3fQyAukK%2Bt2VL0KjUHu1ezmb8Bs9rEZnG6Fauwp%2Fa29Mo6cHdfijWFBU8oZaA%3D%3D" rel="nofollow">java 注解结合 spring aop 实现日志traceId唯一标识</a></p><p><a href="https://link.segmentfault.com/?enc=JPTmAdqFJ%2BTCMOhK0O30hw%3D%3D.Oc343RiRdP2LRqUOXhI29uxsmZSiTNpdQMY0nTF2t1D3RoVTbm%2Fu6btHrKnwfw0Q%2FfnQL8yCU7em%2BUJR1rFjQg%3D%3D" rel="nofollow">java 注解结合 spring aop 自动输出日志新增拦截器与过滤器</a></p><p><a href="https://link.segmentfault.com/?enc=2kzLbz8%2BPdHKnZ0G9YhSfw%3D%3D.FpWaUChyUgQBKUMG50jOqHk5nQ75xT8dExX0XS4ML1dXh3lKc8J4QUlWGOT5IcAmxiVf1LMx98015rHmYWy43A%3D%3D" rel="nofollow">如何动态修改 spring aop 切面信息?让自动日志输出框架更好用</a></p><p><a href="https://link.segmentfault.com/?enc=egpHyDM%2FzBn4nxPXbqAOFQ%3D%3D.%2B%2BMdhQ8fpPBfxt3PNytB94QekBB3oPrTO8soI%2BZkGKj1SfN0ptKW8xx2DxFoZ0HBWtqhO%2BT2vlAmDPiMF9G78YmQua0ck3T7bjKrZFX1Iug%3D" rel="nofollow">如何将 dubbo filter 拦截器原理运用到日志拦截器中?</a></p><p><img src="/img/remote/1460000044085614" alt="chain" title="chain"></p><h2>v1-基本版本</h2><h3>接口</h3><p>最常见的定义方式,在方法执行前后,异常,finally 提供钩子函数。</p><pre><code class="java">package com.github.houbb.auto.log.api;
/**
* autoLog 拦截器
* @author binbin.hou
* @since 0.0.10
*/
public interface IAutoLogInterceptor {
/**
* 执行之前
* @param interceptorContext 拦截器上下文
* @since 0.0.10
*/
void beforeHandle(IAutoLogInterceptorContext interceptorContext);
/**
* 执行之后
* @param interceptorContext 拦截器上下文
* @param result 方法执行结果
* @since 0.0.10
*/
void afterHandle(IAutoLogInterceptorContext interceptorContext,
final Object result);
/**
* 异常处理
* @param interceptorContext 拦截器上下文
* @param exception 异常
* @since 0.0.10
*/
void exceptionHandle(IAutoLogInterceptorContext interceptorContext, Exception exception);
/**
* finally 中执行的代码
* @param interceptorContext 拦截器上下文
* @since 0.0.10
*/
void finallyHandle(IAutoLogInterceptorContext interceptorContext);
}</code></pre><h3>工具中统一使用拦截器</h3><pre><code class="java">package com.github.houbb.auto.log.core.core.impl;
/**
* @author binbin.hou
* @since 0.0.7
*/
public class SimpleAutoLog implements IAutoLog {
/**
* 自动日志输出
*
* @param context 上下文
* @return 结果
* @since 0.0.7
*/
@Override
public Object autoLog(IAutoLogContext context) throws Throwable {
//1. 日志唯一标识
// ... 省略
List<IAutoLogInterceptor> autoLogInterceptors = null;
try {
// ... 省略其他逻辑
// 获取拦截器
autoLogInterceptors = autoLogInterceptors(autoLog);
//1.2 autoLog
if(CollectionUtil.isNotEmpty(autoLogInterceptors)) {
for(IAutoLogInterceptor interceptor : autoLogInterceptors) {
interceptor.beforeHandle(autoLogContext);
}
}
//2. 执行结果
Object result = context.process();
//2.1 方法执行后
if(CollectionUtil.isNotEmpty(autoLogInterceptors)) {
for(IAutoLogInterceptor interceptor : autoLogInterceptors) {
interceptor.afterHandle(autoLogContext, result);
}
}
//2.2 返回方法
return result;
} catch (Exception exception) {
if(CollectionUtil.isNotEmpty(autoLogInterceptors)) {
for(IAutoLogInterceptor interceptor : autoLogInterceptors) {
interceptor.exceptionHandle(autoLogContext, exception);
}
}
throw new AutoLogRuntimeException(exception);
} finally {
// 先执行日志
if(CollectionUtil.isNotEmpty(autoLogInterceptors)) {
for(IAutoLogInterceptor interceptor : autoLogInterceptors) {
interceptor.finallyHandle(autoLogContext);
}
}
}
}
/**
* 创建拦截器列表
* @param autoLog 注解
* @return 结果
* @since 0.0.10
*/
private List<IAutoLogInterceptor> autoLogInterceptors(final AutoLog autoLog) {
List<IAutoLogInterceptor> resultList = new ArrayList<>();
if(ObjectUtil.isNull(autoLog)) {
return resultList;
}
Class<? extends IAutoLogInterceptor>[] interceptorClasses = autoLog.interceptor();
if(ArrayUtil.isEmpty(interceptorClasses)) {
return resultList;
}
// 循环创建
for(Class<? extends IAutoLogInterceptor> clazz : interceptorClasses) {
IAutoLogInterceptor traceIdInterceptor = createAutoLogInterceptor(clazz);
resultList.add(traceIdInterceptor);
}
return resultList;
}
/**
* 创建拦截器
* @param clazz 类
* @return 实体
* @since 0.0.10
*/
private IAutoLogInterceptor createAutoLogInterceptor(final Class<? extends IAutoLogInterceptor> clazz) {
if(IAutoLogInterceptor.class.equals(clazz)) {
return new AutoLogInterceptor();
}
return ClassUtil.newInstance(clazz);
}
}</code></pre><h3>自定义实现拦截器</h3><p>我们想自定义拦截器方法时,只需要实现对应的接口即可。</p><pre><code class="java">/**
* 自定义日志拦截器
* @author binbin.hou
* @since 0.0.12
*/
public class MyAutoLogInterceptor extends AbstractAutoLogInterceptor {
@Override
protected void doBefore(AutoLog autoLog, IAutoLogInterceptorContext context) {
System.out.println("自定义入参:" + Arrays.toString(context.filterParams()));
}
@Override
protected void doAfter(AutoLog autoLog, Object result, IAutoLogInterceptorContext context) {
System.out.println("自定义出参:" + result);
}
@Override
protected void doException(AutoLog autoLog, Exception exception, IAutoLogInterceptorContext context) {
System.out.println("自定义异常:");
exception.printStackTrace();
}
}</code></pre><h3>方法的不足</h3><p>这种方式可以实现常见的功能,但是依然不够优雅。</p><p>我们还是无法非常灵活的定义自己的拦截器实现,就像我们使用 aop 增强,或者 dubbo filter 一样。</p><p>感兴趣的小伙伴可以移步学习一下,此处不做展开。</p><blockquote><a href="https://link.segmentfault.com/?enc=aESZceME5LGZ8DnzPs8S3g%3D%3D.h2%2FzZiGRJp%2FMnTYMTy81Fu7o%2BQ5srtPlmHvhorIvAxUf1AlSJpMr57qd39IrCtL6VEQTDMXsCrcewwkEWjfBnQ%3D%3D" rel="nofollow">Dubbo-02-dubbo invoke filter 链式调用原理</a></blockquote><h2>模拟 dubbo filter</h2><h3>实现 Invoker</h3><p>类似 dubbo invoke,直接在以前的类中初始化即可。</p><pre><code class="java">AutoLogInvoker autoLogInvoker = new AutoLogInvoker(context);
Invocation invocation = new CommonInvocation();
invocation.setAttachment(AutoLogAttachmentKeyConst.AUTO_LOG_CONTEXT, context);
invocation.setAttachment(AutoLogAttachmentKeyConst.AUTO_LOG_START_TIME, startTimeMills);
invocation.setAttachment(AutoLogAttachmentKeyConst.AUTO_LOG_FILTER_PARAMS, filterParams);
Invoker chainInvoker = InvokerChainBuilder.buildInvokerChain(autoLogInvoker);
Result autoLogResult = chainInvoker.invoke(invocation);</code></pre><p>其中 AutoLogInvoker 只是对方法的执行。</p><h4>实现拦截器</h4><p>这是的方法增强就是类似 dubbo filter 链式调用实现的,自定义的时候也会方便很多。</p><p>不需要拘泥于方法的执行位置,直接编写我们的增强逻辑即可。</p><pre><code class="java">package com.github.houbb.auto.log.core.support.interceptor.chain;
import com.alibaba.fastjson.JSON;
import com.github.houbb.auto.log.annotation.AutoLog;
import com.github.houbb.auto.log.api.IAutoLogContext;
import com.github.houbb.auto.log.core.constant.AutoLogAttachmentKeyConst;
import com.github.houbb.common.filter.annotation.FilterActive;
import com.github.houbb.common.filter.api.CommonFilter;
import com.github.houbb.common.filter.api.Invocation;
import com.github.houbb.common.filter.api.Invoker;
import com.github.houbb.common.filter.api.Result;
import com.github.houbb.common.filter.exception.CommonFilterException;
import com.github.houbb.heaven.util.lang.StringUtil;
import com.github.houbb.heaven.util.lang.reflect.ClassUtil;
import com.github.houbb.heaven.util.lang.reflect.ReflectMethodUtil;
import com.github.houbb.id.api.Id;
import com.github.houbb.id.core.core.Ids;
import com.github.houbb.id.core.util.IdThreadLocalHelper;
import com.github.houbb.log.integration.core.Log;
import com.github.houbb.log.integration.core.LogFactory;
import java.lang.reflect.Method;
/**
* 默认的日志拦截器
*/
@FilterActive(order = Integer.MIN_VALUE)
public class AutoLogCommonFilter implements CommonFilter {
private static final Log LOG = LogFactory.getLog(AutoLogCommonFilter.class);
/**
* 是否需要处理日志自动输出
* @param autoLog 上下文
* @return 结果
* @since 0.0.10
*/
protected boolean enableAutoLog(final AutoLog autoLog) {
if(autoLog == null) {
return false;
}
return autoLog.enable();
}
/**
* 获取方法描述
* @param method 方法
* @param autoLog 注解
* @return 结果
* @since 0.0.10
*/
protected String getMethodDescription(Method method, AutoLog autoLog) {
String methodName = ReflectMethodUtil.getMethodFullName(method);
if(autoLog != null
&& StringUtil.isNotEmpty(autoLog.description())) {
methodName += "#" + autoLog.description();
}
return methodName;
}
/**
* 获取 traceId
* @param autoLog 日志注解
* @return 结果
* @since 0.0.10
*/
protected String getTraceId(AutoLog autoLog) {
//1. 优先看当前线程中是否存在
String oldId = IdThreadLocalHelper.get();
if(StringUtil.isNotEmpty(oldId)) {
return formatTraceId(oldId);
}
//2. 返回对应的标识
Id id = getActualTraceId(autoLog);
return formatTraceId(id.id());
}
/**
* 获取日志跟踪号策略
* @param autoLog 注解
* @return 没结果
*/
protected Id getActualTraceId(AutoLog autoLog) {
Class<? extends Id> idClass = autoLog.traceId();
if(Id.class.equals(idClass)) {
return Ids.uuid32();
}
return ClassUtil.newInstance(autoLog.traceId());
}
/**
* 格式化日志跟踪号
* @param id 跟踪号
* @return 结果
* @since 0.0.16
*/
protected String formatTraceId(String id) {
return String.format("[%s] ", id);
}
@Override
public Result invoke(Invoker invoker, Invocation invocation) throws CommonFilterException {
final IAutoLogContext autoLogContext = (IAutoLogContext) invocation.getAttachment(AutoLogAttachmentKeyConst.AUTO_LOG_CONTEXT);
final AutoLog autoLog = autoLogContext.autoLog();
final boolean enableAutoLog = enableAutoLog(autoLog);
if(!enableAutoLog) {
return invoker.invoke(invocation);
}
final String description = getMethodDescription(autoLogContext.method(), autoLog);
// 默认从上下文中取一次
String traceId = IdThreadLocalHelper.get();
try {
// 设置 traceId 策略
if(autoLog.enableTraceId()) {
Id id = getActualTraceId(autoLog);
traceId = id.id();
invocation.setAttachment(AutoLogAttachmentKeyConst.AUTO_LOG_TRACE_ID, traceId);
IdThreadLocalHelper.put(traceId);
}
Result result = invoker.invoke(invocation);
// 日志增强
logForEnhance(autoLogContext, traceId, description, result.getValue(), invocation);
return result;
} catch (Exception e) {
if (autoLog.exception()) {
String message = String.format("[TID=%s][EXCEPTION=%s]", traceId, e.getMessage());
LOG.error(message, e);
}
throw new RuntimeException(e);
}
}
/**
* 增强日志输出
* @param autoLogContext 上下文
* @param traceId 日志跟踪号
* @param description 方法描述
* @param resultValue 返回值
* @param invocation 调用上下文
*/
private void logForEnhance(final IAutoLogContext autoLogContext,
final String traceId,
final String description,
final Object resultValue,
Invocation invocation) {
final AutoLog autoLog = autoLogContext.autoLog();
StringBuilder logBuilder = new StringBuilder();
logBuilder.append(String.format("[TID=%s]", traceId));
logBuilder.append(String.format("[METHOD=%s]", description));
// 入参
if(autoLog.param()) {
Object[] params = (Object[]) invocation.getAttachment(AutoLogAttachmentKeyConst.AUTO_LOG_FILTER_PARAMS);
logBuilder.append(String.format("[PARAM=%s]", JSON.toJSONString(params)));
}
// 出参
if (autoLog.result()) {
logBuilder.append(String.format("[RESULT=%s]", JSON.toJSONString(resultValue)));
}
// 耗时
//3.1 耗时 & 慢日志
if(autoLog.costTime()) {
long startTime = (long) invocation.getAttachment(AutoLogAttachmentKeyConst.AUTO_LOG_START_TIME);
long costTime = System.currentTimeMillis() - startTime;
logBuilder.append(String.format("[COST=%d ms]", costTime));
// 慢日志
final long slowThreshold = autoLog.slowThresholdMills();
if(slowThreshold > 0 && costTime > slowThreshold) {
logBuilder.append(String.format("[SLOW-THRESHOLD=%s]", slowThreshold));
}
}
// 输出日志
LOG.info(logBuilder.toString());
}
}</code></pre><h2>开源地址</h2><p>为了便于大家学习,项目已开源。</p><blockquote><p>Github: <a href="https://link.segmentfault.com/?enc=w%2B4JmMuqX8VpVU%2Bxb3a6vw%3D%3D.8YtehnaFN%2B2BCs0X1Y1rYmha%2BoKGM5I3nvAiscmKX4SVKy8V4aHcv%2FZHMWA39zeg" rel="nofollow">https://github.com/houbb/auto-log</a></p><p>Gitee: <a href="https://link.segmentfault.com/?enc=2s%2BrFJI8ca8afaGjkeHgoA%3D%3D.4m%2F%2BYQIti7v0oNnCQyqgCQgZoQc2BD0RySBtB0hknOZaQ9wC9deldlBJPDMyngxO" rel="nofollow">https://gitee.com/houbinbin/auto-log</a></p></blockquote><h2>小结</h2><p>dubbo filter 模式非常的优雅,以前一直只是学习,没有将其应用到自己的项目中。</p><p>提供的便利性是非常强大的,值得学习运用。</p><h2>参考资料</h2><blockquote><a href="https://link.segmentfault.com/?enc=Y7GLG0fFMZm4ZjVRKmrnWQ%3D%3D.vA0KVLf1P1x2Hop%2BIhewXBxPpUWTaVaLkya81J%2Fx9JuWDPVLzMa5B9aXEk0vo6jg" rel="nofollow">auto-log</a></blockquote>
如何动态修改 spring aop 切面信息?让自动日志输出框架更好用
https://segmentfault.com/a/1190000044038781
2023-07-22T20:55:58+08:00
2023-07-22T20:55:58+08:00
老马啸西风
https://segmentfault.com/u/houbinbin
1
<h2>业务背景</h2><p>很久以前开源了一款 <a href="https://link.segmentfault.com/?enc=OyHiSmesXZzAXzAxZl2RYQ%3D%3D.28qw4e%2B6Cs3v4prZO7SY41HDVlmUve2O8JnvIcut6oU1mCl2hvD7RaQ2pbE62ioz" rel="nofollow">auto-log</a> 自动日志打印框架。</p><p>其中对于 spring 项目,默认实现了基于 aop 切面的日志输出。</p><p>但是发现一个问题,如果切面定义为全切范围过大,于是 v0.2 版本就是基于注解 <code>@AutoLog</code> 实现的。</p><p>只有指定注解的类或者方法才会生效,但是这样使用起来很不方便。</p><p>如何才能动态指定 pointcut,让用户使用时可以自定义切面范围呢?</p><p><img src="/img/remote/1460000044038783" alt="在这里插入图片描述" title="在这里插入图片描述"></p><h2>自定义注解切面原理</h2><h3>常规 aop 方式</h3><pre><code class="java">@Aspect
@Component
@EnableAspectJAutoProxy
@Deprecated
public class AutoLogAop {
@Pointcut("@within(com.github.houbb.auto.log.annotation.AutoLog)" +
"|| @annotation(com.github.houbb.auto.log.annotation.AutoLog)")
public void autoLogPointcut() {
}
/**
* 执行核心方法
*
* 相当于 MethodInterceptor
*
* @param point 切点
* @return 结果
* @throws Throwable 异常信息
* @since 0.0.3
*/
@Around("autoLogPointcut()")
public Object around(ProceedingJoinPoint point) throws Throwable {
// 日志增强逻辑
}
}</code></pre><p>发现这里的 <code>@Pointcut</code> 注解属性是一个常量,无法方便地动态修改。</p><p>于是去查资料,找到了另一种更加灵活的方式。</p><h3>可以指定 pointcut 的方式</h3><p>我们通过 <code>@Value</code> 获取属性配置的切面值,给定默认值。这样用户就可以很方便的自定义。</p><pre><code class="java">/**
* 动态配置的切面
* 自动日志输出 aop
* @author binbin.hou
* @since 0.3.0
*/
@Configuration
@Aspect
//@EnableAspectJAutoProxy
public class AutoLogDynamicPointcut {
/**
* 切面设置,直接和 spring 的配置对应 ${},可以从 properties 或者配置中心读取。更加灵活
*/
@Value("${auto.log.pointcut:@within(com.github.houbb.auto.log.annotation.AutoLog)||@annotation(com.github.houbb.auto.log.annotation.AutoLog)}")
private String pointcut;
@Bean("autoLogPointcutAdvisor")
public AspectJExpressionPointcutAdvisor autoLogPointcutAdvisor() {
AspectJExpressionPointcutAdvisor advisor = new AspectJExpressionPointcutAdvisor();
advisor.setExpression(pointcut);
advisor.setAdvice(new AutoLogAdvice());
return advisor;
}
}</code></pre><p>当然,这里的 Advice 和以前的 aop 不同,需要重新进行实现。</p><h4>AutoLogAdvice</h4><p>只需要实现 MethodInterceptor 接口即可。</p><pre><code class="java">/**
* 切面拦截器
*
* @author binbin.hou
* @since 0.3.0
*/
public class AutoLogAdvice implements MethodInterceptor {
@Override
public Object invoke(MethodInvocation methodInvocation) throws Throwable {
// 增强逻辑
}
}</code></pre><p>介绍完了原理,我们一起来看下改进后的日志打印组件的效果。</p><h2>spring 整合使用</h2><p>完整示例参考 <a href="https://link.segmentfault.com/?enc=S%2FX9aA5lcXUhRq4qPOhbXw%3D%3D.iV8kL08ZPmFEEn0RD1w7n76IFVjKX5chrE2yjFe64YtGY038My1n0UjZTUNGO1SZfvD9aOmz5sJOFQmFiR792vvhedUk5wq0dPyaojircw64XFPy0KY4MSxd%2F5wKpLCN6KQxbYUWSQkBzDIVP1%2BCJC2UaXHxQcqv2hFTgRyr2MjcfNgaXjhY81qtRFKtGrlq" rel="nofollow">SpringServiceTest</a></p><h3>maven 引入</h3><pre><code class="xml"><dependency>
<groupId>com.github.houbb</groupId>
<artifactId>auto-log-spring</artifactId>
<version>0.3.0</version>
</dependency></code></pre><h3>注解声明</h3><p>使用 <code>@EnableAutoLog</code> 启用自动日志输出</p><pre><code class="java">@Configurable
@ComponentScan(basePackages = "com.github.houbb.auto.log.test.service")
@EnableAutoLog
public class SpringConfig {
}</code></pre><h3>测试代码</h3><pre><code class="java">@ContextConfiguration(classes = SpringConfig.class)
@RunWith(SpringJUnit4ClassRunner.class)
public class SpringServiceTest {
@Autowired
private UserService userService;
@Test
public void queryLogTest() {
userService.queryLog("1");
}
}</code></pre><ul><li>输出结果</li></ul><pre><code>信息: public java.lang.String com.github.houbb.auto.log.test.service.impl.UserServiceImpl.queryLog(java.lang.String) param is [1]
五月 30, 2020 12:17:51 下午 com.github.houbb.auto.log.core.support.interceptor.AutoLogMethodInterceptor info
信息: public java.lang.String com.github.houbb.auto.log.test.service.impl.UserServiceImpl.queryLog(java.lang.String) result is result-1
五月 30, 2020 12:17:51 下午 org.springframework.context.support.GenericApplicationContext doClose</code></pre><h3>切面自定义</h3><h4>原理解释</h4><p>spring aop 的切面读取自 <code>@Value("${auto.log.pointcut}")</code>,默认为值 <code>@within(com.github.houbb.auto.log.annotation.AutoLog)||@annotation(com.github.houbb.auto.log.annotation.AutoLog)</code></p><p>也就是默认是读取被 <code>@AutoLog</code> 指定的方法或者类。</p><p>当然,这并不够方便,我们希望可以想平时写 aop 注解一样,指定 spring aop 的扫描范围,直接在 spring 中指定一下 <code>auto.log.pointcut</code> 的属性值即可。</p><h4>测试例子</h4><blockquote><a href="https://link.segmentfault.com/?enc=KZa%2Fp%2FgdSpQOcSbHDWlkiA%3D%3D.3LgZe95Y5X%2FJlGbyFYyZd63FR7EM9tt8IsLXzgkQogIrG9vqkbxLfhrvkpOW8oAikQrOMI6lgcVO7Cm8ZBDWL5I7UysrBt4%2FW6b4Ba4NhE6KM54jSdsiGUk3vVBMsM9aBEiMiSf87T8Nrj4tXV%2BOD3bTPNpqnF6t%2B0u6r14uLr%2FtIBoyedlIgULOuL3OWh%2F5" rel="nofollow">完整测试代码</a></blockquote><p>我们在配置文件 <code>autoLogConfig.properties</code> 中自定义下包扫描的范围:</p><pre><code>auto.log.pointcut=execution(* com.github.houbb.auto.log.test.dynamic.service.MyAddressService.*(..))</code></pre><p>自定义测试 service</p><pre><code class="java">package com.github.houbb.auto.log.test.dynamic.service;
import org.springframework.stereotype.Service;
@Service
public class MyAddressService {
public String queryAddress(String id) {
return "address-" + id;
}
}</code></pre><p>自定义 spring 配置,指定我们定义的配置文件。springboot 啥的,可以直接放在 application.properties 中指定,此处仅作为演示。</p><pre><code class="java">@Configurable
@ComponentScan(basePackages = "com.github.houbb.auto.log.test.dynamic.service")
@EnableAutoLog
@PropertySource("classpath:autoLogConfig.properties")
public class SpringDynamicConfig {
}</code></pre><p>测试</p><pre><code class="java">@ContextConfiguration(classes = SpringDynamicConfig.class)
@RunWith(SpringJUnit4ClassRunner.class)
public class SpringDynamicServiceTest {
@Autowired
private MyAddressService myAddressService;
@Autowired
private MyUserService myUserService;
@Test
public void queryUserTest() {
// 不会被日志拦截
myUserService.queryUser("1");
}
@Test
public void queryAddressTest() {
// 会被日志拦截
myAddressService.queryAddress("1");
}
}</code></pre><h2>开源地址</h2><p>为了便于大家学习,项目已开源。</p><blockquote><p>Github: <a href="https://link.segmentfault.com/?enc=zC%2Bbe5PzivY7CMMt0PoOSw%3D%3D.BTtlCi5vH8rx8%2B0HMB22M3nre3GOsZmeuRMI18RJe8HtLRhNAP8k8kycwKvM0jYM" rel="nofollow">https://github.com/houbb/auto-log</a></p><p>Gitee: <a href="https://link.segmentfault.com/?enc=RVqe7sqL42FcVonz8Rn%2BhQ%3D%3D.34kQV7GXY5eP5RfGoSw5CsKD5UQQEtvMEviqkPV0MJRRcjVwJG4JSmAw6CjDLAw%2F" rel="nofollow">https://gitee.com/houbinbin/auto-log</a></p></blockquote><h2>小结</h2><p>这个项目很长一段时间拘泥于注解的方式,我个人用起来也不是很方便。</p><p>最近才想到了改进的方法,人还是要不断学习进步。</p><p>关于日志最近还学到了 aspect 的编译时增强,和基于 agent 的运行时增强,这 2 种方式都很有趣,有机会会做学习记录。</p>
基于 log4j2 插件实现统一日志脱敏,性能远超正则替换
https://segmentfault.com/a/1190000043867413
2023-06-05T23:43:52+08:00
2023-06-05T23:43:52+08:00
老马啸西风
https://segmentfault.com/u/houbinbin
0
<h2>前言</h2><p><a href="https://link.segmentfault.com/?enc=QLLn7py8kZTY%2BZsi%2FnmMWw%3D%3D.yhiW2prP8vHT2wtHcgUMR%2FY3c5MPfS%2BqDt7xi4EwUeWIPFvWSt%2BnIs0ylQkVJG7CuppPp9UAs%2BoHVPw%2BvhIPng%3D%3D" rel="nofollow">金融用户敏感数据如何优雅地实现脱敏?</a></p><p><a href="https://link.segmentfault.com/?enc=I2qNK5NPvRzosvhr7Yl5kA%3D%3D.lq9D6U1x8paAzqgr%2BQMnk1GUowEBZ6uuUZsHdEcIr1uW4wZ4Ll%2FtJlWw1tyiVvCHp7WwJ%2BuBv2QWEtRTt1yU3Q%3D%3D" rel="nofollow">日志脱敏之后,无法根据信息快速定位怎么办?</a></p><p>经过了这两篇文章之后,我们对日志脱敏应该有了一定的理解。</p><p>但是实际项目中,我们遇到的情况往往更加复杂:</p><p>1)项目的 java bean 定义不规范,大量接口使用 map。</p><p>2)历史项目众多,改造成本巨大。</p><p>种种原因,导致使用注解的方式耗费大量的时间。但是一般给我们改造的时间是有限的。</p><p>那么,有没有一种方法可以统一对敏感信息进行脱敏处理呢?</p><p>答案是有的,我们可以基于 log4j2 实现自己的脱敏策略,统一实现日志的脱敏。</p><p><img src="/img/remote/1460000043867415" alt="在这里插入图片描述" title="在这里插入图片描述"></p><h3>log4j2 Rewrite</h3><p>我们可以基于 log4j2 RewritePolicy 统一使用脱敏策略。</p><p>本项目自 V1.2.0 添加对应支持,后续将提升对应的可拓展性。</p><p>说明:如果使用 slf4j 接口,实现为 log4j2 时也是支持的。</p><h2>使用入门</h2><h3>maven 引入</h3><p>引入核心脱敏包。</p><pre><code class="xml"><dependency>
<groupId>com.github.houbb</groupId>
<artifactId>sensitive-log4j2</artifactId>
<version>1.2.1</version>
</dependency></code></pre><p>其他的一般项目中也有,如 log4j2 包:</p><pre><code class="xml"><dependency>
<groupId>org.apache.logging.log4j</groupId>
<artifactId>log4j-api</artifactId>
<version>${log4j2.version}</version>
</dependency>
<dependency>
<groupId>org.apache.logging.log4j</groupId>
<artifactId>log4j-core</artifactId>
<version>${log4j2.version}</version>
</dependency></code></pre><h3>log4j2.xml 配置</h3><p>例子如下:</p><pre><code class="xml"><?xml version="1.0" encoding="UTF-8"?>
<Configuration status="WARN" packages = "com.github.houbb.sensitive.log4j2.rewrite">
<Appenders>
<Console name="Console" target="SYSTEM_OUT">
</Console>
<Rewrite name="rewrite">
<AppenderRef ref="Console"/>
<SensitiveRewritePolicy/>
</Rewrite>
</Appenders>
<Loggers>
<Root level="DEBUG">
<AppenderRef ref="rewrite" />
</Root>
</Loggers>
</Configuration></code></pre><p>几个步骤:</p><ol><li>指定 package 为 <code>packages = "com.github.houbb.sensitive.log4j2.rewrite"</code></li><li>按照 log4j2 Rewrite 规范,指定重写策略为 <code>SensitiveRewritePolicy</code></li><li>输出时,直接指定为对应的重写之后的结果 <code><AppenderRef ref="rewrite" /></code></li></ol><h3>测试</h3><p>正常的日志打印:</p><pre><code class="java">private static final String TEST_LOG = "mobile:13088887777; bankCard:6217004470007335024, email:mahuateng@qq.com, amount:123.00, " +
"IdNo:340110199801016666, name1:李明, name2:李晓明, name3:李泽明天, name4:山东小栗旬" +
", birthday:20220517, GPS:120.882222, IPV4:127.0.0.1, address:中国上海市徐汇区888号;";
logger.info(TEST_LOG);</code></pre><p>自动脱敏效果如下:</p><pre><code>mobile:130****7777|9FC4D36D63D2B6DC5AE1297544FBC5A2; bankCard:6217***********5024|444F49289B30944AB8C6C856AEA21180, email:mahu*****@qq.com|897915594C94D981BA86C9E83ADD449C, amount:123.00, IdNo:3****************6|F9F05E4ABB3591FC8EA481E8DE1FA4D6, name1:李*|15095D14367F7F02655030D498A4BA03, name2:李**|035E3C0D1A0410367FE6EB8335B2BFDE, name3:李泽**|B87138E5E80AEC87D2581A25CAA3809D, name4:山东***|6F2178D34BC7DD0A07936B5AFF39A16F, birthday:********|1F88D983FAFC50022651122B42F084A0, GPS:**********|E281A9A52DE915154285148D68872CA2, IPV4:127******|F528764D624DB129B32C21FBCA0CB8D6, address:中国上海市徐******|821A601949B1BD18DCBAAE27F2E27147;</code></pre><p>ps: 这里是为了演示各种效果,实际默认对应为 1,2,3,4 这几种策略。</p><h3>log4j2 配置定制化</h3><p>为了满足各种用户的场景,在 V1.2.1 引入了 SensitiveRewritePolicy 策略的可配置化。</p><h4>默认配置</h4><p>log4j2 配置中,<code>SensitiveRewritePolicy</code> 配置默认等价于</p><pre><code class="xml"><SensitiveRewritePolicy
prefix=":=&apos;&quot;"
scanList = "1,2,3,4"
replaceList = "1,2,3,4"
defaultReplace = "12"
replaceHash = "md5"
/></code></pre><h4>属性说明</h4><p>SensitiveRewritePolicy 策略的属性说明。</p><table><thead><tr><th align="left">属性</th><th align="left">说明</th><th align="left">默认值</th><th align="left">备注</th></tr></thead><tbody><tr><td align="left">prefix</td><td align="left">需要脱敏信息的匹配前缀</td><td align="left"><code>:='"</code></td><td align="left">降低误判率</td></tr><tr><td align="left">replaceHash</td><td align="left">哈希策略模式</td><td align="left"><code>md5</code></td><td align="left">支持 md5/none 两种模式</td></tr><tr><td align="left">scanList</td><td align="left">敏感扫描策略列表</td><td align="left"><code>1,2,3,4</code></td><td align="left">1~10 内置的10种敏感信息扫描策略,多个用逗号隔开</td></tr><tr><td align="left">replaceList</td><td align="left">敏感替换策略列表</td><td align="left"><code>1,2,3,4</code></td><td align="left">1~10 内置的10种敏感信息替换策略,多个用逗号隔开</td></tr><tr><td align="left">defaultReplace</td><td align="left">敏感替换默认策略</td><td align="left"><code>12</code></td><td align="left">1~13 内置的13种敏感信息替换策略,指定一个。当列表没有匹配时,默认使用这个</td></tr></tbody></table><p>其中 1-13 的内置策略说明如下:</p><table><thead><tr><th align="left">策略标识</th><th align="left">说明</th></tr></thead><tbody><tr><td align="left">1</td><td align="left">手机号</td></tr><tr><td align="left">2</td><td align="left">身份证</td></tr><tr><td align="left">3</td><td align="left">银行卡</td></tr><tr><td align="left">4</td><td align="left">邮箱</td></tr><tr><td align="left">5</td><td align="left">中国人名</td></tr><tr><td align="left">6</td><td align="left">出生日期</td></tr><tr><td align="left">7</td><td align="left">GPS</td></tr><tr><td align="left">8</td><td align="left">IPV4</td></tr><tr><td align="left">9</td><td align="left">地址</td></tr><tr><td align="left">10</td><td align="left">护照</td></tr><tr><td align="left">11</td><td align="left">匹配任意不掩盖</td></tr><tr><td align="left">12</td><td align="left">匹配任意半掩盖</td></tr><tr><td align="left">13</td><td align="left">匹配任意全掩盖</td></tr></tbody></table><h4>不足之处</h4><p>这里的策略自定义和 log4j2 的插件化比起来,确实算不上强大,但是可以满足 99% 的脱敏场景。</p><p>后续有时间考虑类似 log4j2 的 plugins 思想,实现更加灵活的自定义策略。</p><h2>性能</h2><p>正则的替换可能会导致 cpu 飙升等问题,替换的策略也有限制。</p><p>实现的底层不是基于正则的,性能要远高于正则,大概是 2 倍左右,符合企业级应用性能。</p><p>后续将添加对应的 benchmark。</p><h2>开源地址</h2><p>为了便于大家学习,项目已开源</p><blockquote><a href="https://link.segmentfault.com/?enc=JeDsJwx1vVJ7m1%2BMGjq%2BHw%3D%3D.dPsmeHkHpye1VVlHxCvjmnFm9ordcjCyVo6zt%2FbwYMNwHKyYGknpse7PgyrhmyqY" rel="nofollow">https://github.com/houbb/sensitive</a></blockquote><h2>小结</h2><p>实际项目中,建议二者结合使用。</p><p>基于 log4j2 的方式统一处理非常方便,但是是性能和准确性要有一定的折中。</p><p>如果是新项目,建议使用注解的方式,通过日志标准规范开发,后续拓展性也更加灵活。</p>
日志脱敏之后,无法根据信息快速定位怎么办?
https://segmentfault.com/a/1190000043855850
2023-06-01T21:15:22+08:00
2023-06-01T21:15:22+08:00
老马啸西风
https://segmentfault.com/u/houbinbin
0
<h2>日志脱敏之殇</h2><p>小明同学在一家金融公司上班,为了满足安全监管要求,最近天天忙着做日志脱敏。</p><p>无意间看到了一篇文章<a href="https://link.segmentfault.com/?enc=pdqcgD1wIfSvh8p9BUtqMQ%3D%3D.y8kjH7Egh72P3HIiqeaNDuHmhrDA6T%2BkmCRV%2FTWUTXDaD1fxCp7z%2FLXKai1%2F5vXQ8Y2Vu%2FCE2NDBpPbIlywCaA%3D%3D" rel="nofollow">金融用户敏感数据如何优雅地实现脱敏?</a> 感觉写的不错,用起来也很方便。</p><p>不过日志脱敏之后,新的问题就诞生了:日志脱敏之后,很多问题无法定位。</p><p>比如身份证号日志中看到的是 <code>3****************8</code>,业务方给一个身份证号也没法查日志。这可怎么办?</p><p><img src="/img/remote/1460000043855852" alt="在这里插入图片描述" title="在这里插入图片描述"></p><h2>安全与数据唯一性</h2><p>类似于数据库中敏感信息的存储,一般都会有一个哈希值,用来定位数据信息,同时保障安全。</p><p>那么日志中是否也可以使用类似的方式呢?</p><p>说干就干,小明在开源项目 <a href="https://link.segmentfault.com/?enc=GpHJJ%2Fm5Qx6YdN7YPgdkxQ%3D%3D.FH3Lcf1fxb9IH3TgERKjhz%2FvAcQaGaAyXD6yt3xejuPIzOZ%2F%2Bb5YCG4sFy5ZuRGf" rel="nofollow">sensitive</a> 基础上,添加了对应的哈希实现。</p><h2>使用入门</h2><h3>开源地址</h3><blockquote><a href="https://link.segmentfault.com/?enc=2fW15uLDeT1yh%2BeGv1%2Bfjg%3D%3D.xqUYfzEygbmo8LpirtKX5U2ER3r1jQIX6xWG%2F2XDJPCjBGOS4A6PX8N4bz%2F3erSn" rel="nofollow">https://github.com/houbb/sensitive</a></blockquote><h3>使用方式</h3><p>1)maven 引入</p><pre><code class="xml"><dependency>
<groupId>com.github.houbb</groupId>
<artifactId>sensitive-core</artifactId>
<version>1.1.0</version>
</dependency></code></pre><p>2)引导类指定</p><pre><code class="java">SensitiveBs.newInstance()
.hash(Hashes.md5())</code></pre><p>将哈希策略指定为 md5</p><p>3)功能测试</p><pre><code class="java">final SensitiveBs sensitiveBs = SensitiveBs.newInstance()
.hash(Hashes.md5());
User sensitiveUser = sensitiveBs.desCopy(user);
String sensitiveJson = sensitiveBs.desJson(user);
Assert.assertEquals(sensitiveStr, sensitiveUser.toString());
Assert.assertEquals(originalStr, user.toString());
Assert.assertEquals(expectJson, sensitiveJson);</code></pre><p>可以把如下的对象</p><pre><code>User{username='脱敏君', idCard='123456190001011234', password='1234567', email='12345@qq.com', phone='18888888888'}</code></pre><p>直接脱敏为:</p><pre><code>User{username='脱**|00871641C1724BB717DD01E7E5F7D98A', idCard='123456**********34|1421E4C0F5BF57D3CC557CFC3D667C4E', password='null', email='12******.com|6EAA6A25C8D832B63429C1BEF149109C', phone='1888****888|5425DE6EC14A0722EC09A6C2E72AAE18'}</code></pre><p>这样就可以通过明文,获取对应的哈希值,然后搜索日志了。</p><h2>新的问题</h2><p>不过小明还是觉得不是很满意,因为有很多系统是已经存在的。</p><p>如果全部用注解的方式实现,就会很麻烦,也很难推动。</p><p>应该怎么实现呢?</p><p>小伙伴们有什么好的思路?欢迎评论区留言</p>
金融用户敏感数据如何优雅地实现脱敏?
https://segmentfault.com/a/1190000043851890
2023-05-31T20:59:23+08:00
2023-05-31T20:59:23+08:00
老马啸西风
https://segmentfault.com/u/houbinbin
0
<h2>项目介绍</h2><p>日志脱敏是常见的安全需求。普通的基于工具类方法的方式,对代码的入侵性太强,编写起来又特别麻烦。</p><p><a href="https://link.segmentfault.com/?enc=y%2BMi98WM4nYSCRv0HT4%2FQg%3D%3D.MpiZa8VHeM7OBNJhTRas3xpz7Ar18kh3iDATg7D4y8g8CK74H6X1p15lM6wdkwSM" rel="nofollow">sensitive</a> 提供了基于注解的方式,并且内置了常见的脱敏方式,便于开发。</p><h3>日志脱敏</h3><p>为了金融交易的安全性,国家强制规定对于以下信息是要日志脱敏的:</p><ol><li>用户名</li><li>手机号</li><li>邮箱</li><li>银行卡号</li><li>密码</li><li>身份证号</li></ol><h3>持久化加密</h3><p>存储的时候上面的信息都需要加密,密码为不可逆加密,其他为可逆加密。</p><p>类似的功能有很多。不在本系统的解决范围内。</p><h2>特性</h2><ol><li>基于注解的日志脱敏。</li><li>可以自定义策略实现,策略生效条件。</li><li>内置常见的十几种脱敏内置方案。</li><li>java 深拷贝,且原始对象不用实现任何接口。</li><li>支持用户自定义注解。</li><li>支持基于 FastJSON 直接生成脱敏后的 json</li></ol><h3>变更日志</h3><blockquote><a href="https://link.segmentfault.com/?enc=9yyX8P74MDGYYOAcPKBgSQ%3D%3D.8BfuC9I4VfLslPZERSy3MlBYFberVKObHaQsblC3NBUJz6a53DHBKaxdnnpMDWr%2B%2BoFII0LpVyFqz26TMsEf%2Fg%3D%3D" rel="nofollow">变更日志</a></blockquote><h2>快速开始</h2><h3>环境准备</h3><p>JDK 7+</p><p>Maven 3.x</p><h3>maven 导入</h3><pre><code class="xml"><dependency>
<groupId>com.github.houbb</groupId>
<artifactId>sensitive-core</artifactId>
<version>1.0.0</version>
</dependency></code></pre><h3>核心 api 简介</h3><p><code>SensitiveUtil</code> 工具类的核心方法列表如下:</p><table><thead><tr><th align="left">序号</th><th align="left">方法</th><th align="left">参数</th><th align="left">结果</th><th align="left">说明</th></tr></thead><tbody><tr><td align="left">1</td><td align="left">desCopy()</td><td align="left">目标对象</td><td align="left">深度拷贝脱敏对象</td><td align="left">适应性更强</td></tr><tr><td align="left">2</td><td align="left">desJson()</td><td align="left">目标对象</td><td align="left">脱敏对象 json</td><td align="left">性能较好</td></tr><tr><td align="left">3</td><td align="left">desCopyCollection()</td><td align="left">目标对象集合</td><td align="left">深度拷贝脱敏对象集合</td><td align="left"> </td></tr><tr><td align="left">4</td><td align="left">desJsonCollection()</td><td align="left">目标对象集合</td><td align="left">脱敏对象 json 集合</td><td align="left"> </td></tr></tbody></table><h3>定义对象</h3><ul><li>UserAnnotationBean.java</li></ul><p>通过注解,指定每一个字段的脱敏策略。</p><pre><code class="java">public class UserAnnotationBean {
@SensitiveStrategyChineseName
private String username;
@SensitiveStrategyPassword
private String password;
@SensitiveStrategyPassport
private String passport;
@SensitiveStrategyIdNo
private String idNo;
@SensitiveStrategyCardId
private String bandCardId;
@SensitiveStrategyPhone
private String phone;
@SensitiveStrategyEmail
private String email;
@SensitiveStrategyAddress
private String address;
@SensitiveStrategyBirthday
private String birthday;
@SensitiveStrategyGps
private String gps;
@SensitiveStrategyIp
private String ip;
@SensitiveStrategyMaskAll
private String maskAll;
@SensitiveStrategyMaskHalf
private String maskHalf;
@SensitiveStrategyMaskRange
private String maskRange;
//Getter & Setter
//toString()
}</code></pre><ul><li>数据准备</li></ul><p>构建一个最简单的测试对象:</p><pre><code class="java">UserAnnotationBean bean = new UserAnnotationBean();
bean.setUsername("张三");
bean.setPassword("123456");
bean.setPassport("CN1234567");
bean.setPhone("13066668888");
bean.setAddress("中国上海市浦东新区外滩18号");
bean.setEmail("whatanice@code.com");
bean.setBirthday("20220831");
bean.setGps("66.888888");
bean.setIp("127.0.0.1");
bean.setMaskAll("可恶啊我会被全部掩盖");
bean.setMaskHalf("还好我只会被掩盖一半");
bean.setMaskRange("我比较灵活指定掩盖范围");
bean.setBandCardId("666123456789066");
bean.setIdNo("360123202306018888");</code></pre><ul><li>测试代码</li></ul><pre><code>final String originalStr = "UserAnnotationBean{username='张三', password='123456', passport='CN1234567', idNo='360123202306018888', bandCardId='666123456789066', phone='13066668888', email='whatanice@code.com', address='中国上海市浦东新区外滩18号', birthday='20220831', gps='66.888888', ip='127.0.0.1', maskAll='可恶啊我会被全部掩盖', maskHalf='还好我只会被掩盖一半', maskRange='我比较灵活指定掩盖范围'}";
final String sensitiveStr = "UserAnnotationBean{username='张*', password='null', passport='CN*****67', idNo='3****************8', bandCardId='666123*******66', phone='1306****888', email='wh************.com', address='中国上海********8号', birthday='20*****1', gps='66*****88', ip='127***0.1', maskAll='**********', maskHalf='还好我只会*****', maskRange='我*********围'}";
final String expectSensitiveJson = "{\"address\":\"中国上海********8号\",\"bandCardId\":\"666123*******66\",\"birthday\":\"20*****1\",\"email\":\"wh************.com\",\"gps\":\"66*****88\",\"idNo\":\"3****************8\",\"ip\":\"127***0.1\",\"maskAll\":\"**********\",\"maskHalf\":\"还好我只会*****\",\"maskRange\":\"我*********围\",\"passport\":\"CN*****67\",\"phone\":\"1306****888\",\"username\":\"张*\"}";
UserAnnotationBean sensitiveUser = SensitiveUtil.desCopy(bean);
Assert.assertEquals(sensitiveStr, sensitiveUser.toString());
Assert.assertEquals(originalStr, bean.toString());
String sensitiveJson = SensitiveUtil.desJson(bean);
Assert.assertEquals(expectSensitiveJson, sensitiveJson);</code></pre><p>我们可以直接利用 <code>sensitiveUser</code> 去打印日志信息,而这个对象对于代码其他流程不影响,我们依然可以使用原来的 <code>user</code> 对象。</p><p>当然,也可以使用 <code>sensitiveJson</code> 打印日志信息。</p><h2>@Sensitive 注解</h2><h3>说明</h3><p><code>@SensitiveStrategyChineseName</code> 这种注解是为了便于用户使用,本质上等价于 <code>@Sensitive(strategy = StrategyChineseName.class)</code>。</p><p><code>@Sensitive</code> 注解可以指定对应的脱敏策略。</p><h3>内置注解与映射</h3><table><thead><tr><th align="left">编号</th><th align="left">注解</th><th align="left">等价 @Sensitive</th><th align="left">备注</th></tr></thead><tbody><tr><td align="left">1</td><td align="left"><code>@SensitiveStrategyChineseName</code></td><td align="left"><code>@Sensitive(strategy = StrategyChineseName.class)</code></td><td align="left">中文名称脱敏</td></tr><tr><td align="left">2</td><td align="left"><code>@SensitiveStrategyPassword</code></td><td align="left"><code>@Sensitive(strategy = StrategyPassword.class)</code></td><td align="left">密码脱敏</td></tr><tr><td align="left">3</td><td align="left"><code>@SensitiveStrategyEmail</code></td><td align="left"><code>@Sensitive(strategy = StrategyEmail.class)</code></td><td align="left">email 脱敏</td></tr><tr><td align="left">4</td><td align="left"><code>@SensitiveStrategyCardId</code></td><td align="left"><code>@Sensitive(strategy = StrategyCardId.class)</code></td><td align="left">卡号脱敏</td></tr><tr><td align="left">5</td><td align="left"><code>@SensitiveStrategyPhone</code></td><td align="left"><code>@Sensitive(strategy = StrategyPhone.class)</code></td><td align="left">手机号脱敏</td></tr><tr><td align="left">6</td><td align="left"><code>@SensitiveStrategyIdNo</code></td><td align="left"><code>@Sensitive(strategy = StrategyIdNo.class)</code></td><td align="left">身份证脱敏</td></tr><tr><td align="left">6</td><td align="left"><code>@SensitiveStrategyAddress</code></td><td align="left"><code>@Sensitive(strategy = StrategyAddress.class)</code></td><td align="left">地址脱敏</td></tr><tr><td align="left">7</td><td align="left"><code>@SensitiveStrategyGps</code></td><td align="left"><code>@Sensitive(strategy = StrategyGps.class)</code></td><td align="left">GPS 脱敏</td></tr><tr><td align="left">8</td><td align="left"><code>@SensitiveStrategyIp</code></td><td align="left"><code>@Sensitive(strategy = StrategyIp.class)</code></td><td align="left">IP 脱敏</td></tr><tr><td align="left">9</td><td align="left"><code>@SensitiveStrategyBirthday</code></td><td align="left"><code>@Sensitive(strategy = StrategyBirthday.class)</code></td><td align="left">生日脱敏</td></tr><tr><td align="left">10</td><td align="left"><code>@SensitiveStrategyPassport</code></td><td align="left"><code>@Sensitive(strategy = StrategyPassport.class)</code></td><td align="left">护照脱敏</td></tr><tr><td align="left">11</td><td align="left"><code>@SensitiveStrategyMaskAll</code></td><td align="left"><code>@Sensitive(strategy = StrategyMaskAll.class)</code></td><td align="left">全部脱敏</td></tr><tr><td align="left">12</td><td align="left"><code>@SensitiveStrategyMaskHalf</code></td><td align="left"><code>@Sensitive(strategy = StrategyMaskHalf.class)</code></td><td align="left">一半脱敏</td></tr><tr><td align="left">13</td><td align="left"><code>@SensitiveStrategyMaskRange</code></td><td align="left"><code>@Sensitive(strategy = StrategyMaskRange.class)</code></td><td align="left">指定范围脱敏</td></tr></tbody></table><h3>@Sensitive 定义</h3><pre><code class="java">@Inherited
@Documented
@Target(ElementType.FIELD)
@Retention(RetentionPolicy.RUNTIME)
public @interface Sensitive {
/**
* 注解生效的条件
* @return 条件对应的实现类
*/
Class<? extends ICondition> condition() default ConditionAlwaysTrue.class;
/**
* 执行的策略
* @return 策略对应的类型
*/
Class<? extends IStrategy> strategy();
}</code></pre><h3>与 @Sensitive 混合使用</h3><p>如果你将新增的注解 <code>@SensitiveStrategyChineseName</code> 与 <code>@Sensitive</code> 同时在一个字段上使用。</p><p>为了简化逻辑,优先选择执行 <code>@Sensitive</code>,如果 <code>@Sensitive</code> 执行脱敏,<br>那么 <code>@SensitiveStrategyChineseName</code> 将不会生效。</p><p>如:</p><pre><code class="java">/**
* 测试字段
* 1.当多种注解混合的时候,为了简化逻辑,优先选择 @Sensitive 注解。
*/
@SensitiveStrategyChineseName
@Sensitive(strategy = StrategyPassword.class)
private String testField;</code></pre><h2>更多特性</h2><h3>自定义脱敏策略生效的场景</h3><p>默认情况下,我们指定的场景都是生效的。</p><p>但是你可能需要有些情况下不进行脱敏,比如有些用户密码为 123456,你觉得这种用户不脱敏也罢。</p><ul><li>UserPasswordCondition.java</li></ul><pre><code class="java">@Sensitive(condition = ConditionFooPassword.class, strategy = StrategyPassword.class)
private String password;</code></pre><p>其他保持不变,我们指定了一个 condition,实现如下:</p><ul><li>ConditionFooPassword.java</li></ul><pre><code class="java">public class ConditionFooPassword implements ICondition {
@Override
public boolean valid(IContext context) {
try {
Field field = context.getCurrentField();
final Object currentObj = context.getCurrentObject();
final String password = (String) field.get(currentObj);
return !password.equals("123456");
} catch (IllegalAccessException e) {
throw new RuntimeException(e);
}
}
}</code></pre><p>也就是只有当密码不是 123456 时密码脱敏策略才会生效。</p><h3>属性为集合或者对象</h3><p>如果某个属性是单个集合或者对象,则需要使用注解 <code>@SensitiveEntry</code>。</p><ul><li>放在集合属性上,且属性为普通对象</li></ul><p>会遍历每一个属性,执行上面的脱敏策略。</p><ul><li>放在对象属性上</li></ul><p>会处理对象中各个字段上的脱敏注解信息。</p><ul><li>放在集合属性上,且属性为对象</li></ul><p>遍历每一个对象,处理对象中各个字段上的脱敏注解信息。</p><h4>放在集合属性上,且属性为普通对象</h4><ul><li>UserEntryBaseType.java</li></ul><p>作为演示,集合中为普通的字符串。</p><pre><code class="java">public class UserEntryBaseType {
@SensitiveEntry
@Sensitive(strategy = StrategyChineseName.class)
private List<String> chineseNameList;
@SensitiveEntry
@Sensitive(strategy = StrategyChineseName.class)
private String[] chineseNameArray;
//Getter & Setter & toString()
}</code></pre><h4>放在对象属性上</h4><p>例子如下:</p><pre><code class="java">public class UserEntryObject {
@SensitiveEntry
private User user;
@SensitiveEntry
private List<User> userList;
@SensitiveEntry
private User[] userArray;
//...
}</code></pre><h2>自定义注解</h2><ul><li>v0.0.4 新增功能。允许功能自定义条件注解和策略注解。</li><li>v0.0.11 新增功能。允许功能自定义级联脱敏注解。</li></ul><h3>案例1</h3><h4>自定义密码脱敏策略&自定义密码脱敏策略生效条件</h4><ul><li>策略脱敏</li></ul><pre><code class="java">/**
* 自定义密码脱敏策略
* @author binbin.hou
* date 2019/1/17
* @since 0.0.4
*/
@Inherited
@Documented
@Target(ElementType.FIELD)
@Retention(RetentionPolicy.RUNTIME)
@SensitiveStrategy(CustomPasswordStrategy.class)
public @interface SensitiveCustomPasswordStrategy {
}</code></pre><ul><li>脱敏生效条件</li></ul><pre><code class="java">/**
* 自定义密码脱敏策略生效条件
* @author binbin.hou
* date 2019/1/17
* @since 0.0.4
*/
@Inherited
@Documented
@Target(ElementType.FIELD)
@Retention(RetentionPolicy.RUNTIME)
@SensitiveCondition(ConditionFooPassword.class)
public @interface SensitiveCustomPasswordCondition{
}</code></pre><ul><li>TIPS</li></ul><p><code>@SensitiveStrategy</code> 策略单独使用的时候,默认是生效的。</p><p>如果有 <code>@SensitiveCondition</code> 注解,则只有当条件满足时,才会执行脱敏策略。</p><p><code>@SensitiveCondition</code> 只会对系统内置注解和自定义注解生效,因为 <code>@Sensitive</code> 有属于自己的策略生效条件。</p><ul><li>策略优先级</li></ul><p><code>@Sensitive</code> 优先生效,然后是系统内置注解,最后是用户自定义注解。</p><h4>对应的实现</h4><p>两个元注解 <code>@SensitiveStrategy</code>、<code>@SensitiveCondition</code> 分别指定了对应的实现。</p><ul><li>CustomPasswordStrategy.java</li></ul><pre><code class="java">public class CustomPasswordStrategy implements IStrategy {
@Override
public Object des(Object original, IContext context) {
return "**********************";
}
}</code></pre><ul><li>ConditionFooPassword.java</li></ul><pre><code class="java">/**
* 让这些 123456 的密码不进行脱敏
* @author binbin.hou
* date 2019/1/2
* @since 0.0.1
*/
public class ConditionFooPassword implements ICondition {
@Override
public boolean valid(IContext context) {
try {
Field field = context.getCurrentField();
final Object currentObj = context.getCurrentObject();
final String name = (String) field.get(currentObj);
return !name.equals("123456");
} catch (IllegalAccessException e) {
throw new RuntimeException(e);
}
}
}</code></pre><h4>定义测试对象</h4><p>定义一个使用自定义注解的对象。</p><pre><code class="java">public class CustomPasswordModel {
@SensitiveCustomPasswordCondition
@SensitiveCustomPasswordStrategy
private String password;
@SensitiveCustomPasswordCondition
@SensitiveStrategyPassword
private String fooPassword;
//其他方法
}</code></pre><h4>测试</h4><pre><code class="java">/**
* 自定义注解测试
*/
@Test
public void customAnnotationTest() {
final String originalStr = "CustomPasswordModel{password='hello', fooPassword='123456'}";
final String sensitiveStr = "CustomPasswordModel{password='**********************', fooPassword='123456'}";
CustomPasswordModel model = buildCustomPasswordModel();
Assert.assertEquals(originalStr, model.toString());
CustomPasswordModel sensitive = SensitiveUtil.desCopy(model);
Assert.assertEquals(sensitiveStr, sensitive.toString());
Assert.assertEquals(originalStr, model.toString());
}</code></pre><p>构建对象的方法如下:</p><pre><code class="java">/**
* 构建自定义密码对象
* @return 对象
*/
private CustomPasswordModel buildCustomPasswordModel(){
CustomPasswordModel model = new CustomPasswordModel();
model.setPassword("hello");
model.setFooPassword("123456");
return model;
}</code></pre><h3>案例2</h3><ul><li>v0.0.11 新增功能。允许功能自定义级联脱敏注解。</li></ul><h4>自定义级联脱敏注解</h4><ul><li>自定义级联脱敏注解</li></ul><p>可以根据自己的业务需要,在自定义的注解上使用 <code>@SensitiveEntry</code>。</p><p>使用方式保持和 <code>@SensitiveEntry</code> 一样即可。</p><pre><code class="java">/**
* 级联脱敏注解,如果对象中属性为另外一个对象(集合),则可以使用这个注解指定。
* <p>
* 1. 如果属性为 Iterable 的子类集合,则当做列表处理,遍历其中的对象
* 2. 如果是普通对象,则处理对象中的脱敏信息
* 3. 如果是普通字段/MAP,则不做处理
* @since 0.0.11
*/
@Inherited
@Documented
@Target(ElementType.FIELD)
@Retention(RetentionPolicy.RUNTIME)
@SensitiveEntry
public @interface SensitiveEntryCustom {
}</code></pre><h4>定义测试对象</h4><p>定义一个使用自定义注解的对象。</p><pre><code class="java">public class CustomUserEntryObject {
@SensitiveEntryCustom
private User user;
@SensitiveEntryCustom
private List<User> userList;
@SensitiveEntryCustom
private User[] userArray;
// 其他方法...
}</code></pre><h2>生成脱敏后的 JSON</h2><h3>说明</h3><p>为了避免生成中间脱敏对象,v0.0.6 之后直接支持生成脱敏后的 JSON。</p><h3>使用方法</h3><p>新增工具类方法,可以直接返回脱敏后的 JSON。</p><p>生成的 JSON 是脱敏的,原对象属性值不受影响。</p><pre><code class="java">public static String desJson(Object object)</code></pre><h3>注解的使用方式</h3><p>和 <code>SensitiveUtil.desCopy()</code> 完全一致。</p><h3>使用示例代码</h3><p>所有的测试案例中,都添加了对应的 <code>desJson(Object)</code> 测试代码,可以参考。</p><p>此处只展示最基本的使用。</p><pre><code class="java">final String originalStr = "SystemBuiltInAt{phone='18888888888', password='1234567', name='脱敏君', email='12345@qq.com', cardId='123456190001011234'}";
final String sensitiveJson = "{\"cardId\":\"123456**********34\",\"email\":\"12******.com\",\"name\":\"脱**\",\"phone\":\"1888****888\"}";
SystemBuiltInAt systemBuiltInAt = DataPrepareTest.buildSystemBuiltInAt();
Assert.assertEquals(sensitiveJson, SensitiveUtil.desJson(systemBuiltInAt));
Assert.assertEquals(originalStr, systemBuiltInAt.toString());</code></pre><h3>注意</h3><p>本次 JSON 脱敏基于 <a href="https://link.segmentfault.com/?enc=fWXemRARUEo3bZ3gnpdbJQ%3D%3D.%2BonhswQnhHgzNt5jLJeOrIWFkUf6465NnDoAK3a9ieH48AL%2BJrrE%2BZmrhLsteIsk" rel="nofollow">FastJSON</a>。</p><p>FastJSON 在序列化本身存在一定限制。当对象中有集合,集合中还是对象时,结果不尽如人意。</p><h4>示例代码</h4><p>本测试案例可见测试代码。</p><pre><code class="java">final String originalStr = "UserCollection{userList=[User{username='脱敏君', idCard='123456190001011234', password='1234567', email='12345@qq.com', phone='18888888888'}], userSet=[User{username='脱敏君', idCard='123456190001011234', password='1234567', email='12345@qq.com', phone='18888888888'}], userCollection=[User{username='脱敏君', idCard='123456190001011234', password='1234567', email='12345@qq.com', phone='18888888888'}], userMap={map=User{username='脱敏君', idCard='123456190001011234', password='1234567', email='12345@qq.com', phone='18888888888'}}}";
final String commonJson = "{\"userArray\":[{\"email\":\"12345@qq.com\",\"idCard\":\"123456190001011234\",\"password\":\"1234567\",\"phone\":\"18888888888\",\"username\":\"脱敏君\"}],\"userCollection\":[{\"$ref\":\"$.userArray[0]\"}],\"userList\":[{\"$ref\":\"$.userArray[0]\"}],\"userMap\":{\"map\":{\"$ref\":\"$.userArray[0]\"}},\"userSet\":[{\"$ref\":\"$.userArray[0]\"}]}";
final String sensitiveJson = "{\"userArray\":[{\"email\":\"12******.com\",\"idCard\":\"123456**********34\",\"phone\":\"1888****888\",\"username\":\"脱**\"}],\"userCollection\":[{\"$ref\":\"$.userArray[0]\"}],\"userList\":[{\"$ref\":\"$.userArray[0]\"}],\"userMap\":{\"map\":{\"$ref\":\"$.userArray[0]\"}},\"userSet\":[{\"$ref\":\"$.userArray[0]\"}]}";
UserCollection userCollection = DataPrepareTest.buildUserCollection();
Assert.assertEquals(commonJson, JSON.toJSONString(userCollection));
Assert.assertEquals(sensitiveJson, SensitiveUtil.desJson(userCollection));
Assert.assertEquals(originalStr, userCollection.toString());</code></pre><h4>解决方案</h4><p>如果有这种需求,建议使用原来的 <code>desCopy(Object)</code>。</p><h2>脱敏引导类</h2><p>为了配置的灵活性,引入了引导类。</p><h3>核心 api 简介</h3><p><code>SensitiveBs</code> 引导类的核心方法列表如下:</p><table><thead><tr><th align="left">序号</th><th align="left">方法</th><th align="left">参数</th><th align="left">结果</th><th align="left">说明</th></tr></thead><tbody><tr><td align="left">1</td><td align="left">desCopy()</td><td align="left">目标对象</td><td align="left">深度拷贝脱敏对象</td><td align="left">适应性更强</td></tr><tr><td align="left">2</td><td align="left">desJson()</td><td align="left">目标对象</td><td align="left">脱敏对象 json</td><td align="left">性能较好</td></tr></tbody></table><h3>使用示例</h3><p>使用方式和工具类一致,示意如下:</p><pre><code class="java">SensitiveBs.newInstance().desCopy(user);</code></pre><h3>配置深度拷贝实现</h3><p>默认的使用 FastJson 进行对象的深度拷贝,等价于:</p><pre><code class="java">SensitiveBs.newInstance()
.deepCopy(FastJsonDeepCopy.getInstance())
.desJson(user);</code></pre><p>参见 <a href="https://link.segmentfault.com/?enc=TSub9veEqI%2BrRP6L86kJHQ%3D%3D.8cpjRubD4geIBR%2FBZBIgpNTjcE05aV8k5zCLsMLyFiWRBp7K4604H8wKJKtdtXIdpnjBUEmtR5vobLNdqz%2B%2Bb5RDp%2B4V1C%2BvF%2F%2B%2F9%2FUJ7ZDicneEfNBxqRypmRoYRF2S%2FIjaI1e2rEfEy1yhnQIV4MKn55x20GLfN8YJr4RgNmeV2TmaK4QZiM6wZUCqam89" rel="nofollow">SensitiveBsTest.java</a></p><p>deepCopy 用于指定深度复制的具体实现,支持用户自定义。</p><h2>深度复制(DeepCopy)</h2><h3>说明</h3><p>深度复制可以保证我们日志输出对象脱敏,同时不影响正常业务代码的使用。</p><p>可以实现深度复制的方式有很多种,默认基于 <a href="https://link.segmentfault.com/?enc=9RtJNOZN7USVr5by%2Bz4y2Q%3D%3D.xhr47VpH8PzRiT5JAkvxD7cYtPNAbA0yrpqRQz%2B0HEz4Lzey6VIMS6jRBHiCS1E1" rel="nofollow">fastjson</a> 实现的。</p><p>为保证后续良性发展,v0.0.13 版本之后将深度复制接口抽离为单独的项目:</p><blockquote><a href="https://link.segmentfault.com/?enc=udrh6%2BAfowH1pEkOFM%2FTMw%3D%3D.dqE0HT%2F04jq7vfipTzaiiLdHmWxIBD%2F5WSLRO%2Fpg1vF9Ux66rfPS7gk9gc9gpLau" rel="nofollow">deep-copy</a></blockquote><h3>内置策略</h3><p>目前支持 6 种基于序列化实现的深度复制,便于用户替换使用。</p><p>每一种都可以单独使用,保证依赖更加轻量。</p><h3>自定义</h3><p>为满足不同场景的需求,深度复制策略支持用户自定义。</p><blockquote><a href="https://link.segmentfault.com/?enc=Bu0H8Cqk7lveYwmWIX7G5Q%3D%3D.bo%2FFDCPQLuF2k%2Brg%2BqwsSrTDJqzRvtqaR1X8V2Qr%2BpPlw5gkxV3qyoq%2F4xgOu1t%2FbaSX3ZtdGFSmKdv9Wdjcfw%3D%3D" rel="nofollow">自定义深度复制</a></blockquote><h2>开源地址</h2><blockquote><a href="https://link.segmentfault.com/?enc=NQnv61MN8rX6ZX3OSqYjpw%3D%3D.7pwEgaHgKCbVePKjhKcVTp52RN54EkWlJaB415mWKddJ1r1f7wUgI%2FFtrzPLyexr" rel="nofollow">https://github.com/houbb/sensitive </a></blockquote>
java 如何计算两个汉字的相似度?如何获得一个汉字的相似汉字?
https://segmentfault.com/a/1190000043653066
2023-04-12T17:38:12+08:00
2023-04-12T17:38:12+08:00
老马啸西风
https://segmentfault.com/u/houbinbin
0
<h2>计算汉字相似度</h2><h3>情景</h3><p>有时候我们希望计算两个汉字的相似度,比如文本的 OCR 等场景。用于识别纠正。</p><h3>实现</h3><p>引入 maven</p><pre><code class="xml"><dependency>
<groupId>com.github.houbb</groupId>
<artifactId>nlp-hanzi-similar</artifactId>
<version>1.3.0</version>
</dependency></code></pre><p>java 实现</p><pre><code class="java">double rate1 = HanziSimilarHelper.similar('末', '未');</code></pre><p>返回对应的相似度:</p><pre><code>0.9629629629629629</code></pre><h2>返回一个汉字的相似列表</h2><h3>情景</h3><p>找到相似的汉字,有很多有趣的场景。</p><h3>实现</h3><pre><code class="java">List<String> list = HanziSimilarHelper.similarList('爱');
Assert.assertEquals("[爰, 爯, 受, 爭, 妥, 憂, 李, 爳, 叐, 雙]", list.toString());</code></pre><h2>开源地址</h2><p>为了便于大家学习,上述代码已开源</p><blockquote><a href="https://link.segmentfault.com/?enc=XCF%2BZJg8GcaGAOFK6LeFfw%3D%3D.rNewtsE6mTb%2FVE4r%2BK%2BVn8U8fmN06IvcmsRBwCYZGiINJsoQ%2FvfnfOyrEHj95J2P" rel="nofollow">https://github.com/houbb/nlp-hanzi-similar</a></blockquote><h2>在线体验</h2><blockquote><a href="https://link.segmentfault.com/?enc=OdoaEQ5ODEymXrxhAsJSFQ%3D%3D.83g0Kk2%2FgWmMKoRVnkI72bdUgb6JnYUBZNQiIOLohRCEHIufGbTAdKnH4Z%2F9D4hPirDhYn5LCrifY0rgoo2e4A%3D%3D" rel="nofollow">在线体验</a></blockquote><h2>拓展阅读</h2><p><a href="https://link.segmentfault.com/?enc=IjoxT0H12vHm5C0aTtuPlA%3D%3D.Oi4zPXwgnmqRe6zw8arM2jKN1EEXcarRm%2BcXlvqgzCCF8JN5dcvEylrTwH7Kmpuy8BuJZ2E4WitsYkdeFVVyjw%3D%3D" rel="nofollow">NLP 中文形近字相似度计算思路</a></p><p><a href="https://link.segmentfault.com/?enc=SY%2Bt7%2BtNaUMwfbDwtMiEZA%3D%3D.P7VKvyd5d4nUrc%2FKLVLTKs%2FuVn2KbA4uQFgrLVbw2JPt9XfOTW73f%2B%2BT7HU7TOxQE7fLtTMUbCIZils6dgJEbg%3D%3D" rel="nofollow">中文形近字相似度算法实现,为汉字 NLP 尽一点绵薄之力</a></p><p><a href="https://link.segmentfault.com/?enc=w%2FgHJAaCTQq5bEKrgSAgtA%3D%3D.qVTArSwA6Mxa%2BQCwbQp5gJCBtP9VGNQ%2FJpG5HqvcF0Pl1zEZ81M63Ba0QBiaat5fKMtIw4hbmq1VlD7ZR1%2B84A%3D%3D" rel="nofollow">当代中国最贵的汉字是什么?</a></p><p><a href="https://link.segmentfault.com/?enc=cF2KROKuJBRSFPJvWnit6g%3D%3D.%2F%2BHZcMUACLIJerPr3%2FrRHTk4Xa6ZEyBKqPsIWcMUFlSRXQ5KU9uJjF%2F6C8Bt4GwKnfazKHPxnDyGOlNBc7b%2Bmw%3D%3D" rel="nofollow">NLP 开源形近字算法补完计划(完结篇)</a></p><p><a href="https://link.segmentfault.com/?enc=OY0gQyq56zhC%2B5oDAPNQ7A%3D%3D.0K37tRRcAuW%2FxFBARcE2b6t%2B%2Bg2PNyxKbGiuFsqF6sPB%2FxmESIJm%2F%2F%2FdRXowUxLd7id437mvCoEQV%2FpesuUdLA%3D%3D" rel="nofollow">NLP 开源形近字算法之形近字列表(番外篇)</a></p><p><a href="https://link.segmentfault.com/?enc=cAXaQTHlCZpSdZBHGxZksQ%3D%3D.hwSmh5LimefFsQbYraap3uCmPiNKwPYSXocV5swnvddUUZnJybsxP5SC7r1wUt07LMsd4ZWLtaMKIqlFNBKobw%3D%3D" rel="nofollow">开源项目在线化 中文繁简体转换/敏感词/拼音/分词/汉字相似度/markdown 目录</a></p>
和 chatgpt 聊了一会儿分布式锁 redis/zookeeper distributed lock
https://segmentfault.com/a/1190000043634743
2023-04-07T16:37:49+08:00
2023-04-07T16:37:49+08:00
老马啸西风
https://segmentfault.com/u/houbinbin
0
<h2>前言</h2><p>最近的 chatGPT 很火爆,听说取代程序员指日可待。</p><p>于是和 TA 聊了一会儿分布式锁,我的感受是,超过大部分程序员的水平。</p><p><img src="/img/remote/1460000043634745" alt="在这里插入图片描述" title="在这里插入图片描述"></p><h2>Q1: 谈一谈 java 通过 redis 实现分布式 锁</h2><p>chatGPT:</p><pre><code>Java通过Redis实现分布式锁,是在多个Java应用程序之间实现同步的一种方式。通过Redis的原子性和高性能,实现了分布式锁的可靠性和高效性。
具体实现步骤如下:
获取Redis连接:在Java应用程序中,使用Java Redis客户端连接到Redis数据库。
创建锁:Java应用程序向Redis数据库中写入一条记录,作为锁。
设置过期时间:为了避免死锁,需要为锁设置一个过期时间。
判断是否获取到锁:Java应用程序从Redis数据库中读取锁记录,判断当前是否已经有其他应用程序获取到了锁。如果没有,则当前应用程序获取到了锁,可以执行需要同步的操作。
释放锁:Java应用程序在同步操作完成后,从Redis数据库中删除锁记录,释放锁。
在实现分布式锁时,需要注意以下几点:
锁的名字应该是唯一的,并且易于识别和管理。
设置过期时间是为了避免锁一直被占用而无法释放。
应该采用原子性操作来实现锁,以避免出现多个线程同时访问锁而导致的并发问题。
需要考虑锁的持有时间,避免出现死锁的情况。
总之,通过Redis实现分布式锁可以很好地解决多个Java应用程序之间同步的问题。Java应用程序可以通过Redis实现高效、可靠的分布式锁,提高分布式应用程序的性能和可靠性。</code></pre><p>我们针对过期时间附加两个小问题。</p><h3>Q1.1 谈一谈 redis 分布式锁中,锁的过期时间应该设置多久才合理呢</h3><p>chatGPT:</p><pre><code>在 Redis 分布式锁中,锁的过期时间的设置需要考虑到两个方面的因素:锁的超时时间和业务处理时间。
如果锁的超时时间设置过短,那么在高并发的情况下,可能会出现多个进程同时获取到锁的情况,从而导致锁失效,出现并发问题。
因此,通常建议将锁的过期时间设置为稍微长一些,可以考虑设置为 30 秒到 1 分钟左右。
另外,还需要考虑业务处理时间。如果业务处理时间很长,而锁的过期时间很短,那么在业务处理期间锁失效,就会导致其他进程在锁被释放前获取到锁,从而导致并发问题。
因此,需要根据业务处理时间来合理地设置锁的过期时间,通常建议将锁的过期时间设置为业务处理时间的两倍以上。
需要注意的是,锁的过期时间设置过长也会带来风险,例如当获取锁的进程崩溃或被终止时,其他进程无法获取到锁,从而导致死锁问题。因此,在设置锁的过期时间时需要综合考虑以上因素,找到一个合理的平衡点。</code></pre><h3>Q1.2 如果系统正常业务 30秒处理完成,但是系统受到了攻击,导致处理时间变慢。这个时候,分布式锁的过期时间要怎么设置才合理呢?要求一定不能出错,涉及到金额</h3><p>chatGPT:</p><pre><code>如果系统受到攻击导致处理时间变慢,那么锁的过期时间要根据实际情况进行设置。通常建议将锁的过期时间设置为业务处理时间的两倍以上,但是如果系统受到攻击,处理时间远远超过了正常的业务处理时间,那么在这种情况下,就需要将锁的过期时间适当延长,以确保在极端情况下锁不会过早失效。
具体来说,可以考虑以下两种方案:
动态调整锁的过期时间:当系统受到攻击时,动态调整锁的过期时间。可以根据当前的业务处理时间来决定过期时间的长度,如果业务处理时间变长,就将过期时间适当延长。
采用异步处理的方式:将耗时较长的操作改为异步处理,从而避免阻塞主线程,减少对分布式锁的占用时间。在这种情况下,可以将锁的过期时间设置为正常的业务处理时间的两倍左右。
需要注意的是,在设置锁的过期时间时,一定要根据实际情况进行合理的设置。如果过期时间设置过短,会导致锁失效,出现并发问题;如果过期时间设置过长,会带来死锁的风险。因此,在设置锁的过期时间时,需要综合考虑业务处理时间和系统稳定性等因素,找到一个合理的平衡点。同时,在实际应用中,也可以结合监控系统和预警机制,及时发现和处理系统异常情况,保障系统的稳定性和安全性。</code></pre><p>说的倒也算不错,让他给一下代码。</p><h2>Q2: 给出具体的代码实现</h2><p>下面是Java通过Redis实现分布式锁的代码示例:</p><pre><code class="java">import redis.clients.jedis.Jedis;
import redis.clients.jedis.params.SetParams;
public class RedisLock {
private Jedis jedis;
private String lockKey;
private String lockValue;
private int expireTime;
public RedisLock(Jedis jedis, String lockKey, String lockValue, int expireTime) {
this.jedis = jedis;
this.lockKey = lockKey;
this.lockValue = lockValue;
this.expireTime = expireTime;
}
public boolean acquire() {
SetParams setParams = new SetParams();
setParams.nx();
setParams.ex(expireTime);
String result = jedis.set(lockKey, lockValue, setParams);
if ("OK".equals(result)) {
return true;
}
return false;
}
public boolean release() {
String luaScript = "if redis.call('get', KEYS[1]) == ARGV[1] then return redis.call('del', KEYS[1]) else return 0 end";
Object result = jedis.eval(luaScript, 1, lockKey, lockValue);
if ("1".equals(result.toString())) {
return true;
}
return false;
}
}</code></pre><p>chatGPT 的解释:</p><pre><code>在上面的代码中,我们定义了一个RedisLock类,用于封装获取和释放锁的操作。其中,acquire()方法用于获取锁,release()方法用于释放锁。
在获取锁时,我们使用Redis的set()方法设置一个键值对,并使用NX(不存在则创建)和EX(过期时间)两个参数来保证锁的原子性和可靠性。如果set()方法返回的结果为"OK",则表示获取到了锁,可以执行同步操作。
在释放锁时,我们使用Redis的eval()方法执行一个Lua脚本,判断当前锁的值是否为当前线程所持有的值。如果是,则调用Redis的del()方法删除该键值对,释放锁。
需要注意的是,在使用Redis实现分布式锁时,我们需要确保获取和释放锁的操作是原子性的。同时,我们也需要考虑锁的过期时间,避免出现死锁的情况。</code></pre><p>诚然,这个代码写的作为入门是够用了。</p><p>1) 但是还是有一些改进空间,比如 expireTime/lockKey/lockValue 应该作为方法的参数。</p><p><strong>其实 AI 无论怎么发展,我们还是需要知道底层的原理,不然都不知道如何改进</strong>。</p><p>对于程序员如此,对于画师也是如此。</p><p>2)这个是单点的算法,我们问一下如何解决。</p><h2>Q3:如果 redis 是多个节点,存在主节点重启,上面的方法存在什么问题?</h2><p>chatGPT:</p><pre><code>如果 Redis 是多个节点,存在主节点重启,上面的代码实现可能会出现如下问题:
主节点重启后,由于获取锁和释放锁的操作不在同一个节点上,可能会导致锁的失效或者其他线程获取到该锁,从而导致数据不一致的情况。
如果Redis的主从复制模式中,主节点重启后从节点升为主节点,那么原来的锁信息将无法被获取和释放。
为了解决这些问题,可以考虑使用Redis的RedLock算法。RedLock算法是由Redis的创始人Salvatore Sanfilippo提出的一种分布式锁方案,可以在多个节点之间协作实现可靠的分布式锁。
具体来说,RedLock算法使用多个Redis节点组成一个集群,获取锁和释放锁的操作在多个节点上执行,并且需要满足以下条件:
获取锁时,至少在大部分(如5个节点中的3个)节点上获取到锁才算成功获取到锁。
释放锁时,需要向所有节点发送释放锁的请求,只有大部分节点(如5个节点中的3个)成功释放锁才算成功释放锁。
通过这样的方式,可以在多个节点之间协作实现可靠的分布式锁,避免出现单点故障和数据不一致的情况。
以下是使用Java代码实现RedLock算法的示例:</code></pre><pre><code class="java">import redis.clients.jedis.Jedis;
import redis.clients.jedis.params.SetParams;
import java.util.ArrayList;
import java.util.List;
public class RedisRedLock {
private List<Jedis> jedisList;
private String lockKey;
private String lockValue;
private int expireTime;
private int quorum;
public RedisRedLock(List<Jedis> jedisList, String lockKey, String lockValue, int expireTime) {
this.jedisList = jedisList;
this.lockKey = lockKey;
this.lockValue = lockValue;
this.expireTime = expireTime;
this.quorum = jedisList.size() / 2 + 1;
}
public boolean acquire() {
int count = 0;
long startMillis = System.currentTimeMillis();
while ((System.currentTimeMillis() - startMillis) < expireTime) {
for (Jedis jedis : jedisList) {
SetParams setParams = new SetParams();
setParams.nx();
setParams.ex(expireTime);
String result = jedis.set(lockKey, lockValue, setParams);
if ("OK".equals(result)) {
count++;
}
if (count >= quorum) {
return true;
}
}
}
return false;
}
public boolean release() {
boolean success = true;
for (Jedis jedis : jedisList) {
String luaScript = "if redis.call('get', KEYS[1]) == ARGV[1] then return redis.call('del', KEYS[1]) else return 0 end";
Object result = jedis.eval(luaScript, 1, lockKey, lockValue);
if
(!(result instanceof Long) || (Long) result != 1)) {
success = false;
}
}
return success;
}</code></pre><pre><code>在上述示例代码中,RedisRedLock类表示了一个RedLock分布式锁,包括多个Redis节点、锁的名称、锁的值、锁的过期时间等属性,以及获取锁和释放锁的方法。在获取锁时,需要循环遍历多个Redis节点,使用NX(不存在则创建)命令尝试在Redis中创建一个新的key-value对,并设置过期时间。只有当大部分节点(如5个节点中的3个)都成功获取到锁时,才算获取锁成功。在释放锁时,需要向所有节点发送释放锁的请求,使用Lua脚本来判断当前锁是否为当前线程持有,如果是则释放锁,否则返回0表示锁已被其他线程持有。
需要注意的是,使用RedLock算法可以提高分布式锁的可靠性和稳定性,但是并不能完全避免分布式锁的问题。
例如,当Redis节点之间的网络延迟较大时,可能会导致某些节点无法及时获取到锁信息,从而出现数据不一致的情况。因此,在使用RedLock算法时,需要仔细评估应用场景和业务需求,权衡分布式锁的可靠性和性能,以便选择最适合的方案。</code></pre><p>当然,redlock 也并非是完美的,可以参考下面的文章:</p><blockquote><a href="https://link.segmentfault.com/?enc=kBugUlEx9031ZjxxxRCM3g%3D%3D.ijS6G3LLpWHwG0L1rMQMSuGvLRS8k9XWuLT6mtDzh1MnO%2B1CONqOghOVi1XbBfBV8UXeZhX%2BwpmKxxYzZwA8UA%3D%3D" rel="nofollow">redis 分布式锁设计 redis lock</a></blockquote><h2>Q4: RedLock 存在什么缺点?如何解决</h2><p>chatGPT:</p><pre><code>RedLock算法虽然可以提高分布式锁的可靠性和稳定性,但仍然存在一些缺点和局限性,如下所述:
可能存在网络分区问题:在Redis集群节点之间的网络延迟较大或出现网络分区时,可能会导致某些节点无法及时获取到锁信息,从而出现数据不一致的情况。
Redis节点故障问题:如果一个或多个Redis节点发生故障或宕机,可能会导致某些节点无法正常工作,从而影响分布式锁的可用性和性能。
同时获取锁的线程数量问题:RedLock算法中要求大部分节点都需要获取到锁才能认为获取成功,但是如果同时有大量线程请求获取锁,可能会导致节点的处理能力不足,从而导致获取锁的时间过长。
针对上述缺点和局限性,可以采取以下措施进行解决:
使用多个Redis集群:为了避免网络分区和节点故障等问题,可以使用多个Redis集群进行分布式锁的实现,从而提高可用性和稳定性。
采用自适应锁超时时间:针对同时获取锁的线程数量问题,可以根据实际情况采用自适应的锁超时时间,避免获取锁的时间过长。
使用其他分布式锁方案:RedLock算法虽然具有一定的优势,但并不是适用于所有场景。根据实际需求,可以选择其他分布式锁方案,如基于ZooKeeper实现的分布式锁等。</code></pre><h2>Q5: java 语言,使用 zookeeper 如何实现分布式锁</h2><p>chatGPT:</p><pre><code>在Java语言中,可以使用ZooKeeper实现分布式锁,具体步骤如下:
创建ZooKeeper客户端连接:使用ZooKeeper提供的Java API创建一个ZooKeeper客户端连接,用于访问ZooKeeper服务。
创建锁节点:在ZooKeeper中创建一个顺序临时节点,表示当前请求获取锁的节点。如果创建成功,则表示当前节点获取到了锁,否则需要等待。
监听前一个节点的删除事件:当前节点创建完成后,需要监听前一个节点的删除事件,一旦前一个节点被删除,说明前一个节点已经释放了锁,当前节点可以获取到锁。
释放锁:释放锁时需要删除当前节点。
以下是一段基于ZooKeeper实现分布式锁的Java代码示例:</code></pre><pre><code class="java">public class DistributedLock {
private ZooKeeper zooKeeper;
private String lockPath;
private String lockName;
private String currentNodePath;
private String previousNodePath;
public DistributedLock(ZooKeeper zooKeeper, String lockPath, String lockName) {
this.zooKeeper = zooKeeper;
this.lockPath = lockPath;
this.lockName = lockName;
}
public void lock() throws Exception {
// 创建锁节点
currentNodePath = zooKeeper.create(lockPath + "/" + lockName + "-", null, ZooDefs.Ids.OPEN_ACL_UNSAFE, CreateMode.EPHEMERAL_SEQUENTIAL);
// 获取锁节点的序号
int currentNodeSeqNum = Integer.parseInt(currentNodePath.substring(currentNodePath.lastIndexOf('-') + 1));
// 获取锁节点的父节点下的所有子节点
List<String> children = zooKeeper.getChildren(lockPath, false);
// 对子节点进行排序
Collections.sort(children);
// 获取当前节点在子节点列表中的位置
int currentNodeIndex = children.indexOf(currentNodePath.substring(lockPath.length() + 1));
// 如果当前节点是第一个节点,则获取锁成功
if (currentNodeIndex == 0) {
return;
}
// 获取前一个节点的路径
previousNodePath = lockPath + "/" + children.get(currentNodeIndex - 1);
// 监听前一个节点的删除事件
Stat stat = zooKeeper.exists(previousNodePath, new Watcher() {
@Override
public void process(WatchedEvent event) {
try {
lock();
} catch (Exception e) {
e.printStackTrace();
}
}
});
// 如果前一个节点已经不存在,则重新获取锁
if (stat == null) {
lock();
} else {
synchronized (this) {
wait();
}
}
}
public void unlock() throws Exception {
zooKeeper.delete(currentNodePath, -1);
}
}</code></pre><pre><code>在这个示例代码中,我们首先创建了一个ZooKeeper客户端连接,然后定义了一个DistributedLock类来实现分布式锁,其中lockPath表示锁节点的路径,lockName表示锁节点的名称。
在lock()方法中,我们首先创建了一个顺序临时节点,然后获取锁节点的序号和当前节点在子节点列表中的位置,如果当前节点是第一个节点,则获取锁成功;</code></pre><p>有一说一,对于基本不会使用 zookeeper 的开发者而言,上面的代码作为入门还是不错的。</p><p>但是最后我们还是需要理解原理,作为学习路上的良师益友,chatGPT 是当之无愧的。</p><h2>Q6:zookeeper 是 AP 还是 CP?如果是 CP,那么出现服务不可用怎么办?</h2><p>他推荐我们使用 zk,我们就问一下 cp 的问题。</p><p>chatGPT:</p><pre><code>ZooKeeper是一个CP系统,即在网络分区的情况下,系统优先保证一致性,而可能牺牲可用性。
在ZooKeeper集群中,一旦发生网络分区,可能会导致部分节点无法连接到主节点,这时候这部分节点就会成为不可用状态。对于这种情况,可以采取以下措施:
保证集群的健康状态:在使用ZooKeeper的过程中,需要对集群进行定期的监控,确保每个节点的健康状态,及时发现故障。
使用重试机制:如果某个节点无法连接到主节点,可以采用重试机制,等待一段时间后再次尝试连接,如果仍然失败,可以通过切换到备用节点来保证服务的可用性。
合理设置超时时间:在使用ZooKeeper时,需要设置合理的超时时间,以便在出现网络分区等异常情况时,能够及时检测并作出相应的处理。
总之,在使用ZooKeeper实现分布式锁时,需要考虑到网络分区等异常情况,采取相应的措施来保证服务的可用性和一致性。</code></pre><h2>Q7: redis 和 zookeeper 实现分布式锁,你总结一下二者的利弊,对比一下。详细的表格</h2><p>感觉聊的差不多了,我们让 chatGPT 出一个对比的表格。</p><p>chatGPT:</p><pre><code>下面是Redis和ZooKeeper实现分布式锁的利弊对比表格:</code></pre><table><thead><tr><th align="left">特点</th><th align="left">Redis</th><th align="left">ZooKeeper</th></tr></thead><tbody><tr><td align="left">数据结构</td><td align="left">Redis使用字符串数据类型存储锁信息</td><td align="left">ZooKeeper使用ZNode节点存储锁信息</td></tr><tr><td align="left">实现方式</td><td align="left">Redis使用单节点或者Redis集群实现分布式锁</td><td align="left">ZooKeeper使用ZooKeeper集群实现分布式锁</td></tr><tr><td align="left">性能</td><td align="left">Redis读写速度较快,适用于高并发场景</td><td align="left">ZooKeeper在写入数据时需要进行同步操作,写入速度相对较慢</td></tr><tr><td align="left">可靠性</td><td align="left">Redis的主从复制和Sentinel保证了Redis集群的高可用性</td><td align="left">ZooKeeper采用了Paxos算法保证了集群的数据一致性和高可用性</td></tr><tr><td align="left">稳定性</td><td align="left">Redis的稳定性较好,不易出现死锁和脑裂等问题</td><td align="left">ZooKeeper在网络分区时可能出现脑裂问题,需要进行特殊处理</td></tr><tr><td align="left">开发难度</td><td align="left">Redis的API简单易用,易于开发和维护</td><td align="left">ZooKeeper的API较为复杂,需要一定的开发经验和技术储备</td></tr><tr><td align="left">功能扩展</td><td align="left">Redis支持多种数据结构和数据操作,具有更强的功能扩展性</td><td align="left">ZooKeeper主要用于分布式协调和服务发现等场景,功能相对局限</td></tr></tbody></table><pre><code>总的来说,Redis适合处理高并发、读写频繁的场景,而ZooKeeper则更适合处理数据一致性和高可用性要求较高的场景。
在开发实践中,需要根据具体的业务需求和系统特点选择合适的分布式锁实现方式。</code></pre><h2>Q8: 你建议使用哪一种方式呢</h2><p>chatGPT:</p><pre><code>建议根据具体业务场景和系统架构选择合适的分布式锁实现方式。
如果业务场景中主要是处理高并发请求,读写操作较为频繁,且对数据一致性要求不是特别高,可以优先考虑Redis实现分布式锁。
如果系统对数据一致性和高可用性要求较高,可以考虑使用ZooKeeper实现分布式锁。
同时,如果系统中同时使用了Redis和ZooKeeper,可以考虑综合使用两者实现更为灵活和可靠的分布式锁方案。</code></pre><p>他说到了结合二者,怎么个结合法?</p><h2>Q9:综合使用两者实现是什么意思?展开说一说</h2><p>chatGPT:</p><pre><code>综合使用Redis和ZooKeeper实现分布式锁,主要是将两者的优势结合起来,达到更好的性能和可靠性。
一种常见的方式是,使用Redis实现分布式锁的快速获取和释放,同时使用ZooKeeper实现分布式锁的可靠性和数据一致性。具体实现方式是:
使用Redis的SETNX命令获取锁,如果获取成功,则表示获取到了分布式锁;否则,等待一段时间再次尝试获取锁。
获取到锁之后,设置锁的过期时间,并在Redis中记录当前线程持有锁的标识,用于后续释放锁时进行验证。
在业务逻辑执行完成后,使用Lua脚本或Redis的事务机制,保证释放锁的原子性。释放锁时,首先验证当前线程持有锁的标识,如果匹配,则使用Redis的DEL命令删除锁的标识,释放锁。
同时,将锁的释放同步到ZooKeeper中。在释放锁时,使用ZooKeeper的分布式锁机制,确保同一时刻只有一个线程可以修改锁的状态,避免多个线程同时释放锁的问题。
综合使用Redis和ZooKeeper实现分布式锁,既可以保证锁的获取和释放的高效性,又可以确保分布式锁的数据一致性和高可用性,是一种比较优秀的分布式锁实现方案。</code></pre><h2>Q10: 这样不会导致复杂性变高吗?如果二者不一致怎么办</h2><p>chatGPT:</p><pre><code>综合使用Redis和ZooKeeper实现分布式锁的确会增加一定的复杂性,需要更多的编码工作和配置调整,但是它能够更好地满足分布式锁的可靠性和高可用性要求,确保分布式锁的正确性和稳定性。
在使用Redis和ZooKeeper时,确实存在二者之间的数据一致性问题,例如锁在Redis中已经被释放,但在ZooKeeper中还未被删除,或者锁在ZooKeeper中已经被释放,但在Redis中还未被删除。如果发生这种情况,就会导致分布式锁的状态不一致,可能会造成一些问题。
为了避免这种情况,可以采用以下几种方式:
尽可能减少使用两种不同的分布式锁实现,如果可以,最好使用Redis或ZooKeeper其中一种实现分布式锁,而不是两者同时使用。
在代码实现时,加入检查分布式锁状态的逻辑,确保Redis和ZooKeeper中的分布式锁状态一致,例如在释放分布式锁时,先检查Redis中的锁是否已经被删除,再去ZooKeeper中删除锁的状态。
使用心跳机制定期同步两种分布式锁实现之间的状态,例如每隔一段时间,在Redis中设置一个带有过期时间的key,同时在ZooKeeper中创建一个持久化节点,表示分布式锁的状态,每次检查分布式锁状态时,都先在Redis中检查是否过期,如果过期则到ZooKeeper中检查分布式锁状态。</code></pre><h2>小结</h2><p>chatGPT 其实比一般的博文质量要高不少,而且可以不停地提问拓展,对于知识的学习和拓展很有帮助。</p><p>但是需要我们有一定的辨别能力,因为他可能会胡说八道,而且死不悔改。</p><p><strong>AI 不会取代人,只有会说用 AI 的人取代不会使用 AI 的人</strong>。</p><p>我是老马,期待与你的下次重逢。</p>
中英文拼写检测纠正开源项目使用入门 word-checker 1.1.0
https://segmentfault.com/a/1190000043624920
2023-04-04T18:02:15+08:00
2023-04-04T18:02:15+08:00
老马啸西风
https://segmentfault.com/u/houbinbin
0
<p><img src="/img/remote/1460000043624922" alt="title" title="title"></p><h2>项目简介</h2><p><a href="https://link.segmentfault.com/?enc=ra5qehahoYqOZdRoo3FoEQ%3D%3D.Q8r2Jd9ma9P1qWcrO5dwXysBZo3araIHGMm8DC5jMiseDpeXDLCSL%2FttvXp4WlKx" rel="nofollow">word-checker</a> 本项目用于单词拼写检查。支持英文单词拼写检测,和中文拼写检测。</p><h2>特性说明</h2><ul><li>可以迅速判断当前单词是否拼写错误</li><li>可以返回最佳匹配结果</li><li>可以返回纠正匹配列表,支持指定返回列表的大小</li><li>错误提示支持 i18n</li><li>支持大小写、全角半角格式化处理</li><li>支持自定义词库</li><li>内置 27W+ 的英文词库</li><li>支持指定英文的编辑距离</li><li>支持基本的中文拼写检测</li></ul><h2>变更日志</h2><blockquote><a href="https://link.segmentfault.com/?enc=%2BvcK5DT34ATWJaA9NqydyQ%3D%3D.c44Vbiq%2FCZ7zwjuMGI2aUkk83OQ%2FYZf8aZVZfZo1a2m1Xb25WXnLHkjxgRgKg1CT7YscMi5%2BClHyP0BYryzztg%3D%3D" rel="nofollow">变更日志</a></blockquote><h2>快速开始</h2><h3>JDK 版本</h3><p>Jdk 1.7+</p><h3>maven 引入</h3><pre><code class="xml"><dependency>
<groupId>com.github.houbb</groupId>
<artifactId>word-checker</artifactId>
<version>1.1.0</version>
</dependency></code></pre><h3>测试案例</h3><p>会根据输入,自动返回最佳纠正结果。</p><pre><code class="java">final String speling = "speling";
Assert.assertEquals("spelling", WordCheckerHelper.correct(speling));</code></pre><h2>核心 api 介绍</h2><p>核心 api 在 <code>WordCheckerHelper</code> 工具类下。</p><p><code>WordCheckers</code> 工具类提供了长文本中英文混合的自动纠正功能,当然也支持单个单词。</p><table><thead><tr><th align="left">功能</th><th align="left">方法</th><th align="left">参数</th><th align="left">返回值</th><th align="left">备注</th></tr></thead><tbody><tr><td align="left">文本拼写是否正确</td><td align="left">isCorrect(string)</td><td align="left">待检测的文本</td><td align="left">boolean</td><td align="left">全部正确,才会返回 true</td></tr><tr><td align="left">返回最佳纠正结果</td><td align="left">correct(string)</td><td align="left">待检测的单词</td><td align="left">String</td><td align="left">如果没有找到可以纠正的文本,则返回其本身</td></tr><tr><td align="left">判断文本拼写是否正确</td><td align="left">correctMap(string)</td><td align="left">待检测的单词</td><td align="left"><code>Map<String, List<String>></code></td><td align="left">返回所有匹配的纠正列表 MAP</td></tr><tr><td align="left">判断文本拼写是否正确</td><td align="left">correctMap(string, int limit)</td><td align="left">待检测的文本, 返回列表的大小</td><td align="left">返回指定大小的的纠正列表 MAP</td><td align="left">列表大小 <= limit</td></tr><tr><td align="left">判断文本拼写是否正确</td><td align="left">correctList(string)</td><td align="left">待检测的单词</td><td align="left"><code>List<String></code></td><td align="left">返回所有匹配的纠正列表</td></tr><tr><td align="left">判断文本拼写是否正确</td><td align="left">correctList(string, int limit)</td><td align="left">待检测的文本, 返回列表的大小</td><td align="left">返回指定大小的的纠正列表</td><td align="left">列表大小 <= limit</td></tr></tbody></table><h3>英文测试例子</h3><blockquote>参见 <a href="https://link.segmentfault.com/?enc=iKoSrqCTR3Xo2ZWouZkmCA%3D%3D.QzpkBAH8YTjsENesuSkanlclX27ogfl60Xse0zXTynUYcJA9CAmAaBCzMBkHrGqR9VZifSnGO7yUH3ohamFOmWoJ8EIwX5j9sdoXjWSSW38nySrjVfi06sy6VBFtkCQBype1HHtt3sz1slrY%2BoKCgYudG4VUsB10rkdmzBR73%2Fw%3D" rel="nofollow">EnWordCheckerTest.java</a></blockquote><h4>是否拼写正确</h4><pre><code class="java">final String hello = "hello";
final String speling = "speling";
Assert.assertTrue(WordCheckerHelper.isCorrect(hello));
Assert.assertFalse(WordCheckerHelper.isCorrect(speling));</code></pre><h4>返回最佳匹配结果</h4><pre><code class="java">final String hello = "hello";
final String speling = "speling";
Assert.assertEquals("hello", WordCheckerHelper.correct(hello));
Assert.assertEquals("spelling", WordCheckerHelper.correct(speling));</code></pre><h4>默认纠正匹配列表</h4><pre><code class="java">final String word = "goox";
List<String> stringList = WordCheckerHelper.correctList(word);
Assert.assertEquals("[good, goo, goon, goof, gook, goop, goos, gox, goog, gool, goor]", stringList.toString());</code></pre><h4>指定纠正匹配列表大小</h4><pre><code class="java">final String word = "goox";
final int limit = 2;
List<String> stringList = WordCheckerHelper.correctList(word, limit);
Assert.assertEquals("[good, goo]", stringList.toString());</code></pre><h3>中文拼写纠正</h3><h4>是否拼写正确</h4><pre><code class="java">final String right = "正确";
final String error = "万变不离其中";
Assert.assertTrue(WordCheckerHelper.isCorrect(right));
Assert.assertFalse(WordCheckerHelper.isCorrect(error));</code></pre><h4>返回最佳匹配结果</h4><pre><code class="java">final String right = "正确";
final String error = "万变不离其中";
Assert.assertEquals("正确", WordCheckerHelper.correct(right));
Assert.assertEquals("万变不离其宗", WordCheckerHelper.correct(error));</code></pre><h4>默认纠正匹配列表</h4><pre><code class="java">final String word = "万变不离其中";
List<String> stringList = WordCheckerHelper.correctList(word);
Assert.assertEquals("[万变不离其宗]", stringList.toString());</code></pre><h4>指定纠正匹配列表大小</h4><pre><code class="java">final String word = "万变不离其中";
final int limit = 1;
List<String> stringList = WordCheckerHelper.correctList(word, limit);
Assert.assertEquals("[万变不离其宗]", stringList.toString());</code></pre><h3>长文本中英文混合</h3><h4>情景</h4><p>实际拼写纠正的话,最佳的使用体验是用户输入一个长文本,并且可能是中英文混合的。</p><p>然后实现上述对应的功能。</p><h4>拼写是否正确</h4><pre><code class="java">final String hello = "hello 你好";
final String speling = "speling 你好 以毒功毒";
Assert.assertTrue(WordCheckers.isCorrect(hello));
Assert.assertFalse(WordCheckers.isCorrect(speling));</code></pre><h4>返回最佳纠正结果</h4><pre><code class="java">final String hello = "hello 你好";
final String speling = "speling 你好以毒功毒";
Assert.assertEquals("hello 你好", WordCheckers.correct(hello));
Assert.assertEquals("spelling 你好以毒攻毒", WordCheckers.correct(speling));</code></pre><h4>判断文本拼写是否正确</h4><p>每一个词,对应的纠正结果。</p><pre><code class="java">final String hello = "hello 你好";
final String speling = "speling 你好以毒功毒";
Assert.assertEquals("{hello=[hello], =[ ], 你=[你], 好=[好]}", WordCheckers.correctMap(hello).toString());
Assert.assertEquals("{ =[ ], speling=[spelling, spewing, sperling, seeling, spieling, spiling, speeling, speiling, spelding], 你=[你], 好=[好], 以毒功毒=[以毒攻毒]}", WordCheckers.correctMap(speling).toString());</code></pre><h4>判断文本拼写是否正确</h4><p>同上,指定最多返回的个数。</p><pre><code class="java">final String hello = "hello 你好";
final String speling = "speling 你好以毒功毒";
Assert.assertEquals("{hello=[hello], =[ ], 你=[你], 好=[好]}", WordCheckers.correctMap(hello, 2).toString());
Assert.assertEquals("{ =[ ], speling=[spelling, spewing], 你=[你], 好=[好], 以毒功毒=[以毒攻毒]}", WordCheckers.correctMap(speling, 2).toString());</code></pre><h2>格式化处理</h2><p>有时候用户的输入是各式各样的,本工具支持对于格式化的处理。</p><h3>大小写</h3><p>大写会被统一格式化为小写。</p><pre><code class="java">final String word = "stRing";
Assert.assertTrue(WordCheckerHelper.isCorrect(word));</code></pre><h3>全角半角</h3><p>全角会被统一格式化为半角。</p><pre><code class="java">final String word = "string";
Assert.assertTrue(WordCheckerHelper.isCorrect(word));</code></pre><h2>自定义英文词库</h2><h3>文件配置</h3><p>你可以在项目资源目录创建文件 <code>resources/data/define_word_checker_en.txt</code></p><p>内容如下:</p><pre><code>my-long-long-define-word,2
my-long-long-define-word-two</code></pre><p>不同的词独立一行。</p><p>每一行第一列代表单词,第二列代表出现的次数,二者用逗号 <code>,</code> 隔开。</p><p>次数越大,在纠正的时候返回优先级就越高,默认值为 1。</p><p>用户自定义的词库优先级高于系统内置词库。</p><h3>测试代码</h3><p>我们在指定了对应的单词之后,拼写检测的时候就会生效。</p><pre><code class="java">final String word = "my-long-long-define-word";
final String word2 = "my-long-long-define-word-two";
Assert.assertTrue(WordCheckerHelper.isCorrect(word));
Assert.assertTrue(WordCheckerHelper.isCorrect(word2));</code></pre><h2>自定义中文词库</h2><h3>文件配置</h3><p>你可以在项目资源目录创建文件 <code>resources/data/define_word_checker_zh.txt</code></p><p>内容如下:</p><pre><code>默守成规 墨守成规</code></pre><p>使用英文空格分隔,前面是错误,后面是正确。</p><h2>后期 Road-Map</h2><ul><li>[x] 支持英文分词,处理整个英文句子</li><li>支持中文分词拼写检测</li><li>引入中文纠错算法,同音字和形近字处理。</li><li>支持中英文混合拼写检测</li></ul><h2>技术鸣谢</h2><p><a href="https://link.segmentfault.com/?enc=UvLzlgJ7bEvw6hIynsS5ag%3D%3D.SYDzVKse3mk%2F7Ki6TmZWvbRlhuVNudibwHkVBTqjT2yeS4KnUp7TEX4RVQTcWPr4" rel="nofollow">Words</a> 提供的原始英语单词数据。</p><h2>开源地址</h2><blockquote><a href="https://link.segmentfault.com/?enc=We5pf10hE7PeS09rB9a6Rg%3D%3D.doJVUxf%2FS1pia9jFlf1VJ9iQ1s8QBSnnfuztib3vfp5n%2BUflnm3va94%2FHGPm0eF1" rel="nofollow">https://github.com/houbb/word-checker/</a> 欢迎大家 fork+star ~~~</blockquote>
NLP 开源形近字算法之相似字列表(番外篇)
https://segmentfault.com/a/1190000043593322
2023-03-28T14:01:53+08:00
2023-03-28T14:01:53+08:00
老马啸西风
https://segmentfault.com/u/houbinbin
0
<h2>创作目的</h2><p>国内对于文本的相似度计算,开源的工具是比较丰富的。</p><p>但是对于两个汉字之间的相似度计算,国内基本一片空白。国内的参考的资料少的可怜,国外相关文档也是如此。</p><p>本项目旨在抛砖引玉,实现一个基本的相似度计算工具,为汉字 NLP 贡献一点绵薄之力。</p><p>推荐阅读:</p><p><a href="https://link.segmentfault.com/?enc=4WZXwSrx9yY0O2mKq8wa%2FA%3D%3D.%2FqEk3eamOdVUegL67f6ceya2phIspxOHT4ld6gs%2BsQD8dh%2FNGmFKuZYLe80%2BdoBR%2BpleDLgY7%2Bfy2ia2RsO3zg%3D%3D" rel="nofollow">NLP 中文形近字相似度计算思路</a></p><p><a href="https://link.segmentfault.com/?enc=R1ju0%2FfKWkkaRVVbA2t5zQ%3D%3D.1hIb9ozhJYZPvRYkm1NUQZ86k%2BDE87rZSnZh6urL1BPfIIR6NzLpMwfVVXad%2B3udsuYOoReudQDIVMYGBcZNMA%3D%3D" rel="nofollow">中文形近字相似度算法实现,为汉字 NLP 尽一点绵薄之力</a></p><p><a href="https://link.segmentfault.com/?enc=jsVMbfJOJsDFxRikYuOdnA%3D%3D.Yid1ubh0U3B%2FhQ%2BDlMhLCrCqUyVLWBhXknjOEuEJEO7FOne%2FYSW8oGklP%2BLv8WwnangxB99S26DNF1vCuGg%2Blw%3D%3D" rel="nofollow">当代中国最贵的汉字是什么?</a></p><p><a href="https://link.segmentfault.com/?enc=MFG4qWfo5ub0wD2OykjXaw%3D%3D.swZUuWXUJgEXR6QagoHkDYrA8mqMxPSja85%2Bh85Vnrw%2FjFTZCQxG4rdYDyIFGH%2BXXDbpiwo9p47lubx4ZTgNlg%3D%3D" rel="nofollow">NLP 开源形近字算法补完计划(完结篇)</a></p><p><a href="https://link.segmentfault.com/?enc=%2BcGmba3748kp%2B9YmySd%2FCQ%3D%3D.mSbRDFgqUc0LzQsrAD77KwLOTzWXGrau%2FVCXhr38sPoKcBApTUyjrSzzKBvnwmBB%2BkF3dpvC3ykZTign6G%2BI2Q%3D%3D" rel="nofollow">NLP 开源形近字算法之形近字列表(番外篇)</a></p><p><a href="https://link.segmentfault.com/?enc=ju2Jl0NGtJvXacHkN3%2BXQQ%3D%3D.FD5BzMU0B4EFSsS8RcsfHTY8NadPSaPDYtvErf%2B0GE3l%2FjSZXXqrvjLOA%2B5UMkggqSuizbEHlL1geGPd%2BJmbsA%3D%3D" rel="nofollow">开源项目在线化 中文繁简体转换/敏感词/拼音/分词/汉字相似度/markdown 目录</a></p><p><img src="/img/remote/1460000043593324" alt="在这里插入图片描述" title="在这里插入图片描述"></p><h2>需求</h2><p>有时候我们并不是需要返回两个字的相似,而是需要返回一个汉字的相似列表。</p><h3>实现思路</h3><p>我们可以分别计算所有的汉字之间的相似度,然后保留最大的前100个,放在字典中。</p><p>然后实时查询这个字典即可。</p><h3>实现方式</h3><p><code>bihuashu_2w.txt</code> 中我们主要需要的是对应的 2W 常见汉字。</p><p><code>hanzi_similar_list.txt</code> 用来存放汉字和相似字的映射关系。</p><h4>数据初始化</h4><pre><code class="java">public static void main(String[] args) {
final String path = "D:\\code\\coin\\nlp-hanzi-similar\\src\\main\\resources\\hanzi_similar_list.txt";
// 读取列表
List<String> lines = FileUtil.readAllLines("D:\\code\\coin\\nlp-hanzi-similar\\src\\main\\resources\\nlp\\bihuashu_2w.txt");
// 所有的单词
Set<String> allWordSet = new HashSet<>();
for(String line : lines) {
String word = line.split(" ")[0];
allWordSet.add(word);
}
// 循环对比
for(String word : allWordSet) {
List<String> list = getSimilarListData(word, allWordSet);
String line = word +" " + StringUtil.join(list, "");
FileUtil.append(path, line);
}
}</code></pre><ul><li>优先级队列取前 100 个</li></ul><p>我们通过优先级队列存储:</p><pre><code class="java">private static List<String> getSimilarListData(String word, Set<String> wordSet) {
PriorityQueue<SimilarListDataItem> items = new PriorityQueue<>(new Comparator<SimilarListDataItem>() {
@Override
public int compare(SimilarListDataItem o1, SimilarListDataItem o2) {
// 相似度大的放在前面
return -o1.getRate().compareTo(o2.getRate());
}
});
for(String other : wordSet) {
if(word.equals(other)) {
continue;
}
// 对比
double rate = HanziSimilarHelper.similar(word.charAt(0), other.charAt(0));
SimilarListDataItem item = new SimilarListDataItem(other, rate);
items.add(item);
}
final int limit = 100;
List<String> wordList = new ArrayList<>();
for(SimilarListDataItem item : items) {
wordList.add(item.getWord());
if(wordList.size() >= limit) {
break;
}
}
return wordList;
}</code></pre><h4>相似字的获取</h4><p>初始化好数据之后,一切就变得非常简单:</p><ul><li>接口定义</li></ul><pre><code class="java">/**
* 数据接口-相似列表
* @author binbin.hou
* @since 1.3.0
*/
public interface IHanziSimilarListData {
/**
* 返回数据信息
* @param word 单词
* @return 结果
* @since 1.3.0
*/
List<String> similarList(String word);
}</code></pre><ul><li>数据获取</li></ul><pre><code class="java">public class HanziSimilarListData implements IHanziSimilarListData {
private static volatile Map<String, List<String>> map = Guavas.newHashMap();
@Override
public List<String> similarList(String word) {
if(MapUtil.isEmpty(map)) {
initDataMap();
}
return map.get(word);
}
private void initDataMap() {
if(MapUtil.isNotEmpty(map)) {
return;
}
//DLC
synchronized (map) {
if(MapUtil.isEmpty(map)) {
List<String> lines = StreamUtil.readAllLines("/hanzi_similar_list.txt");
for(String line : lines) {
String[] words = line.split(" ");
// 后面的100个相近词
List<String> list = StringUtil.toCharStringList(words[1]);
map.put(words[0], list);
}
}
}
}
}</code></pre><h4>便利性</h4><p>为了用户使用方便,我们在 <code>HanziSimilarHelper</code> 中添加 2 个工具类方法:</p><pre><code class="java">/**
* 相似的列表
* @param hanziOne 汉字一
* @param limit 大小
* @return 结果
* @since 1.3.0
*/
public static List<String> similarList(char hanziOne, int limit) {
return HanziSimilarBs.newInstance().similarList(hanziOne, limit);
}
/**
* 相似的列表
* @param hanziOne 汉字一
* @return 结果
* @since 1.3.0
*/
public static List<String> similarList(char hanziOne) {
return similarList(hanziOne, 10);
}</code></pre><h4>测试效果</h4><p>我们使用看一下效果:</p><p>我们来看一下【爱】的形近字。</p><pre><code class="java">List<String> list = HanziSimilarHelper.similarList('爱');
Assert.assertEquals("[爰, 爯, 受, 爭, 妥, 憂, 李, 爳, 叐, 雙]", list.toString());</code></pre><h2>开源地址</h2><p>为了便于大家使用学习,项目已开源。</p><blockquote><a href="https://link.segmentfault.com/?enc=Lpdolvb0J5U%2F%2FGexlUjfiQ%3D%3D.FYWpssL9x7aEvSbUdXreuEdKcY4IqcpL5%2BW3o%2BPhlzfsCeGoDahkxOFveUrpseKX" rel="nofollow">https://github.com/houbb/nlp-hanzi-similar</a></blockquote><h2>小结</h2><p>一个字的形近字可以做很多有趣的事情,这个要看大家的想象力。</p><p>实现方式也不难,最核心的还是相似度的计算。</p><p>我是老马,期待与你的下次重逢。</p>
改进 hibernate-validator,新一代校验框架 validator 使用介绍 v0.4
https://segmentfault.com/a/1190000043561273
2023-03-20T17:33:10+08:00
2023-03-20T17:33:10+08:00
老马啸西风
https://segmentfault.com/u/houbinbin
0
<h2>项目介绍</h2><p>java 开发中,参数校验是非常常见的需求。但是 hibernate-validator 在使用过程中,依然会存在一些问题。</p><p><a href="https://link.segmentfault.com/?enc=3sLFZE5V146ncB7MWXCXbw%3D%3D.REkDqYCn4jgF%2BTeEDAwNfNQpBOBLVGSqKDKpQlqTnBaaTuG7SYJJoDcE8EukyXP%2B" rel="nofollow">validator</a> 在 hibernate-validator 等校验工具之上,做了一些改进,使其使用更加便捷优雅,进一步提升工作效率。</p><p><img src="/img/remote/1460000043561275" alt="图片" title="图片"></p><h3>变更日志</h3><blockquote><a href="https://link.segmentfault.com/?enc=TK0Oc8XresnmyVu4Qf8pqw%3D%3D.D%2FaxN2HwH1s06B4bjikAXhQ81O5B0NNXQtaf7DTSIQL4qOt7hoBxPyqEAo9%2F7ymqfSWZ%2Bnej5nEmfddy42o%2BMA%3D%3D" rel="nofollow">变更日志</a></blockquote><h3>特性</h3><ul><li>支持 fluent-validation</li><li>支持 jsr-303 注解</li><li>支持 i18n</li><li>支持用户自定义策略</li><li>支持用户自定义注解</li><li>支持针对属性的校验</li><li>支持过程式编程与注解式编程</li><li>支持指定校验生效的条件</li></ul><h2>创作目的</h2><h3>hibernate-validator 无法满足的场景</h3><p>如今 java 最流行的 hibernate-validator 框架,但是有些场景是无法满足的。</p><p>比如:</p><ol><li>验证新密码和确认密码是否相同。(同一对象下的不同属性之间关系)</li><li>当一个属性值满足某个条件时,才进行其他值的参数校验。</li><li>多个属性值,至少有一个不能为 null</li></ol><p>其实,在<strong>对于多个字段的关联关系处理时,hibernate-validator 就会比较弱</strong>。</p><p>本项目结合原有的优点,进行这一点的功能强化。</p><h3>validation-api 过于复杂</h3><p>validation-api 提供了丰富的特性定义,也同时带来了一个问题。</p><p>实现起来,特别复杂。</p><p>然而我们实际使用中,常常不需要这么复杂的实现。</p><p>validator-api 提供了一套简化很多的 api,便于用户自行实现。</p><h3>自定义缺乏灵活性</h3><p>hibernate-validator 在使用中,自定义约束实现是基于注解的,针对单个属性校验不够灵活。</p><p>本项目中,<strong>将属性校验约束和注解约束区分开,便于复用和拓展</strong>。</p><h3>过程式编程 vs 注解式编程</h3><p>hibernate-validator 核心支持的是注解式编程,基于 bean 的校验。</p><p>一个问题是针对属性校验不灵活,有时候针对 bean 的校验,还是要自己写判断。</p><p>本项目支持 fluent-api 进行过程式编程,同时支持注解式编程。</p><p>尽可能兼顾灵活性与便利性。</p><h2>快速开始</h2><h3>准备工作</h3><p>JDK1.7+</p><p>Maven 3.X+</p><h3>maven 引入</h3><pre><code class="xml"><dependency>
<groupId>com.github.houbb</groupId>
<artifactId>validator-core</artifactId>
<version>0.4.0</version>
</dependency></code></pre><h3>快速入门</h3><h4>定义对象</h4><p>第一步,我们定义一个常见的 java bean 对象,可以指定内置的注解。</p><p>支持 jsr-303 注解和 hibernate-validator 的注解。</p><pre><code class="java">public class User {
/**
* 名称
*/
@HasNotNull({"nickName"})
private String name;
/**
* 昵称
*/
private String nickName;
/**
* 原始密码
*/
@AllEquals("password2")
private String password;
/**
* 新密码
*/
private String password2;
/**
* 性别
*/
@Ranges({"boy", "girl"})
private String sex;
/**
* 失败类型枚举
*/
@EnumRanges(FailTypeEnum.class)
private String failType;
//getter & setter
}</code></pre><h4>ValidHelper 工具方法</h4><p>ValidHelper 作为统一封装的工具类,提供了 java bean 校验常见的方法。</p><p>方法列表:</p><table><thead><tr><th align="left">序号</th><th align="left">方法</th><th align="left">返回值</th><th align="left">说明</th></tr></thead><tbody><tr><td align="left">1</td><td align="left">failOver(Object object)</td><td align="left">IResult</td><td align="left">全部验证后返回</td></tr><tr><td align="left">2</td><td align="left">failFast(Object object)</td><td align="left">IResult</td><td align="left">快速验证后返回</td></tr><tr><td align="left">3</td><td align="left">failOverThrow(Object object)</td><td align="left">void</td><td align="left">全部验证后返回-未通过抛出 ValidRuntimeException 异常</td></tr><tr><td align="left">4</td><td align="left">failFastThrow(Object object)</td><td align="left">void</td><td align="left">快速验证后返回-未通过抛出 ValidRuntimeException 异常</td></tr></tbody></table><p>使用起来很简单,我们以 failFast 为例:</p><pre><code class="java">// 对象定义
User user = new User();
user.sex("what").password("old").password2("new");
// 调用方法
IResult result = ValidHelper.failFast(user);</code></pre><p>结果:</p><pre><code>DefaultResult{pass=false, notPassList=[DefaultConstraintResult{pass=false, message='name: 值 <null> 不是预期值', value=null, constraint='HasNotNullConstraint', expectValue='', fieldName='name'}], allList=null}</code></pre><ul><li>IResult 方法说明</li></ul><p>返回值实现默认为 DefaultResult,接口 IResult 属性如下:</p><pre><code class="java">public interface IResult {
/**
* 是否全部通过验证
* @return 是否
* @since 0.1.0
*/
boolean pass();
/**
* 未通过的列表信息
* @return 验证结果
* @since 0.1.0
*/
List<IConstraintResult> notPassList();
/**
* 所有的验证结果列表
* @return 所有的验证结果
* @since 0.1.0
*/
List<IConstraintResult> allList();
/**
* 输出信息到控台
* (1)主要是为了方便调整
* (2)该功能其实可以做增强,比如输出到文件/数据库等等。
* @return this
* @since 0.0.6
*/
IResult print();
/**
* 对于未通过的信息,
* (1)未通过的界定。
* {@link IConstraintResult#pass()} 为 false
*
* (2)内容信息
* 抛出运行时异常 {@link com.github.houbb.validator.api.exception.ValidRuntimeException},异常信息为 {@link IConstraintResult#message()} 消息
* (3)内容限定
* 为了避免异常内容过多,只抛出第一条即可。
* (4)改方法的增强空间
* 4.1 可以指定什么情况下抛出异常
* 4.2 抛出异常的信息和类别
*
* @return this
* @since 0.0.6
*/
IResult throwsEx();
}</code></pre><h2>注解说明</h2><p>java bean 的校验,基于注解是比较方便的。和 hibernate-validator 使用类似,这里介绍下常见的注解。</p><h3>内置约束注解</h3><p>内置注解如下:</p><table><thead><tr><th align="left">序号</th><th align="left">注解</th><th align="left">value()</th><th align="left">说明</th></tr></thead><tbody><tr><td align="left">1</td><td align="left"><code>@AllEquals</code></td><td align="left"><code>String[]</code></td><td align="left">当前字段及其指定的字段 全部相等</td></tr><tr><td align="left">2</td><td align="left"><code>@EnumRanges</code></td><td align="left"><code>Class<? extends Enum></code></td><td align="left">当前字段必须在枚举值指定的范围内</td></tr><tr><td align="left">3</td><td align="left"><code>@HasNotNull</code></td><td align="left"><code>String[]</code></td><td align="left">当前字段及其指定的字段 至少有一个不为 null</td></tr><tr><td align="left">4</td><td align="left"><code>@Ranges</code></td><td align="left"><code>String[]</code></td><td align="left">当前字段必须在指定的范围内</td></tr></tbody></table><h3>JSR-303 + hibernate-validator 约束注解支持</h3><table><thead><tr><th align="left">序号</th><th align="left">注解</th><th align="left">说明</th></tr></thead><tbody><tr><td align="left">1</td><td align="left"><code>@AssertTrue</code></td><td align="left">为 true 约束条件</td></tr><tr><td align="left">2</td><td align="left"><code>@AssertFalse</code></td><td align="left">为 false 约束条件</td></tr><tr><td align="left">3</td><td align="left"><code>@Null</code></td><td align="left">为 null 约束条件</td></tr><tr><td align="left">4</td><td align="left"><code>@NotNull</code></td><td align="left">不为 null 约束条件</td></tr><tr><td align="left">5</td><td align="left"><code>@Past</code></td><td align="left">是否在当前时间之前约束条件</td></tr><tr><td align="left">6</td><td align="left"><code>@Future</code></td><td align="left">是否在当前时间之后约束条件</td></tr><tr><td align="left">7</td><td align="left"><code>@Pattern</code></td><td align="left">正则表达式约束条件</td></tr><tr><td align="left">8</td><td align="left"><code>@Size</code></td><td align="left">在指定范围内的约束条件</td></tr><tr><td align="left">9</td><td align="left"><code>@Digits</code></td><td align="left">数字位数的约束条件</td></tr><tr><td align="left">10</td><td align="left"><code>@DecimalMax</code></td><td align="left">最大数字的约束条件</td></tr><tr><td align="left">11</td><td align="left"><code>@DecimalMin</code></td><td align="left">最小数字的约束条件</td></tr><tr><td align="left">12</td><td align="left"><code>@Min</code></td><td align="left">最小的约束条件</td></tr><tr><td align="left">13</td><td align="left"><code>@Max</code></td><td align="left">最大的约束条件</td></tr><tr><td align="left">13</td><td align="left"><code>@NotBlank</code></td><td align="left">不能为空格的约束条件</td></tr><tr><td align="left">14</td><td align="left"><code>@NotEmpty</code></td><td align="left">不能为空的约束条件</td></tr><tr><td align="left">15</td><td align="left"><code>@Length</code></td><td align="left">长度的约束条件</td></tr><tr><td align="left">16</td><td align="left"><code>@CNPJ</code></td><td align="left">CNPJ 约束条件</td></tr><tr><td align="left">17</td><td align="left"><code>@CPF</code></td><td align="left">CPF 约束条件</td></tr><tr><td align="left">18</td><td align="left"><code>@URL</code></td><td align="left">URL 约束条件</td></tr><tr><td align="left">18</td><td align="left"><code>@Email</code></td><td align="left">Email 约束条件</td></tr><tr><td align="left">19</td><td align="left"><code>@UniqueElements</code></td><td align="left">元素唯一约束条件</td></tr><tr><td align="left">20</td><td align="left"><code>@Range</code></td><td align="left">指定范围元素约束条件</td></tr></tbody></table><h2>条件注解</h2><h3>说明</h3><p>有时候我们需要根据不同的参数,进行不同的限制条件。</p><p>比如新建时用户 id 不需要传入,但是修改时 id 必填。</p><p>如果是传统的 hibernate-validator 处理就会比较麻烦,此处引入条件注解。</p><h3>内置注解</h3><table><thead><tr><th align="left">序号</th><th align="left">注解</th><th align="left">说明</th></tr></thead><tbody><tr><td align="left">1</td><td align="left"><code>@EqualsCondition</code></td><td align="left">等于指定值的条件</td></tr><tr><td align="left">2</td><td align="left"><code>@NotEqualsCondition</code></td><td align="left">不等于指定值的条件</td></tr><tr><td align="left">3</td><td align="left"><code>@AlwaysTrueCondition</code></td><td align="left">永远生效的条件</td></tr><tr><td align="left">4</td><td align="left"><code>@AlwaysFalseCondition</code></td><td align="left">永远不生效的条件</td></tr></tbody></table><h3>使用</h3><p>使用起来也不难,下面的效果如下:</p><ol><li>operType == 'create' 时,name 的校验才会生效。</li><li>operType != 'create' 时,id 的校验才会生效。</li></ol><p>其他使用方式保持不变。</p><pre><code class="java">public class ConditionUser {
/**
* 操作类型
*/
@Ranges({"create", "edit"})
private String operType;
/**
* 新建时,name 必填
*/
@EqualsCondition(value = "create", fieldName = "operType")
@Size(min = 3)
@NotNull
private String name;
/**
* 不是新建时, id 字段必填
*/
@NotEqualsCondition(value = "create", fieldName = "operType")
@Size(min = 16)
private String id;
//getter & setter
}</code></pre><h2>过程式接口</h2><h3>说明</h3><p>日常开发中,我们都很喜欢使用注解对 java bean 进行校验。</p><p>但是这回导致我们定义的单个属性校验无法复用。</p><p>所以项目中的单个属性校验和注解是一一对应的,为了便于复用。</p><h3>ValidHelper 方法</h3><p>ValidHelper 作为统一封装的工具类,提供单个方法校验常见的方法。</p><p>和 java bean 类似,方法列表:</p><table><thead><tr><th align="left">序号</th><th align="left">方法</th><th align="left">返回值</th><th align="left">说明</th></tr></thead><tbody><tr><td align="left">1</td><td align="left">failOver(Object object, IConstraint constraint)</td><td align="left">IResult</td><td align="left">全部验证后返回</td></tr><tr><td align="left">2</td><td align="left">failFast(Object object, IConstraint constraint)</td><td align="left">IResult</td><td align="left">快速验证后返回</td></tr><tr><td align="left">3</td><td align="left">failOverThrow(Object object, IConstraint constraint)</td><td align="left">void</td><td align="left">全部验证后返回-未通过抛出 ValidRuntimeException 异常</td></tr><tr><td align="left">4</td><td align="left">failFastThrow(Object object, IConstraint constraint)</td><td align="left">void</td><td align="left">快速验证后返回-未通过抛出 ValidRuntimeException 异常</td></tr></tbody></table><h3>使用例子</h3><p>用法和 bean 的类似,只是入参多了第二个约束条件。</p><pre><code class="java">IResult result = ValidHelper.failFast("", Constraints.notEmptyConstraint());</code></pre><h3>IConstraint 对应关系</h3><p>注解和常见的接口方法一一对应,所有的约束方法在 <code>Constraints</code> 工具类中。</p><table><thead><tr><th align="left">序号</th><th align="left">注解</th><th align="left">说明</th><th align="left">对应方法</th></tr></thead><tbody><tr><td align="left">1</td><td align="left"><code>@AssertTrue</code></td><td align="left">为 true 约束条件</td><td align="left">assertTrueConstraint</td></tr><tr><td align="left">2</td><td align="left"><code>@AssertFalse</code></td><td align="left">为 false 约束条件</td><td align="left">assertFalseConstraint</td></tr><tr><td align="left">3</td><td align="left"><code>@Null</code></td><td align="left">为 null 约束条件</td><td align="left">nullConstraint</td></tr><tr><td align="left">4</td><td align="left"><code>@NotNull</code></td><td align="left">不为 null 约束条件</td><td align="left">notNullConstraint</td></tr><tr><td align="left">5</td><td align="left"><code>@Past</code></td><td align="left">是否在当前时间之前约束条件</td><td align="left">pastConstraint</td></tr><tr><td align="left">6</td><td align="left"><code>@Future</code></td><td align="left">是否在当前时间之后约束条件</td><td align="left">futureConstraint</td></tr><tr><td align="left">7</td><td align="left"><code>@Pattern</code></td><td align="left">正则表达式约束条件</td><td align="left">patternConstraint</td></tr><tr><td align="left">8</td><td align="left"><code>@Size</code></td><td align="left">在指定范围内的约束条件</td><td align="left">sizeConstraint</td></tr><tr><td align="left">9</td><td align="left"><code>@Digits</code></td><td align="left">数字位数的约束条件</td><td align="left">digitsConstraint</td></tr><tr><td align="left">10</td><td align="left"><code>@DecimalMax</code></td><td align="left">最大数字的约束条件</td><td align="left">decimalMaxConstraint</td></tr><tr><td align="left">11</td><td align="left"><code>@DecimalMin</code></td><td align="left">最小数字的约束条件</td><td align="left">decimalMinConstraint</td></tr><tr><td align="left">12</td><td align="left"><code>@Min</code></td><td align="left">最小的约束条件</td><td align="left">minConstraint</td></tr><tr><td align="left">13</td><td align="left"><code>@Max</code></td><td align="left">最大的约束条件</td><td align="left">maxConstraint</td></tr><tr><td align="left">13</td><td align="left"><code>@NotBlank</code></td><td align="left">不能为空格的约束条件</td><td align="left">notBlankConstraint</td></tr><tr><td align="left">14</td><td align="left"><code>@NotEmpty</code></td><td align="left">不能为空的约束条件</td><td align="left">notEmptyConstraint</td></tr><tr><td align="left">15</td><td align="left"><code>@Length</code></td><td align="left">长度的约束条件</td><td align="left">lengthConstraint</td></tr><tr><td align="left">16</td><td align="left"><code>@CNPJ</code></td><td align="left">CNPJ 约束条件</td><td align="left">cnpjConstraint</td></tr><tr><td align="left">17</td><td align="left"><code>@CPF</code></td><td align="left">CPF 约束条件</td><td align="left">cpfConstraint</td></tr><tr><td align="left">18</td><td align="left"><code>@URL</code></td><td align="left">URL 约束条件</td><td align="left">urlConstraint</td></tr><tr><td align="left">18</td><td align="left"><code>@Email</code></td><td align="left">Email 约束条件</td><td align="left">emailConstraint</td></tr><tr><td align="left">19</td><td align="left"><code>@UniqueElements</code></td><td align="left">元素唯一约束条件</td><td align="left">uniqueElementsConstraint</td></tr><tr><td align="left">20</td><td align="left"><code>@Range</code></td><td align="left">指定范围元素约束条件</td><td align="left">rangeConstraint</td></tr><tr><td align="left">21</td><td align="left"><code>@AllEquals</code></td><td align="left">当前字段及其指定的字段 全部相等</td><td align="left">allEqualsConstraint</td></tr><tr><td align="left">22</td><td align="left"><code>@EnumRanges</code></td><td align="left">当前字段必须在枚举值指定的范围内</td><td align="left">enumRangesConstraint</td></tr><tr><td align="left">23</td><td align="left"><code>@HasNotNull</code></td><td align="left">当前字段及其指定的字段 至少有一个不为 null</td><td align="left">hasNotNullConstraint</td></tr><tr><td align="left">24</td><td align="left"><code>@Ranges</code></td><td align="left">当前字段必须在指定的范围内</td><td align="left">rangesConstraint</td></tr></tbody></table><h4>条件注解</h4><p>注解和常见的接口方法一一对应,所有的约束方法在 <code>Conditions</code> 工具类中。</p><table><thead><tr><th align="left">序号</th><th align="left">注解</th><th align="left">说明</th><th align="left">对应方法</th></tr></thead><tbody><tr><td align="left">1</td><td align="left"><code>@EqualsCondition</code></td><td align="left">等于指定值的条件</td><td align="left">equalsCondition</td></tr><tr><td align="left">2</td><td align="left"><code>@NotEqualsCondition</code></td><td align="left">不等于指定值的条件</td><td align="left">notEqualsCondition</td></tr><tr><td align="left">3</td><td align="left"><code>@AlwaysTrueCondition</code></td><td align="left">永远生效的条件</td><td align="left">alwaysTrueCondition</td></tr><tr><td align="left">4</td><td align="left"><code>@AlwaysFalseCondition</code></td><td align="left">永远不生效的条件</td><td align="left">alwaysFalseCondition</td></tr></tbody></table><h2>注解自定义</h2><h3>说明</h3><p>内置的注解,自然无法满足所有的场景。</p><p>本项目中,约束和条件注解都是支持自定义的。</p><h3>约束注解 @Constraint</h3><p>所有系统的内置注解都可以作为学习的例子。</p><h4>定义注解</h4><p>以 <code>@AllEquals</code> 为例,核心的部分在 <code>@Constraint(AtAllEqualsConstraint.class)</code>。</p><p>我们在 AtAllEqualsConstraint 中实现具体的约束逻辑。</p><pre><code class="java">@Target(ElementType.FIELD)
@Retention(RetentionPolicy.RUNTIME)
@Constraint(AtAllEqualsConstraint.class)
public @interface AllEquals {
/**
* 当前字段及其指定的字段 全部相等
* 1. 字段类型及其他字段相同
* @return 指定的字段列表
*/
String[] value();
/**
* 提示消息
* @return 错误提示
*/
String message() default "";
/**
* 分组信息
* @return 分组类
* @since 0.1.2
*/
Class[] group() default {};
}</code></pre><h4>实现逻辑</h4><p>推荐直接继承 <code>AbstractAnnotationConstraint<A></code>,实现对应的逻辑即可。</p><pre><code class="java">public class AtAllEqualsConstraint extends AbstractAnnotationConstraint<AllEquals> {
@Override
protected IConstraint buildConstraint(AllEquals annotation) {
return Constraints.allEqualsConstraint(annotation.value());
}
}</code></pre><h3>条件注解 @Condition</h3><p>所有系统的内置注解都可以作为学习的例子。</p><h4>定义注解</h4><p>以 <code>@AlwaysTrueCondition</code> 为例,核心的部分在 <code>@Condition(AtAlwaysTrueCondition.class)</code>。</p><p>我们在 AtAlwaysTrueCondition 中实现具体的约束逻辑。</p><pre><code class="java">@Target(ElementType.FIELD)
@Retention(RetentionPolicy.RUNTIME)
@Condition(AtAlwaysTrueCondition.class)
public @interface AlwaysTrueCondition {
}</code></pre><h4>实现逻辑</h4><p>推荐直接继承 <code>AbstractAnnotationCondition<A></code>,实现对应的逻辑即可。</p><pre><code class="java">public class AtAlwaysTrueCondition extends AbstractAnnotationCondition<AlwaysTrueCondition> {
@Override
protected ICondition buildCondition(AlwaysTrueCondition annotation) {
return Conditions.alwaysTrueCondition();
}
}</code></pre><h2>开源地址</h2><p>为了便于大家学习使用,目前校验框架已开源。</p><p>欢迎大家 fork+star,鼓励一下老马~</p><blockquote><a href="https://link.segmentfault.com/?enc=bODilryvGa5LOT8z4Mv6qQ%3D%3D.%2Bsg4Un9vP3S4H1opB0rQlj52Hf%2BRXBhhRoaRLduWuLfc84cAuhxBeQN5IhIILwik" rel="nofollow">validator</a></blockquote>
java 中文繁简体转换工具 opencc4j 使用介绍 1.8.0
https://segmentfault.com/a/1190000043536187
2023-03-14T13:37:08+08:00
2023-03-14T13:37:08+08:00
老马啸西风
https://segmentfault.com/u/houbinbin
2
<h2>Opencc4j</h2><p><a href="https://link.segmentfault.com/?enc=LQsMQVxU%2FIGYfnXXB9ilmw%3D%3D.lNIxX%2FvV4Cs2VTHusGpwgbsDWtAhtizOEOS5jT3k%2F9BUKowJpLJWGh9CKX7GFYRW" rel="nofollow">Opencc4j</a> 支持中文繁简体转换,考虑到词组级别。</p><blockquote><a href="https://link.segmentfault.com/?enc=3Jpj9vDsww0BSFqRBjIZwQ%3D%3D.oMNjtbADgtQ7RCGvZ1gCnvWMAr78cl3ESPuMfvBKGXlQnWFF0fLTiKNbXYqPiMyb" rel="nofollow">在线体验</a></blockquote><h3>Features 特点</h3><ul><li>严格区分「一简对多繁」和「一简对多异」。</li><li>完全兼容异体字,可以实现动态替换。</li><li>严格审校一简对多繁词条,原则为「能分则不合」。</li><li>词库和函数库完全分离,可以自由修改、导入、扩展。</li><li>兼容 Windows、Linux、Mac 平台。</li><li>支持自定义分词</li><li>支持判断单个字(词)是否为简体/繁体</li><li>支持返回字符串中简体/繁体的列表信息</li><li>支持中国台湾地区繁简体转换</li></ul><h4>v1.8.0 版本变更</h4><ul><li>丰富工具类方法</li><li>优化繁简体判断逻辑</li></ul><blockquote><a href="CHANGELOG.md">变更日志</a></blockquote><h3>创作缘由</h3><ul><li>OpenCC</li></ul><p><a href="https://link.segmentfault.com/?enc=6fJCF50EQWTU%2BvOrgXNAyA%3D%3D.ePTKA8euLXCkNc0O3%2FU2m%2FHsSLBq%2Fk%2F13IloWW%2F7vFG9MbiZbIg2mdF6HUAtdYNl" rel="nofollow">OpenCC</a> 的思想非常优秀,做的也特别棒。但是没有特别为 java 提供的工具。</p><ul><li>jopencc</li></ul><p><a href="https://link.segmentfault.com/?enc=ipDj3uFhsx%2B1BcavnUc12Q%3D%3D.ftqV1mVKfehr%2FQWE%2FsXNraAHfuMrBHwXRRgmkWbTHf82nkribSU4EkRxFiQrQH%2Bb" rel="nofollow">jopencc</a> 没有提供分词功能。</p><h2>快速开始</h2><h3>maven 引入</h3><pre><code class="xml"><dependency>
<groupId>com.github.houbb</groupId>
<artifactId>opencc4j</artifactId>
<version>1.8.0</version>
</dependency></code></pre><h3>api 概览</h3><p>核心工具列表如下:</p><table><thead><tr><th align="left">序号</th><th align="left">工具类</th><th align="left">简介</th></tr></thead><tbody><tr><td align="left">1</td><td align="left">ZhConverterUtil</td><td align="left">基础的繁简体转换</td></tr><tr><td align="left">2</td><td align="left">ZhTwConverterUtil</td><td align="left">台湾地区的繁简体转换</td></tr></tbody></table><p>所有的工具类方法具有相同的方法设计,便于记忆。</p><p>核心方法如下:</p><table><thead><tr><th align="left">序号</th><th align="left">api 方法</th><th align="left">简介</th></tr></thead><tbody><tr><td align="left">1</td><td align="left">toSimple(String)</td><td align="left">转为简体</td></tr><tr><td align="left">2</td><td align="left">toTraditional(String)</td><td align="left">转为繁体</td></tr><tr><td align="left">3</td><td align="left">simpleList(String)</td><td align="left">返回包含的简体列表</td></tr><tr><td align="left">4</td><td align="left">traditionalList(String)</td><td align="left">返回包含的繁体列表</td></tr><tr><td align="left">5</td><td align="left">toSimple(char)</td><td align="left">返回单个汉字对应的所有简体字列表</td></tr><tr><td align="left">6</td><td align="left">toTraditional(char)</td><td align="left">返回单个汉字对应的所有繁体字列表</td></tr><tr><td align="left">7</td><td align="left">isSimple(String)</td><td align="left">是否全部为简体</td></tr><tr><td align="left">8</td><td align="left">isSimple(char)</td><td align="left">单个字符是否为简体</td></tr><tr><td align="left">9</td><td align="left">containsSimple(String)</td><td align="left">字符中是否为包含简体</td></tr><tr><td align="left">10</td><td align="left">isTraditional(String)</td><td align="left">是否全部为繁体</td></tr><tr><td align="left">11</td><td align="left">isTraditional(char)</td><td align="left">单个字符是否为繁体</td></tr><tr><td align="left">12</td><td align="left">containsTraditional(String)</td><td align="left">字符中是否为包含繁体</td></tr><tr><td align="left">13</td><td align="left">isChinese(String)</td><td align="left">是否全部为中文</td></tr><tr><td align="left">14</td><td align="left">isChinese(char)</td><td align="left">单个字符是否为中文</td></tr><tr><td align="left">15</td><td align="left">containsChinese(char)</td><td align="left">字符串中是否包含中文</td></tr></tbody></table><h3>繁简体转换</h3><h4>转为简体 toSimple</h4><pre><code class="java">String original = "生命不息,奮鬥不止";
String result = ZhConverterUtil.toSimple(original);
Assert.assertEquals("生命不息,奋斗不止", result);</code></pre><h4>转为繁体 toTraditional</h4><pre><code class="java">String original = "生命不息,奋斗不止";
String result = ZhConverterUtil.toTraditional(original);
Assert.assertEquals("生命不息,奮鬥不止", result);</code></pre><h3>繁简体判断</h3><p>对单个字符或者词组进行繁简体判断。</p><h4>是否为简体 isSimple</h4><pre><code class="java">Assert.assertTrue(ZhConverterUtil.isSimple('奋'));
Assert.assertTrue(ZhConverterUtil.isSimple("奋"));
Assert.assertTrue(ZhConverterUtil.isSimple("奋斗"));
Assert.assertFalse(ZhConverterUtil.isSimple('奮'));
Assert.assertFalse(ZhConverterUtil.isSimple("奮"));
Assert.assertFalse(ZhConverterUtil.isSimple("奮鬥"));
Assert.assertFalse(ZhConverterUtil.isSimple("奮斗"));
Assert.assertFalse(ZhConverterUtil.isSimple("beef"));</code></pre><h4>是否包含简体 containsSimple</h4><pre><code class="java">Assert.assertTrue(ZhConverterUtil.containsSimple("奋"));
Assert.assertTrue(ZhConverterUtil.containsSimple("奋斗"));
Assert.assertTrue(ZhConverterUtil.containsSimple("奋斗2023"));
Assert.assertFalse(ZhConverterUtil.containsSimple("編"));
Assert.assertFalse(ZhConverterUtil.containsSimple("編號"));</code></pre><h4>是否为繁体 isTraditional</h4><pre><code class="java">Assert.assertTrue(ZhConverterUtil.isTraditional('編'));
Assert.assertTrue(ZhConverterUtil.isTraditional("編"));
Assert.assertTrue(ZhConverterUtil.isTraditional("編號"));
Assert.assertFalse(ZhConverterUtil.isTraditional('编'));
Assert.assertFalse(ZhConverterUtil.isTraditional("编"));
Assert.assertFalse(ZhConverterUtil.isTraditional("编号"));
Assert.assertFalse(ZhConverterUtil.isTraditional("编號"));</code></pre><h4>是否包含繁体 containsTraditional</h4><pre><code class="java">Assert.assertTrue(ZhConverterUtil.containsTraditional("編"));
Assert.assertTrue(ZhConverterUtil.containsTraditional("編號"));
Assert.assertTrue(ZhConverterUtil.containsTraditional("編號2023"));
Assert.assertFalse(ZhConverterUtil.containsTraditional("号"));
Assert.assertFalse(ZhConverterUtil.containsTraditional("编号"));</code></pre><h3>句子中包含的繁简体列表返回</h3><p>返回字符串中繁简体对应的词、字列表,默认支持中文分词。</p><p>繁简体列表返回的词组和分词策略紧密相关。</p><h4>简体列表 simpleList</h4><pre><code class="java">final String original = "生命不息奋斗不止";
final List<String> resultList = ZhConverterUtil.simpleList(original);
Assert.assertEquals("[生, 命, 不, 息, 奋斗, 不, 止]", resultList.toString());</code></pre><h4>繁体列表 traditionalList</h4><p>PS: 很多字是同体字。</p><pre><code class="java">final String original = "生命不息奮鬥不止";
final List<String> resultList = ZhConverterUtil.traditionalList(original);
Assert.assertEquals("[生, 命, 不, 息, 奮, 鬥, 不, 止]", resultList.toString());</code></pre><h3>单个汉字对应的繁简体列表</h3><h4>繁体字列表</h4><pre><code class="java">Assert.assertEquals("[幹, 乾, 干]", ZhConverterUtil.toTraditional('干').toString());
Assert.assertEquals("[發, 髮]", ZhConverterUtil.toTraditional('发').toString());</code></pre><h4>简体字列表</h4><pre><code class="java">Assert.assertEquals("[测]", ZhConverterUtil.toSimple('測').toString());</code></pre><h3>中文工具方法</h3><h4>是否为中文 isChinese</h4><pre><code class="java">Assert.assertTrue(ZhConverterUtil.isChinese("你"));
Assert.assertTrue(ZhConverterUtil.isChinese("你好"));
Assert.assertTrue(ZhConverterUtil.isChinese('你'));
Assert.assertFalse(ZhConverterUtil.isChinese("你0"));
Assert.assertFalse(ZhConverterUtil.isChinese("10"));
Assert.assertFalse(ZhConverterUtil.isChinese('0'));
Assert.assertFalse(ZhConverterUtil.isChinese(""));
Assert.assertFalse(ZhConverterUtil.isChinese(null));</code></pre><h4>是否包含中文 containsChinese</h4><pre><code class="java">Assert.assertTrue(ZhConverterUtil.containsChinese("你"));
Assert.assertTrue(ZhConverterUtil.containsChinese("你好"));
Assert.assertTrue(ZhConverterUtil.containsChinese("你0"));
Assert.assertFalse(ZhConverterUtil.containsChinese("10"));
Assert.assertFalse(ZhConverterUtil.containsChinese(""));
Assert.assertFalse(ZhConverterUtil.containsChinese(null));</code></pre><h2>中国台湾繁简体转换</h2><h3>工具类</h3><p>为保证方法的一致性,引入 <code>ZhTwConverterUtil</code> 工具类,支持方法和 <code>ZhConverterUtil</code> 保持一致。</p><h3>测试用例</h3><p>简体到繁体:</p><pre><code class="java">String original = "使用互联网";
String result = ZhTwConverterUtil.toTraditional(original);
Assert.assertEquals("使用網際網路", result);</code></pre><p>繁体到简体:</p><pre><code class="java">String original = "使用網際網路";
String result = ZhTwConverterUtil.toSimple(original);
Assert.assertEquals("使用互联网", result);</code></pre><h2>配置引导类</h2><h3>引导类说明</h3><p>主要的可配置项包含了分词和数据集合。</p><p>二者都是可以配置,并且支持自定的。</p><h4>默认配置</h4><p>默认工具类等价于如下:</p><pre><code class="java">ZhConvertBootstrap.newInstance()
.segment(Segments.defaults())
.dataMap(DataMaps.defaults());</code></pre><h4>中国台湾地区配置</h4><p>中国台湾地区配置等价于:</p><pre><code class="java">ZhConvertBootstrap.newInstance()
.segment(Segments.defaults())
.dataMap(DataMaps.taiwan());</code></pre><h3>中文分词策略</h3><h4>系统内置分词方式</h4><p>你可以通过 <code>Segments</code> 工具类获取系统内置的分词实现。</p><table><thead><tr><th align="left">序号</th><th align="left">方法</th><th align="left">准确性</th><th align="left">性能</th><th align="left">备注</th></tr></thead><tbody><tr><td align="left">1</td><td align="left">defaults()</td><td align="left">高</td><td align="left">高</td><td align="left">默认分词形式,暂时为 <code>fastForward</code> 策略</td></tr><tr><td align="left">2</td><td align="left">fastForward()</td><td align="left">较高</td><td align="left">高</td><td align="left">fast-forward 分词策略</td></tr><tr><td align="left">3</td><td align="left">chars()</td><td align="left">低</td><td align="left">高</td><td align="left">将字符串转换为单个字符列表,一般不建议使用</td></tr><tr><td align="left">4</td><td align="left">huaBan()</td><td align="left">高</td><td align="left">一般</td><td align="left">花瓣的结巴分词策略</td></tr></tbody></table><h4>花瓣结巴分词</h4><p>花瓣结巴分词在使用时,需要自行引入结巴分词依赖。</p><pre><code class="xml"><dependency>
<groupId>com.huaban</groupId>
<artifactId>jieba-analysis</artifactId>
<version>1.0.2</version>
</dependency></code></pre><h4>自定义</h4><p>你有时候可能除了上述的两种分词方式,会有更加适合自己业务的分词实现。</p><p>Opencc4j 支持自定义分词实现,只需要实现分词接口 <a href="https://link.segmentfault.com/?enc=%2BrkQneuYaYAeMxGXklz8Qw%3D%3D.rbwtLrVIcbqejGLg56TKBEhOIpbsO%2FKKae%2Fg0l%2FxeJhE5jyJ2K25tcdvVkMn9%2BWePQFGGeEX8VPvOxw2yn4c%2BBmEGuYqZ82mOmI45tC66%2FwDfUIjkLje5j%2BpJEPJQlp4FFPLJgoXgDmXRWtg7lmvDE3bD1XxDPCsJ88Qgv7n%2B4s%3D" rel="nofollow">Segment</a></p><ul><li>接口内容</li></ul><pre><code class="java">public interface Segment {
/**
* 分词
* @param original 原始信息
* @return 分词后的列表
*/
List<String> seg(final String original);
}</code></pre><h3>测试代码</h3><h4>自定义分词实现类</h4><pre><code class="java">/**
* 一个最简单的分词实现。
* 注意:仅仅做演示,不可实际使用。
*/
public class FooSegment implements Segment {
@Override
public List<String> seg(String original) {
return Arrays.asList(original, "测试");
}
}</code></pre><h4>分词测试</h4><p>我们自定义的分词,直接在默认添加“测试”这样的信息。</p><pre><code class="java">final String original = "寥落古行宫,宫花寂寞红。白头宫女在,闲坐说玄宗。";
final Segment segment = new FooSegment();
final String result = ZhConvertBootstrap.newInstance()
.segment(segment)
.toTraditional(original);
Assert.assertEquals("寥落古行宮,宮花寂寞紅。白頭宮女在,閒坐說玄宗。測試", result);</code></pre><h3>数据接口自定义</h3><p>不同的地区,对应的转换规则是不同的。</p><p>具体参考一下台湾地区的使用方式即可。</p><h4>接口说明</h4><p>IDataMap 的接口如下。</p><pre><code class="java">/**
* 数据 map 接口
* @author binbin.hou
* @since 1.5.2
*/
public interface IDataMap {
/**
* 繁体=》简体 词组
* @return 结果
* @since 1.5.2
*/
Map<String, List<String>> tsPhrase();
/**
* 繁体=》简体 单个字
* @return 结果
* @since 1.5.2
*/
Map<String, List<String>> tsChar();
/**
* 简体=》繁体 词组
* @return 结果
* @since 1.5.2
*/
Map<String, List<String>> stPhrase();
/**
* 简体=》繁体 单个字
* @return 结果
* @since 1.5.2
*/
Map<String, List<String>> stChar();
/**
* 繁体字所有字符
* @return 繁体字所有字符
* @since 1.6.2
*/
Set<String> tChars();
/**
* 简体字所有字符
* @return 繁体字所有字符
* @since 1.8.0
*/
Set<String> sChars();
}</code></pre><h4>自定义说明</h4><p>如果需要拓展对应的数据,建议继承原始的实现,然后添加额外的数据信息即可。</p><p>可以参考 <a href="https://link.segmentfault.com/?enc=mbQz41n1UhbHWPrGFheO1w%3D%3D.U6mwZMFlQWsKtveeGweiUKW%2BBZTk0kwXS5mH3B3WKefe6eHZEg5k1YeuGMQNpai0Fyqh8MUsAsh5J%2BgCOrIs17AfUtlTsHb5QAH9Y%2BmP3bRvhnHA8p7zUOKw9bexqxoBjVqBbHAkUPtNLVfg%2Fy5dLw%3D%3D" rel="nofollow">中国台湾地区实现</a></p><p>ps: 后续考虑引入更加简单的实现方式,比如基于文本拓展,不过可扩展性没有接口灵活。</p><h2>技术鸣谢</h2><h3>OpenCC</h3><p><a href="https://link.segmentfault.com/?enc=9019%2BGD0W%2FbARLCFP0%2BnXg%3D%3D.%2Bq14cLk2JFQH0kspuik49IqbH9WmGRRF5PTCqXlh0IHGwxdl%2FTpXhhUHgcGZn%2FqY" rel="nofollow">OpenCC</a> 提供的原始数据信息。</p><h3>花瓣</h3><p><a href="https://link.segmentfault.com/?enc=IZoRTpZjMOtdO8Fme6xv8g%3D%3D.v7DiVbm1uqnuAnpLDT66GOoxiBQEkNfnNSg4%2BsmaVFb8WpJV%2Fjw2SP2Ti7OdZJSH" rel="nofollow">jieba-analysis</a> 提供中文分词</p><h2>Issues & Bugs</h2><p><a href="https://link.segmentfault.com/?enc=Lb07NdUCcdFYWfpwPYg2jQ%3D%3D.d1Sk7J9FSEQW6rnUD%2BxxSjwEtRBLw%2FqTwxTo0kHTGBIhh0UeAQJQU0VShKIqe8SR" rel="nofollow">需求和 BUG</a> 在这里,欢迎提供宝贵的建议。</p><p>如果对您有帮助,欢迎 Star 鼓励作者。</p><h2>NLP 开源矩阵</h2><p><a href="https://link.segmentfault.com/?enc=5OHcW7ZoSdegd70jXzOqwQ%3D%3D.slLo6KkX7HCy6UUYTQSvg%2BzK68mhJXPW%2FIn8lVhnKPQ%3D" rel="nofollow">pinyin 汉字转拼音</a></p><p><a href="https://link.segmentfault.com/?enc=xa0k3dLUdAHxdRv7N6a5JA%3D%3D.3tfMY5bSB4j5VMiKfg8TtNPv3rfwQ%2FdMUMaWi5iUpNT2Fg%2BwqZpqsoM6WBjAYr%2BB" rel="nofollow">pinyin2hanzi 拼音转汉字</a></p><p><a href="https://link.segmentfault.com/?enc=7qaHIjo%2BtSt3nM6hXIbDVw%3D%3D.rv4eZwMDJlmKCKG%2BZvhr9lVvCOAwEoUdLfWBm01X9E1H6KFqfuazLPdfBOmUNvKR" rel="nofollow">segment 高性能中文分词</a></p><p><a href="https://link.segmentfault.com/?enc=5Cwc5Z0alRoJxd%2FN2SchJQ%3D%3D.xDTulc%2Fn4j2KsYV0YcpZe1NwId78pUmnOmWsvN5jkY3ZfmZS2UuYkuwlvkXcxXG0" rel="nofollow">opencc4j 中文繁简体转换</a></p><p><a href="https://link.segmentfault.com/?enc=cxYfhPjTIHgcprADUVAh%2Fg%3D%3D.4i3KMRp4V%2B7%2Beai492qVNPQ%2BTj3P5%2FGTOrOai7TKIMvH1D06g7ooV8nlngRnOl9k" rel="nofollow">nlp-hanzi-similar 汉字相似度</a></p><p><a href="https://link.segmentfault.com/?enc=fhgYuL4PnYRsZ7CB%2FTPgAg%3D%3D.ZARi0WZawWTvvUzSa1TCKrWEpvYgmgza7qH4nyeNvy6ztEC9MYkeG99TrDmd5c6x" rel="nofollow">word-checker 拼写检测</a></p><p><a href="https://link.segmentfault.com/?enc=Kmr8%2BOqGizDuZw%2BdHfDxhQ%3D%3D.SZzO3vM6AJEA59ot%2F%2BcTKAVRY9geoNCI1ssl6eaSCBsdW6jObprGVFfkdobIgipN" rel="nofollow">sensitive-word 敏感词</a></p><h2>后期 Road-Map</h2><ul><li>[ ] 数据字典插件化</li><li>[ ] 考虑长文本分段,并行转换</li></ul>
rate-limit 一款 java 开源渐进式分布式限流框架使用介绍
https://segmentfault.com/a/1190000043063753
2022-12-13T17:44:47+08:00
2022-12-13T17:44:47+08:00
老马啸西风
https://segmentfault.com/u/houbinbin
0
<h2>项目简介</h2><p><a href="https://link.segmentfault.com/?enc=dqdjxTFPFQ%2B14gPUyq9UmA%3D%3D.33nSs94F2mfndO16Iso2msku%2BAvh37klOVZ7OJN1Wzf6ty4eTH7I8k05nypDGacB" rel="nofollow">rate-limit</a> 是一个为 java 设计的渐进式限流工具。</p><p>目的是为了深入学习和使用限流,后续将会持续迭代。</p><h3>特性</h3><ul><li>渐进式实现</li><li>支持独立于 spring 使用</li><li>支持整合 spring</li><li>支持整合 spring-boot</li><li>内置多种限流策略</li></ul><p><img src="/img/remote/1460000043063755" alt="在这里插入图片描述" title="在这里插入图片描述"></p><h2>快速开始</h2><h3>需求</h3><ul><li>jdk 1.7</li><li>maven 3.x+</li></ul><h3>maven 导入</h3><pre><code class="xml"><dependency>
<groupId>com.github.houbb</groupId>
<artifactId>rate-limit-core</artifactId>
<version>1.1.0</version>
</dependency></code></pre><h3>入门例子</h3><h4>方法定义</h4><p><code>@RateLimit</code> 限流注解放在方法上,指定对应的限制频率。</p><p>也可以定义在类上,默认下面的所有方法生效。方法上的优先级高于类。</p><table><thead><tr><th align="left">属性</th><th align="left">说明</th><th align="left">默认值</th></tr></thead><tbody><tr><td align="left">value</td><td align="left">方法访问一次消耗的令牌数</td><td align="left"><code>1</code></td></tr><tr><td align="left">timeUnit</td><td align="left">时间单位</td><td align="left"><code>TimeUnit.SECONDS</code></td></tr><tr><td align="left">interval</td><td align="left">时间间隔</td><td align="left"><code>60</code></td></tr><tr><td align="left">count</td><td align="left">可调用次数</td><td align="left"><code>1000</code></td></tr><tr><td align="left">enable</td><td align="left">是否启用</td><td align="left">true</td></tr></tbody></table><p>默认为 60S 内,可以调用 1000 次。</p><pre><code class="java">public class UserService {
@RateLimit(interval = 2, count = 5)
public void limitCount() {
log.info("{}", Thread.currentThread().getName());
}
}</code></pre><p>这个例子中我们 2S 内最多调用 5 次。</p><h4>代码测试</h4><p><code>RateLimitProxy.getProxy(xxx)</code> 通过字节码获取方法对应的方法代理。</p><pre><code class="java">@Test(expected = RateLimitRuntimeException.class)
public void limitCountErrorTest() {
UserService userService = RateLimitProxy.getProxy(new UserService());
for(int i = 0; i < 3; i++) {
userService.limitCount();
}
}</code></pre><p>当调用超出限制时,默认抛出 <code>RateLimitRuntimeException</code> 异常。</p><p>这里默认使用的是令牌桶算法,所以会出现异常。</p><h4>重复注解 @RateLimits</h4><p>有时候我们希望同时做多个的限制:</p><p>(1)一分钟不超过 10 次</p><p>(2)一小时不超过 30 次</p><p>为了支持多个配置,我们引入了新的注解 <code>@RateLimits</code>,可以指定一个 <code>@RateLimit</code> 数组。</p><p>方法上同时使用 <code>@RateLimits</code> + <code>@RateLimit</code> 是可以同时生效的,不过为了简单,一般不建议混合使用。</p><pre><code class="java">@RateLimits({@RateLimit(interval = 2, count = 5)})
public void limitCount() {
//...
}</code></pre><h4>指定引导类</h4><pre><code class="java">RateLimitProxy.getProxy(new UserService());</code></pre><p>等价于</p><pre><code class="java">RateLimitProxy.getProxy(new UserService(), RateLimitBs.newInstance());</code></pre><p>下面我们来一起看一下 RateLimitBs 引导类。</p><h3>引导类</h3><p><code>RateLimitBs</code> 作为引导类,便于用户自定义配置。</p><table><thead><tr><th align="left">方法</th><th align="left">说明</th><th align="left">默认值</th></tr></thead><tbody><tr><td align="left">rateLimit</td><td align="left">限流策略</td><td align="left"><code>RateLimits.tokenBucket()</code> 令牌桶算法</td></tr><tr><td align="left">timer</td><td align="left">时间策略</td><td align="left"><code>Timers.system()</code> 系统时间</td></tr><tr><td align="left">cacheService</td><td align="left">缓存策略</td><td align="left"><code>CommonCacheServiceMap</code> 基于本地 map 的缓存策略</td></tr><tr><td align="left">cacheKeyNamespace</td><td align="left">缓存KEY命名空间</td><td align="left"><code>RATE-LIMIT</code> 避免不同的应用,命名冲突。</td></tr><tr><td align="left">configService</td><td align="left">限制配置策略</td><td align="left"><code>RateLimitConfigService</code> 默认基于方法上的注解</td></tr><tr><td align="left">tokenService</td><td align="left">身份标识策略</td><td align="left"><code>RateLimitTokenService</code> 默认基于 IP</td></tr><tr><td align="left">methodService</td><td align="left">方法标识策略</td><td align="left"><code>RateLimitMethodService</code> 默认基于方法名+参数类型</td></tr><tr><td align="left">rejectListener</td><td align="left">拒绝策略</td><td align="left"><code>RateLimitRejectListenerException</code> 限流时抛出异常</td></tr></tbody></table><p>其中 rateLimit 内置 <code>RateLimits</code> 工具中的策略如下:</p><table><thead><tr><th align="left">方法</th><th align="left">说明</th></tr></thead><tbody><tr><td align="left">fixedWindow()</td><td align="left">固定窗口</td></tr><tr><td align="left">slideWindow(int windowNum)</td><td align="left">滑动窗口,可指定窗口大小</td></tr><tr><td align="left">slideWindow()</td><td align="left">滑动窗口,默认为 10</td></tr><tr><td align="left">slideWindowQueue()</td><td align="left">滑动窗口,基于队列的实现</td></tr><tr><td align="left">leakyBucket()</td><td align="left">漏桶算法</td></tr><tr><td align="left">tokenBucket()</td><td align="left">令牌桶算法</td></tr></tbody></table><h4>配置建议</h4><ol><li>分布式系统,cacheService 建议使用基于 redis 的集中式缓存策略。</li><li>configService 如果想更加灵活,可以基于数据库的配置查询</li></ol><h4>RateLimitBs 引导类</h4><p>RateLimitBs 默认配置如下:</p><pre><code class="java">RateLimitBs.newInstance()
.timer(Timers.system())
.methodService(new RateLimitMethodService())
.tokenService(new RateLimitTokenService())
.rejectListener(new RateLimitRejectListenerException())
.configService(new RateLimitConfigService())
.cacheService(new CommonCacheServiceMap())
.rateLimit(RateLimits.tokenBucket())
.cacheKeyNamespace(RateLimitConst.DEFAULT_CACHE_KEY_NAMESPACE);</code></pre><h2>spring 整合</h2><h3>maven 引入</h3><pre><code class="xml"><dependency>
<groupId>com.github.houbb</groupId>
<artifactId>rate-limit-spring</artifactId>
<version>1.1.0</version>
</dependency></code></pre><h3>类定义</h3><h4>方法</h4><p>和上面使用类似,直接在方法上声明 <code>@RateLimit</code> 注解即可。</p><pre><code class="java">@Service
public class UserService {
private static final Log log = LogFactory.getLog(UserService.class);
@RateLimit(interval = 2, count = 5)
public void limitCount() {
log.info("{}", Thread.currentThread().getName());
}
}</code></pre><h4>配置</h4><p>通过 <code>@EnableRateLimit</code> 声明启用限流。</p><pre><code class="java">@Configuration
@ComponentScan("com.github.houbb.rate.limit.test.core")
@EnableRateLimit
public class SpringConfig {
}</code></pre><p><code>@EnableRateLimit</code> 的属性配置和 RateLimitBs 属性是以一一对应的。</p><table><thead><tr><th align="left">方法</th><th align="left">说明</th><th align="left">默认值</th></tr></thead><tbody><tr><td align="left">rateLimit</td><td align="left">限流策略</td><td align="left">令牌桶算法</td></tr><tr><td align="left">timer</td><td align="left">时间策略</td><td align="left">系统时间</td></tr><tr><td align="left">cacheService</td><td align="left">缓存策略</td><td align="left">基于本地 map 的缓存策略</td></tr><tr><td align="left">cacheKeyNamespace</td><td align="left">缓存KEY命名空间</td><td align="left"><code>RATE-LIMIT</code> 避免不同的应用,命名冲突。</td></tr><tr><td align="left">configService</td><td align="left">限制配置策略</td><td align="left">默认基于方法上的注解</td></tr><tr><td align="left">tokenService</td><td align="left">身份标识策略</td><td align="left">默认基于 IP</td></tr><tr><td align="left">methodService</td><td align="left">方法标识策略</td><td align="left">默认基于方法名+参数类型</td></tr><tr><td align="left">rejectListener</td><td align="left">拒绝策略</td><td align="left">限流时抛出异常</td></tr></tbody></table><p>这里的属性值,都是对应的 spring bean 名称,支持用户自定义。</p><h2>spring-boot 整合</h2><h3>maven 引入</h3><pre><code class="xml"><dependency>
<groupId>com.github.houbb</groupId>
<artifactId>rate-limit-springboot-starter</artifactId>
<version>1.1.0</version>
</dependency></code></pre><h3>使用</h3><p>其他和 spring 保持一致。</p><h2>缓存相关工具</h2><p><a href="https://link.segmentfault.com/?enc=8XmFPq0ds7RMUIqaqb2RpQ%3D%3D.H5N%2F2Qn366PEh1dKIaYuFHmRX2xuh%2BqcVMtMcD6yT8U%3D" rel="nofollow">cache: 手写渐进式 redis</a></p><p><a href="https://link.segmentfault.com/?enc=9cxukWtMPVRNqxGsoz%2FExw%3D%3D.dGR8YzjbBNGU42hZ4Iy1srlC%2B%2BcCzBuEGQ6wNi6MYAUxBL480An4hxrNQErFGNcP" rel="nofollow">common-cache: 通用缓存标准定义</a></p><p><a href="https://link.segmentfault.com/?enc=7hDqt697%2FRgcYQnq2ZBJTA%3D%3D.copo%2BDqTDrjGL6AmLHk47aFgU2nzt5YHL8MQEz%2FcjWu%2BuVLYLFrTzatm02kta87G" rel="nofollow">redis-config: 兼容各种常见的 redis 配置模式</a></p><p><a href="https://link.segmentfault.com/?enc=gPGGxjnQm2c53661mDzEHw%3D%3D.Xfku0yOSXGUvYdlXMWIYju%2F45y%2B2GN%2FaYvC6r9EaTCQ%3D" rel="nofollow">lock: 开箱即用的分布式锁</a></p><p><a href="https://link.segmentfault.com/?enc=J4mvkY8%2F%2B5DVdZjMG1MqBA%3D%3D.kn%2BS5dchNxS3j1vXcbE3zbhU5NvJhCDGifeAju6OWO5W32MtXBfHueBApiszXyLU" rel="nofollow">resubmit: 防重复提交</a></p><p><a href="https://link.segmentfault.com/?enc=OsczlSgRSzB1zKcwzefJzw%3D%3D.Fi6ZNhcb1AlINvM2q6Xch9MzQR3sDma85j3v84DkoztFhzH13%2FJWERBTIrJq1L2l" rel="nofollow">rate-limit: 限流</a></p>
java 实现开箱即用基于 redis 的分布式锁
https://segmentfault.com/a/1190000043005699
2022-12-08T14:16:04+08:00
2022-12-08T14:16:04+08:00
老马啸西风
https://segmentfault.com/u/houbinbin
0
<h2>项目简介</h2><p><a href="https://link.segmentfault.com/?enc=voCsV4uR1mloIsRaVWsdKQ%3D%3D.qmFbPC61g0EzFXYlrZ9agtTyh28hrj0CC5HN87tnfxE%3D" rel="nofollow">lock</a> 为 java 设计的分布式锁,开箱即用,纵享丝滑。</p><p>开源地址:<a href="https://link.segmentfault.com/?enc=jtlcbudbVAvFmsAMxCPlFA%3D%3D.ulyMDLSCspNdu5i7EdIC%2BXRCFXfY0CQpAARC8RquU10%3D" rel="nofollow">https://github.com/houbb/lock</a></p><h3>目的</h3><ul><li>开箱即用,支持注解式和过程式调用</li><li>基于 redis 的分布式锁</li><li>内置支持多种 redis 的整合方式</li><li>渐进式设计,可独立于 spring 使用</li><li>整合 spring</li><li>整合 spring-boot</li></ul><h2>快速开始</h2><h3>需要</h3><p>jdk1.7+</p><p>maven 3.x+</p><h3>maven 引入</h3><pre><code class="xml"><dependency>
<groupId>com.github.houbb</groupId>
<artifactId>lock-core</artifactId>
<version>1.3.0</version>
</dependency></code></pre><h3>入门例子</h3><p>基于本地 redis 的测试案例。</p><pre><code class="java">public void helloTest() {
ILock lock = LockBs.newInstance();
String key = "ddd";
try {
// 加锁
lock.tryLock(key);
System.out.println("业务处理");
} catch (Exception e) {
throw new RuntimeException(e);
} finally {
// 释放锁
lock.unlock(key);
}
}</code></pre><h4>配置化</h4><p>为了便于拓展,LockBs 的配置支持自定义:</p><pre><code class="java">LockBs.newInstance()
.id(Ids.uuid32()) //id 生成策略
.cache(JedisRedisServiceFactory.pooled("127.0.0.1", 6379)) //缓存策略
.lockSupport(new RedisLockSupport()) // 锁实现策略
.lockKeyFormat(new LockKeyFormat()) // 针对 key 的格式化处理策略
.lockReleaseFailHandler(new LockReleaseFailHandler()) //释放锁失败处理
;</code></pre><h2>整合 spring</h2><h3>maven 引入</h3><pre><code class="xml"><dependency>
<groupId>com.github.houbb</groupId>
<artifactId>lock-spring</artifactId>
<version>1.3.0</version>
</dependency></code></pre><h3>指定 bean 使用</h3><h4>启用分布式锁</h4><p><code>@EnableLock</code> 启用分布式锁。</p><p><code>@EnableRedisConfig</code> 启用 redis 的默认配置。</p><pre><code class="xml">@Configurable
@ComponentScan(basePackages = "com.github.houbb.lock.test.service")
@EnableLock
@EnableRedisConfig
public class SpringConfig {
}</code></pre><p><code>EnableLock</code> 注解说明,和引导类对应:</p><pre><code class="java">public @interface EnableLock {
/**
* 唯一标识生成策略
* @return 结果
*/
String id() default "lockId";
/**
* 缓存实现策略 bean 名称
*
* 默认引入 redis-config 中的配置
*
* @return 实现
*/
String cache() default "springRedisService";
/**
* 加锁 key 格式化策略
* @return 策略
*/
String lockKeyFormat() default "lockKeyFormat";
/**
* 锁释放失败处理类
* @return 结果
*/
String lockReleaseFailHandler() default "lockReleaseFailHandler";
}</code></pre><p>其中 <code>springRedisService</code> 使用的是 <a href="https://link.segmentfault.com/?enc=duxAQ4FXxesW%2FQiFB7Zzxg%3D%3D.car3%2BnBgTaXYA3HQ5oTt6aIXktfqbBLycbjdl%2F4jnJDIrd3iuq52DNabDl3OseIU" rel="nofollow">redis-config</a> 中的实现。</p><p>对应注解 <code>@EnableRedisConfig</code>,redis 的配置信息如下:</p><table><thead><tr><th align="left">配置</th><th align="left">说明</th><th align="left">默认值</th></tr></thead><tbody><tr><td align="left">redis.address</td><td align="left">redis 地址</td><td align="left">127.0.0.1</td></tr><tr><td align="left">redis.port</td><td align="left">redis 端口</td><td align="left">6379</td></tr><tr><td align="left">redis.password</td><td align="left">redis 密码</td><td align="left"> </td></tr></tbody></table><h4>使用 LockBs</h4><p>我们可以直接 <code>LockBs</code> 的引导类,这种适合一些更加灵活的场景。</p><pre><code class="java">@ContextConfiguration(classes = SpringConfig.class)
@RunWith(SpringJUnit4ClassRunner.class)
public class SpringServiceRawTest {
@Autowired
private UserService userService;
@Autowired
private LockBs lockBs;
@Test
public void queryLogTest() {
final String key = "name";
try {
lockBs.tryLock(key);
final String value = userService.rawUserName(1L);
} catch (Exception exception) {
throw new RuntimeException(exception);
} finally {
lockBs.unlock(key);
}
}
}</code></pre><h3>aop 注解使用</h3><h4>指定方法注解</h4><p>当然,我们可以在方法上直接指定注解 <code>@Lock</code>,使用更加方便。</p><p>直接使用,AOP 切面生效即可。</p><pre><code class="java">@Service
public class UserService {
@Lock
public String queryUserName(Long userId) {
}
@Lock(value = "#user.name")
public void queryUserName2(User user) {
}
}</code></pre><p><code>@Lock</code> 属性说明,value 用于指定 key,支持 SPEL 表达式。</p><p>其他属性,和引导类的方法参数一一对应。</p><pre><code class="java">public @interface Lock {
/**
* 缓存的 key 策略,支持 SpEL
* @return 结果
*/
String value() default "";
/**
* 时间单位
* @return 单位
*/
TimeUnit timeUnit() default TimeUnit.SECONDS;
/**
* 等待锁时间
* @return 等待锁时间
*/
long waitLockTime() default 10;
/**
* 业务加锁时间
* @return 加锁时间
*/
long lockTime() default 60;
}</code></pre><h2>spring boot 整合</h2><h3>maven 引入</h3><pre><code class="xml"><dependency>
<groupId>com.github.houbb</groupId>
<artifactId>lock-springboot-starter</artifactId>
<version>1.3.0</version>
</dependency></code></pre><h3>使用</h3><p>同 spring</p><h2>后期 Road-MAP</h2><ul><li>[ ] 支持锁的可重入</li></ul><p>持有锁的线程可以多次获取锁</p><ul><li>[x] 分布式锁注解支持</li></ul><h2>拓展阅读</h2><p><a href="https://link.segmentfault.com/?enc=bw2hlezYvk0Fj6x74q8B1w%3D%3D.bc85oF%2BhGMg96U4nGrfvVb3cfX3dF%2FLjklI9PalicGcY6Vy9Av2fs8541ln9uh1WRVIrb3C1g2W9%2FATasCaX2kjwzejPLkDvwh2Ck4CYFj8%3D" rel="nofollow">Redis 分布式锁</a></p><p><a href="https://link.segmentfault.com/?enc=Sa9%2FyANFdLDuQZ39vu76Xg%3D%3D.SHa8ImLkTBu5%2Bnsl7H3EltVVjAK%2FMJ6hkDge2sE3OuopbsFf2wy%2Bgn2Zf%2FUvjaT7fcu2UXunSKLQ1jQcUJs1r64qQvv3IJAumCXz0rOzTRIugWTdaH2EZeilJxB2Jgp8" rel="nofollow">java 从零实现 redis 分布式锁</a></p><h2>缓存相关工具</h2><p><a href="https://link.segmentfault.com/?enc=bk9fWol7DTdUEr37eSaTKQ%3D%3D.MocnULHlZGxVwWDkyhiY6YuA0YHHhvgD6E9eU4KKENU%3D" rel="nofollow">cache: 手写渐进式 redis</a></p><p><a href="https://link.segmentfault.com/?enc=qWShksa2NyOYKhK2lnuIGg%3D%3D.Cu4rzIxAUhjIn3VC5FexlOdK%2BLG9EcNOqidq1MxpPuyFBT8v2h58YHFYWE0Gn2PF" rel="nofollow">common-cache: 通用缓存标准定义</a></p><p><a href="https://link.segmentfault.com/?enc=LXldOAmtajQ2wfYoI72OQw%3D%3D.mjBT4nbeH3NmuIZthFyGjc%2BMkiGU3glus9fVKp5rVECKso23yIdD1ZsIvdH9YFbs" rel="nofollow">redis-config: 兼容各种常见的 redis 配置模式</a></p><p><a href="https://link.segmentfault.com/?enc=pw5XvhSSPkFlat%2B%2FdYwiMQ%3D%3D.XbqMTNbRQxaN4bVc4zra9crjSJcNWZHYw7mYMLbDoZU%3D" rel="nofollow">lock: 开箱即用的分布式锁</a></p><p><a href="https://link.segmentfault.com/?enc=Pi8M4Ti%2B0w0pTgNTC5nIZA%3D%3D.3uIuzEghFVE3xL1hT4mWD8ZfDJzYFt7C69AZXm4EC14FLCloo4pFivp8FB09eDL1" rel="nofollow">resubmit: 防重复提交</a></p><p><a href="https://link.segmentfault.com/?enc=th8jK43cMbZ86pWHHgWKLw%3D%3D.ehj1yz4pgYo%2FnOzgrD%2F%2B3zIiCS%2FQBXRfN6w1xGMoCs3RE6HLGXU3s4ia3oCUtPrC" rel="nofollow">rate-limit: 限流</a></p>
开源项目在线化 中文繁简体转换/敏感词/拼音/分词/汉字相似度/markdown 目录
https://segmentfault.com/a/1190000042751092
2022-11-06T21:53:33+08:00
2022-11-06T21:53:33+08:00
老马啸西风
https://segmentfault.com/u/houbinbin
0
<h2>前言</h2><p>以前在 github 上自己开源了一些项目。碍于技术与精力,大部分项目都是 java 实现的。</p><p>这对于非 java 开发者而言很不友好,对于不会编程的用户更加不友好。</p><p>为了让更多的人可以使用到这些简单的小工具,周末抽时间将几个开源项目添加了 web 服务实现。</p><h2>开源项目</h2><h3>中文繁简体转换</h3><p>简介:支持中文繁简体的在线转换。</p><p>在线地址:<a href="https://link.segmentfault.com/?enc=tK331mDvAJXJVMyF%2BpxITw%3D%3D.xLxjooTTh9LKPH4KqVBb7RoBteU5EbztUWn89KZ7IrZbR4KD2VC4Bhh13VKUGCjS" rel="nofollow">https://houbb.github.io/opensource/opencc4j/</a></p><p>开源地址:<a href="https://link.segmentfault.com/?enc=t5PWWV9XTVqbkl3kKkJCYw%3D%3D.LgasARs6nPYH6o8nSdi8D5inI2iKXUQPGsb6tEswqumiKmViayBn7IHcjvHLgag1" rel="nofollow">https://github.com/houbb/opencc4j</a></p><h4>用法简介</h4><p>此处仅选择一个作为例子介绍,其他用法大同小异。</p><p><img src="/img/remote/1460000042751094" alt="界面" title="界面"></p><p>【处理】将左边的内容,按照下拉框的选择,结果会自动填充在右边。</p><p>【互换】互换左右两边的内容。平时测试繁体=》简体,和简体=》繁体更加方便。</p><p>【复制】复制右边的内容到粘贴板。</p><h3>敏感词</h3><p>简介:基于 DFA 算法实现的高性能 java 敏感词工具框架。请勿发布涉及政治、广告、营销、翻墙、违反国家法律法规等内容。</p><p>在线地址:<a href="https://link.segmentfault.com/?enc=aHln6nw2ysv9H2KADUMSbA%3D%3D.soohbx30d%2BL05QSEf26wzKNboqPH4mq8YX238VpASsqEQBtRd88lHROURA%2FGBROf5CsGcFMkxtTBrEdVYWa7uA%3D%3D" rel="nofollow">https://houbb.github.io/opensource/sensitive-word/</a></p><p>开源地址:<a href="https://link.segmentfault.com/?enc=MBKkz6CzvZLomfAvqNLHHQ%3D%3D.l0w9jCluigCC4BL0get0fOIH4HF3M4YwC8qR4jn2M2Q9xvJTbZejhZACSN8i%2BKrd" rel="nofollow">https://github.com/houbb/sensitive-word</a></p><h3>拼音</h3><p>简介:高性能中文转拼音工具。支持同音字。</p><p>在线地址:<a href="https://link.segmentfault.com/?enc=gMaNreDu2n0N4pzAi2mZIw%3D%3D.eSJA0SOrJehvmmjJxzcuCQkq0hDqKUZ5q6AIbgTP4R6ytY7IDT6t0hm%2FMQHb7K%2BN" rel="nofollow">https://houbb.github.io/opensource/pinyin/</a></p><p>开源地址:<a href="https://link.segmentfault.com/?enc=P%2B8iCtY1LtWOGFvSzWOSKg%3D%3D.bBy1WwS0EBERYbOsb66Vpb4I4NJAW69wfmxhVOJlO%2FI%3D" rel="nofollow">https://github.com/houbb/pinyin</a></p><p>可以很方便的把一段文字转换为对应的拼音,比如前言的内容默认如下:</p><pre><code>yǐ qián zài github shàng zì jǐ kāi yuán le yī xiē xiàng mù 。 ài yú jì shù yǔ jīng lì , dà bù fèn xiàng mù dōu shì java shí xiàn de 。 zhè duì yú fēi java kāi fā zhě ér yán hěn bù yǒu hǎo , duì yú bú huì biān chéng de yòng hù gèng jiā bù yǒu hǎo 。 wèi le ràng gèng duō de rén kě yǐ shǐ yòng dào zhè xiē jiǎn dān de xiǎo gōng jù , zhōu mò chōu shí jiān jiāng jǐ gè kāi yuán xiàng mù tiān jiā le web fú wù shí xiàn 。</code></pre><h3>分词</h3><p>简介:基于结巴分词词库实现的更加灵活优雅易用,高性能的 java 分词实现。</p><p>在线地址:<a href="https://link.segmentfault.com/?enc=uzFd8aaudikBr1KHIenUBQ%3D%3D.Pf7yxu6bS7LAZ5buc6AeFRyF5Fc51riH4QFPfYWtn9uwOuOK1ecsYjwqj55KogoT" rel="nofollow">https://houbb.github.io/opensource/segment/</a></p><p>开源地址:<a href="https://link.segmentfault.com/?enc=YhTQ7B%2BlurFfAyvVH%2BjGpg%3D%3D.aDOTN62tTfrF%2Bt8k%2FzMyBndZryj6hRK8q0SbunjHubfzhxolbS%2FJ2%2B4fHZq8ZWlp" rel="nofollow">https://github.com/houbb/segment</a></p><p>分词,可以说是 NLP 的基石。</p><p>前言的内容分词效果如下:</p><pre><code>[以前[0,2), 在[2,3), [3,4), github[4,10), [10,11), 上自[11,13), 己[13,14), 开源[14,16), 了[16,17), 一些[17,19), 项目[19,21), 。[21,22), 碍于[22,24), 技术[24,26), 与[26,27), 精力[27,29), ,[29,30), 大部分[30,33), 项目[33,35), 都[35,36), 是[36,37), [37,38), java[38,42), [42,43), 实现[43,45), 的[45,46), 。[46,47), [47,48), [48,49), 这[49,50), 对于[50,52), 非[52,53), [53,54), java[54,58), [58,59), 开发者[59,62), 而言[62,64), 很[64,65), 不[65,66), 友好[66,68), ,[68,69), 对于[69,71), 不会[71,73), 编程[73,75), 的[75,76), 用户[76,78), 更加[78,80), 不[80,81), 友好[81,83), 。[83,84), [84,85), [85,86), 为了[86,88), 让[88,89), 更多[89,91), 的[91,92), 人[92,93), 可以[93,95), 使用[95,97), 到[97,98), 这些[98,100), 简单[100,102), 的[102,103), 小工[103,105), 具[105,106), ,[106,107), 周末[107,109), 抽时间[109,112), 将[112,113), 几个[113,115), 开源[115,117), 项目[117,119), 添加[119,121), 了[121,122), [122,123), web[123,126), [126,127), 服务[127,129), 实现[129,131), 。[131,132)]</code></pre><h3>汉字相似度</h3><p>简介:汉字相似度计算工具。中文形近字算法</p><p>在线地址:<a href="https://link.segmentfault.com/?enc=TUXMez2ODf38lB9b4CgOgg%3D%3D.s7qeIdW8z2hXz2%2FCg34mDgiDjT2Sko%2FM59VWvnbiw1s8gsUiZZluxOzJGyLtGJ83HlUxeCh%2F0c40ekD8%2F7%2BqZA%3D%3D" rel="nofollow">https://houbb.github.io/opensource/nlp-hanzi-similar/</a></p><p>开源地址:<a href="https://link.segmentfault.com/?enc=Gohkf7GIdaO1BYpyIVep7g%3D%3D.2BEd3iDBHiBW1k3iHmE10XetUGTSmVhcnH89LxwNd4Y%2BpnCVOCyApmFW1poArXid" rel="nofollow">https://github.com/houbb/nlp-hanzi-similar</a></p><h3>markdown 目录</h3><p>简介:为 markdown 文件生成 toc 目录</p><p>在线地址:<a href="https://link.segmentfault.com/?enc=Y7VXWizKotaKPkOs6alNMQ%3D%3D.g9SJpKDkK7X0P4C7sZHNrG31yREReIQIt70nZhKDfFC%2Bh2%2FO42zU0XLE1e4urp3i2z8Q027b7RJ3BeOXq8%2BBDg%3D%3D" rel="nofollow">https://houbb.github.io/opensource/markdown-toc/</a></p><p>开源地址:<a href="https://link.segmentfault.com/?enc=d59WaU2VhCP47HIZh5SoAA%3D%3D.MF3zZZ83ghajdzegpaPj%2BFdkJmb5HpEa5e2FAiIbAUVMDnlshAOztO63yrzT%2BvWe" rel="nofollow">https://github.com/houbb/markdown-toc</a></p><p>个人平时的博客都是使用 markdown 编写的,专注于写作。markdown-toc 可以为这样的文本生成对应的 toc。</p><p>比如本篇文章,直接放在 github 上,是支持跳转的。</p><pre><code>* [前言](#前言)
* [开源项目](#开源项目)
* [中文繁简体转换](#中文繁简体转换)
* [用法简介](#用法简介)
* [敏感词](#敏感词)
* [拼音](#拼音)
* [分词](#分词)
* [汉字相似度](#汉字相似度)
* [markdown 目录](#markdown-目录)</code></pre><p><img src="/img/remote/1460000042751095" alt="TREE" title="TREE"></p><h2>小结</h2><p>这样的小工具,实现起来非常简单。</p><p>但是对于用户而言,意义还是比较大的。</p><p>最近在学习一些 AI 相关的东西,自己平时不搞 python,很多开源项目,没有 web 体验版本看起来就会很吃力。</p><p><strong>不同的语言产生了难以逾越的鸿沟,愿你我能够借助 WEB 的力量,跨过去</strong>。</p><h2>原文地址</h2><blockquote><a href="https://link.segmentfault.com/?enc=CV3vX%2BMesosdBn1pUENTgw%3D%3D.mPvOosi%2BImCPWYOggRfGyWWN4O5NbDEjDZPqIZynRiMcPZAxROldD6T4n1e29uaDXvYpNXW19DiQo1trcPSLIQ%3D%3D" rel="nofollow">开源项目在线化</a></blockquote>
resubmit 渐进式防重复提交框架简介
https://segmentfault.com/a/1190000042260820
2022-08-02T21:12:27+08:00
2022-08-02T21:12:27+08:00
老马啸西风
https://segmentfault.com/u/houbinbin
0
<h2>resubmit</h2><p><a href="https://link.segmentfault.com/?enc=q0%2BnCQl9H%2FaFNOhW0t8uAg%3D%3D.MmFUg2vC3154a0GVXCttIs6Z9rq5FBvfC4dxv%2B3QW%2Btf8LltZU0xxJu4g%2F7kiA7u" rel="nofollow">resubmit</a> 是一款为 java 设计的渐进式防止重复提交框架。</p><p>推荐阅读:</p><p><a href="https://link.segmentfault.com/?enc=xajnxjxyRLtUVxt1WsI41A%3D%3D.R2D6UMXViZuSxWGj6E1cTZldgr8UwzD6IjSHf09nPJyx875zIsGmoXdtzw4TanWqDNlHhHjM2ueuOkH5ztxOlQ%3D%3D" rel="nofollow">面试官:你们的项目中是怎么做防止重复提交的?</a></p><p><a href="https://link.segmentfault.com/?enc=BCcXFBNn3zJxJ%2FFT1GMLXg%3D%3D.dzm0i3cF8R2tOq4Ks8%2Fsj4BoSXLrb5q3YXe4buq7HqI5p0IiNKFxMnPxgbci9fNFUKysGx7vL7CYmOw3wtX8xA%3D%3D" rel="nofollow">resubmit 渐进式防重复提交框架简介</a></p><h3>创作目的</h3><p>有时候手动加防止重复提交很麻烦,每次手动编写不利于复用。</p><p>所以希望从从简到繁实现一个工具,便于平时使用。</p><h3>特性</h3><ul><li>渐进式实现,可独立 spring 使用</li><li>基于注解+字节码,配置灵活</li><li>支持编程式的调用</li><li>支持注解式,完美整合 spring</li><li>支持整合 spring-boot</li></ul><blockquote><a href="https://link.segmentfault.com/?enc=v%2FqD%2FTB%2FAm47dk42FDsddQ%3D%3D.aHA2o%2FHOfrErC7%2Fyoc7v8x2XEZM6zSHmPzxtWxvNnXXQFMYmDCdGphWcH3LdwPweRPbd7IZXlrx5LlUwB4I96g%3D%3D" rel="nofollow">变更日志</a></blockquote><p><img src="/img/remote/1460000042260822" alt="在这里插入图片描述" title="在这里插入图片描述"></p><h2>快速开始</h2><h3>maven 引入</h3><pre><code class="xml"><dependency>
<group>com.github.houbb</group>
<artifact>resubmit-core</artifact>
<version>1.0.0</version>
</dependency></code></pre><h3>编码</h3><ul><li>UserService.java</li></ul><p><code>@Resubmit</code> 对应的属性如下:</p><table><thead><tr><th align="left">属性</th><th align="left">说明</th><th align="left">默认值</th></tr></thead><tbody><tr><td align="left">value()</td><td align="left">多久内禁止重复提交,单位为毫秒。</td><td align="left">60000</td></tr></tbody></table><pre><code class="java">@Resubmit(5000)
public void queryInfo(final String id) {
System.out.println("query info: " + id);
}</code></pre><ul><li>测试代码</li></ul><p>如果在指定时间差内,重复请求,则会抛出异常 ResubmitException</p><pre><code class="java">@Test(expected = ResubmitException.class)
public void errorTest() {
UserService service = ResubmitProxy.getProxy(new UserService());
service.queryInfo("1");
service.queryInfo("1");
}</code></pre><p>相同的参数直接提交2次,就会报错。</p><ul><li>测试场景2</li></ul><p>如果等待超过指定的 5s,就不会报错。</p><pre><code class="java">@Test
public void untilTtlTest() {
UserService service = ResubmitProxy.getProxy(new UserService());
service.queryInfo("1");
DateUtil.sleep(TimeUnit.SECONDS, 6);
service.queryInfo("1");
}</code></pre><h3>自定义</h3><p><code>ResubmitProxy.getProxy(new UserService());</code> 可以获取 UserService 对应的代理。</p><p>等价于:</p><pre><code class="java">ResubmitBs resubmitBs = ResubmitBs.newInstance()
.cache(new CommonCacheServiceMap())
.keyGenerator(new KeyGenerator())
.tokenGenerator(new HttpServletRequestTokenGenerator());
UserService service = ResubmitProxy.getProxy(new UserService(), resubmitBs);</code></pre><p>其中 ResubmitBs 作为引导类,对应的策略都支持自定义。</p><table><thead><tr><th align="left">属性</th><th align="left">说明</th><th align="left">默认值</th></tr></thead><tbody><tr><td align="left">cache()</td><td align="left">缓存实现策略</td><td align="left">默认为基于 ConcurrentHashMap 实现的基于内存的缓存实现</td></tr><tr><td align="left">keyGenerator()</td><td align="left">key 实现策略,用于唯一标识一个方法+参数,判断是否为相同的提交</td><td align="left">md5 策略</td></tr><tr><td align="left">tokenGenerator()</td><td align="left">token 实现策略,用于唯一标识一个用户。</td><td align="left">从 HttpServletRequest 中的 header 属性 <code>resubmit_token</code> 中获取</td></tr></tbody></table><h2>spring 整合使用</h2><h3>maven 引入</h3><pre><code class="xml"><dependency>
<group>com.github.houbb</group>
<artifact>resubmit-spring</artifact>
<version>1.0.0</version>
</dependency></code></pre><h3>代码编写</h3><ul><li>UserService.java</li></ul><pre><code class="java">@Service
public class UserService {
@Resubmit(5000)
public void queryInfo(final String id) {
System.out.println("query info: " + id);
}
}</code></pre><ul><li>SpringConfig.java</li></ul><pre><code class="java">@ComponentScan("com.github.houbb.resubmit.test.service")
@EnableResubmit
@Configuration
public class SpringConfig {
}</code></pre><h4>@EnableResubmit 注解说明</h4><p><code>@EnableResubmit</code> 中用户可以指定对应的实现策略,便于更加灵活的适应业务场景。</p><p>和 <code>ResubmitBs</code> 中支持自定义的属性一一对应。</p><table><thead><tr><th align="left">属性</th><th align="left">说明</th><th align="left">默认值</th></tr></thead><tbody><tr><td align="left">cache()</td><td align="left">缓存实现策略</td><td align="left">默认为基于 ConcurrentHashMap 实现的基于内存的缓存实现</td></tr><tr><td align="left">keyGenerator()</td><td align="left">key 实现策略,用于唯一标识一个方法+参数,判断是否为相同的提交</td><td align="left">md5 策略</td></tr><tr><td align="left">tokenGenerator()</td><td align="left">token 实现策略,用于唯一标识一个用户。</td><td align="left">从 HttpServletRequest 中的 header 属性 <code>resubmit_token</code> 中获取</td></tr></tbody></table><h3>测试代码</h3><pre><code class="java">@ContextConfiguration(classes = SpringConfig.class)
@RunWith(SpringJUnit4ClassRunner.class)
public class ResubmitSpringTest {
@Autowired
private UserService service;
@Test(expected = ResubmitException.class)
public void queryTest() {
service.queryInfo("1");
service.queryInfo("1");
}
}</code></pre><h2>整合 spring-boot</h2><h3>maven 引入</h3><pre><code class="xml"><dependency>
<groupId>com.github.houbb</groupId>
<artifactId>resubmit-springboot-starter</artifactId>
<version>1.0.0</version>
</dependency></code></pre><h3>代码实现</h3><ul><li>UserService.java</li></ul><p>这个方法实现和前面的一样。</p><pre><code class="java">@Service
public class UserService {
@Resubmit(5000)
public void queryInfo(final String id) {
System.out.println("query info: " + id);
}
}</code></pre><ul><li>Application.java</li></ul><p>启动入口</p><pre><code class="java">@SpringBootApplication
public class ResubmitApplication {
public static void main(String[] args) {
SpringApplication.run(ResubmitApplication.class, args);
}
}</code></pre><h3>测试代码</h3><pre><code class="java">@ContextConfiguration(classes = ResubmitApplication.class)
@RunWith(SpringJUnit4ClassRunner.class)
public class ResubmitSpringBootStarterTest {
@Autowired
private UserService service;
@Test(expected = ResubmitException.class)
public void queryTest() {
service.queryInfo("1");
service.queryInfo("1");
}
}</code></pre><h2>自定义策略</h2><p>上面提到 <code>@EnableResubmit</code> 中的策略支持自定义。</p><p>此处仅以 cache 为例,为了简单,默认是基于本地内存的缓存实现。</p><p><strong>如果你不是单点应用,那么基于 redis 的缓存更加合适</strong></p><h3>自定义缓存 cache</h3><h4>实现缓存</h4><p>只需要实现 <code>ICommonCacheService</code> 接口即可。</p><pre><code class="java">public class MyDefineCache extends CommonCacheServiceMap {
// 这里只是作为演示,实际生产建议使用 redis 作为统一缓存
@Override
public synchronized void set(String key, String value, long expireMills) {
System.out.println("------------- 自定义的设置实现");
super.set(key, value, expireMills);
}
}</code></pre><h4>core 中指定使用</h4><p>在非 spring 项目中,可以在引导类中指定我们定义的缓存。</p><pre><code class="java">ResubmitBs resubmitBs = ResubmitBs.newInstance()
.cache(new MyDefineCache());
UserService service = ResubmitProxy.getProxy(new UserService(), resubmitBs);</code></pre><p>其他使用方式保持不变。</p><h4>spring 中指定使用</h4><p>在 spring 项目中,我们需要调整一下配置,其他不变。</p><pre><code class="java">@ComponentScan("com.github.houbb.resubmit.test.service")
@Configuration
@EnableResubmit(cache = "myDefineCache")
public class SpringDefineConfig {
@Bean("myDefineCache")
public ICommonCacheService myDefineCache() {
return new MyDefineCache();
}
}</code></pre><p><code>@EnableResubmit(cache = "myDefineCache")</code> 指定我们自定义的缓存策略名称。</p><h2>Redis 的内置缓存策略</h2><p>为了便于复用,基于 redis 的缓存策略已实现,后续有时间进行讲解。</p><blockquote><a href="https://link.segmentfault.com/?enc=PMBSVlyC3iNqQw9H60m4Aw%3D%3D.m4yZeE%2FnXpAJOY8B0BQdVuRmfdfAOakc1EGNPgEmMrUovL%2FoZ8KFpxUWjygB1kxu" rel="nofollow">Redis-Config</a></blockquote><h2>开源地址</h2><p>为了便于大家学习使用,目前防重复提交框架已开源。</p><p>欢迎大家 fork+star,鼓励一下老马~</p><blockquote><a href="https://link.segmentfault.com/?enc=HbxR%2FPwa%2Fn75TC7573HyBw%3D%3D.vtsYBamshbYjfYk5CPmmQ3OFkKCA8vbPBL7dPuZP%2Fxjfg5Axi%2F7XlxQ8X%2BDCzjiK" rel="nofollow">https://github.com/houbb/resubmit</a></blockquote>
【mq】从零开始实现 mq-13-注册鉴权 auth
https://segmentfault.com/a/1190000041867402
2022-05-19T14:19:24+08:00
2022-05-19T14:19:24+08:00
老马啸西风
https://segmentfault.com/u/houbinbin
0
<h2>前景回顾</h2><p><a href="https://link.segmentfault.com/?enc=sSvmm6AP85OomalhbuVYBA%3D%3D.jj%2Bs2oPpsm2dW5%2FWrKfDSM9mnlTlV68QQPnrNcCUmSc7Bix3h7utW7rFMk1B%2BeAw39dZxyvIZp%2BWvjch8dwjcA%3D%3D" rel="nofollow">【mq】从零开始实现 mq-01-生产者、消费者启动 </a></p><p><a href="https://link.segmentfault.com/?enc=WzeICIAbg3Vlw0W3GqP1SA%3D%3D.fc8U5cwvyPwRLwJNcmcEiUpc7GSW%2Fig%2BuEQoF40rK%2BWI4VqYkmMReEAB29di1pMmb2318n9nMhId11F%2Fehdbow%3D%3D" rel="nofollow">【mq】从零开始实现 mq-02-如何实现生产者调用消费者?</a></p><p><a href="https://link.segmentfault.com/?enc=wfOO%2F9pdL1dwj762ivPO1g%3D%3D.zUOBksR03f%2FwHHfs5rKdqhnwgg%2FC9%2FIn5OJTyPen5v%2BRbg4evr121mCqW%2BgFVgucZiI4GnHi95a1Ic3RatNnxw%3D%3D" rel="nofollow">【mq】从零开始实现 mq-03-引入 broker 中间人</a></p><p><a href="https://link.segmentfault.com/?enc=ckHUu7ZJ3CoSVZv%2FiLTrBQ%3D%3D.dvLaKI1T%2BRI0WAFurt5D37oPQ1bST8fg2YMx40u9ORP4j1vhA%2Boc6NPBLPPwHAAmgBVn6DLRgMQ%2FWwAqdRogDQ%3D%3D" rel="nofollow">【mq】从零开始实现 mq-04-启动检测与实现优化</a></p><p><a href="https://link.segmentfault.com/?enc=SMbriTwDnYISJILiJFQOhg%3D%3D.N0ywDyI5RrqeNNWb2GM3zLjjr5p73QVt7vRDtmRP9Sg2n3KiDJEuSLEp1BKjqIUFr4XDwqTnkRWGnRPQl5t0IA%3D%3D" rel="nofollow">【mq】从零开始实现 mq-05-实现优雅停机</a></p><p><a href="https://link.segmentfault.com/?enc=l0609wzzd9%2B%2B4zT5W5B0zw%3D%3D.8ktQZCPkucYm5nQHkSz%2FOotjWUf9WlhmBuZd41F2DjRKD20yvZOvIS9%2BG5%2F6mzxxs89hhousybA5aviyDzx12Q%3D%3D" rel="nofollow">【mq】从零开始实现 mq-06-消费者心跳检测 heartbeat</a></p><p><a href="https://link.segmentfault.com/?enc=7hGnoj0ZAmkZTIW7zE6yVA%3D%3D.VEe9y%2BImapvM1PcRHyvQThog%2BXoXVnN120J6a7CsmV7E4bZg2P2OB47rzK6X3eg4HDCj2ut9rX0lvGl7AdVV7g%3D%3D" rel="nofollow">【mq】从零开始实现 mq-07-负载均衡 load balance</a></p><p><a href="https://link.segmentfault.com/?enc=EEkvrKjRpL8wP%2BWoEYSgeg%3D%3D.L6BzNTfJ2DQ9%2B%2BmTmbrNQgSyCaTWr4ZJjI0hfGJzTxzi251%2FMoovMPp1acyHBrHtDvwNLeAuEaNsyzxs40VKQw%3D%3D" rel="nofollow">【mq】从零开始实现 mq-08-配置优化 fluent</a></p><p><a href="https://link.segmentfault.com/?enc=%2FXFWEEPZZVxQj9AhUy0JAA%3D%3D.GZ0%2FYiTAfXeZETNcZ%2FypTkAgulKBko6hRegYx%2Fkx7Wvgnxf498nIAvyzPstKDH4oN%2FhIe3kQ9IDkYWaTPaBGNw%3D%3D" rel="nofollow">【mq】从零开始实现 mq-09-消费者拉取消息 pull message</a></p><p><a href="https://link.segmentfault.com/?enc=Mul7tTry7sgbwo9QUpl7CQ%3D%3D.dqt3hv00o7mOlCle8lF9bzuaSn73jzk2Sbb6ao5Y5JiPor68zaoUdUlyR3KcPc7Bd%2FHWOqgd7xcLw%2BIgiEbi2Q%3D%3D" rel="nofollow">【mq】从零开始实现 mq-10-消费者拉取消息回执 pull message ack</a></p><p><a href="https://link.segmentfault.com/?enc=jwYbLe8lDPyjk2nQRljVJQ%3D%3D.FmchfF3qw%2FmzzKbL1jTOur%2BZxASzW2wFnDo9W5ehrr5wg0EuYrITBTcRpE%2BNgP2ZpLIQhasINZRgCbLZd9XKIw%3D%3D" rel="nofollow">【mq】从零开始实现 mq-11-消费者消息回执添加分组信息 pull message ack groupName</a></p><p><a href="https://link.segmentfault.com/?enc=5%2Fq%2Bozhj0mBISv01pWXYVg%3D%3D.Wgnx5aLVDCmkVvv5W5wlcu8yqkPnpfSy5RIAaLESjGUpU5itC0qP%2FpoMbQaAh8z81kgFJ1n2i%2FbvVlTKKfb7%2Bw%3D%3D" rel="nofollow">【mq】从零开始实现 mq-12-消息的批量发送与回执</a></p><p><a href="https://link.segmentfault.com/?enc=Dzea%2FhCbVtJjbdA4y2DMKg%3D%3D.gPEuS7S48s6h4ws20F90UyG05kMnZt%2FGEojYi6C65u5eCaV4NZB%2F7kkx0zp5OYs6prG1%2F71nEJNElZNZOxOn2w%3D%3D" rel="nofollow">【mq】从零开始实现 mq-13-注册鉴权 auth</a></p><h2>注册鉴权</h2><p>我们前面实现了 mq 的基本功能,不过还是存在一个问题,那就是 mq 没有进行鉴权。</p><p>这就会导致如果部署在公网,任何一个机器都可以连接我们的服务,这显然是不够安全的。</p><p><img src="/img/remote/1460000041867404" alt="13" title="13"></p><h2>生产者实现</h2><h3>属性</h3><p>生产者启动时新增 2 个属性:</p><pre><code class="java">/**
* 账户标识
* @since 0.1.4
*/
private String appKey;
/**
* 账户密码
* @since 0.1.4
*/
private String appSecret;</code></pre><h3>注册逻辑调整</h3><p>注册时,添加这两个属性到服务端。</p><pre><code class="java">public void registerToBroker() {
int successCount = 0;
for(RpcChannelFuture channelFuture : this.channelFutureList) {
ServiceEntry serviceEntry = new ServiceEntry();
serviceEntry.setGroupName(groupName);
serviceEntry.setAddress(channelFuture.getAddress());
serviceEntry.setPort(channelFuture.getPort());
serviceEntry.setWeight(channelFuture.getWeight());
BrokerRegisterReq brokerRegisterReq = new BrokerRegisterReq();
brokerRegisterReq.setServiceEntry(serviceEntry);
brokerRegisterReq.setMethodType(MethodType.P_REGISTER);
brokerRegisterReq.setTraceId(IdHelper.uuid32());
brokerRegisterReq.setAppKey(appKey);
brokerRegisterReq.setAppSecret(appSecret);
log.info("[Register] 开始注册到 broker:{}", JSON.toJSON(brokerRegisterReq));
final Channel channel = channelFuture.getChannelFuture().channel();
MqCommonResp resp = callServer(channel, brokerRegisterReq, MqCommonResp.class);
log.info("[Register] 完成注册到 broker:{}", JSON.toJSON(resp));
if(MqCommonRespCode.SUCCESS.getCode().equals(resp.getRespCode())) {
successCount++;
}
}
if(successCount <= 0 && check) {
log.error("校验 broker 可用性,可连接成功数为 0");
throw new MqException(MqCommonRespCode.P_REGISTER_TO_BROKER_FAILED);
}
}</code></pre><h3>消费者</h3><p>消费者连接到 broker 也是类似的,此处不做赘述。</p><h2>Broker 的处理</h2><h3>注册逻辑</h3><p>以前注册是直接成功,此处加一个业务判断。</p><pre><code class="java">// 生产者注册
if(MethodType.P_REGISTER.equals(methodType)) {
BrokerRegisterReq registerReq = JSON.parseObject(json, BrokerRegisterReq.class);
if(!brokerRegisterValidService.producerValid(registerReq)) {
log.error("{} 生产者注册验证失败", JSON.toJSON(registerReq));
throw new MqException(MqBrokerRespCode.P_REGISTER_VALID_FAILED);
}
return registerProducerService.register(registerReq.getServiceEntry(), channel);
}</code></pre><p>首先会校验有效性,这个是一个接口,可自行灵活替换。</p><h3>其他业务逻辑</h3><p>其他业务处理时,都需要 <code>registerProducerService.checkValid(channelId);</code> 进行有效性判断。</p><pre><code class="java">// 生产者注销
if(MethodType.P_UN_REGISTER.equals(methodType)) {
registerProducerService.checkValid(channelId);
BrokerRegisterReq registerReq = JSON.parseObject(json, BrokerRegisterReq.class);
return registerProducerService.unRegister(registerReq.getServiceEntry(), channel);
}</code></pre><h2>小结</h2><p>注册鉴权实现的原理非常简单,不过可以为安全性提供最基础的保障。</p><p>希望本文对你有所帮助,如果喜欢,欢迎点赞收藏转发一波。</p><p>我是老马,期待与你的下次重逢。</p><h2>开源地址</h2><blockquote><a href="https://link.segmentfault.com/?enc=PtPfjyK9jNTIOKOejrSyrQ%3D%3D.yWC0zpGEQiCSfcorGnLxH2yRm5enTyTNJMbgQrVocrI%3D" rel="nofollow">The message queue in java.(java 简易版本 mq 实现) </a> <a href="https://link.segmentfault.com/?enc=ottees6CKi%2FlmgJyW8oy9Q%3D%3D.QAVdzBVk07bbsNfFbcp0JRYbLmgfExWopnglm%2B%2FMcu8%3D" rel="nofollow">https://github.com/houbb/mq</a></blockquote><h2>拓展阅读</h2><p><a href="https://link.segmentfault.com/?enc=qdxrql2WjR3hTZnwt%2BSE3A%3D%3D.M4fkqPmra3yQk%2FkCEzGlKuf2i%2F3mgOykmoVKJxn48hk%3D" rel="nofollow">rpc-从零开始实现 rpc</a> <a href="https://link.segmentfault.com/?enc=AJbf9u9IFWyb%2BqCffy7jBA%3D%3D.6xw53bMtlPEvwmpTj8M7hujzgpC8tmHky57qlIDLFtc%3D" rel="nofollow">https://github.com/houbb/rpc</a></p>
【mq】从零开始实现 mq-12-消息的批量发送与回执
https://segmentfault.com/a/1190000041860361
2022-05-18T10:17:42+08:00
2022-05-18T10:17:42+08:00
老马啸西风
https://segmentfault.com/u/houbinbin
0
<h2>前景回顾</h2><p><a href="https://link.segmentfault.com/?enc=cZxgNMkXXoNSZheJpePtCQ%3D%3D.ZY33m3WVGJvGEbVYlqpw%2B145SsrRjx68D4yDlFeQnWmmqPNJhj2fPllmk0d462iFk5Zds7FiI6Qj3BcMngCQpQ%3D%3D" rel="nofollow">【mq】从零开始实现 mq-01-生产者、消费者启动 </a></p><p><a href="https://link.segmentfault.com/?enc=CnFZ4IwCkfPdarh5p%2F7rpA%3D%3D.eWgja9KSZqBUq1LUwPVsQTZKp2fbPQfsJTh%2FjlY6nbgZzFDgjzlGiHg008hEaXqpftM5yCbGdTdwrSBIVkk6UQ%3D%3D" rel="nofollow">【mq】从零开始实现 mq-02-如何实现生产者调用消费者?</a></p><p><a href="https://link.segmentfault.com/?enc=QnY1oj7Zz%2B6xRwn9aJ9u8Q%3D%3D.VcaBGXWponC2TdjW%2FmyKPaN1LbVQJQkBllJD0EjzhLTUFrMyXe01wf6BaMvJRO7f5zuzQ2viqk7prMEWsgN97g%3D%3D" rel="nofollow">【mq】从零开始实现 mq-03-引入 broker 中间人</a></p><p><a href="https://link.segmentfault.com/?enc=%2FdUeLdm2E2SNeEQyI2xbBg%3D%3D.dLl4FJKedwuZoKb%2FO%2BStsCwqs1xR6WHW2aP1Bf6Kl9%2BXBKNiiYJZMRqmBkbh3%2BmZsEmypWz%2F%2BjqcXZibzAmTvw%3D%3D" rel="nofollow">【mq】从零开始实现 mq-04-启动检测与实现优化</a></p><p><a href="https://link.segmentfault.com/?enc=uC6YCp3VZloR2nywEVDCEw%3D%3D.F5Le8HfcnTuJORnj8KszLmQ5akKi7QpAl7X7owLYNLjag7%2BtLYOs6roJ05ntk1hn57bplTq2dSchn7Hyaf6m3Q%3D%3D" rel="nofollow">【mq】从零开始实现 mq-05-实现优雅停机</a></p><p><a href="https://link.segmentfault.com/?enc=khhDZn4TkK8lKzNq0gRNeg%3D%3D.jQdoqN9F8M0lksmmTQrz%2FTOUavytuUzCwj8WI7x3zqYv8nYHhl03NIpRSDBxMiRYGA2zZ9fpKiqBywksAVsHhQ%3D%3D" rel="nofollow">【mq】从零开始实现 mq-06-消费者心跳检测 heartbeat</a></p><p><a href="https://link.segmentfault.com/?enc=uK%2Fs3OrdaG0CLX5mkHb9yw%3D%3D.zY6EZ5UUD2QnRKhh%2BJQt4uFAj8O0I7OfTzNRcovlKxAJRQXeLMx91WHhyLZxuo8ty92H4qReQJNyjb351vzx0g%3D%3D" rel="nofollow">【mq】从零开始实现 mq-07-负载均衡 load balance</a></p><p><a href="https://link.segmentfault.com/?enc=Biq5snSfZBxdpXmxboN9lg%3D%3D.sFAPLswlqZF80BxJ9YAEaPZDdSx2jmSIOi9ssYT0z0HZQnUY5UhC5ghkEqq%2Fozb4RhJtR3XgWibgZf7ueCpOEA%3D%3D" rel="nofollow">【mq】从零开始实现 mq-08-配置优化 fluent</a></p><p><a href="https://link.segmentfault.com/?enc=6U6Jjl5dvEqmGc%2BeORUw7w%3D%3D.KO9ZWsUHcIydAMAEfctG2KxVSejBhgpoGLd0FhiRM13C4ld%2FAPaaDDh1oeuzahC8%2FZOjQQahOisQYvXXvGBBOA%3D%3D" rel="nofollow">【mq】从零开始实现 mq-09-消费者拉取消息 pull message</a></p><p><a href="https://link.segmentfault.com/?enc=LfuLhCX3IAAwoqN4I7%2B1Yg%3D%3D.9X1QuyCe8kFxrBuvTs6x%2FJgeEo7KM2qQhgNJ1UQs0rSc%2FmF%2BcLqif0KpCQwNwivl%2FINixGkT8OHU8RoDxEmplA%3D%3D" rel="nofollow">【mq】从零开始实现 mq-10-消费者拉取消息回执 pull message ack</a></p><p><a href="https://link.segmentfault.com/?enc=W7nnjDmU%2BH%2FNV4IXNee9KA%3D%3D.8DL%2FCPhF%2BXawz8AbFPuJLZYFyh7M%2Ff9sPCbbEjHmI%2BF6ROCdRxHcZHKifxcLiJ1d0AFGrHKY5XMwLfvfFthZRg%3D%3D" rel="nofollow">【mq】从零开始实现 mq-11-消费者消息回执添加分组信息 pull message ack groupName</a></p><p><a href="https://link.segmentfault.com/?enc=TvcjHk3fatQXzxC%2BlpDopw%3D%3D.6l2Lknx4JIKcAYkT6YyFdvZ2vU70kxHh32UyWtDFsRrH9x5im8MWk3trU6MvKSyIL2Nv%2Ff81ZD3C1q8toRx3Qw%3D%3D" rel="nofollow">【mq】从零开始实现 mq-12-消息的批量发送与回执</a></p><h2>批量消息</h2><p>对于消息的发送,有时候可能需要一次发送多个,比如日志消息等。</p><p>批量操作可以提升性能。</p><p>本节老马就和大家一起添加一点批量特性。</p><p><img src="/img/remote/1460000041860363" alt="batch" title="batch"></p><h2>消息的批量发送</h2><h3>生产者实现</h3><h4>接口定义</h4><pre><code class="java">/**
* 同步发送消息-批量
* @param mqMessageList 消息类型
* @return 结果
* @since 0.1.3
*/
SendBatchResult sendBatch(final List<MqMessage> mqMessageList);
/**
* 单向发送消息-批量
* @param mqMessageList 消息类型
* @return 结果
* @since 0.1.3
*/
SendBatchResult sendOneWayBatch(final List<MqMessage> mqMessageList);</code></pre><p>一次支持发送多个消息。</p><h4>接口实现</h4><p>生产者实现如下。</p><pre><code class="java">@Override
public SendBatchResult sendBatch(List<MqMessage> mqMessageList) {
final List<String> messageIdList = this.fillMessageList(mqMessageList);
final MqMessageBatchReq batchReq = new MqMessageBatchReq();
batchReq.setMqMessageList(mqMessageList);
String traceId = IdHelper.uuid32();
batchReq.setTraceId(traceId);
batchReq.setMethodType(MethodType.P_SEND_MSG_BATCH);
return Retryer.<SendBatchResult>newInstance()
.maxAttempt(maxAttempt)
.callable(new Callable<SendBatchResult>() {
@Override
public SendBatchResult call() throws Exception {
return doSendBatch(messageIdList, batchReq, false);
}
}).retryCall();
}
@Override
public SendBatchResult sendOneWayBatch(List<MqMessage> mqMessageList) {
List<String> messageIdList = this.fillMessageList(mqMessageList);
MqMessageBatchReq batchReq = new MqMessageBatchReq();
batchReq.setMqMessageList(mqMessageList);
String traceId = IdHelper.uuid32();
batchReq.setTraceId(traceId);
batchReq.setMethodType(MethodType.P_SEND_MSG_ONE_WAY_BATCH);
return doSendBatch(messageIdList, batchReq, true);
}
private SendBatchResult doSendBatch(List<String> messageIdList,
MqMessageBatchReq batchReq,
boolean oneWay) {
log.info("[Producer] 批量发送消息 messageIdList: {}, batchReq: {}, oneWay: {}",
messageIdList, JSON.toJSON(batchReq), oneWay);
// 以第一个 sharding-key 为准。
// 后续的会被忽略
MqMessage mqMessage = batchReq.getMqMessageList().get(0);
Channel channel = getChannel(mqMessage.getShardingKey());
//one-way
if(oneWay) {
log.warn("[Producer] ONE-WAY send, ignore result");
return SendBatchResult.of(messageIdList, SendStatus.SUCCESS);
}
MqCommonResp resp = callServer(channel, batchReq, MqCommonResp.class);
if(MqCommonRespCode.SUCCESS.getCode().equals(resp.getRespCode())) {
return SendBatchResult.of(messageIdList, SendStatus.SUCCESS);
}
throw new MqException(ProducerRespCode.MSG_SEND_FAILED);
}</code></pre><p>ps: 这里和单个发送有一个区别,那就是对于 channel 的选择。因为只能选择一个,所以不能兼顾每一个消息的 sharding-key。</p><h3>Broker 的处理</h3><h4>消息分发</h4><pre><code class="java">// 生产者消息发送-批量
if(MethodType.P_SEND_MSG_BATCH.equals(methodType)) {
return handleProducerSendMsgBatch(channelId, json);
}
// 生产者消息发送-ONE WAY-批量
if(MethodType.P_SEND_MSG_ONE_WAY_BATCH.equals(methodType)) {
handleProducerSendMsgBatch(channelId, json);
return null;
}</code></pre><h4>具体实现</h4><pre><code class="java">/**
* 处理生产者发送的消息
*
* @param channelId 通道标识
* @param json 消息体
* @since 0.1.3
*/
private MqCommonResp handleProducerSendMsgBatch(String channelId, String json) {
MqMessageBatchReq batchReq = JSON.parseObject(json, MqMessageBatchReq.class);
final ServiceEntry serviceEntry = registerProducerService.getServiceEntry(channelId);
List<MqMessagePersistPut> putList = buildPersistPutList(batchReq, serviceEntry);
MqCommonResp commonResp = mqBrokerPersist.putBatch(putList);
// 遍历异步推送
for(MqMessagePersistPut persistPut : putList) {
this.asyncHandleMessage(persistPut);
}
return commonResp;
}</code></pre><p>这里对消息列表进行持久化保存。</p><p>演示的持久化策略如下:</p><pre><code class="java">@Override
public MqCommonResp putBatch(List<MqMessagePersistPut> putList) {
// 构建列表
for(MqMessagePersistPut put : putList) {
this.doPut(put);
}
MqCommonResp commonResp = new MqCommonResp();
commonResp.setRespCode(MqCommonRespCode.SUCCESS.getCode());
commonResp.setRespMessage(MqCommonRespCode.SUCCESS.getMsg());
return commonResp;
}</code></pre><h2>消息的批量ACK</h2><h3>说明</h3><p>以前的实现方式是每一个消息消费完成之后,进行一次 ACK。</p><p>对于 pull 策略的消息消费,我们可以等当前批次结束,统一进行 ACK 回执。</p><h3>消费实现</h3><p>实现调整如下:</p><pre><code class="java">for(MqTopicTagDto tagDto : subscribeList) {
final String topicName = tagDto.getTopicName();
final String tagRegex = tagDto.getTagRegex();
MqConsumerPullResp resp = consumerBrokerService.pull(topicName, tagRegex, size);
if(MqCommonRespCode.SUCCESS.getCode().equals(resp.getRespCode())) {
List<MqMessage> mqMessageList = resp.getList();
if(CollectionUtil.isNotEmpty(mqMessageList)) {
List<MqConsumerUpdateStatusDto> statusDtoList = new ArrayList<>(mqMessageList.size());
for(MqMessage mqMessage : mqMessageList) {
IMqConsumerListenerContext context = new MqConsumerListenerContext();
final String messageId = mqMessage.getTraceId();
ConsumerStatus consumerStatus = mqListenerService.consumer(mqMessage, context);
log.info("消息:{} 消费结果 {}", messageId, consumerStatus);
// 状态同步更新
if(!ackBatchFlag) {
MqCommonResp ackResp = consumerBrokerService.consumerStatusAck(messageId, consumerStatus);
log.info("消息:{} 状态回执结果 {}", messageId, JSON.toJSON(ackResp));
} else {
// 批量
MqConsumerUpdateStatusDto statusDto = new MqConsumerUpdateStatusDto();
statusDto.setMessageId(messageId);
statusDto.setMessageStatus(consumerStatus.getCode());
statusDto.setConsumerGroupName(groupName);
statusDtoList.add(statusDto);
}
}
// 批量执行
if(ackBatchFlag) {
MqCommonResp ackResp = consumerBrokerService.consumerStatusAckBatch(statusDtoList);
log.info("消息:{} 状态批量回执结果 {}", statusDtoList, JSON.toJSON(ackResp));
statusDtoList = null;
}
}
} else {
log.error("拉取消息失败: {}", JSON.toJSON(resp));
}
}</code></pre><p>如果 ackBatchFlag = false,则处理逻辑和以前一样。</p><p>如果 ackBatchFlag = true,则首先把消息放到 list 中,结束后统一执行。</p><h3>broker 实现</h3><h4>消息分发</h4><pre><code class="java">//消费者消费状态 ACK-批量
if(MethodType.C_CONSUMER_STATUS_BATCH.equals(methodType)) {
MqConsumerUpdateStatusBatchReq req = JSON.parseObject(json, MqConsumerUpdateStatusBatchReq.class);
final List<MqConsumerUpdateStatusDto> statusDtoList = req.getStatusList();
return mqBrokerPersist.updateStatusBatch(statusDtoList);
}</code></pre><h4>实现</h4><p>默认的持久化实现,更新如下:</p><pre><code class="java">@Override
public MqCommonResp updateStatusBatch(List<MqConsumerUpdateStatusDto> statusDtoList) {
for(MqConsumerUpdateStatusDto statusDto : statusDtoList) {
this.doUpdateStatus(statusDto.getMessageId(), statusDto.getConsumerGroupName(),
statusDto.getMessageStatus());
}
MqCommonResp commonResp = new MqCommonResp();
commonResp.setRespCode(MqCommonRespCode.SUCCESS.getCode());
commonResp.setRespMessage(MqCommonRespCode.SUCCESS.getMsg());
return commonResp;
}</code></pre><p>遍历每一个元素,进行状态的更新。</p><h2>小结</h2><p>异步和批量,是提升性能最常用的 2 种方式。</p><p>批量的实现相关来说是最简单,也是效果最显著的。</p><p>希望本文对你有所帮助,如果喜欢,欢迎点赞收藏转发一波。</p><p>我是老马,期待与你的下次重逢。</p><h2>开源地址</h2><blockquote><a href="https://link.segmentfault.com/?enc=sHfyJivAjUA8ZsS2QulWQQ%3D%3D.tYoC3LGS0GnoF9Zqcnrv0wgxmNdmGDaenddjcbFDQ94%3D" rel="nofollow">The message queue in java.(java 简易版本 mq 实现) </a> <a href="https://link.segmentfault.com/?enc=5CK7z03zesYqIL5a%2BqxBmQ%3D%3D.cBVnQTEaoxoCOHyK0FdCgDEw%2BdKM4gwdZeB6Ph4vRGE%3D" rel="nofollow">https://github.com/houbb/mq</a></blockquote><h2>拓展阅读</h2><p><a href="https://link.segmentfault.com/?enc=Cd1%2FCfe9%2FalYyMUXEg%2FNiQ%3D%3D.p1segiC%2FaBsNy%2FIM3LcNaO5Cz8BWaf5aylvkerPMeJA%3D" rel="nofollow">rpc-从零开始实现 rpc</a> <a href="https://link.segmentfault.com/?enc=HIkHtPSzoREQHIsupe3Ieg%3D%3D.toekY9tu303zKOiCVrqIJI6lEsCKWWWLHXAyVCtBSic%3D" rel="nofollow">https://github.com/houbb/rpc</a></p>
【mq】从零开始实现 mq-11-消费者消息回执添加分组信息 pull message ack groupName
https://segmentfault.com/a/1190000041859368
2022-05-17T23:31:52+08:00
2022-05-17T23:31:52+08:00
老马啸西风
https://segmentfault.com/u/houbinbin
0
<h2>前景回顾</h2><p><a href="https://link.segmentfault.com/?enc=dVUAYr4FbMDKfBuWAlnr0w%3D%3D.8dKZv63glpfMkKSjvS0N6Oqm8HBavoEKtGU1yBZN1TeOVfmz0oM4CdtkUIdXlYCc%2F9c0mjaG5C1kOcszKbhWgA%3D%3D" rel="nofollow">【mq】从零开始实现 mq-01-生产者、消费者启动 </a></p><p><a href="https://link.segmentfault.com/?enc=FievIcq13kVHdFUuw4idtg%3D%3D.aUDGmzhPjaJkPEeo9mPWVaxxsvVY0cIcVpgdU90u1iz2SQOkYZwaG2%2FsRlWRYjZzk8CUQ2X%2BckjI%2BHAvac%2FWfg%3D%3D" rel="nofollow">【mq】从零开始实现 mq-02-如何实现生产者调用消费者?</a></p><p><a href="https://link.segmentfault.com/?enc=qVj9JobACzgPe%2BYGQJF8jA%3D%3D.7x7BVZuDbuWzwUvYDOFNbisIw0NnguuvexMxHzyCjDyoVi4BBxrz3V9n41p1lRpYv1nSg2LJl0NzKXaBX0e9WQ%3D%3D" rel="nofollow">【mq】从零开始实现 mq-03-引入 broker 中间人</a></p><p><a href="https://link.segmentfault.com/?enc=e8to1cE2sBinHpY3jacpGg%3D%3D.EYf7DLJpps1JxRt9fWetw%2BvJu0MyP1lk8wVAs9uVti76t7l8DfwCBz0r%2FG9dRhymemgzsVkcNVyQtJuLa%2F5Iog%3D%3D" rel="nofollow">【mq】从零开始实现 mq-04-启动检测与实现优化</a></p><p><a href="https://link.segmentfault.com/?enc=PptX2I3uMJy4Q5kJ2jIMEw%3D%3D.DODEKg2bNH98UUsloFxrbEtm1hmpfS7IFRswRzg7AC9gkzJcFA723PC%2B76HgS%2F%2BiJfoPDqaUWBuk5%2BmYeWevSw%3D%3D" rel="nofollow">【mq】从零开始实现 mq-05-实现优雅停机</a></p><p><a href="https://link.segmentfault.com/?enc=uMflKsVErjcBQXYVJtV5ug%3D%3D.sAOpQW4RO3nioB5NcE8M%2FF5iIFw2u9ExdF2VePtFpq2qmxODOHQPszV%2Blvjx4i18VH0O1DNVpQZQZ2feRPiPTQ%3D%3D" rel="nofollow">【mq】从零开始实现 mq-06-消费者心跳检测 heartbeat</a></p><p><a href="https://link.segmentfault.com/?enc=IAWMgLE0Ae%2FTTg%2Bt6U6oFw%3D%3D.9kUjSK8lAWE0ZPFhRhMEbVTB8MKOAaA94WenOhXEqwD%2BBXiYRKG8z60Qp3rMPn%2F2h4CTIHzZzf6cD93e3OWUiQ%3D%3D" rel="nofollow">【mq】从零开始实现 mq-07-负载均衡 load balance</a></p><p><a href="https://link.segmentfault.com/?enc=bYTq2z%2F9MdljXeppqOB%2Bwg%3D%3D.JBdBHoYlxT5ZL8uS1CBTTIuB5o5RR8Hd9JFQ1N6TFJLz9GqiyoLS7SORDxRkqFoyhtt9EGoUJq089cdX0bAScA%3D%3D" rel="nofollow">【mq】从零开始实现 mq-08-配置优化 fluent</a></p><p><a href="https://link.segmentfault.com/?enc=xp16YGlIdUZS4PIgDlS1fw%3D%3D.WBw8%2BORYg2ul7kfPvi8clGwpQLsZzU4hBmAhUvZXVpzDux%2FdP5D%2BCYQh%2BK%2BTMxDhVum0osDzdQLTWfQxH9mHfw%3D%3D" rel="nofollow">【mq】从零开始实现 mq-09-消费者拉取消息 pull message</a></p><p><a href="https://link.segmentfault.com/?enc=Lr9vqDKSjjS4xrLiFwaOXg%3D%3D.8IxaVVT%2F0skDLKRcRkhx8yKfvKFh3sNWKw1GmPTypJSKWjZ83gluGb5AZOJZQDz7cnHiMcDArWWLdKKReI8uDQ%3D%3D" rel="nofollow">【mq】从零开始实现 mq-10-消费者拉取消息回执 pull message ack</a></p><p><a href="https://link.segmentfault.com/?enc=Kzq6bVD18SF80DPMHjFmAQ%3D%3D.waF6n6ak6Ut1cih6W8coMeL1h6gi1P6ICm9UE0e5BizJN2doVRKUxor6l0F01zs8yIKdXSe2RppI5c5zRgsp2A%3D%3D" rel="nofollow">【mq】从零开始实现 mq-11-消费者消息回执添加分组信息 pull message ack groupName</a></p><h2>状态回执</h2><p>上一节我们实现了消息的回执,但是存在一个问题。</p><p>同一个消息,可以被不同的 groupName 进行消费,所以回执是需要根据 groupName 进行分开的,这个上一节中遗漏了。</p><p><img src="/img/remote/1460000041859370" alt="状态回执" title="状态回执"></p><h2>Broker 推送消息的调整</h2><p>以前推送消息是直接推送,但是缺少 groupName 信息。</p><h3>订阅列表获取</h3><p>获取订阅列表的实现调整如下:</p><pre><code class="java">public List<ChannelGroupNameDto> getPushSubscribeList(MqMessage mqMessage) {
final String topicName = mqMessage.getTopic();
Set<ConsumerSubscribeBo> set = pushSubscribeMap.get(topicName);
if(CollectionUtil.isEmpty(set)) {
return Collections.emptyList();
}
//2. 获取匹配的 tag 列表
final List<String> tagNameList = mqMessage.getTags();
Map<String, List<ConsumerSubscribeBo>> groupMap = new HashMap<>();
for(ConsumerSubscribeBo bo : set) {
String tagRegex = bo.getTagRegex();
if(RegexUtil.hasMatch(tagNameList, tagRegex)) {
String groupName = bo.getGroupName();
MapUtil.putToListMap(groupMap, groupName, bo);
}
}
//3. 按照 groupName 分组之后,每一组只随机返回一个。最好应该调整为以 shardingkey 选择
final String shardingKey = mqMessage.getShardingKey();
List<ChannelGroupNameDto> channelGroupNameList = new ArrayList<>();
for(Map.Entry<String, List<ConsumerSubscribeBo>> entry : groupMap.entrySet()) {
List<ConsumerSubscribeBo> list = entry.getValue();
ConsumerSubscribeBo bo = RandomUtils.loadBalance(loadBalance, list, shardingKey);
final String channelId = bo.getChannelId();
BrokerServiceEntryChannel entryChannel = registerMap.get(channelId);
if(entryChannel == null) {
log.warn("channelId: {} 对应的通道信息为空", channelId);
continue;
}
final String groupName = entry.getKey();
ChannelGroupNameDto channelGroupNameDto = ChannelGroupNameDto.of(groupName,
entryChannel.getChannel());
channelGroupNameList.add(channelGroupNameDto);
}
return channelGroupNameList;
}</code></pre><p>ChannelGroupNameDto 的定义如下:</p><pre><code class="java">public class ChannelGroupNameDto {
/**
* 分组名称
*/
private String consumerGroupName;
/**
* 通道
*/
private Channel channel;
//get & set
}</code></pre><h3>消息主动推送</h3><p>我们调整一下消息推送,每次推送完成,根据 groupName 进行状态的更新:</p><pre><code class="java">for(final ChannelGroupNameDto channelGroupNameDto : channelList) {
final Channel channel = channelGroupNameDto.getChannel();
final String consumerGroupName =channelGroupNameDto.getConsumerGroupName();
try {
// 更新状态为消费处理中
mqBrokerPersist.updateStatus(messageId, consumerGroupName, MessageStatusConst.TO_CONSUMER_PROCESS);
String channelId = ChannelUtil.getChannelId(channel);
log.info("开始处理 channelId: {}", channelId);
//1. 调用
mqMessage.setMethodType(MethodType.B_MESSAGE_PUSH);
// 重试推送
MqConsumerResultResp resultResp = Retryer.<MqConsumerResultResp>newInstance()
.maxAttempt(pushMaxAttempt)
.callable(new Callable<MqConsumerResultResp>() {
@Override
public MqConsumerResultResp call() throws Exception {
MqConsumerResultResp resp = callServer(channel, mqMessage,
MqConsumerResultResp.class, invokeService, responseTime);
// 失败校验
if(resp == null
|| !ConsumerStatus.SUCCESS.getCode()
.equals(resp.getConsumerStatus())) {
throw new MqException(BrokerRespCode.MSG_PUSH_FAILED);
}
return resp;
}
}).retryCall();
//2. 更新状态
//2.1 处理成功,取 push 消费状态
if(MqCommonRespCode.SUCCESS.getCode().equals(resultResp.getRespCode())) {
mqBrokerPersist.updateStatus(messageId, consumerGroupName, resultResp.getConsumerStatus());
} else {
// 2.2 处理失败
log.error("消费失败:{}", JSON.toJSON(resultResp));
mqBrokerPersist.updateStatus(messageId, consumerGroupName, MessageStatusConst.TO_CONSUMER_FAILED);
}
log.info("完成处理 channelId: {}", channelId);
} catch (Exception exception) {
log.error("处理异常");
mqBrokerPersist.updateStatus(messageId, consumerGroupName, MessageStatusConst.TO_CONSUMER_FAILED);
}
}</code></pre><h2>消息消费者状态回执</h2><p>ps: 这里 V0.1.1 分支漏写了,不过后面 v0.1.2 分支修正了。</p><pre><code class="java">public MqCommonResp consumerStatusAck(String messageId, ConsumerStatus consumerStatus) {
final MqConsumerUpdateStatusReq req = new MqConsumerUpdateStatusReq();
req.setMessageId(messageId);
req.setMessageStatus(consumerStatus.getCode());
final String traceId = IdHelper.uuid32();
req.setTraceId(traceId);
req.setMethodType(MethodType.C_CONSUMER_STATUS);
// 添加 groupName
req.setConsumerGroupName(groupName);
// 重试
return Retryer.<MqCommonResp>newInstance()
.maxAttempt(consumerStatusMaxAttempt)
.callable(new Callable<MqCommonResp>() {
@Override
public MqCommonResp call() throws Exception {
Channel channel = getChannel(null);
MqCommonResp resp = callServer(channel, req, MqCommonResp.class);
if(!MqCommonRespCode.SUCCESS.getCode().equals(resp.getRespCode())) {
throw new MqException(ConsumerRespCode.CONSUMER_STATUS_ACK_FAILED);
}
return resp;
}
}).retryCall();
}</code></pre><p>消息状态回执时,<code> req.setConsumerGroupName(groupName);</code> 添加 groupName 信息。</p><h2>小结</h2><p>消息状态的回执精确到 groupName 之后,不同的 groupName 消费就可以相互独立,适用性更强更广。</p><p>希望本文对你有所帮助,如果喜欢,欢迎点赞收藏转发一波。</p><p>我是老马,期待与你的下次重逢。</p><h2>开源地址</h2><blockquote><a href="https://link.segmentfault.com/?enc=sQoONDMXgZlO2POwgjXeMQ%3D%3D.sXSOB2QqSBH3T2rXJ5OHFAxNE0tSetU%2BV%2Fu5KqEhan8%3D" rel="nofollow">The message queue in java.(java 简易版本 mq 实现) </a> <a href="https://link.segmentfault.com/?enc=nWcyLFqH6kZezE3Xez2TZA%3D%3D.X4w%2BnabCIXXyQmjhkBoke5C4LvHevtnBcnPe30T4N6c%3D" rel="nofollow">https://github.com/houbb/mq</a></blockquote><h2>拓展阅读</h2><p><a href="https://link.segmentfault.com/?enc=a3fK9RJDxvdCumxFbAReFQ%3D%3D.socA%2BpeG8C99y1K%2BTxBI807jkkN6VtuBbe3jr8rq9aU%3D" rel="nofollow">rpc-从零开始实现 rpc</a> <a href="https://link.segmentfault.com/?enc=LnL4ZeXaKSQuNogceieIiQ%3D%3D.DCUUzxWs1stQO1Bg0Fcym5p%2BBINljSVdF5tmSw4ZhlQ%3D" rel="nofollow">https://github.com/houbb/rpc</a></p>
【mq】从零开始实现 mq-10-消费者拉取消息回执 pull message ack
https://segmentfault.com/a/1190000041837947
2022-05-12T22:22:20+08:00
2022-05-12T22:22:20+08:00
老马啸西风
https://segmentfault.com/u/houbinbin
0
<h2>前景回顾</h2><p><a href="https://link.segmentfault.com/?enc=BxdtA3n4FRgOKZNMqIhfcg%3D%3D.pMc1UXAzewSsZ2xh4F%2FSOW%2FKoHNTNzDxJyRgt4RMo5TLiCFGsjT0nkT2npMqMmCYuvaOSLOvyluoMkmF8VMuUA%3D%3D" rel="nofollow">【mq】从零开始实现 mq-01-生产者、消费者启动 </a></p><p><a href="https://link.segmentfault.com/?enc=JclfPLrGD%2BbiM7iOIx4Xjw%3D%3D.3958B0EOLL5%2BkHul28LF4%2BEIk%2FC9cQK8F9D15xu2ZTMMz9Z5E1PxmkW4v3hSx5K%2FGJxbKpb2Bjp0As0rW%2FpMng%3D%3D" rel="nofollow">【mq】从零开始实现 mq-02-如何实现生产者调用消费者?</a></p><p><a href="https://link.segmentfault.com/?enc=nc0Otw886%2Fhv%2B3SerHIBgg%3D%3D.xUfbOFYVYl1uGfNfqgUE2nWwQcjiWpfyIWnretaUTZQyvb%2FTekPkj8l4VC4txYtetkm8GyZO%2B2mq4sM72toSlg%3D%3D" rel="nofollow">【mq】从零开始实现 mq-03-引入 broker 中间人</a></p><p><a href="https://link.segmentfault.com/?enc=Rnps95y2M4FfTECYv2BPqg%3D%3D.m9ADbILqv6vek8J19oxwQn93eQ1Zvp%2FvpWg1XvqMr2tIrY6PPVFEG%2BhhF%2BEVc19b6rUtN92GgwQmf23nLZtc%2Bg%3D%3D" rel="nofollow">【mq】从零开始实现 mq-04-启动检测与实现优化</a></p><p><a href="https://link.segmentfault.com/?enc=ZemHZXdlnXdu%2Bo9BBrZidw%3D%3D.uZrRGyRQpA4IRtBTurFlXK%2FRyAgRxJPZvsyAyQjzOshGPM2b0KMt65rK6q%2F%2FO5%2FhxGBYEEcfqN5s5UEr00e5nA%3D%3D" rel="nofollow">【mq】从零开始实现 mq-05-实现优雅停机</a></p><p><a href="https://link.segmentfault.com/?enc=VJeK58WC1Tb4fzzjqrlqZg%3D%3D.NUIPLfR1EofStdeOqxnTAd4qA1Ggg5zFRcjZcAHr4Nm6pjTcXaFx%2BjDEOOhHyMYecVbzt8Otn%2Bbi5JvunQcnVQ%3D%3D" rel="nofollow">【mq】从零开始实现 mq-06-消费者心跳检测 heartbeat</a></p><p><a href="https://link.segmentfault.com/?enc=nHUlepQ3bVyy%2Bked91r4bw%3D%3D.47Oc49y%2F266lypxGQTocQ%2FZnwbgLKitUSWJ9eG601n%2BnatMDCNlmRKccgvY8UEbh%2FobVA3GQ210zDDger9Md1Q%3D%3D" rel="nofollow">【mq】从零开始实现 mq-07-负载均衡 load balance</a></p><p><a href="https://link.segmentfault.com/?enc=3Cc4ZdvxkdAvG%2BfdcyjpAg%3D%3D.nlr0X6aYNjq57oOKN4kUExFK7xcRflCgb2WaWr4jhPairD16Hfrw8Fi3nTcC1AWkL4atZekhPkcGbWFMGu%2BQ0Q%3D%3D" rel="nofollow">【mq】从零开始实现 mq-08-配置优化 fluent</a></p><p><a href="https://link.segmentfault.com/?enc=qgtvQtydYncDzRp3w6Vrcw%3D%3D.Og4lPGNhhZvCOCg1nlG8doiNaFkFWLi3wK%2F2wECiZ2aIXc%2FmrSLhXzG3zZH4au6n8%2FNQIo9nNaS94Phdql1egQ%3D%3D" rel="nofollow">【mq】从零开始实现 mq-09-消费者拉取消息 pull message</a></p><p><a href="https://link.segmentfault.com/?enc=hGWn5wTe6AaCeuAAn0lcMg%3D%3D.2ObzbH80yVFJOOB00e%2BPQhwHIMlIc%2BuGUDVhbFHFGyEGSpy2DX8mNL8MYdOhatQ3GV6%2FA7W%2FNJ%2BYDa5RRG7KMA%3D%3D" rel="nofollow">【mq】从零开始实现 mq-10-消费者拉取消息回执 pull message ack</a></p><h2>状态回执</h2><p>大家好,我是老马。</p><p>上一节我们只实现了拉取消息的实现,但是缺少了消费状态回执。</p><p>这一节我们一起来学习下如何实现状态回执。</p><p><img src="/img/remote/1460000041837949" alt="10" title="10"></p><h2>代码实现</h2><h3>回执状态的设计</h3><p>我们规定如下几种回执状态:</p><pre><code class="java">package com.github.houbb.mq.common.constant;
/**
* @author binbin.hou
* @since 0.0.3
*/
public final class MessageStatusConst {
private MessageStatusConst(){}
/**
* 待消费
* ps: 生产者推送到 broker 的初始化状态
*/
public static final String WAIT_CONSUMER = "W";
/**
* 推送给消费端处理中
* ps: broker 准备推送时,首先将状态更新为 P,等待推送结果
* @since 0.1.0
*/
public static final String TO_CONSUMER_PROCESS = "TCP";
/**
* 推送给消费端成功
* @since 0.1.0
*/
public static final String TO_CONSUMER_SUCCESS = "TCS";
/**
* 推送给消费端失败
* @since 0.1.0
*/
public static final String TO_CONSUMER_FAILED = "TCF";
/**
* 消费完成
*/
public static final String CONSUMER_SUCCESS = "CS";
/**
* 消费失败
*/
public static final String CONSUMER_FAILED = "CF";
/**
* 稍后消费
* @since 0.1.0
*/
public static final String CONSUMER_LATER = "CL";
}</code></pre><h3>消费者状态回执</h3><p>我们在消费之后,添加状态回执:</p><pre><code class="java">for(MqMessage mqMessage : mqMessageList) {
IMqConsumerListenerContext context = new MqConsumerListenerContext();
final String messageId = mqMessage.getTraceId();
ConsumerStatus consumerStatus = mqListenerService.consumer(mqMessage, context);
log.info("消息:{} 消费结果 {}", messageId, consumerStatus);
// 状态同步更新
MqCommonResp ackResp = consumerBrokerService.consumerStatusAck(messageId, consumerStatus);
log.info("消息:{} 状态回执结果 {}", messageId, JSON.toJSON(ackResp));
}</code></pre><p>回执实现,根据 messageId 更新对应的消息消费状态。</p><pre><code class="java">public MqCommonResp consumerStatusAck(String messageId, ConsumerStatus consumerStatus) {
final MqConsumerUpdateStatusReq req = new MqConsumerUpdateStatusReq();
req.setMessageId(messageId);
req.setMessageStatus(consumerStatus.getCode());
final String traceId = IdHelper.uuid32();
req.setTraceId(traceId);
req.setMethodType(MethodType.C_CONSUMER_STATUS);
// 重试
return Retryer.<MqCommonResp>newInstance()
.maxAttempt(consumerStatusMaxAttempt)
.callable(new Callable<MqCommonResp>() {
@Override
public MqCommonResp call() throws Exception {
Channel channel = getChannel(null);
MqCommonResp resp = callServer(channel, req, MqCommonResp.class);
if(!MqCommonRespCode.SUCCESS.getCode().equals(resp.getRespCode())) {
throw new MqException(ConsumerRespCode.CONSUMER_STATUS_ACK_FAILED);
}
return resp;
}
}).retryCall();
}</code></pre><h3>Broker 回执处理</h3><h4>消息分发</h4><pre><code class="java">// 消费者消费状态 ACK
if(MethodType.C_CONSUMER_STATUS.equals(methodType)) {
MqConsumerUpdateStatusReq req = JSON.parseObject(json, MqConsumerUpdateStatusReq.class);
final String messageId = req.getMessageId();
final String messageStatus = req.getMessageStatus();
return mqBrokerPersist.updateStatus(messageId, messageStatus);
}</code></pre><h4>简单实现</h4><p>这里是基于本地 map 更新状态的,性能比较差。</p><p>后续会以 mysql 实现。</p><pre><code class="java">public MqCommonResp updateStatus(String messageId, String status) {
// 这里性能比较差,所以不可以用于生产。仅作为测试验证
for(List<MqMessagePersistPut> list : map.values()) {
for(MqMessagePersistPut put : list) {
MqMessage mqMessage = put.getMqMessage();
if(mqMessage.getTraceId().equals(messageId)) {
put.setMessageStatus(status);
break;
}
}
}
MqCommonResp commonResp = new MqCommonResp();
commonResp.setRespCode(MqCommonRespCode.SUCCESS.getCode());
commonResp.setRespMessage(MqCommonRespCode.SUCCESS.getMsg());
return commonResp;
}</code></pre><h2>小结</h2><p>对于消息状态的细化,更加便于我们后续的管理,和问题的定位。</p><p>希望本文对你有所帮助,如果喜欢,欢迎点赞收藏转发一波。</p><p>我是老马,期待与你的下次重逢。</p><h2>开源地址</h2><blockquote><a href="https://link.segmentfault.com/?enc=M0i13XD5LIyr53Mp6vhLRg%3D%3D.07reMxgmptGud4zkBpsrancwiHXuQ7NTu4%2F9lIG5R1k%3D" rel="nofollow">The message queue in java.(java 简易版本 mq 实现) </a> <a href="https://link.segmentfault.com/?enc=%2F5Rr2q5V0FBb7I%2FNclInlw%3D%3D.35EBv4JtVukqxgXXmvhpBc95NSzAvmm%2FL8cwx17tSJ4%3D" rel="nofollow">https://github.com/houbb/mq</a></blockquote><h2>拓展阅读</h2><p><a href="https://link.segmentfault.com/?enc=dn8tfNjMObh06t7Kb29RkQ%3D%3D.ZQpB%2Bzz3MoAmx09E0yeCWxfphUtA%2Bot5IQWdiuPvogs%3D" rel="nofollow">rpc-从零开始实现 rpc</a> <a href="https://link.segmentfault.com/?enc=yS9%2BssuScGdHGObrKz%2BMQA%3D%3D.VOTj0t6YXfCcPXPUwh0SXRVLoxHDUVFvFd9auNrAqR8%3D" rel="nofollow">https://github.com/houbb/rpc</a></p>
【mq】从零开始实现 mq-09-消费者拉取消息 pull message
https://segmentfault.com/a/1190000041830922
2022-05-11T19:40:51+08:00
2022-05-11T19:40:51+08:00
老马啸西风
https://segmentfault.com/u/houbinbin
0
<h2>前景回顾</h2><p><a href="https://link.segmentfault.com/?enc=BN7vKOf0clKcln%2FLGzJTWQ%3D%3D.5kP9gWxUJPZ9f%2F%2Fbph7swyj2mGuPKfT9yawQlrzMXc4KAcrGR5IatAhgK86rEgt%2F8obH6Vvx3f7VF%2FNBzLBI1w%3D%3D" rel="nofollow">【mq】从零开始实现 mq-01-生产者、消费者启动 </a></p><p><a href="https://link.segmentfault.com/?enc=JN%2FsP0ohReghe2loqzzr2g%3D%3D.ePf3k%2BeUfPXK7Mvpu7v1oESz%2FtymE1A0a1RaWNt%2BY%2F9zLCLhHMA2FWQlSQ48D%2BN3hpEN80vOkAQFqCTf2ks%2FcA%3D%3D" rel="nofollow">【mq】从零开始实现 mq-02-如何实现生产者调用消费者?</a></p><p><a href="https://link.segmentfault.com/?enc=%2FJxhQ33B5r0iYEtm6Zg9TA%3D%3D.G%2BViuD9ORHFUX6518LPWKinRCOmLLJqSniIW7guTHVW1xz25ddK7IGYMegS9nkw2%2F7Gx4gYGR8x4yuhHHmgqDg%3D%3D" rel="nofollow">【mq】从零开始实现 mq-03-引入 broker 中间人</a></p><p><a href="https://link.segmentfault.com/?enc=3FnD6ZhUnKXjwQ7tJqz7Dw%3D%3D.ii1rAPIT7HUSe9PDWIeeYoXfGIVncl4qHzaeV3RvhMEys%2FZfq2TDV%2F85QWZ3eBrS7pIQttWRil%2BwLyRxOlBzTg%3D%3D" rel="nofollow">【mq】从零开始实现 mq-04-启动检测与实现优化</a></p><p><a href="https://link.segmentfault.com/?enc=pFJnT%2FzOXr5ck361YWl9Xw%3D%3D.amPtOZ3Soxo5oANZQezJoc375ZnAc4%2FL4GG%2BHlbqiredbCOjAm5pvCyVHiuuY0B1b4x2NkY7RjDKAtmKGjHV3Q%3D%3D" rel="nofollow">【mq】从零开始实现 mq-05-实现优雅停机</a></p><p><a href="https://link.segmentfault.com/?enc=voMRbsO55IdV2mJw3%2F3Tog%3D%3D.TKU3ldfuWVpWCLP1zNX2WT2SW%2BuMfJmxxWLNT8IqDRosLdWUHrNQd1nVqXavn1g7mbCQuwoNt%2BzSDgcW0rMTPA%3D%3D" rel="nofollow">【mq】从零开始实现 mq-06-消费者心跳检测 heartbeat</a></p><p><a href="https://link.segmentfault.com/?enc=Pmpp1yCIZbkGmZAWH%2BSzMg%3D%3D.dHg1tLNW6v3Oiwz%2F3cqVRn7xooqxC010nBg5JEysiHz8KtC7HGVPKcY88QT2kHwxaS%2F7SI6jHa7xsUdA7%2BzpzA%3D%3D" rel="nofollow">【mq】从零开始实现 mq-07-负载均衡 load balance</a></p><p><a href="https://link.segmentfault.com/?enc=BCv7IDHGjQIlhxqvduUU9Q%3D%3D.gdd%2F11AZBlelTtlk0VLYUOaC0dnoND0wQU%2F7LFBGz1n0jNI9UbiCuy6zJvsP%2FNLbR5mPrnJMrLQQZ9CRpi2%2FiA%3D%3D" rel="nofollow">【mq】从零开始实现 mq-08-配置优化 fluent</a></p><p><a href="https://link.segmentfault.com/?enc=UIFW4udmdjKIDjBpCHsdWA%3D%3D.g%2F49LVPFn%2BL%2FwHlCF%2F5KH40LmNtL8wISAvnU13sya1WPkLMn3EDSNpyBifDqAVzGkYPQCsZ1yc1IvEkPybw5wQ%3D%3D" rel="nofollow">【mq】从零开始实现 mq-09-消费者拉取消息 pull message</a></p><h2>消息的推与拉</h2><p>大家好,我是老马。</p><p>这一节我们来一起看一下 MQ 消息中的推和拉两种模式。</p><h3>推</h3><p>消息由 broker 直接推送给消费者,实时性比较好。</p><p>缺点是如果消费者处理不过来,就会造成大量问题。</p><h3>拉</h3><p>消息由消费者定时从 broker 拉取,优点是实现简单,可以根据消费者自己的处理能力来消费。</p><p>缺点是实时性相对较差。</p><p>实际业务中,需要结合具体的场景,选择合适的策略。</p><p><img src="/img/remote/1460000041830924" alt="mq-pull" title="mq-pull"></p><h2>拉取策略实现</h2><h3>push 策略</h3><p>我们首先看一下 push 策略的简化核心实现:</p><pre><code class="java">package com.github.houbb.mq.consumer.core;
/**
* 推送消费策略
*
* @author binbin.hou
* @since 1.0.0
*/
public class MqConsumerPush extends Thread implements IMqConsumer {
@Override
public void run() {
// 启动服务端
log.info("MQ 消费者开始启动服务端 groupName: {}, brokerAddress: {}",
groupName, brokerAddress);
//1. 参数校验
this.paramCheck();
try {
//0. 配置信息
//1. 初始化
//2. 连接到服务端
//3. 标识为可用
//4. 添加钩子函数
//5. 启动完成以后的事件
this.afterInit();
log.info("MQ 消费者启动完成");
} catch (Exception e) {
log.error("MQ 消费者启动异常", e);
throw new MqException(ConsumerRespCode.RPC_INIT_FAILED);
}
}
/**
* 初始化完成以后
*/
protected void afterInit() {
}
// 其他方法
/**
* 获取消费策略类型
* @return 类型
* @since 0.0.9
*/
protected String getConsumerType() {
return ConsumerTypeConst.PUSH;
}
}</code></pre><p>我们在 push 中预留了一个 <code>afterInit</code> 方法,便于子类重载。</p><h2>pull 策略</h2><h3>消费者实现</h3><pre><code class="java">package com.github.houbb.mq.consumer.core;
/**
* 拉取消费策略
*
* @author binbin.hou
* @since 0.0.9
*/
public class MqConsumerPull extends MqConsumerPush {
private static final Log log = LogFactory.getLog(MqConsumerPull.class);
/**
* 拉取定时任务
*
* @since 0.0.9
*/
private final ScheduledExecutorService scheduledExecutorService = Executors.newSingleThreadScheduledExecutor();
/**
* 单次拉取大小
* @since 0.0.9
*/
private int size = 10;
/**
* 初始化延迟毫秒数
* @since 0.0.9
*/
private int pullInitDelaySeconds = 5;
/**
* 拉取周期
* @since 0.0.9
*/
private int pullPeriodSeconds = 5;
/**
* 订阅列表
* @since 0.0.9
*/
private final List<MqTopicTagDto> subscribeList = new ArrayList<>();
// 设置
@Override
protected String getConsumerType() {
return ConsumerTypeConst.PULL;
}
@Override
public synchronized void subscribe(String topicName, String tagRegex) {
MqTopicTagDto tagDto = buildMqTopicTagDto(topicName, tagRegex);
if(!subscribeList.contains(tagDto)) {
subscribeList.add(tagDto);
}
}
@Override
public void unSubscribe(String topicName, String tagRegex) {
MqTopicTagDto tagDto = buildMqTopicTagDto(topicName, tagRegex);
subscribeList.remove(tagDto);
}
private MqTopicTagDto buildMqTopicTagDto(String topicName, String tagRegex) {
MqTopicTagDto dto = new MqTopicTagDto();
dto.setTagRegex(tagRegex);
dto.setTopicName(topicName);
return dto;
}
}</code></pre><h4>订阅相关</h4><p>pull 策略可以把订阅/取消订阅放在本地,避免与服务端的交互。</p><h4>定时拉取</h4><p>我们重载了 push 策略的 <code>afterInit</code> 方法。</p><pre><code class="java">/**
* 初始化拉取消息
* @since 0.0.6
*/
@Override
public void afterInit() {
//5S 发一次心跳
scheduledExecutorService.scheduleAtFixedRate(new Runnable() {
@Override
public void run() {
if(CollectionUtil.isEmpty(subscribeList)) {
log.warn("订阅列表为空,忽略处理。");
return;
}
for(MqTopicTagDto tagDto : subscribeList) {
final String topicName = tagDto.getTopicName();
final String tagRegex = tagDto.getTagRegex();
MqConsumerPullResp resp = consumerBrokerService.pull(topicName, tagRegex, size);
if(MqCommonRespCode.SUCCESS.getCode().equals(resp.getRespCode())) {
List<MqMessage> mqMessageList = resp.getList();
if(CollectionUtil.isNotEmpty(mqMessageList)) {
for(MqMessage mqMessage : mqMessageList) {
IMqConsumerListenerContext context = new MqConsumerListenerContext();
mqListenerService.consumer(mqMessage, context);
}
}
} else {
log.error("拉取消息失败: {}", JSON.toJSON(resp));
}
}
}
}, pullInitDelaySeconds, pullPeriodSeconds, TimeUnit.SECONDS);
}</code></pre><p>应用启动时,指定时间定时拉取消息并进行消费处理。</p><p>其中 <code>consumerBrokerService.pull(topicName, tagRegex, size);</code> 拉取实现如下:</p><pre><code class="java">public MqConsumerPullResp pull(String topicName, String tagRegex, int fetchSize) {
MqConsumerPullReq req = new MqConsumerPullReq();
req.setSize(fetchSize);
req.setGroupName(groupName);
req.setTagRegex(tagRegex);
req.setTopicName(topicName);
final String traceId = IdHelper.uuid32();
req.setTraceId(traceId);
req.setMethodType(MethodType.C_MESSAGE_PULL);
Channel channel = getChannel(null);
return this.callServer(channel, req, MqConsumerPullResp.class);
}</code></pre><h3>Borker 相关</h3><h4>消息分发</h4><pre><code class="java">// 消费者主动 pull
if(MethodType.C_MESSAGE_PULL.equals(methodType)) {
MqConsumerPullReq req = JSON.parseObject(json, MqConsumerPullReq.class);
return mqBrokerPersist.pull(req, channel);
}</code></pre><h4>实现</h4><p>mqBrokerPersist 是一个接口,此处演示基于本地实现的,后续会实现基于数据库的持久化。</p><p>原理是类似的,此处仅作为演示。</p><pre><code class="java">@Override
public MqConsumerPullResp pull(MqConsumerPullReq pullReq, Channel channel) {
//1. 拉取匹配的信息
//2. 状态更新为代理中
//3. 如何更新对应的消费状态呢?
// 获取状态为 W 的订单
final int fetchSize = pullReq.getSize();
final String topic = pullReq.getTopicName();
final String tagRegex = pullReq.getTagRegex();
List<MqMessage> resultList = new ArrayList<>(fetchSize);
List<MqMessagePersistPut> putList = map.get(topic);
// 性能比较差
if(CollectionUtil.isNotEmpty(putList)) {
for(MqMessagePersistPut put : putList) {
final String status = put.getMessageStatus();
if(!MessageStatusConst.WAIT_CONSUMER.equals(status)) {
continue;
}
final MqMessage mqMessage = put.getMqMessage();
List<String> tagList = mqMessage.getTags();
if(InnerRegexUtils.hasMatch(tagList, tagRegex)) {
// 设置为处理中
// TODO: 消息的最终状态什么时候更新呢?
// 可以给 broker 一个 ACK
put.setMessageStatus(MessageStatusConst.PROCESS_CONSUMER);
resultList.add(mqMessage);
}
if(resultList.size() >= fetchSize) {
break;
}
}
}
MqConsumerPullResp resp = new MqConsumerPullResp();
resp.setRespCode(MqCommonRespCode.SUCCESS.getCode());
resp.setRespMessage(MqCommonRespCode.SUCCESS.getMsg());
resp.setList(resultList);
return resp;
}</code></pre><p>我们遍历找到匹配的消息,将其状态更新为中间状态。</p><p>不过这里还是缺少了一个关键的步骤,那就是消息的 ACK。</p><p>我们将在下一小节进行实现。</p><h2>小结</h2><p>消息的推送和拉取各有自己的优缺点,需要我们结合自己的业务,进行选择。</p><p>一般而言,IM 更加适合消息的推送;一般的业务,为了削峰填谷,更加适合拉取的模式。</p><p>希望本文对你有所帮助,如果喜欢,欢迎点赞收藏转发一波。</p><p>我是老马,期待与你的下次重逢。</p><h2>开源地址</h2><blockquote><a href="https://link.segmentfault.com/?enc=PZzq6jaHyfK1WOmId52FSw%3D%3D.CfktGWGAj8ln7Kbq1p2GXGa%2B%2FMSqtPU5spHL7GBjEvs%3D" rel="nofollow">The message queue in java.(java 简易版本 mq 实现) </a> <a href="https://link.segmentfault.com/?enc=EYedevIHBUyZ9MqUaJefuQ%3D%3D.QOXwABXDfVEe4pdsEncFGy2N%2BraczkdHT1EbSG2wVWs%3D" rel="nofollow">https://github.com/houbb/mq</a></blockquote><h2>拓展阅读</h2><p><a href="https://link.segmentfault.com/?enc=cLegU%2B9A%2FpXNibh7EdWJ5A%3D%3D.mHqqfIJjLEeieHASzPTnTQzTPWJIQVRKLqirxdUe5gw%3D" rel="nofollow">rpc-从零开始实现 rpc</a> <a href="https://link.segmentfault.com/?enc=fz3LnmqApgQEwrW%2FgbqwGQ%3D%3D.6oW6qIAIvD%2BIcmrpFkI5mIWn0wI7x%2B6eihZLnkk4CYQ%3D" rel="nofollow">https://github.com/houbb/rpc</a></p>
【mq】从零开始实现 mq-08-配置优化 fluent
https://segmentfault.com/a/1190000041825450
2022-05-10T22:15:07+08:00
2022-05-10T22:15:07+08:00
老马啸西风
https://segmentfault.com/u/houbinbin
0
<h2>前景回顾</h2><p><a href="https://link.segmentfault.com/?enc=s9G80wdqUboUXpCPXdWZOg%3D%3D.rhCSiGvo1LagbnOJ6HSoFBZEpTauYmuH7g8hcOxl8piQtLI4qiOJVHfXz663bG1C10rUWFdQLH1bb7QuOMyxlw%3D%3D" rel="nofollow">【mq】从零开始实现 mq-01-生产者、消费者启动 </a></p><p><a href="https://link.segmentfault.com/?enc=YeESuk2ooMSz3zgAvpWLhg%3D%3D.btf2V7RW0hLqswZeks%2B66uM7BEg8QdQVT0Y94EOLBQqD32JGebekETSEZ0DwUf%2Fs6Zt6XcpPPEUN6QoLWUWRxg%3D%3D" rel="nofollow">【mq】从零开始实现 mq-02-如何实现生产者调用消费者?</a></p><p><a href="https://link.segmentfault.com/?enc=B1Q%2BBqZnORuIky70rq6MHw%3D%3D.%2B5gEWsZq5mgQD6m3EkDaYEvVF7QBX3nnBYfKBPz8SLoyMK4zZdL41Mf9dIYW5qikX%2BPe8i07iXBf0LHSFTFbXQ%3D%3D" rel="nofollow">【mq】从零开始实现 mq-03-引入 broker 中间人</a></p><p><a href="https://link.segmentfault.com/?enc=r1vLPKSAp7ZtVYjhgIoKVA%3D%3D.8aTXlFWCVTpuhLVPmanrkd%2B1zQBIwrIXoyE3pqaBWjhs2GHrj26XoEzh4AVc7WPATCBmTQUB8sA0RiOeaFUHLA%3D%3D" rel="nofollow">【mq】从零开始实现 mq-04-启动检测与实现优化</a></p><p><a href="https://link.segmentfault.com/?enc=hSS6c%2FvjXTUysb52yBAwOw%3D%3D.9brBPkBIPvpLX%2BbrrWgj79yGI6bDkTL7azRgiODM06m5EZHY0Rpnd1R9Em3fzGMje335JqPFNckP8oJbzV1ILQ%3D%3D" rel="nofollow">【mq】从零开始实现 mq-05-实现优雅停机</a></p><p><a href="https://link.segmentfault.com/?enc=6HkNn7zx03Y3lNRpnAiqJg%3D%3D.V5%2B9teSzbVvbHEFsQynWXg8K2vBuEpB8eTTiStYFsvHxmno%2FvPsKAgZNMgmwYzy18fS%2BiJBozkQYlkk2EXQk7w%3D%3D" rel="nofollow">【mq】从零开始实现 mq-06-消费者心跳检测 heartbeat</a></p><p><a href="https://link.segmentfault.com/?enc=2to3JuKT%2FvtSXwV%2FlDYFnA%3D%3D.K2qDMjwUWgvKvA97xrpWMZSWlPWhnLwOq0QWSlwL%2BsufBkhmf%2B%2FK2%2Fj43Hf93qgz27PLF%2FNMdCcuS1D6TtfIrA%3D%3D" rel="nofollow">【mq】从零开始实现 mq-07-负载均衡 load balance</a></p><p><a href="https://link.segmentfault.com/?enc=Cdflbi87F3Ne%2B7TDgCphEg%3D%3D.emBgGRVWC9ZrzY9lM5aohCa7Q%2BNUoS2PR%2FWuFnSHs%2BQeAqbjUVramdc5FyLtTd9NDSGpTL8GexYHuwAiCbxGiQ%3D%3D" rel="nofollow">【mq】从零开始实现 mq-08-配置优化 fluent</a></p><h2>fluent</h2><p>大家好,我是老马。</p><p>fluent 的配置方式,是我个人非常喜欢的一种配置方式。</p><p>传统的 java 使用 get/set 方法进行属性设置。</p><p>类似这种:</p><pre><code class="java">MqBroker mqBroker = new MqBroker();
mqBroker.setPort(9999);
mqBroker.setAddress("127.0.0.1");</code></pre><p>fluent 写法可以让我们写起来代码更加流畅:</p><pre><code class="java">MqBroker.newInstance()
.port(9999)
.address("127.0.0.1")</code></pre><p>写起来更加丝滑流畅。</p><p><img src="/img/remote/1460000041825452" alt="08" title="08"></p><h2>Broker 配置</h2><h3>属性</h3><pre><code class="java">/**
* 端口号
*/
private int port = BrokerConst.DEFAULT_PORT;
/**
* 调用管理类
*
* @since 1.0.0
*/
private final IInvokeService invokeService = new InvokeService();
/**
* 消费者管理
*
* @since 0.0.3
*/
private IBrokerConsumerService registerConsumerService = new LocalBrokerConsumerService();
/**
* 生产者管理
*
* @since 0.0.3
*/
private IBrokerProducerService registerProducerService = new LocalBrokerProducerService();
/**
* 持久化类
*
* @since 0.0.3
*/
private IMqBrokerPersist mqBrokerPersist = new LocalMqBrokerPersist();
/**
* 推送服务
*
* @since 0.0.3
*/
private IBrokerPushService brokerPushService = new BrokerPushService();
/**
* 获取响应超时时间
* @since 0.0.3
*/
private long respTimeoutMills = 5000;
/**
* 负载均衡
* @since 0.0.7
*/
private ILoadBalance<ConsumerSubscribeBo> loadBalance = LoadBalances.weightRoundRobbin();
/**
* 推送最大尝试次数
* @since 0.0.8
*/
private int pushMaxAttempt = 3;</code></pre><h3>flent 配置</h3><pre><code class="java">public MqBroker port(int port) {
this.port = port;
return this;
}
public MqBroker registerConsumerService(IBrokerConsumerService registerConsumerService) {
this.registerConsumerService = registerConsumerService;
return this;
}
public MqBroker registerProducerService(IBrokerProducerService registerProducerService) {
this.registerProducerService = registerProducerService;
return this;
}
public MqBroker mqBrokerPersist(IMqBrokerPersist mqBrokerPersist) {
this.mqBrokerPersist = mqBrokerPersist;
return this;
}
public MqBroker brokerPushService(IBrokerPushService brokerPushService) {
this.brokerPushService = brokerPushService;
return this;
}
public MqBroker respTimeoutMills(long respTimeoutMills) {
this.respTimeoutMills = respTimeoutMills;
return this;
}
public MqBroker loadBalance(ILoadBalance<ConsumerSubscribeBo> loadBalance) {
this.loadBalance = loadBalance;
return this;
}</code></pre><h2>Producer 配置</h2><h3>属性</h3><pre><code class="java">/**
* 分组名称
*/
private String groupName = ProducerConst.DEFAULT_GROUP_NAME;
/**
* 中间人地址
*/
private String brokerAddress = "127.0.0.1:9999";
/**
* 获取响应超时时间
* @since 0.0.2
*/
private long respTimeoutMills = 5000;
/**
* 检测 broker 可用性
* @since 0.0.4
*/
private volatile boolean check = true;
/**
* 调用管理服务
* @since 0.0.2
*/
private final IInvokeService invokeService = new InvokeService();
/**
* 状态管理类
* @since 0.0.5
*/
private final IStatusManager statusManager = new StatusManager();
/**
* 生产者-中间服务端服务类
* @since 0.0.5
*/
private final IProducerBrokerService producerBrokerService = new ProducerBrokerService();
/**
* 为剩余的请求等待时间
* @since 0.0.5
*/
private long waitMillsForRemainRequest = 60 * 1000;
/**
* 负载均衡策略
* @since 0.0.7
*/
private ILoadBalance<RpcChannelFuture> loadBalance = LoadBalances.weightRoundRobbin();
/**
* 消息发送最大尝试次数
* @since 0.0.8
*/
private int maxAttempt = 3;</code></pre><h3>fluent 配置</h3><pre><code class="java">public MqProducer groupName(String groupName) {
this.groupName = groupName;
return this;
}
public MqProducer brokerAddress(String brokerAddress) {
this.brokerAddress = brokerAddress;
return this;
}
public MqProducer respTimeoutMills(long respTimeoutMills) {
this.respTimeoutMills = respTimeoutMills;
return this;
}
public MqProducer check(boolean check) {
this.check = check;
return this;
}
public MqProducer waitMillsForRemainRequest(long waitMillsForRemainRequest) {
this.waitMillsForRemainRequest = waitMillsForRemainRequest;
return this;
}
public MqProducer loadBalance(ILoadBalance<RpcChannelFuture> loadBalance) {
this.loadBalance = loadBalance;
return this;
}
public MqProducer maxAttempt(int maxAttempt) {
this.maxAttempt = maxAttempt;
return this;
}</code></pre><h2>Consuemr 配置</h2><h3>属性</h3><pre><code class="java">/**
* 组名称
*/
private String groupName = ConsumerConst.DEFAULT_GROUP_NAME;
/**
* 中间人地址
*/
private String brokerAddress = "127.0.0.1:9999";
/**
* 获取响应超时时间
* @since 0.0.2
*/
private long respTimeoutMills = 5000;
/**
* 检测 broker 可用性
* @since 0.0.4
*/
private volatile boolean check = true;
/**
* 为剩余的请求等待时间
* @since 0.0.5
*/
private long waitMillsForRemainRequest = 60 * 1000;
/**
* 调用管理类
*
* @since 1.0.0
*/
private final IInvokeService invokeService = new InvokeService();
/**
* 消息监听服务类
* @since 0.0.5
*/
private final IMqListenerService mqListenerService = new MqListenerService();
/**
* 状态管理类
* @since 0.0.5
*/
private final IStatusManager statusManager = new StatusManager();
/**
* 生产者-中间服务端服务类
* @since 0.0.5
*/
private final IConsumerBrokerService consumerBrokerService = new ConsumerBrokerService();
/**
* 负载均衡策略
* @since 0.0.7
*/
private ILoadBalance<RpcChannelFuture> loadBalance = LoadBalances.weightRoundRobbin();
/**
* 订阅最大尝试次数
* @since 0.0.8
*/
private int subscribeMaxAttempt = 3;
/**
* 取消订阅最大尝试次数
* @since 0.0.8
*/
private int unSubscribeMaxAttempt = 3;</code></pre><h3>fluent 配置</h3><pre><code class="java">public MqConsumerPush subscribeMaxAttempt(int subscribeMaxAttempt) {
this.subscribeMaxAttempt = subscribeMaxAttempt;
return this;
}
public MqConsumerPush unSubscribeMaxAttempt(int unSubscribeMaxAttempt) {
this.unSubscribeMaxAttempt = unSubscribeMaxAttempt;
return this;
}
public MqConsumerPush groupName(String groupName) {
this.groupName = groupName;
return this;
}
public MqConsumerPush brokerAddress(String brokerAddress) {
this.brokerAddress = brokerAddress;
return this;
}
public MqConsumerPush respTimeoutMills(long respTimeoutMills) {
this.respTimeoutMills = respTimeoutMills;
return this;
}
public MqConsumerPush check(boolean check) {
this.check = check;
return this;
}
public MqConsumerPush waitMillsForRemainRequest(long waitMillsForRemainRequest) {
this.waitMillsForRemainRequest = waitMillsForRemainRequest;
return this;
}
public MqConsumerPush loadBalance(ILoadBalance<RpcChannelFuture> loadBalance) {
this.loadBalance = loadBalance;
return this;
}</code></pre><h2>小结</h2><p>这一节的实现非常简单,可以说是没有啥技术难度。</p><p>只是为了让使用者更加方便。</p><p>希望本文对你有所帮助,如果喜欢,欢迎点赞收藏转发一波。</p><p>我是老马,期待与你的下次重逢。</p><h2>开源地址</h2><blockquote><a href="https://link.segmentfault.com/?enc=QJto6YjIQ%2FYnsogYXQhLVA%3D%3D.nPgmvof67tBLAXbhLMkxSM43FK7nItFfF6dCxUk3Ans%3D" rel="nofollow">The message queue in java.(java 简易版本 mq 实现) </a> <a href="https://link.segmentfault.com/?enc=6kdFeNMxeqaEfWjkEQ2GqA%3D%3D.PmMCCeGntKQj85AsusAJVwbLNzulfzAGx7r1loEJhbU%3D" rel="nofollow">https://github.com/houbb/mq</a></blockquote><h2>拓展阅读</h2><p><a href="https://link.segmentfault.com/?enc=RPq69fMQSIycZ1HfpwFUTA%3D%3D.qNKiF4acQJ5tX8hDHpSPyfq4cOU3fepBrf2EIn2tY2g%3D" rel="nofollow">rpc-从零开始实现 rpc</a> <a href="https://link.segmentfault.com/?enc=xy6jovJpPVXWhmBX0KYiuA%3D%3D.dKm3q3Htu3%2BhUORJYGyF4VTFpApc8jnDf%2BCREiTpPw4%3D" rel="nofollow">https://github.com/houbb/rpc</a></p>
【mq】从零开始实现 mq-07-负载均衡 load balance
https://segmentfault.com/a/1190000041812058
2022-05-08T13:01:33+08:00
2022-05-08T13:01:33+08:00
老马啸西风
https://segmentfault.com/u/houbinbin
0
<h2>前景回顾</h2><p><a href="https://link.segmentfault.com/?enc=fRX6kIOf6BTblShEQVoWYg%3D%3D.W5DRrUEmrcxdLrEUAYAF5v8UJh1mi0%2BrzZtY8DL2zlPN2jxQQ6fQXiRT4h9xLDsVa4Y1QB2WdTg8Rp1mhchgBw%3D%3D" rel="nofollow">【mq】从零开始实现 mq-01-生产者、消费者启动 </a></p><p><a href="https://link.segmentfault.com/?enc=Lnpm%2FFZ7D1LLpVQZQ9flnw%3D%3D.NadCFP8GF%2BDX8PeK9ncHJC63aWIMK8Vbr6cBdgXH%2BukycsPwiPMiGm6fJedwCNWvsZ8vQE51kAhSFpj7XAsTiA%3D%3D" rel="nofollow">【mq】从零开始实现 mq-02-如何实现生产者调用消费者?</a></p><p><a href="https://link.segmentfault.com/?enc=As3HZ3GEKLBdnw0bGP4Lvw%3D%3D.1xGORt%2FtFciqwZoDShLpMBtnqxjY31NkxCYi7ied3Xg7gI5HK4EfJ02v8RMVdm62Q2mlotR2V1Btt3YVTBEMHQ%3D%3D" rel="nofollow">【mq】从零开始实现 mq-03-引入 broker 中间人</a></p><p><a href="https://link.segmentfault.com/?enc=DQDYMhR1L803DkfVACdVoQ%3D%3D.lnZiz7iXZz4BFHJwFr6iUqibFf91tsemkRq11gKa9FmSlS9OKxOugWp9bZtiulfgOA2k8SngOb4MdocGz5gLug%3D%3D" rel="nofollow">【mq】从零开始实现 mq-04-启动检测与实现优化</a></p><p><a href="https://link.segmentfault.com/?enc=x1niiDCJ4rO0BW1u8w7KUA%3D%3D.JNLPKcmx2ZETGHJu2npJBiMBRVBLbhicX5hYq1rYZAHHnJwo%2BpuUZjY%2FM2AD%2F4mpltQAtGVAQSttfPJzDkQtLQ%3D%3D" rel="nofollow">【mq】从零开始实现 mq-05-实现优雅停机</a></p><p><a href="https://link.segmentfault.com/?enc=nH1WC3VymIHMu0Du7TVcyw%3D%3D.Jl8a%2FlWgwgCf5uLZ79nFcNk4U7wQ%2BOU1lrFlHbFy6QNG2DVzXAu3MlWVqoYcnSi6zOLANZCcakeNqw6Y94iPrQ%3D%3D" rel="nofollow">【mq】从零开始实现 mq-06-消费者心跳检测 heartbeat</a></p><p><a href="https://link.segmentfault.com/?enc=hLw%2FYB8ctzu5oDM9dfvpFw%3D%3D.yoF24hDDHg4SvbXXrsfQXIkBq%2BvjktO19KFOZgVxuPEq%2B3MLwsdI4U%2Fjx5gGAa1p8U%2BKHOZElZrZ%2B8rF5auXZw%3D%3D" rel="nofollow">【mq】从零开始实现 mq-07-负载均衡 load balance</a></p><h2>为什么需要负载均衡</h2><p>大家好,我是老马。</p><p>这一节让我们看一下如何实现 MQ 的负载均衡。</p><p>为什么需要负载均衡呢?</p><h3>作用</h3><p>负载均衡最核心的作用:</p><p>(1)可以避免单点故障</p><p>(2)可以让请求均分的分散到每一个节点</p><h3>实现思路</h3><p>负载均衡实现的方式比较多,最简单的就是随机选择一个。</p><p>拓展阅读:</p><blockquote><a href="https://link.segmentfault.com/?enc=2sY3dFUGRu4dmJpg2v3wkQ%3D%3D.X%2FJFHnd%2B5bVUslI%2F7hdV5zEkScpfgzdoMcfsjKj2seDyeXp5kG2PUHiji7JAlzaY12CniKvSp%2BH%2Fixi0LgBfrg%3D%3D" rel="nofollow">从零手写实现负载均衡</a> <a href="https://link.segmentfault.com/?enc=zwwv6pOB%2Bke9mwen%2Bg%2F43Q%3D%3D.xcAN0RAXsoh9%2FGwgVaSxuNScNpanJK%2FDF4gkPL%2FxHGugyvH%2FpzyBCucke5jAdeFctKHIyMFsgZiw674k2sfWkw%3D%3D" rel="nofollow">http://houbb.github.io/2020/0...</a></blockquote><p><img src="/img/remote/1460000041812060" alt="负载均衡" title="负载均衡"></p><h2>MQ 中用到负载均衡的地方</h2><h3>生产者发送</h3><p>生产者发送消息时,可以发送给任一 broker。</p><h3>broker 推送给消费者</h3><p>broker 接收到消息以后,在推送给消费者时,也可以任一选择一个。</p><h3>消费者的消费 ACK</h3><p>消费者消费完,状态回执给 broker,可以选择任一一个。</p><h3>消息黏连</h3><p>有些消息比较特殊,比如需要保证消费的有序性,可以通过 shardingKey 的方式,在负载的时候固定到指定的片区。</p><h2>代码实现</h2><h3>生产者发送</h3><p>统一调整获取 channel 的方法。</p><pre><code class="java">@Override
public Channel getChannel(String key) {
// 等待启动完成
while (!statusManager.status()) {
log.debug("等待初始化完成...");
DateUtil.sleep(100);
}
RpcChannelFuture rpcChannelFuture = RandomUtils.loadBalance(this.loadBalance,
channelFutureList, key);
return rpcChannelFuture.getChannelFuture().channel();
}</code></pre><p>工具类实现为核心实现:</p><pre><code class="java">/**
* 负载均衡
*
* @param list 列表
* @param key 分片键
* @return 结果
* @since 0.0.7
*/
public static <T extends IServer> T loadBalance(final ILoadBalance<T> loadBalance,
final List<T> list, String key) {
if(CollectionUtil.isEmpty(list)) {
return null;
}
if(StringUtil.isEmpty(key)) {
LoadBalanceContext<T> loadBalanceContext = LoadBalanceContext.<T>newInstance()
.servers(list);
return loadBalance.select(loadBalanceContext);
}
// 获取 code
int hashCode = Objects.hash(key);
int index = hashCode % list.size();
return list.get(index);
}</code></pre><p>如果指定了 shardingKey,那么根据 shadringKey 进行 hash 判断。</p><p>如果没有,则进行默认的负载均衡策略。</p><h3>Broker 消息推送给消费者</h3><p>消费者订阅列表的获取:</p><pre><code class="java">@Override
public List<Channel> getSubscribeList(MqMessage mqMessage) {
final String topicName = mqMessage.getTopic();
Set<ConsumerSubscribeBo> set = subscribeMap.get(topicName);
if(CollectionUtil.isEmpty(set)) {
return Collections.emptyList();
}
//2. 获取匹配的 tag 列表
final List<String> tagNameList = mqMessage.getTags();
Map<String, List<ConsumerSubscribeBo>> groupMap = new HashMap<>();
for(ConsumerSubscribeBo bo : set) {
String tagRegex = bo.getTagRegex();
if(hasMatch(tagNameList, tagRegex)) {
//TODO: 这种设置模式,统一添加处理 haven
String groupName = bo.getGroupName();
List<ConsumerSubscribeBo> list = groupMap.get(groupName);
if(list == null) {
list = new ArrayList<>();
}
list.add(bo);
groupMap.put(groupName, list);
}
}
//3. 按照 groupName 分组之后,每一组只随机返回一个。最好应该调整为以 shardingkey 选择
final String shardingKey = mqMessage.getShardingKey();
List<Channel> channelList = new ArrayList<>();
for(Map.Entry<String, List<ConsumerSubscribeBo>> entry : groupMap.entrySet()) {
List<ConsumerSubscribeBo> list = entry.getValue();
ConsumerSubscribeBo bo = RandomUtils.loadBalance(loadBalance, list, shardingKey);
final String channelId = bo.getChannelId();
BrokerServiceEntryChannel entryChannel = registerMap.get(channelId);
if(entryChannel == null) {
log.warn("channelId: {} 对应的通道信息为空", channelId);
continue;
}
channelList.add(entryChannel.getChannel());
}
return channelList;
}</code></pre><p>核心逻辑:<code>RandomUtils.loadBalance(loadBalance, list, shardingKey);</code> 获取,其他的保持不变。</p><h3>消费者 ACK</h3><p>消费者也是类似的,获取 channel 的方式调整如下:</p><pre><code class="java">public Channel getChannel(String key) {
// 等待启动完成
while (!statusManager.status()) {
log.debug("等待初始化完成...");
DateUtil.sleep(100);
}
RpcChannelFuture rpcChannelFuture = RandomUtils.loadBalance(loadBalance,
channelFutureList, key);
return rpcChannelFuture.getChannelFuture().channel();
}</code></pre><h2>小结</h2><p>负载均衡在分布式服务中,是必备的特性之一。实现的原理并不算复杂。</p><p>希望本文对你有所帮助,如果喜欢,欢迎点赞收藏转发一波。</p><p>我是老马,期待与你的下次重逢。</p><h2>开源地址</h2><blockquote><a href="https://link.segmentfault.com/?enc=9AnG2y0ehE81TN1wHhaABQ%3D%3D.kcQ2xrs8HHdP%2Bp4%2ByIIZOqAKoJxFYBIutk3OIcRX2Fo%3D" rel="nofollow">The message queue in java.(java 简易版本 mq 实现) </a> <a href="https://link.segmentfault.com/?enc=nWpJArfeBa0SV4PGH312mQ%3D%3D.5YHe%2FvcKUMgRo%2FSa0o4wBJDcSs6AnPsu6RI%2BCssgssw%3D" rel="nofollow">https://github.com/houbb/mq</a></blockquote><h2>拓展阅读</h2><p><a href="https://link.segmentfault.com/?enc=r73swLH9lUSmZWdoWEk%2FHQ%3D%3D.DBnL0wWC5AWHgDiDK5G46Sza7jswIdttQszNUlkpiog%3D" rel="nofollow">rpc-从零开始实现 rpc</a> <a href="https://link.segmentfault.com/?enc=qZLOAO3ECxHKNPVMOD4BIQ%3D%3D.N8z033%2Bw%2FzT5pwOJ2RcvgTD8hVhtYs4VTO%2FXtc13yLw%3D" rel="nofollow">https://github.com/houbb/rpc</a></p>
【mq】从零开始实现 mq-06-消费者心跳检测 heartbeat
https://segmentfault.com/a/1190000041801455
2022-05-06T14:56:32+08:00
2022-05-06T14:56:32+08:00
老马啸西风
https://segmentfault.com/u/houbinbin
0
<h2>前景回顾</h2><p><a href="https://link.segmentfault.com/?enc=oULqZBt5E7YKPQPUHZ4IsA%3D%3D.DIZmEVPuCnaR8OkkwZ6KkMkyuc2OfAYOKZzTH0Lxq%2FHokxIOFq5L5WdwfFUwAOzusGuS3zgFgwVgNgjjwJFqrQ%3D%3D" rel="nofollow">【mq】从零开始实现 mq-01-生产者、消费者启动 </a></p><p><a href="https://link.segmentfault.com/?enc=RntKY24dM2NbLZ5jW8Mr5A%3D%3D.Lz1GtpiF0MO2Ps9Yh9uXvxBj2Du7UxNbAB4ZVurE2Lc0ASdwRv5o7qpSvD8BGIDmRsvizucK3ayIFjXznjSgrA%3D%3D" rel="nofollow">【mq】从零开始实现 mq-02-如何实现生产者调用消费者?</a></p><p><a href="https://link.segmentfault.com/?enc=orh1I473ilNTiNjLkHeijQ%3D%3D.hVrObCAd77lXRL567e0cqniVVijBVJBDx30vcg7jEgCzx1Jv5DEP8SFotkQWvg9bfjnXQWZYDRglBxF7%2BhILSg%3D%3D" rel="nofollow">【mq】从零开始实现 mq-03-引入 broker 中间人</a></p><p><a href="https://link.segmentfault.com/?enc=tVbxpuosiAVya0zRzdOpkA%3D%3D.dWl08kKOEbXJCQTFk%2FWk2tdiZmQXKB1A2JuTvwCkRvEzwDtANwpOyHwYP8y1KgHxXUEjjjlwVBP8nxGBBlF6dw%3D%3D" rel="nofollow">【mq】从零开始实现 mq-04-启动检测与实现优化</a></p><p><a href="https://link.segmentfault.com/?enc=TfOWbOLVHoHvSSk9NOHiPQ%3D%3D.1V2lzjG2fcpbmRToWQPP5JUEbqaTA9%2BZqJ8MwyqOhCcfSNpvqjB0%2BnPnRq75zPF%2BRl%2BgVzLq9Bic5rpODT8nOg%3D%3D" rel="nofollow">【mq】从零开始实现 mq-05-实现优雅停机</a></p><p><a href="https://link.segmentfault.com/?enc=hFBzkQY73DAbS7bBkV1%2FvQ%3D%3D.P3O4YPLNg0ItCN0y2RxujSsTHozBgEvqNOHU0O%2BX3rW5hYWeKMQkWqpy%2BWS%2Bkr9S1j1lvm5p4%2FUZpfa7%2BVOuWQ%3D%3D" rel="nofollow">【mq】从零开始实现 mq-06-消费者心跳检测 heartbeat</a></p><h2>为什么需要心跳?</h2><p><strong>心跳(heartbeat ),顾名思义就是心脏的跳动。</strong></p><p>医学上一般通过心跳是否跳动,来判断一个人是否活着。</p><p>那么,分布式服务中如何判断一个服务是否还活着呢?</p><h3>实现思路</h3><p>比如 mq 中,broker 需要把消息实时推送给在线的消费者。</p><p>那么如何判断一个消费者是否活着呢?</p><p>我们可以让消费者定时,比如每 5 秒钟给 broker 发送一个心跳包,考虑到网络延迟等,如果连续 1min 都没有收到心跳,我们则移除这个消费者,认为服务已经挂了。</p><p><img src="/img/remote/1460000041801457" alt="heartbeat" title="heartbeat"></p><h2>消费者实现</h2><p>上代码!</p><h3>心跳实现</h3><p>心跳可以是一个很简单的消息体。</p><pre><code class="java">@Override
public void heartbeat() {
final MqHeartBeatReq req = new MqHeartBeatReq();
final String traceId = IdHelper.uuid32();
req.setTraceId(traceId);
req.setMethodType(MethodType.C_HEARTBEAT);
req.setAddress(NetUtil.getLocalHost());
req.setPort(0);
req.setTime(System.currentTimeMillis());
log.debug("[HEARTBEAT] 往服务端发送心跳包 {}", JSON.toJSON(req));
// 通知全部
for(RpcChannelFuture channelFuture : channelFutureList) {
try {
Channel channel = channelFuture.getChannelFuture().channel();
callServer(channel, req, null);
} catch (Exception exception) {
log.error("[HEARTBEAT] 往服务端处理异常", exception);
}
}
}</code></pre><p>消费者把心跳通知所有的 broker.</p><h3>心跳的定时执行</h3><p>我们启动一个定时任务,5S 钟执行一次。</p><pre><code class="java">/**
* 初始化心跳
* @since 0.0.6
*/
private void initHeartbeat() {
//5S 发一次心跳
scheduledExecutorService.scheduleAtFixedRate(new Runnable() {
@Override
public void run() {
heartbeat();
}
}, 5, 5, TimeUnit.SECONDS);
}</code></pre><p>心跳是在连接到 broker 之后就开始启动:</p><pre><code class="java">@Override
public void initChannelFutureList(ConsumerBrokerConfig config) {
//1. 配置初始化
//...
//2. 初始化
this.channelFutureList = ChannelFutureUtils.initChannelFutureList(brokerAddress,
initChannelHandler(), check);
//3. 初始化心跳
this.initHeartbeat();
}</code></pre><h2>Broker 实现</h2><p>消费者定时发送消息,生产者肯定是需要接受的。</p><h3>接收心跳</h3><p>为了简单,我们让心跳是 ONE-WAY 的。</p><pre><code class="java">// 消费者心跳
if(MethodType.C_HEARTBEAT.equals(methodType)) {
MqHeartBeatReq req = JSON.parseObject(json, MqHeartBeatReq.class);
registerConsumerService.heartbeat(req, channel);
return null;
}</code></pre><h3>hearbeat 处理</h3><p>每次收到消息,我们把请求的 channelId 记录下来,并保存最新的访问时间</p><pre><code class="java">@Override
public void heartbeat(MqHeartBeatReq mqHeartBeatReq, Channel channel) {
final String channelId = ChannelUtil.getChannelId(channel);
log.info("[HEARTBEAT] 接收消费者心跳 {}, channelId: {}",
JSON.toJSON(mqHeartBeatReq), channelId);
ServiceEntry serviceEntry = new ServiceEntry();
serviceEntry.setAddress(mqHeartBeatReq.getAddress());
serviceEntry.setPort(mqHeartBeatReq.getPort());
BrokerServiceEntryChannel entryChannel = InnerChannelUtils.buildEntryChannel(serviceEntry, channel);
entryChannel.setLastAccessTime(mqHeartBeatReq.getTime());
heartbeatMap.put(channelId, entryChannel);
}</code></pre><h3>移除消费者</h3><p>如果一些消费者长时间没有心跳,我们就认为服务已经挂了。</p><p>在 <code>LocalBrokerConsumerService</code> 服务启动的时候,同时启用一个定时清理任务。</p><pre><code class="java">public LocalBrokerConsumerService() {
//120S 扫描一次
final long limitMills = 2 * 60 * 1000;
scheduledExecutorService.scheduleAtFixedRate(new Runnable() {
@Override
public void run() {
for(Map.Entry<String, BrokerServiceEntryChannel> entry : heartbeatMap.entrySet()) {
String key = entry.getKey();
long lastAccessTime = entry.getValue().getLastAccessTime();
long currentTime = System.currentTimeMillis();
if(currentTime - lastAccessTime > limitMills) {
removeByChannelId(key);
}
}
}
}, 2 * 60, 2 * 60, TimeUnit.SECONDS);
}</code></pre><p>这个任务 2min 执行一次,如果 2min 都没有心跳,这移除对应的消费者。</p><h2>小结</h2><p>心跳,是网络传输中验证服务可用性非常简单,但是有效的方式。</p><p>希望本文对你有所帮助,如果喜欢,欢迎点赞收藏转发一波。</p><p>我是老马,期待与你的下次重逢。</p><h2>开源地址</h2><blockquote><a href="https://link.segmentfault.com/?enc=mSsQH4e9dAfrqfj52e6k%2FA%3D%3D.5BL%2Bd3bPE%2F7esk1fd8Ai78WD8tCFPrfRk3Us2l475ds%3D" rel="nofollow">The message queue in java.(java 简易版本 mq 实现) </a> <a href="https://link.segmentfault.com/?enc=27HK5Kx%2FwkE%2Fs%2B%2BtLbTjQw%3D%3D.1jJhhh1vcN%2FRDnCsW4uBhiULYni2ekfNxqFGe0x5sS0%3D" rel="nofollow">https://github.com/houbb/mq</a></blockquote><h2>拓展阅读</h2><p><a href="https://link.segmentfault.com/?enc=3ja7ByNcoTdw6k5NLKN84Q%3D%3D.3gn5l9HCE%2BHbtb8MT31d3NWQp3b%2F6ncjScMQyKtqRSU%3D" rel="nofollow">rpc-从零开始实现 rpc</a> <a href="https://link.segmentfault.com/?enc=w6i4%2FAwWal%2FPf4m7xzWgjg%3D%3D.DTFYnx0A5zJZiKMUNlmkcDKilbPv1Z3VK46CCxUyZ4c%3D" rel="nofollow">https://github.com/houbb/rpc</a></p>
【mq】从零开始实现 mq-05-实现优雅停机
https://segmentfault.com/a/1190000041796827
2022-05-05T19:41:55+08:00
2022-05-05T19:41:55+08:00
老马啸西风
https://segmentfault.com/u/houbinbin
0
<h2>前景回顾</h2><p><a href="https://link.segmentfault.com/?enc=fquAhOe0MtvL%2F6g2elj%2B0Q%3D%3D.pWz56O8OHiAlgcxLBsg0hESkiMp%2FhheYHNhSXFk4JfOx%2FuUMGWzyLYpAKWaCYrJ7McPPOOS5g1ZI8mp1JSdRUw%3D%3D" rel="nofollow">【mq】从零开始实现 mq-01-生产者、消费者启动 </a></p><p><a href="https://link.segmentfault.com/?enc=yyK7qWQbJkaS5DgdZNvxtg%3D%3D.hVRv2d5SciqBvzkO%2FGKVs1SdmwG%2FLXIO3FzQQZpvM3IAGKU%2Bq55vW%2BIrBT79%2Bn0tUJbn%2Bac1g8eIQtwA3y5vqw%3D%3D" rel="nofollow">【mq】从零开始实现 mq-02-如何实现生产者调用消费者?</a></p><p><a href="https://link.segmentfault.com/?enc=KXjMQgEWud9aB29C0bIl2g%3D%3D.pUFSvQQmS7k78dBUTPZ5g32wDNDRWRF5kuPd0fc%2Ffpd6IkZhS6556i4t1MqYAkkYmDo%2BlyO8synNnmjnT8c5Mw%3D%3D" rel="nofollow">【mq】从零开始实现 mq-03-引入 broker 中间人</a></p><p><a href="https://link.segmentfault.com/?enc=H610vWA5FDjt14FtcHiWWg%3D%3D.Y%2BQyk93DetqrdlfMHW05nnt9t5ANj1qmcJNklCt4eoOihMEUULEzbLiRtmWYehBfvy9R0mfK0ipjxRcIC%2FACGQ%3D%3D" rel="nofollow">【mq】从零开始实现 mq-04-启动检测与实现优化</a></p><p><a href="https://link.segmentfault.com/?enc=OVJZCHNpo86m80b5VfR0aQ%3D%3D.aaA5xXrt3ONjx%2F3TTxXzhW378CS5gk0QLEl0yozs4ngPddfpWpBha6uknmu0ZFNlK97OmmRweQY%2BXCz4toPRkQ%3D%3D" rel="nofollow">【mq】从零开始实现 mq-05-实现优雅停机</a></p><h3>为什么需要优雅关闭?</h3><p>我记得多年前,那个时候 rpc 框架主流用的还是 dubbo,每次都是半夜还是上线,上线上完基本都是凌晨 2-3 点。</p><p>为什么要半夜上线呢?</p><p>因为这个时候一般业务流量最低。</p><p>还有就是上线发布,每次都要人工等待一段几分钟。</p><p>因为 rpc 调用入口已经关闭了,但是本身可能还没有处理完。</p><p>那么有没有方法可以让服务的关闭更加优雅,而不是人工等待呢?</p><p><img src="/img/remote/1460000041796829" alt="05" title="05"></p><h3>实现思路</h3><p>人工等待几分钟的方式一般可以解决问题,但是大部分情况是无用功,还比较浪费时间。</p><p>比较自然的一种方式是引入钩子函数。</p><p>当应用准备关闭时,首先判断是否存在处理中的请求,不存在则直接关闭;存在,则等待请求完成再关闭。</p><h2>实现</h2><p>生产者和消费者是类似的,我们以生产者为例。</p><h3>启动实现的调整</h3><pre><code class="java">@Override
public synchronized void run() {
this.paramCheck();
// 启动服务端
log.info("MQ 生产者开始启动客户端 GROUP: {} brokerAddress: {}",
groupName, brokerAddress);
try {
//0. 配置信息
ProducerBrokerConfig config = ProducerBrokerConfig.newInstance()
.groupName(groupName)
.brokerAddress(brokerAddress)
.check(check)
.respTimeoutMills(respTimeoutMills)
.invokeService(invokeService)
.statusManager(statusManager);
//1. 初始化
this.producerBrokerService.initChannelFutureList(config);
//2. 连接到服务端
this.producerBrokerService.registerToBroker();
//3. 标识为可用
statusManager.status(true);
//4. 添加钩子函数
final DefaultShutdownHook rpcShutdownHook = new DefaultShutdownHook();
rpcShutdownHook.setStatusManager(statusManager);
rpcShutdownHook.setInvokeService(invokeService);
rpcShutdownHook.setWaitMillsForRemainRequest(waitMillsForRemainRequest);
rpcShutdownHook.setDestroyable(this.producerBrokerService);
ShutdownHooks.rpcShutdownHook(rpcShutdownHook);
log.info("MQ 生产者启动完成");
} catch (Exception e) {
log.error("MQ 生产者启动遇到异常", e);
throw new MqException(ProducerRespCode.RPC_INIT_FAILED);
}
}</code></pre><h3>状态管理类</h3><p>这里我们引入 statusManager 管理整体的状态。</p><p>默认的如下:</p><pre><code class="java">public class StatusManager implements IStatusManager {
private boolean status;
@Override
public boolean status() {
return this.status;
}
@Override
public IStatusManager status(boolean status) {
this.status = status;
return this;
}
}</code></pre><p>就是对一个是否可用的状态进行维护,然后在 channel 获取等地方便于判断当前服务的状态。</p><h3>钩子函数</h3><p>DefaultShutdownHook 实现如下:</p><pre><code class="java">public class DefaultShutdownHook extends AbstractShutdownHook {
/**
* 调用管理类
* @since 0.0.5
*/
private IInvokeService invokeService;
/**
* 销毁管理类
* @since 0.0.5
*/
private Destroyable destroyable;
/**
* 状态管理类
* @since 0.0.5
*/
private IStatusManager statusManager;
/**
* 为剩余的请求等待时间
* @since 0.0.5
*/
private long waitMillsForRemainRequest = 60 * 1000;
//get & set
/**
* (1)设置 status 状态为等待关闭
* (2)查看是否 {@link IInvokeService#remainsRequest()} 是否包含请求
* (3)超时检测-可以不添加,如果难以关闭成功,直接强制关闭即可。
* (4)关闭所有线程池资源信息
* (5)设置状态为成功关闭
*/
@Override
protected void doHook() {
statusManager.status(false);
// 设置状态为等待关闭
logger.info("[Shutdown] set status to wait for shutdown.");
// 循环等待当前执行的请求执行完成
long startMills = System.currentTimeMillis();
while (invokeService.remainsRequest()) {
long currentMills = System.currentTimeMillis();
long costMills = currentMills - startMills;
if(costMills >= waitMillsForRemainRequest) {
logger.warn("[Shutdown] still remains request, but timeout, break.");
break;
}
logger.debug("[Shutdown] still remains request, wait for a while.");
DateUtil.sleep(10);
}
// 销毁
destroyable.destroyAll();
// 设置状态为关闭成功
statusManager.status(false);
logger.info("[Shutdown] set status to shutdown success.");
}
}</code></pre><p>(1)进行关闭前,首先判断通过 <code>invokeService.remainsRequest()</code> 判断是否有未处理完的消息,有则进行等待。</p><p>(2)当然,我们还需要考虑网络消息丢失的场景,不可能一直等待。</p><p>所以引入了超时中断,最大等待时间也是可以自行定义的。</p><pre><code class="java">if(costMills >= waitMillsForRemainRequest) {
logger.warn("[Shutdown] still remains request, but timeout, break.");
break;
}</code></pre><p>(3)关闭之后</p><p>将 status 设置为 false,标识当前服务不可用。</p><h2>小结</h2><p>随着 rpc 技术的成熟,优雅关闭已经成为一个很基本的功能点。</p><p>一个小小的改动,可以节约生产发布时间,早点下班陪陪家人。</p><p>希望本文对你有所帮助,如果喜欢,欢迎点赞收藏转发一波。</p><p>我是老马,期待与你的下次重逢。</p><h2>开源地址</h2><blockquote><a href="https://link.segmentfault.com/?enc=PWTfPUjnmGPYGJr%2FTEzJnw%3D%3D.0bJ%2FhZ%2FeDPojrv0QdQJV3x6ayXI1bInKAuFH1qv1BTQ%3D" rel="nofollow">The message queue in java.(java 简易版本 mq 实现) </a> <a href="https://link.segmentfault.com/?enc=0%2BHleE%2FkAq4YX9JEcVad3w%3D%3D.0IAw7TIKLacUlmRkeHY7gpCu%2BA%2FNrolBDLpdKoul4rw%3D" rel="nofollow">https://github.com/houbb/mq</a></blockquote><h2>拓展阅读</h2><p><a href="https://link.segmentfault.com/?enc=4n8U%2FsxZi9yKsq9fb86r%2FA%3D%3D.3N3t9399gFkR4kxxZGbHzpt4rbNcpRowMDuNzg6tTW8%3D" rel="nofollow">rpc-从零开始实现 rpc</a> <a href="https://link.segmentfault.com/?enc=a2xe4KavNymXr2HPicH%2BVA%3D%3D.sAAIemAMiCBLT9C3SamDmIO8dLLm7S0rtCEfXVJeBk0%3D" rel="nofollow">https://github.com/houbb/rpc</a></p>
【mq】从零开始实现 mq-04-启动检测与实现优化
https://segmentfault.com/a/1190000041786673
2022-05-03T14:18:37+08:00
2022-05-03T14:18:37+08:00
老马啸西风
https://segmentfault.com/u/houbinbin
0
<h2>前景回顾</h2><p><a href="https://link.segmentfault.com/?enc=Aj8YT0y0VeEyhM0PQwVAlw%3D%3D.%2BdbGTOrIvJNxsOVPY%2F%2F%2F%2Fpk4rYBx8K%2BStUlrrdrxeI62Hz5k9etvYKXXRaDTlns%2FbomGRAIxmkBd9gdm9TeAcA%3D%3D" rel="nofollow">【mq】从零开始实现 mq-01-生产者、消费者启动 </a></p><p><a href="https://link.segmentfault.com/?enc=sL7sbIow9iFAAa49IFySwg%3D%3D.gRwrpUXLsT5hOTTJnl4MVc1e0k%2FAxWQf%2B9cptq5onkDtTaMAdgbXMbnKkFSqUvuYy9sWQO%2BqLa3gkqDT9tTdJg%3D%3D" rel="nofollow">【mq】从零开始实现 mq-02-如何实现生产者调用消费者?</a></p><p><a href="https://link.segmentfault.com/?enc=gEV9Mkme4ozB6J8bUorwTA%3D%3D.bAD0O5mYAVpQ98nLSBMAKrrzZoO%2BLF6ietpigVlOX%2FiadxvXMLG7VLH9mJKkhaDnYtO1ie7GKf30Ltmxfu%2Bx0Q%3D%3D" rel="nofollow">【mq】从零开始实现 mq-03-引入 broker 中间人</a></p><p><a href="https://link.segmentfault.com/?enc=ALcSypI%2BEhKDLxnEshvBWQ%3D%3D.621USIhDqI7YLErI3PPkhV7ttk6NpuVbVhIvW5PxmsuHezXtFv7cEsv5%2F0cU%2FBM5QmbPYBr1aTj6Op%2F%2FUMkNgw%3D%3D" rel="nofollow">【mq】从零开始实现 mq-04-启动检测与实现优化</a></p><p>上一节我们引入了中间人 broker,让消息的生产者和消费者解耦。</p><p>这一节我们对初始化代码进行优化,便于后期拓展维护。</p><p><img src="/img/remote/1460000041786675" alt="启动检测" title="启动检测"></p><h2>生产者启动优化</h2><h3>启动实现</h3><p>整体实现调整如下:</p><pre><code class="java">@Override
public synchronized void run() {
this.paramCheck();
// 启动服务端
log.info("MQ 生产者开始启动客户端 GROUP: {}, PORT: {}, brokerAddress: {}",
groupName, port, brokerAddress);
try {
//channel future
this.channelFutureList = ChannelFutureUtils.initChannelFutureList(brokerAddress,
initChannelHandler(), check);
// register to broker
this.registerToBroker();
// 标识为可用
enableFlag = true;
log.info("MQ 生产者启动完成");
} catch (Exception e) {
log.error("MQ 生产者启动遇到异常", e);
throw new MqException(ProducerRespCode.RPC_INIT_FAILED);
}
}</code></pre><p>看起来是不是比起原来清爽很多呢?</p><p>但是<strong>复杂性只会转移,不会消失</strong>。</p><p>答案就是封装到 initChannelFutureList 中去了。</p><h3>initChannelFutureList</h3><p>因为这里是生产者、消费者都会用到。</p><p>所以我们先放在统一的工具类中,实现本身和以前大同小异。</p><pre><code class="java">/**
* 初始化列表
* @param brokerAddress 地址
* @param channelHandler 处理类
* @param check 是否检测可用性
* @return 结果
* @since 0.0.4
*/
public static List<RpcChannelFuture> initChannelFutureList(final String brokerAddress,
final ChannelHandler channelHandler,
final boolean check) {
List<RpcAddress> addressList = InnerAddressUtils.initAddressList(brokerAddress);
List<RpcChannelFuture> list = new ArrayList<>();
for(RpcAddress rpcAddress : addressList) {
try {
final String address = rpcAddress.getAddress();
final int port = rpcAddress.getPort();
EventLoopGroup workerGroup = new NioEventLoopGroup();
Bootstrap bootstrap = new Bootstrap();
ChannelFuture channelFuture = bootstrap.group(workerGroup)
.channel(NioSocketChannel.class)
.option(ChannelOption.SO_KEEPALIVE, true)
.handler(new ChannelInitializer<Channel>(){
@Override
protected void initChannel(Channel ch) throws Exception {
ch.pipeline()
.addLast(new LoggingHandler(LogLevel.INFO))
.addLast(channelHandler);
}
})
.connect(address, port)
.syncUninterruptibly();
log.info("启动客户端完成,监听 address: {}, port:{}", address, port);
RpcChannelFuture rpcChannelFuture = new RpcChannelFuture();
rpcChannelFuture.setChannelFuture(channelFuture);
rpcChannelFuture.setAddress(address);
rpcChannelFuture.setPort(port);
rpcChannelFuture.setWeight(rpcAddress.getWeight());
list.add(rpcChannelFuture);
} catch (Exception exception) {
log.error("注册到 broker 服务端异常", exception);
if(check) {
throw new MqException(MqCommonRespCode.REGISTER_TO_BROKER_FAILED);
}
}
}
if(check
&& CollectionUtil.isEmpty(list)) {
log.error("check=true 且可用列表为空,启动失败。");
throw new MqException(MqCommonRespCode.REGISTER_TO_BROKER_FAILED);
}
return list;
}</code></pre><p>这里的 check 为了避免 2 种情况:</p><p>(1)某一个 broker 不可用</p><p>(2)没有可用的 broker 信息。</p><h2>消费者启动优化</h2><p>消费者连接 broker 和生产者是类似的。</p><p>这里只是放一下实现,不做更多的赘述。</p><pre><code class="java">@Override
public void run() {
// 启动服务端
log.info("MQ 消费者开始启动服务端 groupName: {}, brokerAddress: {}",
groupName, brokerAddress);
//1. 参数校验
this.paramCheck();
try {
//channel future
this.channelFutureList = ChannelFutureUtils.initChannelFutureList(brokerAddress,
initChannelHandler(),
check);
// register to broker
this.registerToBroker();
// 标识为可用
enableFlag = true;
log.info("MQ 消费者启动完成");
} catch (Exception e) {
log.error("MQ 消费者启动异常", e);
throw new MqException(ConsumerRespCode.RPC_INIT_FAILED);
}
}</code></pre><h2>小结</h2><p>这一小节的内容特别简单,对初始化部分做了优化,便于后期维护拓展。</p><p>希望本文对你有所帮助,如果喜欢,欢迎点赞收藏转发一波。</p><p>我是老马,期待与你的下次重逢。</p><h2>开源地址</h2><blockquote><a href="https://link.segmentfault.com/?enc=GTGLcU0QqYmpOAwNBpmQaA%3D%3D.zU62xmMPh%2BWMtwWgO6YGhUr8TzWdY4%2FYjCQ%2B%2FZYTMBo%3D" rel="nofollow">The message queue in java.(java 简易版本 mq 实现) </a> <a href="https://link.segmentfault.com/?enc=7HDIvU9F01YjYC38TnQlJg%3D%3D.OtJESdeg7HPU8EnXRc2Pdhhyk%2FbwZsPm%2BUvVFwaK3i4%3D" rel="nofollow">https://github.com/houbb/mq</a></blockquote><h2>拓展阅读</h2><p><a href="https://link.segmentfault.com/?enc=u1RG9Na0nnSU6cTB0cn6dA%3D%3D.aRCOipYUj%2FeCPUXWmLMT%2FXZOhmpbUDM%2B1Dqp4DUQsXo%3D" rel="nofollow">rpc-从零开始实现 rpc</a> <a href="https://link.segmentfault.com/?enc=9U3t%2BgxM%2Fn1%2Fe2uLqGwzoA%3D%3D.kyEdd56ydmaXEhmmVTWOZFPgGL3uGvfk%2FTPwoDFsFyk%3D" rel="nofollow">https://github.com/houbb/rpc</a></p>
【mq】从零开始实现 mq-03-引入 broker 中间人
https://segmentfault.com/a/1190000041781079
2022-04-30T17:52:35+08:00
2022-04-30T17:52:35+08:00
老马啸西风
https://segmentfault.com/u/houbinbin
0
<h2>前景回顾</h2><p><a href="https://link.segmentfault.com/?enc=l5rv0%2FITkR2OT8BgTT4zqw%3D%3D.i2ZAoJvPeEtfPB62ccoTIyg324l%2FIIn4R%2B6baseMe7FfhVvS%2FnS8kB%2FftjcjwI4HF72aUjKZ%2B%2B2NkemrRcJb3A%3D%3D" rel="nofollow">【mq】从零开始实现 mq-01-生产者、消费者启动 </a></p><p><a href="https://link.segmentfault.com/?enc=BD%2BQ1fHh%2BdR5e3rzWMDpeQ%3D%3D.4hHVnYVYWtUA0i%2BCMjAKlLaMpBYWorBFZ5P4JFn63Zaqh1eTNUlPUNeslxbH9wXsGyLIxObCgc0za326GQM6%2BQ%3D%3D" rel="nofollow">【mq】从零开始实现 mq-02-如何实现生产者调用消费者?</a></p><p><a href="https://link.segmentfault.com/?enc=gjdEJJ1Yr1VhOhev5RN2hw%3D%3D.TXoQKRUO1gKJznskzeInfSlh9lH8J8WHWgUBLwvsPCyr42GNSMtszkbicgBulkjmLF%2B46XnVSG5AXrg2UUJvFw%3D%3D" rel="nofollow">【mq】从零开始实现 mq-03-引入 broker 中间人</a></p><p>上一节我们学习了如何实现生产者给消费者发送消息,但是是通过直连的方式。</p><p>那么如何才能达到解耦的效果呢?</p><p>答案就是引入 broker,消息的中间人。</p><p><img src="/img/remote/1460000041781081" alt="broker" title="broker"></p><h2>MqBroker 实现</h2><h3>核心启动类</h3><p>类似我们前面 consumer 的启动实现:</p><pre><code class="java">package com.github.houbb.mq.broker.core;
/**
* @author binbin.hou
* @since 1.0.0
*/
public class MqBroker extends Thread implements IMqBroker {
// 省略
private ChannelHandler initChannelHandler() {
MqBrokerHandler handler = new MqBrokerHandler();
handler.setInvokeService(invokeService);
handler.setRegisterConsumerService(registerConsumerService);
handler.setRegisterProducerService(registerProducerService);
handler.setMqBrokerPersist(mqBrokerPersist);
handler.setBrokerPushService(brokerPushService);
handler.setRespTimeoutMills(respTimeoutMills);
return handler;
}
@Override
public void run() {
// 启动服务端
log.info("MQ 中间人开始启动服务端 port: {}", port);
EventLoopGroup bossGroup = new NioEventLoopGroup();
EventLoopGroup workerGroup = new NioEventLoopGroup();
try {
final ByteBuf delimiterBuf = DelimiterUtil.getByteBuf(DelimiterUtil.DELIMITER);
ServerBootstrap serverBootstrap = new ServerBootstrap();
serverBootstrap.group(workerGroup, bossGroup)
.channel(NioServerSocketChannel.class)
.childHandler(new ChannelInitializer<Channel>() {
@Override
protected void initChannel(Channel ch) throws Exception {
ch.pipeline()
.addLast(new DelimiterBasedFrameDecoder(DelimiterUtil.LENGTH, delimiterBuf))
.addLast(initChannelHandler());
}
})
// 这个参数影响的是还没有被accept 取出的连接
.option(ChannelOption.SO_BACKLOG, 128)
// 这个参数只是过一段时间内客户端没有响应,服务端会发送一个 ack 包,以判断客户端是否还活着。
.childOption(ChannelOption.SO_KEEPALIVE, true);
// 绑定端口,开始接收进来的链接
ChannelFuture channelFuture = serverBootstrap.bind(port).syncUninterruptibly();
log.info("MQ 中间人启动完成,监听【" + port + "】端口");
channelFuture.channel().closeFuture().syncUninterruptibly();
log.info("MQ 中间人关闭完成");
} catch (Exception e) {
log.error("MQ 中间人启动异常", e);
throw new MqException(BrokerRespCode.RPC_INIT_FAILED);
} finally {
workerGroup.shutdownGracefully();
bossGroup.shutdownGracefully();
}
}
}</code></pre><p><code>initChannelHandler</code> 中有不少新面孔,我们后面会详细介绍。</p><h3>MqBrokerHandler 处理逻辑</h3><pre><code class="java">package com.github.houbb.mq.broker.handler;
import java.util.List;
/**
* @author binbin.hou
* @since 1.0.0
*/
public class MqBrokerHandler extends SimpleChannelInboundHandler {
private static final Log log = LogFactory.getLog(MqBrokerHandler.class);
/**
* 调用管理类
* @since 1.0.0
*/
private IInvokeService invokeService;
/**
* 消费者管理
* @since 0.0.3
*/
private IBrokerConsumerService registerConsumerService;
/**
* 生产者管理
* @since 0.0.3
*/
private IBrokerProducerService registerProducerService;
/**
* 持久化类
* @since 0.0.3
*/
private IMqBrokerPersist mqBrokerPersist;
/**
* 推送服务
* @since 0.0.3
*/
private IBrokerPushService brokerPushService;
/**
* 获取响应超时时间
* @since 0.0.3
*/
private long respTimeoutMills;
//set 方法
@Override
protected void channelRead0(ChannelHandlerContext ctx, Object msg) throws Exception {
ByteBuf byteBuf = (ByteBuf) msg;
byte[] bytes = new byte[byteBuf.readableBytes()];
byteBuf.readBytes(bytes);
RpcMessageDto rpcMessageDto = null;
try {
rpcMessageDto = JSON.parseObject(bytes, RpcMessageDto.class);
} catch (Exception exception) {
log.error("RpcMessageDto json 格式转换异常 {}", new String(bytes));
return;
}
if (rpcMessageDto.isRequest()) {
MqCommonResp commonResp = this.dispatch(rpcMessageDto, ctx);
if(commonResp == null) {
log.debug("当前消息为 null,忽略处理。");
return;
}
// 写回响应,和以前类似。
writeResponse(rpcMessageDto, commonResp, ctx);
} else {
final String traceId = rpcMessageDto.getTraceId();
// 丢弃掉 traceId 为空的信息
if(StringUtil.isBlank(traceId)) {
log.debug("[Server Response] response traceId 为空,直接丢弃", JSON.toJSON(rpcMessageDto));
return;
}
// 添加消息
invokeService.addResponse(traceId, rpcMessageDto);
}
}
/**
* 异步处理消息
* @param mqMessage 消息
* @since 0.0.3
*/
private void asyncHandleMessage(MqMessage mqMessage) {
List<Channel> channelList = registerConsumerService.getSubscribeList(mqMessage);
if(CollectionUtil.isEmpty(channelList)) {
log.info("监听列表为空,忽略处理");
return;
}
BrokerPushContext brokerPushContext = new BrokerPushContext();
brokerPushContext.setChannelList(channelList);
brokerPushContext.setMqMessage(mqMessage);
brokerPushContext.setMqBrokerPersist(mqBrokerPersist);
brokerPushContext.setInvokeService(invokeService);
brokerPushContext.setRespTimeoutMills(respTimeoutMills);
brokerPushService.asyncPush(brokerPushContext);
}
}</code></pre><h3>消息分发</h3><p>broker 接收到消息以后,dispatch 实现如下:</p><pre><code class="java">/**
* 消息的分发
*
* @param rpcMessageDto 入参
* @param ctx 上下文
* @return 结果
*/
private MqCommonResp dispatch(RpcMessageDto rpcMessageDto, ChannelHandlerContext ctx) {
try {
final String methodType = rpcMessageDto.getMethodType();
final String json = rpcMessageDto.getJson();
String channelId = ChannelUtil.getChannelId(ctx);
final Channel channel = ctx.channel();
log.debug("channelId: {} 接收到 method: {} 内容:{}", channelId,
methodType, json);
// 生产者注册
if(MethodType.P_REGISTER.equals(methodType)) {
BrokerRegisterReq registerReq = JSON.parseObject(json, BrokerRegisterReq.class);
return registerProducerService.register(registerReq.getServiceEntry(), channel);
}
// 生产者注销
if(MethodType.P_UN_REGISTER.equals(methodType)) {
BrokerRegisterReq registerReq = JSON.parseObject(json, BrokerRegisterReq.class);
return registerProducerService.unRegister(registerReq.getServiceEntry(), channel);
}
// 生产者消息发送
if(MethodType.P_SEND_MSG.equals(methodType)) {
MqMessage mqMessage = JSON.parseObject(json, MqMessage.class);
MqMessagePersistPut persistPut = new MqMessagePersistPut();
persistPut.setMqMessage(mqMessage);
persistPut.setMessageStatus(MessageStatusConst.WAIT_CONSUMER);
MqCommonResp commonResp = mqBrokerPersist.put(persistPut);
this.asyncHandleMessage(mqMessage);
return commonResp;
}
// 生产者消息发送-ONE WAY
if(MethodType.P_SEND_MSG_ONE_WAY.equals(methodType)) {
MqMessage mqMessage = JSON.parseObject(json, MqMessage.class);
MqMessagePersistPut persistPut = new MqMessagePersistPut();
persistPut.setMqMessage(mqMessage);
persistPut.setMessageStatus(MessageStatusConst.WAIT_CONSUMER);
mqBrokerPersist.put(persistPut);
this.asyncHandleMessage(mqMessage);
return null;
}
// 消费者注册
if(MethodType.C_REGISTER.equals(methodType)) {
BrokerRegisterReq registerReq = JSON.parseObject(json, BrokerRegisterReq.class);
return registerConsumerService.register(registerReq.getServiceEntry(), channel);
}
// 消费者注销
if(MethodType.C_UN_REGISTER.equals(methodType)) {
BrokerRegisterReq registerReq = JSON.parseObject(json, BrokerRegisterReq.class);
return registerConsumerService.unRegister(registerReq.getServiceEntry(), channel);
}
// 消费者监听注册
if(MethodType.C_SUBSCRIBE.equals(methodType)) {
ConsumerSubscribeReq req = JSON.parseObject(json, ConsumerSubscribeReq.class);
return registerConsumerService.subscribe(req, channel);
}
// 消费者监听注销
if(MethodType.C_UN_SUBSCRIBE.equals(methodType)) {
ConsumerUnSubscribeReq req = JSON.parseObject(json, ConsumerUnSubscribeReq.class);
return registerConsumerService.unSubscribe(req, channel);
}
// 消费者主动 pull
if(MethodType.C_MESSAGE_PULL.equals(methodType)) {
MqConsumerPullReq req = JSON.parseObject(json, MqConsumerPullReq.class);
return mqBrokerPersist.pull(req, channel);
}
throw new UnsupportedOperationException("暂不支持的方法类型");
} catch (Exception exception) {
log.error("执行异常", exception);
MqCommonResp resp = new MqCommonResp();
resp.setRespCode(MqCommonRespCode.FAIL.getCode());
resp.setRespMessage(MqCommonRespCode.FAIL.getMsg());
return resp;
}
}</code></pre><h4>消息推送</h4><p><code>this.asyncHandleMessage(mqMessage);</code> 是 broker 接收到消息之后的处理逻辑。</p><pre><code class="java">/**
* 异步处理消息
* @param mqMessage 消息
* @since 0.0.3
*/
private void asyncHandleMessage(MqMessage mqMessage) {
List<Channel> channelList = registerConsumerService.getSubscribeList(mqMessage);
if(CollectionUtil.isEmpty(channelList)) {
log.info("监听列表为空,忽略处理");
return;
}
BrokerPushContext brokerPushContext = new BrokerPushContext();
brokerPushContext.setChannelList(channelList);
brokerPushContext.setMqMessage(mqMessage);
brokerPushContext.setMqBrokerPersist(mqBrokerPersist);
brokerPushContext.setInvokeService(invokeService);
brokerPushContext.setRespTimeoutMills(respTimeoutMills);
brokerPushService.asyncPush(brokerPushContext);
}</code></pre><p>推送的核心实现如下:</p><pre><code class="java">package com.github.houbb.mq.broker.support.push;
/**
* @author binbin.hou
* @since 0.0.3
*/
public class BrokerPushService implements IBrokerPushService {
private static final Log log = LogFactory.getLog(BrokerPushService.class);
private static final ExecutorService EXECUTOR_SERVICE = Executors.newSingleThreadExecutor();
@Override
public void asyncPush(final BrokerPushContext context) {
EXECUTOR_SERVICE.submit(new Runnable() {
@Override
public void run() {
log.info("开始异步处理 {}", JSON.toJSON(context));
final List<Channel> channelList = context.getChannelList();
final IMqBrokerPersist mqBrokerPersist = context.getMqBrokerPersist();
final MqMessage mqMessage = context.getMqMessage();
final String messageId = mqMessage.getTraceId();
final IInvokeService invokeService = context.getInvokeService();
final long responseTime = context.getRespTimeoutMills();
for(Channel channel : channelList) {
try {
String channelId = ChannelUtil.getChannelId(channel);
log.info("开始处理 channelId: {}", channelId);
//1. 调用
mqMessage.setMethodType(MethodType.B_MESSAGE_PUSH);
MqConsumerResultResp resultResp = callServer(channel, mqMessage,
MqConsumerResultResp.class, invokeService, responseTime);
//2. 更新状态
mqBrokerPersist.updateStatus(messageId, resultResp.getConsumerStatus());
//3. 后期添加重试策略
log.info("完成处理 channelId: {}", channelId);
} catch (Exception exception) {
log.error("处理异常");
mqBrokerPersist.updateStatus(messageId, ConsumerStatus.FAILED.getCode());
}
}
log.info("完成异步处理");
}
});
}
}</code></pre><p>此处在消息推送之后,需要更新消息的 ACK 状态。</p><h4>消息生产者处理类</h4><p><code>IBrokerProducerService</code> 接口定义如下:</p><pre><code class="java">package com.github.houbb.mq.broker.api;
/**
* <p> 生产者注册服务类 </p>
*
* @author houbinbin
* @since 0.0.3
*/
public interface IBrokerProducerService {
/**
* 注册当前服务信息
* (1)将该服务通过 {@link ServiceEntry#getGroupName()} 进行分组
* 订阅了这个 serviceId 的所有客户端
* @param serviceEntry 注册当前服务信息
* @param channel channel
* @since 0.0.8
*/
MqCommonResp register(final ServiceEntry serviceEntry, Channel channel);
/**
* 注销当前服务信息
* @param serviceEntry 注册当前服务信息
* @param channel 通道
* @since 0.0.8
*/
MqCommonResp unRegister(final ServiceEntry serviceEntry, Channel channel);
/**
* 获取服务地址信息
* @param channel channel
* @return 结果
* @since 0.0.3
*/
ServiceEntry getServiceEntry(final Channel channel);
}</code></pre><p>实现如下:</p><p>本地基于 map 存储请求过来的基本信息。</p><pre><code class="java">package com.github.houbb.mq.broker.support.api;
/**
* <p> 生产者注册服务类 </p>
*
* @author houbinbin
* @since 0.0.3
*/
public class LocalBrokerProducerService implements IBrokerProducerService {
private static final Log log = LogFactory.getLog(LocalBrokerProducerService.class);
private final Map<String, BrokerServiceEntryChannel> registerMap = new ConcurrentHashMap<>();
@Override
public MqCommonResp register(ServiceEntry serviceEntry, Channel channel) {
final String channelId = ChannelUtil.getChannelId(channel);
BrokerServiceEntryChannel entryChannel = InnerChannelUtils.buildEntryChannel(serviceEntry, channel);
registerMap.put(channelId, entryChannel);
MqCommonResp resp = new MqCommonResp();
resp.setRespCode(MqCommonRespCode.SUCCESS.getCode());
resp.setRespMessage(MqCommonRespCode.SUCCESS.getMsg());
return resp;
}
@Override
public MqCommonResp unRegister(ServiceEntry serviceEntry, Channel channel) {
final String channelId = ChannelUtil.getChannelId(channel);
registerMap.remove(channelId);
MqCommonResp resp = new MqCommonResp();
resp.setRespCode(MqCommonRespCode.SUCCESS.getCode());
resp.setRespMessage(MqCommonRespCode.SUCCESS.getMsg());
return resp;
}
@Override
public ServiceEntry getServiceEntry(Channel channel) {
final String channelId = ChannelUtil.getChannelId(channel);
return registerMap.get(channelId);
}
}</code></pre><h4>消息消费者处理类</h4><p>接口定义如下:</p><pre><code class="java">package com.github.houbb.mq.broker.api;
/**
* <p> 消费者注册服务类 </p>
*
* @author houbinbin
* @since 0.0.3
*/
public interface IBrokerConsumerService {
/**
* 注册当前服务信息
* (1)将该服务通过 {@link ServiceEntry#getGroupName()} 进行分组
* 订阅了这个 serviceId 的所有客户端
* @param serviceEntry 注册当前服务信息
* @param channel channel
* @since 0.0.3
*/
MqCommonResp register(final ServiceEntry serviceEntry, Channel channel);
/**
* 注销当前服务信息
* @param serviceEntry 注册当前服务信息
* @param channel channel
* @since 0.0.3
*/
MqCommonResp unRegister(final ServiceEntry serviceEntry, Channel channel);
/**
* 监听服务信息
* (1)监听之后,如果有任何相关的机器信息发生变化,则进行推送。
* (2)内置的信息,需要传送 ip 信息到注册中心。
*
* @param serviceEntry 客户端明细信息
* @param clientChannel 客户端 channel 信息
* @since 0.0.3
*/
MqCommonResp subscribe(final ConsumerSubscribeReq serviceEntry,
final Channel clientChannel);
/**
* 取消监听服务信息
* (1)监听之后,如果有任何相关的机器信息发生变化,则进行推送。
* (2)内置的信息,需要传送 ip 信息到注册中心。
*
* @param serviceEntry 客户端明细信息
* @param clientChannel 客户端 channel 信息
* @since 0.0.3
*/
MqCommonResp unSubscribe(final ConsumerUnSubscribeReq serviceEntry,
final Channel clientChannel);
/**
* 获取所有匹配的消费者
* 1. 同一个 groupName 只返回一个,注意负载均衡
* 2. 返回匹配当前消息的消费者通道
*
* @param mqMessage 消息体
* @return 结果
*/
List<Channel> getSubscribeList(MqMessage mqMessage);
}</code></pre><p>默认实现:</p><pre><code class="java">package com.github.houbb.mq.broker.support.api;
/**
* @author binbin.hou
* @since 1.0.0
*/
public class LocalBrokerConsumerService implements IBrokerConsumerService {
private final Map<String, BrokerServiceEntryChannel> registerMap = new ConcurrentHashMap<>();
/**
* 订阅集合
* key: topicName
* value: 对应的订阅列表
*/
private final Map<String, Set<ConsumerSubscribeBo>> subscribeMap = new ConcurrentHashMap<>();
@Override
public MqCommonResp register(ServiceEntry serviceEntry, Channel channel) {
final String channelId = ChannelUtil.getChannelId(channel);
BrokerServiceEntryChannel entryChannel = InnerChannelUtils.buildEntryChannel(serviceEntry, channel);
registerMap.put(channelId, entryChannel);
MqCommonResp resp = new MqCommonResp();
resp.setRespCode(MqCommonRespCode.SUCCESS.getCode());
resp.setRespMessage(MqCommonRespCode.SUCCESS.getMsg());
return resp;
}
@Override
public MqCommonResp unRegister(ServiceEntry serviceEntry, Channel channel) {
final String channelId = ChannelUtil.getChannelId(channel);
registerMap.remove(channelId);
MqCommonResp resp = new MqCommonResp();
resp.setRespCode(MqCommonRespCode.SUCCESS.getCode());
resp.setRespMessage(MqCommonRespCode.SUCCESS.getMsg());
return resp;
}
@Override
public MqCommonResp subscribe(ConsumerSubscribeReq serviceEntry, Channel clientChannel) {
final String channelId = ChannelUtil.getChannelId(clientChannel);
final String topicName = serviceEntry.getTopicName();
Set<ConsumerSubscribeBo> set = subscribeMap.get(topicName);
if(set == null) {
set = new HashSet<>();
}
ConsumerSubscribeBo subscribeBo = new ConsumerSubscribeBo();
subscribeBo.setChannelId(channelId);
subscribeBo.setGroupName(serviceEntry.getGroupName());
subscribeBo.setTopicName(topicName);
subscribeBo.setTagRegex(serviceEntry.getTagRegex());
set.add(subscribeBo);
subscribeMap.put(topicName, set);
MqCommonResp resp = new MqCommonResp();
resp.setRespCode(MqCommonRespCode.SUCCESS.getCode());
resp.setRespMessage(MqCommonRespCode.SUCCESS.getMsg());
return resp;
}
@Override
public MqCommonResp unSubscribe(ConsumerUnSubscribeReq serviceEntry, Channel clientChannel) {
final String channelId = ChannelUtil.getChannelId(clientChannel);
final String topicName = serviceEntry.getTopicName();
ConsumerSubscribeBo subscribeBo = new ConsumerSubscribeBo();
subscribeBo.setChannelId(channelId);
subscribeBo.setGroupName(serviceEntry.getGroupName());
subscribeBo.setTopicName(topicName);
subscribeBo.setTagRegex(serviceEntry.getTagRegex());
// 集合
Set<ConsumerSubscribeBo> set = subscribeMap.get(topicName);
if(CollectionUtil.isNotEmpty(set)) {
set.remove(subscribeBo);
}
MqCommonResp resp = new MqCommonResp();
resp.setRespCode(MqCommonRespCode.SUCCESS.getCode());
resp.setRespMessage(MqCommonRespCode.SUCCESS.getMsg());
return resp;
}
@Override
public List<Channel> getSubscribeList(MqMessage mqMessage) {
final String topicName = mqMessage.getTopic();
Set<ConsumerSubscribeBo> set = subscribeMap.get(topicName);
if(CollectionUtil.isEmpty(set)) {
return Collections.emptyList();
}
//2. 获取匹配的 tag 列表
final List<String> tagNameList = mqMessage.getTags();
Map<String, List<ConsumerSubscribeBo>> groupMap = new HashMap<>();
for(ConsumerSubscribeBo bo : set) {
String tagRegex = bo.getTagRegex();
if(hasMatch(tagNameList, tagRegex)) {
//TODO: 这种设置模式,统一添加处理
String groupName = bo.getGroupName();
List<ConsumerSubscribeBo> list = groupMap.get(groupName);
if(list == null) {
list = new ArrayList<>();
}
list.add(bo);
groupMap.put(groupName, list);
}
}
//3. 按照 groupName 分组之后,每一组只随机返回一个。最好应该调整为以 shardingkey 选择
final String shardingKey = mqMessage.getShardingKey();
List<Channel> channelList = new ArrayList<>();
for(Map.Entry<String, List<ConsumerSubscribeBo>> entry : groupMap.entrySet()) {
List<ConsumerSubscribeBo> list = entry.getValue();
ConsumerSubscribeBo bo = RandomUtils.random(list, shardingKey);
BrokerServiceEntryChannel entryChannel = registerMap.get(bo.getChannelId());
channelList.add(entryChannel.getChannel());
}
return channelList;
}
private boolean hasMatch(List<String> tagNameList,
String tagRegex) {
if(CollectionUtil.isEmpty(tagNameList)) {
return false;
}
Pattern pattern = Pattern.compile(tagRegex);
for(String tagName : tagNameList) {
if(RegexUtils.match(pattern, tagName)) {
return true;
}
}
return false;
}
}</code></pre><p><code>getSubscribeList</code> 的逻辑可能稍微复杂点,其实就是消息过来,找到匹配的订阅消费者而已。</p><p>因为同一个 groupName 的消费者消息只消费一次,所以需要一次分组。</p><h4>消息持久化</h4><p>接口如下:</p><pre><code class="java">package com.github.houbb.mq.broker.support.persist;
/**
* @author binbin.hou
* @since 0.0.3
*/
public interface IMqBrokerPersist {
/**
* 保存消息
* @param mqMessage 消息
* @since 0.0.3
*/
MqCommonResp put(final MqMessagePersistPut mqMessage);
/**
* 更新状态
* @param messageId 消息唯一标识
* @param status 状态
* @return 结果
* @since 0.0.3
*/
MqCommonResp updateStatus(final String messageId,
final String status);
/**
* 拉取消息
* @param pull 拉取消息
* @return 结果
*/
MqConsumerPullResp pull(final MqConsumerPullReq pull, final Channel channel);
}</code></pre><p>本地默认实现:</p><pre><code class="java">package com.github.houbb.mq.broker.support.persist;
/**
* 本地持久化策略
* @author binbin.hou
* @since 1.0.0
*/
public class LocalMqBrokerPersist implements IMqBrokerPersist {
private static final Log log = LogFactory.getLog(LocalMqBrokerPersist.class);
/**
* 队列
* ps: 这里只是简化实现,暂时不考虑并发等问题。
*/
private final Map<String, List<MqMessagePersistPut>> map = new ConcurrentHashMap<>();
//1. 接收
//2. 持久化
//3. 通知消费
@Override
public synchronized MqCommonResp put(MqMessagePersistPut put) {
log.info("put elem: {}", JSON.toJSON(put));
MqMessage mqMessage = put.getMqMessage();
final String topic = mqMessage.getTopic();
List<MqMessagePersistPut> list = map.get(topic);
if(list == null) {
list = new ArrayList<>();
}
list.add(put);
map.put(topic, list);
MqCommonResp commonResp = new MqCommonResp();
commonResp.setRespCode(MqCommonRespCode.SUCCESS.getCode());
commonResp.setRespMessage(MqCommonRespCode.SUCCESS.getMsg());
return commonResp;
}
@Override
public MqCommonResp updateStatus(String messageId, String status) {
// 这里性能比较差,所以不可以用于生产。仅作为测试验证
for(List<MqMessagePersistPut> list : map.values()) {
for(MqMessagePersistPut put : list) {
MqMessage mqMessage = put.getMqMessage();
if(mqMessage.getTraceId().equals(messageId)) {
put.setMessageStatus(status);
break;
}
}
}
MqCommonResp commonResp = new MqCommonResp();
commonResp.setRespCode(MqCommonRespCode.SUCCESS.getCode());
commonResp.setRespMessage(MqCommonRespCode.SUCCESS.getMsg());
return commonResp;
}
@Override
public MqConsumerPullResp pull(MqConsumerPullReq pull, Channel channel) {
//TODO... 待实现
return null;
}
}</code></pre><p>ps: 后续将会基于 springboot+mysql 进行持久化策略实现。</p><h2>消费者启动调整</h2><p>我们将生产者、消费者的启动都进行调整,连接到 broker 中。</p><p>二者是类似的,此处以消费者为例。</p><h3>核心启动类</h3><pre><code class="java">package com.github.houbb.mq.consumer.core;
/**
* 推送消费策略
*
* @author binbin.hou
* @since 1.0.0
*/
public class MqConsumerPush extends Thread implements IMqConsumer {
// 属性&设置
@Override
public void run() {
// 启动服务端
log.info("MQ 消费者开始启动服务端 groupName: {}, brokerAddress: {}",
groupName, brokerAddress);
//1. 参数校验
this.paramCheck();
try {
// channel handler
ChannelHandler channelHandler = this.initChannelHandler();
//channel future
this.channelFutureList = ChannelFutureUtils.initChannelFutureList(brokerAddress, channelHandler);
// register to broker
this.registerToBroker();
// 标识为可用
enableFlag = true;
log.info("MQ 消费者启动完成");
} catch (Exception e) {
log.error("MQ 消费者启动异常", e);
throw new MqException(ConsumerRespCode.RPC_INIT_FAILED);
}
}
//订阅&取消订阅
@Override
public void registerListener(IMqConsumerListener listener) {
this.mqListenerService.register(listener);
}
}</code></pre><h4>初始化 handler</h4><pre><code class="java">private ChannelHandler initChannelHandler() {
final ByteBuf delimiterBuf = DelimiterUtil.getByteBuf(DelimiterUtil.DELIMITER);
final MqConsumerHandler mqConsumerHandler = new MqConsumerHandler(invokeService, mqListenerService);
// handler 实际上会被多次调用,如果不是 @Shareable,应该每次都重新创建。
ChannelHandler handler = new ChannelInitializer<Channel>() {
@Override
protected void initChannel(Channel ch) throws Exception {
ch.pipeline()
.addLast(new DelimiterBasedFrameDecoder(DelimiterUtil.LENGTH, delimiterBuf))
.addLast(mqConsumerHandler);
}
};
return handler;
}</code></pre><h4>注册到服务端</h4><pre><code class="java">/**
* 注册到所有的服务端
* @since 0.0.3
*/
private void registerToBroker() {
for(RpcChannelFuture channelFuture : this.channelFutureList) {
ServiceEntry serviceEntry = new ServiceEntry();
serviceEntry.setGroupName(groupName);
serviceEntry.setAddress(channelFuture.getAddress());
serviceEntry.setPort(channelFuture.getPort());
serviceEntry.setWeight(channelFuture.getWeight());
BrokerRegisterReq brokerRegisterReq = new BrokerRegisterReq();
brokerRegisterReq.setServiceEntry(serviceEntry);
brokerRegisterReq.setMethodType(MethodType.C_REGISTER);
brokerRegisterReq.setTraceId(IdHelper.uuid32());
log.info("[Register] 开始注册到 broker:{}", JSON.toJSON(brokerRegisterReq));
final Channel channel = channelFuture.getChannelFuture().channel();
MqCommonResp resp = callServer(channel, brokerRegisterReq, MqCommonResp.class);
log.info("[Register] 完成注册到 broker:{}", JSON.toJSON(resp));
}
}</code></pre><h4>订阅与取消订阅</h4><p>消费者对于关心的消息,实现也比较简单:</p><pre><code class="java">public void subscribe(String topicName, String tagRegex) {
ConsumerSubscribeReq req = new ConsumerSubscribeReq();
String messageId = IdHelper.uuid32();
req.setTraceId(messageId);
req.setMethodType(MethodType.C_SUBSCRIBE);
req.setTopicName(topicName);
req.setTagRegex(tagRegex);
req.setGroupName(groupName);
Channel channel = getChannel();
MqCommonResp resp = callServer(channel, req, MqCommonResp.class);
if(!MqCommonRespCode.SUCCESS.getCode().equals(resp.getRespCode())) {
throw new MqException(ConsumerRespCode.SUBSCRIBE_FAILED);
}
}</code></pre><p>取消订阅:</p><pre><code class="java">public void unSubscribe(String topicName, String tagRegex) {
ConsumerUnSubscribeReq req = new ConsumerUnSubscribeReq();
String messageId = IdHelper.uuid32();
req.setTraceId(messageId);
req.setMethodType(MethodType.C_UN_SUBSCRIBE);
req.setTopicName(topicName);
req.setTagRegex(tagRegex);
req.setGroupName(groupName);
Channel channel = getChannel();
MqCommonResp resp = callServer(channel, req, MqCommonResp.class);
if(!MqCommonRespCode.SUCCESS.getCode().equals(resp.getRespCode())) {
throw new MqException(ConsumerRespCode.UN_SUBSCRIBE_FAILED);
}
}</code></pre><h2>测试</h2><h3>broker 启动</h3><pre><code class="java">MqBroker broker = new MqBroker();
broker.start();</code></pre><p>启动日志:</p><pre><code>[DEBUG] [2022-04-21 20:36:27.158] [main] [c.g.h.l.i.c.LogFactory.setImplementation] - Logging initialized using 'class com.github.houbb.log.integration.adaptors.stdout.StdOutExImpl' adapter.
[INFO] [2022-04-21 20:36:27.186] [Thread-0] [c.g.h.m.b.c.MqBroker.run] - MQ 中间人开始启动服务端 port: 9999
[INFO] [2022-04-21 20:36:29.060] [Thread-0] [c.g.h.m.b.c.MqBroker.run] - MQ 中间人启动完成,监听【9999】端口</code></pre><h3>consumer 启动</h3><pre><code class="java">final MqConsumerPush mqConsumerPush = new MqConsumerPush();
mqConsumerPush.start();
mqConsumerPush.subscribe("TOPIC", "TAGA");
mqConsumerPush.registerListener(new IMqConsumerListener() {
@Override
public ConsumerStatus consumer(MqMessage mqMessage, IMqConsumerListenerContext context) {
System.out.println("---------- 自定义 " + JSON.toJSONString(mqMessage));
return ConsumerStatus.SUCCESS;
}
});</code></pre><p>启动日志:</p><pre><code>...
[INFO] [2022-04-21 20:37:40.985] [Thread-0] [c.g.h.m.c.c.MqConsumerPush.registerToBroker] - [Register] 完成注册到 broker:{"respMessage":"成功","respCode":"0000"}</code></pre><p>启动时会注册到 broker。</p><h3>producer 启动</h3><pre><code class="java">MqProducer mqProducer = new MqProducer();
mqProducer.start();
String message = "HELLO MQ!";
MqMessage mqMessage = new MqMessage();
mqMessage.setTopic("TOPIC");
mqMessage.setTags(Arrays.asList("TAGA", "TAGB"));
mqMessage.setPayload(message);
SendResult sendResult = mqProducer.send(mqMessage);
System.out.println(JSON.toJSON(sendResult));</code></pre><p>日志:</p><pre><code>...
[INFO] [2022-04-21 20:39:17.885] [Thread-0] [c.g.h.m.p.c.MqProducer.registerToBroker] - [Register] 完成注册到 broker:{"respMessage":"成功","respCode":"0000"}
...</code></pre><p>此时消费者消费到我们发送的消息。</p><pre><code>---------- 自定义 {"methodType":"B_MESSAGE_PUSH","payload":"HELLO MQ!","tags":["TAGA","TAGB"],"topic":"TOPIC","traceId":"2237bbfe55b842328134e6a100e36364"}</code></pre><h2>小结</h2><p>到这里,我们就实现了基于中间人的生产者与消费者通讯。</p><p>希望本文对你有所帮助,如果喜欢,欢迎点赞收藏转发一波。</p><p>我是老马,期待与你的下次重逢。</p><h2>开源地址</h2><blockquote><a href="https://link.segmentfault.com/?enc=MwXof4MREqBck3xNxYAnhg%3D%3D.drxDbq%2BgOQvmrELFTMb3jgy3F1EYqBG5b4JCbbgO64o%3D" rel="nofollow">The message queue in java.(java 简易版本 mq 实现) </a> <a href="https://link.segmentfault.com/?enc=xif51gYyuCJX%2Bt%2B6NHJCMA%3D%3D.02O%2FoDsQv9f6dwC214SRbOTGn9sFFJRQaKSimwL%2BHG0%3D" rel="nofollow">https://github.com/houbb/mq</a></blockquote><h2>拓展阅读</h2><p><a href="https://link.segmentfault.com/?enc=nta9Y%2BQ%2FlHdwJnwo%2BboGkQ%3D%3D.sG%2BGz3viK1SKzM2mLRIpyrBBHUsNBooinX31m9XzAO4%3D" rel="nofollow">rpc-从零开始实现 rpc</a> <a href="https://link.segmentfault.com/?enc=lXPTTajqywq5ktJGvs8%2BCQ%3D%3D.zAubGujfitVF35fVoSqObPcE1a81EEytOGKE4263K5M%3D" rel="nofollow">https://github.com/houbb/rpc</a></p>
【MQ】java 从零开始实现消息队列 mq-02-如何实现生产者调用消费者?
https://segmentfault.com/a/1190000041746056
2022-04-23T12:04:50+08:00
2022-04-23T12:04:50+08:00
老马啸西风
https://segmentfault.com/u/houbinbin
0
<h2>前景回顾</h2><p>上一节我们学习了如何实现基于 netty 客服端和服务端的启动。</p><p><a href="https://link.segmentfault.com/?enc=Dg6D0ctvzl3WzvGPhIxSNg%3D%3D.9sFaF1aBlb6t5Lsx4AhCekc3XJiMcCnrCiwEpgBKAmHCt8n%2FTc9uAunODIfTZIDDNOCn3RfE5DGEjrbt9MVnLw%3D%3D" rel="nofollow">【mq】从零开始实现 mq-01-生产者、消费者启动</a></p><p><a href="https://link.segmentfault.com/?enc=HU2xJOVKJxRu5Y53A2Nreg%3D%3D.mJ2sefCitK0X2D86HFUJujLs8bhXgpJZIN9zT5KdxKy3iH%2BQ4SgpRluHs8rK1%2FkUr2c3ubyZR1aZC%2FXCflHsFw%3D%3D" rel="nofollow">【mq】java 从零开始实现消息队列 mq-02-如何实现生产者调用消费者?</a></p><p>那么客户端如何调用服务端呢?</p><p>我们本节就来一起实现一下。</p><p><img src="/img/remote/1460000041746058" alt="02.png" title="02.png"></p><h2>消费者实现</h2><h3>启动类的调整</h3><pre><code class="java">ServerBootstrap serverBootstrap = new ServerBootstrap();
serverBootstrap.group(workerGroup, bossGroup)
.channel(NioServerSocketChannel.class)
.childHandler(new ChannelInitializer<Channel>() {
@Override
protected void initChannel(Channel ch) throws Exception {
ch.pipeline()
.addLast(new DelimiterBasedFrameDecoder(DelimiterUtil.LENGTH, delimiterBuf))
.addLast(new MqConsumerHandler(invokeService));
}
})
// 这个参数影响的是还没有被accept 取出的连接
.option(ChannelOption.SO_BACKLOG, 128)
// 这个参数只是过一段时间内客户端没有响应,服务端会发送一个 ack 包,以判断客户端是否还活着。
.childOption(ChannelOption.SO_KEEPALIVE, true);</code></pre><p>这里我们通过指定分隔符解决 netty 粘包问题。</p><blockquote><a href="https://link.segmentfault.com/?enc=UeZlZJniUQFQPRKNDi57Cw%3D%3D.68sPMgohF93uFnMXYRS4hNHa76sEEsvcTFz4PLZ6a5JMdNMr%2FQ%2Bl3R0S%2B21QdtwDDLKV9ZoZl4oFZSNqYRkrGDZKhrBgbLcBsll4b%2FR70Uovuq39nBOt40j3m6Dk%2Fl4d" rel="nofollow">解决 netty 粘包问题</a></blockquote><h3>MqConsumerHandler 处理类</h3><p>MqConsumerHandler 的实现如下,添加对应的业务处理逻辑。</p><pre><code class="java">package com.github.houbb.mq.consumer.handler;
/**
* @author binbin.hou
* @since 1.0.0
*/
public class MqConsumerHandler extends SimpleChannelInboundHandler {
private static final Log log = LogFactory.getLog(MqConsumerHandler.class);
/**
* 调用管理类
* @since 1.0.0
*/
private final IInvokeService invokeService;
public MqConsumerHandler(IInvokeService invokeService) {
this.invokeService = invokeService;
}
@Override
protected void channelRead0(ChannelHandlerContext ctx, Object msg) throws Exception {
ByteBuf byteBuf = (ByteBuf) msg;
byte[] bytes = new byte[byteBuf.readableBytes()];
byteBuf.readBytes(bytes);
RpcMessageDto rpcMessageDto = null;
try {
rpcMessageDto = JSON.parseObject(bytes, RpcMessageDto.class);
} catch (Exception exception) {
log.error("RpcMessageDto json 格式转换异常 {}", new String(bytes));
return;
}
if (rpcMessageDto.isRequest()) {
MqCommonResp commonResp = this.dispatch(rpcMessageDto, ctx);
if(commonResp == null) {
log.debug("当前消息为 null,忽略处理。");
return;
}
writeResponse(rpcMessageDto, commonResp, ctx);
} else {
final String traceId = rpcMessageDto.getTraceId();
// 丢弃掉 traceId 为空的信息
if(StringUtil.isBlank(traceId)) {
log.debug("[Server Response] response traceId 为空,直接丢弃", JSON.toJSON(rpcMessageDto));
return;
}
// 添加消息
invokeService.addResponse(traceId, rpcMessageDto);
}
}
}</code></pre><h3>rpc 消息体定义</h3><p>为了统一标准,我们的 rpc 消息体 <code>RpcMessageDto</code> 定义如下:</p><pre><code class="java">package com.github.houbb.mq.common.rpc;
/**
* @author binbin.hou
* @since 1.0.0
*/
public class RpcMessageDto implements Serializable {
/**
* 请求时间
*/
private long requestTime;
/**
* 请求标识
*/
private String traceId;
/**
* 方法类型
*/
private String methodType;
/**
* 是否为请求消息
*/
private boolean isRequest;
private String respCode;
private String respMsg;
private String json;
//getter&setter
}</code></pre><h4>消息分发</h4><p>对于接收到的消息体 RpcMessageDto,分发逻辑如下:</p><pre><code class="java">/**
* 消息的分发
*
* @param rpcMessageDto 入参
* @param ctx 上下文
* @return 结果
*/
private MqCommonResp dispatch(RpcMessageDto rpcMessageDto, ChannelHandlerContext ctx) {
final String methodType = rpcMessageDto.getMethodType();
final String json = rpcMessageDto.getJson();
String channelId = ChannelUtil.getChannelId(ctx);
log.debug("channelId: {} 接收到 method: {} 内容:{}", channelId,
methodType, json);
// 消息发送
if(MethodType.P_SEND_MESSAGE.equals(methodType)) {
// 日志输出
log.info("收到服务端消息: {}", json);
// 如果是 broker,应该进行处理化等操作。
MqCommonResp resp = new MqCommonResp();
resp.setRespCode(MqCommonRespCode.SUCCESS.getCode());
resp.setRespMessage(MqCommonRespCode.SUCCESS.getMsg());
return resp;
}
throw new UnsupportedOperationException("暂不支持的方法类型");
}</code></pre><p>这里对于接收到的消息,只做一个简单的日志输出,后续将添加对应的业务逻辑处理。</p><h3>结果回写</h3><p>收到请求以后,我们需要返回对应的响应。</p><p>基于 channel 的回写实现如下:</p><pre><code class="java">/**
* 结果写回
*
* @param req 请求
* @param resp 响应
* @param ctx 上下文
*/
private void writeResponse(RpcMessageDto req,
Object resp,
ChannelHandlerContext ctx) {
final String id = ctx.channel().id().asLongText();
RpcMessageDto rpcMessageDto = new RpcMessageDto();
// 响应类消息
rpcMessageDto.setRequest(false);
rpcMessageDto.setTraceId(req.getTraceId());
rpcMessageDto.setMethodType(req.getMethodType());
rpcMessageDto.setRequestTime(System.currentTimeMillis());
String json = JSON.toJSONString(resp);
rpcMessageDto.setJson(json);
// 回写到 client 端
ByteBuf byteBuf = DelimiterUtil.getMessageDelimiterBuffer(rpcMessageDto);
ctx.writeAndFlush(byteBuf);
log.debug("[Server] channel {} response {}", id, JSON.toJSON(rpcMessageDto));
}</code></pre><h2>调用管理类</h2><p>为了方便管理异步返回的请求结果,我们统一定义了 IInvokeService 类,用于管理请求与响应。</p><h3>接口</h3><pre><code class="java">package com.github.houbb.mq.common.support.invoke;
import com.github.houbb.mq.common.rpc.RpcMessageDto;
/**
* 调用服务接口
* @author binbin.hou
* @since 1.0.0
*/
public interface IInvokeService {
/**
* 添加请求信息
* @param seqId 序列号
* @param timeoutMills 超时时间
* @return this
* @since 1.0.0
*/
IInvokeService addRequest(final String seqId,
final long timeoutMills);
/**
* 放入结果
* @param seqId 唯一标识
* @param rpcResponse 响应结果
* @return this
* @since 1.0.0
*/
IInvokeService addResponse(final String seqId, final RpcMessageDto rpcResponse);
/**
* 获取标志信息对应的结果
* @param seqId 序列号
* @return 结果
* @since 1.0.0
*/
RpcMessageDto getResponse(final String seqId);
}</code></pre><h3>实现</h3><p>实现本身也不难。</p><pre><code class="java">package com.github.houbb.mq.common.support.invoke.impl;
/**
* 调用服务接口
* @author binbin.hou
* @since 1.0.0
*/
public class InvokeService implements IInvokeService {
private static final Log logger = LogFactory.getLog(InvokeService.class);
/**
* 请求序列号 map
* (1)这里后期如果要添加超时检测,可以添加对应的超时时间。
* 可以把这里调整为 map
*
* key: seqId 唯一标识一个请求
* value: 存入该请求最长的有效时间。用于定时删除和超时判断。
* @since 0.0.2
*/
private final ConcurrentHashMap<String, Long> requestMap;
/**
* 响应结果
* @since 1.0.0
*/
private final ConcurrentHashMap<String, RpcMessageDto> responseMap;
public InvokeService() {
requestMap = new ConcurrentHashMap<>();
responseMap = new ConcurrentHashMap<>();
final Runnable timeoutThread = new TimeoutCheckThread(requestMap, responseMap);
Executors.newScheduledThreadPool(1)
.scheduleAtFixedRate(timeoutThread,60, 60, TimeUnit.SECONDS);
}
@Override
public IInvokeService addRequest(String seqId, long timeoutMills) {
logger.debug("[Invoke] start add request for seqId: {}, timeoutMills: {}", seqId,
timeoutMills);
final long expireTime = System.currentTimeMillis()+timeoutMills;
requestMap.putIfAbsent(seqId, expireTime);
return this;
}
@Override
public IInvokeService addResponse(String seqId, RpcMessageDto rpcResponse) {
// 1. 判断是否有效
Long expireTime = this.requestMap.get(seqId);
// 如果为空,可能是这个结果已经超时了,被定时 job 移除之后,响应结果才过来。直接忽略
if(ObjectUtil.isNull(expireTime)) {
return this;
}
//2. 判断是否超时
if(System.currentTimeMillis() > expireTime) {
logger.debug("[Invoke] seqId:{} 信息已超时,直接返回超时结果。", seqId);
rpcResponse = RpcMessageDto.timeout();
}
// 这里放入之前,可以添加判断。
// 如果 seqId 必须处理请求集合中,才允许放入。或者直接忽略丢弃。
// 通知所有等待方
responseMap.putIfAbsent(seqId, rpcResponse);
logger.debug("[Invoke] 获取结果信息,seqId: {}, rpcResponse: {}", seqId, JSON.toJSON(rpcResponse));
logger.debug("[Invoke] seqId:{} 信息已经放入,通知所有等待方", seqId);
// 移除对应的 requestMap
requestMap.remove(seqId);
logger.debug("[Invoke] seqId:{} remove from request map", seqId);
// 同步锁
synchronized (this) {
this.notifyAll();
logger.debug("[Invoke] {} notifyAll()", seqId);
}
return this;
}
@Override
public RpcMessageDto getResponse(String seqId) {
try {
RpcMessageDto rpcResponse = this.responseMap.get(seqId);
if(ObjectUtil.isNotNull(rpcResponse)) {
logger.debug("[Invoke] seq {} 对应结果已经获取: {}", seqId, rpcResponse);
return rpcResponse;
}
// 进入等待
while (rpcResponse == null) {
logger.debug("[Invoke] seq {} 对应结果为空,进入等待", seqId);
// 同步等待锁
synchronized (this) {
this.wait();
}
logger.debug("[Invoke] {} wait has notified!", seqId);
rpcResponse = this.responseMap.get(seqId);
logger.debug("[Invoke] seq {} 对应结果已经获取: {}", seqId, rpcResponse);
}
return rpcResponse;
} catch (InterruptedException e) {
logger.error("获取响应异常", e);
throw new MqException(MqCommonRespCode.RPC_GET_RESP_FAILED);
}
}
}</code></pre><p>这里 getResponse 获取不到会进入等待,直到 addResponse 唤醒。</p><p>但是这也有一个问题,如果一个请求的响应丢失了怎么办?</p><p>总不能一直等待吧。</p><h3>TimeoutCheckThread 超时检测线程</h3><p>超时检测线程就可以帮我们处理一些超时未返回的结果。</p><pre><code class="java">package com.github.houbb.mq.common.support.invoke.impl;
import com.github.houbb.heaven.util.common.ArgUtil;
import com.github.houbb.mq.common.rpc.RpcMessageDto;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
/**
* 超时检测线程
* @author binbin.hou
* @since 0.0.2
*/
public class TimeoutCheckThread implements Runnable {
/**
* 请求信息
* @since 0.0.2
*/
private final ConcurrentHashMap<String, Long> requestMap;
/**
* 请求信息
* @since 0.0.2
*/
private final ConcurrentHashMap<String, RpcMessageDto> responseMap;
/**
* 新建
* @param requestMap 请求 Map
* @param responseMap 结果 map
* @since 0.0.2
*/
public TimeoutCheckThread(ConcurrentHashMap<String, Long> requestMap,
ConcurrentHashMap<String, RpcMessageDto> responseMap) {
ArgUtil.notNull(requestMap, "requestMap");
this.requestMap = requestMap;
this.responseMap = responseMap;
}
@Override
public void run() {
for(Map.Entry<String, Long> entry : requestMap.entrySet()) {
long expireTime = entry.getValue();
long currentTime = System.currentTimeMillis();
if(currentTime > expireTime) {
final String key = entry.getKey();
// 结果设置为超时,从请求 map 中移除
responseMap.putIfAbsent(key, RpcMessageDto.timeout());
requestMap.remove(key);
}
}
}
}</code></pre><p>处理逻辑就是定时检测,如果超时了,就默认设置结果为超时,并且从请求集合中移除。</p><h2>消息生产者实现</h2><h3>启动核心类</h3><pre><code class="java">public class MqProducer extends Thread implements IMqProducer {
private static final Log log = LogFactory.getLog(MqProducer.class);
/**
* 分组名称
*/
private final String groupName;
/**
* 端口号
*/
private final int port;
/**
* 中间人地址
*/
private String brokerAddress = "";
/**
* channel 信息
* @since 0.0.2
*/
private ChannelFuture channelFuture;
/**
* 客户端处理 handler
* @since 0.0.2
*/
private ChannelHandler channelHandler;
/**
* 调用管理服务
* @since 0.0.2
*/
private final IInvokeService invokeService = new InvokeService();
/**
* 获取响应超时时间
* @since 0.0.2
*/
private long respTimeoutMills = 5000;
/**
* 可用标识
* @since 0.0.2
*/
private volatile boolean enableFlag = false;
/**
* 粘包处理分隔符
* @since 1.0.0
*/
private String delimiter = DelimiterUtil.DELIMITER;
//set 方法
@Override
public synchronized void run() {
// 启动服务端
log.info("MQ 生产者开始启动客户端 GROUP: {}, PORT: {}, brokerAddress: {}",
groupName, port, brokerAddress);
EventLoopGroup workerGroup = new NioEventLoopGroup();
try {
// channel handler
this.initChannelHandler();
// 省略,同以前
// 标识为可用
enableFlag = true;
} catch (Exception e) {
log.error("MQ 生产者启动遇到异常", e);
throw new MqException(ProducerRespCode.RPC_INIT_FAILED);
}
}
}</code></pre><p>其中初始化 handler 的实现如下:</p><pre><code class="java">private void initChannelHandler() {
final ByteBuf delimiterBuf = DelimiterUtil.getByteBuf(delimiter);
final MqProducerHandler mqProducerHandler = new MqProducerHandler();
mqProducerHandler.setInvokeService(invokeService);
// handler 实际上会被多次调用,如果不是 @Shareable,应该每次都重新创建。
ChannelHandler handler = new ChannelInitializer<Channel>() {
@Override
protected void initChannel(Channel ch) throws Exception {
ch.pipeline()
.addLast(new DelimiterBasedFrameDecoder(DelimiterUtil.LENGTH, delimiterBuf))
.addLast(mqProducerHandler);
}
};
this.channelHandler = handler;
}</code></pre><h3>MqProducerHandler 生产者处理逻辑</h3><p>和消费者处理逻辑类似。</p><p>这里最核心的就是添加响应结果:<code>invokeService.addResponse(rpcMessageDto.getTraceId(), rpcMessageDto);</code></p><pre><code class="java">package com.github.houbb.mq.producer.handler;
/**
* @author binbin.hou
* @since 1.0.0
*/
public class MqProducerHandler extends SimpleChannelInboundHandler {
private static final Log log = LogFactory.getLog(MqProducerHandler.class);
/**
* 调用管理类
*/
private IInvokeService invokeService;
public void setInvokeService(IInvokeService invokeService) {
this.invokeService = invokeService;
}
@Override
protected void channelRead0(ChannelHandlerContext ctx, Object msg) throws Exception {
ByteBuf byteBuf = (ByteBuf)msg;
byte[] bytes = new byte[byteBuf.readableBytes()];
byteBuf.readBytes(bytes);
String text = new String(bytes);
log.debug("[Client] channelId {} 接收到消息 {}", ChannelUtil.getChannelId(ctx), text);
RpcMessageDto rpcMessageDto = null;
try {
rpcMessageDto = JSON.parseObject(bytes, RpcMessageDto.class);
} catch (Exception exception) {
log.error("RpcMessageDto json 格式转换异常 {}", JSON.parse(bytes));
return;
}
if(rpcMessageDto.isRequest()) {
// 请求类
final String methodType = rpcMessageDto.getMethodType();
final String json = rpcMessageDto.getJson();
} else {
// 丢弃掉 traceId 为空的信息
if(StringUtil.isBlank(rpcMessageDto.getTraceId())) {
log.debug("[Client] response traceId 为空,直接丢弃", JSON.toJSON(rpcMessageDto));
return;
}
invokeService.addResponse(rpcMessageDto.getTraceId(), rpcMessageDto);
log.debug("[Client] response is :{}", JSON.toJSON(rpcMessageDto));
}
}
}</code></pre><h3>消息的发送</h3><p>关心请求结果的:</p><pre><code class="java">public SendResult send(MqMessage mqMessage) {
String messageId = IdHelper.uuid32();
mqMessage.setTraceId(messageId);
mqMessage.setMethodType(MethodType.P_SEND_MESSAGE);
MqCommonResp resp = callServer(mqMessage, MqCommonResp.class);
if(MqCommonRespCode.SUCCESS.getCode().equals(resp.getRespCode())) {
return SendResult.of(messageId, SendStatus.SUCCESS);
}
return SendResult.of(messageId, SendStatus.FAILED);
}</code></pre><p>不关心请求结果的发送:</p><pre><code class="java">public SendResult sendOneWay(MqMessage mqMessage) {
String messageId = IdHelper.uuid32();
mqMessage.setTraceId(messageId);
mqMessage.setMethodType(MethodType.P_SEND_MESSAGE);
this.callServer(mqMessage, null);
return SendResult.of(messageId, SendStatus.SUCCESS);
}</code></pre><p>其中 callServer 实现如下:</p><pre><code class="java">/**
* 调用服务端
* @param commonReq 通用请求
* @param respClass 类
* @param <T> 泛型
* @param <R> 结果
* @return 结果
* @since 1.0.0
*/
public <T extends MqCommonReq, R extends MqCommonResp> R callServer(T commonReq, Class<R> respClass) {
final String traceId = commonReq.getTraceId();
final long requestTime = System.currentTimeMillis();
RpcMessageDto rpcMessageDto = new RpcMessageDto();
rpcMessageDto.setTraceId(traceId);
rpcMessageDto.setRequestTime(requestTime);
rpcMessageDto.setJson(JSON.toJSONString(commonReq));
rpcMessageDto.setMethodType(commonReq.getMethodType());
rpcMessageDto.setRequest(true);
// 添加调用服务
invokeService.addRequest(traceId, respTimeoutMills);
// 遍历 channel
// 关闭当前线程,以获取对应的信息
// 使用序列化的方式
ByteBuf byteBuf = DelimiterUtil.getMessageDelimiterBuffer(rpcMessageDto);
//负载均衡获取 channel
Channel channel = channelFuture.channel();
channel.writeAndFlush(byteBuf);
String channelId = ChannelUtil.getChannelId(channel);
log.debug("[Client] channelId {} 发送消息 {}", channelId, JSON.toJSON(rpcMessageDto));
if (respClass == null) {
log.debug("[Client] 当前消息为 one-way 消息,忽略响应");
return null;
} else {
//channelHandler 中获取对应的响应
RpcMessageDto messageDto = invokeService.getResponse(traceId);
if (MqCommonRespCode.TIMEOUT.getCode().equals(messageDto.getRespCode())) {
throw new MqException(MqCommonRespCode.TIMEOUT);
}
String respJson = messageDto.getJson();
return JSON.parseObject(respJson, respClass);
}
}</code></pre><h2>测试代码</h2><h3>启动消费者</h3><pre><code class="java">MqConsumerPush mqConsumerPush = new MqConsumerPush();
mqConsumerPush.start();</code></pre><p>启动日志如下:</p><pre><code>[DEBUG] [2022-04-21 19:55:26.346] [main] [c.g.h.l.i.c.LogFactory.setImplementation] - Logging initialized using 'class com.github.houbb.log.integration.adaptors.stdout.StdOutExImpl' adapter.
[INFO] [2022-04-21 19:55:26.369] [Thread-0] [c.g.h.m.c.c.MqConsumerPush.run] - MQ 消费者开始启动服务端 groupName: C_DEFAULT_GROUP_NAME, port: 9527, brokerAddress:
[INFO] [2022-04-21 19:55:27.845] [Thread-0] [c.g.h.m.c.c.MqConsumerPush.run] - MQ 消费者启动完成,监听【9527】端口</code></pre><h3>启动生产者</h3><pre><code class="java">MqProducer mqProducer = new MqProducer();
mqProducer.start();
//等待启动完成
while (!mqProducer.isEnableFlag()) {
System.out.println("等待初始化完成...");
DateUtil.sleep(100);
}
String message = "HELLO MQ!";
MqMessage mqMessage = new MqMessage();
mqMessage.setTopic("TOPIC");
mqMessage.setTags(Arrays.asList("TAGA", "TAGB"));
mqMessage.setPayload(message.getBytes(StandardCharsets.UTF_8));
SendResult sendResult = mqProducer.send(mqMessage);
System.out.println(JSON.toJSON(sendResult));</code></pre><p>生产者日志:</p><pre><code>[INFO] [2022-04-21 19:56:39.609] [Thread-0] [c.g.h.m.p.c.MqProducer.run] - MQ 生产者启动客户端完成,监听端口:9527
...
[DEBUG] [2022-04-21 19:56:39.895] [main] [c.g.h.m.c.s.i.i.InvokeService.addRequest] - [Invoke] start add request for seqId: a70ea2c4325641d6a5b198323228dc24, timeoutMills: 5000
...
[DEBUG] [2022-04-21 19:56:40.282] [main] [c.g.h.m.c.s.i.i.InvokeService.getResponse] - [Invoke] seq a70ea2c4325641d6a5b198323228dc24 对应结果已经获取: com.github.houbb.mq.common.rpc.RpcMessageDto@a8f0b4
...
{"messageId":"a70ea2c4325641d6a5b198323228dc24","status":"SUCCESS"}</code></pre><p>消费者日志:</p><pre><code>[DEBUG] [2022-04-21 19:56:40.179] [nioEventLoopGroup-2-1] [c.g.h.m.c.h.MqConsumerHandler.dispatch] - channelId: 502b73fffec4485c-00003954-00000001-384d194f6233433e-c8246542 接收到 method: P_SEND_MESSAGE 内容:{"methodType":"P_SEND_MESSAGE","payload":"SEVMTE8gTVEh","tags":["TAGA","TAGB"],"topic":"TOPIC","traceId":"a70ea2c4325641d6a5b198323228dc24"}
[INFO] [2022-04-21 19:56:40.180] [nioEventLoopGroup-2-1] [c.g.h.m.c.h.MqConsumerHandler.dispatch] - 收到服务端消息: {"methodType":"P_SEND_MESSAGE","payload":"SEVMTE8gTVEh","tags":["TAGA","TAGB"],"topic":"TOPIC","traceId":"a70ea2c4325641d6a5b198323228dc24"}
[DEBUG] [2022-04-21 19:56:40.234] [nioEventLoopGroup-2-1] [c.g.h.m.c.h.MqConsumerHandler.writeResponse] - [Server] channel 502b73fffec4485c-00003954-00000001-384d194f6233433e-c8246542 response {"requestTime":1650542200182,"traceId":"a70ea2c4325641d6a5b198323228dc24","request":false,"methodType":"P_SEND_MESSAGE","json":"{\"respCode\":\"0000\",\"respMessage\":\"成功\"}"}</code></pre><p>可以看到消费者成功的获取到了生产者的消息。</p><h2>小结</h2><p>到这里,我们就实现了一个消息生产者调用消费者的实现。</p><p>但是你可能会问,这不就是 rpc 吗?</p><p>没有解耦。</p><p>是的,为了解决耦合问题,我们将在下一节引入 broker 消息的中间人。</p><p>希望本文对你有所帮助,如果喜欢,欢迎点赞收藏转发一波。</p><p>我是老马,期待与你的下次重逢。</p><h2>开源地址</h2><blockquote><a href="https://link.segmentfault.com/?enc=1uNEwu4XY07G0anWWBwmzA%3D%3D.lddNnqm%2FAUxihC5c%2BdE%2FjNH8fKvdBGlR1MOxzTkLS2Q%3D" rel="nofollow">The message queue in java.(java 简易版本 mq 实现) </a> <a href="https://link.segmentfault.com/?enc=%2Fv8R8PGL8OqMgx65pvUs4g%3D%3D.c0roEEa8q3C%2BAGqu27jxmq2oAtQj2YC4AcvGLe6T4H4%3D" rel="nofollow">https://github.com/houbb/mq</a></blockquote><h2>拓展阅读</h2><p><a href="https://link.segmentfault.com/?enc=hew8%2FBuu5WE8cpzdcx99lQ%3D%3D.Dx9S7hGipYOY4G8vwKPVtxcSSeytcu9Es5rUiP4GUDQ%3D" rel="nofollow">rpc-从零开始实现 rpc</a> <a href="https://link.segmentfault.com/?enc=B3ot61f4N8nANcZblgsa0A%3D%3D.xw6AvR1sCv3E3u74SMpdCypI1nw8JcilL%2FD0Br7yzrM%3D" rel="nofollow">https://github.com/houbb/rpc</a></p>
【mq】从零开始实现 mq-01-生产者、消费者启动
https://segmentfault.com/a/1190000041739705
2022-04-21T21:59:32+08:00
2022-04-21T21:59:32+08:00
老马啸西风
https://segmentfault.com/u/houbinbin
0
<h2>MQ 是什么?</h2><p>MQ(Message Queue)消息队列,是基础数据结构中“先进先出”的一种数据结构。</p><p>指把要传输的数据(消息)放在队列中,用队列机制来实现消息传递——生产者产生消息并把消息放入队列,然后由消费者去处理。</p><p>消费者可以到指定队列拉取消息,或者订阅相应的队列,由MQ服务端给其推送消息。</p><h3>MQ 的作用?</h3><p>消息队列中间件是分布式系统中重要的组件,主要解决应用解耦,异步消息,流量削锋等问题,实现高性能,高可用,可伸缩和最终一致性架构。</p><p>解耦:一个业务需要多个模块共同实现,或者一条消息有多个系统需要对应处理,只需要主业务完成以后,发送一条MQ,其余模块消费MQ消息,即可实现业务,降低模块之间的耦合。</p><p>异步:主业务执行结束后从属业务通过MQ,异步执行,减低业务的响应时间,提高用户体验。</p><p>削峰:高并发情况下,业务异步处理,提供高峰期业务处理能力,避免系统瘫痪。</p><p>ps: 以上内容摘选自百科。</p><h2>实现 mq 的准备工作</h2><h3>maven 引入</h3><pre><code class="xml"><dependency>
<groupId>io.netty</groupId>
<artifactId>netty-all</artifactId>
<version>4.1.42.Final</version>
</dependency>
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>fastjson</artifactId>
<version>1.2.76</version>
</dependency></code></pre><h3>模块划分</h3><p><a href="https://link.segmentfault.com/?enc=hJlmSB8%2FmMwx4YiWZvL9BA%3D%3D.QB%2FP0mWTofnD9Zdus3I%2FXI7475UHeIZa1xBEuQPLdDw%3D" rel="nofollow">The message queue in java.</a> 作为 mq 的从零开始的学习项目,目前已开源。</p><p>项目的模块如下:</p><table><thead><tr><th align="left">模块</th><th align="left">说明</th></tr></thead><tbody><tr><td align="left">mq-common</td><td align="left">公共代码</td></tr><tr><td align="left">mq-broker</td><td align="left">注册中心</td></tr><tr><td align="left">mq-producer</td><td align="left">消息生产者</td></tr><tr><td align="left">mq-consumer</td><td align="left">消息消费者</td></tr></tbody></table><h2>消息消费者</h2><h3>接口定义</h3><pre><code class="java">package com.github.houbb.mq.consumer.api;
/**
* @author binbin.hou
* @since 1.0.0
*/
public interface IMqConsumer {
/**
* 订阅
* @param topicName topic 名称
* @param tagRegex 标签正则
*/
void subscribe(String topicName, String tagRegex);
/**
* 注册监听器
* @param listener 监听器
*/
void registerListener(final IMqConsumerListener listener);
}</code></pre><p><code>IMqConsumerListener</code> 作为消息监听类的接口,定义如下:</p><pre><code class="java">public interface IMqConsumerListener {
/**
* 消费
* @param mqMessage 消息体
* @param context 上下文
* @return 结果
*/
ConsumerStatus consumer(final MqMessage mqMessage,
final IMqConsumerListenerContext context);
}</code></pre><p>ConsumerStatus 代表消息消费的几种状态。</p><h3>消息体</h3><p>启动消息体 MqMessage 定义如下:</p><pre><code class="java">package com.github.houbb.mq.common.dto;
import java.util.Arrays;
import java.util.List;
/**
* @author binbin.hou
* @since 1.0.0
*/
public class MqMessage {
/**
* 标题名称
*/
private String topic;
/**
* 标签
*/
private List<String> tags;
/**
* 内容
*/
private byte[] payload;
/**
* 业务标识
*/
private String bizKey;
/**
* 负载分片标识
*/
private String shardingKey;
// getter&setter&toString
}</code></pre><h3>push 消费者策略实现</h3><p>消费者启动的实现如下:</p><pre><code class="java">/**
* 推送消费策略
*
* @author binbin.hou
* @since 1.0.0
*/
public class MqConsumerPush extends Thread implements IMqConsumer {
// 省略...
@Override
public void run() {
// 启动服务端
log.info("MQ 消费者开始启动服务端 groupName: {}, port: {}, brokerAddress: {}",
groupName, port, brokerAddress);
EventLoopGroup bossGroup = new NioEventLoopGroup();
EventLoopGroup workerGroup = new NioEventLoopGroup();
try {
ServerBootstrap serverBootstrap = new ServerBootstrap();
serverBootstrap.group(workerGroup, bossGroup)
.channel(NioServerSocketChannel.class)
.childHandler(new ChannelInitializer<Channel>() {
@Override
protected void initChannel(Channel ch) throws Exception {
ch.pipeline().addLast(new MqConsumerHandler());
}
})
// 这个参数影响的是还没有被accept 取出的连接
.option(ChannelOption.SO_BACKLOG, 128)
// 这个参数只是过一段时间内客户端没有响应,服务端会发送一个 ack 包,以判断客户端是否还活着。
.childOption(ChannelOption.SO_KEEPALIVE, true);
// 绑定端口,开始接收进来的链接
ChannelFuture channelFuture = serverBootstrap.bind(port).syncUninterruptibly();
log.info("MQ 消费者启动完成,监听【" + port + "】端口");
channelFuture.channel().closeFuture().syncUninterruptibly();
log.info("MQ 消费者关闭完成");
} catch (Exception e) {
log.error("MQ 消费者启动异常", e);
throw new MqException(ConsumerRespCode.RPC_INIT_FAILED);
} finally {
workerGroup.shutdownGracefully();
bossGroup.shutdownGracefully();
}
}
// 省略...
}</code></pre><p>ps: 初期我们把 consumer 作为服务端,后续引入 broker 则只有 broker 是服务端。</p><h3>MqConsumerHandler 处理类</h3><p>这个类是一个空的实现。</p><pre><code class="java">public class MqConsumerHandler extends SimpleChannelInboundHandler {
@Override
protected void channelRead0(ChannelHandlerContext channelHandlerContext, Object object) throws Exception {
//nothing
}
}</code></pre><h3>测试代码</h3><pre><code class="java">MqConsumerPush mqConsumerPush = new MqConsumerPush();
mqConsumerPush.start();</code></pre><p>启动日志:</p><pre><code>[DEBUG] [2022-04-21 19:16:41.343] [main] [c.g.h.l.i.c.LogFactory.setImplementation] - Logging initialized using 'class com.github.houbb.log.integration.adaptors.stdout.StdOutExImpl' adapter.
[INFO] [2022-04-21 19:16:41.356] [Thread-0] [c.g.h.m.c.c.MqConsumerPush.run] - MQ 消费者开始启动服务端 groupName: C_DEFAULT_GROUP_NAME, port: 9527, brokerAddress:
[INFO] [2022-04-21 19:16:43.196] [Thread-0] [c.g.h.m.c.c.MqConsumerPush.run] - MQ 消费者启动完成,监听【9527】端口</code></pre><h2>消息生产者</h2><h3>接口定义</h3><p>最基本的消息发送接口。</p><pre><code class="java">package com.github.houbb.mq.producer.api;
import com.github.houbb.mq.common.dto.MqMessage;
import com.github.houbb.mq.producer.dto.SendResult;
/**
* @author binbin.hou
* @since 1.0.0
*/
public interface IMqProducer {
/**
* 同步发送消息
* @param mqMessage 消息类型
* @return 结果
*/
SendResult send(final MqMessage mqMessage);
/**
* 单向发送消息
* @param mqMessage 消息类型
* @return 结果
*/
SendResult sendOneWay(final MqMessage mqMessage);
}</code></pre><h3>生产者实现</h3><p>MqProducer 启动的实现如下,基于 netty。</p><pre><code class="java">package com.github.houbb.mq.producer.core;
/**
* 默认 mq 生产者
* @author binbin.hou
* @since 1.0.0
*/
public class MqProducer extends Thread implements IMqProducer {
//省略...
@Override
public void run() {
// 启动服务端
log.info("MQ 生产者开始启动客户端 GROUP: {}, PORT: {}, brokerAddress: {}",
groupName, port, brokerAddress);
EventLoopGroup workerGroup = new NioEventLoopGroup();
try {
Bootstrap bootstrap = new Bootstrap();
ChannelFuture channelFuture = bootstrap.group(workerGroup)
.channel(NioSocketChannel.class)
.option(ChannelOption.SO_KEEPALIVE, true)
.handler(new ChannelInitializer<Channel>(){
@Override
protected void initChannel(Channel ch) throws Exception {
ch.pipeline()
.addLast(new LoggingHandler(LogLevel.INFO))
.addLast(new MqProducerHandler());
}
})
.connect("localhost", port)
.syncUninterruptibly();
log.info("MQ 生产者启动客户端完成,监听端口:" + port);
channelFuture.channel().closeFuture().syncUninterruptibly();
log.info("MQ 生产者开始客户端已关闭");
} catch (Exception e) {
log.error("MQ 生产者启动遇到异常", e);
throw new MqException(ProducerRespCode.RPC_INIT_FAILED);
} finally {
workerGroup.shutdownGracefully();
}
}
//省略...
}</code></pre><h3>MqProducerHandler 处理类</h3><p>默认的空实现,什么都不做。</p><pre><code class="java">package com.github.houbb.mq.producer.handler;
import io.netty.channel.ChannelHandlerContext;
import io.netty.channel.SimpleChannelInboundHandler;
/**
* @author binbin.hou
* @since 1.0.0
*/
public class MqProducerHandler extends SimpleChannelInboundHandler {
@Override
protected void channelRead0(ChannelHandlerContext channelHandlerContext, Object object) throws Exception {
//do nothing now
}
}</code></pre><h3>启动代码</h3><pre><code class="java">MqProducer mqProducer = new MqProducer();
mqProducer.start();</code></pre><p>启动日志:</p><pre><code>[DEBUG] [2022-04-21 19:17:11.960] [main] [c.g.h.l.i.c.LogFactory.setImplementation] - Logging initialized using 'class com.github.houbb.log.integration.adaptors.stdout.StdOutExImpl' adapter.
[INFO] [2022-04-21 19:17:11.974] [Thread-0] [c.g.h.m.p.c.MqProducer.run] - MQ 生产者开始启动客户端 GROUP: P_DEFAULT_GROUP_NAME, PORT: 9527, brokerAddress:
四月 21, 2022 7:17:13 下午 io.netty.handler.logging.LoggingHandler channelRegistered
信息: [id: 0x5cb48145] REGISTERED
四月 21, 2022 7:17:13 下午 io.netty.handler.logging.LoggingHandler connect
信息: [id: 0x5cb48145] CONNECT: localhost/127.0.0.1:9527
四月 21, 2022 7:17:13 下午 io.netty.handler.logging.LoggingHandler channelActive
信息: [id: 0x5cb48145, L:/127.0.0.1:57740 - R:localhost/127.0.0.1:9527] ACTIVE
[INFO] [2022-04-21 19:17:13.833] [Thread-0] [c.g.h.m.p.c.MqProducer.run] - MQ 生产者启动客户端完成,监听端口:9527</code></pre><h2>小结</h2><p>基于 netty 最基本的服务端启动、客户端启动到这里就结束了。</p><p>千里之行,始于足下。</p><p>我们下一节将和大家一起学习,如何实现客户端与服务端之间的交互。</p><p>希望本文对你有所帮助,如果喜欢,欢迎点赞收藏转发一波。</p><p>我是老马,期待与你的下次重逢。</p><h2>开源地址</h2><blockquote><a href="https://link.segmentfault.com/?enc=qa0Hlbuae5k8uWaVYUUoQQ%3D%3D.qyP47KomOYI%2FWG0dom%2F5iFSGlIX2qSpgHXOHYU7DDvM%3D" rel="nofollow">The message queue in java.(java 简易版本 mq 实现) </a>: <a href="https://link.segmentfault.com/?enc=tkctEKEuxRW0MGvMu85e6Q%3D%3D.G%2BEDQlkmI9V10JDlv1FYkmeXqV%2FUOIi7FMRIQPCg4nk%3D" rel="nofollow">https://github.com/houbb/mq</a></blockquote><h2>拓展阅读</h2><p><a href="https://link.segmentfault.com/?enc=im1fOWFra0b4el85q3HIlA%3D%3D.2K3dizIEDq%2FQ7w0tfKH0rmZMSBbt%2F0aLV3p0K0xtT78%3D" rel="nofollow">rpc-从零开始实现 rpc</a>: <a href="https://link.segmentfault.com/?enc=BfkQvt0o2qhNkU7f0jYoMQ%3D%3D.kXPoAWHZO2GCB6Qn4o1VC4BDR4Ke0nvRxLnVsc5DsFM%3D" rel="nofollow">https://github.com/houbb/rpc</a></p><p><a href="https://link.segmentfault.com/?enc=Ccx8ZBJfrFmp3ZkEExK3Mw%3D%3D.fEPwOGScjUJAVZbdwsPp1z6LQZ94O1%2B0iAKROGftS2KNBv7PHAa31queX6cJY9tX%2BrzoBYiIU6ATH1vCQASTVg%3D%3D" rel="nofollow">【mq】从零开始实现 mq-01-生产者、消费者启动</a></p><p><img src="/img/bVcZizc" alt="" title=""></p>
提升文件上传性能的 4 种方式,你会吗?
https://segmentfault.com/a/1190000041464684
2022-02-26T12:36:39+08:00
2022-02-26T12:36:39+08:00
老马啸西风
https://segmentfault.com/u/houbinbin
0
<h2>业务需求</h2><p>产品经理:小明啊,我们需要做一个附件上传的需求,内容可能是图片、pdf 或者视频。</p><p>小明:可以实现的,不过要限制下文件大小。最好别超过 30MB,太大了上传比较慢,服务器压力也大。</p><p>产品经理:沟通下来,视频是一定要的。就限制 50MB 以下吧。</p><p>小明:可以。</p><p><img src="/img/remote/1460000041464686" alt="A FEW DAYS LATER" title="A FEW DAYS LATER"></p><p>测试同学:这文件上传也太慢了吧,我试了一个 50mb 的文件,花了一分钟。</p><p>小明:whats up,这么慢。</p><p>产品经理:不行,你这太慢了, 想办法优化下。</p><h2>优化之路</h2><h3>问题定位</h3><p>整体的文件上传调用链路如下图:</p><p><img src="/img/remote/1460000041464687" alt="调用链路" title="调用链路"></p><p>小明发现前端开始上传,到请求到后端就花费了近 30 秒,应该是浏览器解析文件导致的慢。</p><p>后端服务请求文件服务也比较慢。</p><h3>解决方案</h3><p>小明:文件服务有异步接口吗?</p><p>文件服务:暂时没有。</p><p>小明:这个上传确实很慢,有优化建议吗?</p><p>文件服务:没有,看了下就是这么慢。</p><p>小明:……</p><p>最后小明还是决定把后端的同步返回,调整为异步返回,降低用户的等待时间。</p><p>把后端的实现调整了一番适应业务,前端调用后获取异步返回标识,后端根据标识查询文件服务同步返回的结果。</p><p>缺点也很明显,<strong>异步上传失败,用户是不知道的</strong>。</p><p>不过碍于时间原因,也就是能权衡利弊,暂时上线了。</p><p>最近小明有些时间,于是就想着自己实现一个文件服务。</p><h2>文件服务</h2><p>碍于文件服务的功能非常原始,小明就想着自己实现一个,从以下几个方面优化:</p><p>(1)压缩</p><p>(2)异步</p><p>(3)秒传</p><p>(4)并发</p><p>(5)直连</p><h3>压缩</h3><p>日常开发中,尽可能和产品沟通清楚,让用户上传/下载压缩包文件。</p><p>因为<strong>网络传输是非常耗时的</strong>。</p><p>压缩文件还有一个好处就是节约存储空间,当然,一般我们不用考虑这个成本。</p><p>优点:实现简单,效果拔群。</p><p>缺点:需要结合业务,并且说服产品。如果产品希望图片预览,视频播放,压缩就不太适用。</p><h3>异步</h3><p>对于比较耗时的操作,我们会自然的想到异步执行,降低用户同步等待的时间。</p><p>服务端接收到文件内容后,返回一个请求标识,异步执行处理逻辑。</p><p>那如何获取执行结果呢?</p><p>一般有 2 种常见方案:</p><p>(1)提供结果查询接口</p><p>相对简单,但是可能会有无效查询。</p><p>(2)提供异步结果回调功能</p><p>实现比较麻烦,可以第一时间获取执行结果。</p><h3>秒传</h3><p>小伙伴们应该都用过云盘,云盘有时候上传文件,非常大的文件,却可以瞬间上传完成。</p><p>这是如何实现的呢?</p><p>每一个文件内容,都对应唯一的文件哈希值。</p><p>我们可以在上传之前,查询该哈希值是否存在,如果已经存在,则直接增加一个引用即可,跳过了文件传输的环节。</p><p>当然,这个只在你的用户文件数据量很大,且有一定重复率的时候优势才能体现出来。</p><p>伪代码如下:</p><pre><code class="java">public FileUploadResponse uploadByHash(final String fileName,
final String fileBase64) {
FileUploadResponse response = new FileUploadResponse();
//判断文件是否存在
String fileHash = Md5Util.md5(fileBase64);
FileInfoExistsResponse fileInfoExistsResponse = fileInfoExists(fileHash);
if (!RespCodeConst.SUCCESS.equals(fileInfoExistsResponse.getRespCode())) {
response.setRespCode(fileInfoExistsResponse.getRespCode());
response.setRespMessage(fileInfoExistsResponse.getRespMessage());
return response;
}
Boolean exists = fileInfoExistsResponse.getExists();
FileUploadByHashRequest request = new FileUploadByHashRequest();
request.setFileName(fileName);
request.setFileHash(fileHash);
request.setAsyncFlag(asyncFlag);
// 文件不存在再上传内容
if (!Boolean.TRUE.equals(exists)) {
request.setFileBase64(fileBase64);
}
// 调用服务端
return fillAndCallServer(request, "api/file/uploadByHash", FileUploadResponse.class);
}</code></pre><h3>并发</h3><p>另一种方式就是对一个比较大的文件进行切分。</p><p>比如 100MB 的文件,切成 10 个子文件,然后并发上传。一个文件对应唯一的批次号。</p><p>下载的时候,根据批次号,并发下载文件,拼接成一个完整的文件。</p><p>伪代码如下:</p><pre><code class="java">public FileUploadResponse concurrentUpload(final String fileName,
final String fileBase64) {
// 首先进行分段
int limitSize = fileBase64.length() / 10;
final List<String> segments = StringUtil.splitByLength(fileBase64, limitSize);
// 并发上传
int size = segments.size();
final ConcurrentHashMap<Integer, String> map = new ConcurrentHashMap<>();
final CountDownLatch lock = new CountDownLatch(size);
for(int i = 0; i < segments.size(); i++) {
final int index = i;
Thread t = new Thread() {
public void run() {
// 并发上传
// countDown
lock.countDown();
}
};
t.start();
}
// 等待完成
lock.await();
// 针对上传后的信息处理
}</code></pre><h3>直连</h3><p>当然,还有一种策略就是客户端直接访问服务端,跳过后端服务。</p><p><img src="/img/remote/1460000041464688" alt="文件直连" title="文件直连"></p><p>当然,这个前提要求文件服务必须提供 HTTP 文件上传接口。</p><p>还需要考虑安全问题,最好是前端调用后端,获取授权 token,然后携带 token 进行文件上传。</p><h2>拓展阅读</h2><p><a href="https://link.segmentfault.com/?enc=A0J9HQxZzqcZswpnEM3tOw%3D%3D.5SCxZegcb4wv2Xh6TOePN25nScOtYImI1CuQrNhWr4AO%2Bf83a3jv0b7JKEYO06oQ3OdEd%2FmpVj94SSk1vXDqhQ%3D%3D" rel="nofollow">提升文件上传性能的 4 种方式,你会吗?</a></p><p><a href="https://link.segmentfault.com/?enc=Az5tTtSLBCUzp7BIG%2BeSig%3D%3D.pZyvj2eU8Nhf%2Fz64ot%2FBvQBeue2X3U9SVwe%2FQ33VoJyU8ziAMP2sT24OxZn1ldDMvaCwtOnH157g5YFaFF62pQ%3D%3D" rel="nofollow">异步查询转同步的 7 种实现方式</a></p><p><a href="https://link.segmentfault.com/?enc=%2B5n2kwUwRPwzR39nP6RXSg%3D%3D.2jMdPLN2jwwvD2Ic7MEjbx0BlRFJqWbRz6UdKjVs3zh3JEJrp3gdmfQzr1LoZKOybgbI3jmCbcdkEgu%2FPqnaFA%3D%3D" rel="nofollow">java压缩归档算法框架工具 compress</a></p><h2>小结</h2><p>文件上传是非常常见的业务需求,上传的性能问题是肯定要考虑和优化的一个问题。</p><p>上面的几种方法可以灵活的组合使用,结合自己的业务进行更好的实践。</p><p>希望本文对你有所帮助,如果喜欢,欢迎点赞收藏转发一波。</p><p>我是老马,期待与你的下次重逢。</p><p><img src="/img/bVcVhsC" alt="" title=""></p>
金融互联网公司如何保证用户私人信息安全?
https://segmentfault.com/a/1190000041451300
2022-02-23T21:48:08+08:00
2022-02-23T21:48:08+08:00
老马啸西风
https://segmentfault.com/u/houbinbin
0
<h2>海绵宝宝的烦恼</h2><p>海绵宝宝非常喜欢网上购物,这让他感觉到被资本腐朽的快乐。</p><p><img src="/img/remote/1460000041451302" alt="海绵宝宝" title="海绵宝宝"></p><p>但是有一件事他一直觉得很麻烦,快速上的收件单写满了他的个人信息,撕起来还很麻烦。</p><pre><code>快递单号:202202181111
收件人姓名:海绵宝宝
收件人手机:138 8888 8888
收件人地址:太平洋比基尼海滩比奇堡镇贝壳街124号的波萝屋
备注:暗号是天王盖地虎</code></pre><p>你有没有遇到过和海绵宝宝一样的烦恼呢?又是怎么解决的呢?</p><h2>用户信息隐私安全</h2><h3>明天上线的需求</h3><p>小明今年 26 岁,是一名普普通通的上班族。在某互联网公司做技术开发。</p><p>“小明啊,有一个简单的需求。”,产品经理笑着朝小明走来。</p><p>“哦,又是简单的需求?”小明看了一下产品,“需求文档写了吗?”</p><p>“简单的很,写啥文档呀。”,产品经理接着说,“和上次类似,加一个商品交易记录列表就行。这个需求很急哈,明天能上线吧?”</p><p>“类似个锤子,上次的需求不也是做了一周。你把文档写清楚,过一下需求,排期另说。”,小明也不上当。</p><p><img src="/img/remote/1460000041451303" alt="产品需求" title="产品需求"></p><p>在详细设计完成之后,小明开始进行开发阶段。</p><p>写的还算比较顺利:</p><pre><code class="java">public void doTransaction(TransactionDto dto) {
// 输出日志
log.info("进行交易:{}", JSON.toJSON(dto));
// 执行业务处理
// 执行落库
}</code></pre><p>TransactionDto 中包含了商品购买者的姓名、手机号、居住地址以及交易的相关信息。</p><p>数据库中直接把响应的明文存储,页面直接列表展现。</p><p>这个需求很简单,小明想着,就等着测试验证了。</p><h3>用户信息安全</h3><p>不过在代码评审的时候,项目经理却提出了一个问题,你的代码保护用户的信息安全了吗?</p><p>(1)不应该日志中明文输出用户私人信息</p><p>(2)不应该页面中明文展示用户私人信息</p><p>(3)不应该在数据库中明文存储用户私人信息</p><p>并且以快递单号为例子,他应该如下:</p><pre><code>快递单号:202202181111
收件人姓名:海**宝
收件人手机:138 **** 8888
收件人地址:太平洋比基尼海滩比奇堡镇********
备注:暗号是天王盖地虎</code></pre><p>这样的好处很明显,就算收件人不去撕掉这张单号,也不会暴露太多用户个人信息。</p><p>小明有些不理解,“不让日志输出,那怎么排查问题啊?”</p><p>“你可以输出脱敏信息。禁止日志输出,就是避免可以查看日志的人,泄漏用户个人信息。”</p><p>“那页面不让明文展示,运营怎么解决日常问题呢?”</p><p>“页面可以添加明文显示的按钮,限定对应的权限,并且记录操作日志。”</p><p>“数据库不让存储明文,那怎么玩啊?”</p><p>“你可以去了解下可逆加密。”,项目经理顿了顿,“重写吧,再给你 2 天时间。”</p><p>“好吧”,小明有些失落。</p><h3>技术实现的调整</h3><h4>日志脱敏脱敏</h4><p>关于日志脱敏,小明能想到的最直接的方法,就是重载类的 toString() 方然,然后用工具类脱敏敏感信息。</p><p>不过他的同事老马给他推荐了一个基于注解的脱敏工具包,用起来还算方便:</p><blockquote><a href="https://link.segmentfault.com/?enc=cCG04BxaGgvTPyG6S8hL1g%3D%3D.gtqu%2FeQ%2B8qIHDLUcWvw0J7TWelCQUz%2FkSvPB7I0iUz%2F%2FCp7RygznbzF0W%2FwLt5oS" rel="nofollow">https://github.com/houbb/sensitive</a></blockquote><ul><li>基于注解的日志脱敏。</li><li>可以自定义策略实现,策略生效条件。</li><li>常见的脱敏内置方案。</li><li>java 深拷贝,且原始对象不用实现任何接口。</li><li>支持用户自定义注解。</li><li>支持基于 FastJSON 直接生成脱敏后的 json</li></ul><h4>数据库可逆加密</h4><p>为了避免开发者、DBA、恶意攻击者等泄漏数据库信息,数据库中的敏感信息需要进行加密。</p><p>比如数据库中的手机号</p><pre><code>phone 13066668888</code></pre><p>需要调整为如下:</p><pre><code>phone_chiper BABABABABABABABBABABABA # 加密之后的密文
phone_mask 130****8888 # 手机号掩码
phone_hash FFFFFFFFFFFFFFFFFFFFFFFF # 手机号哈希</code></pre><p>加密算法要求是可逆加密,如 AES/3DES/SM4 等。</p><p>掩码可以和上面的脱敏保持一致,主要用户初步的信息确认等。</p><p>哈希一般用于精准查询,采用 MD5/SHA 等常见单向哈希即可。</p><p>你是一名黑客,你看到了数据库中的掩码+哈希,且知道他们哈希算法是 MD5 哈希,可以获取对应的手机号明文吗?</p><p><img src="/img/remote/1460000041451304" alt="hacker" title="hacker"></p><p>答案是可以的,所以我们在<strong>选择哈希算法实现的时候要考虑这些问题</strong>。</p><p>如何获取明文,又如何避免这个问题呢?</p><p>欢迎小伙伴在评论区留言。</p><h4>页面的功能设计</h4><p>产品在敏感信息方面,如果是一个新手,可能会要求明文展示,或者明文导出。</p><p>如果你作为安全部门,或者项目经理,一定要把相关的需求给直接的明文的需求砍掉。</p><p>产品经理:砍我可以,砍需求不行。</p><p>安全部门:数据必须保证安全。</p><p>项目经理:我们加一个明文查看的按钮功能,添加权限控制,记录查看日志。不影响业务,又保证数据安全。</p><p>前后端开发:……</p><h2>架构层面</h2><p>当然,上面的情况,可能只是小明作为一名开发的日常。</p><p>那如果是一家公司呢?</p><p>如果你作为已加电商/金融公司的技术架构,你会如何保护用户信息的安全隐私呢?</p><h3>加密机服务</h3><p>当公司的业务规模达到一定程度时,我们的应用往往就不再是单体,目前主流的是微服务架构。</p><p>每一家金融公司,都会有一个加密机服务,用于统一提供上面小明遇到的业务问题。</p><p>为不同类型的敏感数据,提供相应的脱敏、可逆加密、哈希服务。</p><h3>为什么需要</h3><p>我们把这些实现,写成一个工具类放在代码里不就行了吗?为什么还要搞一个加密机服务呢。</p><p>先说比较常见的原因:</p><p>(1)提高工作效率</p><p>有统一的加密服务和对应的 SDK 之后,可以缩短迭代周期。</p><p>研发的技术水平良莠不齐,节约了他们学习+编写的时间。</p><p>测试的水平也是类似,同样也可以节约测试验证的时间。</p><p>而这,也正是公共服务最大的魅力。</p><p>(2)架构之美</p><p>如果没有统一的加密服务,每个开发来一套,会导致每个系统的差异比较大。</p><p>整体的数据信息就会乱七八糟,当需要数据整合,或者统一的加密升级时,代价会非常高。</p><p>(3)家贼难防</p><p><strong>如今的公司,都在不断的削弱研发的生产权限</strong>。</p><p>一家正规的公司,严格按照流程,是不会出现删库跑路的事情的。</p><p><img src="/img/remote/1460000041451305" alt="删库跑路" title="删库跑路"></p><p>代码编写,需要功能测试+代码覆盖率+code review,避免研发植入业务无关代码。</p><p>脚本执行,需要业务端发起+项目经理审核+DBA 审核,避免恶意操作。</p><p>生产发布变更,需要上线计划+审核,避免恶意操作。</p><p>日志查看,主流的都是 ELK 平台,研发无登录生产机器的权限。</p><p>生产功能,研发没有使用权限。</p><p>这一套组合拳下来,就是让研发作为一个纯纯的工具人,只能做事,像防贼一样防着研发。</p><p>一名研发离职,完全知道系统的秘钥+算法+系统弱点,想获取用户隐私信息也不是不可能。</p><p>但是如果引入加密机之后呢?</p><p>加密机会让公司的架构编写,技术水平没的说,技术安全有保证,相对也比较稳定。</p><p>研发作为使用者,不知道秘钥,不知道算法,攻击也就无从谈起。</p><p>这又何尝不是一种技术者的悲哀。</p><p><img src="/img/remote/1460000041451306" alt="悲哀" title="悲哀"></p><h2>小结</h2><p>每一家公司都有保护用户隐私安全的义务,只可惜现实中很多用户的隐私安全依然无法保证。</p><p>对于一个国家,需要推行相应的法律合规。</p><p>对于一家公司,需要架构,安全部门,产研测的共同努力。</p><p>对于一位用户,我们也要有保护自己信息安全的意识。</p><p>希望本文对你有所帮助,如果喜欢,欢迎点赞收藏转发一波。</p><p>我是老马,期待与你的下次重逢。</p><p><img src="/img/remote/1460000041451307" alt="在这里插入图片描述" title="在这里插入图片描述"></p>
关于为了吃瓜通宵7天写了一个网站却没钱买域名这件小事
https://segmentfault.com/a/1190000041179326
2021-12-25T18:07:12+08:00
2021-12-25T18:07:12+08:00
老马啸西风
https://segmentfault.com/u/houbinbin
0
<h2>我不做猹了,闰土!</h2><p>我记得那夜的月亮很亮,很圆。</p><p>漫天星河,横挂在无边的瓜田之上。</p><p>我捧着手里的瓜,细细地品味着,饱满多汁的瓜瓤在味蕾流淌。</p><p>晚风轻柔,远处有着不知名的虫儿哼着不知名的调调。</p><p>如此良夜,我甚至想高歌一曲。</p><p>忽然,一柄银色的钢叉向我袭来,吓得我汗毛倒立,差点没被口中的瓜瓤呛到。</p><p>我想拔腿就跑,但是我知道已经来不及了。</p><p>我正面迎向了眼前的少年,大概也算是英勇的姿态吧。</p><p>银色的钢叉从我的身体穿过,我感到有什么东西在流淌。</p><p><img src="/img/remote/1460000041179328" alt="月下吃瓜图" title="月下吃瓜图"></p><p>曾经我以为只要我吃瓜的速度足够快,钢叉就追不上我。</p><p><strong>我越是提升吃瓜的速度,越是发现猹是有极限的,我不做猹了,闰土!</strong></p><p>我无力地横躺在瓜田之上,渐渐闭上了眼睛。</p><p>我记得那夜的月亮很亮,很圆。</p><h2>但是,我拒绝!</h2><p>当我睁开眼睛的时候,已经是中午了。</p><p>“今天 xxx 的事情你知道吗?天啊,这个世道真的是。”</p><p>“谁说不是呢?最近吃瓜真的是吃的累了。”</p><p>“这么多钱,我们全家从山顶洞人开始干都挣不到。”</p><p>听着周围七嘴八舌的声音,我感觉有些懵。</p><p>睡了个午觉,又发生什么我不知道的事情了?</p><p>我慌忙地掏出手机,打开 APP,找到热榜,开始贪婪的吸收着热点的信息,希望跟上这个变化莫测的时代。</p><p>可是,下一刻我觉得有一些迷茫,我为什么要孜孜不倦地追逐每天的热点呢?</p><p><strong>大概是想和他人有一些茶余饭后的谈资。大概是内心觉得不应该错过正在发生的每一件大事。</strong></p><p>但是,我觉得一切本不应该这样。</p><p>“只要我愿意在手机里安装摆渡、围脖、B乎这些 APP,忍受着每天无尽的推送,广告,10min 刷一次消息热榜,我就不会错过发生的大事,是吗?”</p><p>“但是,我拒绝!”</p><p><img src="/img/remote/1460000041179329" alt="但是,我拒绝" title="但是,我拒绝"></p><p>信息,是为人服务的。而不是人成为信息的奴隶。</p><p>我想在这个互联网时代,设计一款以人为核心的吃瓜服务。</p><h2>云雀叫了一整天</h2><p>记得早先少年时,还没有接触过互联网,能接触世界的大概只有电视和报纸。</p><p>信息不多,反而没有那么多信息焦虑。</p><p>我希望这个吃瓜服务可以满足下面的条件:</p><p>(1)免费</p><p>(2)免安装</p><p>(3)简洁</p><p>(4)无广告</p><p>乙方听了想打人,甲方听了连夜申请破产。</p><p>诚然,想全部做到有些难,不过也不是不可能。</p><p>那么代价是什么呢?</p><p>$dollar$。</p><p>没错,天下没有免费的午餐,除非有人替我们付款。</p><p>于是,第一步,掏钱买服务器。</p><p>第二步,产品设计。</p><p>(1)免安装</p><p>要想做到免安装,可选的方案并不多。</p><p>基于网站,或者小程序。当然,小程序的限制比较多。</p><p>于是,我选择了邮件。</p><p>只需要提供邮件,就可以定时接收每天的热点信息。</p><p>(2)简洁性</p><p>要多简洁呢?</p><p>简洁到张小龙直呼佩服,美工直接下岗就行。</p><p>邮件的接收界面如下:</p><p><img src="/img/remote/1460000041179330" alt="邮件接收界面" title="邮件接收界面"></p><p>(3)定制化</p><p>每个用户关注的内容不同,可以自由配置接收的信息。</p><p><img src="/img/remote/1460000041179331" alt="推送定制化" title="推送定制化"></p><p>接下来就差一个程序员就可以上市了。</p><p>什么?没钱请。算了,我自己来吧。</p><h2>机器人朝九晚五有什么错?</h2><p>于是加班加点了一周,总算是把前后端的代码写完了。</p><p>接下来还不是小菜一碟,<code>3 * (5/2)</code> 搞定了基本环境安装。</p><p>服务部署,发现页面不显示,各种 BUG 搞了一天,最后算是跑起来了。</p><p>运维,真 TND 的辛苦。</p><p>想着自己 996,机器也要 996 这似乎太不讲机道主义了。</p><p>于是把机器的任务改成每天只推送早报和晚报,朝九晚五,我愿称之为机器界公务员。</p><p>接下来是不是要上市了?</p><p>非也,我们还是先谈谈吃瓜服务的定位吧。</p><p>英文名:what's up?</p><p>中文名: 发生肾么事了?</p><p>slogan: 你这瓜保熟吗</p><p>应用想推广,首先要有域名吧。</p><p>我看了一眼 whatsup.com 的域名已经售出,而且有点贵,上市之路中断。</p><p>当然,服务部署之后还需要持续升级,运营,推广。</p><p>不过好在主要是自己用,<strong>至少有一个用户,这也够了</strong>。</p><p>如果感兴趣,可以关-=注【老马啸西风】,后台回复【热点】。可以获取注册地址,限定名额 20 个吧。</p><p>写到这里,本该结束了。</p><p>还是说一些废话吧。</p><p>作为一个一直想着改变世界的 coder,这么多年从来没为自己写过什么程序。</p><p>我想很多编码的小伙伴都是如此,我们就像是一位位陶者,把泥土塑造成大厦,却不在大厦之中。</p><pre><code>《陶者》
陶尽门前土,屋上无片瓦。
十指不沾泥,鳞鳞居大厦。</code></pre><p><img src="/img/bVcVhsC" alt="" title=""></p>
NLP 开源形近字算法补完计划(完结篇)
https://segmentfault.com/a/1190000041019402
2021-11-25T22:17:34+08:00
2021-11-25T22:17:34+08:00
老马啸西风
https://segmentfault.com/u/houbinbin
0
<h2>前言</h2><p>所有的故事都有开始,也终将结束。</p><p>本文将作为 NLP 汉字相似度的完结篇,为该系列画上一个句号。</p><p><a href="https://link.segmentfault.com/?enc=%2BwwFh0sMWp2telMHA89h0Q%3D%3D.kjaiWp059lpqSVqoGCFQTjxo1qlUuHx7xOlgb23Yvcc8hVkfFiiezsQtLfheIjL%2BqF6gvqG8iwllh%2FRLGT9ReA%3D%3D" rel="nofollow">起-NLP 中文形近字相似度计算思路</a></p><p><a href="https://link.segmentfault.com/?enc=hxf492MyIJ%2FcA%2FQCxZZwzQ%3D%3D.qGQmJrj0c%2FAHu%2BKJrL16cYwiKZHG5aw%2FLlciqoIcqKHYOrbhmNRmqlGbgqVtviF9%2B4aNVHsflRlQEloAHDI%2BIQ%3D%3D" rel="nofollow">承-中文形近字相似度算法实现,为汉字 NLP 尽一点绵薄之力</a></p><p><a href="https://link.segmentfault.com/?enc=0s%2BRIEMt2neK2gtcXQ9RvQ%3D%3D.pO%2BABwgd9IAQSWhzd485Too1Z1L9h8yRrJxOb1fnOlizu499ELQ7gE5d2A5IGUBYY9eSmUHJJLb0LhHYcHQwew%3D%3D" rel="nofollow">转-当代中国最贵的汉字是什么?</a></p><h2>不足之处</h2><p>之所以有本篇,是因为上一次的算法实现存在一些不足。</p><h3>巴别塔</h3><p>《圣经》中有关于巴别塔建造,最终人们因为语言问题而停工的故事。</p><p><img src="/img/remote/1460000041019404" alt="巴别塔" title="巴别塔"></p><pre><code>创11:6 “看哪!他们成为一样的人民,都是一样的言语,如今既作起这事来,以后他们所要作的事,就没有不成就的了。
创11:7 我们下去,在那里变乱他们的口音,使他们的言语彼此不通。”
创11:8 于是,耶和华使他们从那里分散在全地上;他们就停工不造那城了。</code></pre><p>为了避免语言问题,我一开始就实现了一个 exe4j 打包的对比程序,自己跑的很顺畅。</p><p>小伙伴一跑,运行失败。各种环境配置一顿操作,最后还是报错。</p><p>于是,我写了一个 python 简易版本,便于做 NLP 研究的小伙伴们学习。</p><blockquote><a href="https://link.segmentfault.com/?enc=F2GXFVgSxBKyuzkA%2F%2BO6fQ%3D%3D.gPlD9GxS7YR3UVe3uN5ea106Z2MOp6iwQyfaoQNSlLtkqOLTB76lememY1gcLMXyoY6J9cDX7ksde%2FMKXSQdpg%3D%3D" rel="nofollow">https://github.com/houbb/nlp-hanzi-similar/releases/tag/pythn</a></blockquote><p>java 是一种语言,python 是一种语言。</p><p><strong>编程语言,让人和机器之间可以沟通,却让人与人之间产生了隔阂。</strong></p><h3>拆字</h3><p>在 <a href="https://link.segmentfault.com/?enc=jrOf0K7ycnWpgcOmurnigA%3D%3D.zi5%2FscwBBdK6ZdpHKPf%2FRfn6bK5ecY0OEXe85w4c1AHv7OV5SdZqBl0lKcSp8SG4OmsCCTlrxqqCJ0TgdLvNrw%3D%3D" rel="nofollow">当代中国最贵的汉字是什么?</a> 一文中,我们首次说明了汉字的拆合。</p><p>汉字的拆分实现,核心目的之一就是为了完善汉字的相似度比较。</p><p>通过对比汉字的拆分部分,然后获取拆字的相似度,提高对比的准确性。</p><h2>拆字相似度</h2><h3>简单的需求</h3><p>为了便于小伙伴们理解,我们用产品经理的思维和大家介绍一下实现方式。</p><pre><code>我的需求比较简单。
你看,【明】可以拆分【日】【月】,【冐】也可以拆分为【日】【月】。对比一下,结果是显然的。
怎么实现我不管,明天上线吧。</code></pre><p>小伙伴们,应该已经知道怎么实现了吧?</p><p><img src="/img/remote/1460000041019405" alt="简单" title="简单"></p><h3>使用体验</h3><p>诚如产品所言,这个需求已经实现。</p><h4>maven 引入</h4><pre><code class="xml"><dependency>
<groupId>com.github.houbb</groupId>
<artifactId>nlp-hanzi-similar</artifactId>
<version>1.2.0</version>
</dependency></code></pre><h4>使用</h4><pre><code class="java">double rate1 = HanziSimilarHelper.similar('末', '未');</code></pre><p>对应的结果为:0.9696969696969697</p><p>更多使用细节,参考开源地址:</p><blockquote><a href="https://link.segmentfault.com/?enc=ek9k5XWCTwEH6s0AdxwGog%3D%3D.yjjU1OhqAJKqtaaNTP3xsE1iQJMgO07XXGSouMLcAdEQNGnzwfYEzI1%2BuaWXqEfp" rel="nofollow">https://github.com/houbb/nlp-hanzi-similar</a></blockquote><h2>写在完结前</h2><h3>涉及的项目</h3><p>汉字的相似度计算到这里算是告一段落。</p><p>主要涉及的资料及项目有:</p><p><a href="https://link.segmentfault.com/?enc=v4koYPYUDkkLsIsfq5lQ0Q%3D%3D.9%2FZ3qPTJIukMbn5VED95HAI0KL8m4%2BcV0JcxmqazmFk%3D" rel="nofollow">拼音</a></p><p><a href="https://link.segmentfault.com/?enc=RNhdQTU96TABrL3G8Z%2Fl2Q%3D%3D.ELmrYcdJiQHHiGlsTIs4mGpOGyTwPS9wMd95h01NmV268nOd9oFK3mWvnoqedwROpcXBTYAnhx5oaqb7W50EWw%3D%3D" rel="nofollow">拆字</a></p><p><a href="https://link.segmentfault.com/?enc=UPRIUpoSvY75aLvB%2Bhbwxw%3D%3D.j0MPAidPVHbEvO5RppeSiYqJFvQSq99YppgAgDzxtAB9AsXpiKYpdsdeuf3NrhTbwS93%2B0ttu2IW4qu%2B97pppXQFN9tI8WuBG5gXtKgj8Y3gLmg64G9LU7dPnX9SwZnFDqz3W3bA%2FWaQW71AM66f1A%3D%3D" rel="nofollow">四角编码词库</a></p><p><a href="https://link.segmentfault.com/?enc=WiIn9fwsM5%2FPv%2Fn%2FwRmaMw%3D%3D.dZMyPGwFZujEBw6FwapvaOoVR0PV%2FIom4L43O765YN0xPtmjzo%2F0KXEcf0cgGuxw2u0H225Y1x9htqbrdAS768MfGF56ZuR%2FyjBq3DqEV%2Fcmv3zwrRjUXZbkcg3D%2Fcn0AYvIriaZEpwHrItUzibnsA%3D%3D" rel="nofollow">汉字结构词库</a></p><p><a href="https://link.segmentfault.com/?enc=DxGt3eR4dsZhN3XLymoSWA%3D%3D.aI8ESv7Px6p97Y2P753AYkSn2KDHi7moWT92G%2FC%2Be0rU90upVj3nElz6%2BNlV%2BBtgNIzyPvxLYEVJudxpG5Eqc3ifRJecqkodzgupzPb5cGnrFI5%2F%2B3NxD3t92uy3ZzRwIeS7AdfCAQsqJh2qVtY%2BzA%3D%3D" rel="nofollow">汉字偏旁词库</a></p><p><a href="https://link.segmentfault.com/?enc=niCZ9pNNnC0U8vXeWUxdPQ%3D%3D.maazAOpaSVVsclNFUPo31oc4NbyZsDaD8DmU%2Fn8Konr9pXGLW2LfMppwdqtDmYFOAHBlFdSr7XdHOD%2BBSKHv28RSxJbTggNrzrr5Tpj8nudQZGPfihsAI%2BTlY29rt5JY" rel="nofollow">笔画数词库</a></p><p>当然,还可以结果 <a href="https://link.segmentfault.com/?enc=pTBBG0KknhlGf2zU4DI7lg%3D%3D.394OGu%2B%2FfNYiw269CmGPHKH5LieTsJpWWgsOEYvu74ydHfAy7Lc0tjcypVq0Onfo" rel="nofollow">opencc4j</a> 进行繁简体的处理,此处不再延伸。</p><h3>之后的计划</h3><p>NLP 的领域还有很多东西需要大家攻克,毕竟中文 NLP 才刚刚开始。</p><p><strong>技术尚未成功,同志仍需努力。</strong></p><p>据说最近鹅城的某位黄老爷惹得大家怨声载道。</p><p>很多小伙伴说,如果有一款软件可以实现【月丷夫马言卂彳山兀攴人言】的沟通功能,那么我肯定会用。</p><p>所谓说者无心,听者有意。</p><p>写一个通讯软件,主要是为了巩固下 netty 的学习,其他的都不重要。</p><p><img src="/img/remote/1460000041019406" alt="没有你,对我很重要" title="没有你,对我很重要"></p><p>虽然知道就算有,大家肯定也不太会改变,但是老马还是准备试试。</p><h2>java 实现思路</h2><p>警告,如果你头发已经所剩无几,或者对实现并不感兴趣。</p><p>那么就可以收藏+点赞+评论【不明觉厉】,然后离开了。</p><p>下面是枯燥的代码实现环节。</p><p><img src="/img/remote/1460000041019407" alt="轻松" title="轻松"></p><h3>程序员的思维</h3><p>下面是程序员的思维。</p><p>首先要解决几个问题:</p><p>(1)汉字的拆分实现</p><p>这个直接复用已经实现的汉字拆分实现。</p><pre><code class="java">List<String> stringList = ChaiziHelper.chai(charWord.charAt(0));</code></pre><p>相同的一个汉字可以有多种拆分方式,简单起见,我们默认取第一个。</p><p>(2)相似的比较</p><p>假设我们对比 A B 两个汉字,可以拆分为如下的子集。</p><p>A = {A1, A2, ..., Am}</p><p>B = {B1, B2, ..., Bm}</p><pre><code class="java">/**
* 获取拆分后对应的拆分字符
* @param charWord 字符
* @return 结果
*/
private char[] getSplitChars(String charWord) {
List<String> stringList = ChaiziHelper.chai(charWord.charAt(0));
// 这里应该选择哪一个是有讲究的。此处为了简单,默认选择第一个。
String string = stringList.get(0);
return string.toCharArray();
}</code></pre><p>拆分后的子集对比有多种实现方式,简单起见,我们直接遍历元素,判断另一个子集是否存在。</p><p>当然,遍历的时候要以拆分数量较少的的为基准。</p><pre><code class="java">int minLen = Math.min(charsOne.length, charsTwo.length);
// 比较
double totalScore = 0.0;
for(int i = 0; i < minLen; i++) {
char iChar = charsOne[i];
String textChar = iChar+"";
if(ArrayPrimitiveUtil.contains(charsTwo, iChar)) {
//累加分数
}
}</code></pre><p>(3)拆分子集的权重</p><p>比如 <code>一</code> <code>月</code> 两个汉字都是子集,但是因为笔画数不同,权重也不同。</p><p>我们用一个子集的笔画数占整体汉字的笔画数计算权重。</p><pre><code class="java"> int textNumber = getNumber(textChar, similarContext);
double scoreOne = textNumber*1.0 / numberOne * 1.0;
double scoreTwo = textNumber*1.0 / numberTwo * 1.0;
totalScore += (scoreOne + scoreTwo) / 2.0;</code></pre><p>ps: 这里的除以 2,是为了归一化。保证最后的结果在 0-1 之间。</p><p>(4)笔画数</p><p>获取笔画数的方式,我们可以直接复用以前的方法。</p><p>如果没有匹配的,默认笔画数为 1。</p><pre><code class="java">private int getNumber(String text, IHanziSimilarContext similarContext) {
Map<String, Integer> map = similarContext.bihuashuData().dataMap();
Integer number = map.get(text);
if(number == null) {
return 1;
}
return number;
}</code></pre><h3>java 完整实现</h3><p>我们把所有的碎片拼接起来,就得到一个完整的实现。</p><pre><code class="java">/**
* 拆字
*
* @author 老马啸西风
* @since 1.0.0
*/
public class ChaiziSimilar implements IHanziSimilar {
@Override
public double similar(IHanziSimilarContext similarContext) {
String hanziOne = similarContext.charOne();
String hanziTwo = similarContext.charTwo();
int numberOne = getNumber(hanziOne, similarContext);
int numberTwo = getNumber(hanziTwo, similarContext);
// 拆分
char[] charsOne = getSplitChars(hanziOne);
char[] charsTwo = getSplitChars(hanziTwo);
int minLen = Math.min(charsOne.length, charsTwo.length);
// 比较
double totalScore = 0.0;
for(int i = 0; i < minLen; i++) {
char iChar = charsOne[i];
String textChar = iChar+"";
if(ArrayPrimitiveUtil.contains(charsTwo, iChar)) {
int textNumber = getNumber(textChar, similarContext);
double scoreOne = textNumber*1.0 / numberOne * 1.0;
double scoreTwo = textNumber*1.0 / numberTwo * 1.0;
totalScore += (scoreOne + scoreTwo) / 2.0;
}
}
return totalScore * similarContext.chaiziRate();
}
/**
* 获取拆分后对应的拆分字符
* @param charWord 字符
* @return 结果
*/
private char[] getSplitChars(String charWord) {
List<String> stringList = ChaiziHelper.chai(charWord.charAt(0));
// 这里应该选择哪一个是有讲究的。此处为了简单,默认选择第一个。
String string = stringList.get(0);
return string.toCharArray();
}
/**
* 获取笔画数
* @param text 文本
* @param similarContext 上下文
* @return 结果
*/
private int getNumber(String text, IHanziSimilarContext similarContext) {
Map<String, Integer> map = similarContext.bihuashuData().dataMap();
Integer number = map.get(text);
if(number == null) {
return 1;
}
return number;
}
}</code></pre><h2>小结</h2><p>本文引入了汉字拆字,进一步丰富了相似度的实现。</p><p>当然,实现本身依然有很多值得提升的地方,比如拆分后的选择,是否可以递归拆分等,这个还是留给后人研究吧。</p><p>我是老马,期待与你的下次重逢。</p>
当代中国最贵的汉字是什么?
https://segmentfault.com/a/1190000041013405
2021-11-24T22:06:53+08:00
2021-11-24T22:06:53+08:00
老马啸西风
https://segmentfault.com/u/houbinbin
0
<h2>最贵的汉字</h2><p>“你说,当代中国最贵的汉字是什么?”,有一天同事忽然问我。</p><p>“【诺】,因为一诺千金”,我脱口而出。</p><p>“不对。”</p><p>“【字】,一字千金?”</p><p>同事摇头。</p><p>“爱?”</p><p>“这个是无价的,不算。”</p><p>“人才。不是说 21 世纪最贵的是人才吗?”</p><p>“一个字,你这不算。你咋不说家书抵万金呢?”</p><p>“算了,不猜了。”,我脑海里为数不多的几个字转了一圈,没有一个符合的。</p><p>我有些疑惑地看着他,表示放弃。</p><p><img src="/img/remote/1460000041013407" alt="疑惑" title="疑惑"></p><p>“答案是拆。”</p><p>“拆?”</p><p>“对。拆,拆迁的拆。”</p><p>说完,同事笑了笑。笑中带着三分世俗,六分狡黠,还有一分凄凉。</p><p>"拆,财。一拆,财就有了。"</p><p>不过,我却糊涂起来,“那么这个拆,是和你有关系呢?还是和我有关系呢?”</p><p>“都有关系”,同事顿了顿,“也都没有。”</p><p>“和我俩有啥关系,我们把房东的房子拆了周游世界?”</p><p>"No,我们今天来拆点别的。不拆房子。"</p><p>“不拆房子,那拆什么?”</p><p>“拆字。”</p><p>“字?”</p><p>“对。字,汉字的字。”</p><h2>拆字</h2><p>“汉字,知道吧?”,同事继续说着。</p><p>“废话。小学生都知道的问题。”</p><p>“汉字的三要素知道吗?”</p><p>“音形义。”</p><p>“你知道如何用程序实现两个形近字的判断吗?”</p><p>“你是没关注老马吧,他前两天刚写了一篇 NLP 形近字的实现算法。”</p><p>“那篇算法我看了,一般般吧。有些场景不适应。”</p><p>“比如?”</p><p>“比如【明】与【冐】,结构不同,偏旁不同。但是他俩像不像?”</p><p><img src="/img/remote/1460000041013408" alt="竟能如此相像" title="竟能如此相像"></p><p>真实文化限制了我的想象力,在震惊了 0.9 秒之后,我立刻反驳。</p><p>“说的轻巧,说的你能解决这个问题一样?”</p><p>同事笑了笑,“难也不难。这就涉及到今天的主角,拆字。”</p><p>“哦。愿闻其详。”</p><p>“我们可以把汉字拆开最基本的组成部分,比如【明】可以拆成【日】【月】,【冐】也是类似的。然后去比较每一个部分是否相同,用来计算相似度。”</p><p>“这倒是挺不错”,我不禁点头称赞,“那么问题来了,这种拆字程序要多久才能实现呢?”</p><p>同事笑了笑,“在下不才,这两天下班期间写好了一个拆字程序。”</p><p>“秀啊,你给拆一个秦时明月汉时关我瞅瞅。”</p><p>同事小手一敲,结果输出 <code>手乀禾日寸日月冂一一水又日寸丷天</code>。</p><p>“可以可以。除了 NLP 形近字,你这程序还有啥用?”</p><p>“用处挺多的,不过最主要的是帅。比如这张我用拆字结合云图自动生成的图片。”</p><p><img src="/img/remote/1460000041013409" alt="拆分与云图" title="拆分与云图"></p><p><strong>技术总会过时,但是帅,却是一辈子的事情。</strong></p><p>“你知道这个图,是由哪一句话拆分得到的吗?给你个提示,出自一首词。”</p><h2>合字</h2><p>想到同事给出的难题,最近日夜难寐,到底要怎么解呢?</p><p>正所谓,天下大势,合久必分,分久必合。</p><p>既然汉字能拆分,那么,能不能合并呢?</p><p>于是花了两天时间,写了一个汉字的合并程序。</p><p>可以把 <code>手乀禾日寸日月冂一一水又日寸丷天</code> 合并为 <code>秦时明月汉时关</code>。</p><p>同事看了后,觉得挺有趣。</p><p>“你这个怎么实现的,有点意思。”</p><p>“不难,和汉字的拆分逻辑反过来就行。”</p><p>“比如日月合并的时候,你如何判断是明还是冐呢?”</p><p>“我做了一个字频统计,单个汉字字频高的优先。当然有时候这并不合适,最好还是结合 n-gram 的方式,提高上下文正确率。”</p><p>“不错。匹配算法呢?”</p><p>“FAST-Forward 算法,和以前开源的敏感词类似。”</p><p>“可以可以。那么,我一开始给你的谜面你解开了吗?”</p><p><img src="/img/remote/1460000041013410" alt="话里画外" title="话里画外"></p><h2>不拆不合</h2><p>“运气好,解开了。”</p><p>“怎么做到的?”</p><p>“我在晚上找到了全宋词,不过想到你出的题目肯定是常用的诗词,所以第一步缩小了范围。”</p><p>“然后呢?”</p><p>“我把你图片里的部首合并,汉字穷举。然后做检索。找到之后,把内容正向拆分对比,运气不错找到了。”</p><p>“思路不错。是不是发现拆分用来做验证码也挺不错的。”</p><p>“是的,比起传统验证码会被机器学习的风险,这个识别的成本相对较高。”</p><p>“字拆分用来融入到解密游戏也不错,类似《文字游戏》之类的,招你做技术研发也不亏。”</p><p>同事笑了笑,叹息道:“我最近在想,有时候不是什么事情都要有用吧。技术总会过时,文化却源远流长。拆也好,财也好,有了固然好,没有也无妨。”</p><p>我一时不知道说什么,看着昨晚程序解开的谜底,那里有一行字。</p><pre><code>怒发冲冠,凭栏处、潇潇雨歇。抬望眼,仰天长啸,壮怀激烈。三十功名尘与土,八千里路云和月。</code></pre><p><img src="/img/remote/1460000041013411" alt="让子弹飞" title="让子弹飞"></p>
NLP 中文形近字相似度算法开源实现
https://segmentfault.com/a/1190000041000210
2021-11-22T21:28:44+08:00
2021-11-22T21:28:44+08:00
老马啸西风
https://segmentfault.com/u/houbinbin
0
<h2>项目简介</h2><p><a href="https://link.segmentfault.com/?enc=kAoTeNs2k7VAUMvJfwg3lw%3D%3D.haqUr6A2nPz7PyQ302KeTQygToeVLwo3gbRfgIGOYRufRSTzAdA95b3AAlXBnR0H" rel="nofollow">nlp-hanzi-similar</a> 为汉字提供相似性的计算。</p><p><img src="/img/remote/1460000041000212" alt="在这里插入图片描述" title="在这里插入图片描述"></p><h3>创作目的</h3><p>有一个小伙伴说自己在做语言认知科学方向的课题研究,看了我以前写的 <a href="https://link.segmentfault.com/?enc=F2mlgn8nrn76RNI7cIMGTQ%3D%3D.PGzlT8atEELStWw8jKyMFJUKX8AmwL88tzEDnWIbVjpjDWr5p6symLOrHcMAxhjwsWvQgCTrbDrBcvCjfOBGbQ%3D%3D" rel="nofollow">NLP 中文形近字相似度计算思路</a></p><p>就想问下有没有源码或者相关资料。</p><p>国内对于文本的相似度计算,开源的工具是比较丰富的。</p><p>但是对于两个汉字之间的相似度计算,国内基本一片空白。国内的参考的资料少的可怜,国外相关文档也是如此。</p><p>于是将以前写的相似度算法整理开源,希望能帮到这位小伙伴。</p><p>本项目旨在抛砖引玉,实现一个基本的相似度计算工具,为汉字 NLP 贡献一点绵薄之力。</p><h3>特性</h3><ul><li>fluent 方法,一行代码搞定一切</li><li>高度自定义,允许用户定义自己的实现</li><li>词库自定义,适应各种应用场景</li><li>丰富的实现策略</li></ul><p>默认实现了基于 四角编码+拼音+汉字结构+汉字偏旁+笔画数 的相似度比较。</p><h2>变更日志</h2><blockquote><a>变更日志</a></blockquote><h2>快速开始</h2><h3>需要</h3><p>jdk1.7+</p><p>maven 3.x+</p><h3>maven 引入</h3><pre><code class="xml"><dependency>
<groupId>com.github.houbb</groupId>
<artifactId>nlp-hanzi-similar</artifactId>
<version>1.0.0</version>
</dependency></code></pre><h3>快速开始</h3><h4>基本用法</h4><p><code>HanziSimilarHelper.similar</code> 获取两个汉字的相似度。</p><pre><code class="java">double rate1 = HanziSimilarHelper.similar('末', '未');</code></pre><p>结果为:</p><pre><code>0.9629629629629629</code></pre><h4>自定义权重</h4><p>默认是根据 四角编码+拼音+汉字结构+汉字偏旁+笔画数 进行相似度比较。</p><p>如果默认的系统权重无法满足你的需求,你可以通过自定义权重调整:</p><pre><code class="java">double rate = HanziSimilarBs.newInstance()
.jiegouRate(10)
.sijiaoRate(8)
.bushouRate(6)
.bihuashuRate(2)
.pinyinRate(1)
.similar('末', '未');</code></pre><h4>自定义相似度</h4><p>有些情况下,系统的计算是无法满足的。</p><p>用户可以在根目录下 <code>hanzi_similar_define.txt</code> 进行自定义。</p><pre><code>入人 0.9
人入 0.9</code></pre><p>这样在计算 <code>人</code> 和 <code>入</code> 的相似度时,会优先以用户自定义的为准。</p><pre><code class="java">double rate = HanziSimilarHelper.similar('人', '入');</code></pre><p>此时的结果为用户自定义的值。</p><h2>引导类</h2><h3>说明</h3><p>为了便于用户自定义,<code>HanziSimilarBs</code> 支持用户进行自定义配。</p><p>HanziSimilarBs 中允许自定义的配置列表如下:</p><table><thead><tr><th align="left">序号</th><th align="left">属性</th><th align="left">说明</th></tr></thead><tbody><tr><td align="left">1</td><td align="left">bihuashuRate</td><td align="left">笔画数权重</td></tr><tr><td align="left">2</td><td align="left">bihuashuData</td><td align="left">笔画数数据</td></tr><tr><td align="left">3</td><td align="left">bihuashuSimilar</td><td align="left">笔画数相似度策略</td></tr><tr><td align="left">4</td><td align="left">jiegouRate</td><td align="left">结构权重</td></tr><tr><td align="left">5</td><td align="left">jiegouData</td><td align="left">结构数据</td></tr><tr><td align="left">6</td><td align="left">jiegouSimilar</td><td align="left">结构相似度策略</td></tr><tr><td align="left">7</td><td align="left">bushouRate</td><td align="left">部首权重</td></tr><tr><td align="left">8</td><td align="left">bushouData</td><td align="left">部首数据</td></tr><tr><td align="left">9</td><td align="left">bushouSimilar</td><td align="left">部首相似度策略</td></tr><tr><td align="left">10</td><td align="left">sijiaoRate</td><td align="left">四角编码权重</td></tr><tr><td align="left">12</td><td align="left">sijiaoData</td><td align="left">四角编码数据</td></tr><tr><td align="left">13</td><td align="left">sijiaoSimilar</td><td align="left">四角编码相似度策略</td></tr><tr><td align="left">14</td><td align="left">pinyinRate</td><td align="left">拼音权重</td></tr><tr><td align="left">15</td><td align="left">pinyinData</td><td align="left">拼音数据</td></tr><tr><td align="left">16</td><td align="left">pinyinSimilar</td><td align="left">拼音相似度策略</td></tr><tr><td align="left">17</td><td align="left">hanziSimilar</td><td align="left">汉字相似度核心策略</td></tr><tr><td align="left">18</td><td align="left">userDefineData</td><td align="left">用户自定义数据</td></tr></tbody></table><p>所有的配置都可以基于接口,用户进行自定义。</p><h2>快速体验</h2><h3>说明</h3><p>如果 java 语言不是你的主要开发语言,你可以通过下面的 exe 文件快速体验一下。</p><h3>下载地址</h3><blockquote><a href="https://link.segmentfault.com/?enc=ith0FOyjLbIa1KSTlLBawg%3D%3D.2%2B%2FV4fHzFyEExMFeZ3O80jqKsaC4NhlLdiLHjx7Z5rxwr4dDb0S41t1wUV19vFeDxmOJCkToemDITdlZKzyM88zxWGlf7eDPlWxujdTw4K4vAcn4vVqgU5rDfHjLgQpl" rel="nofollow">https://github.com/houbb/nlp-hanzi-similar/releases/download/exe/hanzi-similar.zip</a></blockquote><p>下载后直接解压得到 <code>hanzi-similar.exe</code> 免安装的可执行文件。</p><h3>执行效果</h3><p>界面是使用 java swing 实现的,所以美观什么的,已经完全放弃治疗 T_T。</p><p>使用 exe4j 打包。</p><p>字符一输入一个汉字,字符二输入另一个汉字,点击计算,则可以获取对应的相似度。</p><p><img src="/img/remote/1460000041000213" alt="在这里插入图片描述" title="在这里插入图片描述"></p><h2>字典的弊端</h2><p>这个项目开源,是因为有一位小伙伴有相关的需求,但是他不懂 java。</p><p>一开始想把项目设计成为字典的形式,两个字对应一个相似度。</p><p>但是有一个问题,2W 汉字,和 2W 汉字的相似度字典,数量已经是近亿的数据量。</p><p>空间复杂度过高,同时会导致时间复杂度问题。</p><p>所以目前采用的是实时计算,有时间做一下其他语言的迁移 :)</p><h2>实现原理</h2><h3>实现思路</h3><p>不同于文本相似度,汉字相似度的单位是汉字。</p><p>所以相似度是对于汉字的拆解,比如笔画,拼音,部首,结构等。</p><p>推荐阅读:</p><blockquote><a href="https://link.segmentfault.com/?enc=qLwKHBmV3sk0KkQd0YXBNA%3D%3D.S0WLKDJqK2khJmc9LmYoPew9%2FWagz5UuYKMcWJtE6MtbAnEGaM82Ke08ZiA%2Bn%2Fsk17kr9Ryn076uW6Flp5oZ6g%3D%3D" rel="nofollow">NLP 中文形近字相似度计算思路</a></blockquote><p>计算思路描述了实现的原理,但是小伙伴反应不会实现,于是才有了本项目。</p><h3>核心代码</h3><p>核心实现如下,就是各种相似度,进行加权计算。</p><pre><code class="java">/**
* 相似度
*
* @param context 上下文
* @return 结果
* @since 1.0.0
*/
@Override
public double similar(final IHanziSimilarContext context) {
final String charOne = context.charOne();
final String charTwo = context.charTwo();
//1. 是否相同
if(charOne.equals(charTwo)) {
return 1.0;
}
//2. 是否用户自定义
Map<String, Double> defineMap = context.userDefineData().dataMap();
String defineKey = charOne+charTwo;
if(defineMap.containsKey(defineKey)) {
return defineMap.get(defineKey);
}
//3. 通过权重计算获取
//3.1 四角编码
IHanziSimilar sijiaoSimilar = context.sijiaoSimilar();
double sijiaoScore = sijiaoSimilar.similar(context);
//3.2 结构
IHanziSimilar jiegouSimilar = context.jiegouSimilar();
double jiegouScore = jiegouSimilar.similar(context);
//3.3 部首
IHanziSimilar bushouSimilar = context.bushouSimilar();
double bushouScore = bushouSimilar.similar(context);
//3.4 笔画
IHanziSimilar biahuashuSimilar = context.bihuashuSimilar();
double bihuashuScore = biahuashuSimilar.similar(context);
//3.5 拼音
IHanziSimilar pinyinSimilar = context.pinyinSimilar();
double pinyinScore = pinyinSimilar.similar(context);
//4. 计算总分
double totalScore = sijiaoScore + jiegouScore + bushouScore + bihuashuScore + pinyinScore;
//4.1 避免浮点数比较问题
if(totalScore <= 0) {
return 0;
}
//4.2 正则化
double limitScore = context.sijiaoRate() + context.jiegouRate()
+ context.bushouRate() + context.bihuashuRate() + context.pinyinRate();
return totalScore / limitScore;
}</code></pre><p>具体的细节,如果感兴趣,可以自行阅读源码。</p><h3>开源地址</h3><p>为了便于大家的学习和使用,本项目已开源。</p><p>开源地址:</p><blockquote><a href="https://link.segmentfault.com/?enc=WgsbQE9DKtXL%2F6UfhRMHQQ%3D%3D.qwSwBCpd41IiCLQlQSCdPkcG2vpxVtz2%2FgaIVEWdHtQGlPdlFm0KK00gV82Kg35d" rel="nofollow">https://github.com/houbb/nlp-hanzi-similar</a></blockquote><p>欢迎大家,fork&star 鼓励一下老马~</p><h2>算法的优缺点</h2><h3>优点</h3><p>为数不多的几篇 paper 是从汉字的结构入手的。</p><p>本算法引入了四角编码+结构+部首+笔画+拼音的方式,使其更加符合国内的使用直觉。</p><h3>缺点</h3><p>部首这部分因为当时数据问题,实际上是有缺憾的。</p><p>后续准备引入拆字字典,对汉字的所有组成部分进行对比,而不是目前一个简单的部首。</p><h2>后期 Road-MAP</h2><ul><li>[ ] 丰富相似度策略</li><li>[ ] 优化默认权重</li><li>[ ] 优化 exe 界面</li></ul><p><img src="/img/remote/1460000041000214" alt="在这里插入图片描述" title="在这里插入图片描述"></p>
Awesome metaverse projects (元宇宙精选资源汇总)
https://segmentfault.com/a/1190000040919076
2021-11-06T10:18:03+08:00
2021-11-06T10:18:03+08:00
老马啸西风
https://segmentfault.com/u/houbinbin
2
<h2>Awesome Metaverse</h2><p>关于 <a href="https://link.segmentfault.com/?enc=BA8iPCg7mBSR4enrS%2Foqtg%3D%3D.gDpaKVlxXcL7qgwOSNr5HyteRRVWg9Chwxyi3DVOMFlKJ1%2FSicfyuXB49Ib%2BNpzu" rel="nofollow">Metaverse</a> 的精彩项目和信息资源列表。 </p><p>由于关于 Metaverse 是什么存在许多相互竞争的想法,请随时以拉取请求、问题和评论的形式留下反馈。</p><h3>WebXR</h3><ul><li><a href="https://link.segmentfault.com/?enc=1HjDK2r3NO6s4pGKqf3UZw%3D%3D.Is9EnpQUA3CBEH6Y9nevZmZyO9tf8pO3BeRW5U6dsxpQF5NbJf0O%2BWHBf%2F6LLP4MuLrDaem7SpC4UBvHZFyQJQ%3D%3D" rel="nofollow">WebXR Explainer</a> - 什么是 WebXR,有哪些用例?</li><li><a href="https://link.segmentfault.com/?enc=ae1DOvGDAnEkQXjCu8UjIg%3D%3D.dTX4wEqscHIRctlk3QBPcnerhhvI8g%2FKSIKjDKsxSqNWZ4Zw28%2FPPWkasKL%2Fk4G9" rel="nofollow">Awesome WebVR</a> - 精选的 WebVR 软件包和资源列表</li><li><p><a href="https://link.segmentfault.com/?enc=d5B701B7yJlTHup1%2F%2FegQQ%3D%3D.pAsY4ezyy3%2Fja4cW8EhS0fz5uhI2tVuPXPoKHpw5zETjjnhW7pT%2BfVTQPFVi6uDu" rel="nofollow">Exokit</a> - WebXR 元浏览器、引擎、头像系统等</p><ul><li><a href="https://link.segmentfault.com/?enc=LLhaJEeKJFcMuu%2BB%2FQ5WTA%3D%3D.TKJqKxLLZM4eZPvEkyBmmXDI5kWdJU8lBuoDiT6fqAQ%3D" rel="nofollow">Exokit overview</a> - Exokit 工具包的非官方概述</li><li><a href="https://link.segmentfault.com/?enc=LTvJtWmrkLTEkVODJiXTIQ%3D%3D.zV%2BquWPMvDQ7xgcOqenEbga6JpXDcfYsG%2FFIu4cG7HFwW%2Bq3HNKii2wdEbBq%2Bezu" rel="nofollow">Exokit Web</a> - 网页元节引擎</li><li><a href="https://link.segmentfault.com/?enc=o5NyF6UKgk0IxzMKYhCShQ%3D%3D.Wl9YbfzGDzkJ3kfWUPFhzB5ZXZMsb7B6lneGpa6UKc7rSK2z2dXTnhtCNVlim5XQ" rel="nofollow">Exokit Web 2</a> - 使 API 更简单易懂</li></ul></li><li><a href="https://link.segmentfault.com/?enc=kdO0ur4LSdu%2B2BOjtTi7hQ%3D%3D.xvsHyHqb%2BzqBJrPzmQv7mWu%2B6v91N%2BvOxzVU4Qry0TY%3D" rel="nofollow">Webaverse</a> - 使用开放标准制作您拥有的空间 Web 对象</li><li><p><a href="https://link.segmentfault.com/?enc=FbYQN5IWb%2BUT9T%2F1lvEMcA%3D%3D.vX9MGLcqLWgHA9p7bt8bL4Y3P90KFFWLWGraklcow%2Fx%2BWPx%2BWdCEnRIzy5KyThOq" rel="nofollow">Awesome WebXR</a> - 编译 WebXR 演示、框架、编辑器等的链接</p><ul><li><a href="https://link.segmentfault.com/?enc=x3aFFGErYsgZVsf%2Fahn2UQ%3D%3D.sV4S7wGlxLR0eHs1dtzyh01XQ7tN7UjrPi3scCdiAktvw8tfUbZw%2FEnlLji0qC83" rel="nofollow">Awesome WebXR 2</a></li></ul></li></ul><h3>社交虚拟现实(Social VR)</h3><p><strong>开源</strong></p><ul><li><a href="https://link.segmentfault.com/?enc=8jIOzTArV0rMmnE7lBg9ZQ%3D%3D.uVuw%2Bee3%2FrO6vTrVVoGPQhhjpYvXu4yDoXp%2FyBHb8oDcMDQjsjTwh3Kl%2BTAw%2BUo%2F" rel="nofollow">Janus Guide</a> - JanusXR 是一个开源的 3D 浏览器和可视化编辑器</li><li><a href="https://link.segmentfault.com/?enc=nu0pF63WJVV10w%2B7n3WdTw%3D%3D.eIe%2BLMgMUjyf6nuScz0lQnO44IlzF3mMoku1uR7UaJcwosIqnLFvRkVbMgELp2ud" rel="nofollow">JanusWeb</a> - 用于构建互连虚拟现实体验的 Web 框架</li><li><a href="https://link.segmentfault.com/?enc=M9TWlpjSTpOxZdwKiEJaNA%3D%3D.Z4zSSaEicvGTT%2F8FLvpoCpihNn49pJXZ2ltu6JZmAhc%3D" rel="nofollow">Hubs</a> - WebXR 中的多用户虚拟空间,由 Mozilla 制作</li><li><a href="https://link.segmentfault.com/?enc=9uCKKm2dvSIneAh5BxS1NA%3D%3D.5vnxLhmz9CuBMwHqSeD6Veue9rWQBdJmpgKg24%2BFFpZp2pwC4Tev31FHS5BC%2Fy74" rel="nofollow">Spoke</a> - 轻松创建自定义 3D 环境</li><li><a href="https://link.segmentfault.com/?enc=mS1RkCmXy1sNBTbq%2BcnBTw%3D%3D.6tjK%2BbNGTS74mE4hIGUv9HvSOM%2BrN%2BF86Z4MZitl9cs%3D" rel="nofollow">Three.js 编辑器</a> - <a href="https://link.segmentfault.com/?enc=LGqjcSEzpNHZYDL8gAjU1g%3D%3D.UVJ8Xf19aDt%2Bs7HibAAilET1Pf%2B%2FTtIzIayqFaruTwc%3D" rel="nofollow">three.js</a> 的网络编辑器</li><li><a href="https://link.segmentfault.com/?enc=UnpiW568vDTMK6%2BURZOjxw%3D%3D.ruvuaGe88GZtgRi%2FvE8QOqRvzB38R3PJPEKgpglkW2M%3D" rel="nofollow">Babylonjs 编辑器</a> - [babylon.js] 的网络编辑器(<a href="https://link.segmentfault.com/?enc=Clrw4anUaPtQyZRSspLSEw%3D%3D.T%2BfRk2Qkghrrz81NFRr%2BgjiuKJZ6eGnP34oP%2B1rpV%2FY%3D" rel="nofollow">https://www.babylonjs.com/</a>)</li><li><a href="https://link.segmentfault.com/?enc=uugwmYDr9pHOColgrZxj9w%3D%3D.4e4NhdtdBTsS5g9x93jKmm%2BbS14IvLCFgJJjtUvOi%2FM%3D" rel="nofollow">High Fidelity</a> - 开源VR软件平台(已弃用)</li><li><a href="https://link.segmentfault.com/?enc=S1%2FSxcI4vJlwllrLvd4xlg%3D%3D.w4iVNENr5ELWgJBmvMNg9bny9jmWePoB9DF9wpi%2BDc0%3D" rel="nofollow">Vircadia</a> - 社区维护的 High Fidelity 分支</li><li><a href="https://link.segmentfault.com/?enc=N5EM9dzm%2F38CL6jgt2KSYw%3D%3D.5yfFadLXM3yLCeusrdnf6OxSpPeiYv3acJPcaZ5zSHA%3D" rel="nofollow">Tivoli Cloud</a> - High Fidelity 的开源分支</li><li><a href="https://link.segmentfault.com/?enc=RI0e10QozGGQLTI9Gr2FzA%3D%3D.w2arfnb1oLIY9akfWSD2eGXi4PTYscxWO%2FMAZnN29vg%3D" rel="nofollow">Decentraland</a> - 基于区块链的虚拟世界</li><li><a href="https://link.segmentfault.com/?enc=dhh7v54%2FsQzElWx1z3F%2FGA%3D%3D.HNyfseMGN0%2FXeYyTKFdO3B9RPaFg1%2FuO%2Fkbcrmhwti0%3D" rel="nofollow">Webaverse</a> - 无处不在的 3D 应用程序</li><li><a href="https://link.segmentfault.com/?enc=2ye2Nz3Qr8b%2BGh1oOwBVPg%3D%3D.M%2Fut2MCMCgcD7kKsER8KxgB03Z1Z3cqUkKeTo2tQr0JSLdd85T%2Fip25qsfenX%2Fxp" rel="nofollow">XR3ngine</a> - 用于构建可扩展的实时社交应用程序的开源框架</li></ul><p><strong>非免费</strong></p><ul><li><a href="https://link.segmentfault.com/?enc=Z72xsW9HgCoMNJMPAiW1fw%3D%3D.Uwhb4vHuh1fadxYoPt%2FfUOyRi3GraYdsRbuY5Ln2V7Y%3D" rel="nofollow">Cryptovoxels</a> - 用户拥有的虚拟世界</li><li><a href="https://link.segmentfault.com/?enc=oxbR%2FHNqwueNourlQ3EppA%3D%3D.gPIUoIWz6tNO6%2B%2FFIeRqcEo2IMDvz7Abt%2Fm80m38H%2Bo%3D" rel="nofollow">Somnium Space</a> - 基于区块链的持久虚拟世界</li><li><a href="https://link.segmentfault.com/?enc=gOqYzTih4au6RHlMqDDb%2Bw%3D%3D.RWxPDpD0P%2FReA%2FGdNaDuSqPc4IxfUoXRaeAr%2FoCf0UA%3D" rel="nofollow">NeosVR</a> - 旨在加速社交 VR 应用程序开发的引擎</li><li><p><a href="https://link.segmentfault.com/?enc=BkPw%2BEWRi4R3hIh1kxH2uQ%3D%3D.XihMAwsGEugp6%2BgfnohljhxBpShxcP1vIxjWsLh8SgE%3D" rel="nofollow">VRChat</a> - 最大的社交 VR 平台,拥有世界和头像的 UGC</p><ul><li><a href="https://link.segmentfault.com/?enc=dC%2B2eLODuC9yvEfRPNV97Q%3D%3D.LKG4zHhwigGOS5Lz%2Ft9MHRu4k2rqcOieBd3P9bg20FiXwWEXSTrs41Z9J33524bB" rel="nofollow">Awesome VRChat</a> - 有兴趣为 VRchat 开发内容的人的一站式商店</li></ul></li><li><a href="https://link.segmentfault.com/?enc=Go3B0Gm4Hb1l8ZCb2TUl4Q%3D%3D.OThnNt0OlK4YlEPEPnw1djv3xAOPK%2FshHRsnddS4p1440cEXmZKiqezzdRNpUu9NiTxWlBb3jvUHv%2BKCrvS3fjgwpvStqJ%2Fpwhc%2BnhADlK4AjaAoIjnbUD4Ujhj93lms" rel="nofollow">Roblox</a> - 大型在线多人 UGC 游戏平台</li><li><a href="https://link.segmentfault.com/?enc=cL4ppmAZC%2F7m2aLdD4HFrg%3D%3D.TezeD3nz2JmwL6wL531DILbHOSX1NGL4nZkNtIcQb0FzORo37f7vY3rYZ%2B0Pwp0cDOc5dB%2FsZElPvaV9EGi6zA%3D%3D" rel="nofollow">Omniverse</a> - 3D 生产流水线的实时模拟和协作平台</li><li><a href="https://link.segmentfault.com/?enc=vn7f6vC9ZRVhzebc5t5HXg%3D%3D.UaKoqB4GsnT8anM3lSTS89KMnpOLF7Kf%2FUFUlm%2BQBzc%3D" rel="nofollow">dot bigbang</a> - 基于 Web 的多人 UGC 游戏平台,内置工具和 Typescript 脚本</li><li><a href="https://link.segmentfault.com/?enc=jD%2BNZtVlKYL60Q7IOsAsdg%3D%3D.cg5xB0guy1LE40gjtc%2FyP7JI2OaKkPTarKX4ICwQ7bY%3D" rel="nofollow">Helios</a> - 基于虚幻引擎的 UGC 世界、头像和游戏平台</li><li><a href="https://link.segmentfault.com/?enc=FhffkcWdgQkoc3pU4uaMeA%3D%3D.oVyoVjn3b6Ed6rpYUOTyJJnfxDQJYydnOQxQer0Rwt0%3D" rel="nofollow">Meta</a> - The Metaverse 的 Meta(前 Facebook)公告视频</li></ul><h3>头像提供者</h3><ul><li><a href="https://link.segmentfault.com/?enc=s4I%2BjT6ZFUi0kOScjBi7lQ%3D%3D.iDRdaRiyG%2FcawwZ%2BmITHjlK%2B3d2uD01frUqWuBBWRnI%3D" rel="nofollow">Ready Player Me</a> - 元界跨游戏头像平台</li><li><a href="https://link.segmentfault.com/?enc=pF6e3e%2BHqFoA%2FHCoBHEqJA%3D%3D.hN5Kpa0zIZxTm6qWV4Bf1%2FJIZMJjLn7yvRHDkdaIkh4%3D" rel="nofollow">VRoid Hub</a> - 人形VRM头像上传平台</li><li><a href="https://link.segmentfault.com/?enc=%2BNzYUkdu4DQ2H4hfMFHyEw%3D%3D.NasJ6eJBbE56TqYPaDlkgrU4AerT3Ozby2HFPa0xV%2B0%3D" rel="nofollow">Cryptoavatars</a> - 以太坊区块链上的独特 VRM 头像</li></ul><h3>协议和标准</h3><ul><li><a href="https://link.segmentfault.com/?enc=wU1gwNSX6KQiAKwOdNktpw%3D%3D.T4HVGz71eW%2BInvS9QYJ%2FUfmCUT0DJ3nUkAchKNMSpD8%3D" rel="nofollow">Dat</a> - 一个点对点协议</li><li><a href="https://link.segmentfault.com/?enc=L0W%2FIfR51Rx9vhPHA3Z5Cg%3D%3D.uSnWoJ8xjfxnaojZkVxPYg%3D%3D" rel="nofollow">IPFS</a> - 一种点对点超媒体协议</li><li><a href="https://link.segmentfault.com/?enc=xh5MK%2FXTUxHvrfTJ1Aaezg%3D%3D.CMXvyYwW3x6VpUjhwmQCY2zeMdwNUUii32l1Ze%2BRJFk%3D" rel="nofollow">glTF</a> - 运行时 3D 资产交付</li><li><a href="https://link.segmentfault.com/?enc=QZGhYtZTVT65nS1MmJ3%2Bqg%3D%3D.8lkFDBmnYo8R%2BNmwJO8WEfQHabQDV36kbSCybdWcoNg%3D" rel="nofollow">VRM</a> - 3D 人形头像的文件格式</li><li><a href="https://link.segmentfault.com/?enc=a2xXCL4HPPHpMiOJTs%2FtNQ%3D%3D.61tgppAohnOGrNU72M331z5AOKWmPFEgGPtxyBnjsXw%3D" rel="nofollow">以太坊</a> - 去中心化的世界计算机</li><li><a href="https://link.segmentfault.com/?enc=ltJSNqUwEH5en9qTxgmmZA%3D%3D.xCBPq0%2FiTQbhTzvf%2BO8Iu8K15PYHT%2BABiM7LgfCfoeNuonXCbL8AT9NBevmeC3Y5" rel="nofollow">WebXR</a> - 在网络上访问 VR/AR 硬件</li><li><a href="https://link.segmentfault.com/?enc=A0Z1A9tQ6UG%2BsnZT3CKzXA%3D%3D.%2FHa%2Frpjff4Z1OFA6XuQrVkeJDZunx3LWinoSTNGQpxI%3D" rel="nofollow">开放元界互操作性(Open Metaverse Interoperability)</a> - 可互操作的虚拟世界协议</li></ul><h3>书籍</h3><p><strong>科幻</strong></p><ul><li><a href="https://link.segmentfault.com/?enc=inlTXbm%2FN0GIk6Pk%2Bsz7Ig%3D%3D.3XaHkSoungpu9IxlPS%2Fmfq17NR9Jtxiig0f2AVkKbohXHMtBV84bE5825PQ6gLOI" rel="nofollow">Neuromancer</a> - (80s) 定义了赛博朋克流派和赛博空间一词</li><li><a href="https://link.segmentfault.com/?enc=cNUeL3nJPm1i7lLILn%2FOkA%3D%3D.%2FJ8UHjPPdDPHzamrGt%2FpfBIjclHw2vQVRASuDE8gr215UYir5nH3UySolSy542DB" rel="nofollow">Snow Crash</a> - (90 年代) 创造术语 Metaverse 作为互联网的继承者</li><li><a href="https://link.segmentfault.com/?enc=kUOcc1bOlRzXIF4HjOvo4Q%3D%3D.PCMdH2tIcJ%2F90WEKXzZMPRMP3uE%2F4LpbZPv6zMLA%2BhbwMK%2BEwKSZ%2BImOhvF6OuNn" rel="nofollow">Ready Player One</a> - (2011) 后来成为斯皮尔伯格电影的热门书</li><li><a href="https://link.segmentfault.com/?enc=lErQU6z65TKYdmb3ZPPhnQ%3D%3D.uixYg1vDpE2R5VdEVdtsozRGxaZ5g0UnOa77mXkEF7GRZAVdIBbwjCDwsyNUee5h6FIYEqVjEZBJ9s%2B1jka3XeGT9GpUBYOL8oNhm6rOryM%3D" rel="nofollow">Ready Player Two</a> - (2020) 准备好的玩家一的续集</li><li><a href="https://link.segmentfault.com/?enc=boHEMyckwyPXV%2B1azwRrAw%3D%3D.qafs%2FcKRk7VuKJg2FvlNrSC4lDm7xf57Wfy9AetrnMjONV7wtOvKfWyHlJF7%2F9Gp" rel="nofollow">Rainbows End</a> - (2006) 越来越多的数字/虚拟世界与无处不在的计算</li><li><a href="https://link.segmentfault.com/?enc=VtyLr8Cv3Jqk7SmEA1FIEQ%3D%3D.OT%2BvN%2FmmYiAj2IptIA04V0j1SEk%2BKQLbtr1MqFpfkH7JLDNaTjW1XeYQrWpTS8qP" rel="nofollow">Idoru</a> - 虚拟名人和分散的虚拟世界,桥梁三部曲中的第 2 册</li></ul><p><strong>非小说</strong></p><ul><li><a href="https://link.segmentfault.com/?enc=3fLZZ%2FBFgKpeCkzJ%2BnYxMw%3D%3D.ScVlHFSr54mbNKuwCty7KW6jS29wB6H6PhKZ9ifuEmlkiOJLtfXxpZ6iyUgGui46fVOHdL5MNX4RI7kjeSWzDtdfwKvxb3PrtYOJ2xj4Srk%3D" rel="nofollow">空间网络</a> - Web 3.0 将如何连接人类、机器和人工智能以改变世界</li></ul><h3>电影</h3><ul><li>The Matrix 矩阵</li><li>The Thirteenth Floor 十三楼</li><li>Existenz 存在</li><li>Free Guy 自由人</li><li>Tron 创</li><li>Wreck it Ralph 2 破坏它拉尔夫 2</li><li>Ready Player One 准备好球员一</li></ul><h3>文章和博客</h3><ul><li><a href="https://link.segmentfault.com/?enc=wZK2qyjOm1mPoqGPYxr7fQ%3D%3D.NFvekgZT1KB2620cKeypydg4FjZVPgF2mp4pK7RoKw%2BAGaH2XgN526XWjTr0ovtf" rel="nofollow">元节入门</a> - Matthew Ball 的 9 部分系列</li><li><a href="https://link.segmentfault.com/?enc=eGBn0ELJsz0DIZUe%2Fv4ykg%3D%3D.XKOMbBGU3fpbZM2FzdBrsdkJWFcTt0chzu2TVdofyOuM5MWWdpPQh9cOvMuq00alh51sUq5jMbARufA4sq9nkhUtJ0ZvOQVQmCYYcRTcw0%2Bt22Lq3dmFjRVH1fd5FqvgY%2BQUQV58ouptjxRI8v%2FbuGuXcCB3rDVnNHMefE69LHA%3D" rel="nofollow">马修·鲍尔——媒体的未来:电影、元界等</a> - Metaverse 的投资者实地指南</li><li><a href="https://link.segmentfault.com/?enc=gr6vpbzkEB9bH6KIN6%2BfAg%3D%3D.2GjuiQWcIxJhPN6V6bvWFUfxUwjI2g8LcT5yBfsTEuiF7NJ7pgYP1iJckwl1As8R" rel="nofollow">Matthew Ball 的 Metaverse</a> - 它是什么,在哪里可以找到它,谁来建造它,以及 Fortnite</li><li><a href="https://link.segmentfault.com/?enc=4%2FLnFc1YrJ2yNKzz1SF1Cg%3D%3D.zxrxna5LzpOkgJD98Lh4va9XqxM4A69wY8%2FnHKUDX8KJ9Q%2B2XYk2xLSx1QlIJzGBikaSHlGyJDyffj7yi9RzhRgzUs86zG8Y9UUUMDU1igdz9LRCnMEuffKzFznpeqBUsXfgJVxKqOQ35OCKhLzEqg%3D%3D" rel="nofollow">Metaverse 入门</a> - Venturebeat 上的文章互联网的下一次迭代</li><li><a href="https://link.segmentfault.com/?enc=%2BuceDfTif5N7qtXNxJwy%2FQ%3D%3D.vIk3y5YOO2Nsq7sI1tmdcQ5PK3bY5KyVKVix4D2ZBAhf5Pfq8gDaGcJxxL%2BND38ZGkYHLrht5eS%2BXSVOG8aolg%3D%3D" rel="nofollow">Metaverse 的零件列表</a> - Philip Rosedale 的列表</li><li><a href="https://link.segmentfault.com/?enc=bY3Ib5gJy26xGivrIqOzxw%3D%3D.AZdRYugbd7A8xCNbfn9Nm5oNXfYRy2ulBq1cMxBuY51b6LYd7H1oZkiNLislbbgc3zwF%2BvFpbWi1dXcwd2iIkgTaadojdPgD8NOBb2ebapc%3D" rel="nofollow">元界解释</a> - Loup Ventures 将元界分为 4 个部分</li><li><a href="https://link.segmentfault.com/?enc=sBilSO5anmOMfBYWmlrEzA%3D%3D.N41TzDVyA8iiboXEHGRllzpG3%2BXkxt0YoE5baSlpj94%3D" rel="nofollow">Metaversing</a> - 探索与 Metaverse 相关的问题以及设计选择和挑战</li><li><a href="https://link.segmentfault.com/?enc=h12rLfDO2WBO%2BsFNxnx0Qg%3D%3D.SRajtLn8%2BMdM1Si6EXOTn3UAt9UiAtXzZJ6ikwawNy64yL5hHI9OVB0WZwGC24WXIr%2F9uexlQUuUKlV2tObkcfosIyOPMsAXMGELgPglbAY%3D" rel="nofollow">Metaverse 创客指南</a> - 2015 年关于 Metaverse 构建的 5 条原则的文章</li><li><a href="https://link.segmentfault.com/?enc=nCyVwxBjChx2fiTNBpfWeA%3D%3D.A1Qf9zS%2FD68nSonZbDE%2FA2iGZHvNTqnEt9l9b6I5wUa9kn1YsS0DaosWQNtC35sX" rel="nofollow">元界实验室</a> - VR/AR、神经网络和3D互联网实验</li><li><a href="https://link.segmentfault.com/?enc=TNxftcTyxaa2uS4C96FuJA%3D%3D.P5cHpySv17Txb4E4FwPooszMS1eUcA5KXGol%2FAqSs9i9JjYETPaLkeEB%2B5ADwWfw%2BQi%2BI0Aankxg%2BLD5Rv46tw%3D%3D" rel="nofollow">PUDDLES to OASIS</a> - 关于连接不同虚拟世界的有线文章</li><li><a href="https://link.segmentfault.com/?enc=8WUpq5JYmHKkC895JttmDA%3D%3D.w%2FM5PMceSD9zM0CbETp5fDApm0L15%2ByYNClEwPzS%2FLvcj1ap75hjlS0EkXtQqHGvOgDAHGsUcpWYbAJm7iOseQ%3D%3D" rel="nofollow">多元宇宙,而不是元宇宙</a> - Techcrunch 关于多元宇宙的 5 部分系列</li><li><a href="https://link.segmentfault.com/?enc=GRJebvmJbRz0Z4cAq1vuMQ%3D%3D.zj6ioVDd9LmuqvHlLfb6wOIKxDd6xtFmEqCGrGw3GKVmpxCWIMq68djwxy9MVJRMd7n8wlVIvoKfeKRB%2F8Mc9TPIkpM%2Bny0qeNjMXND4miRsMYhXqPu2tcE4f%2B%2FhDR%2BgPIdg09K9ZQtHPNX9rATmzLDDxSUf5n2XoO8svpeQAmM%3D" rel="nofollow">Minecraft 中的图书馆和我们在 Metaverse 中的未来</a> - 未经审查的图书馆</li><li><a href="https://link.segmentfault.com/?enc=nRpl%2FUjxY0HvtmadyZ8g2g%3D%3D.NeBuqjMR0cNuzRULqH34e%2FuL4CBxZ6I8YBjSjMO9wQNofbuesA4HrBi35%2BSH0frpOtJcMSEkfS6%2BRd9Lvna9dHl5xbiRKN4owek%2FRulKtRYJbBmpk9bq%2BZtWO8%2F6rANlOIjmiddRlxUf7X7ZX4J1Nw%3D%3D" rel="nofollow">Roblox 相信用户生成的内容将为我们带来元界</a> - Roblox 希望玩家构建元宇宙</li><li><a href="https://link.segmentfault.com/?enc=qwGD0Rf17SJYHqPSrhF96w%3D%3D.FzwhbyAch96CdNAMx3B7A2t3M4ZcUwpBsPABFgB92i7Lg94NdlN%2FtgGudjTRFXr3QAkU5%2BnUoSQvG4R5wwifNmQf4FZiWb7AeVjGCguPDboNGT23R31uCVlepTBe7xBP" rel="nofollow">Balaji Srinivasan 关于建立“假名经济”</a> - 假名已经成为虚拟世界的常态</li><li><a href="https://link.segmentfault.com/?enc=amrXY7KsSrSRbKpQBjdCjA%3D%3D.%2Br64AsI11gE9AecvJOl1drCFsGtY6v7qp7r%2B%2BFfNpmjodnwPDmm41wX%2FRyZ7C4yrIL8brZNl5lwZams%2BZMjQZOjnRNOsTYfc634am0Uljr7utdRjODAPi1x3a%2BRSRlJFqwVj%2FBOeE6VlldhYocsEgQ%3D%3D" rel="nofollow">元界是什么,我需要关心吗?</a> - 边缘文章是一部分定义,一部分愿望,一部分炒作</li><li><a href="https://link.segmentfault.com/?enc=4LTwzx%2Bz4%2F2r7GS5HFt38w%3D%3D.2y1%2F9lS8%2B6Eg%2FbbB7D7RpfElAHNQXeEww%2BelCEeerpFvhqAf0iq%2B%2Bnzes4xsnjy%2BuQlUt8Dilh11TZiJFwKPvNGOKrSdBef1QKIvnkRmXtEl907A6ZnZu2G16ROYVaQJtAtnpWFklTCml4kxv2I1zA%3D%3D" rel="nofollow">元界的经济</a> - Epic CEO Tim Sweeney专访</li><li><a href="https://link.segmentfault.com/?enc=mNQaLFGmCzUUuQqSDKooCA%3D%3D.hBq8zqjdPFdL5SCWBHr5fMh4peB2gUgjsbpp76OBYnM1mGwpBS1de0wwOchlfc%2F6ctF6jp3Ss71uvEYUDvp4PmbsFV0UF%2FV5qlHnK8cGRz4YkhVRZQ%2FdF%2BN6u35M3usE" rel="nofollow">马克扎克伯格元界访谈</a> - 扎克在 Facebook 上成为“元界公司”</li><li><a href="https://link.segmentfault.com/?enc=xSnzZxa5MRwSsk%2B9a%2FRzPA%3D%3D.j2vqe1CfoFIGBeVn92jelLTV2HDjPLwq4VD5U6NusXref9keuPtLl444qyDu6W%2FkjvQHrIifCuhglM9AkhvCvEQsqggvfgVqd%2BtZ6W%2BsIcE%3D" rel="nofollow">Metaverse 就是一切</a> - Doug Thompson 关于我们现在谈论 Metaverse 的文章</li><li><a href="https://link.segmentfault.com/?enc=y8L5Ml4jOJrctXtpsoB%2BOQ%3D%3D.yJHAwHCGZzwBNtGM6bolp%2Fsw3oXBwq4rfdvZ3LrLD%2F5HUUdGvkjV%2B70QQnCFa7%2B5%2Fb8nhr6tluUtu4RKqXV0qxBfWd%2BzoLP9AU0If%2B3G12g%3D" rel="nofollow">元界七法则</a> - auradeluxe 即将到来的沉浸式现实框架</li></ul><p><strong>加密</strong></p><ul><li><a href="https://link.segmentfault.com/?enc=nCrsV4kTXP7PgB9qaZIkEQ%3D%3D.H9ZfjwxY52xVwoJj%2FCI8JhcqU0of39%2BD9SE6mB8r2IP5G1nY8NGgyeyaoL6PEGJ3iPW%2BozspfUpHvbMfZn5m%2BA%3D%3D" rel="nofollow">Into the Void: Where Crypto Meets The Metaverse</a> - Piers Kicks 文章关于基于区块链的开放 Metaverse 基础</li><li><a href="https://link.segmentfault.com/?enc=DKgPz5V%2F6akezjAKT5MMig%3D%3D.jXJ9v15%2BldX0dBkXik7VZzF1vubeb1dAjZbslo73Q46masGmRDAcMHw7kBG8I4UOdZhAeAh1j4ptq70LPOZMNg%3D%3D" rel="nofollow">The Open Metaverse OS</a> - web3 创新如何与 The Metaverse 融合的探索和框架</li><li><a href="https://link.segmentfault.com/?enc=dNiu9vus1flYhjpA%2B6lIdQ%3D%3D.brCtZzezqpOmDSlZIoXZfP%2BehCdgNf42f0%2B4Cg3tMeZJKUK8eSehw2a3b4olcjXJaD%2FctM3f3qZ85PT01Qsku6idnLgg8X8tbfmllzZdUF5Xa%2F%2F5a63bhLGA5B061NTTLcxRbrto8Q5RoZvVE%2B0b5g%3D%3D" rel="nofollow">Larping Ethereum</a> - 从互操作实用程序中研究 NFT 价值</li><li><a href="https://link.segmentfault.com/?enc=8BibWZ48tjM5xtot8Br7cA%3D%3D.zOMhKCcN00ZtoZTJl%2BAs3Y9d2PLjF8qB%2Bn40q2UjuLoyXNbUufY1dzX7zrlSABnViDKrVq1hz5kxXalNEcvz6g%3D%3D" rel="nofollow">区块链游戏世界的互操作性</a> - 关于连接基于区块链的虚拟世界的想法</li><li><a href="https://link.segmentfault.com/?enc=OX5O8fZf2vWaTZX1wMeyIw%3D%3D.YxYrgnqpxx%2FAwLJZnZy%2Bdee%2BHDkg5S53RwRYyisZhqpQ1ZoEAoRuYvMQy%2BwCKRBA" rel="nofollow">虚拟经济</a> - 关于加密和虚拟世界交叉点的令人惊叹的网站演示</li><li><a href="https://link.segmentfault.com/?enc=CRVV41v4%2FCfkq6BFdsMfaw%3D%3D.RZZt4zN9gTfCCZoPQH23B1LXn6AExUMOq6qXgj55qkje5eNRTm0DTwg68av%2BOIE5" rel="nofollow">Zima Red</a> - NFT、虚拟世界、区块链游戏</li><li><a href="https://link.segmentfault.com/?enc=57N67NIXZLZPA8j7kXlnGQ%3D%3D.TViOpWokixqkIdTWEheBN107EU7sNLkRGQa%2BX2D9148ZgAOQBdnQzJsZpv1P8FSi%2FBp6tyozzf7R2E%2F6i87G3g%3D%3D" rel="nofollow">NFT Canon</a> - 由 a16z 策划的关于 NFT 的资源</li><li><a href="https://link.segmentfault.com/?enc=w5PyJ080c7LO95YXw9AP%2FA%3D%3D.qFVep5DObXZ70pVQ9DqVQBQ9QKnsV1id5J35kLrG4UqROxp7JLAatR7AuNUKEu6Jbrh7nRBnHvF%2F3GnJ2OPVGQ%3D%3D" rel="nofollow">The Great Online Game</a> - 关于互联网多人元游戏的博客文章</li><li><a href="https://link.segmentfault.com/?enc=b8ACwdQXq5fhv%2FE2KyEl2w%3D%3D.OP%2BOCPe7Tsr0jrCUVEfxp92BYDnAkpzmaE2Y096mRgdbdaDFw%2Brxnwj8SoePiF2nXOrzrTKogEes8mD%2BJDNs2OQp3cIqzC9BUOqQn2pfWxLbieUABH99TnrU6tB%2FfJjgEvlqsJCBUgZahxR5oiR8kC6mRUARrQMshnviTnCDtiw%3D" rel="nofollow">VR & 区块链入门</a> - Kent Bye 关于去中心化元界的 20 个大想法</li></ul><h3>白皮书</h3><ul><li><a href="https://link.segmentfault.com/?enc=GehpzIlF%2BDZH6ZWQFZ7G%2FQ%3D%3D.LWUPsEUjr0hW5KupCsy1nkHrGSeg3A6TngXf%2FmFbCZgZVdg75xjWMAZ7ePVPa0%2BZ17Hso%2BRR4rSFB%2B4ZOstpHq6hwbop0ApCHNxRqo8BJsU%3D" rel="nofollow">Somnium Space Economy</a></li><li><a href="https://link.segmentfault.com/?enc=tqvQOIFrJAVgSi4yFiygqA%3D%3D.vPb70zsc7RTUvjc3d%2F7KxaRvepT%2Bf2W5U9D8tTzrqNYf9cJWwgd8cm0MIpm3XcUfTa1uZ9EHCA82TBVxehmBtw%3D%3D" rel="nofollow">沙盒游戏白皮书</a></li><li><a href="https://link.segmentfault.com/?enc=A1GNe%2BfNLKWJwE3GAYwKrw%3D%3D.9hf%2BE7%2Bq8f%2Fvyx%2FNroFdJ70SWvKnd4OyCT5wRVQu6SgP25xMbR0MdVKZD8z8K5sy" rel="nofollow">Decentraland 白皮书</a></li><li><a href="https://link.segmentfault.com/?enc=WIEC6KtufOuOZUWOB0ZlyQ%3D%3D.mfkKgwaQKQXqEDbJVX%2FV8qO%2F9hTdt71%2FJ6J9Wx6DMZYNkZc%2Fz4uI2ZsQy%2BWBI5dvoR4a00wJCGPnSwDt8HIGHcQ5vVNk7U2GM7hsTxOAOnw%3D" rel="nofollow">Webaverse 白皮书</a></li></ul><h3>链接</h3><ul><li><a href="https://link.segmentfault.com/?enc=KI61uKdfiwYRKJIgFT6jOQ%3D%3D.W2Zsm%2FZq%2F4MT%2FtE3QQi%2BJZyS78fau%2BlL%2B5kHK1odVd6HEBqplFuJHfsxO70E9W2e" rel="nofollow"><strong>M3-org Research</strong></a> - 由元界的建设者和制造者进行的开放研究</li><li><a href="https://link.segmentfault.com/?enc=QimARG6ntcs66LMqn8YbAA%3D%3D.pWyXeVwEqEsmdQA8QD0pYPN1VLcdYcFa%2BMxijXuNJfJcQLS%2BVc0L28cwG4vGICAGT2ilYvmabp0HGDve4cmLWA%3D%3D" rel="nofollow">urn-namespaces</a> - 跨元节资产的通用标识符</li><li><p><a href="https://link.segmentfault.com/?enc=pIL%2BG8%2FgXiF3dk1CrOpTPQ%3D%3D.0og%2F9O3QwAo%2BvesOaA6uI3QslQYkSc3Z%2Bs6tl2QLGn8%3D" rel="nofollow">Anarchy Arcade</a> - 基于 Source 引擎构建的高级 3D 桌面和快捷方式启动器</p><ul><li><a href="https://link.segmentfault.com/?enc=V4K8w%2BvUQUtyijdf8H2RRQ%3D%3D.Veuq4gJDxvtGBLAho6Pr2t2bTFBa2LqPd92bQ%2FQ2IeysPmhlgoPLqlaIHtODpxrhpVTlmdy5M2zOeFSaivK4bg%3D%3D" rel="nofollow">MetaShortcutFormat</a> - 规范为通用快捷方式动态生成丰富的存在表示</li></ul></li><li><a href="https://link.segmentfault.com/?enc=s5aqvYrkbNF2yItCAEjldA%3D%3D.zbisrPIcnELGG7r0V73zlvTZzPbP24h%2FKyDiDeSdPwkbPGkcP3uLxlmsB5FYvrDTpa%2Fuue8dmIn0jgz%2FKN7hBPv5FV6%2FDxBX4slmpCE%2Fl%2B8%3D" rel="nofollow">新兴网络元节</a> - 回顾网络的过去,为未来的元节催化条件</li><li><a href="https://link.segmentfault.com/?enc=3jMWDz%2B%2FKFSMqNgpto%2FwuA%3D%3D.F4jOftR3Pf8NA%2BJhQ8toexdV16qDltIihBkPQxFnWfZnzio9SZR6TYbMJ6xe01RT8EzI9w8dPquTNPnFrMCQB9q%2B9jMnAuAXyUyZhqaZsvM%3D" rel="nofollow">Hubs Master Plan</a> - 新兴网络Metaverse的Hubs / Spoke计划</li><li><a href="https://link.segmentfault.com/?enc=Y6nm01oIBH8z9yRsPHgGgA%3D%3D.xbrHMDdQmN2e5%2B9lj7Yi2eyby%2BXBkBalcVJkAYRw90CNANOcEC%2BMG7cA72Kshnp1cs4fa6AmdSp%2BYCrbNFtsG%2FhAAxAcTe%2Fodgotjl%2BCuUt5PsYmpvqzdU%2B0dvyfD2VojughrJdjtyVAipbgA3ny9Q%3D%3D" rel="nofollow">Metaverse 标准概述 (2014)</a> - 来自 IEEE 虚拟世界标准工作团体</li><li><a href="https://link.segmentfault.com/?enc=vnTzmp8dBsdSwn5LDL%2BPcw%3D%3D.liaHVPBHcllLuLXVrJB02%2FgyCXcc5VVko93q4nVJNHE%2FQRTAnKA5U6%2BYfmckZl6zMvcXd5UfbqWab5BIwhVjkZYVOajGRFxf0cDNb6sosWU%3D" rel="nofollow">SIGGRAPH 2019 的 Tim Sweeney</a> - Metaverse 的基本原理和技术</li><li><p><a href="https://link.segmentfault.com/?enc=iH4n9ahkPowCxEtJsNjAwQ%3D%3D.6Y9dhowMD99EbFq9lahW8JQlhwjdUeDkkPAZtSYQGNjAAAprRujMrBoUYx7NDrFen7jA1nwTD%2FBs34i1%2FPe017sTUlROX2O4uLFLpCeBhH8%3D" rel="nofollow">Solipsis</a> - 迈向点对点共享虚拟现实</p><ul><li><a href="https://link.segmentfault.com/?enc=VXgYbT0mehdZW6F%2BuqYO2A%3D%3D.1gFHRZQPJsvdPKO5jjRc3JmcS9ig26Qr4fwY7Z2Qm1kE2scBDVQm%2BGQSGv9XRTDl8p1QIXlwtQGLp3jDbPwYGDp7xO38%2Fb%2FmifJfKESYrKc28jEaL3mo0Tr7t8zWp7Rpl%2FwJyu62HxHWZBccED7v2A%3D%3D" rel="nofollow">Solipsis 演示</a> - 幻灯片版</li></ul></li><li><a href="https://link.segmentfault.com/?enc=Ucsvpf3rB1DYTxVBeTGDig%3D%3D.KY2w7Mi4UBunT9TDtUigJVbVtFWaJd9GzzUj%2FlCqckNGt%2F0DvODYpfAuwRC3EjZYvocLVH2e4jjWpSuaH%2FNPs%2Flktwx1v%2FIf4%2BZ3YCscwQ8%3D" rel="nofollow">3D 虚拟世界和元界</a> - 当前状态和未来可能性</li><li><a href="https://gist.github.com/TrevorFSmith/411561c640bf060c9c18fe4d5ac5e455">元节阅读列表</a> - TrevorFSmith 的元节讨论主题</li><li><a href="https://link.segmentfault.com/?enc=Z03t4xTbWlZ%2BkFtEzjt2BQ%3D%3D.xfxGZUI8fgH8KoI%2FFlI2GWA7LUpnwMaFJ%2BWM8b6a7HU%3D" rel="nofollow">xrdevlog.com</a> - 元界互操作性项目和实验</li></ul><h2>开源</h2><p>本文档已开源:</p><blockquote><a href="https://link.segmentfault.com/?enc=qXYInT7HiJXSkXylAX0ZVg%3D%3D.A7LAyXO9sOdYML037mnn21f6JNM3mjrDGnxNqOa7jTMCl2tTvaBGF5Nd15ilZXT8" rel="nofollow">https://github.com/houbb/awesome-metaverse-zh</a></blockquote><p>欢迎各位小伙伴 star~</p><p>我是老马,期待与你的下次重逢。</p><p><img src="/img/remote/1460000040919078" alt="在这里插入图片描述" title="在这里插入图片描述"></p>
更好的 java 重试框架 sisyphus 的 3 种使用方式
https://segmentfault.com/a/1190000040852516
2021-10-23T11:30:44+08:00
2021-10-23T11:30:44+08:00
老马啸西风
https://segmentfault.com/u/houbinbin
0
<h2>回顾</h2><p>我们前面学习了</p><p><a href="https://link.segmentfault.com/?enc=T90hSKN4sceTTWld%2BXwgbg%3D%3D.6XyeGA%2F7LYDytd3Ol5RlyLy%2FWGFTEyM1TT5JzR61JpxXxbuXg8UhfMmah9gdEf%2F8" rel="nofollow">更好的 java 重试框架 sisyphus 入门简介</a></p><p><a href="https://link.segmentfault.com/?enc=hzNh5iSYve0A%2B6U1g59IMA%3D%3D.dDlaX2JgDnnz6eF6oeL%2FtPTIHUa5bLHOU657YNLnA9w%2Fxnyr49Yf7V3S%2BR48t0m5" rel="nofollow">更好的 java 重试框架 sisyphus 配置的 2 种方式介绍</a></p><p><a href="https://link.segmentfault.com/?enc=AS1nRhMtTtcFMawUG3BOUA%3D%3D.iAE3doKZGSbdTcKafdYL01vOwuvYEjBcLj%2BjF6ZHZhQOAG3YkPFUbuAZ8F0MVjuG" rel="nofollow">更好的 java 重试框架 sisyphus 背后的故事</a></p><blockquote><a href="https://link.segmentfault.com/?enc=OTY30%2Fedz7KYkRohVGG8ow%3D%3D.wdbchLFwLm7KOvsCSw6cT4HVbyQO2lpiAz0QCk27pUXaACUkDE8WRD24jUQMmx91" rel="nofollow">java 重试框架 sisyphus 开源地址</a></blockquote><p>这一节让我们一起学习下 sisyphus 的 3 种使用方式。</p><h2>sisyphus 代理模板</h2><h3>目的</h3><p>为了便于用户更加方便地使用注解,同时又不依赖 spring。</p><p>提供基于代码模式+字节码增强实现的方式。</p><h2>使用案例</h2><h3>maven 引入</h3><p>引入注解相关模块。</p><pre><code class="xml"><dependency>
<groupId>com.github.houbb</groupId>
<artifactId>sisyphus-annotation</artifactId>
<version>0.0.9</version>
</dependency></code></pre><h3>定义测试方法</h3><p>以下测试代码可以参考 [spring-test]() 模块。</p><ul><li>MenuServiceImpl.java</li></ul><pre><code class="java">public class MenuServiceImpl {
public void queryMenu(long id) {
System.out.println("查询菜单...");
throw new RuntimeException();
}
@Retry
public void queryMenuRetry(long id) {
System.out.println("查询菜单...");
throw new RuntimeException();
}
}</code></pre><h3>测试</h3><p>使用 RetryTemplate 进行测试</p><h4>无重试注解的方法</h4><pre><code class="java">@Test(expected = RuntimeException.class)
public void templateTest() {
MenuServiceImpl menuService = RetryTemplate.getProxyObject(new MenuServiceImpl());
menuService.queryMenu(1);
}</code></pre><ul><li>日志信息</li></ul><pre><code>查询菜单...</code></pre><p>只请求了一次。</p><h4>有注解的方法</h4><pre><code class="java">@Test(expected = RuntimeException.class)
public void templateRetryTest() {
MenuServiceImpl menuService = RetryTemplate.getProxyObject(new MenuServiceImpl());
menuService.queryMenuRetry(1);
}</code></pre><ul><li>日志信息</li></ul><pre><code>查询菜单...
查询菜单...
查询菜单...</code></pre><h2>sisyphus spring 整合</h2><h3>目的</h3><p>类似于 spring-retry 框架,如果你使用 spring 框架,那么整合本项目将会非常简单。</p><p>注解的方式和过程式编程,二者尽可能的保持一致性,你想从一种方式变为另一种也比较简单。</p><p>想从 spring-retry 切换到本框架也很方便。</p><h3>maven 引入</h3><pre><code class="xml"><dependency>
<groupId>${project.groupId}</groupId>
<artifactId>sisyphus-spring</artifactId>
<version>${project.version}</version>
</dependency></code></pre><p>会默认引入 spring 以及 AOP 相关 jar。</p><h3>业务代码</h3><p>你可以参考 sisyphus-test 模块。</p><p>下面模拟非常常见的一些业务方法。</p><p>使用 <code>@Retry</code> 标识方法需要进行重试。</p><ul><li>SpringService.java</li></ul><pre><code class="java">public interface SpringService {
/**
* 查询示例代码
* @return 结果
*/
String query();
}</code></pre><ul><li>SpringServiceImpl.java</li></ul><pre><code class="java">import com.github.houbb.sisyphus.annotation.annotation.Retry;
import com.github.houbb.sisyphus.test.service.SpringService;
import org.springframework.stereotype.Service;
/**
* @author binbin.hou
* @since 0.0.4
*/
@Service
public class SpringServiceImpl implements SpringService {
@Override
@Retry
public String query() {
System.out.println("spring service query...");
throw new RuntimeException();
}
}</code></pre><h3>开启重试</h3><p>基于注解直接如下配置即可。</p><p>使用 <code>@EnableRetry</code> 标识需要开启重试。</p><pre><code class="java">@Configurable
@ComponentScan(basePackages = "com.github.houbb.sisyphus.test.service")
@EnableRetry
public class SpringConfig {
}</code></pre><h3>测试代码</h3><pre><code class="java">import com.github.houbb.sisyphus.test.config.SpringConfig;
import com.github.houbb.sisyphus.test.service.SpringService;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.test.context.ContextConfiguration;
import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;
/**
* @author binbin.hou
* @since 0.0.4
*/
@ContextConfiguration(classes = SpringConfig.class)
@RunWith(SpringJUnit4ClassRunner.class)
public class SpringServiceTest {
@Autowired
private SpringService springService;
@Test(expected = RuntimeException.class)
public void queryTest() {
springService.query();
}
}</code></pre><ul><li>日志信息</li></ul><pre><code>spring service query...
spring service query...
spring service query...</code></pre><h2>sisyphus springboot 整合</h2><h3>目的</h3><p>类似于 spring-retry 框架,如果你使用 springboot 框架,那么整合本项目将会非常简单。</p><p>注解的方式和过程式编程,二者尽可能的保持一致性,你想从一种方式变为另一种也比较简单。</p><p>想从 spring-retry 切换到本框架也很方便。</p><p>整体与 spring 整合类似,而且更加简单。</p><h3>maven 引入</h3><pre><code class="xml"><dependency>
<groupId>${project.groupId}</groupId>
<artifactId>sisyphus-springboot-starter</artifactId>
<version>${project.version}</version>
</dependency></code></pre><p>会默认引入 springboot 整合相关的依赖。</p><h3>业务代码</h3><p>你可以参考 sisyphus-test springboot 测试模块。</p><p>下面模拟非常常见的一些业务方法。</p><p>使用 <code>@Retry</code> 标识方法需要进行重试。</p><ul><li>SpringService.java</li></ul><pre><code class="java">public interface SpringService {
/**
* 查询示例代码
* @return 结果
*/
String query();
}</code></pre><ul><li>SpringServiceImpl.java</li></ul><pre><code class="java">import com.github.houbb.sisyphus.annotation.annotation.Retry;
import com.github.houbb.sisyphus.test.service.SpringService;
import org.springframework.stereotype.Service;
/**
* @author binbin.hou
* @since 0.0.4
*/
@Service
public class SpringServiceImpl implements SpringService {
@Override
@Retry
public String query() {
System.out.println("spring service query...");
throw new RuntimeException();
}
}</code></pre><h3>测试代码</h3><ul><li>SisyphusApplicationTest.java</li></ul><pre><code class="java">@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration(classes = SisyphusApplication.class)
public class SisyphusApplicationTest {
@Autowired
private SpringService springService;
@Test(expected = RuntimeException.class)
public void queryTest() {
springService.query();
}
}</code></pre><p>其中 SisyphusApplication.java 代码如下:</p><p>就是最基本的 springboot 启动入口。</p><pre><code class="java">@SpringBootApplication
@ComponentScan(basePackages = "com.github.houbb.sisyphus.test.service")
public class SisyphusApplication {
public static void main(String[] args) {
SpringApplication.run(SisyphusApplication.class, args);
}
}</code></pre><ul><li>日志信息</li></ul><pre><code>spring service query...
spring service query...
spring service query...</code></pre><h2>小结</h2><p>3 种使用方式基本可以满足日常开发中的各种场景。</p><blockquote><a href="https://link.segmentfault.com/?enc=%2FqXus3Z0MeypFW45I1IVfA%3D%3D.MMclLHdwx88lbcdf3po9t4j4AY8Vj2AH6Z00FFe7Qeqa7DpwtQSMD8%2BeYr03VDUj" rel="nofollow">java 重试框架 sisyphus 开源地址</a></blockquote><p>希望本文对你有所帮助,如果喜欢,欢迎点赞收藏转发一波。</p><p>我是老马,期待与你的下次重逢。</p><p><img src="/img/remote/1460000040852518" alt="在这里插入图片描述" title="在这里插入图片描述"></p>
更好的 java 重试框架 sisyphus 配置的 2 种方式介绍
https://segmentfault.com/a/1190000040846089
2021-10-21T20:36:16+08:00
2021-10-21T20:36:16+08:00
老马啸西风
https://segmentfault.com/u/houbinbin
0
<h2>回顾</h2><p>我们前面学习了</p><p><a href="https://link.segmentfault.com/?enc=1uMuv8ruMF8sKN1w8rYjDw%3D%3D.vuCwJ0MhAoFEnZyFEBO94nG9Ffs98hCxArJLJ667OCQaeQ52AITcfHPHo1BbPtEZ" rel="nofollow">更好的 java 重试框架 sisyphus 入门简介</a></p><p><a href="https://link.segmentfault.com/?enc=zxxb2bRfh1DJDX%2BG7ie%2BHg%3D%3D.ZPf5wgTOdkaXLVXjqnxeuCsAWzNAfEHL8csXs%2Fkx4tfthdtjfgvsbkiugZiN8hjI" rel="nofollow">更好的 java 重试框架 sisyphus 背后的故事</a></p><p>这一节让我们一起学习下 sisyphus 基于函数式的配置和注解式的配置。</p><h2>函数式配置概览</h2><p>为了满足更加方便的配置,Retryer 类提供了许多可以配置的信息。</p><h3>默认配置</h3><pre><code class="java">/**
* 默认配置测试
*/
public void defaultConfigTest() {
Retryer.<String>newInstance()
.condition(RetryConditions.hasExceptionCause())
.retryWaitContext(RetryWaiter.<String>retryWait(NoRetryWait.class).context())
.maxAttempt(3)
.listen(RetryListens.noListen())
.recover(Recovers.noRecover())
.callable(new Callable<String>() {
@Override
public String call() throws Exception {
System.out.println("called...");
throw new RuntimeException();
}
}).retryCall();
}</code></pre><p>和下面的代码是等价的:</p><pre><code class="java">public void helloTest() {
Retryer.<String>newInstance()
.callable(new Callable<String>() {
@Override
public String call() throws Exception {
System.out.println("called...");
throw new RuntimeException();
}
}).retryCall();
}</code></pre><h2>方法说明</h2><h3>condition</h3><p>重试触发的条件,可以指定多个条件。</p><p>默认为抛出异常。</p><h3>retryWaitContext</h3><p>重试等待的策略,可以指定多个。</p><p>默认为不做任何等待。</p><h3>maxAttempt</h3><p>指定最大重试次数,包括第一次执行。</p><p>默认值:3 次。</p><h3>listen</h3><p>指定重试的监听实现,默认为不做监听。</p><h3>recover</h3><p>当重试完成之后,依然满足重试条件,则可以指定恢复的策略。</p><p>默认不做恢复。</p><h3>callable</h3><p>待重试执行的方法。</p><h3>retryCall</h3><p>触发重试执行。</p><h2>接口的详细介绍</h2><h3>接口及其实现</h3><p>所有的接口,都可以直接查看对应的子类实例。</p><h3>用户自定义</h3><p>基于替换的灵活性,用户可以实现接口,定义更符合自己业务的实现。</p><h2>sisyphus 注解</h2><p>配置具有很高的灵活性,但是对于开发人员的使用,就没有注解那样简单灵活。</p><p>所以本框架也实现了基于注解的重试。</p><h3>设计的规范</h3><p>保证接口和注解二者的统一性。</p><h3>maven 引入</h3><pre><code class="xml"><dependency>
<groupId>${project.groupId}</groupId>
<artifactId>sisyphus-annotation</artifactId>
<version>${project.version}</version>
</dependency></code></pre><h2>注解</h2><p>核心注解主要有两个。</p><h3>Retry</h3><p>用于指定重试的相关配置。</p><pre><code class="java">/**
* 重试注解
* 1. 实际需要,只允许放在方法上。
* 2. 如果放在接口上,是否所有的子类都生效?为了简单明确,不提供这种实现。
* 3. 保持注解和接口的一致性。{@link com.github.houbb.sisyphus.api.core.Retry} 接口
* @author binbin.hou
* @since 0.0.3
*/
@Documented
@Inherited
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
@RetryAble(DefaultRetryAbleHandler.class)
public @interface Retry {
/**
* 重试类实现
* @return 重试
* @since 0.0.5
*/
Class<? extends com.github.houbb.sisyphus.api.core.Retry> retry() default DefaultRetry.class;
/**
* 最大尝试次数
* 1. 包含方法第一次正常执行的次数
* @return 次数
*/
int maxAttempt() default 3;
/**
* 重试触发的场景
* @return 重试触发的场景
*/
Class<? extends RetryCondition> condition() default ExceptionCauseRetryCondition.class;
/**
* 监听器
* 1. 默认不进行监听
* @return 监听器
*/
Class<? extends RetryListen> listen() default NoRetryListen.class;
/**
* 恢复操作
* 1. 默认不进行任何恢复操作
* @return 恢复操作对应的类
*/
Class<? extends Recover> recover() default NoRecover.class;
/**
* 等待策略
* 1. 支持指定多个,如果不指定,则不进行任何等待,
* @return 等待策略
*/
RetryWait[] waits() default {};
}</code></pre><h3>RetryWait</h3><p>用于指定重试的等待策略。</p><pre><code class="java">package com.github.houbb.sisyphus.annotation.annotation;
import com.github.houbb.sisyphus.annotation.annotation.metadata.RetryWaitAble;
import com.github.houbb.sisyphus.annotation.handler.impl.DefaultRetryWaitAbleHandler;
import com.github.houbb.sisyphus.core.constant.RetryWaitConst;
import com.github.houbb.sisyphus.core.support.wait.NoRetryWait;
import java.lang.annotation.*;
/**
* 重试等待策略
* 1. 为了对应重试策略,所有的内置注解应该实现当前的注解。
* 2. 是否允许自定义注解?
*
* 当注解+对象同时出现的时候,视为组合。
*
* @author binbin.hou
* @since 0.0.3
*/
@Retention(RetentionPolicy.RUNTIME)
@Inherited
@Documented
@Target(ElementType.ANNOTATION_TYPE)
@RetryWaitAble(DefaultRetryWaitAbleHandler.class)
public @interface RetryWait {
/**
* 默认值
* 1. fixed 模式,则对应固定等待时间
* 2. 递增
* @return 默认值
*/
long value() default RetryWaitConst.VALUE_MILLS;
/**
* 最小值
* @return 最小值
*/
long min() default RetryWaitConst.MIN_MILLS;
/**
* 最大值
* @return 最大值
*/
long max() default RetryWaitConst.MAX_MILLS;
/**
* 影响因数
* 1. 递增重试,默认为 {@link RetryWaitConst#INCREASE_MILLS_FACTOR}
* 2. 指数模式。默认为 {@link RetryWaitConst#MULTIPLY_FACTOR}
* @return 影响因数
*/
double factor() default Double.MIN_VALUE;
/**
* 指定重试的等待时间 class 信息
* @return 重试等待时间 class
*/
Class<? extends com.github.houbb.sisyphus.api.support.wait.RetryWait> retryWait() default NoRetryWait.class;
}</code></pre><h2>注解的使用</h2><p>定义好了注解,肯定要有注解的相关使用。</p><p>关于注解的使用,主要有两种方式。</p><h3>Proxy+CGLIB</h3><p>基于代理模式和字节码增强。</p><p>如果是项目中没有使用 spring,直接使用这种方式比较方便。</p><h3>Spring-AOP</h3><p>可以和 spring 直接整合。</p><p>使用方式和 spring-retry 是一样的。</p><p>这些内容将放在下一节进行详细讲解。</p><h2>小结</h2><p>灵活的配置才能更加符合实际生产使用中的各种需求。</p><p>一般实际使用推荐使用注解的配置方式,非常的简单方便。</p><blockquote><a href="https://link.segmentfault.com/?enc=CfPxQLbFB0Jh%2BYFZ6z7eOA%3D%3D.0K%2BeWll9E8d0dFOWtZIvjgCNqPp%2B4VxRV8PhiE7%2FdAA%2BN39cn5KOMAhqQpEjOKCI" rel="nofollow">java 重试框架 sisyphus 开源地址</a></blockquote><p>希望本文对你有所帮助,如果喜欢,欢迎点赞收藏转发一波。</p><p>我是老马,期待与你的下次重逢。</p><p><img src="/img/remote/1460000040846091" alt="在这里插入图片描述" title="在这里插入图片描述"></p>
更好的 java 重试框架 sisyphus 入门简介
https://segmentfault.com/a/1190000040841228
2021-10-20T21:48:08+08:00
2021-10-20T21:48:08+08:00
老马啸西风
https://segmentfault.com/u/houbinbin
0
<h2>What is Sisyphus</h2><p><a href="https://link.segmentfault.com/?enc=N1kz2JK%2Fs%2BUqokB5Qs%2FRAg%3D%3D.wuIeZWkrHaDe9o90KC42tV%2BTTVbSvsNnotfbhnSi9NB08tTcPAXuTBgc6U%2BLL%2FoF" rel="nofollow">sisyphus </a> 综合了 spring-retry 和 gauva-retrying 的优势,使用起来也非常灵活。</p><h3>为什么选择这个名字</h3><p>我觉得重试做的事情和西西弗斯很相似。</p><p>一遍遍的重复,可能徒劳无功,但是乐此不疲。</p><p>人一定要想象西西弗斯的快乐。——加缪</p><h3>其他原因</h3><p>以前看了 <a href="https://link.segmentfault.com/?enc=MdVLTLN6Mu1rbyel26gtcA%3D%3D.xkV%2FGluM4fzL8Yv%2FTa1vljxC9pq9Iw%2Fnd8UuVsv4LBeMQcz0oaxmFgOMX9v%2BNF%2Fu" rel="nofollow">java retry</a> 的相关框架,<br>虽然觉得其中有很多不足之处。但是没有任何重复造轮子的冲动,觉得是徒劳无功的。</p><p>当然这段时间也看了 Netty 的接口设计,和 Hibernate-Validator 的接口设计,觉得非常的巧妙。</p><p>觉得把这些东西结合,可以写出一个还不错的框架,就写了起来。</p><p>至少,sisyphus 是快乐的。</p><h2>关于版本</h2><p>这次的框架版本采用了比较保守的方式,使用 <code>0.0.X</code>。</p><p>原因有两个:</p><p>(1)我认为前期出于实验阶段。代码并不成熟,自测也不充分。所以不适合用于生产。</p><p>(2)这样可以快速迭代,而不至于为了追求更好导致版本特性迟迟无法迭代。</p><h3>版本特性</h3><p>我用了 5 个版本,实现了主要的特性:</p><p>(1)基于 fluent 接口声明式调用</p><p>(2)基于 annotation 的代理实现</p><p>(3)spring 的整合实现</p><p>(4)自定义注解的实现</p><h3>未完成的工作</h3><ul><li>更方便的工具类。</li><li>使用文档</li><li>测试代码</li></ul><h3>感受</h3><p>想法是很容易产生的,但是想把它变成一个稳定的框架需要很长的时间锤炼。</p><h2>为什么选择 sisyphus</h2><p>作为开发者,我们一般都会选择比较著名的框架。</p><p>比如 guava-retrying spring-retry。</p><p>或者干脆自己写一个。</p><h2>为什么不是 guava-retrying/spring-retry</h2><p><a href="https://link.segmentfault.com/?enc=GjE1OxebfgdaoJE5Sf6Wsg%3D%3D.FDQV7cIrnUNHHbS%2BICwf1I5MTEo6iSrLaaqqcyyZjgllL9D1BpIUfFuZcJJ1mWNd" rel="nofollow">java retry</a> 这篇文章中我列举了常见的实现方式<br>以及上述的两种框架,也讲述了其中的不足。</p><h3>guava-retrying 优缺点</h3><h4>优点</h4><ul><li>使用灵活</li><li>fluent 优雅写法</li><li>提供足够多的实现</li></ul><h4>缺点</h4><ul><li>没有默认基于注解的实现</li><li>重试策略设计并不友好</li></ul><h3>spring-retry</h3><h4>优点</h4><ul><li>使用简单</li></ul><h4>缺点</h4><ul><li>重试条件单一</li><li>重试等待策略单一</li><li>无法自定义注解</li></ul><h2>为什么不自己写一个</h2><h3>个人感受</h3><p>我作为一名开发,平时说实在的,看到重试。</p><p>我肯定会偷懒写一个 for 循环,重试几次就结束了。</p><p>因为时间不允许。</p><p>如果你更勤快一点,就可以选择 spring-retry/guava-retrying。如果你熟悉他们的优缺点的话。</p><h3>如果你渴望创造</h3><p>sisyphus 所有的实现都是基于接口的。</p><p>你完全可以实现自己的实现,所有的东西基本完全可以被替换。</p><p>当然一些常见的策略实现,项目的基本框架都有详尽的注释,当做参考也可以有一点帮助。</p><h2>sisyphus 做的更多的事情</h2><h3>netty 的灵感</h3><p>参考了 netty 的设计,保证接口实现的一致性。</p><p>而且 sisyphus 还做了更多,还保证了接口和注解之间的一致性。</p><p>使用引导类,保证使用时的便利性,后期拓展的灵活性。</p><h3>hibernate-validator</h3><p>hibernate-validator 的作者是我知道为数不多的对于 java 注解应用很棒的开发者。(虽然所知甚少)</p><p>自定义注解就是从这个框架中学来的。</p><h3>与 spring 为伍</h3><p>spring 基本与我们的代码形影不离,所以你可以很简单的结合 spring.</p><p>就像你使用 spring-retry 一样。</p><h2>快速开始</h2><h3>需要</h3><p>jdk1.7+</p><p>maven 3.x+</p><h3>maven 引入</h3><p>sisyphus 使用 maven 管理 jar,</p><pre><code class="xml"><plugin>
<groupId>com.github.houbb</groupId>
<artifactId>sisyphus-core</artifactId>
<version>0.0.6</version>
</plugin></code></pre><h3>编码</h3><p>作为入门案例,我们首先介绍些简单灵活的声明式编程</p><pre><code class="java">public void helloTest() {
Retryer.<String>newInstance()
.callable(new Callable<String>() {
@Override
public String call() throws Exception {
System.out.println("called...");
throw new RuntimeException();
}
}).retryCall();
}</code></pre><h4>代码简介</h4><p><code>Retryer.<String>newInstance()</code> 创建引导类的实例,String 是 callable 也就是待重试方法的返回值类型。</p><p><code>callable()</code> 指定待重试的方法实现。</p><p><code>retryCall()</code> 触发重试调用。</p><h4>日志信息</h4><pre><code>called...
called...
called...</code></pre><p>以及一些异常信息。</p><h3>等价配置</h3><p>上面的配置其实有很多默认值,如下:</p><pre><code class="java">/**
* 默认配置测试
*/
@Test(expected = RuntimeException.class)
public void defaultConfigTest() {
Retryer.<String>newInstance()
.maxAttempt(3)
.listen(RetryListens.noListen())
.recover(Recovers.noRecover())
.condition(RetryConditions.hasExceptionCause())
.retryWaitContext(RetryWaiter.<String>retryWait(NoRetryWait.class).context())
.callable(new Callable<String>() {
@Override
public String call() throws Exception {
System.out.println("called...");
throw new RuntimeException();
}
}).retryCall();
}</code></pre><p>这些默认值都是可以配置的。</p><p>比如什么时候触发重试?重试几次?多久触发一次重试?这些都会在下面的章节进行详细讲解。</p><h2>小结</h2><p>本文简单介绍了重试框架的设计缘由,及其使用入门。</p><blockquote><a href="https://link.segmentfault.com/?enc=SgOjEAylCPNUkc7qdv49VQ%3D%3D.UjzK45381EF682ePJG%2FBWktJyX2impFYMB6BX9VdaGSn7Hvf6WQeLPwfU%2B0RjL0I" rel="nofollow">java 重试框架 sisyphus 开源地址 </a></blockquote><p>希望本文对你有所帮助,如果喜欢,欢迎点赞收藏转发一波。</p><p>我是老马,期待与你的下次重逢。</p><p><img src="/img/remote/1460000040841230" alt="在这里插入图片描述" title="在这里插入图片描述"></p>
更好的 java 重试框架 sisyphus 背后的故事
https://segmentfault.com/a/1190000040835650
2021-10-19T21:28:59+08:00
2021-10-19T21:28:59+08:00
老马啸西风
https://segmentfault.com/u/houbinbin
0
<p><a href="https://link.segmentfault.com/?enc=p7Sk1P6dTeXNrGzy9o6KvQ%3D%3D.QWkKghWZNM5kORFpFpItcvYPvXTrlx2BXUH0Pc%2BxLpuyERdcBfnbiHwSc%2FCsl5KY" rel="nofollow">sisyphus </a> 综合了 spring-retry 和 gauva-retrying 的优势,使用起来也非常灵活。</p><p>今天,让我们一起看一下西西弗斯背后的故事。</p><h2>情景导入</h2><h3>简单的需求</h3><p>产品经理:实现一个按条件,查询用户信息的服务。</p><p>小明:好的。没问题。</p><h4>代码</h4><ul><li>UserService.java</li></ul><pre><code class="java">public interface UserService {
/**
* 根据条件查询用户信息
* @param condition 条件
* @return User 信息
*/
User queryUser(QueryUserCondition condition);
}</code></pre><ul><li>UserServiceImpl.java</li></ul><pre><code class="java">public class UserServiceImpl implements UserService {
private OutService outService;
public UserServiceImpl(OutService outService) {
this.outService = outService;
}
@Override
public User queryUser(QueryUserCondition condition) {
outService.remoteCall();
return new User();
}
}</code></pre><h4>谈话</h4><p>项目经理:这个服务有时候会失败,你看下。</p><p>小明:<code>OutService</code> 在是一个 RPC 的外部服务,但是有时候不稳定。</p><p>项目经理:如果调用失败了,你可以调用的时候重试几次。你去看下重试相关的东西</p><h2>重试</h2><h3>重试作用</h3><p>对于重试是有场景限制的,不是什么场景都适合重试,比如参数校验不合法、写操作等(要考虑写是否幂等)都不适合重试。</p><p>远程调用超时、网络突然中断可以重试。在微服务治理框架中,通常都有自己的重试与超时配置,比如dubbo可以设置retries=1,timeout=500调用失败只重试1次,超过500ms调用仍未返回则调用失败。</p><p>比如外部 RPC 调用,或者数据入库等操作,<strong>如果一次操作失败,可以进行多次重试,提高调用成功的可能性</strong>。</p><h2>V1.0 支持重试版本</h2><h3>思考</h3><p>小明:我手头还有其他任务,这个也挺简单的。5 分钟时间搞定他。</p><h3>实现</h3><ul><li>UserServiceRetryImpl.java</li></ul><pre><code class="java">public class UserServiceRetryImpl implements UserService {
@Override
public User queryUser(QueryUserCondition condition) {
int times = 0;
OutService outService = new AlwaysFailOutServiceImpl();
while (times < RetryConstant.MAX_TIMES) {
try {
outService.remoteCall();
return new User();
} catch (Exception e) {
times++;
if(times >= RetryConstant.MAX_TIMES) {
throw new RuntimeException(e);
}
}
}
return null;
}
}</code></pre><h2>V1.1 代理模式版本</h2><h3>易于维护</h3><p>项目经理:你的代码我看了,功能虽然实现了,但是尽量写的易于维护一点。</p><p>小明:好的。(心想,是说要写点注释什么的?)</p><h3>代理模式</h3><p>为其他对象提供一种代理以控制对这个对象的访问。</p><p>在某些情况下,一个对象不适合或者不能直接引用另一个对象,而代理对象可以在客户端和目标对象之间起到中介作用。</p><p>其特征是代理与委托类有同样的接口。</p><h3>实现</h3><p>小明想到以前看过的代理模式,心想用这种方式,<strong>原来的代码改动量较少,以后想改起来也方便些</strong>。</p><ul><li>UserServiceProxyImpl.java</li></ul><pre><code class="java">public class UserServiceProxyImpl implements UserService {
private UserService userService = new UserServiceImpl();
@Override
public User queryUser(QueryUserCondition condition) {
int times = 0;
while (times < RetryConstant.MAX_TIMES) {
try {
return userService.queryUser(condition);
} catch (Exception e) {
times++;
if(times >= RetryConstant.MAX_TIMES) {
throw new RuntimeException(e);
}
}
}
return null;
}
}</code></pre><h2>V1.2 动态代理模式</h2><h3>方便拓展</h3><p>项目经理:小明啊,这里还有个方法也是同样的问题。你也给加上重试吧。</p><p>小明:好的。</p><p>小明心想,我在写一个代理,但是转念冷静了下来,如果还有个服务也要重试怎么办呢?</p><ul><li>RoleService.java</li></ul><pre><code class="java">public interface RoleService {
/**
* 查询
* @param user 用户信息
* @return 是否拥有权限
*/
boolean hasPrivilege(User user);
}</code></pre><h3>代码实现</h3><ul><li>DynamicProxy.java</li></ul><pre><code class="java">public class DynamicProxy implements InvocationHandler {
private final Object subject;
public DynamicProxy(Object subject) {
this.subject = subject;
}
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
int times = 0;
while (times < RetryConstant.MAX_TIMES) {
try {
// 当代理对象调用真实对象的方法时,其会自动的跳转到代理对象关联的handler对象的invoke方法来进行调用
return method.invoke(subject, args);
} catch (Exception e) {
times++;
if (times >= RetryConstant.MAX_TIMES) {
throw new RuntimeException(e);
}
}
}
return null;
}
/**
* 获取动态代理
*
* @param realSubject 代理对象
*/
public static Object getProxy(Object realSubject) {
// 我们要代理哪个真实对象,就将该对象传进去,最后是通过该真实对象来调用其方法的
InvocationHandler handler = new DynamicProxy(realSubject);
return Proxy.newProxyInstance(handler.getClass().getClassLoader(),
realSubject.getClass().getInterfaces(), handler);
}
}</code></pre><ul><li>测试代码</li></ul><pre><code class="java">@Test
public void failUserServiceTest() {
UserService realService = new UserServiceImpl();
UserService proxyService = (UserService) DynamicProxy.getProxy(realService);
User user = proxyService.queryUser(new QueryUserCondition());
LOGGER.info("failUserServiceTest: " + user);
}
@Test
public void roleServiceTest() {
RoleService realService = new RoleServiceImpl();
RoleService proxyService = (RoleService) DynamicProxy.getProxy(realService);
boolean hasPrivilege = proxyService.hasPrivilege(new User());
LOGGER.info("roleServiceTest: " + hasPrivilege);
}</code></pre><h2>V1.3 动态代理模式增强</h2><h3>对话</h3><p>项目经理:小明,你动态代理的方式是挺会偷懒的,可是我们有的类没有接口。这个问题你要解决一下。</p><p>小明:好的。(谁?写服务竟然不定义接口)</p><ul><li>ResourceServiceImpl.java</li></ul><pre><code class="java">public class ResourceServiceImpl {
/**
* 校验资源信息
* @param user 入参
* @return 是否校验通过
*/
public boolean checkResource(User user) {
OutService outService = new AlwaysFailOutServiceImpl();
outService.remoteCall();
return true;
}
}</code></pre><h3>字节码技术</h3><p>小明看了下网上的资料,解决的办法还是有的。</p><ul><li>CGLIB</li></ul><p><a href="https://link.segmentfault.com/?enc=TC7xlh45CRgQF0IjVJ%2Bljw%3D%3D.PEgWBoKpvxCAcB9EFdA3m2nEwDgKTuhQ2g7qAaVrg7ml2o5X0LrNKBV5XoYrDE7m" rel="nofollow">CGLIB</a> 是一个功能强大、高性能和高质量的代码生成库,用于扩展JAVA类并在运行时实现接口。</p><ul><li>javassist</li></ul><p><a href="https://link.segmentfault.com/?enc=JxARo%2FvOdJhJrjCyZtCORQ%3D%3D.smP%2B9M%2FVihx6PyZTcbpcn4wQyuCagySMJpnEFKVxSeI%3D" rel="nofollow">javassist</a> (Java编程助手)使Java字节码操作变得简单。</p><p>它是Java中编辑字节码的类库;它允许Java程序在运行时定义新类,并在JVM加载类文件时修改类文件。</p><p>与其他类似的字节码编辑器不同,Javassist提供了两个级别的API:源级和字节码级。</p><p>如果用户使用源代码级API,他们可以编辑类文件,而不需要了解Java字节码的规范。</p><p>整个API只使用Java语言的词汇表进行设计。您甚至可以以源文本的形式指定插入的字节码;Javassist动态编译它。</p><p>另一方面,字节码级API允许用户直接编辑类文件作为其他编辑器。</p><ul><li>ASM</li></ul><p><a href="https://link.segmentfault.com/?enc=L1Pz4BAyAKhpc0%2BqV1jJig%3D%3D.PmFxYanBKntgyjGE98t8jeS214Wys7poWVs9YRn0OKI%3D" rel="nofollow">ASM</a> 是一个通用的Java字节码操作和分析框架。</p><p>它可以用来修改现有的类或动态地生成类,直接以二进制形式。</p><p>ASM提供了一些通用的字节码转换和分析算法,可以从这些算法中构建自定义复杂的转换和代码分析工具。</p><p>ASM提供与其他Java字节码框架类似的功能,但主要关注性能。</p><p>因为它的设计和实现都尽可能地小和快,所以非常适合在动态系统中使用(当然也可以以静态的方式使用,例如在编译器中)。</p><h3>实现</h3><p>小明看了下,就选择使用 CGLIB。</p><ul><li>CglibProxy.java</li></ul><pre><code class="java">public class CglibProxy implements MethodInterceptor {
@Override
public Object intercept(Object o, Method method, Object[] objects, MethodProxy methodProxy) throws Throwable {
int times = 0;
while (times < RetryConstant.MAX_TIMES) {
try {
//通过代理子类调用父类的方法
return methodProxy.invokeSuper(o, objects);
} catch (Exception e) {
times++;
if (times >= RetryConstant.MAX_TIMES) {
throw new RuntimeException(e);
}
}
}
return null;
}
/**
* 获取代理类
* @param clazz 类信息
* @return 代理类结果
*/
public Object getProxy(Class clazz){
Enhancer enhancer = new Enhancer();
//目标对象类
enhancer.setSuperclass(clazz);
enhancer.setCallback(this);
//通过字节码技术创建目标对象类的子类实例作为代理
return enhancer.create();
}
}</code></pre><ul><li>测试</li></ul><pre><code class="java">@Test
public void failUserServiceTest() {
UserService proxyService = (UserService) new CglibProxy().getProxy(UserServiceImpl.class);
User user = proxyService.queryUser(new QueryUserCondition());
LOGGER.info("failUserServiceTest: " + user);
}
@Test
public void resourceServiceTest() {
ResourceServiceImpl proxyService = (ResourceServiceImpl) new CglibProxy().getProxy(ResourceServiceImpl.class);
boolean result = proxyService.checkResource(new User());
LOGGER.info("resourceServiceTest: " + result);
}</code></pre><h2>V2.0 AOP 实现</h2><h3>对话</h3><p>项目经理:小明啊,最近我在想一个问题。不同的服务,重试的时候次数应该是不同的。因为服务对稳定性的要求各不相同啊。</p><p>小明:好的。(心想,重试都搞了一周了,今天都周五了。)</p><p>下班之前,小明一直在想这个问题。刚好周末,花点时间写个重试小工具吧。</p><h3>设计思路</h3><ul><li>技术支持</li></ul><p>spring</p><p>java 注解</p><ul><li>注解定义</li></ul><p>注解可在方法上使用,定义需要重试的次数</p><ul><li>注解解析</li></ul><p>拦截指定需要重试的方法,解析对应的重试次数,然后进行对应次数的重试。</p><h3>实现</h3><ul><li>Retryable.java</li></ul><pre><code class="java">@Target({ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface Retryable {
/**
* Exception type that are retryable.
* @return exception type to retry
*/
Class<? extends Throwable> value() default RuntimeException.class;
/**
* 包含第一次失败
* @return the maximum number of attempts (including the first failure), defaults to 3
*/
int maxAttempts() default 3;
}</code></pre><ul><li>RetryAspect.java</li></ul><pre><code class="java">@Aspect
@Component
public class RetryAspect {
@Pointcut("execution(public * com.github.houbb.retry.aop..*.*(..)) &&" +
"@annotation(com.github.houbb.retry.aop.annotation.Retryable)")
public void myPointcut() {
}
@Around("myPointcut()")
public Object around(ProceedingJoinPoint point) throws Throwable {
Method method = getCurrentMethod(point);
Retryable retryable = method.getAnnotation(Retryable.class);
//1. 最大次数判断
int maxAttempts = retryable.maxAttempts();
if (maxAttempts <= 1) {
return point.proceed();
}
//2. 异常处理
int times = 0;
final Class<? extends Throwable> exceptionClass = retryable.value();
while (times < maxAttempts) {
try {
return point.proceed();
} catch (Throwable e) {
times++;
// 超过最大重试次数 or 不属于当前处理异常
if (times >= maxAttempts ||
!e.getClass().isAssignableFrom(exceptionClass)) {
throw new Throwable(e);
}
}
}
return null;
}
private Method getCurrentMethod(ProceedingJoinPoint point) {
try {
Signature sig = point.getSignature();
MethodSignature msig = (MethodSignature) sig;
Object target = point.getTarget();
return target.getClass().getMethod(msig.getName(), msig.getParameterTypes());
} catch (NoSuchMethodException e) {
throw new RuntimeException(e);
}
}
}</code></pre><h3>方法的使用</h3><ul><li>fiveTimes()</li></ul><p>当前方法一共重试 5 次。<br>重试条件:服务抛出 <code>AopRuntimeExption</code></p><pre><code>@Override
@Retryable(maxAttempts = 5, value = AopRuntimeExption.class)
public void fiveTimes() {
LOGGER.info("fiveTimes called!");
throw new AopRuntimeExption();
}</code></pre><ul><li>测试日志</li></ul><pre><code>2018-08-08 15:49:33.814 INFO [main] com.github.houbb.retry.aop.service.impl.UserServiceImpl:66 - fiveTimes called!
2018-08-08 15:49:33.815 INFO [main] com.github.houbb.retry.aop.service.impl.UserServiceImpl:66 - fiveTimes called!
2018-08-08 15:49:33.815 INFO [main] com.github.houbb.retry.aop.service.impl.UserServiceImpl:66 - fiveTimes called!
2018-08-08 15:49:33.815 INFO [main] com.github.houbb.retry.aop.service.impl.UserServiceImpl:66 - fiveTimes called!
2018-08-08 15:49:33.815 INFO [main] com.github.houbb.retry.aop.service.impl.UserServiceImpl:66 - fiveTimes called!
java.lang.reflect.UndeclaredThrowableException
...</code></pre><h2>V3.0 spring-retry 版本</h2><h3>对话</h3><p>周一来到公司,项目经理又和小明谈了起来。</p><p>项目经理:重试次数是满足了,但是重试其实应该讲究策略。比如调用外部,第一次失败,可以等待 5S 在次调用,如果又失败了,可以等待 10S 再调用。。。</p><p>小明:了解。</p><h3>思考</h3><p>可是今天周一,还有其他很多事情要做。</p><p>小明在想,没时间写这个呀。看看网上有没有现成的。</p><h3>spring-retry</h3><p><a href="https://link.segmentfault.com/?enc=IEhPqduKbjnEueJsej%2FiTQ%3D%3D.d6gD%2Fl3kuCS0r%2By%2FmCqT%2FY62CzpKKouC16AA4pdWj9g3zypEt6GvfpmVaEJE1j6q" rel="nofollow">Spring Retry</a> 为 Spring 应用程序提供了声明性重试支持。 它用于Spring批处理、Spring集成、Apache Hadoop(等等)的Spring。</p><p>在分布式系统中,为了保证数据分布式事务的强一致性,大家在调用RPC接口或者发送MQ时,针对可能会出现网络抖动请求超时情况采取一下重试操作。 大家用的最多的重试方式就是MQ了,但是如果你的项目中没有引入MQ,那就不方便了。</p><p>还有一种方式,是开发者自己编写重试机制,但是大多不够优雅。</p><h3>注解式使用</h3><ul><li>RemoteService.java</li></ul><p>重试条件:遇到 <code>RuntimeException</code> </p><p>重试次数:3</p><p>重试策略:重试的时候等待 5S, 后面时间依次变为原来的 2 倍数。</p><p>熔断机制:全部重试失败,则调用 <code>recover()</code> 方法。</p><pre><code class="java">@Service
public class RemoteService {
private static final Logger LOGGER = LoggerFactory.getLogger(RemoteService.class);
/**
* 调用方法
*/
@Retryable(value = RuntimeException.class,
maxAttempts = 3,
backoff = @Backoff(delay = 5000L, multiplier = 2))
public void call() {
LOGGER.info("Call something...");
throw new RuntimeException("RPC调用异常");
}
/**
* recover 机制
* @param e 异常
*/
@Recover
public void recover(RuntimeException e) {
LOGGER.info("Start do recover things....");
LOGGER.warn("We meet ex: ", e);
}
}</code></pre><ul><li>测试</li></ul><pre><code class="java">@RunWith(SpringRunner.class)
@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.NONE)
public class RemoteServiceTest {
@Autowired
private RemoteService remoteService;
@Test
public void test() {
remoteService.call();
}
}</code></pre><ul><li>日志</li></ul><pre><code>2018-08-08 16:03:26.409 INFO 1433 --- [ main] c.g.h.r.spring.service.RemoteService : Call something...
2018-08-08 16:03:31.414 INFO 1433 --- [ main] c.g.h.r.spring.service.RemoteService : Call something...
2018-08-08 16:03:41.416 INFO 1433 --- [ main] c.g.h.r.spring.service.RemoteService : Call something...
2018-08-08 16:03:41.418 INFO 1433 --- [ main] c.g.h.r.spring.service.RemoteService : Start do recover things....
2018-08-08 16:03:41.425 WARN 1433 --- [ main] c.g.h.r.spring.service.RemoteService : We meet ex:
java.lang.RuntimeException: RPC调用异常
at com.github.houbb.retry.spring.service.RemoteService.call(RemoteService.java:38) ~[classes/:na]
...</code></pre><p>三次调用的时间点:</p><pre><code>2018-08-08 16:03:26.409
2018-08-08 16:03:31.414
2018-08-08 16:03:41.416</code></pre><h3>缺陷</h3><p>spring-retry 工具虽能优雅实现重试,但是存在两个不友好设计:</p><p>一个是重试实体限定为 <code>Throwable</code> 子类,说明重试针对的是可捕捉的功能异常为设计前提的,但是我们希望依赖某个数据对象实体作为重试实体,<br>但 sping-retry框架必须强制转换为Throwable子类。</p><p>另一个就是重试根源的断言对象使用的是 doWithRetry 的 Exception 异常实例,不符合正常内部断言的返回设计。</p><p>Spring Retry 提倡以注解的方式对方法进行重试,重试逻辑是同步执行的,重试的“失败”针对的是Throwable,<br>如果你要以返回值的某个状态来判定是否需要重试,可能只能通过自己判断返回值然后显式抛出异常了。</p><p><code>@Recover</code> 注解在使用时无法指定方法,如果一个类中多个重试方法,就会很麻烦。</p><h2>guava-retrying</h2><h3>谈话</h3><p>小华:我们系统也要用到重试</p><p>项目经理:小明前段时间用了 spring-retry,分享下应该还不错</p><p>小明:spring-retry 基本功能都有,但是<strong>必须是基于异常来进行控制</strong>。如果你要以返回值的某个状态来判定是否需要重试,可能只能通过自己判断返回值然后显式抛出异常了。</p><p>小华:我们项目中想根据对象的属性来进行重试。你可以看下 guava-retry,我很久以前用过,感觉还不错。</p><p>小明:好的。</p><h3>guava-retrying</h3><p><a href="https://link.segmentfault.com/?enc=MQm0Bavucz%2BH3dSSFMNX4g%3D%3D.JqIEg30L0mU%2F%2BQWGuF7QINw3Igo13UNj2oGPK%2FAnVJ93oJb77u%2F5vi5V1kVlWEXR" rel="nofollow">guava-retrying</a> 模块提供了一种通用方法, 可以使用Guava谓词匹配增强的特定停止、重试和异常处理功能来重试任意Java代码。</p><ul><li>优势</li></ul><p>guava retryer工具与spring-retry类似,都是通过定义重试者角色来包装正常逻辑重试,但是Guava retryer有更优的策略定义,在支持重试次数和重试频度控制基础上,能够兼容支持多个异常或者自定义实体对象的重试源定义,让重试功能有更多的灵活性。</p><p>Guava Retryer也是线程安全的,入口调用逻辑采用的是 <code>java.util.concurrent.Callable</code> 的 <code>call()</code> 方法</p><h3>代码例子</h3><h4>入门案例</h4><p>遇到异常之后,重试 3 次停止</p><ul><li>HelloDemo.java</li></ul><pre><code class="java">public static void main(String[] args) {
Callable<Boolean> callable = new Callable<Boolean>() {
@Override
public Boolean call() throws Exception {
// do something useful here
LOGGER.info("call...");
throw new RuntimeException();
}
};
Retryer<Boolean> retryer = RetryerBuilder.<Boolean>newBuilder()
.retryIfResult(Predicates.isNull())
.retryIfExceptionOfType(IOException.class)
.retryIfRuntimeException()
.withStopStrategy(StopStrategies.stopAfterAttempt(3))
.build();
try {
retryer.call(callable);
} catch (RetryException | ExecutionException e) {
e.printStackTrace();
}
}</code></pre><ul><li>日志</li></ul><pre><code>2018-08-08 17:21:12.442 INFO [main] com.github.houbb.retry.guava.HelloDemo:41 - call...
com.github.rholder.retry.RetryException: Retrying failed to complete successfully after 3 attempts.
2018-08-08 17:21:12.443 INFO [main] com.github.houbb.retry.guava.HelloDemo:41 - call...
2018-08-08 17:21:12.444 INFO [main] com.github.houbb.retry.guava.HelloDemo:41 - call...
at com.github.rholder.retry.Retryer.call(Retryer.java:174)
at com.github.houbb.retry.guava.HelloDemo.main(HelloDemo.java:53)
Caused by: java.lang.RuntimeException
at com.github.houbb.retry.guava.HelloDemo$1.call(HelloDemo.java:42)
at com.github.houbb.retry.guava.HelloDemo$1.call(HelloDemo.java:37)
at com.github.rholder.retry.AttemptTimeLimiters$NoAttemptTimeLimit.call(AttemptTimeLimiters.java:78)
at com.github.rholder.retry.Retryer.call(Retryer.java:160)
... 1 more</code></pre><h2>总结</h2><h3>优雅重试共性和原理</h3><p>正常和重试优雅解耦,重试断言条件实例或逻辑异常实例是两者沟通的媒介。</p><p>约定重试间隔,差异性重试策略,设置重试超时时间,进一步保证重试有效性以及重试流程稳定性。</p><p>都使用了命令设计模式,通过委托重试对象完成相应的逻辑操作,同时内部封装实现重试逻辑。</p><p>spring-retry 和 guava-retry 工具都是线程安全的重试,能够支持并发业务场景的重试逻辑正确性。</p><h3>优雅重试适用场景</h3><p>功能逻辑中存在不稳定依赖场景,需要使用重试获取预期结果或者尝试重新执行逻辑不立即结束。比如远程接口访问,数据加载访问,数据上传校验等等。</p><p>对于异常场景存在需要重试场景,同时希望把正常逻辑和重试逻辑解耦。</p><p>对于需要基于数据媒介交互,希望通过重试轮询检测执行逻辑场景也可以考虑重试方案。</p><h3>谈话</h3><p>项目经理:我觉得 guava-retry 挺好的,就是不够方便。小明啊,你给封装个基于注解的吧。</p><p>小明:……</p><h2>更好的实现</h2><p>于是小明含泪写下了 sisyphus.</p><blockquote><a href="https://link.segmentfault.com/?enc=jKPf7mEtNpu6lqlueBsMWw%3D%3D.5V76rFPZMvKXYnSsWzXNLinFFXTCeE02O8OGQ3sCOz8FXCiApyL7PiE9pxIqCIqw" rel="nofollow">java 重试框架——sisyphus </a></blockquote><p>希望本文对你有所帮助,如果喜欢,欢迎点赞收藏转发一波。</p><p>我是老马,期待与你的下次重逢。</p><p><img src="/img/remote/1460000040835652" alt="在这里插入图片描述" title="在这里插入图片描述"></p>
java 文档自动生成的神器 idoc
https://segmentfault.com/a/1190000040814148
2021-10-14T23:15:02+08:00
2021-10-14T23:15:02+08:00
老马啸西风
https://segmentfault.com/u/houbinbin
0
<h2>写文档</h2><p>作为一名开发者,每个人都要写代码。</p><p>工作中,几乎每一位开发者都要写文档。</p><p>因为工作是人和人的协作,产品要写需求文档,开发要写详细设计文档,接口文档。</p><p>可是,作为一个懒人,平时最讨厌的一件事情就是写文档。</p><p><img src="/img/remote/1460000040814150" alt="在这里插入图片描述" title="在这里插入图片描述"></p><p>写文档最令我不爽的地方是在于<strong>代码备注要改一遍,然后文档再改一遍。</strong></p><p>所有重复的劳作,都是对于我们宝贵摸鱼时间的最大浪费。</p><p>于是,我就常常想,能不能只写一遍呢?</p><h2>i-doc 项目简介</h2><p><a href="https://link.segmentfault.com/?enc=HB56qKkJv02T%2Fa8LYcqZSA%3D%3D.gGVvsc2DiTAV9aPe%2BgVDZIcvFKVEqeXU65jCRxCjdsY%3D" rel="nofollow">idoc</a> 为 java 项目生成项目文档。</p><p>基于原生的 java 注释,尽可能的生成简介的文档。用户可以自定义自己的模板,生成自己需要的文档。</p><p>实现原理:基于 maven 插件,类似于 javadoc。可以更加灵活,允许用户自定义。</p><h3>特性</h3><p>(1)基于 maven 项目生成包含大部分信息的元数据</p><p>(2)默认支持 markdown 简化文档的生成,支持自定义模板</p><p>(3)支持用户自定义文档生成器</p><p>(4)支持用户自定生成文档的类过滤器</p><p>(5)添加字段类型别名,支持用户自定义</p><h2>快速入门</h2><h3>需要</h3><p>jdk1.8+</p><p>maven 3.x+</p><h3>maven 引入</h3><p>使用 maven 引入当前 idoc 插件。</p><pre><code class="xml"><build>
<plugins>
<plugin>
<groupId>com.github.houbb</groupId>
<artifactId>idoc-core</artifactId>
<version>0.3.0</version>
</plugin>
</plugins>
</build></code></pre><h3>测试对象的创建</h3><p>为了演示文档,我们创建了一个 Address 对象。</p><pre><code class="java">package com.github.houbb.idoc.test.model;
/**
* 地址
* @author binbin.hou
* @since 0.0.1
*/
public class Address {
/**
* 城市
*/
private String country;
/**
* 街道
*/
private String street;
public String getCountry() {
return country;
}
public void setCountry(String country) {
this.country = country;
}
public String getStreet() {
return street;
}
public void setStreet(String street) {
this.street = street;
}
}
</code></pre><h3>执行插件</h3><pre><code>mvn com.github.houbb:idoc-core:0.3.0:idoc</code></pre><h3>命令行日志信息</h3><pre><code>[INFO] ------------------------------------ Start generate doc
[INFO] 共计 【1】 个文件待处理,请耐心等待。进度如下:
==================================================================================================== 100%
[INFO] Generator doc with docGenerator: com.github.houbb.idoc.core.api.generator.ConsoleDocGenerator
[INFO] ------------------------------------ 文档信息如下:
[类名] com.github.houbb.idoc.test.model.Address
[类信息] {"comment":"地址","docAnnotationList":[],"docFieldList":[{"comment":"城市","name":"country","type":"java.lang.String"},{"comment":"街道","name":"street","type":"java.lang.String"}],"docMethodList":[{"docMethodParameterList":[],"docMethodReturn":{"fullName":"java.lang.String","name":"String","packageName":"java.lang"},"docTagList":[],"exceptionList":[],"modifiers":["public"],"name":"getCountry","seeList":[],"signature":"getCountry()"},{"docMethodParameterList":[{"docAnnotationList":[],"name":"country","type":"java.lang.String"}],"docMethodReturn":{},"docTagList":[],"exceptionList":[],"modifiers":["public"],"name":"setCountry","seeList":[],"signature":"setCountry(country)"},{"docMethodParameterList":[],"docMethodReturn":{"fullName":"java.lang.String","name":"String","packageName":"java.lang"},"docTagList":[],"exceptionList":[],"modifiers":["public"],"name":"getStreet","seeList":[],"signature":"getStreet()"},{"docMethodParameterList":[{"docAnnotationList":[],"name":"street","type":"java.lang.String"}],"docMethodReturn":{},"docTagList":[],"exceptionList":[],"modifiers":["public"],"name":"setStreet","seeList":[],"signature":"setStreet(street)"}],"docTagList":[{"lineNum":5,"name":"author","parameters":["binbin.hou"],"value":"binbin.hou"},{"lineNum":6,"name":"since","parameters":["0.0.1"],"value":"0.0.1"}],"fullName":"com.github.houbb.idoc.test.model.Address","modifiers":["public"],"name":"Address","packageName":"com.github.houbb.idoc.test.model"}
[INFO] ------------------------------------ Finish generate doc</code></pre><h3>更多生成方式</h3><p>当然,你可以发现这里只是把元数据进行输出到控台,意义不大。</p><p>我们可以根据需求,自定义实现生成类。</p><p>比如下面的方式,可以使用内置的 <code>MarkdownDocGenerator</code> 输出到 markdown 文件。</p><pre><code class="xml"><plugin>
<groupId>com.github.houbb</groupId>
<artifactId>idoc-core</artifactId>
<version>0.3.0</version>
<configuration>
<generates>
<generate>com.github.houbb.idoc.ftl.api.generator.MarkdownDocGenerator</generate>
</generates>
</configuration>
<dependencies>
<dependency>
<groupId>com.github.houbb</groupId>
<artifactId>idoc-ftl</artifactId>
<version>0.3.0</version>
</dependency>
</dependencies>
</plugin></code></pre><p>效果可以参考:</p><blockquote><a href="https://link.segmentfault.com/?enc=OJ5anY12cmXjfdbvADbgAQ%3D%3D.r9HZCXf69nWHOc1aAHn8d8fICOzBYG86iEgkh26zR4NswsftbkxLknrGAjD0Q4xx4Lmf9%2BHeQwd9yvbJjtOLmXNwv%2FrDjI86So2j%2BYZ3%2BybdxfdIB0elzC0jquTlROC1" rel="nofollow">heaven 文档目录</a></blockquote><p>ps: <a href="https://link.segmentfault.com/?enc=NhvTciXk6ZzeU9ECNDV2xw%3D%3D.stcnI2UdeWj5k5BKt8PNpQjJI97RNGgoSYQ50fnPQT8%3D" rel="nofollow">heaven</a> 项目是个人整理了多年的工具包,几百个类,手写文档估计要很久。</p><h2>设计初衷</h2><h3>节约时间</h3><p>Java 文档一直是一个大问题。</p><p>很多项目不写文档,即使写文档,对于开发人员来说也是非常痛苦的。</p><p>不写文档的缺点自不用多少,手动写文档的缺点也显而易见:</p><ol><li>非常浪费时间,而且会出错。</li><li>无法保证及时更新。代码已经变了,但是文档还要同步修改。需要强制人来维护这一种一致性。这很难。</li></ol><h3>为什么不是 swagger-ui</h3><p>java 的文档有几类:</p><ol><li>jdk 自带的 doc 生成。这个以前实践给别人用过,别人用 C#,看到 java 的默认文档感觉很痛苦。</li></ol><p>就算是我们 java 开发者,也很讨厌看 jdk 的文档。看着不美观,也很累。</p><ol start="2"><li>swagger-ui 是基于 java 注解的文档生成工具。相对而言比较优雅,也非常强大。</li></ol><p>但是缺点也是有的。开发人员要写 jdk 原来的注释+注解。注解太多,导致写起来也很痛苦,大部分开发者后来还是选择了放弃。</p><p>那么问题来了?我们怎么办才能尽可能的让开发人员,和文档阅读人员都乐于接受呢?</p><p>jdk 自带的 doc 就是基于 maven 插件的,本项目也是。</p><p>区别如下:</p><ol><li>尽可能的保证和 Java 原生注释一致,让开发者很容易就可以使用。</li><li>尽可能的信息全面,但是文档简洁。让文档的阅读者享受到等同于手写文档的体验。</li><li>将信息的获取和生成区分开。方便用户自己定义自己的输出方式。</li></ol><h2>参数配置说明</h2><p>为了更加灵活的实现文档的生成和文档元数据的生成,提供如下参数</p><h3>插件配置属性简介</h3><table><thead><tr><th align="left">属性</th><th align="left">是否必填</th><th align="left">说明</th><th align="left">默认值</th><th align="left">备注</th></tr></thead><tbody><tr><td align="left">encoding</td><td align="left">否</td><td align="left">项目编码</td><td align="left">UTF-8</td><td align="left"> </td></tr><tr><td align="left">includes</td><td align="left">否</td><td align="left">元数据包含的文件信息</td><td align="left"><code>**\/*.java</code></td><td align="left">默认扫描所有 java 文件</td></tr><tr><td align="left">excludes</td><td align="left">否</td><td align="left">元数据排除的文件信息</td><td align="left">无</td><td align="left">默认不排除</td></tr><tr><td align="left">isOverwriteWhenExists</td><td align="left">否</td><td align="left">文档存在时是否覆盖</td><td align="left">true</td><td align="left"> </td></tr><tr><td align="left">isAllInOne</td><td align="left">否</td><td align="left">所有类信息是否生成单个文档</td><td align="left">true</td><td align="left">命令行文档生成器,此属性无意义。</td></tr><tr><td align="left">generates</td><td align="left">否</td><td align="left">文档生成类</td><td align="left">命令行文档生成信息</td><td align="left">可以同时指定多个。类名全称。用户自定义参见 <code>com.github.houbb.idoc.api.core.genenrator.IDocGenerator</code></td></tr><tr><td align="left">generateFilters</td><td align="left">否</td><td align="left">文档生成类过滤器</td><td align="left">无</td><td align="left">可以同时指定多个。类名全称。用户自定义参见 <code>com.github.houbb.idoc.api.core.filter.IDocGenerateFilter</code></td></tr><tr><td align="left">targetDir</td><td align="left">否</td><td align="left">生成目标文件目录</td><td align="left">无</td><td align="left">自定义指定文档生成的文件夹</td></tr></tbody></table><h3>isAllInOne</h3><p>简单的文档,建议直接生成在一个文件。</p><p>如果较为复杂,则可以设为 false,则会按照</p><h3>generates 相关问题</h3><p>默认的命令行文档,主要用于演示和信息查看,不具有实际意义。</p><p>建议引入 <code>idoc-ftl</code> 模块,使用 <code>MarkdownDocGenerator</code> 生成器。</p><p>可以同时指定多个。</p><p>可引入 <code>idoc-api</code> 自行定义。</p><h3>generateFilters 建议</h3><p>实际的文档,主要关心定义的方法。</p><p>我们可以针对 DocClass 的包名,过滤只生成 Service 方法文档。</p><p>如果是在以前的基础上,则可以加上 <code>@since</code> <code>@version</code> 等信息的过滤。</p><p>可以同时指定多个。</p><p>可引入 <code>idoc-api</code> 自行定义。</p><h2>自定义 Filter</h2><p>可以参考当前项目的 <code>idoc-test</code> 模块。 </p><p>整体配置如下:</p><pre><code class="xml"><build>
<plugins>
<plugin>
<groupId>com.github.houbb</groupId>
<artifactId>idoc-core</artifactId>
<version>0.3.0</version>
<configuration>
<isAllInOne>true</isAllInOne>
<generates>
<generate>com.github.houbb.idoc.ftl.api.generator.MarkdownDocGenerator</generate>
</generates>
<generateFilters>
<generateFilter>com.github.houbb.idoc.test.filter.MyGenerateFilter</generateFilter>
</generateFilters>
</configuration>
<dependencies>
<dependency>
<groupId>com.github.houbb</groupId>
<artifactId>idoc-test</artifactId>
<version>${project.version}</version>
</dependency>
</dependencies>
</plugin>
</plugins>
</build></code></pre><h3>指定文档生成器</h3><p>指定使用 Markdown 文档生成器,可以同时指定多个。</p><pre><code class="xml"><generates>
<generate>com.github.houbb.idoc.ftl.api.generator.MarkdownDocGenerator</generate>
</generates></code></pre><h4>引入包</h4><p>MarkdownDocGenerator 在 <code>idoc-ftl</code> 模块中,需要引入对应的依赖。</p><p>当然 <code>idoc-core</code> 默认依赖 <code>idoc-ftl</code>。</p><h3>指定文件生成类的过滤器</h3><p>如果不定义自己的类生成过滤器,则会生成所有的类信息。</p><p>一般使用中我们只关心 service 方法,所以添加了类的过滤实现。</p><p>实现如下:</p><h4>引入 idoc-api 包</h4><pre><code class="xml"><dependency>
<groupId>com.github.houbb</groupId>
<artifactId>idoc-api</artifactId>
<version>${project.version}</version>
</dependency></code></pre><h4>实现 IDocGenerateFilter</h4><pre><code class="java">package com.github.houbb.idoc.test.filter;
import com.github.houbb.idoc.api.core.filter.IDocGenerateFilter;
import com.github.houbb.idoc.api.model.metadata.DocClass;
/**
* 自定义生成过滤器
* @author binbin.hou
* @since 0.0.1
*/
public class MyGenerateFilter implements IDocGenerateFilter {
@Override
public boolean include(DocClass docClass) {
if("QueryUserService".equalsIgnoreCase(docClass.getName())) {
return true;
}
return false;
}
}</code></pre><h4>插件中配置使用</h4><pre><code class="xml"><generateFilters>
<generateFilter>com.github.houbb.idoc.test.filter.MyGenerateFilter</generateFilter>
</generateFilters></code></pre><p>注意,也需要将你定义这个过滤器的 jar 添加依赖,否则无法找到对应的类信息。</p><pre><code class="xml"><dependencies>
<dependency>
<groupId>com.github.houbb</groupId>
<artifactId>idoc-test</artifactId>
<version>${project.version}</version>
</dependency>
</dependencies></code></pre><h2>类代码信息</h2><h3>User 信息</h3><pre><code class="java">/**
* 用户信息
* @author binbin.hou
* @since 0.0.1
*/
public class User {
/**
* 名称
* @require 是
* @remark 中文名称,请认真填写
*/
private String name;
/**
* 年龄
*/
private int age;
/**
* 生日
*/
private Date birthday;
/**
* 地址
*/
private List<Address> addressList;
/**
* 伴侣
*/
private User mate;
//...
}</code></pre><h4>i-doc 定义的标签</h4><p><code>@require</code> 表示当前字段是否必填,作为方法入参时。</p><p><code>@remark</code> 表示当前字段的备注信息。</p><h3>方法类信息</h3><ul><li>QueryUserService.java</li></ul><pre><code class="java">/**
* 查询用户服务类
* @author binbin.hou
* @since 0.0.1
*/
public interface QueryUserService {
/**
* 根据用户信息查询用户
* @param user 用户信息
* @return 结果
* @since 0.0.2,2019/02/12
*/
public User queryUser(final User user);
}</code></pre><h3>执行插件</h3><pre><code>mvn com.github.houbb:idoc-core:0.3.0:idoc</code></pre><ul><li>日志信息</li></ul><pre><code>[INFO] ------------------------------------ Start generate doc
[INFO] 共计 【4】 个文件待处理,请耐心等待。进度如下:
==================================================================================================== 100%
[INFO] Generator doc with docGenerator: com.github.houbb.idoc.ftl.api.generator.MarkdownDocGenerator
[INFO] Markdown 生成文档文件 all in one 路径: /Users/houbinbin/code/_github/idoc/idoc-test/src/main/resources/idoc-gen/idoc-test-全部文档.md
[INFO] ------------------------------------ Finish generate doc</code></pre><h3>文档信息</h3><p>当前文件路径日志会打印,比如我自己测试的为:</p><p><code>/Users/houbinbin/code/_github/idoc/idoc-test/src/main/resources/idoc-gen/idoc-test-全部文档.md</code></p><h2>文档生成效果</h2><p>参见文档:</p><blockquote><a>idoc-test-全部文档.md</a></blockquote><h2>字段类型别名支持</h2><p>可以参考当前项目的 <code>idoc-test</code> 模块。</p><h3>为什么需要</h3><p>有时候页面显示类型,希望更加友好。</p><p>所以系统内置了一些别名显示,也同时支持自定义别名。</p><h2>类型字段的别名</h2><h3>系统内置</h3><p>系统当前版本提供了常见的别名。</p><p>详情见 <code>com.github.houbb.idoc.core.util.JavaTypeAliasUtil</code></p><table><thead><tr><th align="left">类型</th><th align="left">别称</th></tr></thead><tbody><tr><td align="left">java.lang.Float</td><td align="left">浮点型</td></tr><tr><td align="left">java.lang.Double</td><td align="left">浮点型</td></tr><tr><td align="left">java.util.Date</td><td align="left">日期</td></tr><tr><td align="left">java.time.LocalDateTime</td><td align="left">日期时间</td></tr><tr><td align="left">java.util.Currency</td><td align="left">货币</td></tr><tr><td align="left">float</td><td align="left">浮点型</td></tr><tr><td align="left">java.lang.Integer</td><td align="left">整型</td></tr><tr><td align="left">long</td><td align="left">长整型</td></tr><tr><td align="left">java.math.BigDecimal</td><td align="left">数字</td></tr><tr><td align="left">java.lang.Character</td><td align="left">字符</td></tr><tr><td align="left">java.lang.Long</td><td align="left">长整型</td></tr><tr><td align="left">java.lang.Short</td><td align="left">短整型</td></tr><tr><td align="left">java.util.Map</td><td align="left">映射</td></tr><tr><td align="left">java.time.LocalTime</td><td align="left">时间</td></tr><tr><td align="left">java.lang.Boolean</td><td align="left">布尔值</td></tr><tr><td align="left">java.math.BigInteger</td><td align="left">数字</td></tr><tr><td align="left">java.lang.String</td><td align="left">字符串</td></tr><tr><td align="left">java.lang.Byte</td><td align="left">字节</td></tr><tr><td align="left">double</td><td align="left">浮点型</td></tr><tr><td align="left">byte</td><td align="left">字节</td></tr><tr><td align="left">java.util.Collection</td><td align="left">集合</td></tr><tr><td align="left">int</td><td align="left">整型</td></tr><tr><td align="left">java.util.List</td><td align="left">列表</td></tr><tr><td align="left">boolean</td><td align="left">布尔值</td></tr><tr><td align="left">java.time.LocalDate</td><td align="left">日期</td></tr><tr><td align="left">char</td><td align="left">字符</td></tr><tr><td align="left">short</td><td align="left">短整型</td></tr><tr><td align="left">void</td><td align="left">空</td></tr><tr><td align="left">array</td><td align="left">数组</td></tr></tbody></table><h3>自定义的方式</h3><p>可以通过 typeAlias 指定自定义的字段别称。</p><pre><code class="xml"><configuration>
<generateFilters>
<generateFilter>com.github.houbb.idoc.test.filter.MyGenerateFilter</generateFilter>
</generateFilters>
<isAllInOne>true</isAllInOne>
<typeAliases>
<typeAlias>
<key>java.lang.String</key>
<value>String自定义说明</value>
</typeAlias>
</typeAliases>
</configuration></code></pre><h3>优先级</h3><p>用户自定义的字段别名优先级高于系统默认。</p><p>后面定义的别名会直接覆盖前面的别名。</p><h2>测试代码演示</h2><h3>对象定义</h3><pre><code class="java">/**
* 别名测试
* @author binbin.hou
* @since 0.0.1
*/
public class TypeAliasSimpleBean {
/**
* 名称
*/
private String name;
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
}</code></pre><h3>测试日志</h3><p>运行测试日志如下:</p><pre><code>{"comment":"别名测试","docAnnotationList":[],"docFieldList":[{"comment":"名称","name":"name","type":"java.lang.String","typeAlias":"String自定义说明"}],"docMethodList":[{"docMethodParameterList":[],"docMethodReturn":{"fullName":"java.lang.String","name":"String","packageName":"java.lang"},"docTagList":[],"exceptionList":[],"modifiers":["public"],"name":"getName","seeList":[],"signature":"getName()"},{"docMethodParameterList":[{"docAnnotationList":[],"name":"name","type":"java.lang.String","typeAlias":"String自定义说明"}],"docMethodReturn":{},"docTagList":[],"exceptionList":[],"modifiers":["public"],"name":"setName","seeList":[],"signature":"setName(name)"}],"docTagList":[{"lineNum":5,"name":"author","parameters":["binbin.hou"],"value":"binbin.hou"},{"lineNum":6,"name":"since","parameters":["0.0.1"],"value":"0.0.1"}],"fullName":"com.github.houbb.idoc.test.model.TypeAliasSimpleBean","modifiers":["public"],"name":"TypeAliasSimpleBean","packageName":"com.github.houbb.idoc.test.model"}</code></pre><p>其中 typeAlias 就是字段类型的别名,我们可以用来更加友好的显示字段信息。</p><h2>其他的思考</h2><h3>自定义方式的便利性</h3><p>自定义的方式采用基于 xml 的方式是比较方便。</p><p>但是数量比较多的时候就没有那么方便,本来考虑添加对应的配置属性接口,权衡下还是使用了 xml 配置的方式。</p><h3>是否使用 comment 信息?</h3><p>如果一个字段,没有指定别名,是否使用 comment 信息做替代?</p><p>建议使用,当前版本不做处理。</p><ul><li>为什么使用</li></ul><p>比起冗长的类信息,大部分人更乐于看到解释。</p><p>如果是针对同构的系统(都是 java 语言),则可以理解。</p><p>如果是针对异构的系统(比如前台是 php),则不易于理解。</p><ul><li>为什么不处理</li></ul><p>大部分的接口都是常见字段, 性价比不高。</p><p>可能存在字段没有些 comment 的情况,会导致判断的复杂性。</p><h3>如果用户不想使用别名</h3><p>直接修改模板即可,使用原来的字段 <code>type</code> 属性即可。</p><h2>开源地址</h2><blockquote><a href="https://link.segmentfault.com/?enc=8Bj6AfL9wXtqtuHTDN2hDQ%3D%3D.TUdFzAphq2FqXmJEabCQzf7GTxAwq1hUrGFEXSRGLps%3D" rel="nofollow">https://github.com/houbb/idoc</a></blockquote><p>当然,这个项目还有很长的路要走。</p><p>如果喜欢,欢迎 fork star~</p><p><img src="/img/remote/1460000040814151" alt="在这里插入图片描述" title="在这里插入图片描述"></p>
java 从零开始手写 RPC (07)-timeout 超时处理
https://segmentfault.com/a/1190000040808170
2021-10-13T19:55:18+08:00
2021-10-13T19:55:18+08:00
老马啸西风
https://segmentfault.com/u/houbinbin
0
<p>《过时不候》</p><p>最漫长的莫过于等待</p><p>我们不可能永远等一个人</p><p>就像请求</p><p>永远等待响应</p><p><img src="/img/remote/1460000040808172" alt="在这里插入图片描述" title="在这里插入图片描述"></p><h2>超时处理</h2><p><a href="https://link.segmentfault.com/?enc=EfZyaH5bdL4d1xBEzmBUgA%3D%3D.U3UbSpCYxc%2FFTWoEIuqVtXscrx2pxCiToeqiib7Szw3vF8Q92jUwLjkFOakEqG%2F%2FBkwwUhJ%2FJT4U%2B167GG%2F2CQ%3D%3D" rel="nofollow">java 从零开始手写 RPC (01) 基于 socket 实现</a></p><p><a href="https://link.segmentfault.com/?enc=v%2BlavZkfSt4XCCwsx%2FGgag%3D%3D.FeJr2nKFZoPUbNdPzWEBC8yfoArPY4JQSFJaP0LuP7vaFGZK3MaIygDuilx3KXj5NRDxYUKUSjtJd02wJMYdpw%3D%3D" rel="nofollow">java 从零开始手写 RPC (02)-netty4 实现客户端和服务端</a></p><p><a href="https://link.segmentfault.com/?enc=uRkT%2FusichdXbFkgmSa7zA%3D%3D.kiK40KZOmp1MlzGzRAwxmx1rT7c0M4Et7is%2FmnVS2qk3CImvOD7BKKb2hGXBaTj8hK9ewPP9DnPmRMdL%2FGdoPA%3D%3D" rel="nofollow">java 从零开始手写 RPC (03) 如何实现客户端调用服务端?</a></p><p><a href="https://link.segmentfault.com/?enc=9p69Z%2FiC5gKJh6ymzwnsig%3D%3D.PuQbvljg0zC1uPg0QXulrSWGhZIwsUIj55eNJzk%2FKy9Dazamo2ejGlV0ClkhOfrT1uOEKukQ%2FcHtRFyLvvDfAg%3D%3D" rel="nofollow">java 从零开始手写 RPC (04) 序列化</a></p><p><a href="https://link.segmentfault.com/?enc=Kcx%2FiF75ERJOOTF8ddSj5Q%3D%3D.5bin359Xs8UmHXtkcYfYJCvWLGfY9HuX3rmocFod1XAco1I%2FB%2BU8Bo7Xma6o89o5Ci6ippQg%2FFD3I8wo%2BOl9ZQ%3D%3D" rel="nofollow">java 从零开始手写 RPC (05) 基于反射的通用化实现</a></p><h3>必要性</h3><p>前面我们实现了通用的 rpc,但是存在一个问题,同步获取响应的时候没有超时处理。</p><p>如果 server 挂掉了,或者处理太慢,客户端也不可能一直傻傻的等。</p><p>当外部的调用超过指定的时间后,就直接报错,避免无意义的资源消耗。</p><h3>思路</h3><p>调用的时候,将开始时间保留。</p><p>获取的时候检测是否超时。</p><p>同时创建一个线程,用来检测是否有超时的请求。</p><h2>实现</h2><h3>思路</h3><p>调用的时候,将开始时间保留。</p><p>获取的时候检测是否超时。</p><p>同时创建一个线程,用来检测是否有超时的请求。</p><h3>超时检测线程</h3><p>为了不影响正常业务的性能,我们另起一个线程检测调用是否已经超时。</p><pre><code class="java">package com.github.houbb.rpc.client.invoke.impl;
import com.github.houbb.heaven.util.common.ArgUtil;
import com.github.houbb.rpc.common.rpc.domain.RpcResponse;
import com.github.houbb.rpc.common.rpc.domain.impl.RpcResponseFactory;
import com.github.houbb.rpc.common.support.time.impl.Times;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
/**
* 超时检测线程
* @author binbin.hou
* @since 0.0.7
*/
public class TimeoutCheckThread implements Runnable{
/**
* 请求信息
* @since 0.0.7
*/
private final ConcurrentHashMap<String, Long> requestMap;
/**
* 请求信息
* @since 0.0.7
*/
private final ConcurrentHashMap<String, RpcResponse> responseMap;
/**
* 新建
* @param requestMap 请求 Map
* @param responseMap 结果 map
* @since 0.0.7
*/
public TimeoutCheckThread(ConcurrentHashMap<String, Long> requestMap,
ConcurrentHashMap<String, RpcResponse> responseMap) {
ArgUtil.notNull(requestMap, "requestMap");
this.requestMap = requestMap;
this.responseMap = responseMap;
}
@Override
public void run() {
for(Map.Entry<String, Long> entry : requestMap.entrySet()) {
long expireTime = entry.getValue();
long currentTime = Times.time();
if(currentTime > expireTime) {
final String key = entry.getKey();
// 结果设置为超时,从请求 map 中移除
responseMap.putIfAbsent(key, RpcResponseFactory.timeout());
requestMap.remove(key);
}
}
}
}</code></pre><p>这里主要存储请求,响应的时间,如果超时,则移除对应的请求。</p><h3>线程启动</h3><p>在 DefaultInvokeService 初始化时启动:</p><pre><code class="java">final Runnable timeoutThread = new TimeoutCheckThread(requestMap, responseMap);
Executors.newScheduledThreadPool(1)
.scheduleAtFixedRate(timeoutThread,60, 60, TimeUnit.SECONDS);</code></pre><h2>DefaultInvokeService</h2><p>原来的设置结果,获取结果是没有考虑时间的,这里加一下对应的判断。</p><h3>设置请求时间</h3><ul><li>添加请求 addRequest</li></ul><p>会将过时的时间直接放入 map 中。</p><p>因为放入是一次操作,查询可能是多次。</p><p>所以时间在放入的时候计算完成。</p><pre><code class="java">@Override
public InvokeService addRequest(String seqId, long timeoutMills) {
LOG.info("[Client] start add request for seqId: {}, timeoutMills: {}", seqId,
timeoutMills);
final long expireTime = Times.time()+timeoutMills;
requestMap.putIfAbsent(seqId, expireTime);
return this;
}</code></pre><h3>设置请求结果</h3><ul><li>添加响应 addResponse</li></ul><ol><li>如果 requestMap 中已经不存在这个请求信息,则说明可能超时,直接忽略存入结果。</li><li>此时检测是否出现超时,超时直接返回超时信息。</li><li>放入信息后,通知其他等待的所有进程。</li></ol><pre><code class="java">@Override
public InvokeService addResponse(String seqId, RpcResponse rpcResponse) {
// 1. 判断是否有效
Long expireTime = this.requestMap.get(seqId);
// 如果为空,可能是这个结果已经超时了,被定时 job 移除之后,响应结果才过来。直接忽略
if(ObjectUtil.isNull(expireTime)) {
return this;
}
//2. 判断是否超时
if(Times.time() > expireTime) {
LOG.info("[Client] seqId:{} 信息已超时,直接返回超时结果。", seqId);
rpcResponse = RpcResponseFactory.timeout();
}
// 这里放入之前,可以添加判断。
// 如果 seqId 必须处理请求集合中,才允许放入。或者直接忽略丢弃。
// 通知所有等待方
responseMap.putIfAbsent(seqId, rpcResponse);
LOG.info("[Client] 获取结果信息,seqId: {}, rpcResponse: {}", seqId, rpcResponse);
LOG.info("[Client] seqId:{} 信息已经放入,通知所有等待方", seqId);
// 移除对应的 requestMap
requestMap.remove(seqId);
LOG.info("[Client] seqId:{} remove from request map", seqId);
synchronized (this) {
this.notifyAll();
}
return this;
}</code></pre><h3>获取请求结果</h3><ul><li>获取相应 getResponse</li></ul><ol><li>如果结果存在,直接返回响应结果</li><li>否则进入等待。</li><li>等待结束后获取结果。</li></ol><pre><code class="java">@Override
public RpcResponse getResponse(String seqId) {
try {
RpcResponse rpcResponse = this.responseMap.get(seqId);
if(ObjectUtil.isNotNull(rpcResponse)) {
LOG.info("[Client] seq {} 对应结果已经获取: {}", seqId, rpcResponse);
return rpcResponse;
}
// 进入等待
while (rpcResponse == null) {
LOG.info("[Client] seq {} 对应结果为空,进入等待", seqId);
// 同步等待锁
synchronized (this) {
this.wait();
}
rpcResponse = this.responseMap.get(seqId);
LOG.info("[Client] seq {} 对应结果已经获取: {}", seqId, rpcResponse);
}
return rpcResponse;
} catch (InterruptedException e) {
throw new RpcRuntimeException(e);
}
}</code></pre><p>可以发现获取部分的逻辑没变,因为超时会返回一个超时对象:RpcResponseFactory.timeout();</p><p>这是一个非常简单的实现,如下:</p><pre><code class="java">package com.github.houbb.rpc.common.rpc.domain.impl;
import com.github.houbb.rpc.common.exception.RpcTimeoutException;
import com.github.houbb.rpc.common.rpc.domain.RpcResponse;
/**
* 响应工厂类
* @author binbin.hou
* @since 0.0.7
*/
public final class RpcResponseFactory {
private RpcResponseFactory(){}
/**
* 超时异常信息
* @since 0.0.7
*/
private static final DefaultRpcResponse TIMEOUT;
static {
TIMEOUT = new DefaultRpcResponse();
TIMEOUT.error(new RpcTimeoutException());
}
/**
* 获取超时响应结果
* @return 响应结果
* @since 0.0.7
*/
public static RpcResponse timeout() {
return TIMEOUT;
}
}</code></pre><p>响应结果指定一个超时异常,这个异常会在代理处理结果时抛出:</p><pre><code class="java">RpcResponse rpcResponse = proxyContext.invokeService().getResponse(seqId);
Throwable error = rpcResponse.error();
if(ObjectUtil.isNotNull(error)) {
throw error;
}
return rpcResponse.result();</code></pre><h2>测试代码</h2><h3>服务端</h3><p>我们故意把服务端的实现添加沉睡,其他保持不变。</p><pre><code class="java">public class CalculatorServiceImpl implements CalculatorService {
public CalculateResponse sum(CalculateRequest request) {
int sum = request.getOne()+request.getTwo();
// 故意沉睡 3s
try {
TimeUnit.SECONDS.sleep(3);
} catch (InterruptedException e) {
e.printStackTrace();
}
return new CalculateResponse(true, sum);
}
}</code></pre><h3>客户端</h3><p>设置对应的超时时间为 1S,其他不变:</p><pre><code class="java">public static void main(String[] args) {
// 服务配置信息
ReferenceConfig<CalculatorService> config = new DefaultReferenceConfig<CalculatorService>();
config.serviceId(ServiceIdConst.CALC);
config.serviceInterface(CalculatorService.class);
config.addresses("localhost:9527");
// 设置超时时间为1S
config.timeout(1000);
CalculatorService calculatorService = config.reference();
CalculateRequest request = new CalculateRequest();
request.setOne(10);
request.setTwo(20);
CalculateResponse response = calculatorService.sum(request);
System.out.println(response);
}</code></pre><p>日志如下:</p><pre><code>.log.integration.adaptors.stdout.StdOutExImpl' adapter.
[INFO] [2021-10-05 14:59:40.974] [main] [c.g.h.r.c.c.RpcClient.connect] - RPC 服务开始启动客户端
...
[INFO] [2021-10-05 14:59:42.504] [main] [c.g.h.r.c.c.RpcClient.connect] - RPC 服务启动客户端完成,监听地址 localhost:9527
[INFO] [2021-10-05 14:59:42.533] [main] [c.g.h.r.c.p.ReferenceProxy.invoke] - [Client] start call remote with request: DefaultRpcRequest{seqId='62e126d9a0334399904509acf8dfe0bb', createTime=1633417182525, serviceId='calc', methodName='sum', paramTypeNames=[com.github.houbb.rpc.server.facade.model.CalculateRequest], paramValues=[CalculateRequest{one=10, two=20}]}
[INFO] [2021-10-05 14:59:42.534] [main] [c.g.h.r.c.i.i.DefaultInvokeService.addRequest] - [Client] start add request for seqId: 62e126d9a0334399904509acf8dfe0bb, timeoutMills: 1000
[INFO] [2021-10-05 14:59:42.535] [main] [c.g.h.r.c.p.ReferenceProxy.invoke] - [Client] start call channel id: 00e04cfffe360988-000004bc-00000000-1178e1265e903c4c-7975626f
...
Exception in thread "main" com.github.houbb.rpc.common.exception.RpcTimeoutException
at com.github.houbb.rpc.common.rpc.domain.impl.RpcResponseFactory.<clinit>(RpcResponseFactory.java:23)
at com.github.houbb.rpc.client.invoke.impl.DefaultInvokeService.addResponse(DefaultInvokeService.java:72)
at com.github.houbb.rpc.client.handler.RpcClientHandler.channelRead0(RpcClientHandler.java:43)
at io.netty.channel.SimpleChannelInboundHandler.channelRead(SimpleChannelInboundHandler.java:105)
at io.netty.channel.AbstractChannelHandlerContext.invokeChannelRead(AbstractChannelHandlerContext.java:362)
at io.netty.channel.AbstractChannelHandlerContext.invokeChannelRead(AbstractChannelHandlerContext.java:348)
at io.netty.channel.AbstractChannelHandlerContext.fireChannelRead(AbstractChannelHandlerContext.java:340)
at io.netty.handler.logging.LoggingHandler.channelRead(LoggingHandler.java:241)
at io.netty.channel.AbstractChannelHandlerContext.invokeChannelRead(AbstractChannelHandlerContext.java:362)
at io.netty.channel.AbstractChannelHandlerContext.invokeChannelRead(AbstractChannelHandlerContext.java:348)
at io.netty.channel.AbstractChannelHandlerContext.fireChannelRead(AbstractChannelHandlerContext.java:340)
at io.netty.handler.codec.ByteToMessageDecoder.fireChannelRead(ByteToMessageDecoder.java:310)
at io.netty.handler.codec.ByteToMessageDecoder.channelRead(ByteToMessageDecoder.java:284)
at io.netty.channel.AbstractChannelHandlerContext.invokeChannelRead(AbstractChannelHandlerContext.java:362)
at io.netty.channel.AbstractChannelHandlerContext.invokeChannelRead(AbstractChannelHandlerContext.java:348)
at io.netty.channel.AbstractChannelHandlerContext.fireChannelRead(AbstractChannelHandlerContext.java:340)
at io.netty.channel.DefaultChannelPipeline$HeadContext.channelRead(DefaultChannelPipeline.java:1359)
at io.netty.channel.AbstractChannelHandlerContext.invokeChannelRead(AbstractChannelHandlerContext.java:362)
at io.netty.channel.AbstractChannelHandlerContext.invokeChannelRead(AbstractChannelHandlerContext.java:348)
at io.netty.channel.DefaultChannelPipeline.fireChannelRead(DefaultChannelPipeline.java:935)
at io.netty.channel.nio.AbstractNioByteChannel$NioByteUnsafe.read(AbstractNioByteChannel.java:138)
at io.netty.channel.nio.NioEventLoop.processSelectedKey(NioEventLoop.java:645)
at io.netty.channel.nio.NioEventLoop.processSelectedKeysOptimized(NioEventLoop.java:580)
at io.netty.channel.nio.NioEventLoop.processSelectedKeys(NioEventLoop.java:497)
at io.netty.channel.nio.NioEventLoop.run(NioEventLoop.java:459)
at io.netty.util.concurrent.SingleThreadEventExecutor$5.run(SingleThreadEventExecutor.java:858)
at io.netty.util.concurrent.DefaultThreadFactory$DefaultRunnableDecorator.run(DefaultThreadFactory.java:138)
at java.lang.Thread.run(Thread.java:748)
...
[INFO] [2021-10-05 14:59:45.615] [nioEventLoopGroup-2-1] [c.g.h.r.c.i.i.DefaultInvokeService.addResponse] - [Client] seqId:62e126d9a0334399904509acf8dfe0bb 信息已超时,直接返回超时结果。
[INFO] [2021-10-05 14:59:45.617] [nioEventLoopGroup-2-1] [c.g.h.r.c.i.i.DefaultInvokeService.addResponse] - [Client] 获取结果信息,seqId: 62e126d9a0334399904509acf8dfe0bb, rpcResponse: DefaultRpcResponse{seqId='null', error=com.github.houbb.rpc.common.exception.RpcTimeoutException, result=null}
[INFO] [2021-10-05 14:59:45.617] [nioEventLoopGroup-2-1] [c.g.h.r.c.i.i.DefaultInvokeService.addResponse] - [Client] seqId:62e126d9a0334399904509acf8dfe0bb 信息已经放入,通知所有等待方
[INFO] [2021-10-05 14:59:45.618] [nioEventLoopGroup-2-1] [c.g.h.r.c.i.i.DefaultInvokeService.addResponse] - [Client] seqId:62e126d9a0334399904509acf8dfe0bb remove from request map
[INFO] [2021-10-05 14:59:45.618] [nioEventLoopGroup-2-1] [c.g.h.r.c.c.RpcClient.channelRead0] - [Client] response is :DefaultRpcResponse{seqId='62e126d9a0334399904509acf8dfe0bb', error=null, result=CalculateResponse{success=true, sum=30}}
[INFO] [2021-10-05 14:59:45.619] [main] [c.g.h.r.c.i.i.DefaultInvokeService.getResponse] - [Client] seq 62e126d9a0334399904509acf8dfe0bb 对应结果已经获取: DefaultRpcResponse{seqId='null', error=com.github.houbb.rpc.common.exception.RpcTimeoutException, result=null}
...</code></pre><p>可以发现,超时异常。</p><h2>不足之处</h2><p>对于超时的处理可以拓展为双向的,比如服务端也可以指定超时限制,避免资源的浪费。</p><h2>小结</h2><p>为了便于大家学习,以上源码已经开源:</p><blockquote><a href="https://link.segmentfault.com/?enc=5%2FE0u2ORFiCpFjIKs62cYg%3D%3D.6bY7kuRbpxQ8GUxWa2Mh%2Bi1FOa%2BrYzxAhUJ%2FOPBwaRk%3D" rel="nofollow">https://github.com/houbb/rpc</a></blockquote><p>希望本文对你有所帮助,如果喜欢,欢迎点赞收藏转发一波。</p><p>我是老马,期待与你的下次重逢。</p><p><img src="/img/remote/1460000040808173" alt="在这里插入图片描述" title="在这里插入图片描述"></p>