<?xml version="1.0" encoding="utf-8"?>
<feed xmlns="http://www.w3.org/2005/Atom">
            <title type="text">wangdaye'blog</title>
            <subtitle type="text">和优秀的人一起改变世界</subtitle>
    <updated>2026-05-15T10:14:32+08:00</updated>
        <id>https://www.wangdaye.net</id>
        <link rel="alternate" type="text/html" href="https://www.wangdaye.net" />
        <link rel="self" type="application/atom+xml" href="https://www.wangdaye.net/atom.xml" />
    <rights>Copyright © 2026, wangdaye'blog</rights>
    <generator uri="https://halo.run/" version="1.4.2">Halo</generator>
            <entry>
                <title><![CDATA[工具使用 redis-rdb-tools]]></title>
                <link rel="alternate" type="text/html" href="https://www.wangdaye.net/archives/gong-ju-shi-yong--r-e-d-i-s---r-d-b---t-o-o-l-s" />
                <id>tag:https://www.wangdaye.net,2025-04-10:gong-ju-shi-yong--r-e-d-i-s---r-d-b---t-o-o-l-s</id>
                <published>2025-04-10T10:54:55+08:00</published>
                <updated>2025-04-10T11:35:23+08:00</updated>
                <author>
                    <name>王大爷</name>
                    <uri>https://www.wangdaye.net</uri>
                </author>
                <content type="html">
                        <![CDATA[<p>在 Mac 系统中安装并使用 <code>redis-rdb-tools</code>（用于分析 Redis RDB 文件），可按照以下步骤操作：</p><hr /><h3 id="一安装-redis-rdb-tools"><strong>一、安装 redis-rdb-tools</strong></h3><ol><li><p><strong>确保 Python 环境</strong><br /><code>redis-rdb-tools</code> 依赖 Python 3.6 及以上版本。Mac 系统通常预装 Python，但需检查版本：</p><pre><code class="language-bash">python3 --version</code></pre><p>若未安装 Python3，可通过 Homebrew 安装：</p><pre><code class="language-bash">brew install python</code></pre></li><li><p><strong>使用 pip 安装 rdbtools</strong><br />通过国内镜像加速安装（推荐清华源）：</p><pre><code class="language-bash">pip3 install -i https://pypi.tuna.tsinghua.edu.cn/simple rdbtools</code></pre><p>此步骤会安装核心工具 <code>rdb</code>，用于解析 RDB 文件。</p></li><li><p><strong>加速解析（可选：安装 python-lzf）</strong><br />安装 <code>python-lzf</code> 可显著提升解析速度，但需编译环境支持：</p><pre><code class="language-bash">pip3 install -i https://pypi.tuna.tsinghua.edu.cn/simple python-lzf</code></pre><ul><li><strong>若编译报错</strong>：需安装 Xcode 命令行工具：<pre><code class="language-bash">xcode-select --install</code></pre></li></ul></li></ol><hr /><h3 id="二生成并分析-rdb-文件"><strong>二、生成并分析 RDB 文件</strong></h3><ol><li><p><strong>获取 Redis RDB 文件</strong></p><ul><li>确认 Redis 配置文件 <code>redis.conf</code> 中 <code>dir</code> 和 <code>dbfilename</code> 的路径（默认路径如 <code>/usr/local/redis-3.2.8/db/dump.rdb</code>）。</li><li>若需手动生成 RDB 快照，执行命令：<pre><code class="language-bash">redis-cli BGSAVE</code></pre></li></ul></li><li><p><strong>使用 rdb 工具分析</strong><br />执行以下命令生成内存报告（CSV 格式）：</p><pre><code class="language-bash">rdb -c memory /path/to/dump.rdb --bytes 1024 &gt; report.csv</code></pre><ul><li><code>--bytes 1024</code>：过滤内存占用超过 1KB 的 Key。</li><li>报告包含 Key 名称、类型、内存大小、元素数量等信息。</li></ul></li><li><p><strong>常用分析场景</strong></p><ul><li><strong>查找大 Key</strong>：按内存排序 CSV 文件，定位占用高的 Key。</li><li><strong>统计 Key 模式</strong>：筛选高频前缀（如 <code>user:*</code>），排查异常写入逻辑。</li></ul></li></ol><hr /><h3 id="三解决常见问题"><strong>三、解决常见问题</strong></h3><ol><li><p><strong>权限问题</strong><br />若安装时提示权限不足，可添加 <code>--user</code> 参数或使用虚拟环境：</p><pre><code class="language-bash">pip3 install --user rdbtools</code></pre></li><li><p><strong>依赖冲突</strong><br />建议使用 <code>venv</code> 隔离 Python 环境：</p><pre><code class="language-bash">python3 -m venv myenvsource myenv/bin/activatepip install rdbtools</code></pre></li><li><p><strong>RDB 文件路径</strong><br />若找不到 RDB 文件，检查 Redis 配置文件中的 <code>dir</code> 和 <code>dbfilename</code> 参数，或通过命令查询：</p><pre><code class="language-bash">redis-cli CONFIG GET dir</code></pre></li></ol><hr /><h3 id="四替代方案可视化工具"><strong>四、替代方案（可视化工具）</strong></h3><p>若需图形化分析，可尝试以下工具：</p><ul><li><strong>RedisInsight</strong>：官方可视化工具，支持内存分析、实时监控。</li><li><strong>rdr</strong>：开源 RDB 文件解析器，提供 Web 界面展示 Key 分布。</li></ul><h3 id="五增量扫描对比法">五、增量扫描对比法</h3><p>适用场景：适合非实时性需求，通过对比不同时间点的 Key 分布差异来定位增长热点。</p><p>​首次快照生成<br />在起始时间点（如 24 小时前）执行 BGSAVE 生成 RDB 文件，保存为 dump_start.rdb。</p><p>​结束快照生成<br />在结束时间点执行 BGSAVE，保存为 dump_end.rdb。<br />​使用 RDB 工具对比差异<br />通过 redis-rdb-tools 或 rdb 工具解析两个 RDB 文件，生成 Key 列表并对比差异：</p><pre><code>bash# 解析起始快照  rdb --command json dump_start.rdb &gt; start.json  # 解析结束快照  rdb --command json dump_end.rdb &gt; end.json  # 使用 diff 工具对比 Key 数量变化  diff start.json end.json | grep &quot;SET\|HSET&quot; &gt; key_growth.log </code></pre><p>​优点：对线上服务无性能影响7。<br />​缺点：需手动生成快照，RDB 文件较大时解析耗时较长</p><hr /><p>通过以上步骤，你可以在 Mac 上高效安装并使用 <code>redis-rdb-tools</code> 分析 Redis 数据增长问题。如需进一步优化排查逻辑，可结合 <code>INFO keyspace</code> 命令和业务日志综合分析。</p>]]>
                </content>
            </entry>
            <entry>
                <title><![CDATA[从一个实际AI项目看，AI应用开发需要学什么？]]></title>
                <link rel="alternate" type="text/html" href="https://www.wangdaye.net/archives/cong-yi-ge-shi-ji-a-i-xiang-mu-fan-tui-a-i-ying-yong-kai-fa-xu-yao-xue-shen-me-" />
                <id>tag:https://www.wangdaye.net,2025-04-02:cong-yi-ge-shi-ji-a-i-xiang-mu-fan-tui-a-i-ying-yong-kai-fa-xu-yao-xue-shen-me-</id>
                <published>2025-04-02T13:58:35+08:00</published>
                <updated>2025-04-02T17:25:52+08:00</updated>
                <author>
                    <name>王大爷</name>
                    <uri>https://www.wangdaye.net</uri>
                </author>
                <content type="html">
                        <![CDATA[<h1 id="从一个实际ai项目看ai应用开发需要学什么">从一个实际AI项目看，AI应用开发需要学什么？</h1><h2 id="引言">引言</h2><p>作为一个Java开发人员，如何上手AI开发应用，这应该是大多数同学的困惑，本文使用一个实际的AI应用出发，分析其实现技术，从而给出学习路径。给大家解惑：我该如何学，学什么？</p><h2 id="一个ai项目分析">一个AI项目分析</h2><blockquote><p>AI应用榜单：<a href="https://www.aicpb.com/">https://www.aicpb.com/</a></p><p>国内AI应用榜单热评榜｜2025年第12周** 第二名</p></blockquote><h3 id="ai项目功能分析">AI项目功能分析</h3><p><img src="https://www.wangdaye.net/upload/2025/04/image-20250402135240518-ce8b72292cb948b093be7d617a2b44c0.png" alt="image20250402135240518.png" />!</p><p>这是一个针对对运营各个文案场景的AI应用。用于互联网运营工作。其中维护了各种Agent实现，针对运营人员，在小红书，大众点评等等平台，发帖子的行为。仿写别人文章的行为，洗稿。</p><h3 id="ai项目技术能力分析">AI项目技术能力分析</h3><ul><li>爬虫 - 定期爬取小红书等网站的内容。</li><li>AI 分类 - 将爬取完成的内容AI进行有限归类。</li><li>OCR：图片信息提取。</li><li>多模态：文生图。</li><li>多Agent：仿写Agent，客服Agent，各业务Agent。</li><li>结构化输出：大模型输出格式结构化成Bean。</li></ul><p><strong>总结</strong> ：除了几个基座模型应用（ChatGpt，DeepSeek，豆包）其他的大模型应用，基本都是以优秀的交互体验取胜。技术不是核心壁垒。（所有的AI应用基本都是，RAG，多模态应用，智能体，工作流，MCP，这几类为主。）</p><h2 id="当前市面技术分析">当前市面技术分析</h2><p>AI 开发框架：Langchain，Spring AI，langchain4j，Spring AI Alibaba。</p><p>基本都集成了基础功能：Agent，工作流，Function Calling，MCP协议 等基础能力。</p><h3 id="对比总结">对比总结</h3><table><thead><tr><th align="left"><strong>框架</strong></th><th align="left"><strong>语言/生态</strong></th><th align="left"><strong>核心优势</strong></th><th align="left"><strong>适用场景</strong></th><th align="left"><strong>局限性</strong></th></tr></thead><tbody><tr><td align="left"><strong>LangChain</strong></td><td align="left">Python</td><td align="left">社区活跃，功能全面，多模态支持</td><td align="left">快速原型开发，复杂NLP任务</td><td align="left">依赖Python生态，企业级支持较弱</td></tr><tr><td align="left"><strong>LangChain4j</strong></td><td align="left">Java</td><td align="left">标准化API，Spring集成，高性能</td><td align="left">企业级Java应用，RAG系统</td><td align="left">部分功能尚在开发中</td></tr><tr><td align="left"><strong>Spring AI</strong></td><td align="left">Java/Spring</td><td align="left">Spring生态兼容，企业级特性丰富</td><td align="left">现有Spring项目AI扩展</td><td align="left">依赖Spring框架</td></tr><tr><td align="left"><strong>Spring AI Alibaba</strong></td><td align="left">Java/阿里云</td><td align="left">阿里云服务深度集成（推测）</td><td align="left">阿里云生态的中文AI应用（需验证）</td><td align="left">Spring AI 二次开发</td></tr></tbody></table><p><strong>结论</strong>：选择Spring AI Alibaba，这是阿里基于Spring AI同时集成自身的云服务（百炼平台）形成了二次开发框架：Spring AI Alibaba(<a href="https://java2ai.com/">https://java2ai.com/</a>)，上手门槛较低，且有国内技术团队迭代维护。是我们主要选择的技术框架。</p><h3 id="学习建议">学习建议</h3><p>第一步，先将Spring webflux概念学习一遍，对流应用开发有基本的了解。</p><p>第二步，langchain4j，Spring AI Alibaba ，Spring AI，的各个demo和文档看一遍，对一些基础概念心中有数。</p><p>第三步，在扣子上查看一些排名较高的工作流应用，Agent，尝试使用代码实现。</p><p>第四步，Prompt 提示词编写与维护学习（优秀的提示词可以最大化节点能力）。</p><p><strong>基础概念学习</strong>：可以LangChain4j的源码和demo为主去学基础概念，使用Spring AI 的源码和样例辅助</p><blockquote><p>LangChain4j 基本是照抄Langchain的设计，而现在AI应用开发中很多概念都是LangChain先提出。</p></blockquote><h2 id="基于justai的样例实现">基于JustAI的样例实现</h2><blockquote><p>如下案例基于如上JustAI应用，需要使用的能力，编写的代码demo，将如下能力实现，就可以做到JustAI的所有能力。</p><p>归根就是用产品设计，将如下能力组合提供。</p><p><strong>基础概念</strong>：LLM，工具(Function Calling)，Memory，Prompt，格式化输出(Structured Output)，智能体，多模态。</p><p><strong>LLM</strong>：大模型基座，一般选择俩个以上的API基座，互相补充，或者特殊场景的调优，比如阿里有专业的金融大模型。</p><p><strong>工具(Function Calling)</strong> ：工具，用于弥补大模型在专业任务上的不足，比如科学计算器，比如天气查询。</p><p><strong>Memory</strong> ：上下文记忆，一般为20轮次对话</p><p>其他Agent基础概念 ：<a href="https://docs.spring.io/spring-ai/reference/">https://docs.spring.io/spring-ai/reference/</a></p></blockquote><p><strong>如下实现了6个 样例：AI Agent，文生图，结构化，爬虫Tool，仿写工作流，OCR</strong></p><h3 id="ai-agent">AI Agent</h3><blockquote><p>基于业务场景细分管理不同的Prompt模板。配合交互输出。</p><p>如下以 自然语言生成SQL的场景，写一个Agent Demo</p></blockquote><h4 id="样例演示">样例演示</h4><p>自然语言生成SQL的Prompt</p><pre><code class="language-shell">Given the DDL in the DDL section, write an SQL query to answer the question in the QUESTION section.Only produce select queries. If the question would result in an insert, update,or delete, or if the query would alter the DDL in any way, say that the operationisn't supported. If the question can't be answered, say that the DDL doesn't supportanswering that question.Answer with the raw SQL query only; no markdown or other punctuation that isn't part of the query itself.QUESTION{question}DDL{ddl}</code></pre><p>DDL 语句如下（其实有更简洁的描述）：</p><pre><code class="language-sql">-- Authorscreate table Authors (id int not null auto_increment,firstName varchar(255) not null,lastName varchar(255) not null,                      primary key (id));-- Publisherscreate table Publishers (id int not null auto_increment, name varchar(255) not null,  primary key (id));-- Bookscreate table Books (id int not null auto_increment,isbn varchar(255) not null,title varchar(255) not null,author_ref int not null,publisher_ref int not null,  primary key (id));</code></pre><p>代码样例如下（JAVA）：</p><pre><code class="language-java">@PostMapping(path = &quot;/sql&quot;)public Answer sql(@RequestBody SqlRequest sqlRequest) throws IOException {String schema = ddlResource.getContentAsString(Charset.defaultCharset());String query = chatClient.prompt().user(userSpec -&gt; userSpec.text(sqlPromptTemplateResource).param(&quot;question&quot;, sqlRequest.question()).param(&quot;ddl&quot;, schema)).call().content();assert query != null;if (query.toLowerCase().startsWith(&quot;select&quot;)) {return new Answer(query,jdbcTemplate.queryForList(query));}throw new SQLGenerationException(query);}}</code></pre><p>提问：</p><pre><code class="language-shell">How many books has Craig Walls written?</code></pre><p>返回：</p><pre><code class="language-sql">SELECT COUNT(*) FROM Books INNER JOIN Authors ON Books.author_ref = Authors.id WHERE Authors.firstName = 'Craig' AND Authors.lastName = 'Walls'</code></pre><h4 id="需学习内容">需学习内容</h4><ul><li><p>Spring webflux：《Spring in Action》第五版，《Spring 5.0 项目实战》</p></li><li><p>Prompt工程：<a href="https://prompt-guide.xiniushu.com/">https://prompt-guide.xiniushu.com/</a></p></li><li><p>Prompt指南1：<a href="https://github.com/f/awesome-chatgpt-prompts">https://github.com/f/awesome-chatgpt-prompts</a></p></li><li><p>Prompt指南2：<a href="https://github.com/PlexPt/awesome-chatgpt-prompts-zh">https://github.com/PlexPt/awesome-chatgpt-prompts-zh</a></p></li></ul><p><strong>提示</strong> ：可以使用扣子工作流内置的功能，逐个调试Prompt，和大模型效果对比</p><h3 id="多模态-文生图">多模态-文生图</h3><blockquote><p>文生图的核心不是一次性生成图片，而是基于当前图片调整的能力。</p><p>本Demo 是依赖于阿里云百炼平台的能力。</p></blockquote><p>场景有三个：</p><ol><li>生成：一句话生成商标。</li><li>修改：基于当前生成的商标，修改细节，细节优化。</li><li>缩放：图片缩放（由于大模型生成图片的尺度受限）。</li><li>保存：将完整的图片保存到云服务和本地。</li></ol><h4 id="样例演示-1">样例演示</h4><p>1、生成</p><pre><code class="language-java">@GetMapping(&quot;/image&quot;)public void image(HttpServletResponse response) {  ImageResponse imageResponse = imageModel.call(new ImagePrompt(&quot;小狗图片&quot;));  String imageUrl = imageResponse.getResult().getOutput().getUrl();  try {    URL url = URI.create(imageUrl).toURL();    InputStream in = url.openStream();    response.setHeader(&quot;Content-Type&quot;, MediaType.IMAGE_PNG_VALUE);    response.getOutputStream().write(in.readAllBytes());    response.getOutputStream().flush();  } catch (IOException e) {    response.setStatus(HttpServletResponse.SC_INTERNAL_SERVER_ERROR);  }}</code></pre><p>2、修改</p><p><img src="https://www.wangdaye.net/upload/2025/04/image-20250402111744320-caf238fc1a884e4bb95b541898ea571d.png" alt="image20250402111744320.png" /></p><pre><code class="language-java">// 涂抹+原始图片+提示词 修改图片private ImageSynthesisParam genImageSynthesis(){        HashMap&lt;String,Object&gt; extraInputMap = new HashMap&lt;&gt;();        extraInputMap.put(&quot;base_image_url&quot;, &quot;http://synthesis-source.oss-accelerate.aliyuncs.com/lingji/validation/mask2img/demo/source3.jpg&quot;);        extraInputMap.put(&quot;mask_image_url&quot;, &quot;http://synthesis-source.oss-accelerate.aliyuncs.com/lingji/validation/mask2img/demo/glasses.png&quot;);        String prompt = &quot;a dog wearing red glasses&quot;;        String model = &quot;wanx-x-painting&quot;;        return ImageSynthesisParam.builder()                .model(model)                .prompt(prompt)                .n(1)                .size(&quot;1024*1024&quot;)                .extraInputs(extraInputMap)                .build();    }</code></pre><p>3、缩放</p><p><img src="https://www.wangdaye.net/upload/2025/04/image-20250402112131411-953db06e095a4d02b4ab255dfc85fd7d.png" alt="image20250402112131411.png" /></p><p>调用代码如下</p><pre><code class="language-shell">curl --location --request POST 'https://dashscope.aliyuncs.com/api/v1/services/aigc/image2image/out-painting' \--header 'X-DashScope-Async: enable' \--header &quot;Authorization: Bearer $DASHSCOPE_API_KEY&quot; \--header 'Content-Type: application/json' \--data-raw '{    &quot;model&quot;: &quot;image-out-painting&quot;,    &quot;input&quot;: {        &quot;image_url&quot;: &quot;https://huarong123.oss-cn-hangzhou.aliyuncs.com/image/%E5%9B%BE%E5%83%8F%E7%94%BB%E9%9D%A2%E6%89%A9%E5%B1%95.png&quot;    },    &quot;parameters&quot;:{        &quot;angle&quot;:0,        &quot;output_ratio&quot;:&quot;4:3&quot;,        &quot;best_quality&quot;:false,        &quot;limit_image_size&quot;:true    }}'</code></pre><p>4、保存</p><p>通常生成的最终图片要保存的云存储，或者本地。这一般基于各个家自己实现，阿里云，七牛云都可以</p><h4 id="需要学习内容">需要学习内容</h4><ul><li>Spring AI Alibaba 图生文API（）</li><li>百炼平台（文生图）文档</li></ul><h3 id="结构化输出">结构化输出</h3><blockquote><p>如下是基于Spring AI 的demo</p></blockquote><p>大模型输出的格式不固定，没法兼容应用程序业务逻辑，所以在特殊的环节上需要使用很多方式将大模型的输出内容转为结构化内容。</p><ul><li>一种是使用Prompt提要求。</li><li>大模型输出完成之后二次加工整合。</li></ul><p>第一种为主流，响应速度较快（与大模型交互次数较少），使用第一种思路整合</p><h4 id="样例演示-2">样例演示</h4><p>Prompt 如下：</p><pre><code class="language-markdown">requirement: 请用大概 120 字，作者为 王大爷 ，为计算机的发展历史写一首现代诗;format: 以纯文本输出 json，请不要包含任何多余的文字——包括 markdown 格式;outputExample: { &quot;title&quot;: {title},&quot;author&quot;: {author},&quot;date&quot;: {date},&quot;content&quot;: {content}};</code></pre><p>代码样例（JAVA）</p><pre><code class="language-java">@GetMapping(&quot;/play&quot;)public StreamToBeanEntity simpleChat(HttpServletResponse response) {var converter = new BeanOutputConverter&lt;&gt;(new ParameterizedTypeReference&lt;StreamToBeanEntity&gt;() {});Flux&lt;String&gt; flux = this.chatClient.prompt().user(u -&gt; u.text(&quot;&quot;&quot;requirement: 请用大概 120 字，作者为 王大爷 ，为计算机的发展历史写一首现代诗;format: 以纯文本输出 json，请不要包含任何多余的文字——包括 markdown 格式;outputExample: { &quot;title&quot;: {title}, &quot;author&quot;: {author}, &quot;date&quot;: {date}, &quot;content&quot;: {content}};&quot;&quot;&quot;)).stream().content();String result = String.join(&quot;\n&quot;, Objects.requireNonNull(flux.collectList().block())).replaceAll(&quot;\\n&quot;, &quot;&quot;).replaceAll(&quot;\\s+&quot;, &quot; &quot;).replaceAll(&quot;\&quot;\\s*:&quot;, &quot;\&quot;:&quot;).replaceAll(&quot;:\\s*\&quot;&quot;, &quot;:\&quot;&quot;);log.info(&quot;LLMs 响应的 json 数据为：{}&quot;, result);return converter.convert(result);}</code></pre><h4 id="需学习内容-1">需学习内容</h4><ul><li><p>Prompt工程：<a href="https://prompt-guide.xiniushu.com/">https://prompt-guide.xiniushu.com/</a></p></li><li><p>Spring AI结构化输出文档：<a href="https://docs.spring.io/spring-ai/reference/api/structured-output-converter.html">https://docs.spring.io/spring-ai/reference/api/structured-output-converter.html</a></p></li></ul><h3 id="大模型爬虫tool">大模型爬虫Tool</h3><blockquote><p>基于Function Calling 协议 编写爬虫Tool。</p><p>如下是基于Spring AI 框架的demo。</p></blockquote><p>定时任务 - 定时爬取小红书，知乎上的内容。</p><h4 id="样例演示-3">样例演示</h4><p>1、定义Tool</p><pre><code class="language-java">public interface CrawlerService {String run(String url);}public class CrawlerJinaServiceImpl extends CrawlerService {@Overridepublic String run(String targetUrl) {URL url = URI.create(CrawlerConstants.JINA_BASE_URL).toURL();Map&lt;String, String&gt; requestParam = Map.of(&quot;url&quot;, targetUrl);String requestBody = this.objectMapper.writeValueAsString(requestParam);logger.debug(&quot;Jina request body: {}&quot;, requestBody);HttpURLConnection connection = this.initHttpURLConnection(jinaProperties.getToken(), url, this.getOptions(),requestBody);return objectMapper.writeValueAsString(this.convert2Response(this.getResponse(connection)));}}</code></pre><p>2、注册Tool</p><pre><code class="language-sql">@Configuration@EnableConfigurationProperties({ CrawlerJinaProperties.class })@ConditionalOnProperty(prefix = CrawlerJinaProperties.JINA_PROPERTIES_PREFIX, name = &quot;enabled&quot;, havingValue = &quot;true&quot;)public class CrawlerAutoConfiguration {@Bean@ConditionalOnMissingBean@Description(&quot;web Reader Service Plugin.&quot;)public CrawlerJinaServiceImpl jinaFunction(CrawlerJinaProperties jinaProperties, ObjectMapper objectMapper) {Assert.notNull(jinaProperties, &quot;Jina reader api token must not be empty&quot;);return new CrawlerJinaServiceImpl(jinaProperties, objectMapper);}}</code></pre><p>3、大模型使用Tool</p><pre><code class="language-sql">/*** 调用工具 - function*/@GetMapping(&quot;/chat-tool-function&quot;)public String chatTranslateFunction(@RequestParam(value = &quot;query&quot;, defaultValue = &quot;查看一下这个网页：www.baidu.com&quot;) String query) {return dashScopeChatClient.prompt(query).tools(&quot;jinaFunction&quot;).call().content();}</code></pre><p>在大模型使用的时候，它会基于 @Description(&quot;web Reader Service Plugin.&quot;) 注解，和提问，自动判断是否需要使用工具，使用什么工具。</p><blockquote><p>可以使用现有的 Java爬虫框架：gecco，webCollector. 封装为Tool 供其调用</p></blockquote><h4 id="需学习内容-2">需学习内容</h4><p>Spring AI Tools文档：<a href="https://docs.spring.io/spring-ai/reference/api/tools-migration.html">https://docs.spring.io/spring-ai/reference/api/tools-migration.html</a></p><h3 id="仿写工作流">仿写工作流</h3><blockquote><p>基于当前热门的小红书文章，洗稿，基于要求生成一篇文章</p></blockquote><h4 id="样例演示-4">样例演示</h4><p>工作流程如下：</p><ol><li>爬虫获取数据（参见爬虫Tools实现）：小红书，知乎，美团等等数据。</li><li>选择风格，要求（用户交互）（选择风格Promt）</li><li>节点1（LLM） - 原文内容提取（移除掉干扰项，将图片清除，表情清除）</li><li>节点2 （LLM）- 完成对目标文章切割按照大纲切割</li><li>节点4 （LLM）- {大纲 - 部分} +{部分原文} 生成部分+{ 用户要求 }。（循环）</li><li>节点5 （逻辑）- 将生成的各个部分组合为一篇整体的文稿。</li></ol><p>实际工作流如下：</p><p><img src="https://www.wangdaye.net/upload/2025/04/image-20250402163556984-3928e5aa563343da91f0e3a3bf3682cd.png" alt="image20250402163556984.png" /></p><p>如下列出几个核心节点的逻辑：</p><p>1、节点1</p><blockquote><p>原文内容抽取Prompt</p></blockquote><pre><code class="language-shell">请对以下网页内容进行清洗，并抽取网页中的文章内容信息。请避免包含文章内容无关的信息，例如：html标签、页头、页尾。请对清洗后的信息进行markdown的格式输出。### 约束条件1.文本信息中如果有明显的标题信息请直接抽取，不需要自定义生成或加工2.文本的正文内容请尊重原本描述抽取，不需要自定义生成或加工3.关键词将用作图片多模态检索，请不要包含无意义的词组或短语。4.请完整抽取原文内容，并保持段落排版5.请过滤版权信息、编辑、作者、来源、转载以及与原文内容无关的信息### 抓取内容如下：```{{#1718967141318.text#}}```</code></pre><p>2、节点2</p><blockquote><p>完成对目标文章切割按照大纲切割</p></blockquote><pre><code class="language-shell">请分析文章的内容结构，将文本划分为一个或多个语义完整的章节，每个章节不少于400字。并为每个章节生成若干个用于检索图片的关键词或描述，多个关键词使用逗号进行分割。输出结构为jsonExample:{    &quot;sections&quot;: [        {           ”keywords“: ”关键词或描述字符串“,           ”section“: &quot;章节内容1&quot;        },        {           ”keywords“: ”关键词或描述字符串“,           ”section“: &quot;章节内容2&quot;        },         {           ”keywords“: ”关键词或描述字符串“,           ”section“: &quot;章节内容2&quot;        }    ]}</code></pre><p>3、节点3（循环体）</p><blockquote><p>该节点用于分段仿写</p></blockquote><pre><code class="language-shell">你是一位专业的文字工作者，你可以胜任各种领域的文章仿写，请你对以下提供的参考文章进行仿写并排版。### 约束条件1.请确保生成的文章符合编辑规范，不要包含明显的标题、正文、结论等字眼2.请分析该段落是否需要配图，如果需要请输出仿写后的段落后接配图信息,如果是单段落请将配图放置余段落前，配图信息只需要输出markdown格式的图片3.请尽可能遵循文本描述的客观事实信息，不要胡编乱造4.请遵循文本内容的写作风格和用词，输出的段落请尽量确保行文通顺自然5.请确认排版信息符合markdown规范。6.请不要添加文章以外的多余信息，如落款、出处、来源、发布时间、作者、编辑等。### 参考文章内容：- 标题：{{#17190400701430.title#}}- 图片：{{#1719052020538.text#}}- 是否单段落：{{#1719062922068.single_section#}}- 内容：{{#1719051705186.section#}}</code></pre><h4 id="需学习内容-3">需学习内容</h4><ul><li>可视化工作流 - coze.cn</li><li>编码工作流(Spring AI Alibaba Graph)</li><li>Prompt工程：<a href="https://prompt-guide.xiniushu.com/">https://prompt-guide.xiniushu.com/</a></li></ul><h3 id="ocr文字识别">OCR文字识别</h3><blockquote><p>在java中可以使用框架Tess4J，做OCR识别</p><p>也可以使用通义大模型qwen-vl-ocr专门用于图转文，提取信息比较精确。</p></blockquote><p>在JAVA中可以使用框架Tess4J，做OCR识别。</p><pre><code class="language-xml">&lt;!-- Tess4J依赖 --&gt;&lt;dependency&gt;    &lt;groupId&gt;net.sourceforge.tess4j&lt;/groupId&gt;    &lt;artifactId&gt;tess4j&lt;/artifactId&gt;    &lt;version&gt;5.4.0&lt;/version&gt;&lt;/dependency&gt;</code></pre><h4 id="样例演示-5">样例演示</h4><p>1、代码实现</p><pre><code class="language-java">    // 执行OCR识别    private void execute(BufferedImage targetImage) {        File file = new File(tempImage);        ITesseract instance = new Tesseract();        // 设置语言库位置        instance.setDatapath(&quot;src/main/resources/data&quot;);        // 设置语言        instance.setLanguage(language);        Thread thread = new Thread() {            public void run() {                String result = null;                try {                    result = instance.doOCR(file);                } catch (Exception e) {                    e.printStackTrace();                }                resultArea.setText(result);            }        };        ProgressBar.show(this, thread, &quot;图片正在识别中，请稍后...&quot;, &quot;执行结束&quot;, &quot;取消&quot;);    }</code></pre><p>2、测试结果：<br /><img src="https://www.wangdaye.net/upload/2025/04/b90894ee96ad35ba06442c13649aeb1e-7f81d8e3d2c148cca3f1d02fbc1edc88.jpeg" alt="b90894ee96ad35ba06442c13649aeb1e.jpeg" /></p><p>十 年 生 死 两 茫 茫 。 不 思 量 , 自 难 忘 。<br />干 里 孤 坟 , 无 处 话 凄 凉 。 纵 使 相 逢 应 不 识 , 尘 满 面 , 鬓 如 霜 。<br />夜 来 幽 梦 忽 还 乡 。 小 轩 窗 , 正 梳 妆 。<br />相 顾 无 言 , 惟 有 泪 十 行 。 料 得 年 年 肠 断 处 , 明 月 夜 , 短 松 岗 。</p><h4 id="需学习内容-4">需学习内容</h4><ul><li>Java框架-tess4j 的使用</li></ul><h2 id="深入研究">深入研究</h2><p>深入研究一般细分三个方向：<strong>RAG应用，MCP应用，垂直模型训练</strong>。如下整理了一些学习资料。</p><table><thead><tr><th><strong>类型</strong></th><th><strong>名称/描述</strong></th><th><strong>链接</strong></th></tr></thead><tbody><tr><td><strong>Prompt工程</strong></td><td>提示词工程指南（xiniushu）</td><td><a href="https://prompt-guide.xiniushu.com/">链接</a></td></tr><tr><td> </td><td>Prompt工程实践指南</td><td><a href="https://developer.aliyun.com/article/1604351">链接</a></td></tr><tr><td> </td><td>Awesome-Prompt，awesome-chatgpt-prompts-zh（github）</td><td><a href="https://github.com/PlexPt/awesome-chatgpt-prompts-zh">链接</a></td></tr><tr><td><strong>模型训练</strong></td><td>OpenAI官方微调指南</td><td><a href="https://openai.xiniushu.com/docs/guides/fine-tuning">链接</a></td></tr><tr><td> </td><td>LLaMA-Factory（开源微调工具库）</td><td><a href="https://github.com/hiyouga/LLaMA-Factory">链接</a></td></tr><tr><td><strong>LLM Agent</strong></td><td>LLM Agent论文合集（GitHub仓库）</td><td><a href="https://github.com/WooooDyy/LLM-Agent-Paper-List">链接</a></td></tr><tr><td>****</td><td>什么是MCP？</td><td><a href="https://zhuanlan.zhihu.com/p/27327515233">链接</a></td></tr><tr><td> </td><td>Spring AI 使用MCP客户端文档</td><td><a href="https://docs.spring.io/spring-ai/reference/api/mcp/mcp-client-boot-starter-docs.html">链接</a></td></tr><tr><td> </td><td>Spring AI实现检索增强生成（RAG）文档</td><td><a href="https://docs.spring.io/spring-ai/reference/api/retrieval-augmented-generation.html">链接</a></td></tr><tr><td><strong>基础理论</strong></td><td>《深度学习》（花书）</td><td>书籍（无公开链接）</td></tr><tr><td> </td><td>《Python机器学习实战》</td><td>书籍（无公开链接）</td></tr><tr><td> </td><td>Transformer 论文《Attention Is All You Need》</td><td><a href="https://arxiv.org/abs/1706.03762">arXiv链接</a></td></tr><tr><td><strong>向量数据库</strong></td><td>Milvus官方文档</td><td><a href="https://milvus.io/docs/zh">链接</a></td></tr><tr><td> </td><td>向量数据库学习指南（第三方教程）</td><td>[链接](</td></tr></tbody></table>]]>
                </content>
            </entry>
            <entry>
                <title><![CDATA[Java优质开源系统设计项目]]></title>
                <link rel="alternate" type="text/html" href="https://www.wangdaye.net/archives/j-a-v-a--you-zhi-kai-yuan-xi-tong-she-ji-xiang-mu" />
                <id>tag:https://www.wangdaye.net,2024-12-02:j-a-v-a--you-zhi-kai-yuan-xi-tong-she-ji-xiang-mu</id>
                <published>2024-12-02T22:38:44+08:00</published>
                <updated>2024-12-10T11:53:08+08:00</updated>
                <author>
                    <name>王大爷</name>
                    <uri>https://www.wangdaye.net</uri>
                </author>
                <content type="html">
                        <![CDATA[<h1 id="java-优质开源系统设计项目">Java 优质开源系统设计项目</h1><h2 id="基础框架">基础框架</h2><h3 id="web-框架">Web 框架</h3><ul><li><a href="https://github.com/spring-projects/spring-boot" title="spring-boot">Spring Boot</a>：Spring Boot 可以轻松创建独立的生产级基于 Spring 的应用程序，内置 web 服务器让你可以像运行普通 Java 程序一样运行项 目。另外，大部分 Spring Boot 项目只需要少量的配置即可，这有别于 Spring 的重配置。</li><li><a href="https://github.com/sofastack/sofa-boot">SOFABoot</a>：SOFABoot 基于 Spring Boot ，不过在其基础上增加了 Readiness Check，类隔离，日志空间隔离等等能力。 配套提供的还有：SOFARPC（RPC 框架）、SOFABolt（基于 Netty 的远程通信框架）、SOFARegistry（注册中心）...详情请参考：<a href="https://github.com/sofastack">SOFAStack</a> 。</li><li><a href="https://github.com/tipsy/javalin">Javalin</a>：一个轻量级的 Web 框架，同时支持 Java 和 Kotlin，被微软、红帽、Uber 等公司使用。</li><li><a href="https://github.com/playframework/playframework">Play Framework</a>：面向 Java 和 Scala 的高速 Web 框架。</li><li><a href="https://github.com/lets-blade/blade">Blade</a>：一款追求简约、高效的 Web 框架，基于 Java8 + Netty4。</li></ul><h3 id="微服务云原生">微服务/云原生</h3><ul><li><a href="https://github.com/line/armeria">Armeria</a>：适合任何情况的微服务框架。你可以用你喜欢的技术构建任何类型的微服务，包括<a href="https://grpc.io/">gRPC</a>、 <a href="https://thrift.apache.org/">Thrift</a>、<a href="https://kotlinlang.org/">Kotlin</a>、 <a href="https://square.github.io/retrofit/">Retrofit</a>、<a href="https://www.reactive-streams.org/">Reactive Streams</a>、 <a href="https://spring.io/projects/spring-boot">Spring Boot</a>和<a href="https://www.dropwizard.io/">Dropwizard</a></li><li><a href="https://github.com/quarkusio/quarkus">Quarkus</a> : 用于编写 Java 应用程序的云原生和容器优先的框架。</li></ul><h3 id="api-文档">api-文档</h3><ul><li><a href="https://swagger.io/">Swagger</a> ：较主流的 RESTful 风格的 API 文档工具，提供了一套工具和规范，让开发人员能够更轻松地创建和维护可读性强、易于使用和交互的 API 文档。</li><li><a href="https://doc.xiaominfo.com/">Knife4j</a>：集 Swagger2 和 OpenAPI3 为一体的增强解决方案。</li></ul><h3 id="bean-映射">Bean 映射</h3><ul><li><a href="https://github.com/mapstruct/mapstruct">MapStruct</a>（推荐）：满足 JSR269 规范的一个 Java 注解处理器，用于为 Java Bean 生成类型安全且高性能的映射。它基于编译阶段生成 get/set 代码，此实现过程中没有反射，不会造成额外的性能损失。</li><li><a href="https://github.com/jmapper-framework/jmapper-core">JMapper</a> : 一个高性能且易于使用的 Bean 映射框架。</li></ul><h3 id="其他">其他</h3><ul><li><a href="https://github.com/google/guice">Guice</a>：Google 开源的一个轻量级依赖注入框架，相当于一个功能极简化的轻量级 Spring Boot。在某些情况下非常实用，就比如说我们的项目只需要使用依赖注入，不需要 AOP 等功能特性。</li><li><a href="https://github.com/spring-projects/spring-batch">Spring Batch</a> : Spring Batch 是一个轻量级但功能又十分全面的批处理框架，主要用于批处理场景比如从数据库、文件或队列中读取大量记录。不过，需要注意的是：Spring Batch 不是调度框架。商业和开源领域都有许多优秀的企业调度框架比如 Quartz、XXL-JOB、Elastic-Job。它旨在与调度程序一起工作，而不是取代调度程序。</li></ul><h2 id="认证授权">认证授权</h2><h3 id="权限认证">权限认证</h3><ul><li><a href="https://github.com/dromara/sa-token">Sa-Token</a>：轻量级 Java 权限认证框架。支持认证授权、单点登录、踢人下线、自动续签等功能。相比于 Spring Security 和 Shiro 来说，Sa-Token 内置的开箱即用的功能更多，使用也更简单。</li><li><a href="https://github.com/spring-projects/spring-security">Spring Security</a>：Spring 官方安全框架，能够用于身份验证、授权、加密和会话管理，是目前使用最广泛的 Java 安全框架。</li><li><a href="https://github.com/apache/shiro">Shiro</a>：Java 安全框架，功能和 Spring Security 类似，但使用起来更简单。</li></ul><h3 id="第三方登录">第三方登录</h3><ul><li><a href="https://github.com/Wechat-Group/WxJava">WxJava</a> : WxJava （微信开发 Java SDK），支持包括微信支付、开放平台、小程序、企业微信/企业号和公众号等的后端开发。</li><li><a href="https://github.com/justauth/JustAuth">JustAuth</a>：小而全而美的第三方登录开源组件。目前已经集成了诸如：GitHub、Gitee、支付宝、新浪微博、微信、Google、Facebook、Twitter、StackOverflow 等国内外数十家第三方平台。</li></ul><h3 id="单点登录sso">单点登录（SSO）</h3><ul><li><a href="https://github.com/apereo/cas">CAS</a>：企业多语言网络单点登录解决方案。</li><li><a href="https://gitee.com/dromara/MaxKey">MaxKey</a>：单点登录认证系统，提供安全、标准和开放的用户身份管理(IDM)、身份认证(AM)、单点登录(SSO)、RBAC 权限管理和资源管理等。</li><li><a href="https://github.com/keycloak/keycloak">Keycloak</a>：免费、开源身份认证和访问管理系统，支持高度可配置的单点登录功能。</li></ul><h2 id="网络通讯">网络通讯</h2><ul><li><a href="https://github.com/netty/netty">Netty</a> : 一个基于 NIO 的 client-server(客户端服务器)框架，使用它可以快速简单地开发网络应用程序。</li><li><a href="https://github.com/square/retrofit">Retrofit</a>：适用于 Android 和 Java 的类型安全的 HTTP 客户端。Retrofit 的 HTTP 请求使用的是 <a href="https://square.github.io/okhttp/">OkHttp</a> 库（一款被广泛使用网络框架）。</li><li><a href="https://gitee.com/dromara/forest">Forest</a>：轻量级 HTTP 客户端 API 框架，让 Java 发送 HTTP/HTTPS 请求不再难。它比 OkHttp 和 HttpClient 更高层，是封装调用第三方 restful api client 接口的好帮手，是 retrofit 和 feign 之外另一个选择。</li><li><a href="https://github.com/YeautyYE/netty-websocket-spring-boot-starter">netty-websocket-spring-boot-starter</a> :帮助你在 Spring Boot 中使用 Netty 来开发 WebSocket 服务器，并像 spring-websocket 的注解开发一样简单。</li></ul><h2 id="数据库">数据库</h2><h3 id="数据库连接池">数据库连接池</h3><ul><li><a href="https://github.com/alibaba/druid">Druid</a> : 阿里巴巴数据库事业部出品，为监控而生的数据库连接池。</li><li><a href="https://github.com/brettwooldridge/HikariCP">HikariCP</a> : 一个可靠的高性能 JDBC 连接池。Springboot 2.0 选择 HikariCP 作为默认数据库连接池。</li></ul><h3 id="数据库框架">数据库框架</h3><ul><li><a href="https://github.com/baomidou/mybatis-plus">MyBatis-Plus</a> : <a href="http://www.mybatis.org/mybatis-3/">MyBatis</a> 增强工具，在 MyBatis 的基础上只做增强不做改变，为简化开发、提高效率而生。</li><li><a href="https://gitee.com/mybatis-flex/mybatis-flex">MyBatis-Flex</a>：一个优雅的 MyBatis 增强框架，无其他任何第三方依赖，支持 CRUD、分页查询、多表查询、批量操作。</li><li><a href="https://github.com/jOOQ/jOOQ">jOOQ</a>：用 Java 编写 SQL 的最佳方式。</li><li><a href="https://github.com/redisson/redisson" title="redisson">Redisson</a>：Redisson 是一款架设在 Redis 基础之上的 Java 驻内存数据网格 (In-Memory Data Grid)，它充分利用了 Redis 键值数据库的优势，为 Java 开发者提供了一系列具有分布式特性的常用工具类。例如，分布式 Java 对象（<code>Set</code>，<code>SortedSet</code>，<code>Map</code>，<code>List</code>，<code>Queue</code>，<code>Deque</code> 等）、分布式锁等。详细介绍请看：<a href="https://github.com/redisson/redisson/wiki/Redisson%E9%A1%B9%E7%9B%AE%E4%BB%8B%E7%BB%8D" title="Redisson项目介绍">Redisson 项目介绍</a>。</li></ul><h3 id="数据同步">数据同步</h3><ul><li><a href="https://github.com/alibaba/canal" title="canal">Canal</a> [kə'næl] : Canal 译意为水道/管道/沟渠，主要用途是基于 MySQL 数据库增量日志解析，提供增量数据订阅和消费。</li><li><a href="https://github.com/alibaba/DataX" title="DataX">DataX</a>：DataX 是阿里巴巴集团内被广泛使用的离线数据同步工具/平台，实现包括 MySQL、Oracle、SqlServer、Postgre、HDFS、Hive、ADS、HBase、TableStore(OTS)、MaxCompute(ODPS)、DRDS 等各种异构数据源之间高效的数据同步功能。相关项目：<a href="https://github.com/WeiYe-Jing/datax-web">DataX-Web</a> （DataX 集成可视化页面，选择数据源即可一键生成数据同步任务）。</li></ul><p>其他：<a href="https://github.com/DTStack/flinkx">Flinkx</a> （基于 Flink 的分布式数据同步工具）。</p><h3 id="时序数据库">时序数据库</h3><ul><li><a href="https://github.com/apache/iotdb">IoTDB</a>：一款 Java 语言编写的国产时序数据库，为用户提供数据收集、存储和分析等服务。与 Hadoop、Spark 和可视化工具(如 Grafana)无缝集成，满足了工业 IoT 领域中海量数据存储、高吞吐量数据写入和复杂数据查询分析的需求。</li><li><a href="https://github.com/kairosdb/kairosdb">KairosDB</a>：一个基于 Cassandra 的快速分布式可扩展时间序列数据库。</li></ul><h2 id="搜索引擎">搜索引擎</h2><ul><li><a href="https://github.com/elastic/elasticsearch" title="elasticsearch">Elasticsearch</a> （推荐）：开源，分布式，RESTful 搜索引擎。</li><li><a href="https://github.com/meilisearch/meilisearch">Meilisearch</a>：一个功能强大、快速、开源、易于使用和部署的搜索引擎，支持中文搜索（不需要添加额外的配置）。</li><li><a href="https://lucene.apache.org/solr/">Solr</a> : Solr（读作“solar”）是 Apache Lucene 项目的开源企业搜索平台。</li><li><a href="https://gitee.com/dromara/easy-es">Easy-ES</a>：傻瓜级 ElasticSearch 搜索引擎 ORM 框架。</li></ul><h2 id="测试">测试</h2><h3 id="测试框架">测试框架</h3><ul><li><a href="http://junit.org/">JUnit</a> : Java 测试框架。</li><li><a href="https://github.com/mockito/mockito">Mockito</a>：Mockito 是一个模拟测试框架，可以让你用优雅，简洁的接口写出漂亮的单元测试。（对那些不容易构建的对象用一个虚拟对象来代替，使其在调试期间用来作为真实对象的替代品）</li><li><a href="https://github.com/powermock/powermock">PowerMock</a>：编写单元测试仅靠 Mockito 是不够。因为 Mockito 无法 mock 私有方法、final 方法及静态方法等。PowerMock 这个 framework，主要是为了扩展其他 mock 框架，如 Mockito、EasyMock。它使用一个自定义的类加载器，纂改字节码，突破 Mockito 无法 mock 静态方法、构造方法、final 类、final 方法以及私有方法的限制。</li><li><a href="https://github.com/tomakehurst/wiremock">WireMock</a>：模拟 HTTP 服务的工具（Mock your APIs）。</li><li><a href="https://github.com/testcontainers/testcontainers-java">Testcontainers</a>：一个支持 JUnit 的测试工具库，提供轻量级的且一次性的常见数据库测试支持、Selenium Web 浏览器或者其他任何可以在 Docker 容器中运行的实例支持。</li></ul><p>相关阅读：</p><ul><li><a href="https://martinfowler.com/articles/practical-test-pyramid.html">The Practical Test Pyramid- Martin Fowler</a> (很赞的一篇文章，不过是英文的)</li><li><a href="https://juejin.im/post/6844903982058618894">浅谈测试之 PowerMock</a></li></ul><h3 id="测试平台">测试平台</h3><ul><li><a href="https://github.com/metersphere/metersphere">MeterSphere</a> : 一站式开源持续测试平台，涵盖测试跟踪、接口测试、性能测试、团队协作等功能，全面兼容 JMeter、Postman、Swagger 等开源、主流标准。</li><li><a href="https://www.apifox.cn/">Apifox</a>：API 文档、API 调试、API Mock、API 自动化测试。</li></ul><h3 id="api-调试">API 调试</h3><ul><li><a href="https://insomnia.rest/">Insomnia</a> :像人类而不是机器人一样调试 API。我平时经常用的，界面美观且轻量，总之很喜欢。</li><li><a href="https://www.getpostman.com/">Postman</a>：API 请求生成器。</li><li><a href="https://github.com/liyasthomas/postwoman" title="postwoman">Postwoman</a>：API 请求生成器-一个免费、快速、漂亮的 Postma 替代品。</li><li><a href="https://gitee.com/dromara/fast-request">Restful Fast Request</a>：IDEA 版 Postman，API 调试工具 + API 管理工具 + API 搜索工具。</li></ul><h2 id="任务调度">任务调度</h2><ul><li><a href="https://github.com/quartz-scheduler/quartz">Quartz</a>：一个很火的开源任务调度框架，Java 定时任务领域的老大哥或者说参考标准， 很多其他任务调度框架都是基于 <code>quartz</code> 开发的，比如当当网的<code>elastic-job</code>就是基于<code>quartz</code>二次开发之后的分布式调度解决方案</li><li><a href="https://github.com/xuxueli/xxl-job">XXL-JOB</a> :XXL-JOB 是一个分布式任务调度平台，其核心设计目标是开发迅速、学习简单、轻量级、易扩展。现已开放源代码并接入多家公司线上产品线，开箱即用。</li><li><a href="http://elasticjob.io/index_zh.html">Elastic-Job</a>：Elastic-Job 是当当网开源的一个基于 Quartz 和 Zookeeper 的分布式调度解决方案，由两个相互独立的子项目 Elastic-Job-Lite 和 Elastic-Job-Cloud 组成，一般我们只要使用 Elastic-Job-Lite 就好。</li><li><a href="https://github.com/analysys/EasyScheduler" title="EasyScheduler">EasyScheduler</a> （已经更名为 DolphinScheduler，已经成为 Apache 孵化器项目）：分布式易扩展的可视化工作流任务调度平台，主要解决“复杂任务依赖但无法直接监控任务健康状态”的问题。</li><li><a href="https://gitee.com/KFCFans/PowerJob">PowerJob</a>：新一代分布式任务调度与计算框架，支持 CRON、API、固定频率、固定延迟等调度策略，提供工作流来编排任务解决依赖关系，使用简单，功能强大，文档齐全，欢迎各位接入使用！<a href="http://www.powerjob.tech/">http://www.powerjob.tech/</a> 。</li></ul><h2 id="分布式">分布式</h2><h3 id="api-网关">API 网关</h3><ul><li><a href="https://github.com/Kong/kong" title="kong">Kong</a>：Kong 是一个云原生、快速的、可伸缩的分布式微服务抽象层(也称为 API 网关、API 中间件或在某些情况下称为服务网格)。2015 年作为开源项目发布，其核心价值是高性能和可扩展性。</li><li><a href="https://github.com/Dromara/soul" title="soul">ShenYu</a>：适用于所有微服务的可伸缩、高性能、响应性 API 网关解决方案。</li><li><a href="https://github.com/spring-cloud/spring-cloud-gateway">Spring Cloud Gateway</a> : 基于 Spring Framework 5.x 和 Spring Boot 2.x 构建的高性能网关。</li><li><a href="https://github.com/Netflix/zuul">Zuul</a> : Zuul 是一个 L7 应用程序网关，它提供了动态路由，监视，弹性，安全性等功能。</li></ul><h3 id="配置中心">配置中心</h3><ul><li><a href="https://github.com/ctripcorp/apollo" title="apollo">Apollo</a>（推荐）：Apollo（阿波罗）是携程框架部门研发的分布式配置中心，能够集中化管理应用不同环境、不同集群的配置，配置修改后能够实时推送到应用端，并且具备规范的权限、流程治理等特性，适用于微服务配置管理场景。</li><li><a href="https://github.com/alibaba/nacos">Nacos</a>（推荐）：Nacos 是 Spring Cloud Alibaba 提供的服务注册发现组件，类似于 Consul、Eureka。并且，提供了分布式配置管理功能。</li><li><a href="https://github.com/spring-cloud/spring-cloud-config">Spring Cloud Config</a>：Spring Cloud Config 是 Spring Cloud 家族中最早的配置中心，虽然后来又发布了 Consul 可以代替配置中心功能，但是 Config 依然适用于 Spring Cloud 项目，通过简单的配置即可实现功能。</li><li><a href="https://github.com/hashicorp/consul">Consul</a>：Consul 是 HashiCorp 公司推出的开源软件，提供了微服务系统中的服务治理、配置中心、控制总线等功能。这些功能中的每一个都可以根据需要单独使用，也可以一起使用以构建全方位的服务网格，总之 Consul 提供了一种完整的服务网格解决方案。</li></ul><h3 id="链路追踪">链路追踪</h3><ul><li><a href="https://github.com/apache/skywalking" title="skywalking">Skywalking</a> : 针对分布式系统的应用性能监控，尤其是针对微服务、云原生和面向容器的分布式系统架构。</li><li><a href="https://github.com/openzipkin/zipkin" title="zipkin">Zipkin</a>：Zipkin 是一个分布式跟踪系统。它有助于收集解决服务体系结构中的延迟问题所需的时序数据。功能包括该数据的收集和查找。</li><li><a href="https://github.com/dianping/cat" title="cat">CAT</a>：CAT 作为服务端项目基础组件，提供了 Java, C/C++, Node.js, Python, Go 等多语言客户端，已经在美团点评的基础架构中间件框架（MVC 框架，RPC 框架，数据库框架，缓存框架等，消息队列，配置系统等）深度集成，为美团点评各业务线提供系统丰富的性能指标、健康状况、实时告警等。</li></ul><p>相关阅读：<a href="https://skywalking.apache.org/zh/blog/2019-03-29-introduction-of-skywalking-and-simple-practice.html">Skywalking 官网对于主流开源链路追踪系统的对比</a></p><h3 id="分布式锁">分布式锁</h3><ul><li><a href="https://gitee.com/baomidou/lock4j">Lock4j</a>：支持 Redisson、ZooKeeper 等不同方案的高性能分布式锁。</li><li><a href="https://github.com/redisson/redisson" title="redisson">Redisson</a>：Redisson 在分布式锁方面提供全面且强大的支持，超越了简单的 Redis 锁实现。</li></ul><h2 id="高性能">高性能</h2><h3 id="多线程">多线程</h3><ul><li><a href="https://github.com/opengoofy/hippo4j">Hippo4j</a>：异步线程池框架，支持线程池动态变更&amp;监控&amp;报警，无需修改代码轻松引入。支持多种使用模式，轻松引入，致力于提高系统运行保障能力。</li><li><a href="https://github.com/dromara/dynamic-tp">Dynamic Tp</a>：轻量级动态线程池，内置监控告警功能，集成三方中间件线程池管理，基于主流配置中心（已支持 Nacos、Apollo，Zookeeper、Consul、Etcd，可通过 SPI 自定义实现）。</li><li><a href="https://gitee.com/jd-platform-opensource/asyncTool">asyncTool</a> : 京东的一位大佬开源的多线程工具库，里面大量使用到了 <code>CompletableFuture</code> ，可以解决任意的多线程并行、串行、阻塞、依赖、回调的并行框架，可以任意组合各线程的执行顺序，带全链路执行结果回调。</li></ul><h3 id="缓存">缓存</h3><h4 id="本地缓存">本地缓存</h4><ul><li><a href="https://github.com/ben-manes/caffeine">Caffeine</a> : 一款强大的本地缓存解决方案，性能非常强大。</li><li><a href="https://github.com/google/guava">Guava</a>：Google Java 核心库，内置了比较完善的本地缓存实现。</li><li><a href="https://github.com/snazy/ohc">OHC</a> ：Java 堆外缓存解决方案（项目从 2021 年开始就不再进行维护了）。</li></ul><h4 id="分布式缓存">分布式缓存</h4><ul><li><a href="https://github.com/redis/redis">Redis</a>：一个使用 C 语言开发的内存数据库，分布式缓存首选。</li><li><a href="https://github.com/dragonflydb/dragonfly">Dragonfly</a>：一种针对现代应用程序负荷需求而构建的内存数据库，完全兼容 Redis 和 Memcached 的 API，迁移时无需修改任何代码，号称全世界最快的内存数据库。</li><li><a href="https://github.com/Snapchat/KeyDB">KeyDB</a>： Redis 的一个高性能分支，专注于多线程、内存效率和高吞吐量。</li></ul><h4 id="多级缓存">多级缓存</h4><ul><li><a href="https://gitee.com/ld/J2Cache">J2Cache</a>：基于本地内存和 Redis 的两级 Java 缓存框架。</li><li><a href="https://github.com/alibaba/jetcache">JetCache</a>：阿里开源的缓存框架，支持多级缓存、分布式缓存自动刷新、 TTL 等功能。</li></ul><h3 id="消息队列">消息队列</h3><p><strong>分布式队列</strong>：</p><ul><li><a href="https://github.com/apache/rocketmq" title="RocketMQ">RocketMQ</a>：阿里巴巴开源的一款高性能、高吞吐量的分布式消息中间件。</li><li><a href="https://github.com/apache/kafka" title="Kafaka">Kafaka</a>: Kafka 是一种分布式的，基于发布 / 订阅的消息系统。</li><li><a href="https://github.com/rabbitmq" title="RabbitMQ">RabbitMQ</a> :由 erlang 开发的基于 AMQP（Advanced Message Queue 高级消息队列协议）协议实现的消息队列。</li></ul><p><strong>内存队列</strong>：</p><ul><li><a href="https://github.com/LMAX-Exchange/disruptor">Disruptor</a>：Disruptor 是英国外汇交易公司 LMAX 开发的一个高性能队列，研发的初衷是解决内存队列的延迟问题（在性能测试中发现竟然与 I/O 操作处于同样的数量级）。</li></ul><h3 id="读写分离和分库分表">读写分离和分库分表</h3><ul><li><a href="https://github.com/apache/shardingsphere">ShardingSphere</a>：ShardingSphere 是一套开源的分布式数据库中间件解决方案组成的生态圈，它由 Sharding-JDBC、Sharding-Proxy 和 Sharding-Sidecar（计划中）这 3 款相互独立的产品组成。</li><li><a href="https://github.com/MyCatApache/MyCat2">MyCat</a> : MyCat 是数据库分库分表的中间件，MyCat 使用最多的两个功能是：读写分离和分库分表。MyCat 是一些社区爱好者在阿里 Cobar 的基础上进行二次开发，解决了 Cobar 当时存 在的一些问题，并且加入了许多新的功能在其中。</li><li><a href="https://github.com/baomidou/dynamic-datasource-spring-boot-starter">dynamic-datasource-spring-boot-starter</a>：一个基于 Spring Boot 的快速集成多数据源的启动器，支持多数据源、动态数据源、主从分离、读写分离和分布式事务。</li></ul><h2 id="高可用">高可用</h2><h3 id="限流">限流</h3><p>分布式限流：</p><ul><li><a href="https://github.com/alibaba/Sentinel">Sentinel</a>（推荐）：面向分布式服务架构的高可用防护组件，主要以流量为切入点，从流量控制、熔断降级、系统自适应保护等多个维度来帮助用户保障微服务的稳定性。</li><li><a href="https://github.com/Netflix/Hystrix">Hystrix</a>：类似于 Sentinel 。</li></ul><p>相关阅读：<a href="https://sentinelguard.io/zh-cn/blog/sentinel-vs-hystrix.html">Sentinel 与 Hystrix 的对比</a>。</p><p>单机限流：</p><ul><li><a href="https://github.com/vladimir-bukhtoyarov/bucket4j">Bucket4j</a>：一个非常不错的基于令牌/漏桶算法的限流库。</li><li><a href="https://github.com/resilience4j/resilience4j">Resilience4j</a>：一个轻量级的容错组件，其灵感来自于 Hystrix。</li></ul><h3 id="监控">监控</h3><ul><li><a href="https://github.com/codecentric/spring-boot-admin">Spring Boot Admin</a>：管理和监控 Spring Boot 应用程序。</li><li><a href="https://github.com/dropwizard/metrics">Metrics</a>：捕获 JVM 和应用程序级别的指标。所以你知道发生了什么事。</li></ul><h3 id="日志">日志</h3><ul><li>EKL 老三件套 : 最原始的时候，ELK 是由 3 个开源项目的首字母构成，分别是 Elasticsearch、Logstash、Kibana。</li><li>新一代 ELK 架构 : Elasticsearch+Logstash+Kibana+Beats。</li><li>EFK : EFK 中的 F 代表的是 <a href="https://github.com/fluent/fluentd">Fluentd</a>。</li><li><a href="https://gitee.com/dromara/TLog">TLog</a>：一个轻量级的分布式日志标记追踪神器，10 分钟即可接入，自动对日志打标签完成微服务的链路追踪。</li></ul><h2 id="字节码操作">字节码操作</h2><ul><li><a href="https://asm.ow2.io/">ASM</a>：通用 Java 字节码操作和分析框架。它可用于直接以二进制形式修改现有类或动态生成类。</li><li><a href="https://github.com/raphw/byte-buddy">Byte Buddy</a>：Java 字节码生成和操作库，用于在 Java 应用程序运行时创建和修改 Java 类，无需使用编译器</li><li><a href="https://github.com/jboss-javassist/javassist">Javassist</a>：动态编辑 Java 字节码的类库。</li><li><a href="https://github.com/Col-E/Recaf">Recaf</a>：现代 Java 字节码编辑器，基于 ASM（Java 字节码操作框架） 来修改字节码，可简化编辑已编译 Java 应用程序的过程。</li></ul>]]>
                </content>
            </entry>
            <entry>
                <title><![CDATA[Google Drive搭建机器学习环境]]></title>
                <link rel="alternate" type="text/html" href="https://www.wangdaye.net/archives/g-o-o-g-l-e--d-r-i-v-e-da-jian-ji-qi-xue-xi-huan-jing" />
                <id>tag:https://www.wangdaye.net,2024-08-15:g-o-o-g-l-e--d-r-i-v-e-da-jian-ji-qi-xue-xi-huan-jing</id>
                <published>2024-08-15T21:55:58+08:00</published>
                <updated>2024-08-15T21:57:21+08:00</updated>
                <author>
                    <name>王大爷</name>
                    <uri>https://www.wangdaye.net</uri>
                </author>
                <content type="html">
                        <![CDATA[<p>1.打开Google Drive创建一个<a href="https://so.csdn.net/so/search?q=notebook&amp;spm=1001.2101.3001.7020">notebook</a>，将深度学习网络、训练权重和数据集上传到云端硬盘，文件拖拉过去就可以。</p><p><img src="https://www.wangdaye.net/upload/2024/08/image-9f22af7682c84d74980e0e846faf5422.png" alt="image.png" /></p><p>2在文件夹内，右键单击，选择连接colab</p><p><img src="https://www.wangdaye.net/upload/2024/08/image-95ea183e18a64bddb0a3e2954e4f72e5.png" alt="image.png" /><img src="https://i-blog.csdnimg.cn/blog_migrate/66637cdba0ef1f1d102d61500a3103ec.png" alt="" /></p><p>3. 更改为GPU训练</p><p><img src="https://www.wangdaye.net/upload/2024/08/image-22b7dc7e42cd490b85682d59a6426826.png" alt="image.png" /></p><p><img src="https://www.wangdaye.net/upload/2024/08/image-6efa5c667c8e49f199be3d9119b64186.png" alt="image.png" /><img src="https://i-blog.csdnimg.cn/blog_migrate/6a2b4c34d882a2b4c3ab87184e2bbb18.png" alt="" /></p><p>5. 加载盘</p><blockquote><p>from google.colab import drive<br />drive.mount('/content/drive/')</p></blockquote><p>6. 切换到你要跑的目录下面</p><blockquote><h1 id="指定当前的工作文件夹">指定当前的工作文件夹</h1><p>import os</p><h1 id="此处为google-drive中的文件路径drive为之前指定的工作根目录要加上">此处为google drive中的文件路径,drive为之前指定的工作根目录，要加上</h1><p>os.chdir(&quot;/content/drive/MyDrive/colab/Notebooks/UNet&quot;) </p></blockquote><p>7. 安装Pytorch以及torchvision</p><blockquote><p>Colab 一般情况下已经自带了pytorch环境了。若没有可以进行相应的安装：<br />!pip install torch torchvision  # 在Colab中执行操作语句时，感叹号不能漏</p></blockquote><p>8. 执行训练命令</p><blockquote><p>!python train.py</p></blockquote><p> 9. 训练完成的模型权重在logs文件夹内</p><blockquote><div id="article_content" class="article_content clearfix"></blockquote><div id="content_views" class="markdown_views prism-atom-one-dark"><blockquote><p>由于部分训练的需要，数据集可以上传到云端硬盘Google Drive，然后再Colab中加载云端硬盘读取数据进行训练，具体实现过程如下：<br />挂载云端硬盘：</p></blockquote><pre><code> from google.colab import drive from google.colab import files    drive.mount('/content/drive')云端硬盘挂载成功后的路径为：    /content/drive/My Drive/</code></pre></div><link href="https://csdnimg.cn/release/blogv2/dist/mdeditor/css/editerView/markdown_views-f23dff6052.css" rel="stylesheet"><link href="https://csdnimg.cn/release/blogv2/dist/mdeditor/css/style-c216769e99.css" rel="stylesheet"></div>]]>
                </content>
            </entry>
            <entry>
                <title><![CDATA[从手写数字识别入门深度学习丨MNIST数据集详解]]></title>
                <link rel="alternate" type="text/html" href="https://www.wangdaye.net/archives/cong-shou-xie-shu-zi-shi-bie-ru-men-shen-du-xue-xi-gun-m-n-i-s-t-shu-ju-ji-xiang-jie" />
                <id>tag:https://www.wangdaye.net,2024-05-31:cong-shou-xie-shu-zi-shi-bie-ru-men-shen-du-xue-xi-gun-m-n-i-s-t-shu-ju-ji-xiang-jie</id>
                <published>2024-05-31T08:41:51+08:00</published>
                <updated>2024-05-31T08:41:51+08:00</updated>
                <author>
                    <name>王大爷</name>
                    <uri>https://www.wangdaye.net</uri>
                </author>
                <content type="html">
                        <![CDATA[<p><span style="background-color:#ffffff;"><span style="color:#222222;">就像无数人从敲下“Hello World”开始代码之旅一样，许多研究员从“MNIST数据集”开启了人工智能的探索之路。</span></span></p><p><span style="background-color:#ffffff;"><span style="color:#222222;">MNIST数据集（Mixed National Institute of Standards and Technology database）是一个用来训练各种图像处理系统的二进制图像数据集，广泛应用于机器学习中的训练和测试。</span></span></p><p><span style="background-color:#ffffff;"><span style="color:#222222;">作为一个入门级的计算机视觉数据集，发布20多年来，它已经被无数机器学习入门者“咀嚼”千万遍，是最受欢迎的深度学习数据集之一。</span></span></p><p><span style="background-color:#ffffff;"><span style="color:#222222;">今天就让我们来一睹真容。</span></span></p><p><strong>目录</strong></p><p><a href="#t0">一、数据集简介</a></p><p><a href="#t1">二、数据集详细信息</a></p><p><a href="#t2">三、数据集任务定义及介绍</a></p><p><a href="#t3">图像分类</a></p><p><a href="#t4">四、数据集文件结构解读</a></p><p><a href="#t5">五、数据集下载链接</a></p><hr /><h2 id="一数据集简介"><a name="t0"></a><strong><span style="background-color:#ffffff;"><span style="color:#444444;">一、数据集简介</span></span></strong></h2><p><strong><span style="background-color:#ffffff;"><span style="color:#1b67ff;">发布方：</span></span></strong><span style="background-color:#ffffff;"><span style="color:#222222;">National Institute of Standards and Technology(美国国家标准技术研究所，简称NIST)</span></span></p><p><strong><span style="background-color:#ffffff;"><span style="color:#1b67ff;">发布时间：</span></span></strong><span style="background-color:#ffffff;"><span style="color:#222222;">1998</span></span></p><p><strong><span style="background-color:#ffffff;"><span style="color:#1b67ff;">背景：</span></span></strong></p><p><span style="background-color:#ffffff;"><span style="color:#222222;">该数据集的论文想要证明在模式识别问题上，基于CNN的方法可以取代之前的基于手工特征的方法，所以作者创建了一个手写数字的数据集，以手写数字识别作为例子证明CNN在模式识别问题上的优越性。</span></span></p><p><strong><span style="background-color:#ffffff;"><span style="color:#1b67ff;">简介：</span></span></strong></p><p><span style="background-color:#ffffff;"><span style="color:#222222;">MNIST数据集是从NIST的两个手写数字数据集：Special Database 3 和Special Database 1中分别取出部分图像，并经过一些图像处理后得到的。</span></span></p><p><span style="background-color:#ffffff;"><span style="color:#222222;">MNIST数据集共有70000张图像，其中训练集60000张，测试集10000张。所有图像都是28×28的灰度图像，每张图像包含一个手写数字。</span></span></p><h2 id="二数据集详细信息"><a name="t1"></a><strong><span style="background-color:#ffffff;"><span style="color:#444444;">二、数据集详细信息</span></span></strong></h2><p><strong><span style="background-color:#ffffff;"><span style="color:#1b67ff;">1. 数据量</span></span></strong></p><p><strong><span style="background-color:#ffffff;"><span style="color:#222222;">训练集60000张图像</span></span></strong><span style="background-color:#ffffff;"><span style="color:#222222;">，其中30000张来自NIST的Special Database 3，30000张来自NIST的Special Database 1。</span></span></p><p><strong><span style="background-color:#ffffff;"><span style="color:#222222;">测试集10000张图像</span></span></strong><span style="background-color:#ffffff;"><span style="color:#222222;">，其中5000张来自NIST的Special Database 3，5000张来自NIST的Special Database 1。</span></span></p><p><strong><span style="background-color:#ffffff;"><span style="color:#1b67ff;">2. 标注量</span></span></strong></p><p><span style="background-color:#ffffff;"><span style="color:#222222;">每张图像都有标注。</span></span></p><p><strong><span style="background-color:#ffffff;"><span style="color:#1b67ff;">3. 标注类别</span></span></strong></p><p><span style="background-color:#ffffff;"><span style="color:#222222;">共10个类别，每个类别代表0~9之间的一个数字，每张图像只有一个类别。</span></span></p><p><strong><span style="color:#1b67ff;">4. 可视化</span></strong></p><p><img src="https://img-blog.csdnimg.cn/img_convert/77336419715c882542f34abd07d79368.png" alt="" /></p><p><span style="background-color:#ffffff;"><span style="color:#888888;">图1：MNIST样例图</span></span></p><p><span style="background-color:#ffffff;"><span style="color:#222222;">NIST原始的Special Database 3 数据集和Special Database 1数据集均是二值图像，MNIST从这两个数据集中取出图像后，通过图像处理方法使得每张图像都变成28×28大小的灰度图像，且手写数字在图像中居中显示。</span></span></p><h2 id="三数据集任务定义及介绍"><a name="t2"></a><strong><span style="background-color:#ffffff;"><span style="color:#444444;">三、数据集任务定义及介绍</span></span></strong></h2><h3 id="图像分类"><a name="t3"></a><strong><span style="color:#1b67ff;">图像分类</span></strong></h3><p><strong><span style="background-color:#ffffff;"><span style="color:#222222;">● 图像分类定义</span></span></strong></p><p><span style="background-color:#ffffff;"><span style="color:#222222;">图像分类是计算机视觉领域中，基于语义信息对不同图像进行分类的一种模式识别方法。</span></span></p><p><strong><span style="background-color:#ffffff;"><span style="color:#222222;">● 图像分类评价指标</span></span></strong></p><p><strong><span style="background-color:#ffffff;"><span style="color:#222222;">a. Accuracy：</span></span></strong></p><p><span style="background-color:#ffffff;"><span style="color:#222222;">n_correct / n_total，标签预测正确的样本占所有样本的比例。</span></span></p><p><strong><span style="background-color:#ffffff;"><span style="color:#222222;">b. 某个类别的Precision：</span></span></strong></p><p><span style="background-color:#ffffff;"><span style="color:#222222;">TP/(TP+FP)，被预测为该类别的样本中，有多少样本是预测正确的。</span></span></p><p><strong><span style="background-color:#ffffff;"><span style="color:#222222;">c. 某个类别的Recall：</span></span></strong></p><p><span style="background-color:#ffffff;"><span style="color:#222222;">TP/(TP+FN)，在该类别的样本中，有多少样本是预测正确的。</span></span></p><p><span style="background-color:#ffffff;"><span style="color:#222222;">注：在上面的评价指标中，TP代表True Positive，FP代表False Positive，FN代表False Negative，n_correct代表所有预测正确的样本数量，n_total代表所有的样本数量。</span></span></p><h2 id="四数据集文件结构解读"><a name="t4"></a><strong><span style="background-color:#ffffff;"><span style="color:#444444;">四、数据集文件结构解读</span></span></strong></h2><p><strong><span style="background-color:#ffffff;"><span style="color:#1b67ff;">1. 目录结构</span></span></strong></p><p><strong><span style="background-color:#ffffff;"><span style="color:#222222;">● 解压前</span></span></strong></p><pre><code>dataset_compressed/├── t10k-images-idx3-ubyte.gz                #测试集图像压缩包(1648877 bytes)├── t10k-labels-idx1-ubyte.gz                #测试集标签压缩包(4542 bytes)├── train-images-idx3-ubyte.gz                #训练集图像压缩包(9912422 bytes)└── train-labels-idx1-ubyte.gz                #训练集标签压缩包(28881 bytes)</code></pre><p><strong><span style="background-color:#ffffff;"><span style="color:#222222;">● 解压后</span></span></strong></p><pre><code>dataset_uncompressed/├── t10k-images-idx3-ubyte                #测试集图像数据├── t10k-labels-idx1-ubyte                #测试集标签数据├── train-images-idx3-ubyte                #训练集图像数据└── train-labels-idx1-ubyte                #训练集标签数据</code></pre><p><strong><span style="background-color:#ffffff;"><span style="color:#1b67ff;">2. 文件结构</span></span></strong></p><p><span style="background-color:#ffffff;"><span style="color:#222222;">该数据集将图像和标签都以矩阵的形式存储于一种称为idx格式的二进制文件中。该数据集的4个二进制文件的存储格式分别如下：</span></span></p><p><span style="background-color:#ffffff;"><span style="color:#222222;">● <strong>训练集标签数据 (train-labels-idx1-ubyte):</strong></span></span></p><div class="table-box"><table border="1" cellspacing="0" style="margin-left:23.3pt;"><tbody><tr><td style="border-color:#dfe2e5;width:117pt;"><p><strong><span style="color:#000000;">偏移量</span>****<span style="color:#000000;">(bytes)</span></strong></p></td><td style="width:104.25pt;"><p><strong><span style="color:#000000;">值类型</span></strong></p></td><td style="width:126.75pt;"><p><strong><span style="color:#000000;">数值</span></strong></p></td><td style="width:138pt;"><p><strong><span style="color:#000000;">含义</span></strong></p></td></tr><tr><td style="border-color:#dfe2e5;width:117pt;"><p><span style="color:#000000;">0</span></p></td><td style="width:104.25pt;"><p><span style="color:#000000;">32</span><span style="color:#000000;">位整型</span></p></td><td style="width:126.75pt;"><p><span style="color:#000000;">0x00000801</span></p><p><span style="color:#000000;">(2049)</span></p></td><td style="width:138pt;"><p><span style="color:#000000;">magic number</span></p></td></tr><tr><td style="border-color:#dfe2e5;width:117pt;"><p><span style="color:#000000;">4</span></p></td><td style="width:104.25pt;"><p><span style="color:#000000;">32</span><span style="color:#000000;">位整型</span></p></td><td style="width:126.75pt;"><p><span style="color:#000000;">60000</span></p></td><td style="width:138pt;"><p><span style="color:#000000;">有效数值的数量</span></p><p><span style="color:#000000;">(</span><span style="color:#000000;">即标签的数量</span><span style="color:#000000;">)</span></p></td></tr><tr><td style="border-color:#dfe2e5;width:117pt;"><p><span style="color:#000000;">8</span></p></td><td style="width:104.25pt;"><p><span style="color:#000000;">8</span><span style="color:#000000;">位无符号整型</span></p></td><td style="width:126.75pt;"><p><span style="color:#000000;">不定</span></p><p><span style="color:#000000;">(0~9</span><span style="color:#000000;">之间</span><span style="color:#000000;">)</span></p></td><td style="width:138pt;"><p><span style="color:#000000;">标签</span></p></td></tr><tr><td style="border-color:#dfe2e5;width:117pt;"><p><span style="color:#000000;">...</span></p></td><td style="width:104.25pt;"><p><span style="color:#000000;">...</span></p></td><td style="width:126.75pt;"><p><span style="color:#000000;">...</span></p></td><td style="width:138pt;"><p><span style="color:#000000;">...</span></p></td></tr><tr><td style="border-color:#dfe2e5;width:117pt;"><p><span style="color:#000000;">xxxx</span></p></td><td style="width:104.25pt;"><p><span style="color:#000000;">8</span><span style="color:#000000;">位无符号整型</span></p></td><td style="width:126.75pt;"><p><span style="color:#000000;">不定</span></p><p><span style="color:#000000;">(0~9</span><span style="color:#000000;">之间</span><span style="color:#000000;">)</span></p></td><td style="width:138pt;"><p><span style="color:#000000;">标签</span></p></td></tr></tbody></table></div><p><strong><span style="background-color:#ffffff;"><span style="color:#222222;">● 训练集图像数据(train-images-idx3-ubyte):</span></span></strong></p><div class="table-box"><table border="1" cellspacing="0" style="margin-left:23.3pt;"><tbody><tr><td style="border-color:#dfe2e5;width:118.5pt;"><p><strong><span style="color:#000000;">偏移量</span>****<span style="color:#000000;">(bytes)</span></strong></p></td><td style="width:102.75pt;"><p><strong><span style="color:#000000;">值类型</span></strong></p></td><td style="width:127.5pt;"><p><strong><span style="color:#000000;">数值</span></strong></p></td><td style="width:135.75pt;"><p><strong><span style="color:#000000;">含义</span></strong></p></td></tr><tr><td style="border-color:#dfe2e5;width:118.5pt;"><p><span style="color:#000000;">0</span></p></td><td style="width:102.75pt;"><p><span style="color:#000000;">32</span><span style="color:#000000;">位整型</span></p></td><td style="width:127.5pt;"><p><span style="color:#000000;">0x00000803</span></p><p><span style="color:#000000;">(2051)</span></p></td><td style="width:135.75pt;"><p><span style="color:#000000;">magic number</span></p></td></tr><tr><td style="border-color:#dfe2e5;width:118.5pt;"><p><span style="color:#000000;">4</span></p></td><td style="width:102.75pt;"><p><span style="color:#000000;">32</span><span style="color:#000000;">位整型</span></p></td><td style="width:127.5pt;"><p><span style="color:#000000;">60000</span></p></td><td style="width:135.75pt;"><p><span style="color:#000000;">有效数值的数量</span></p><p><span style="color:#000000;">(</span><span style="color:#000000;">即图像的数量</span><span style="color:#000000;">)</span></p></td></tr><tr><td style="border-color:#dfe2e5;width:118.5pt;"><p><span style="color:#000000;">8</span></p></td><td style="width:102.75pt;"><p><span style="color:#000000;">32</span><span style="color:#000000;">位整型</span></p></td><td style="width:127.5pt;"><p><span style="color:#000000;">28</span></p></td><td style="width:135.75pt;"><p><span style="color:#000000;">图像的高</span></p><p><span style="color:#000000;">(rows)</span></p></td></tr><tr><td style="border-color:#dfe2e5;width:118.5pt;"><p><span style="color:#000000;">12</span></p></td><td style="width:102.75pt;"><p><span style="color:#000000;">32</span><span style="color:#000000;">位整型</span></p></td><td style="width:127.5pt;"><p><span style="color:#000000;">28</span></p></td><td style="width:135.75pt;"><p><span style="color:#000000;">图像的宽</span></p><p><span style="color:#000000;">(columns)</span></p></td></tr><tr><td style="border-color:#dfe2e5;width:118.5pt;"><p><span style="color:#000000;">16</span></p></td><td style="width:102.75pt;"><p><span style="color:#000000;">8</span><span style="color:#000000;">位无符号整型</span></p></td><td style="width:127.5pt;"><p><span style="color:#000000;">不定</span></p><p><span style="color:#000000;">(0~255</span><span style="color:#000000;">之间</span><span style="color:#000000;">)</span></p></td><td style="width:135.75pt;"><p><span style="color:#000000;">图像内容</span></p></td></tr><tr><td style="border-color:#dfe2e5;width:118.5pt;"><p><span style="color:#000000;">...</span></p></td><td style="width:102.75pt;"><p><span style="color:#000000;">...</span></p></td><td style="width:127.5pt;"><p><span style="color:#000000;">...</span></p></td><td style="width:135.75pt;"><p><span style="color:#000000;">...</span></p></td></tr><tr><td style="border-color:#dfe2e5;width:118.5pt;"><p><span style="color:#000000;">xxxx</span></p></td><td style="width:102.75pt;"><p><span style="color:#000000;">8</span><span style="color:#000000;">位无符号整型</span></p></td><td style="width:127.5pt;"><p><span style="color:#000000;">不定</span></p><p><span style="color:#000000;">(0~255</span><span style="color:#000000;">之间</span><span style="color:#000000;">)</span></p></td><td style="width:135.75pt;"><p><span style="color:#000000;">图像内容</span></p></td></tr></tbody></table></div><p><strong><span style="background-color:#ffffff;"><span style="color:#222222;">● 测试集标签数据(t10k-labels-idx1-ubyte):</span></span></strong></p><div class="table-box"><table border="1" cellspacing="0" style="margin-left:23.3pt;"><tbody><tr><td style="border-color:#dfe2e5;width:120.75pt;"><p><strong><span style="color:#000000;">偏移量</span>****<span style="color:#000000;">(bytes)</span></strong></p></td><td style="width:103.5pt;"><p><strong><span style="color:#000000;">值类型</span></strong></p></td><td style="width:128.25pt;"><p><strong><span style="color:#000000;">数值</span></strong></p></td><td style="width:135.75pt;"><p><strong><span style="color:#000000;">含义</span></strong></p></td></tr><tr><td style="border-color:#dfe2e5;width:120.75pt;"><p><span style="color:#000000;">0</span></p></td><td style="width:103.5pt;"><p><span style="color:#000000;">32</span><span style="color:#000000;">位整型</span></p></td><td style="width:128.25pt;"><p><span style="color:#000000;">0x00000801</span></p><p><span style="color:#000000;">(2049)</span></p></td><td style="width:135.75pt;"><p><span style="color:#000000;">magic number</span></p></td></tr><tr><td style="border-color:#dfe2e5;width:120.75pt;"><p><span style="color:#000000;">4</span></p></td><td style="width:103.5pt;"><p><span style="color:#000000;">32</span><span style="color:#000000;">位整型</span></p></td><td style="width:128.25pt;"><p><span style="color:#000000;">10000</span></p></td><td style="width:135.75pt;"><p><span style="color:#000000;">有效数值的数量</span></p><p><span style="color:#000000;">(</span><span style="color:#000000;">即标签的数量</span><span style="color:#000000;">)</span></p></td></tr><tr><td style="border-color:#dfe2e5;width:120.75pt;"><p><span style="color:#000000;">8</span></p></td><td style="width:103.5pt;"><p><span style="color:#000000;">8</span><span style="color:#000000;">位无符号整型</span></p></td><td style="width:128.25pt;"><p><span style="color:#000000;">不定</span></p><p><span style="color:#000000;">(0~9</span><span style="color:#000000;">之间</span><span style="color:#000000;">)</span></p></td><td style="width:135.75pt;"><p><span style="color:#000000;">标签</span></p></td></tr><tr><td style="border-color:#dfe2e5;width:120.75pt;"><p><span style="color:#000000;">...</span></p></td><td style="width:103.5pt;"><p><span style="color:#000000;">...</span></p></td><td style="width:128.25pt;"><p><span style="color:#000000;">...</span></p></td><td style="width:135.75pt;"><p><span style="color:#000000;">...</span></p></td></tr><tr><td style="border-color:#dfe2e5;width:120.75pt;"><p><span style="color:#000000;">xxxx</span></p></td><td style="width:103.5pt;"><p><span style="color:#000000;">8</span><span style="color:#000000;">位无符号整型</span></p></td><td style="width:128.25pt;"><p><span style="color:#000000;">不定</span></p><p><span style="color:#000000;">(0~9</span><span style="color:#000000;">之间</span><span style="color:#000000;">)</span></p></td><td style="width:135.75pt;"><p><span style="color:#000000;">标签</span></p></td></tr></tbody></table></div><p><strong><span style="background-color:#ffffff;"><span style="color:#222222;">● 测试集图像数据 (t10k-images-idx3-ubyte):</span></span></strong></p><div class="table-box"><table border="1" cellspacing="0" style="margin-left:23.3pt;"><tbody><tr><td style="border-color:#dfe2e5;width:124.5pt;"><p><strong><span style="color:#000000;">偏移量</span>****<span style="color:#000000;">(bytes)</span></strong></p></td><td style="width:102.75pt;"><p><strong><span style="color:#000000;">值类型</span></strong></p></td><td style="width:127.5pt;"><p><strong><span style="color:#000000;">数值</span></strong></p></td><td style="width:135.75pt;"><p><strong><span style="color:#000000;">含义</span></strong></p></td></tr><tr><td style="border-color:#dfe2e5;width:124.5pt;"><p><span style="color:#000000;">0</span></p></td><td style="width:102.75pt;"><p><span style="color:#000000;">32</span><span style="color:#000000;">位整型</span></p></td><td style="width:127.5pt;"><p><span style="color:#000000;">0x00000803</span></p><p><span style="color:#000000;">(2051)</span></p></td><td style="width:135.75pt;"><p><span style="color:#000000;">magic number</span></p></td></tr><tr><td style="border-color:#dfe2e5;width:124.5pt;"><p><span style="color:#000000;">4</span></p></td><td style="width:102.75pt;"><p><span style="color:#000000;">32</span><span style="color:#000000;">位整型</span></p></td><td style="width:127.5pt;"><p><span style="color:#000000;">10000</span></p></td><td style="width:135.75pt;"><p><span style="color:#000000;">有效数值的数量</span></p><p><span style="color:#000000;">(</span><span style="color:#000000;">即图像的数量</span><span style="color:#000000;">)</span></p></td></tr><tr><td style="border-color:#dfe2e5;width:124.5pt;"><p><span style="color:#000000;">8</span></p></td><td style="width:102.75pt;"><p><span style="color:#000000;">32</span><span style="color:#000000;">位整型</span></p></td><td style="width:127.5pt;"><p><span style="color:#000000;">28</span></p></td><td style="width:135.75pt;"><p><span style="color:#000000;">图像的高</span></p><p><span style="color:#000000;">(rows)</span></p></td></tr><tr><td style="border-color:#dfe2e5;width:124.5pt;"><p><span style="color:#000000;">12</span></p></td><td style="width:102.75pt;"><p><span style="color:#000000;">32</span><span style="color:#000000;">位整型</span></p></td><td style="width:127.5pt;"><p><span style="color:#000000;">28</span></p></td><td style="width:135.75pt;"><p><span style="color:#000000;">图像的宽</span></p><p><span style="color:#000000;">(columns)</span></p></td></tr><tr><td style="border-color:#dfe2e5;width:124.5pt;"><p><span style="color:#000000;">16</span></p></td><td style="width:102.75pt;"><p><span style="color:#000000;">8</span><span style="color:#000000;">位无符号整型</span></p></td><td style="width:127.5pt;"><p><span style="color:#000000;">不定</span></p><p><span style="color:#000000;">(0~255</span><span style="color:#000000;">之间</span><span style="color:#000000;">)</span></p></td><td style="width:135.75pt;"><p><span style="color:#000000;">图像内容</span></p></td></tr><tr><td style="border-color:#dfe2e5;width:124.5pt;"><p><span style="color:#000000;">...</span></p></td><td style="width:102.75pt;"><p><span style="color:#000000;">...</span></p></td><td style="width:127.5pt;"><p><span style="color:#000000;">...</span></p></td><td style="width:135.75pt;"><p><span style="color:#000000;">...</span></p></td></tr><tr><td style="border-color:#dfe2e5;width:124.5pt;"><p><span style="color:#000000;">xxxx</span></p></td><td style="width:102.75pt;"><p><span style="color:#000000;">8</span><span style="color:#000000;">位无符号整型</span></p></td><td style="width:127.5pt;"><p><span style="color:#000000;">不定</span></p><p><span style="color:#000000;">(0~255</span><span style="color:#000000;">之间</span><span style="color:#000000;">)</span></p></td><td style="width:135.75pt;"><p><span style="color:#000000;">图像内容</span></p></td></tr></tbody></table></div><p><span style="background-color:#ffffff;"><span style="color:#222222;">对于idx格式的二进制文件，其基本格式如下：</span></span></p><pre><code> magic numbersize in dimension 0size in dimension 1size in dimension 2 .....size in dimension Ndata</code></pre><p><span style="background-color:#ffffff;"><span style="color:#222222;">每个idx文件都以magic number开头，magic number是一个4个字节，32位的整数，用于说明该idx文件的data字段存储的数据类型。</span></span></p><p><span style="background-color:#ffffff;"><span style="color:#222222;">其中前两个字节总是0，第3个字节不同的取值代表了idx文件中data部分不同的数值类型，对应关系如下：</span></span></p><div class="table-box"><table border="1" cellspacing="0" style="margin-left:23.3pt;"><tbody><tr><td style="border-color:#dfe2e5;width:90.75pt;"><p><strong><span style="color:#000000;">取值</span></strong></p></td><td style="width:225.75pt;"><p><strong><span style="color:#000000;">含义</span></strong></p></td></tr><tr><td style="border-color:#dee0e3;width:90.75pt;"><p><span style="color:#000000;">0x08</span></p></td><td style="width:225.75pt;"><p><span style="color:#000000;">8</span><span style="color:#000000;">位无符号整型</span><span style="color:#000000;">(unsigned char, 1 byte)</span></p></td></tr><tr><td style="border-color:#dee0e3;width:90.75pt;"><p><span style="color:#000000;">0x09</span></p></td><td style="width:225.75pt;"><p><span style="color:#000000;">8</span><span style="color:#000000;">位有符号整型</span><span style="color:#000000;">(char, 1 byte)</span></p></td></tr><tr><td style="border-color:#dee0e3;width:90.75pt;"><p><span style="color:#000000;">0x0B</span></p></td><td style="width:225.75pt;"><p><span style="color:#000000;">短整型</span><span style="color:#000000;">(short, 2 bytes)</span></p></td></tr><tr><td style="border-color:#dee0e3;width:90.75pt;"><p><span style="color:#000000;">0x0C</span></p></td><td style="width:225.75pt;"><p><span style="color:#000000;">整型</span> <span style="color:#000000;">(int, 4 bytes)</span></p></td></tr><tr><td style="border-color:#dee0e3;width:90.75pt;"><p><span style="color:#000000;">0x0D</span></p></td><td style="width:225.75pt;"><p><span style="color:#000000;">浮点型</span> <span style="color:#000000;">(float, 4 bytes)</span></p></td></tr><tr><td style="border-color:#dee0e3;width:90.75pt;"><p><span style="color:#000000;">0x0E</span></p></td><td style="width:225.75pt;"><p><span style="color:#000000;">双精度浮点型</span> <span style="color:#000000;">(double, 8 bytes)</span></p></td></tr></tbody></table></div><p><span style="background-color:#ffffff;"><span style="color:#222222;">在MNIST数据集的4个二进制文件中，data部分的数值类型都是“8位无符号整型”，所以magic number的第3个字节总是0x08。</span></span></p><p><span style="background-color:#ffffff;"><span style="color:#222222;">magic number的第4个字节代表其存储的向量或矩阵的维度。比如存储的是一维向量，那么magic number的第4个字节是0x01，如果存储的是二维矩阵，那么magic number的第4个字节就是0x02。</span></span></p><p><span style="background-color:#ffffff;"><span style="color:#222222;">所以在MNIST数据集的4个二进制文件中，标签文件的magic number第4个字节都是0x01，而在图像文件中，因为一张图像的维度是2，而多张图像拼成的矩阵维度是3，所以图像文件magic number第4个字节都是0x03。</span></span></p><p><span style="background-color:#ffffff;"><span style="color:#222222;">该数据集的官网说明了4个二进制文件中的整型数据是以大端方式 (MSB first) 存储的，所以在读取这4个二进制文件的前面几个32位整型数据时，需要注意声明数据存储格式是大端还是小端。</span></span></p><h2 id="五数据集下载链接"><a name="t5"></a><strong><span style="background-color:#ffffff;"><span style="color:#444444;">五、数据集下载链接</span></span></strong></h2><p><strong><span style="background-color:#ffffff;"><span style="color:#1b67ff;">数据集下载</span></span></strong></p><p><span style="background-color:#ffffff;"><span style="color:#222222;">OpenDataLab平台为大家提供了完整的数据集信息、直观的数据分布统计、流畅的下载速度、便捷的可视化脚本，欢迎体验。点击原文链接查看。</span></span></p><p><span style="background-color:#ffffff;"><span style="color:#222222;"><a href="https://opendatalab.com/MNIST" title="https://opendatalab.com/MNIST">https://opendatalab.com/MNIST</a></span></span></p><p><span style="background-color:#ffffff;"><strong><span style="color:#888888;">参考资料</span></strong></span></p><p><span style="background-color:#ffffff;"><span style="color:#888888;">[1]Y LeCun,L Bottou,Y Bengio,etal.Gradient-based learning applied to document recognition[J].Proceedings of the IEEE,1998,86(11):2278-2324.</span></span></p><p><span style="background-color:#ffffff;"><span style="color:#888888;">[2][http://yann.lecun.com/exdb/mnist/](<a href="http://yann.lecun.com/exdb/mnist/">http://yann.lecun.com/exdb/mnist/</a> &quot;<a href="http://yann.lecun.com/exdb/mnist/">http://yann.lecun.com/exdb/mnist/</a>&quot;)</span></span></p>]]>
                </content>
            </entry>
            <entry>
                <title><![CDATA[详解Gson的TypeToken原理]]></title>
                <link rel="alternate" type="text/html" href="https://www.wangdaye.net/archives/xiang-jie-g-s-o-n-de-t-y-p-e-t-o-k-e-n-yuan-li" />
                <id>tag:https://www.wangdaye.net,2024-05-31:xiang-jie-g-s-o-n-de-t-y-p-e-t-o-k-e-n-yuan-li</id>
                <published>2024-05-31T08:08:11+08:00</published>
                <updated>2024-05-31T08:08:11+08:00</updated>
                <author>
                    <name>王大爷</name>
                    <uri>https://www.wangdaye.net</uri>
                </author>
                <content type="html">
                        <![CDATA[<h3 id="概要">概要</h3><p>借助对<a href="/developer/tools/blog-entry?target=https%3A%2F%2Fxtboke.cn%2Ftag-TypeToken.html&amp;source=article&amp;objectId=1672483">TypeToken</a>原理的分析，加强对泛型擦除的理解，使得我们能够知道什么时候，通过什么方式可以获取到泛型的类型。</p><h3 id="泛型擦除">泛型擦除</h3><p>众所周知，<span class="mod-overview__keyword">Java</span>的泛型只在编译时有效，到了运行时这个泛型类型就会被擦除掉，即<code>List&lt;String&gt;</code>和<code>List&lt;Integer&gt;</code>在运行时其实都是<code>List&lt;Object&gt;</code>类型。</p><p>为什么选择这种实现机制？不擦除不行么？ 在Java诞生10年后，才想实现类似于C++模板的概念，即泛型。Java的类库是Java生态中非常宝贵的财富，必须保证向后兼容（即现有的代码和类文件依旧合法）和迁移兼容（泛化的代码和非泛化的代码可互相调用）基于上面这两个背景和考虑，Java设计者采取了&quot;类型擦除&quot;这种折中的实现方式。</p><p>同时正正有这个这么&quot;坑&quot;的机制，令到我们无法在运行期间随心所欲的获取到泛型参数的具体类型。</p><h3 id="typetoken">TypeToken</h3><p>使用</p><p>使用过<a href="/developer/tools/blog-entry?target=https%3A%2F%2Fxtboke.cn%2Ftag-Gson.html&amp;source=article&amp;objectId=1672483">Gson</a>的同学都知道在反序列化时需要定义一个TypeToken类型，像这样</p><div class="rno-markdown-code"><div class="rno-markdown-code-toolbar"><div class="rno-markdown-code-toolbar-info"><div class="rno-markdown-code-toolbar-item is-type"><span class="is-m-hidden">代码语言：</span>javascript</div></div><div class="rno-markdown-code-toolbar-opt"><div class="rno-markdown-code-toolbar-copy"><span class="is-m-hidden">复制</span></div></div></div><div class="developer-code-block"><pre><code>private Type type = new TypeToken&lt;List&lt;Map&lt;String, Foo&gt;&gt;&gt;(){}.getType();//调用fromJson方法时把type传过去，如果type的类型和json保持一致，则可以反序列化出来gson.fromJson(json, type);</code></pre></div></div><h4 id="三个问题">三个问题</h4><ol><li>为什么要用TypeToken来定义反序列化的类型？ 正如上面说的，如果直接把List&lt;Map&lt;String, Foo&gt;&gt;的类型传过去，但是因为运行时泛型被擦除了，所以得到的其实是<code>List&lt;Object&gt;</code>，那么后面的Gson就不知道要转成<code>Map&lt;String, Foo&gt;</code>类型了，这时Gson会默认转成LinkedTreeMap类型。</li><li>为什么带有大括号{}？ 这个大括号就是精髓所在。大家都知道，在Java语法中，在这个语境，{}是用来定义匿名类，这个匿名类是继承了TypeToken类，它是TypeToken的子类。</li><li>为什么要通过子类来获取泛型的类型？ 这是TypeToken能够获取到泛型类型的关键，这是一个巧妙的方法。 这个想法是这样子的，既然像<code>List&lt;String&gt;</code>这样中的泛型会被擦除掉，那么我用一个子类<code>SubList extends List&lt;String&gt;</code>这样的话，在JVM内部中会不会把父类泛型的类型给保存下来呢？我这个子类需要继承的父类的泛型都是已经确定了的呀，果然，JVM是有保存这部分信息的，它是保存在子类的Class信息中，具体看： <a href="/developer/tools/blog-entry?target=https%3A%2F%2Fstackoverflow.com%2Fquestions%2F937933%2Fwhere-are-generic-types-stored-in-java-class-files&amp;source=article&amp;objectId=1672483">https://stackoverflow.com/questions/937933/where-are-generic-types-stored-in-java-class-files</a> 那么我们怎么获取这部分信息呢？ 还好，Java有提供<span class="mod-overview__keyword">API</span>出来：</li></ol><p>Type mySuperClass = foo.getClass().getGenericSuperclass(); Type type = ((ParameterizedType)mySuperClass).getActualTypeArguments()[0]; System.out.println(type);</p><p>分析一下这段代码，Class类的getGenericSuperClass()方法的注释是：</p><blockquote><p>Returns the Type representing the direct superclass of the entity (class, interface, primitive type or void) represented by thisClass. If the superclass is a parameterized type, the Type object returned must accurately reflect the actual type parameters used in the source code. The parameterized type representing the superclass is created if it had not been created before. See the declaration of ParameterizedType for the semantics of the creation process for parameterized types. If thisClass represents either theObject class, an interface, a primitive type, or void, then null is returned. If this object represents an array class then theClass object representing theObject class is returned</p></blockquote><p>概括来说就是对于带有泛型的class，返回一个ParameterizedType对象，对于Object、接口和原始类型返回null，对于数 组class则是返回Object.class。ParameterizedType是表示带有泛型参数的类型的Java类型，JDK1.5引入了泛型之 后，Java中所有的Class都实现了Type接口，ParameterizedType则是继承了Type接口，所有包含泛型的Class类都会实现 这个接口。</p><p>自己调试一下就知道它返回的是什么了。</p><h4 id="原理">原理</h4><p>核心的方法就是刚刚说的那两句，剩下的就很简单了。 我们看看TypeToken的getType方法</p><div class="rno-markdown-code"><div class="rno-markdown-code-toolbar"><div class="rno-markdown-code-toolbar-info"><div class="rno-markdown-code-toolbar-item is-type"><span class="is-m-hidden">代码语言：</span>javascript</div></div><div class="rno-markdown-code-toolbar-opt"><div class="rno-markdown-code-toolbar-copy"><span class="is-m-hidden">复制</span></div></div></div><div class="developer-code-block"><pre><code>public final Type getType() { //直接返回type return type; }</code></pre></div></div><p>看type的初始化</p><div class="rno-markdown-code"><div class="rno-markdown-code-toolbar"><div class="rno-markdown-code-toolbar-info"><div class="rno-markdown-code-toolbar-item is-type"><span class="is-m-hidden">代码语言：</span>javascript</div></div><div class="rno-markdown-code-toolbar-opt"><div class="rno-markdown-code-toolbar-copy"><span class="is-m-hidden">复制</span></div></div></div><div class="developer-code-block"><pre><code>//注意这里用了protected关键字，限制了只有子类才能访问protected TypeToken() { this.type = getSuperclassTypeParameter(getClass()); this.rawType = (Class&lt;? super T&gt;) $Gson$Types.getRawType(type); this.hashCode = type.hashCode(); } //getSuperclassTypeParameter方法 //这几句就是上面的说到 static Type getSuperclassTypeParameter(Class&lt;?&gt; subclass) { Type superclass = subclass.getGenericSuperclass(); if (superclass instanceof Class) { throw new RuntimeException(&quot;Missing type parameter.&quot;); } ParameterizedType parameterized = (ParameterizedType) superclass; //这里注意一下，返回的是Gson自定义的，在$Gson$Types里面定义的TypeImpl等，这个类都是继承Type的。 return $Gson$Types.canonicalize(parameterized.getActualTypeArguments()[0]); }</code></pre></div></div><h3 id="总结">总结</h3><p>在了解原理之后，相信大家都知道怎么去获取泛型的类型了。</p><h3 id="参考资料">参考资料</h3><p><a href="/developer/tools/blog-entry?target=https%3A%2F%2Fwww.cnblogs.com%2Fdoudouxiaoye%2Fp%2F5688629.html&amp;source=article&amp;objectId=1672483">https://www.cnblogs.com/doudouxiaoye/p/5688629.html</a></p>]]>
                </content>
            </entry>
            <entry>
                <title><![CDATA[使用Google搜索API_key查询]]></title>
                <link rel="alternate" type="text/html" href="https://www.wangdaye.net/archives/使用google搜索apikey" />
                <id>tag:https://www.wangdaye.net,2024-05-18:使用google搜索apikey</id>
                <published>2024-05-18T18:21:54+08:00</published>
                <updated>2024-05-18T18:22:59+08:00</updated>
                <author>
                    <name>王大爷</name>
                    <uri>https://www.wangdaye.net</uri>
                </author>
                <content type="html">
                        <![CDATA[<blockquote><p><a href="https://zhuanlan.zhihu.com/p/174666017">https://zhuanlan.zhihu.com/p/174666017</a></p></blockquote><h2 id="前言">前言</h2><p>最近需要爬取一些 google 搜索结果，首先看的就是有没有相关的 API，官方有提供 API 查询，但爬取量有一定限制。</p><h2 id="太长不看版">太长不看版</h2><ol><li>接口地址为 <code>https://www.googleapis.com/customsearch/v1?key={YOUR_KEY}&amp;q={SEARCH_WORDS}&amp;cx={YOUR_CX}&amp;start={10}&amp;num={10}</code></li><li>KEY 从 <a href="https://link.zhihu.com/?target=https%3A//console.developers.google.com/apis/credentials">谷歌云 API 控制台</a> 来的，需要有外币卡先注册谷歌云账号。但似乎付费的话就不用这个 KEY 了，仅用 CX 即可，这个待查。</li><li>CX 从 <a href="https://link.zhihu.com/?target=https%3A//programmablesearchengine.google.com/cse/create/new">谷歌可编程搜索</a> 中来</li><li>一天只有 100 次的免费搜索限额，但最高只能查询前 100 条。如需增加则 5 刀 1000 次，但一天上限 10000。 次，对于我来说已经足够用了</li></ol><h2 id="怎么做">怎么做</h2><p>首先来到 <a href="https://link.zhihu.com/?target=https%3A//developers.google.com/custom-search/v1/overview%3Fhl%3Den_US">Google Developers</a> 的相关文档页面，可以看到大概的介绍。</p><h3 id="生成-api-key">生成 API KEY</h3><p>点击 overview 中的 <a href="https://link.zhihu.com/?target=https%3A//developers.google.com/custom-search/v1/overview%23api_key">Get a Key</a>，此处需要登录谷歌帐号，以及注册谷歌云帐号（应该需要绑定外币信用卡）并且创建一个 project，此处略过不表，最后你会得到一个 Key。</p><figure data-size="normal"><p><noscript><img src="https://pic2.zhimg.com/v2-0dcc7d4700d873e89c86c5b2d7f2eba9_b.jpg" alt="" /></noscript></p><div>![](https://pic2.zhimg.com/80/v2-0dcc7d4700d873e89c86c5b2d7f2eba9_1440w.jpg)</div></figure><p>这个 Key 可以从<a href="https://link.zhihu.com/?target=https%3A//console.developers.google.com/apis/credentials/key">谷歌云控制台</a>中看到，建议加上应用限制和 API 限制，以防泄露后被滥用。</p><figure data-size="normal"><p><noscript><img src="https://pic4.zhimg.com/v2-ae2f6aa240bd9b6320f4b69a66efb273_b.jpg" alt="" /></noscript></p><div>![](https://pic4.zhimg.com/80/v2-ae2f6aa240bd9b6320f4b69a66efb273_1440w.webp)</div></figure><h3 id="生成-cx">生成 cx</h3><p>cx 是 Google 可编程搜索引擎(Programmable Search Engine)的 id 标识，在此处 <a href="https://link.zhihu.com/?target=https%3A//programmablesearchengine.google.com/cse/create/new">新增搜索引擎</a> 可以获取。这里可以指定要搜索的网站，比如说我只希望通过该 API 搜索出来的网站是 shodan.io，谷歌语法里面相当于 <code>site:shodan.io</code>，可以这么设置 ：</p><figure data-size="normal"><p><noscript><img src="https://pic2.zhimg.com/v2-186108d2d148a490cbe3b4d3030f41a1_b.jpg" alt="" /></noscript></p><div>![](https://pic2.zhimg.com/80/v2-186108d2d148a490cbe3b4d3030f41a1_1440w.webp)</div></figure><p>新增完成之后点击修改搜索引擎，并点击设置，你就可以看到你的搜索引擎 id，就是我们说的 cx</p><figure data-size="normal"><p><noscript><img src="https://pic4.zhimg.com/v2-7b2b97e329112e2ef242a38892dcb2c3_b.jpg" alt="" /></noscript></p><div>![](https://pic4.zhimg.com/80/v2-7b2b97e329112e2ef242a38892dcb2c3_1440w.webp)</div></figure><p>里面还有一些选项，自己可以看着修改~如果还想看看文档，可点击在页面下方一点的【以程序化方式访问】-【使用入门】</p><h3 id="api">API</h3><p>JSON API 可以从 <a href="https://link.zhihu.com/?target=https%3A//developers.google.com/custom-search/v1/introduction%23api_overview">文档</a> 中查看</p><p>完整的可请求参数如下，基本上和高级搜索保持一致：</p><div class="highlight"><pre><code>https://www.googleapis.com/customsearch/v1?q={searchTerms}&amp;num={count?}&amp;start={startIndex?}&amp;lr={language?}&amp;safe={safe?}&amp;cx={cx?}&amp;sort={sort?}&amp;filter={filter?}&amp;gl={gl?}&amp;cr={cr?}&amp;googlehost={googleHost?}&amp;c2coff={disableCnTwTranslation?}&amp;hq={hq?}&amp;hl={hl?}&amp;siteSearch={siteSearch?}&amp;siteSearchFilter={siteSearchFilter?}&amp;exactTerms={exactTerms?}&amp;excludeTerms={excludeTerms?}&amp;linkSite={linkSite?}&amp;orTerms={orTerms?}&amp;relatedSite={relatedSite?}&amp;dateRestrict={dateRestrict?}&amp;lowRange={lowRange?}&amp;highRange={highRange?}&amp;searchType={searchType}&amp;fileType={fileType?}&amp;rights={rights?}&amp;imgSize={imgSize?}&amp;imgType={imgType?}&amp;imgColorType={imgColorType?}&amp;imgDominantColor={imgDominantColor?}&amp;alt=json&quot;</code></pre></div><p>简化版：<code>https://www.googleapis.com/customsearch/v1?key={YOUR_KEY}&amp;q={SEARCH_WORDS}&amp;cx={YOUR_CX}&amp;start={10}&amp;num={10}</code></p><h2 id="存在的一些问题">存在的一些问题</h2><h3 id="搜索结果与-api-不一致">搜索结果与 API 不一致</h3><p>因为不同 IP 使用谷歌搜索会出现不一样的结果，比如美国和香港的 IP 访问必然会不一样。可以使用 API 中的 <code>lr</code> 参数修改语言选项，也可以在【修改搜索引擎】中修改语言和地区选项。</p><figure data-size="normal"><p><noscript><img src="https://pic1.zhimg.com/v2-367cce8237e8b60e3c5d1a3d89e847a0_b.jpg" alt="" /></noscript></p><div>![](https://pic1.zhimg.com/80/v2-367cce8237e8b60e3c5d1a3d89e847a0_1440w.webp)</div></figure><h3 id="请求频率">请求频率</h3><p>由于一天查的上限就这么多，所以等待时间尽量拉长吧，我 5-10s 请求一次没啥问题</p>]]>
                </content>
            </entry>
            <entry>
                <title><![CDATA[从设计者的角度出发理解源码–FastJson]]></title>
                <link rel="alternate" type="text/html" href="https://www.wangdaye.net/archives/fastjsonread" />
                <id>tag:https://www.wangdaye.net,2024-02-14:fastjsonread</id>
                <published>2024-02-14T08:10:59+08:00</published>
                <updated>2024-02-14T08:57:09+08:00</updated>
                <author>
                    <name>王大爷</name>
                    <uri>https://www.wangdaye.net</uri>
                </author>
                <content type="html">
                        <![CDATA[<h1 id="从设计者的角度出发理解源码--fastjson">从设计者的角度出发理解源码--FastJson</h1><h2 id="引言">引言</h2><p>本篇，作为《从..理解源码》的第二篇，延续前一篇的思路，笔者视图尝试从作者的角度触发分析作者的思路历程，和上篇相比，FastJson的知识可能略多。这篇文章，对fastjson只能算是浅尝即止，我认为最厉害的未过于，作者的持续习惯，包括但不限于：</p><ul><li><p>基于Benchmark搜集高性能代码的习惯</p></li><li><p>阅读原始文档获得第一手资料，对于JDK各种版本特性了然于胸。</p></li><li><p>喜欢探索代码优雅的编码方式。</p></li><li><p>产品思维：先发现问题，先用简单的方式实现，后面在迭代更好的方案。</p></li><li><p>自我高要求：把自己的代码拉出来被鞭尸，虚心接受建议，持续迭代，即便有其他框架在开发，即便自己已经是P9。</p></li></ul><p>这个框架的阅读过程中，感受到作者的拼劲全力，感受到：<strong>一个极客，孜孜不倦，为当年立下的Flag，将自己空闲时间，都投入到少年时的一个梦：极致的性能</strong>。</p><blockquote><p>写本篇文章时，由于fastjson2已发布，本篇文章基于fastjson2</p></blockquote><h2 id="为什么要写fastjson">为什么要写FastJson</h2><p><strong>哎，以往的框架，Jakson,Goson，最大的问题其实是使用繁琐，和在不同环境下，兼容性不足</strong></p><p>我需要给别人一个足够的理由，做俩个选择</p><ol><li>选择放弃原本的实用。</li><li>选择使用我的东西。</li></ol><p><strong>有了</strong>：</p><ul><li><p>以性能为突破口</p><blockquote><p>我从性能上出发，应该是一个比较好的突破口，因为一个逻辑一旦涉及到序列化操作，该序列化操作的频次应该不会低。</p><p>如果我可以真正做到序列化性能有比较大的提升，对他们来说很有吸引力。</p></blockquote></li><li><p>上手体验为突破口：</p><p>1.使用API设计均使用静态方法思路，减少编码步骤，</p><p>2.提供充足的注解类工具，使用注解天生体验好的特性。</p><blockquote><table><thead><tr><th>框架</th><th>写法</th><th>上手代码量</th></tr></thead><tbody><tr><td>Fastjson</td><td>String mapString = JSON.toJSONString(map);</td><td>1 行</td></tr><tr><td>Jackson</td><td>ObjectMapper mapper = new ObjectMapper();<br />String beanString = mapper.toJson(map);</td><td>2 行</td></tr><tr><td>Gson</td><td>Gson gson = new Gson();<br />String beanString = gson.toJson(map);</td><td>2行</td></tr></tbody></table><p>FastJSON提供的注解如下：</p><pre><code class="language-java">// 字段注解com.alibaba.fastjson2.annotation.JSONFieldcom.alibaba.fastjson2.annotation.JSONType// com.alibaba.fastjson2.annotation.JSONBuildercom.alibaba.fastjson2.annotation.JSONCompiledcom.alibaba.fastjson2.annotation.JSONCompiler</code></pre></blockquote></li><li><p>先在公司内部（阿里）获得一定的认可度，后面推广可以借助公司的力量。</p></li></ul><blockquote><p>其实，做框架设计起初和写PPT有点类似，你得先有一个能引人入胜的思路，最好能有个最简陋的版本，先将人吸引过来，然后你逐步去完善它，持之以恒的迭代，再产生新的思路，再迭代。</p></blockquote><p><strong>哎，光能证明你性能好也不行呀，稳定性怎么保证，市面上相似的太多了。</strong></p><p><strong>有了</strong>：</p><ul><li>持续关注issues及时修复，把每个issues的修复单元测试及时提交修改完成之后，让他们随意查看，逐步建立起信任。</li><li>保持初心，持之以恒，一个框架能被大面积使用，不在于它开始bug的多少，而在于立意远大，且有人维护。</li><li>这个东西如果做好了，是能反向推动我成长的，因为它的适用范围足够广（安卓的，Web，大数据等等）</li></ul><h2 id="序列化设计bean-json">序列化设计（Bean-&gt;Json）</h2><p>序列化流程，如果再细分的话，有如下流程：</p><ol><li><p>获取所有需要提取的属性列表。</p></li><li><p>依次遍历属性列表获取属性，向一段JSON容器里面放置key，val。</p></li><li><p>在放置key，val的时候，可能涉及某一些转义符的处理，json结构的处理等等。</p></li></ol><h3 id="不同场景的优化">不同场景的优化</h3><p><strong>哎，我如何基于不同情况做出不同的处理方式呢，这样我可以针对每一种情况去做具体的优化</strong></p><p><strong>有了</strong>：</p><ul><li>我可以使用模板方法写一个ObjectWriter抽象类完成主逻辑。</li><li>将已知情况细分清楚，基于不同情况，各自情况完成各自的实现。</li></ul><pre><code class="language-java">// ObjectWriter抽象类，以及部分具体实现com.alibaba.fastjson2.writer.ObjectWritercom.alibaba.fastjson2.util.JodaSupport.LocalDateTimeWritercom.alibaba.fastjson2.util.GuavaSupport.AsMapWriter....</code></pre><p><strong>哎，总不能把所有情况都穷举出来，写出各自的实现，这种优化思路还是不太灵活。</strong></p><p><strong>有了</strong>：Bean转JSON，本质上，就是从一个Bean里面取内容，然后往一个JSON里面塞么。</p><p>我可以为每一个Bean的，读写逻辑，分别依据各自的情况生成相应的字节码读写器。</p><ul><li>依据每一个Bean的情况去做更具体的优化。</li><li>后期扩展空间也大（毕竟是动态生成的代码）。</li><li>需要通过一个抽象类来约束核心逻辑，使用ASM框架运行时编写Class。</li></ul><pre><code class="language-java">// 如下摘自：com.alibaba.fastjson2.JSON#toJSONString(java.lang.Object)，部分逻辑省略//1：获取ObjectWritercom.alibaba.fastjson2.writer.ObjectWriterProvider#getObjectWriter //1.1：使用 ASM 来创建 ObjectWriter 对象 1.1 creator = ObjectWriterCreatorASM.INSTANCE; //  //1.2：通过字节码编写代码，继承ObjectWriterAdapter，创建Class。  1.2 com.alibaba.fastjson2.reader.ObjectWriterCreatorASM#createObjectWriter//2：完成读序列化   objectWriter.write(writer, object, null, null, 0); //3：返回return writer.toString();</code></pre><h3 id="asm生成的代码">ASM生成的代码</h3><p><strong>如下我列出了FastJSON生成的ObjectReaderAdapter:</strong></p><ul><li><p>FastJSON生成的ObjectReaderAdapter: <a href="https://note.youdao.com/s/AfCaGZSV">https://note.youdao.com/s/AfCaGZSV</a></p></li><li><p>FastJSON生成的ObjectWriterAdapter: <a href="https://note.youdao.com/s/W9gi9fRD">https://note.youdao.com/s/W9gi9fRD</a></p></li></ul><h3 id="不能使用asm的环境">不能使用ASM的环境</h3><p><strong>哎，可能部分使用者的场景不能使用字节码，可能由于环境问题，比如Android，GraalVM环境等，或者出于安全上面考量，使用者主观不想用字节码实现</strong></p><p>有了：</p><ul><li>支持让使用者指定使用的实现方式。</li><li>默认使用ASM字节码实现，如果发现是安卓等不能够使用的环境，自动使用反射实现，且支持手动指定。</li></ul><pre><code class="language-java">// 核心源码如下： 1.优先支持手动指定方式，2.如果不指定，默认使用ASM方式，3.如果当前环境不支持ASM，则自动转为MH，或者反射。switch (JSONFactory.CREATOR) {   case &quot;reflect&quot;:  case &quot;lambda&quot;:    creator = ObjectWriterCreator.INSTANCE;     break;  case &quot;asm&quot;:  default:    try {      if (!JDKUtils.ANDROID &amp;&amp; !JDKUtils.GRAAL) {         creator = ObjectWriterCreatorASM.INSTANCE;       }    } catch (Throwable ignored) {}    if (creator == null) {       // 如果 creator 仍然为 null，则使用反射或 Lambda 表达式来创建 ObjectWriter 对象      creator = ObjectWriterCreator.INSTANCE;     }    break;}</code></pre><p><strong>哎，反射好像性能不太好，我如何在这个基础之上优化一点呢？</strong></p><p><strong>有了</strong>:</p><ul><li>我可以使用：Lambda表达式+MethodHandler，来替代反射获取属性的调用。</li><li>JDK自带了很多FunctionInterface接口比如Customer ，这样我不仅可以提升性能，还可以在调用处统一写法。</li></ul><blockquote><p>invokedynamic指令与MethodHandle的功能简单说：就是之前的动态转发的逻辑都是在字节码层面去完成的，某个方法调用，转发给哪个具体实现类去执行，都是字节码自己安排好了，基于一段固定逻辑自己走下来</p><p>而invokedynamic指令与MethodHandle则可以将“转发给谁”，这个谁，在应用代码层面去指定。</p><p>Lambda表达式主要就是基于如下几点</p><p>1.通过ASM字节码技术，生成最轻量代码（比如static方法）</p><p>2.基于指令invokedynamic与MethodHandle特性实现的，将转发能力赋予了业务代码。</p></blockquote><pre><code class="language-java">// P1：创建set函数的Lambda对象BiConsumer function = (BiConsumer) lambdaSetter(objectClass, fieldClass, method); return createFieldReader(objectClass, objectType, fieldName, fieldType, fieldClass, ordinal, features, format, locale, defaultValue, jsonSchema, method, function, initReader);// P2：将set函数的Lambda对象放到缓存fieldReaders中putIfAbsent(fieldReaders, fieldName, fieldReader, objectClass);// P3：使用function读取值public void readFieldValue(JSONReader jsonReader, T object) {     function.accept(object, (V) fieldValue);}</code></pre><p>知识补习：</p><ul><li><a href="https://www.wangdaye.net/archives/jvm%E4%B8%AD%E7%9A%84%E5%8A%A8%E6%80%81%E5%88%86%E6%B4%BE%E6%9C%BA%E5%88%B6%E5%88%B0%E5%BA%95%E6%98%AF%E6%80%8E%E4%B9%88%E5%9B%9E%E4%BA%8B">JVM中的动态分派机制到底是怎么回事</a></li><li><a href="https://juejin.cn/post/7202188318889508924">invokedynamic详解</a></li><li><a href="https://www.wangdaye.net/archives/toleinj">Lambda表达式是如何设计的</a></li></ul><h3 id="安全性方面">安全性方面</h3><p><strong>哎，如何防止JSON注入呢？毕竟用户输入什么我又不可控制？</strong></p><p>有了：</p><ul><li><p>Json-schema不就就是干这个的，我可以用Json-schema约束输入值，防止被注入。</p><blockquote><p>Json-schema是一个特殊的JSON，用来约束JSON的数据结构。</p></blockquote></li><li><p>使用模板方法设计模式，实现一个基础模板类，基于基础类型（int，long，string，object）来实现具体的验证逻辑（用户通过注解的形式作用在某个具体字段上）</p><blockquote><p>-- 基础模板</p><p>com.alibaba.fastjson2.schema.JSONSchema</p><p>-- 核心方法（字段验证）</p><p>com.alibaba.fastjson2.schema.JSONSchema#validate</p><p>-- 具体类型实现</p><p>com.alibaba.fastjson2.schema.BooleanSchema</p><p>com.alibaba.fastjson2.schema.ObjectSchema</p><p>-- 组合规则</p><p>com.alibaba.fastjson2.schema.AnyOf</p><p>com.alibaba.fastjson2.schema.AllOf</p></blockquote></li><li><p>其中，object，组合类（AnyOf，AllOf）方法可能有一些特殊实现，因为object类型需要考虑递归的情况。组合类的需要考虑，多条规则之间的验证关系。</p></li></ul><p>知识补习：</p><ul><li><a href="https://json-schema.apifox.cn/">JSON-SCHEMA中文文档</a></li></ul><p><strong>哎，自己通过字节码生成Class，比如如果生成的Class里面有while循环的行为呢？安全性方面如何保证呢？</strong></p><p>有了：</p><ul><li>使用一个自定义的ClassLoader加载动态生成的Class。</li><li>对自定义的ClassLoader做安全配置，限制代码行为。</li></ul><pre><code class="language-java">//P1 DynamicClassLoader// ProtectionDomain表示代码源和权限的集合，里面封装了代码源（即代码从哪里来，来源是哪里）和权限（即代码有什么权限，能干嘛）的信息。public class DynamicClassLoader extends ClassLoader {  private static final DynamicClassLoader instance = new DynamicClassLoader();    private static final java.security.ProtectionDomain DOMAIN;    static {      DOMAIN = (java.security.ProtectionDomain) java.security.AccessController.doPrivileged(        (PrivilegedAction&lt;Object&gt;) DynamicClassLoader.class::getProtectionDomain      );    }}//P2 ObjectReaderCreatorASM//使用DynamicClassLoader，初始化ObjectReaderCreatorASM的classLoader属性。public ObjectReaderCreatorASM(ClassLoader classLoader) {    this.classLoader = classLoader instanceof DynamicClassLoader ? (DynamicClassLoader) classLoader : new DynamicClassLoader(classLoader);  }//P3 ObjectReaderCreatorASM.jitObjectReader//调用classLoader的defineClassPublic方法，将code中的数据定义为一个新的类，并返回这个类的Class对象，赋值给readerClass。byte[] code = cw.toByteArray();Class&lt;?&gt; readerClass = classLoader.defineClassPublic(classNameFull, code, 0, code.length);//P4 com.alibaba.fastjson2.util.DynamicClassLoader#defineClassPublic// 定义类时候使用DOMAIN作为限制。public Class&lt;?&gt; defineClassPublic(String name, byte[] b, int off, int len) throws ClassFormatError {  return defineClass(name, b, off, len, DOMAIN);}</code></pre><p>知识补习：</p><ul><li><p><a href="https://blog.csdn.net/yfqnihao/article/details/8271415">策略和保护域</a></p></li><li><p><a href="https://www.wangdaye.net/archives/%E7%B1%BB%E5%8A%A0%E8%BD%BD%E5%99%A8%E5%8E%9F%E7%90%86">类加载器</a></p></li></ul><h3 id="属性读写顺序">属性读写顺序</h3><p><strong>哎，读取和写入Key的顺序如何保证呢？先写哪些属性，后写哪些属性？如何将性能最大化？</strong></p><p><strong>有了</strong>：我可以在生成字节码阶段做一些事情</p><ul><li><p>在生成字节码阶段，获取JavaBean所有属性列表，且将这些属性按照Hash值排序，ListA。</p></li><li><p>在生成字节码阶段，遍历ListA属性，生成字节码写入逻辑，这样我的代码里从上至下的编码顺序，即是正确的读取写入顺序。</p></li><li><p>在执行字节码类阶段，通过一个全局作用域的Byte数组，来维护JSON信息，不断在Byte数组后追加信息。</p></li><li><p>在执行字节码类阶段，使用Unsafe类操作内存设置值，避免频繁安全检查。</p></li></ul><pre><code class="language-java">// P1 com.alibaba.fastjson2.writer.ObjectWriterCreatorASM#createObjectWriter// 1.遍历JavaBean的属性，获取fieldWriters  BeanUtils.declaredFields(objectClass, field -&gt; {     FieldWriter fieldWriter = creteFieldWriter(objectClass, writerFieldFeatures, provider, beanInfo, fieldInfo, field);     ...    fieldWriterMap.put(fieldWriter.fieldName, fieldWriter);  });   fieldWriters = new ArrayList&lt;&gt;(fieldWriterMap.values()); // P2 com.alibaba.fastjson2.writer.ObjectWriterCreatorASM#genMethodWrite  // 2.遍历fieldWriters，按先后次序生成字节码  for (int i = 0; i &lt; fieldWriters.size(); i++) {    FieldWriter fieldWriter = fieldWriters.get(i);    gwFieldValue(mwc, fieldWriter, OBJECT, i);  }// P3 https://note.youdao.com/s/W9gi9fRD，（生成的WriterAdapter字节码）  // 3.如下为生成的字节码的编码顺序，  if ((var14 = ((ExcelTransformReq)var2).getColumn1()) != null)  ...    if ((var14 = ((ExcelTransformReq)var2).getColumn10()) != null)   // P4 设置byte数值  // 4.写入数据  com.wang.track.analysis.OWG_1_25_ExcelTransformReq#write      // 4.2 具体filed写入，其中JSONWriter里面维护全局作用域，以及全局Byte[]    com.alibaba.fastjson2.writer.FieldWriter#writeFieldName    com.alibaba.fastjson2.JSONWriter#writeName         // 4.2.1 使用unsafe直接操作数组        UNSAFE.putLong(bytes, ARRAY_BYTE_BASE_OFFSET + off, name);</code></pre><p>知识补习：</p><ul><li><a href="https://www.wangdaye.net/archives/深入理解sunmiscunsafe原理">深入理解sun.misc.Unsafe原理</a></li></ul><h3 id="多种编码格式的适配">多种编码格式的适配</h3><p><strong>哎，字符串的编码格式种类，我如何适配到多种编码格式呢？</strong></p><p><strong>有了</strong>：其实不同编码格式的区别无非是，存储容器，拼接与位移策略，在不同的情况下的不同。</p><ul><li>定义抽象一个类叫JSONWriter，这个类里面不做具体实现，只做方法的定义，与核心解析逻辑编写。（模板方法设计模式）</li><li>继承JSONWriter实现多种编码格式的实现类，比如：JSONWriterJSONB，JSONWriterUTF16，JSONWriterUTF8。</li><li>生成JSON时，根据情况选中具体JSONWriter实现。</li><li>将JSONWriter与WriterAdapter，使用桥接模式进行组装执行。（桥接设计模式）</li></ul><pre><code class="language-java">// 如下列出了JSONWriter，已经它的具体实现com.alibaba.fastjson2.JSONWritercom.alibaba.fastjson2.JSONWriterUTF16com.alibaba.fastjson2.JSONWriterUTF8com.alibaba.fastjson2.JSONWriterJSONB  // 根据配置项，或者环境情况，初始化对应的实现。// com.alibaba.fastjson2.JSONWriter#of...  if (FIELD_STRING_VALUE != null &amp;&amp; !ANDROID &amp;&amp; !OPENJ9) {  jsonWriter = new JSONWriterUTF16JDK8UF(context);} else {  jsonWriter = new JSONWriterUTF16JDK8(context);}  ...// 将JSONWriter作为入参，传入WriterAdapter，使用桥接的思路。com.wang.track.analysis.OWG_1_25_ExcelTransformReq#write(JSONWriter var1, Object var2, Object var3, Type var4, long var5)this.fieldWriter0.writeFieldName(var1);  </code></pre><p>知识补习：</p><ul><li><a href="https://zhuanlan.zhihu.com/p/575645658">常见的设计模式</a></li></ul><p><strong>哎，复杂对象（一个class里有另一个class），该如何处理？</strong></p><p>有了：</p><ul><li><p>我可以使用懒加载的思路，在读取每个属性值的时候，先判断一下该属性类型，是否是复杂类型，如果是，则递归使用创建复杂类型的Reader，思路和如上类似。</p><pre><code class="language-java">// com.wang.track.analysis.ORG_1_25_ExcelTransformReq// ObjectReader 类里封装了递归生成reader的逻辑public FieldReader fieldReader0;public ObjectReader objectReader0;// com.alibaba.fastjson2.reader.ObjectReader1#readJSONBObjectlong hashCode = jsonReader.readFieldNameHashCode();if (hashCode == getTypeKeyHash() &amp;&amp; i == 0) {long typeHash = jsonReader.readTypeHashCode();JSONReader.Context context = jsonReader.getContext();ObjectReader autoTypeObjectReader = autoType(context, typeHash);}</code></pre></li><li><p>使用一个容器（map结构），将本次流程中所创建的reader，使用，classA-xxxReader，的形式缓存。</p><pre><code class="language-java">// com.alibaba.fastjson2.reader.ObjectReaderProvider// 如下是各种查询的缓存Mapfinal ConcurrentMap&lt;Type, ObjectReader&gt; cache = new ConcurrentHashMap&lt;&gt;();final ConcurrentMap&lt;Type, ObjectReader&gt; cacheFieldBased = new ConcurrentHashMap&lt;&gt;();final ConcurrentMap&lt;Integer, ConcurrentHashMap&lt;Long, ObjectReader&gt;&gt; tclHashCaches = new ConcurrentHashMap&lt;&gt;();final ConcurrentMap&lt;Long, ObjectReader&gt; hashCache = new ConcurrentHashMap&lt;&gt;();final ConcurrentMap&lt;Class, Class&gt; mixInCache = new ConcurrentHashMap&lt;&gt;();</code></pre></li></ul><h3 id="代码结构图">代码结构图</h3><p>如下是最终的架构图：</p><p><img src="https://www.wangdaye.net/upload/2024/02/image-f8a9cd2fadb64d0bbd1c30a6a8797325.png" alt="image.png" /></p><blockquote><p>1.入口</p><pre><code class="language-java">JSON.toJSONString(testBean);</code></pre><p>2.初始化ObjectWriterProvider（默认ASM实现），全局上下文（context）</p><pre><code class="language-java">final ObjectWriterProvider provider = defaultObjectWriterProvider;final JSONWriter.Context context = new JSONWriter.Context(provider);</code></pre><p>3.初始化JSONWriter（判断当前所处环境，以及参数，自动选择合适的JSONWriter）</p><pre><code class="language-java">JSONWriter writer = JSONWriter.of(context)</code></pre><p>3.初始化getObjectWriter</p><pre><code class="language-java">// 初始化getObjectWriterObjectWriter&lt;?&gt; objectWriter = provider.getObjectWriter(valueClass,valueClass,(defaultWriterFeatures &amp; JSONWriter.Feature.FieldBased.mask) != 0); </code></pre><p>4.解析</p><pre><code class="language-java">// 此时objectWriter默认的类型为....OWG_1_25_ExcelTransformReq，为ASM生成的classobjectWriter.write(writer, object, null, null, 0);</code></pre><p>5.具体的字符串输出</p><pre><code class="language-java">return writer.toString();</code></pre></blockquote><h2 id="反序列化设计json-bean">反序列化设计(Json-&gt;Bean)</h2><blockquote><p>由于parseObject方法的代表性，如下均以com.alibaba.fastjson2.JSON#parseObject方法作为思路解析的依据</p></blockquote><p>其实作者对序列化，反序列化的思路是差不多的。只不过反序列化有一些区别，这部分只针对有区分的部分做推倒。</p><ul><li>Json转为Bean时，key的顺序不可预测。</li><li>JSON-&gt;Bean，只需要调用Bean的set方法，所以不需要考虑反射性能差的原因。</li></ul><h3 id="key无序问题">key无序问题</h3><p><strong>哎，JSON的key的顺序是不可预测的，我该如何处理？</strong></p><p><strong>有了</strong>：相同字符串的Hash值是一样的，我可以基于这个规律，在写字节码的时候，按顺序写不就行了，通过Hash来作为IF的判断依据。</p><blockquote><p>可能有同学有疑问，使用Map结构足够了，为什么还有生成这样的代码</p><p>因为在key的数量相当有限的情况下，逻辑分支的性能（经过JVM指令优化）远比Map寻址的性能高。</p></blockquote><pre><code class="language-java">// 根据Name的Hash值，使用switch编写逻辑分支代码。var9 = var1.readFieldNameHashCode()int var11 = (int)(var9 ^ var9 &gt;&gt;&gt; 32);  switch(var11) {  case 1079836942:    if (var9 == 3832966174269534051L) {      var10001 = var1.readString();      ((ExcelTransformReq)var6).setColumn15(var10001);      continue;    }    break;  case 1079902478:    if (var9 == 3833247649246244707L) {      var10001 = var1.readString();      ((ExcelTransformReq)var6).setColumn25(var10001);      continue;    }    break;    ...</code></pre><h3 id="代码结构图-1">代码结构图</h3><p><img src="https://www.wangdaye.net/upload/2024/02/image-41fe16b2a5bc418886cd7092ecbdcbb7.png" alt="image.png" /></p><blockquote><p>1.入口</p><pre><code class="language-java">com.alibaba.fastjson2.JSON#parseObject</code></pre><p>2.初始化ObjectReaderProvider，与Context（全局上下文）</p><pre><code class="language-java">ObjectReaderProvider provider = JSONFactory.getDefaultObjectReaderProvider();JSONReader.Context context = new JSONReader.Context(provider);</code></pre><p>3.初始化objectReader</p><pre><code class="language-java">ObjectReader&lt;T&gt; objectReader = provider.getObjectReader(clazz,(defaultReaderFeatures &amp; JSONReader.Feature.FieldBased.mask) != 0);</code></pre><p>4.初始化JSONReader</p><pre><code class="language-java">JSONReader reader = JSONReader.of(text, context);</code></pre><p>5.解析</p><pre><code class="language-java">T object = objectReader.readObject(reader, clazz, null, 0);</code></pre></blockquote><h2 id="环境变量设计">环境变量设计</h2><h3 id="环境变量信息搜集">环境变量信息搜集</h3><p><strong>哎，我需要面对不同的环境，如果有一个全局视野知道当前环境的情况那些功能可用？</strong></p><p>有了，我可以使用一个JDKUtils，所有当前环境的信息，都通过<strong>各种方式</strong>取出来，然后放到它里面统一维护，用这个类来解决环境变量问题。</p><ul><li><p>JVM版本等，通过系统参数获取。(类加载阶段，static代码块)</p><blockquote><p>System.getProperty(&quot;java.vm.name&quot;)</p></blockquote></li><li><p>判断当前环境（安卓还是JavaWeb），通过寻找Class的方式获取(类加载阶段，static代码块)</p><blockquote><p>Class&lt;?&gt; factorClass = Class.forName(&quot;java.lang.management.ManagementFactory&quot;);</p></blockquote></li><li><p>框架公共方法，通过MethodHandles获取(运行阶段，方法首次调用时)</p><blockquote><p>MethodHandles.Lookup lookup = trustedLookup(classStringCoding);<br />CallSite callSite = LambdaMetafactory.metafactory(..);<br />isAscii = (Predicate&lt;byte[]&gt;) callSite.getTarget().invokeExact();</p><p>PREDICATE_IS_ASCII = isAscii;</p></blockquote></li></ul><pre><code class="language-java">// P1// 环境工具类com.alibaba.fastjson2.util.JDKUtils// static阶段获取public static final Unsafe UNSAFE;public static final long ARRAY_BYTE_BASE_OFFSET;public static final long ARRAY_CHAR_BASE_OFFSET;public static final int JVM_VERSION;public static final Field FIELD_STRING_VALUE;  // 如下是通过BiFunction（函数式编程），将不同环境中的能力统一出口public static final ToIntFunction&lt;String&gt; STRING_CODER;public static final Function&lt;String, byte[]&gt; STRING_VALUE;// P2,如下是获取安卓SDK版本号android_sdk_int = Class.forName(&quot;android.os.Build$VERSION&quot;).getField(&quot;SDK_INT&quot;).getInt(null);// p3,获取Java版本号String javaSpecVer = System.getProperty(&quot;java.specification.version&quot;);// P4,判断当前环境是否有sql的jar包dataSourceClass = Class.forName(&quot;javax.sql.DataSource&quot;);// P5,根据不同版本通过Lambda表达式封装实现if (JVM_VERSION &gt;= 17) {  handle = trustedLookup.findStatic(classStringCoding =                                     String.class,&quot;isASCII&quot;,MethodType.methodType(boolean.class,byte[].class);}....if (handle == null &amp;&amp; JVM_VERSION &gt;= 11) {  classStringCoding = Class.forName(&quot;java.lang.StringCoding&quot;);  handle = trustedLookup.findStatic(classStringCoding,&quot;isASCII&quot;,MethodType.methodType(boolean.class, byte[].class)}                               MethodHandles.Lookup lookup = trustedLookup(classStringCoding);CallSite callSite = LambdaMetafactory.metafactory(..);isAscii = (Predicate&lt;byte[]&gt;) callSite.getTarget().invokeExact();PREDICATE_IS_ASCII = isAscii;</code></pre><h3 id="流程数据维护和流程能力扩展">流程数据维护和流程能力扩展</h3><p><strong>哎，全局视野如何保证？比如现在多层嵌套的JSON，由于我使用Byte[]维护。</strong></p><p>有了：</p><p>在解析开始时候，设置一个全局Context，这个Context贯穿流程始终。</p><ul><li>维护当前进行中的值，或者解析状态值，比如JSON数据，当前写入偏移量等。</li><li>维护一些全局配置，比如用户配置的功能属性开关。</li><li>使用Long值+位移，来实现不同能力的开关。</li></ul><p><strong>哎，功能参数和扩展性如何设计呢？</strong></p><ul><li>配置项比较多，目前有四五十个，后面可能还会拓展到百个。</li><li>需要再解析前后加一些钩子。</li></ul><p>有了：</p><ul><li><p>配置项可以统一使用Long+位移的方式，这样一个Long值，就能代表63个配置项，极大减少空间。</p><blockquote><pre><code class="language-java">// com.alibaba.fastjson2.JSONReader.Featurepublic enum Feature {FieldBased(1),IgnoreNoneSerializable(1 &lt;&lt; 1),ErrorOnNoneSerializable(1 &lt;&lt; 2),SupportArrayToBean(1 &lt;&lt; 3),InitStringFieldAsEmpty(1 &lt;&lt; 4)}</code></pre></blockquote></li><li><p>其实扩展性无非是俩种类型，一种是影响主逻辑的，一种是不影响主逻辑（单纯是生命周期回调），第1种我可以使用Lambda表达式，第2种我可以使用Processor思路去做（统一维护触发对象，在特定实际触发）。</p><blockquote><p>1.生命周期触发</p><p>2.置入代码调整主逻辑。</p></blockquote></li><li><p>使用全局Context，维护所有的扩展配置，使这些配置在全流程中可见，被动触发不受限制。</p></li></ul><pre><code class="language-java">// 分别是序列化，反序列化，的Contextcom.alibaba.fastjson2.JSONWriter.Contextcom.alibaba.fastjson2.JSONReader.Context  // 如下是JSONReader.Context的部分定义，其中objectSupplier，arraySupplier为public static final class com.alibaba.fastjson2.JSONReader.Context {   long features;   // 扩展点：第1种   Supplier&lt;Map&gt; objectSupplier;   Supplier&lt;List&gt; arraySupplier;   // 扩展点：第2种   AutoTypeBeforeHandler autoTypeBeforeHandler;   ExtraProcessor extraProcessor;}// 如下是JSONWriter.Context的部分定义，public static final class com.alibaba.fastjson2.JSONWriter.Context {    long features;    boolean hasFilter;  // 扩展点：第2种    PropertyPreFilter propertyPreFilter;    PropertyFilter propertyFilter;    NameFilter nameFilter;    ValueFilter valueFilter;    BeforeFilter beforeFilter;    AfterFilter afterFilter;    ...}</code></pre><p><strong>哎，这些扩展点，需要通过不同的入参传入，体验不太友好，因为这些配置项到底是哪个层面的，没有办法客观区分出来？</strong></p><p>答：有了，使用参数注解，属性注解天生就具备一个能力：该注解生效在哪个属性上！</p><blockquote></blockquote><h3 id="高低版本jdk如何兼容">高低版本JDK如何兼容</h3><p><strong>哎，不同环境可能有一些能力的实现，或者方法名称不一样？我是否可以借力其他框架的某一些能力？</strong></p><p>有了：上面好像用过 Lambda表达式的一种能力，将不同环境的实现，统一入口。</p><blockquote><p>CallSite callSite = LambdaMetafactory.metafactory(..);</p></blockquote><p>我可以基于这个思路，通过Class.forName，寻找出当前环境能提供的内容，甚至可以查找一些三方包。</p><p>只要符合要求的，我都包装进来</p><ul><li>这样即便某些低版本的JDK也可以使用高版本的一些能力（如果它引用的一些好的三方包）。</li><li>模板方法的核心逻辑封装不需要考虑环境问题（调用入口已经统一）。</li></ul><blockquote><p>方法的传递，其实使用MethodHandle已经足够了，是否再将MethodHandle包装为Lambda，取决于是否要统一调用入口。</p><p>所以如下，既有MethodHandle，也有Lambda（BiFunction，ToIntFunction）</p></blockquote><pre><code class="language-java">public static final BiFunction&lt;char[], Boolean, String&gt; STRING_CREATOR_JDK8; // JDK 8 的 String 创建函数public static final BiFunction&lt;byte[], Byte, String&gt; STRING_CREATOR_JDK11; // JDK 11 的 String 创建函数public static final ToIntFunction&lt;String&gt; STRING_CODER; // String 的编码函数public static final Function&lt;String, byte[]&gt; STRING_VALUE; // String 的值函数public static final MethodHandle METHOD_HANDLE_HAS_NEGATIVE; // 是否有负数的方法句柄public static final Predicate&lt;byte[]&gt; PREDICATE_IS_ASCII; // 是否为 ASCII 的谓词</code></pre><p>如下则是通过其他框架来扩展自身能力：</p><pre><code class="language-java">// com.alibaba.fastjson2.writer.ObjectWriterProvider#getObjectWriterInternalswitch (className) {  case &quot;com.google.common.collect.HashMultimap&quot;:  case &quot;com.google.common.collect.LinkedListMultimap&quot;:  case &quot;com.google.common.collect.LinkedHashMultimap&quot;:  case &quot;com.google.common.collect.ArrayListMultimap&quot;:  case &quot;com.google.common.collect.TreeMultimap&quot;:    objectWriter = GuavaSupport.createAsMapWriter(objectClass);    break;  case &quot;com.google.common.collect.AbstractMapBasedMultimap$RandomAccessWrappedList&quot;:    objectWriter = ObjectWriterImplList.INSTANCE;    break;  case &quot;com.alibaba.fastjson.JSONObject&quot;:    objectWriter = ObjectWriterImplMap.of(objectClass);    break;  case &quot;android.net.Uri$OpaqueUri&quot;:  case &quot;android.net.Uri$HierarchicalUri&quot;:  case &quot;android.net.Uri$StringUri&quot;:    objectWriter = ObjectWriterImplToString.INSTANCE;    break;  default:    break;}</code></pre><h2 id="性能提升的思路">性能提升的思路</h2><ul><li>Lambada表达式+MethodHandler，替换反射调用。</li><li>Unsafe方法，替换反射调用set方法。</li><li>多维度缓存，确保在高频调用逻辑均由缓存承载。</li><li>使用全局上下文，维护复用全局变量，减少重复对象的生成。</li><li>编码相关：位移操作代替加减乘除；数组索引代替map中的key；减少容器自动扩容行为;尽量在类加载阶段获取信息;尽可能的使用IdentityHashMap。</li><li>CodeGen算法使用。</li><li>使用字节码提升JSONPath性能。</li><li>使用Fnv算法代替原生的hash算法。</li></ul>]]>
                </content>
            </entry>
            <entry>
                <title><![CDATA[类加载器原理]]></title>
                <link rel="alternate" type="text/html" href="https://www.wangdaye.net/archives/类加载器原理" />
                <id>tag:https://www.wangdaye.net,2024-01-28:类加载器原理</id>
                <published>2024-01-28T23:45:27+08:00</published>
                <updated>2024-01-28T23:45:33+08:00</updated>
                <author>
                    <name>王大爷</name>
                    <uri>https://www.wangdaye.net</uri>
                </author>
                <content type="html">
                        <![CDATA[<h1 id="类加载器">类加载器</h1><p>虚拟机设计团队把类加载阶段中的“通过一个类的全限定名来获取描述此类的二进制字节流”这个动作放到Java虚拟机外部去实现，以便让应用程序自己决定如何去获取所需要的类。实现这个动作的代码模块称为“类加载器”。类加载器可以说是Java语言的一项创新，也是Java语言流行的重要原因之一，它最初是为了满足Java Applet的需求而开发出来的。虽然目前Java Applet技术基本上已经“死掉”[插图]，但类加载器却在类层次划分、OSGi、热部署、代码加密等领域大放异彩，成为了Java技术体系中一块重要的基石，可谓是失之桑榆，收之东隅。</p><blockquote><p>如下摘录自《深入理解JAVA虚拟机》</p></blockquote><h2 id="类与类加载器">类与类加载器</h2><p>类加载器虽然只用于实现类的加载动作，但它在Java程序中起到的作用却远远不限于类加载阶段。对于任意一个类，都需要由加载它的类加载器和这个类本身一同确立其在Java虚拟机中的唯一性，每一个类加载器，都拥有一个独立的类名称空间。这句话可以表达得更通俗一些：比较两个类是否“相等”，只有在这两个类是由同一个类加载器加载的前提下才有意义，否则，即使这两个类来源于同一个Class文件，被同一个虚拟机加载，只要加载它们的类加载器不同，那这两个类就必定不相等。</p><p>这里所指的“相等”，包括代表类的Class对象的equals()方法、isAssignableFrom()方法、isInstance()方法的返回结果，也包括使用instanceof关键字做对象所属关系判定等情况。如果没有注意到类加载器的影响，在某些情况下可能会产生具有迷惑性的结果，代码清单7-8中演示了不同的类加载器对instanceof关键字运算的结果的影响。</p><pre><code>/＊＊ ＊ 类加载器与instanceof关键字演示 ＊ ＊ @author zzm ＊/public class ClassLoaderTest {    public static void main(String[] args) throws Exception {        ClassLoader myLoader = new ClassLoader() {              @Override              public Class&lt;?&gt; loadClass(String name) throws ClassNotFoundException {                  try {                        String fileName = name.substring(name.lastIndexOf(&quot;.&quot;) + 1) +&quot;.class&quot;;                        InputStream is = getClass().getResourceAsStream(fileName);                        if (is == null) {                            return super.loadClass(name);                        }                        byte[] b = new byte[is.available()];                        is.read(b);                        return defineClass(name, b, 0, b.length);                  } catch (IOException e) {                        throw new ClassNotFoundException(name);                    }                }          };          Object obj = myLoader.loadClass(&quot;org.fenixsoft.classloading.ClassLoaderTest&quot;).newInstance();          System.out.println(obj.getClass());          System.out.println(obj instanceof org.fenixsoft.classloading.ClassLoaderTest);      }}</code></pre><p>运行结果：</p><pre><code>class org.fenixsoft.classloading.ClassLoaderTestfalse</code></pre><p>代码清单7-8中构造了一个简单的类加载器，尽管很简单，但是对于这个演示来说还是够用了。它可以加载与自己在同一路径下的Class文件。我们使用这个类加载器去加载了一个名为“org.fenixsoft.classloading.ClassLoaderTest”的类，并实例化了这个类的对象。两行输出结果中，从第一句可以看出，这个对象确实是类org.fenixsoft.classloading. ClassLoaderTest实例化出来的对象，但从第二句可以发现，这个对象与类org.fenixsoft. classloading.ClassLoaderTest做所属类型检查的时候却返回了false，这是因为虚拟机中存在了两个ClassLoaderTest类，一个是由系统应用程序类加载器加载的，另外一个是由我们自定义的类加载器加载的，虽然都来自同一个Class文件，但依然是两个独立的类，做对象所属类型检查时结果自然为false。</p><h2 id="双亲委派模型">双亲委派模型</h2><p>从Java虚拟机的角度来讲，只存在两种不同的类加载器：一种是启动类加载器（Bootstrap ClassLoader），这个类加载器使用C++语言实现[插图]，是虚拟机自身的一部分；另一种就是所有其他的类加载器，这些类加载器都由Java语言实现，独立于虚拟机外部，并且全都继承自抽象类java.lang.ClassLoader。</p><p>从Java开发人员的角度来看，类加载器还可以划分得更细致一些，绝大部分Java程序都会使用到以下3种系统提供的类加载器。</p><ul><li>启动类加载器（Bootstrap ClassLoader）：前面已经介绍过，这个类将器负责将存放在&lt;JAVA_HOME&gt;\lib目录中的，或者被-Xbootclasspath参数所指定的路径中的，并且是虚拟机识别的（仅按照文件名识别，如rt.jar，名字不符合的类库即使放在lib目录中也不会被加载）类库加载到虚拟机内存中。启动类加载器无法被Java程序直接引用，用户在编写自定义类加载器时，如果需要把加载请求委派给引导类加载器，那直接使用null代替即可，如代码清单7-9所示为java.lang.ClassLoader.getClassLoader()方法的代码片段。</li></ul><p><strong>代码清单7-9 ClassLoader.getClassLoader()方法的代码片段</strong></p><pre><code>    /＊＊    Returns the class loader for the class. Some implementations may use nullto represent the bootstrap class loader. This method will return null in suchimplementations if this class was loaded by the bootstrap class loader.    ＊/    public ClassLoader getClassLoader() {        ClassLoader cl = getClassLoader0();        if (cl == null)            return null;        SecurityManager sm = System.getSecurityManager();        if (sm != null) {            ClassLoader ccl = ClassLoader.getCallerClassLoader();            if (ccl != null &amp;&amp; ccl != cl &amp;&amp; !cl.isAncestor(ccl)) {                sm.checkPermission(SecurityConstants.GET_CLASSLOADER_PERMISSION);            }        }        return cl;    }</code></pre><ul><li>扩展类加载器（Extension ClassLoader）：这个加载器由sun.misc.Launcher$ExtClassLoader实现，它负责加载&lt;JAVA_HOME&gt;\lib\ext目录中的，或者被java.ext.dirs系统变量所指定的路径中的所有类库，开发者可以直接使用扩展类加载器。</li><li>应用程序类加载器（Application ClassLoader）：这个类加载器由sun.misc.Launcher$App-ClassLoader实现。由于这个类加载器是ClassLoader中的getSystemClassLoader()方法的返回值，所以一般也称它为系统类加载器。它负责加载用户类路径（ClassPath）上所指定的类库，开发者可以直接使用这个类加载器，如果应用程序中没有自定义过自己的类加载器，一般情况下这个就是程序中默认的类加载器。</li></ul><p>我们的应用程序都是由这3种类加载器互相配合进行加载的，如果有必要，还可以加入自己定义的类加载器。这些类加载器之间的关系一般如图7-2所示。</p><p><img src="https://www.wangdaye.net/upload/2024/01/image-fc60a056be9c445a899c1890af703fba.png" alt="图7-2 类加载器双亲委派模型" /></p><p>图7-2中展示的类加载器之间的这种层次关系，称为类加载器的双亲委派模型（Parents Delegation Model）。双亲委派模型要求除了顶层的启动类加载器外，其余的类加载器都应当有自己的父类加载器。这里类加载器之间的父子关系一般不会以继承（Inheritance）的关系来实现，而是都使用组合（Composition）关系来复用父加载器的代码。</p><p>类加载器的双亲委派模型在JDK 1.2期间被引入并被广泛应用于之后几乎所有的Java程序中，但它并不是一个强制性的约束模型，而是Java设计者推荐给开发者的一种类加载器实现方式。</p><p>双亲委派模型的工作过程是：如果一个类加载器收到了类加载的请求，它首先不会自己去尝试加载这个类，而是把这个请求委派给父类加载器去完成，每一个层次的类加载器都是如此，因此所有的加载请求最终都应该传送到顶层的启动类加载器中，只有当父加载器反馈自己无法完成这个加载请求（它的搜索范围中没有找到所需的类）时，子加载器才会尝试自己去加载。</p><p>使用双亲委派模型来组织类加载器之间的关系，有一个显而易见的好处就是Java类随着它的类加载器一起具备了一种带有优先级的层次关系。例如类java.lang.Object，它存放在rt.jar之中，无论哪一个类加载器要加载这个类，最终都是委派给处于模型最顶端的启动类加载器进行加载，因此Object类在程序的各种类加载器环境中都是同一个类。相反，如果没有使用双亲委派模型，由各个类加载器自行去加载的话，如果用户自己编写了一个称为java. lang.Object的类，并放在程序的ClassPath中，那系统中将会出现多个不同的Object类，Java类型体系中最基础的行为也就无法保证，应用程序也将会变得一片混乱。如果读者有兴趣的话，可以尝试去编写一个与rt.jar类库中已有类重名的Java类，将会发现可以正常编译，但永远无法被加载运行[插图]。</p><p>双亲委派模型对于保证Java程序的稳定运作很重要，但它的实现却非常简单，实现双亲委派的代码都集中在java.lang.ClassLoader的loadClass()方法之中，如代码清单7-10所示，逻辑清晰易懂：先检查是否已经被加载过，若没有加载则调用父加载器的loadClass()方法，若父加载器为空则默认使用启动类加载器作为父加载器。如果父类加载失败，抛出ClassNotFoundException异常后，再调用自己的findClass()方法进行加载。</p><p><strong>代码清单7-10 双亲委派模型的实现</strong></p><pre><code>   protected synchronized Class&lt;?&gt; loadClass(String name, boolean resolve) throwsClassNotFoundException   {       //首先，检查请求的类是否已经被加载过了       Class c = findLoadedClass(name);       if (c == null) {           try {           if (parent != null) {               c = parent.loadClass(name, false);           } else {               c = findBootstrapClassOrNull(name);           }           } catch (ClassNotFoundException e) {               //如果父类加载器抛出ClassNotFoundException               //说明父类加载器无法完成加载请求           }           if (c == null) {               //在父类加载器无法加载的时候               //再调用本身的findClass方法来进行类加载               c = findClass(name);           }       }       if (resolve) {           resolveClass(c);       }       return c;   }</code></pre><h2 id="破坏双亲委派模型">破坏双亲委派模型</h2><p>上文提到过双亲委派模型并不是一个强制性的约束模型，而是Java设计者推荐给开发者的类加载器实现方式。在Java的世界中大部分的类加载器都遵循这个模型，但也有例外，到目前为止，双亲委派模型主要出现过3较大规模的“被破坏”情况。</p><p>双亲委派模型的第一次“被破坏”其实发生在双亲委派模型出现之前——即JDK 1.2发布之前。由于双亲委派模型在JDK 1.2之后才被引入，而类加载器和抽象类java.lang. ClassLoader则在JDK 1.0时代就已经存在，面对已经存在的用户自定义类加载器的实现代码，Java设计者引入双亲委派模型时不得不做出一些妥协。为了向前兼容，JDK 1.2之后的java.lang.ClassLoader添加了一个新的protected方法findClass()，在此之前，用户去继承java. lang.ClassLoader的唯一目的就是为了重写loadClass()方法，因为虚拟机在进行类加载的时候会调用加载器的私有方法loadClassInternal()，而这个方法的唯一逻辑就是去调用自己的loadClass()。</p><p>上一节我们已经看过loadClass()方法的代码，双亲委派的具体逻辑就实现在这个方法之中，JDK 1.2之后已不提倡用户再去覆盖loadClass()方法，而应当把自己的类加载逻辑写到findClass()方法中，在loadClass()方法的逻辑里如果父类加载失败，则会调用自己的findClass()方法来完成加载，这样就可以保证新写出来的类加载器是符合双亲委派规则的。</p><p>双亲委派模型的第二次“被破坏”是由这个模型自身的缺陷所导致的，双亲委派很好地解决了各个类加载器的基础类的统一问题（越基础的类由越上层的加载器进行加载），基础类之所以称为“基础”，是因为它们总是作为被用户代码调用的API，但世事往往没有绝对的完美，如果基础类又要调用回用户的代码，那该怎么办？</p><p>这并非是不可能的事情，一个典型的例子便是JNDI服务，JNDI现在已经是Java的标准服务，它的代码由启动类加载器去加载（在JDK 1.3时放进去的rt.jar），但JNDI的目的就是对资源进行集中管理和查找，它需要调用由独立厂商实现并部署在应用程序的ClassPath下的JNDI接口提供者（SPI，Service Provider Interface）的代码，但启动类加载器不可能“认识”这些代码啊！那该怎么办？为了解决这个问题，Java设计团队只好引入了一个不太优雅的设计：线程上下文类加载器（Thread Context ClassLoader）。</p><p>这个类加载器可以通过java.lang.Thread类的setContextClassLoaser()方法进行设置，如果创建线程时还未设置，它将会从父线程中继承一个，如果在应用程序的全局范围内都没有设置过的话，那这个类加载器默认就是应用程序类加载器。</p><p>有了线程上下文类加载器，就可以做一些“舞弊”的事情了，JNDI服务使用这个线程上下文类加载器去加载所需要的SPI代码，也就是父类加载器请求子类加载器去完成类加载的动作，这种行为实际上就是打通了双亲委派模型的层次结构来逆向使用类加载器，实际上已经违背了双亲委派模型的一般性原则，但这也是无可奈何的事情。Java中所有涉及SPI的加载动作基本上都采用这种方式，例如JNDI、JDBC、JCE、JAXB和JBI等。</p><p>双亲委派模型的第三次“被破坏”是由于用户对程序动态性的追求而导致的，这里所说的“动态性”指的是当前一些非常“热门”的名词：代码热替换（HotSwap）、模块热部署（Hot Deployment）等，说白了就是希望应用程序能像我们的计算机外设那样，接上鼠标、U盘，不用重启机器就能立即使用，鼠标有问题或要升级就换个鼠标，不用停机也不用重启。对于个人计算机来说，重启一次其实没有什么大不了的，但对于一些生产系统来说，关机重启一次可能就要被列为生产事故，这种情况下热部署就对软件开发者，尤其是企业级软件开发者具有很大的吸引力。</p><p>Sun公司所提出的JSR-294[插图]、JSR-277[插图]规范在与JCP组织的模块化规范之争中落败给JSR-291（即OSGi R4.2），虽然Sun不甘失去Java模块化的主导权，独立在发展Jigsaw项目，但目前OSGi已经成为了业界“事实上”的Java模块化标准[插图]，而OSGi实现模块化热部署的关键则是它自定义的类加载器机制的实现。每一个程序模块（OSGi中称为Bundle）都有一个自己的类加载器，当需要更换一个Bundle时，就把Bundle连同类加载器一起换掉以实现代码的热替换。</p><p>在OSGi环境下，类加载器不再是双亲委派模型中的树状结构，而是进一步发展为更加复杂的网状结构，当收到类加载请求时，OSGi将按照下面的顺序进行类搜索：</p><p>1）将以java.*开头的类委派给父类加载器加载。<br />2）否则，将委派列表名单内的类委派给父类加载器加载。<br />3）否则，将Import列表中的类委派给Export这个类的Bundle的类加载器加载。<br />4）否则，查找当前Bundle的ClassPath，使用自己的类加载器加载。<br />5）否则，查找类是否在自己的Fragment Bundle中，如果在，则委派给FragmentBundle的类加载器加载。<br />6）否则，查找Dynamic Import列表的Bundle，委派给对应Bundle的类加载器加载。<br />7）否则，类查找失败。上面的查找顺序中只有开头两点仍然符合双亲委派规则，其余的类查找都是在平级的类加载器中进行的。</p><p>上面的查找顺序中只有开头两点仍然符合双亲委派规则，其余的类查找都是在平级的类加载器中进行的。</p><p>笔者虽然使用了“被破坏”这个词来形容上述不符合双亲委派模型原则的行为，但这里“被破坏”并不带有贬义的感情色彩。只要有足够意义和理由，突破已有的原则就可认为是一种创新。正如OSGi中的类加载器并不符合传统的双亲委派的类加载器，并且业界对其为了实现热部署而带来的额外的高复杂度还存在不少争议，但在Java程序员中基本有一个共识：OSGi中对类加载器的使用是很值得学习的，弄懂了OSGi的实现，就可以算是掌握了类加载器的精髓。</p>]]>
                </content>
            </entry>
            <entry>
                <title><![CDATA[深入理解sun.misc.Unsafe原理]]></title>
                <link rel="alternate" type="text/html" href="https://www.wangdaye.net/archives/深入理解sunmiscunsafe原理" />
                <id>tag:https://www.wangdaye.net,2024-01-27:深入理解sunmiscunsafe原理</id>
                <published>2024-01-27T15:36:30+08:00</published>
                <updated>2024-01-27T15:55:46+08:00</updated>
                <author>
                    <name>王大爷</name>
                    <uri>https://www.wangdaye.net</uri>
                </author>
                <content type="html">
                        <![CDATA[<h2 id="前言">前言</h2><p><a href="https://so.csdn.net/so/search?q=Unsafe&amp;spm=1001.2101.3001.7020">Unsafe</a>类在JDK源码中被广泛使用，在Spark使用off-heap memory时也会使用到，该类功能很强大，涉及到类加载机制（<a href="https://blog.csdn.net/zyzzxycj/article/details/89846181">深入理解ClassLoader工作机制</a>），其实例一般情况是获取不到的，源码中的设计是采用<a href="https://so.csdn.net/so/search?q=%E5%8D%95%E4%BE%8B%E6%A8%A1%E5%BC%8F&amp;spm=1001.2101.3001.7020">单例模式</a>，不是系统加载初始化就会抛出SecurityException异常。</p><p>这个类的提供了一些绕开JVM的更底层功能，基于它的实现可以提高效率。但是，它是一把双刃剑：正如它的名字所预示的那样，它是Unsafe的，它所分配的内存需要手动free（不被GC回收）。如果对Unsafe类理解的不够透彻，就进行使用的话，就等于给自己挖了无形之坑，最为致命。</p><p>由于sun并没有将其开源，也没给出官方的Document，所以笔者只能参考一些博客（如<a href="http://mishadoff.com/blog/java-magic-part-4-sun-dot-misc-dot-unsafe/">Java Magic. Part 4: sun.misc.Unsafe</a>）以及Unsafe在JDK源码中的一些使用，来加深理解。</p><h2 id="获取unsafe类的实例">获取Unsafe类的实例</h2><ol><li>必须是Bootstrap ClassLoader加载的类<br />getUnsafe方法源码:<br /><img src="https://www.wangdaye.net/upload/2024/01/image-8da682049dcc4ab0ab32d788140b62cc.png" alt="image.png" /><br />isSystemDomainLoader:<br /><img src="https://www.wangdaye.net/upload/2024/01/image-900ad62eae7a45bf9ac9e545190d2c36.png" alt="image.png" /></li><li>通过反射暴力获取</li></ol><pre><code class="language-Java">    // 通过反射实例化Unsafe    Field f = Unsafe.class.getDeclaredField(&quot;theUnsafe&quot;);    f.setAccessible(true);    Unsafe unsafe = (Unsafe) f.get(null);</code></pre><h2 id="unsafe类中的核心方法">Unsafe类中的核心方法</h2><pre><code>//重新分配内存         public native long reallocateMemory(long address, long bytes);           //分配内存           public native long allocateMemory(long bytes);           //释放内存           public native void freeMemory(long address);           //在给定的内存块中设置值           public native void setMemory(Object o, long offset, long bytes, byte value);           //从一个内存块拷贝到另一个内存块           public native void copyMemory(Object srcBase, long srcOffset, Object destBase, long destOffset, long bytes);           //获取值，不管java的访问限制，其他有类似的getInt，getDouble，getLong，getChar等等           public native Object getObject(Object o, long offset);           //设置值，不管java的访问限制，其他有类似的putInt,putDouble，putLong，putChar等等           public native void putObject(Object o, long offset);           //从一个给定的内存地址获取本地指针，如果不是allocateMemory方法的，结果将不确定           public native long getAddress(long address);           //存储一个本地指针到一个给定的内存地址,如果地址不是allocateMemory方法的，结果将不确定           public native void putAddress(long address, long x);           //该方法返回给定field的内存地址偏移量，这个值对于给定的filed是唯一的且是固定不变的           public native long staticFieldOffset(Field f);           //报告一个给定的字段的位置，不管这个字段是private，public还是保护类型，和staticFieldBase结合使用           public native long objectFieldOffset(Field f);           //获取一个给定字段的位置           public native Object staticFieldBase(Field f);           //确保给定class被初始化，这往往需要结合基类的静态域（field）           public native void ensureClassInitialized(Class c);           //可以获取数组第一个元素的偏移地址           public native int arrayBaseOffset(Class arrayClass);           //可以获取数组的转换因子，也就是数组中元素的增量地址。将arrayBaseOffset与arrayIndexScale配合使用， 可以定位数组中每个元素在内存中的位置           public native int arrayIndexScale(Class arrayClass);           //获取本机内存的页数，这个值永远都是2的幂次方           public native int pageSize();           //告诉虚拟机定义了一个没有安全检查的类，默认情况下这个类加载器和保护域来着调用者类           public native Class defineClass(String name, byte[] b, int off, int len, ClassLoader loader, ProtectionDomain protectionDomain);           //定义一个类，但是不让它知道类加载器和系统字典           public native Class defineAnonymousClass(Class hostClass, byte[] data, Object[] cpPatches);           //锁定对象，必须是没有被锁的         public native void monitorEnter(Object o);           //解锁对象           public native void monitorExit(Object o);           //试图锁定对象，返回true或false是否锁定成功，如果锁定，必须用monitorExit解锁           public native boolean tryMonitorEnter(Object o);           //引发异常，没有通知           public native void throwException(Throwable ee);           //CAS，如果对象偏移量上的值=期待值，更新为x,返回true.否则false.类似的有compareAndSwapInt,compareAndSwapLong,compareAndSwapBoolean,compareAndSwapChar等等。           public final native boolean compareAndSwapObject(Object o, long offset,  Object expected, Object x);           // 该方法获取对象中offset偏移地址对应的整型field的值,支持volatile load语义。类似的方法有getIntVolatile，getBooleanVolatile等等           public native Object getObjectVolatile(Object o, long offset);            //线程调用该方法，线程将一直阻塞直到超时，或者是中断条件出现。           public native void park(boolean isAbsolute, long time);           //终止挂起的线程，恢复正常.java.util.concurrent包中挂起操作都是在LockSupport类实现的，也正是使用这两个方法         public native void unpark(Object thread);           //获取系统在不同时间系统的负载情况           public native int getLoadAverage(double[] loadavg, int nelems);           //创建一个类的实例，不需要调用它的构造函数、初使化代码、各种JVM安全检查以及其它的一些底层的东西。即使构造函数是私有，我们也可以通过这个方法创建它的实例,对于单例模式，简直是噩梦。          public native Object allocateInstance(Class cls) throws InstantiationException; </code></pre><h3 id="总的来说就是这么几类">总的来说就是这么几类</h3><p>（1）Info相关。主要返回某些低级别的内存信息：addressSize(), pageSize()</p><p>（2）Objects相关。主要提供Object和它的域操纵方法：allocateInstance(),objectFieldOffset()</p><p>（3）Class相关。主要提供Class和它的静态域操纵方法：staticFieldOffset(),defineClass(),defineAnonymousClass(),ensureClassInitialized()</p><p>（4）Arrays相关。数组操纵方法：arrayBaseOffset(),arrayIndexScale()</p><p>（5）Synchronization相关。主要提供低级别同步原语（如基于CPU的CAS（Compare-And-Swap）原语）：monitorEnter(),tryMonitorEnter(),monitorExit(),compareAndSwapInt(),putOrderedInt()</p><p>（6）Memory相关。直接内存访问方法（绕过JVM堆直接操纵本地内存）：allocateMemory(),copyMemory(),freeMemory(),getAddress(),getInt(),putInt()</p><h3 id="unsafe该怎么用举些例子">Unsafe该怎么用？举些例子</h3><h4 id="1unsafeallocateinstance">1、Unsafe.allocateInstance</h4><pre><code>public static void main(String[] args) throws Exception {            Field f = Unsafe.class.getDeclaredField(&quot;theUnsafe&quot;);            f.setAccessible(true);            Unsafe unsafe = (Unsafe) f.get(null);            A o1 = new A(); // constructor            o1.a(); // prints 1            A o2 = A.class.newInstance(); // reflection            o2.a(); // prints 1            A o3 = (A) unsafe.allocateInstance(A.class); // unsafe            o3.a(); // prints 0        }        static class A {            private long a; // not initialized value, default 0            public A() {                this.a = 1; // initialization            }            public long a() {                return this.a;            }        }allocateInstance()根本没有进入构造方法，对于单例模式，简直是噩梦。</code></pre><h4 id="2内存修改绕过安全检查器unsafeobjectfieldoffset">2、内存修改，绕过安全检查器（Unsafe.objectFieldOffset）</h4><pre><code>public static void main(String[] args) throws Exception {            Field f = Unsafe.class.getDeclaredField(&quot;theUnsafe&quot;);            f.setAccessible(true);            Unsafe unsafe = (Unsafe) f.get(null);            Guard guard = new Guard();            guard.giveAccess();   // false, no access            // bypass            Field field = guard.getClass().getDeclaredField(&quot;ACCESS_ALLOWED&quot;);            unsafe.putInt(guard, unsafe.objectFieldOffset(field), 42); // memory corruption            guard.giveAccess(); // true, access granted        }        static class Guard {            private int ACCESS_ALLOWED = 1;            public boolean giveAccess() {                return 42 == ACCESS_ALLOWED;            }        }</code></pre><p>通过获取目标对象的字段在内存中的offset，并使用putInt()方法，类的ACCESS_ALLOWED被修改。在已知类结构的时候，数据的偏移总是可以获得的（与c++中的类中数据的偏移计算是一致的）。</p><h4 id="3sizeof-计算内存大小unsafegetdeclaredfields和unsafeobjectfieldoffset">3、sizeOf 计算内存大小（Unsafe.getDeclaredFields和Unsafe.objectFieldOffset）</h4><pre><code>public static void main(String[] args) throws Exception {            Guard guard = new Guard();            sizeOf(guard); // 16, the size of guard        }        static class Guard {            private int ACCESS_ALLOWED = 1;            public boolean giveAccess() {                return 42 == ACCESS_ALLOWED;            }        }        public static long sizeOf(Object o) throws Exception {            Field f = Unsafe.class.getDeclaredField(&quot;theUnsafe&quot;);            f.setAccessible(true);            Unsafe unsafe = (Unsafe) f.get(null);            HashSet&lt;Field&gt; fields = new HashSet();            Class c = o.getClass();            while (c != Object.class) {                for (Field field : c.getDeclaredFields()) {                    if ((field.getModifiers() &amp; Modifier.STATIC) == 0) {                        fields.add(field);                    }                }                c = c.getSuperclass();            }            // get offset            long maxSize = 0;            for (Field field : fields) {                long offset = unsafe.objectFieldOffset(field);                if (offset &gt; maxSize) {                    maxSize = offset;                }            }            return ((maxSize/8) + 1) * 8;   // padding        }</code></pre><p>算法的思路非常清晰：从底层子类开始，依次取出它自己和它的所有超类的非静态域，放置到一个HashSet中（重复的只计算一次，Java是单继承），然后使用objectFieldOffset()获得一个最大偏移，最后还考虑了对齐。</p><h4 id="4实现java浅复制">4、实现Java浅复制</h4><pre><code>public static void main(String[] args) throws Exception {            Field f = Unsafe.class.getDeclaredField(&quot;theUnsafe&quot;);            f.setAccessible(true);            unsafe = (Unsafe) f.get(null);            Guard guard = new Guard();            shallowCopy(guard);        }        private static Unsafe getUnsafe() {            return unsafe;        }        static Object shallowCopy(Object obj) throws Exception {            long size = sizeOf(obj);            long start = toAddress(obj);            long address = getUnsafe().allocateMemory(size);            getUnsafe().copyMemory(start, address, size);            return fromAddress(address);        }        static long toAddress(Object obj) {            Object[] array = new Object[]{obj};            long baseOffset = getUnsafe().arrayBaseOffset(Object[].class);            return normalize(getUnsafe().getLong(array, baseOffset));        }        static Object fromAddress(long address) {            Object[] array = new Object[] {null};            long baseOffset = getUnsafe().arrayBaseOffset(Object[].class);            getUnsafe().putLong(array, baseOffset, address);            return array[0];        }        static class Guard {            private int ACCESS_ALLOWED = 1;            public boolean giveAccess() {                return 42 == ACCESS_ALLOWED;            }        }        public static long sizeOf(Object o) throws Exception {            Field f = Unsafe.class.getDeclaredField(&quot;theUnsafe&quot;);            f.setAccessible(true);            Unsafe unsafe = (Unsafe) f.get(null);            HashSet&lt;Field&gt; fields = new HashSet();            Class c = o.getClass();            while (c != Object.class) {                for (Field field : c.getDeclaredFields()) {                    if ((field.getModifiers() &amp; Modifier.STATIC) == 0) {                        fields.add(field);                    }                }                c = c.getSuperclass();            }            // get offset            long maxSize = 0;            for (Field field : fields) {                long offset = unsafe.objectFieldOffset(field);                if (offset &gt; maxSize) {                    maxSize = offset;                }            }            return ((maxSize / 8) + 1) * 8;   // padding        }</code></pre><p>思路很简单，利用Unsafe.copyMemory()，将老地址及其指向的对象的size，拷贝到新的内存地址上。<br />并且浅复制函数可以应用于任意java对象，它的尺寸是动态计算的。<br />（在实际测试的时候，执行unsafe.copyMemory时，JVM会输出hs_err_pid.log日志然后挂掉，该问题还有待排查）</p><h4 id="5隐藏密码">5、隐藏密码</h4><p>一般密码都要存成byte[]或者char[]数组，为什么呢？？<br />因为我们使用完了，可以直接将他们设为null。但如果密码存在String中，将其设为null，密码实际任然存在在内存中，等到GC后，才能被释放，就很不安全。<br />所以当我们把密码字段存储在String中时，在密码字段使用完之后，最安全的做法是：将它的值覆盖。<br />很多不再需要的，但是又是比较机密的对象，想快点消灭证据，都可以通过这种方法来消除。</p><pre><code>Field stringValue = String.class.getDeclaredField(&quot;value&quot;);        stringValue.setAccessible(true);        char[] mem = (char[]) stringValue.get(password);        for (int i = 0; i &lt; mem.length; i++) {            mem[i] = '?';        }</code></pre><h4 id="6多继承">6、多继承</h4><p>在java中没有多继承，除非我们能在这些不同的类互相强制转换。</p><pre><code>long intClassAddress = normalize(getUnsafe().getInt(new Integer(0), 4L));    long strClassAddress = normalize(getUnsafe().getInt(&quot;&quot;, 4L));    getUnsafe().putAddress(intClassAddress + 36, strClassAddress);</code></pre><p>上面这段代码将String添加为int的父类，所以我们转换的时候就不会报错了。</p><pre><code>(String) (Object) (new Integer(666))    static inline bool    compareAndSwap (volatile jint *addr, jint old, jint new_val)    {      jboolean result = false;      spinlock lock;      // result=原先指针指向的地址的值(*addr)是否与旧的值(old)相等      if ((result = (*addr == old)))        // 如果相等则把内存修改为新值        *addr = new_val;      return result;</code></pre><h4 id="7动态加载类">7、动态加载类</h4><p>标准的动态加载类的方法是Class.forName()(在编写jdbc程序时，记忆深刻)，使用Unsafe也可以动态加载java 的class文件。<br />我们可以在程序运行是动态加载编译好的.class文件，不明白<a href="https://so.csdn.net/so/search?q=ClassLoader&amp;spm=1001.2101.3001.7020">ClassLoader</a>的可以参考&lt;<a href="https://blog.csdn.net/zyzzxycj/article/details/89846181">深入理解ClassLoader工作机制</a>&gt;<br /><strong>具体是怎么实现的呢？？</strong></p><pre><code>// 我们首先读取一个class文件到byte数组中    byte[] classContents = getClassContent();    // 然后通过Unsafe.defineClass()来加载对应的Class    Class c = getUnsafe().defineClass(                  null, classContents, 0, classContents.length);    // 最后调用    c.getMethod(&quot;a&quot;).invoke(c.newInstance(), null); // 1    private static byte[] getClassContent() throws Exception {        File f = new File(&quot;/home/mishadoff/tmp/A.class&quot;);        FileInputStream input = new FileInputStream(f);        byte[] content = new byte[(int)f.length()];        input.read(content);        input.close();        return content;    }</code></pre><p>动态加载、代理、切片等功能中可以应用。</p><h4 id="8快速序列化">8、快速序列化</h4><p>我们都知道标准的Java Serializable速度很慢，它还限制类必须有public无参构造函数。<br />Externalizable相对好些，但它需要为序列化的类指定schema。<br />更流行的如kyro，在小内存的情况下不适用。<br /><strong>序列化过程：</strong></p><ol><li>用反射构建对象的schema</li><li>用Unsafe中的getLong, getInt, getObject等方法来检索对象中字段的值。</li><li>增加对象对应的类的标示符，来标记序列化结果。</li><li>将结果写入文件或者输出流。(可以增加压缩来减小序列化结果)</li></ol><p><strong>反序列化过程：</strong></p><ol><li>使用Unsafe.allocateInstance()来实例化一个被序列化的对象。(不需要执行构造函数)</li><li>构建schema，同序列化过程中的第一步。</li><li>从文件或者输入流中读取所有的字段。</li><li>用Unsafe中的putLong, putInt, putObject等方法来填充该对象。</li></ol><p>在kyro序列化中，也有一些使用Unsafe的尝试：<a href="https://code.google.com/archive/p/kryo/issues/75">https://code.google.com/archive/p/kryo/issues/75</a><br />（笔者还没仔细看，有兴趣的可以阅读一下。。估计能看到这儿的也挺累了）</p><h4 id="9在非java堆中分配内存">9、在非Java堆中分配内存</h4><p>使用java 的new会在堆中为对象分配内存，并且对象的生命周期内，会被JVM GC管理。<br />Unsafe分配的内存，不受Integer.MAX_VALUE的限制，并且分配在非堆内存，使用它时，需要非常谨慎：忘记手动回收时，会产生内存泄露；非法的地址访问时，会导致JVM崩溃。在需要分配大的连续区域、实时编程（不能容忍JVM延迟）时，可以使用它。java.nio使用这一技术。<br /><a href="https://so.csdn.net/so/search?q=Spark&amp;spm=1001.2101.3001.7020">Spark</a>中的Netty也使用了这个技术。<br />在Spark UnsafeMemoryAllocator源码中我们可以看到其使用了Unsafe.allocateMemory()并会抛出OOM异常：<br /><img src="https://www.wangdaye.net/upload/2024/01/image-1f3f9bca7db4431db5091a9e9d3dc7bc.png" alt="image.png" /></p><h4 id="10大数组">10、大数组</h4><p>Java的数组最大容量受常量Integer.MAX_VALUE的限制，如果我们用直接申请内存的方式去创建数组，那么数组大小只会收到堆的大小的限制。</p><pre><code>private static Unsafe unsafe;       public static void main(String[] args) throws Exception {            Field f = Unsafe.class.getDeclaredField(&quot;theUnsafe&quot;);            f.setAccessible(true);            unsafe = (Unsafe) f.get(null);            // 设置数组大小为Integer.MAX_VALUE的2倍            long SUPER_SIZE = (long) Integer.MAX_VALUE * 2;            SuperArray array = new SuperArray(SUPER_SIZE);            System.out.println(&quot;Array size:&quot; + array.size()); // 4294967294            int sum = 0;            for (int i = 0; i &lt; 100; i++) {                array.set((long) Integer.MAX_VALUE + i, (byte) 3);                sum += array.get((long) Integer.MAX_VALUE + i);            }            System.out.println(&quot;Sum of 100 elements:&quot; + sum);  // 300        }        private static Unsafe getUnsafe() {            return unsafe;        }        static class SuperArray {            private final static int BYTE = 1;            private long size;            private long address;            public SuperArray(long size) {                this.size = size;                address = getUnsafe().allocateMemory(size * BYTE);            }            public void set(long i, byte value) {                getUnsafe().putByte(address + i * BYTE, value);            }            public int get(long idx) {                return getUnsafe().getByte(address + idx * BYTE);            }            public long size() {                return size;            }        }</code></pre><p>输出结果：<br /><img src="https://www.wangdaye.net/upload/2024/01/image-8618a63d0c234004bea64dc989ad91f9.png" alt="image.png" /></p><h3 id="unsafe类的底层源码实现">Unsafe类的底层源码实现</h3><h4 id="cas">CAS</h4><p>终于等到CAS出场了。CAS在AQS、ConcurrentHashmap、ForkJoinPool、FutureTask、StampedLock等都有大量的应用。甚至可以说很多Java程序员觉得CAS就是他们所了解的最小单元了。确实，也包括我。。<br />但是总觉得源码看到这儿了，不往下看就像一件事做了一半。<br /><a href="http://natUnsafe.cc">natUnsafe.cc</a></p><pre><code>// natUnsafe.cc - Implementation of sun.misc.Unsafe native methods.    /** Copyright (C) 2006, 2007       Free Software Foundation       This file is part of libgcj.    This software is copyrighted work licensed under the terms of the    Libgcj License.  Please consult the file &quot;LIBGCJ_LICENSE&quot; for    details.  */    #include &lt;gcj/cni.h&gt;    #include &lt;gcj/field.h&gt;    #include &lt;gcj/javaprims.h&gt;    #include &lt;jvm.h&gt;    #include &lt;sun/misc/Unsafe.h&gt;    #include &lt;java/lang/System.h&gt;    #include &lt;java/lang/InterruptedException.h&gt;    #include &lt;java/lang/Thread.h&gt;    #include &lt;java/lang/Long.h&gt;    #include &quot;sysdep/locks.h&quot;    // Use a spinlock for multi-word accesses    class spinlock    {      static volatile obj_addr_t lock;    public:    spinlock ()      {        while (! compare_and_swap (&amp;lock, 0, 1))          _Jv_ThreadYield ();      }      ~spinlock ()      {        release_set (&amp;lock, 0);      }    };    // This is a single lock that is used for all synchronized accesses if    // the compiler can't generate inline compare-and-swap operations.  In    // most cases it'll never be used, but the i386 needs it for 64-bit    // locked accesses and so does PPC32\.  It's worth building libgcj with    // target=i486 (or above) to get the inlines.    volatile obj_addr_t spinlock::lock;    static inline bool    compareAndSwap (volatile jint *addr, jint old, jint new_val)    {      jboolean result = false;      spinlock lock;      if ((result = (*addr == old)))        *addr = new_val;      return result;    }    static inline bool    compareAndSwap (volatile jlong *addr, jlong old, jlong new_val)    {      jboolean result = false;      spinlock lock;      if ((result = (*addr == old)))        *addr = new_val;      return result;    }    static inline bool    compareAndSwap (volatile jobject *addr, jobject old, jobject new_val)    {      jboolean result = false;      spinlock lock;      if ((result = (*addr == old)))        *addr = new_val;      return result;    }    jlong    sun::misc::Unsafe::objectFieldOffset (::java::lang::reflect::Field *field)    {      _Jv_Field *fld = _Jv_FromReflectedField (field);      // FIXME: what if it is not an instance field?      return fld-&gt;getOffset();    }    jint    sun::misc::Unsafe::arrayBaseOffset (jclass arrayClass)    {      // FIXME: assert that arrayClass is array.      jclass eltClass = arrayClass-&gt;getComponentType();      return (jint)(jlong) _Jv_GetArrayElementFromElementType (NULL, eltClass);    }    jint    sun::misc::Unsafe::arrayIndexScale (jclass arrayClass)    {      // FIXME: assert that arrayClass is array.      jclass eltClass = arrayClass-&gt;getComponentType();      if (eltClass-&gt;isPrimitive())        return eltClass-&gt;size();      return sizeof (void *);    }    // These methods are used when the compiler fails to generate inline    // versions of the compare-and-swap primitives.    jboolean    sun::misc::Unsafe::compareAndSwapInt (jobject obj, jlong offset,                          jint expect, jint update)    {      jint *addr = (jint *)((char *)obj + offset);      return compareAndSwap (addr, expect, update);    }    jboolean    sun::misc::Unsafe::compareAndSwapLong (jobject obj, jlong offset,                           jlong expect, jlong update)    {      volatile jlong *addr = (jlong*)((char *) obj + offset);      return compareAndSwap (addr, expect, update);    }    jboolean    sun::misc::Unsafe::compareAndSwapObject (jobject obj, jlong offset,                         jobject expect, jobject update)    {      jobject *addr = (jobject*)((char *) obj + offset);      return compareAndSwap (addr, expect, update);    }    void    sun::misc::Unsafe::putOrderedInt (jobject obj, jlong offset, jint value)    {      volatile jint *addr = (jint *) ((char *) obj + offset);      *addr = value;    }    void    sun::misc::Unsafe::putOrderedLong (jobject obj, jlong offset, jlong value)    {      volatile jlong *addr = (jlong *) ((char *) obj + offset);      spinlock lock;      *addr = value;    }    void    sun::misc::Unsafe::putOrderedObject (jobject obj, jlong offset, jobject value)    {      volatile jobject *addr = (jobject *) ((char *) obj + offset);      *addr = value;    }    void    sun::misc::Unsafe::putIntVolatile (jobject obj, jlong offset, jint value)    {      write_barrier ();      volatile jint *addr = (jint *) ((char *) obj + offset);      *addr = value;    }    void    sun::misc::Unsafe::putLongVolatile (jobject obj, jlong offset, jlong value)    {      volatile jlong *addr = (jlong *) ((char *) obj + offset);      spinlock lock;      *addr = value;    }    void    sun::misc::Unsafe::putObjectVolatile (jobject obj, jlong offset, jobject value)    {      write_barrier ();      volatile jobject *addr = (jobject *) ((char *) obj + offset);      *addr = value;    }    #if 0  // FIXME    void    sun::misc::Unsafe::putInt (jobject obj, jlong offset, jint value)    {      jint *addr = (jint *) ((char *) obj + offset);      *addr = value;    }    #endif    void    sun::misc::Unsafe::putLong (jobject obj, jlong offset, jlong value)    {      jlong *addr = (jlong *) ((char *) obj + offset);      spinlock lock;      *addr = value;    }    void    sun::misc::Unsafe::putObject (jobject obj, jlong offset, jobject value)    {      jobject *addr = (jobject *) ((char *) obj + offset);      *addr = value;    }    jint    sun::misc::Unsafe::getIntVolatile (jobject obj, jlong offset)    {      volatile jint *addr = (jint *) ((char *) obj + offset);      jint result = *addr;      read_barrier ();      return result;    }    jobject    sun::misc::Unsafe::getObjectVolatile (jobject obj, jlong offset)    {      volatile jobject *addr = (jobject *) ((char *) obj + offset);      jobject result = *addr;      read_barrier ();      return result;    }    jlong    sun::misc::Unsafe::getLong (jobject obj, jlong offset)    {      jlong *addr = (jlong *) ((char *) obj + offset);      spinlock lock;      return *addr;    }    jlong    sun::misc::Unsafe::getLongVolatile (jobject obj, jlong offset)    {      volatile jlong *addr = (jlong *) ((char *) obj + offset);      spinlock lock;      return *addr;    }    void    sun::misc::Unsafe::unpark (::java::lang::Thread *thread)    {      natThread *nt = (natThread *) thread-&gt;data;      nt-&gt;park_helper.unpark ();    }    void    sun::misc::Unsafe::park (jboolean isAbsolute, jlong time)    {      using namespace ::java::lang;      Thread *thread = Thread::currentThread();      natThread *nt = (natThread *) thread-&gt;data;      nt-&gt;park_helper.park (isAbsolute, time);    }我们可以看到compareAndSwap就这么几行代码:    }</code></pre><p>好了，现在终于彻底知道了整个链路的原理了。</p>]]>
                </content>
            </entry>
            <entry>
                <title><![CDATA[JVM中的动态分派机制到底是怎么回事？]]></title>
                <link rel="alternate" type="text/html" href="https://www.wangdaye.net/archives/jvm中的动态分派机制到底是怎么回事" />
                <id>tag:https://www.wangdaye.net,2024-01-24:jvm中的动态分派机制到底是怎么回事</id>
                <published>2024-01-24T21:53:38+08:00</published>
                <updated>2024-01-24T22:40:56+08:00</updated>
                <author>
                    <name>王大爷</name>
                    <uri>https://www.wangdaye.net</uri>
                </author>
                <content type="html">
                        <![CDATA[<blockquote><p>如下摘录自《深入理解JAVA虚拟机》</p></blockquote><h1 id="方法调用">方法调用</h1><p>方法调用并不等同于方法执行，方法调用阶段唯一的任务就是确定被调用方法的版本（即调用哪一个方法），暂时还不涉及方法内部的具体运行过程。在程序运行时，进行方法调用是最普遍、最频繁的操作，但前面已经讲过，Class文件的编译过程中不包含传统编译中的连接步骤，一切方法调用在Class文件里面存储的都只是符号引用，而不是方法在实际运行时内存布局中的入口地址（相当于之前说的直接引用）。这个特性给Java带来了更强大的动态扩展能力，但也使得Java方法调用过程变得相对复杂起来，需要在类加载期间，甚至到运行期间才能确定目标方法的直接引用。</p><h2 id="解析">解析</h2><p>继续前面关于方法调用的话题，所有方法调用中的目标方法在Class文件里面都是一个常量池中的符号引用，在类加载的解析阶段，会将其中的一部分符号引用转化为直接引用，这种解析能成立的前提是：方法在程序真正运行之前就有一个可确定的调用版本，并且这个方法的调用版本在运行期是不可改变的。换句话说，调用目标在程序代码写好、编译器进行编译时就必须确定下来。这类方法的调用称为解析（Resolution）。在Java语言中符合“编译期可知，运行期不可变”这个要求的方法，主要包括静态方法和私有方法两大类，前者与类型直接关联，后者在外部不可被访问，这两种方法各自的特点决定了它们都不可能通过继承或别的方式重写其他版本，因此它们都适合在类加载阶段进行解析。</p><p>与之相对应的是，在Java虚拟机里面提供了5条方法调用字节码指令，分别如下:</p><ul><li>invokestatic：调用静态方法。</li><li>invokespecial：调用实例构造器<init>方法、私有方法和父类方法。</li><li>invokevirtual：调用所有的虚方法.</li><li>invokeinterface：调用接口方法，会在运行时再确定一个实现此接口的对象。</li><li>invokedynamic：先在运行时动态解析出调用点限定符所引用的方法，然后再执行该方法，在此之前的4条调用指令，分派逻辑是固化在Java虚拟机内部的，而invokedynamic指令的分派逻辑是由用户所设定的引导方法决定的。</li></ul><p>只要能被invokestatic和invokespecial指令调用的方法，都可以在解析阶段中确定唯一的调用版本，符合这个条件的有静态方法、私有方法、实例构造器、父类方法4类，它们在类加载的时候就会把符号引用解析为该方法的直接引用。这些方法可以称为非虚方法，与之相反，其他方法称为虚方法（除去final方法，后文会提到）。代码清单8-5演示了一个最常见的解析调用的例子，此样例中，静态方法sayHello()只可能属于类型StaticResolution，没有任何手段可以覆盖或隐藏这个方法。</p><pre><code>/＊＊ ＊ 方法静态解析演示 ＊ ＊ @author zzm ＊/public class StaticResolution {    public static void sayHello() {            System.out.println(&quot;hello world&quot;);    }    public static void main(String[] args) {           StaticResolution.sayHello();    }}</code></pre><p>使用javap命令查看这段程序的字节码，会发现的确是通过invokestatic命令来调用sayHello()方法的。</p><pre><code>D:\Develop\&gt;javap -verbose StaticResolutionpublic static void main(java.lang.String[]);  Code:  Stack=0, Locals=1, Args_size=1  0:   invokestatic    #31;            //Method sayHello:()V  3:   return LineNumberTable:  line 15: 0  line 16: 3</code></pre><p>Java中的非虚方法除了使用invokestatic、invokespecial调用的方法之外还有一种，就是被final修饰的方法。虽然final方法是使用invokevirtual指令来调用的，但是由于它无法被覆盖，没有其他版本，所以也无须对方法接收者进行多态选择，又或者说多态选择的结果肯定是唯一的。在Java语言规范中明确说明了final方法是一种非虚方法。</p><p>解析调用一定是个静态的过程，在编译期间就完全确定，在类装载的解析阶段就会把涉及的符号引用全部转变为可确定的直接引用，不会延迟到运行期再去完成。而分派（Dispatch）调用则可能是静态的也可能是动态的，根据分派依据的宗量数[插图]可分为单分派和多分派。这两类分派方式的两两组合就构成了静态单分派、静态多分派、动态单分派、动态多分派4种分派组合情况，下面我们再看看虚拟机中的方法分派是如何进行的。</p><h2 id="分派">分派</h2><p>众所周知，Java是一门面向对象的程序语言，因为Java具备面向对象的3个基本特征：继承、封装和多态。本节讲解的分派调用过程将会揭示多态性特征的一些最基本的体现，如“重载”和“重写”在Java虚拟机之中是如何实现的，这里的实现当然不是语法上该如何写，我们关心的依然是虚拟机如何确定正确的目标方法。</p><h3 id="静态分派">静态分派</h3><p>在开始讲解静态分派前，笔者准备了一段经常出现在面试题中的程序代码，读者不妨先看一遍，想一下程序的输出结果是什么。后面我们的话题将围绕这个类的方法来重载（Overload）代码，以分析虚拟机和编译器确定方法版本的过程。方法静态分派如代码清单8-6所示。</p><p><strong>代码清单8-6 方法静态分派演示</strong></p><pre><code>package org.fenixsoft.polymorphic;/＊＊ ＊ 方法静态分派演示 ＊ @author zzm ＊/public class StaticDispatch {    static abstract class Human {    }    static class Man extends Human {    }    static class Woman extends Human {    }    public void sayHello(Human guy) {            System.out.println(&quot;hello,guy!&quot;);    }    public void sayHello(Man guy) {            System.out.println(&quot;hello,gentleman!&quot;);   }    public void sayHello(Woman guy) {            System.out.println(&quot;hello,lady!&quot;);    }    public static void main(String[] args) {            Human man = new Man();            Human woman = new Woman();            StaticDispatch sr = new StaticDispatch();            sr.sayHello(man);            sr.sayHello(woman);    }}</code></pre><p>运行结果：</p><pre><code>hello,guy!hello,guy!</code></pre><p>代码清单8-6中的代码实际上是在考验阅读者对重载的理解程度，相信对Java编程稍有经验的程序员看完程序后都能得出正确的运行结果，但为什么会选择执行参数类型为Human的重载呢？在解决这个问题之前，我们先按如下代码定义两个重要的概念。</p><pre><code>Human man = new Man();</code></pre><p>我们把上面代码中的“Human”称为变量的静态类型（Static Type），或者叫做的外观类型（Apparent Type），后面的“Man”则称为变量的实际类型（ActualType），静态类型和实际类型在程序中都可以发生一些变化，区别是静态类型的变化仅仅在使用时发生，变量本身的静态类型不会被改变，并且最终的静态类型是在编译期可知的；而实际类型变化的结果在运行期才可确定，编译器在编译程序的时候并不知道一个对象的实际类型是什么。例如下面的代码：</p><pre><code>//实际类型变化Human man = new Man();man = new Woman();//静态类型变化sr.sayHello((Man) man)sr.sayHello((Woman) man)</code></pre><p>解释了这两个概念，再回到代码清单8-6的样例代码中。main()里面的两次sayHello()方法调用，在方法接收者已经确定是对象“sr”的前提下，使用哪个重载版本，就完全取决于传入参数的数量和数据类型。代码中刻意地定义了两个静态类型相同但实际类型不同的变量，但虚拟机（准确地说是编译器）在重载时是通过参数的静态类型而不是实际类型作为判定依据的。并且静态类型是编译期可知的，因此，在编译阶段，Javac编译器会根据参数的静态类型决定使用哪个重载版本，所以选择了sayHello(Human)作为调用目标，并把这个方法的符号引用写到main()方法里的两条invokevirtual指令的参数中。</p><p>所有依赖静态类型来定位方法执行版本的分派动作称为静态分派。静态分派的典型应用是方法重载。静态分派发生在编译阶段，因此确定静态分派的动作实际上不是由虚拟机来执行的。另外，编译器虽然能确定出方法的重载版本，但在很多情况下这个重载版本并不是“唯一的”，往往只能确定一个“更加合适的”版本。这种模糊的结论在由0和1构成的计算机世界中算是比较“稀罕”的事情，产生这种模糊结论的主要原因是字面量不需要定义，所以字面量没有显式的静态类型，它的静态类型只能通过语言上的规则去理解和推断。代码清单8-7演示了何为“更加合适的”版本。</p><p>代码清单8-7 重载方法匹配优先级</p><pre><code>package org.fenixsoft.polymorphic;public class Overload {    public static void sayHello(Object arg) {           System.out.println(&quot;hello Object&quot;);    }    public static void sayHello(int arg) {           System.out.println(&quot;hello int&quot;);    }    public static void sayHello(long arg) {           System.out.println(&quot;hello long&quot;);    }    public static void sayHello(Character arg) {           System.out.println(&quot;hello Character&quot;);    }    public static void sayHello(char arg) {           System.out.println(&quot;hello char&quot;);    }    public static void sayHello(char... arg) {           System.out.println(&quot;hello char ...&quot;);    }    public static void sayHello(Serializable arg) {           System.out.println(&quot;hello Serializable&quot;);     }    public static void main(String[] args) {            sayHello('a');     }}</code></pre><p>上面的代码运行后会输出：</p><pre><code>hello char</code></pre><p>这很好理解，'a'是一个char类型的数据，自然会寻找参数类型为char的重载方法，如果注释掉sayHello(char arg)方法，那输出会变为：</p><pre><code>hello int</code></pre><p>这时发生了一次自动类型转换，'a'除了可以代表一个字符串，还可以代表数字97（字符'a'的Unicode数值为十进制数字97），因此参数类型为int的重载也是合适的。我们继续注释掉sayHello(int arg)方法，那输出会变为：</p><pre><code>hello long</code></pre><p>这时发生了两次自动类型转换，'a'转型为整数97之后，进一步转型为长整数97L，匹配了参数类型为long的重载。笔者在代码中没有写其他的类型如float、double等的重载，不过实际上自动转型还能继续发生多次，按照char-&gt;int-&gt;long-&gt;float-&gt;double的顺序转型进行匹配。但不会匹配到byte和short类型的重载，因为char到byte或short的转型是不安全的。我们继续注释掉sayHello(long arg)方法，那输出会变为：</p><pre><code>hello Character</code></pre><p>这时发生了一次自动装箱，'a'被包装为它的封装类型java.lang.Character，所以匹配到了参数类型为Character的重载，继续注释掉sayHello(Character arg)方法，那输出会变为：</p><pre><code>hello Serializable</code></pre><p>这个输出可能会让人感觉摸不着头脑，一个字符或数字与序列化有什么关系？出现hello Serializable，是因为java.lang.Serializable是java.lang.Character类实现的一个接口，当自动装箱之后发现还是找不到装箱类，但是找到了装箱类实现了的接口类型，所以紧接着又发生一次自动转型。char可以转型成int，但是Character是绝对不会转型为Integer的，它只能安全地转型为它实现的接口或父类。Character还实现了另外一个接口java.lang.Comparable<Character>，如果同时出现两个参数分别为Serializable和Comparable<Character>的重载方法，那它们在此时的优先级是一样的。编译器无法确定要自动转型为哪种类型，会提示类型模糊，拒绝编译。程序必须在调用时显式地指定字面量的静态类型，如：sayHello((Comparable<Character>)'a')，才能编译通过。下面继续注释掉sayHello(Serializable arg)方法，输出会变为：</p><pre><code>hello Object</code></pre><p>这时是char装箱后转型为父类了，如果有多个父类，那将在继承关系中从下往上开始搜索，越接近上层的优先级越低。即使方法调用传入的参数值为null时，这个规则仍然适用。我们把sayHello(Object arg)也注释掉，输出将会变为：</p><pre><code>hello char ...</code></pre><p>7个重载方法已经被注释得只剩一个了，可见变长参数的重载优先级是最低的，这时候字符'a'被当做了一个数组元素。笔者使用的是char类型的变长参数，读者在验证时还可以选择int类型、Character类型、Object类型等的变长参数重载来把上面的过程重新演示一遍。但要注意的是，有一些在单个参数中能成立的自动转型，如char转型为int，在变长参数中是不成立的</p><p>代码清单8-7演示了编译期间选择静态分派目标的过程，这个过程也是Java语言实现方法重载的本质。演示所用的这段程序属于很极端的例子，除了用做面试题为难求职者以外，在实际工作中几乎不可能有实际用途。笔者拿来做演示仅仅是用于讲解重载时目标方法选择的过程，大部分情况下进行这样极端的重载都可算是真正的“关于茴香豆的茴有几种写法的研究”。无论对重载的认识有多么深刻，一个合格的程序员都不应该在实际应用中写出如此极端的重载代码。</p><p>另外还有一点读者可能比较容易混淆：笔者讲述的解析与分派这两者之间的关系并不是二选一的排他关系，它们是在不同层次上去筛选、确定目标方法的过程。例如，前面说过，静态方法会在类加载期就进行解析，而静态方法显然也是可以拥有重载版本的，选择重载版本的过程也是通过静态分派完成的。</p><h3 id="动态分派">动态分派</h3><p>了解了静态分派，我们接下来看一下动态分派的过程，它和多态性的另外一个重要体现——重写（Override）有着很密切的关联。我们还是用前面的Man和Woman一起sayHello的例子来讲解动态分派，请看代码清单8-8中所示的代码。</p><p><strong>代码清单8-8 方法动态分派演示</strong></p><pre><code>package org.fenixsoft.polymorphic;/＊＊ ＊ 方法动态分派演示 ＊ @author zzm ＊/public class DynamicDispatch {    static abstract class Human {           protected abstract void sayHello();    }    static class Man extends Human {           @Override           protected void sayHello() {                  System.out.println(&quot;man say hello&quot;);          }    }    static class Woman extends Human {           @Override           protected void sayHello() {                  System.out.println(&quot;woman say hello&quot;);           }    }    public static void main(String[] args) {           Human man = new Man();           Human woman = new Woman();           man.sayHello();           woman.sayHello();           man = new Woman();           man.sayHello();    }}</code></pre><p>运行结果：</p><pre><code>man say hellowoman say hellowoman say hello</code></pre><p>这个运行结果相信不会出乎任何人的意料，对于习惯了面向对象思维的Java程序员会觉得这是完全理所当然的。现在的问题还是和前面的一样，虚拟机是如何知道要调用哪个方法的？</p><p>显然这里不可能再根据静态类型来决定，因为静态类型同样都是Human的两个变量man和woman在调用sayHello()方法时执行了不同的行为，并且变量man在两次调用中执行了不同的方法。导致这个现象的原因很明显，是这两个变量的实际类型不同，Java虚拟机是如何根据实际类型来分派方法执行版本的呢？我们使用javap命令输出这段代码的字节码，尝试从中寻找答案，输出结果如代码清单8-9所示。</p><p><strong>代码清单8-9 main()方法的字节码</strong></p><pre><code>public static void main(java.lang.String[]);  Code:   Stack=2, Locals=3, Args_size=1   0:   new     #16;             // class org/fenixsoft/polymorphic/Dynamic-Dispatch$Man   3:   dup   4:   invokespecial   #18;    // Method org/fenixsoft/polymorphic/Dynamic-Dispatch$Man.&quot;&lt;init&gt;&quot;:()V   7:   astore_1   8:   new     #19;             // class org/fenixsoft/polymorphic/Dynamic-Dispatch$Woman   11: dup   12: invokespecial   #21;    //Method org/fenixsoft/polymorphic/DynamicDispatch$Woman.&quot;&lt;init&gt;&quot;:()V   15: astore_2   16: aload_1   17: invokevirtual   #22;    // Method org/fenixsoft/polymorphic/Dynamic-Dispatch$Human.sayHello:()V   20: aload_2   21: invokevirtual   #22;    // Method org/fenixsoft/polymorphic/Dynamic-Dispatch$Human.sayHello:()V   24: new     #19;             // class org/fenixsoft/polymorphic/Dynamic-Dispatch$Woman   27: dup   28: invokespecial   #21;            //Method org/fenixsoft/polymorphic/DynamicDispatch$Woman.&quot;&lt;init&gt;&quot;:()V   31: astore_1   32: aload_1   33: invokevirtual   #22;            // Method org/fenixsoft/polymorphic/DynamicDispatch$Human.sayHello:()V   36: return</code></pre><p>0～15行的字节码是准备动作，作用是建立man和woman的内存空间、调用Man和Woman类型的实例构造器，将这两个实例的引用存放在第1、2个局部变量表Slot之中，这个动作也就对应了代码中的这两句：</p><pre><code>Human man = new Man();Human woman = new Woman();</code></pre><p>接下来的16～21句是关键部分，16、20两句分别把刚刚创建的两个对象的引用压到栈顶，这两个对象是将要执行的sayHello()方法的所有者，称为接收者（Receiver）；17和21句是方法调用指令，这两条调用指令单从字节码角度来看，无论是指令（都是invokevirtual）还是参数（都是常量池中第22项的常量，注释显示了这个常量是Human.sayHello()的符号引用）完全一样的，但是这两句指令最终执行的目标方法并不相同。原因就需要从invokevirtual指令的多态查找过程开始说起，invokevirtual指令的运行时解析过程大致分为以下几个步骤：</p><ul><li>找到操作数栈顶的第一个元素所指向的对象的实际类型，记作C。</li><li>如果在类型C中找到与常量中的描述符和简单名称都相符的方法，则进行访问权限校验，如果通过则返回这个方法的直接引用，查找过程结束；如果不通过，则返回java.lang. IllegalAccessError异常。</li><li>否则，按照继承关系从下往上依次对C的各个父类进行第2步的搜索和验证过程。</li><li>如果始终没有找到合适的方法，则抛出java.lang.AbstractMethodError异常。</li></ul><p>由于invokevirtual指令执行的第一步就是在运行期确定接收者的实际类型，所以两次调用中的invokevirtual指令把常量池中的类方法符号引用解析到了不同的直接引用上，这个过程就是Java语言中方法重写的本质。我们把这种在运行期根据实际类型确定方法执行版本的分派过程称为动态分派。</p><h3 id="虚拟机动态分派的实现">虚拟机动态分派的实现</h3><p>前面介绍的分派过程，作为对虚拟机概念模型的解析基本上已经足够了，它已经解决了虚拟机在分派中“会做什么”这个问题。但是虚拟机“具体是如何做到的”，可能各种虚拟机的实现都会有些差别。</p><p>由于动态分派是非常频繁的动作，而且动态分派的方法版本选择过程需要运行时在类的方法元数据中搜索合适的目标方法，因此在虚拟机的实际实现中基于性能的考虑，大部分实现都不会真正地进行如此频繁的搜索。面对这种情况，最常用的“稳定优化”手段就是为类在方法区中建立一个虚方法表（Vritual Method Table，也称为vtable，与此对应的，在invokeinterface执行时也会用到接口方法表——Inteface Method Table，简称itable），使用虚方法表索引来代替元数据查找以提高性能。我们先看看代码清单8-10所对应的虚方法表结构示例，如图8-3所示。</p><p><img src="https://www.wangdaye.net/upload/2024/01/image-00ac032c2cb04f51bc3076e66d1aa956.png" alt="图8-3 方法表结构" /></p><p>虚方法表中存放着各个方法的实际入口地址。如果某个方法在子类中没有被重写，那子类的虚方法表里面的地址入口和父类相同方法的地址入口是一致的，都指向父类的实现入口。如果子类中重写了这个方法，子类方法表中的地址将会替换为指向子类实现版本的入口地址。图8-3中，Son重写了来自Father的全部方法，因此Son的方法表没有指向Father类型数据的箭头。但是Son和Father都没有重写来自Object的方法，所以它们的方法表中所有从Object继承来的方法都指向了Object的数据类型。</p><p>为了程序实现上的方便，具有相同签名的方法，在父类、子类的虚方法表中都应当具有一样的索引序号，这样当类型变换时，仅需要变更查找的方法表，就可以从不同的虚方法表中按索引转换出所需的入口地址。</p><p>方法表一般在类加载的连接阶段进行初始化，准备了类的变量初始值后，虚拟机会把该类的方法表也初始化完毕。</p><p>上文中笔者说方法表是分派调用的“稳定优化”手段，虚拟机除了使用方法表之外，在条件允许的情况下，还会使用内联缓存（Inline Cache）和基于“类型继承关系分析”（Class Hierarchy Analysis，CHA）技术的守护内联（Guarded Inlining）两种非稳定的“激进优化”手段来获得更高的性能，关于这两种优化技术的原理和运作过程，读者可以参考本书第11章中的相关内容。</p><h2 id="动态类型语言支持">动态类型语言支持</h2><p>Java虚拟机的字节码指令集的数量从Sun公司的第一款Java虚拟机问世至JDK 7来临之前的十余年时间里，一直没有发生任何变化。随着JDK 7的发布，字节码指令集终于迎来了第一位新成员——invokedynamic指令。这条新增加的指令是JDK 7实现“动态类型语言”（Dynamically Typed Language）支持而进行的改进之一，也是为JDK 8可以顺利实现Lambda表达式做技术准备。在本节中，我们将详细讲解JDK 7这项新特性出现的前因后果和它的深远意义。</p><h3 id="动态类型语言">动态类型语言</h3><p>在介绍Java虚拟机的动态类型语言支持之前，我们要先弄明白动态类型语言是什么？它与Java语言、Java虚拟机有什么关系？了解JDK 1.7提供动态类型语言支持的技术背景，对理解这个语言特性是很有必要的。</p><p>什么是动态类型语言？动态类型语言的关键特征是它的类型检查的主体过程是在运行期而不是编译期，满足这个特征的语言有很多，常用的包括：APL、Clojure、Erlang、Groovy、JavaScript、Jython、Lisp、Lua、PHP、Prolog、Python、Ruby、Smalltalk和Tcl等。相对的，在编译期就进行类型检查过程的语言（如C++和Java等）就是最常用的静态类型语言。</p><p>觉得上面定义过于概念化？那我们不妨通过两个例子以最浅显的方式来说明什么是“在编译期/运行期进行”和什么是“类型检查”。首先看下面这段简单的Java代码，它是否能正常编译和运行？</p><pre><code>public static void main(String[] args) {    int[][][] array = new int[1][0][-1];}</code></pre><p>这段代码能够正常编译，但运行的时候会报NegativeArraySizeException异常。在Java虚拟机规范中明确规定了NegativeArraySizeException是一个运行时异常，通俗一点来说，运行时异常就是只要代码不运行到这一行就不会有问题。与运行时异常相对应的是连接时异常，例如很常见的NoClassDefFoundError便属于连接时异常，即使会导致连接时异常的代码放在一条无法执行到的分支路径上，类加载时（Java的连接过程不在编译阶段，而在类加载阶段）也照样会抛出异常。</p><p>不过，在C语言中，含义相同的代码会在编译期报错：</p><pre><code>int main(void) {   int i[1][0][-1];              //GCC拒绝编译，报“size of array is negative”   return 0;}</code></pre><p>由此看来，一门语言的哪一种检查行为要在运行期进行，哪一种检查要在编译期进行并没有必然的因果逻辑关系，关键是语言规范中人为规定的。再举一个例子来解释“类型检查”，例如下面这一句非常简单的代码：</p><pre><code>obj.println(&quot;hello world&quot;);</code></pre><p>虽然每个人都能看懂这行代码要做什么，但对于计算机来说，这一行代码“没头没尾”是无法执行的，它需要一个具体的上下文才有讨论的意义。</p><p>现在假设这行代码是在Java语言中，并且变量obj的静态类型为java.io.PrintStream，那变量obj的实际类型就必须是PrintStream的子类（实现了PrintStream接口的类）才是合法的。否则，哪怕obj属于一个确实有用println(String)方法，但与PrintStream接口没有继承关系，代码依然不可能运行——因为类型检查不合法。</p><p>但是相同的代码在ECMAScript（JavaScript）中情况则不一样，无论obj具体是何种类型，只要这种类型的定义中确实包含有println(String)方法，那方法调用便可成功。</p><p>这种差别产生的原因是Java语言在编译期间已将println(String)方法完整的符号引用（本例中为一个CONSTANT_InterfaceMethodref_info常量）生成出来，作为方法调用指令的参数存储到Class文件中，例如下面这段代码：</p><pre><code>invokevirtual #4; //Method java/io/PrintStream.println:(Ljava/lang/String;)V</code></pre><p>这个符号引用包含了此方法定义在哪个具体类型之中、方法的名字以及参数顺序、参数类型和方法返回值等信息，通过这个符号引用，虚拟机可以翻译出这个方法的直接引用。而在ECMAScript等动态类型语言中，变量obj本身是没有类型的，变量obj的值才具有类型，编译时最多只能确定方法名称、参数、返回值这些信息，而不会去确定方法所在的具体类型（即方法接收者不固定）。“变量无类型而变量值才有类型”这个特点也是动态类型语言的一个重要特征。</p><p>了解了动态和静态类型语言的区别后，也许读者的下一个问题就是动态、静态类型语言两者谁更好，或者谁更加先进？这种比较不会有确切答案，因为它们都有自己的优点，选择哪种语言是需要经过权衡的。静态类型语言在编译期确定类型，最显著的好处是编译器可以提供严谨的类型检查，这样与类型相关的问题能在编码的时候就及时发现，利于稳定性及代码达到更大规模。而动态类型语言在运行期确定类型，这可以为开发人员提供更大的灵活性，某些在静态类型语言中需用大量“臃肿”代码来实现的功能，由动态类型语言来实现可能会更加清晰和简洁，清晰和简洁通常也就意味着开发效率的提升。</p><h3 id="jdk-17与动态类型">JDK 1.7与动态类型</h3><p>回到本节的主题，来看看Java语言、虚拟机与动态类型语言之间有什么关系。Java虚拟机毫无疑问是Java语言的运行平台，但它的使命并不仅限于此，早在1997年出版的《Java虚拟机规范》中就规划了这样一个愿景：“在未来，我们会对Java虚拟机进行适当的扩展，以便更好地支持其他语言运行于Java虚拟机之上”。而目前确实已经有许多动态类型语言运行于Java虚拟机之上了，如Clojure、Groovy、Jython和JRuby等，能够在同一个虚拟机上可以达到静态类型语言的严谨性与动态类型语言的灵活性，这是一件很美妙的事情。</p><p>但遗憾的是，Java虚拟机层面对动态类型语言的支持一直都有所欠缺，主要表现在方法调用方面：JDK 1.7以前的字节码指令集中，4条方法调用指令（invokevirtual、invokespecial、invokestatic、invokeinterface）的第一个参数都是被调用的方法的符号引用（CONSTANT_Methodref_info或者CONSTANT_InterfaceMethodref_info常量），前面已经提到过，方法的符号引用在编译时产生，而动态类型语言只有在运行期才能确定接收者类型。这样，在Java虚拟机上实现的动态类型语言就不得不使用其他方式（如编译时留个占位符类型，运行时动态生成字节码实现具体类型到占位符类型的适配）来实现，这样势必让动态类型语言实现的复杂度增加，也可能带来额外的性能或者内存开销。尽管可以利用一些办法（如Call Site Caching）让这些开销尽量变小，但这种底层问题终归是应当在虚拟机层次上去解决才最合适，因此在Java虚拟机层面上提供动态类型的直接支持就成为了Java平台的发展趋势之一，这就是JDK 1.7（JSR-292）中invokedynamic指令以及java.lang.invoke包出现的技术背景。</p><h3 id="javalanginvoke包">java.lang.invoke包</h3><p>JDK 1.7实现了JSR-292，新加入的java.lang.invoke包[插图]就是JSR-292的一个重要组成部分，这个包的主要目的是在之前单纯依靠符号引用来确定调用的目标方法这种方式以外，提供一种新的动态确定目标方法的机制，称为MethodHandle。这种表达方式也许不太好懂？那不妨把MethodHandle与C/C<ins>中的FunctionPointer，或者C#里面的Delegate类比一下。举个例子，如果我们要实现一个带谓词的排序函数，在C/C</ins>中常用的做法是把谓词定义为函数，用函数指针把谓词传递到排序方法，如下：</p><pre><code>void sort(int list[], const int size, int (＊compare)(int, int))</code></pre><p>但Java语言做不到这一点，即没有办法单独地把一个函数作为参数进行传递。普遍的做法是设计一个带有compare()方法的Comparator接口，以实现了这个接口的对象作为参数，例如Collections.sort()就是这样定义的：</p><pre><code>void sort(List list, Comparator c)</code></pre><p>不过，在拥有Method Handle之后，Java语言也可以拥有类似于函数指针或者委托的方法别名的工具了。代码清单8-11演示了MethodHandle的基本用途，无论obj是何种类型（临时定义的ClassA抑或是实现PrintStream接口的实现类System.out），都可以正确地调用到println()方法。</p><p><strong>代码清单8-11 MethodHandle演示</strong></p><pre><code>    import static java.lang.invoke.MethodHandles.lookup;    import java.lang.invoke.MethodHandle;    import java.lang.invoke.MethodType;    /＊＊    ＊ JSR-292 Method Handle基础用法演示    ＊ @author zzm    ＊/   public class MethodHandleTest {      static class ClassA {            public void println(String s) {                System.out.println(s);            }      }      public static void main(String[] args) throws Throwable {              Object obj = System.currentTimeMillis() % 2 == 0 ? System.out : newClassA();            /＊无论obj最终是哪个实现类，下面这句都能正确调用到println方法            getPrintlnMH(obj).invokeExact(&quot;icyfenix&quot;);      }      private static MethodHandle getPrintlnMH(Object reveiver) throws Throwable {            /＊MethodType：代表“方法类型”，包含了方法的返回值（methodType()的第一个参数）和具体参数（methodType()第二个及以后的参数）＊/            MethodType mt = MethodType.methodType(void.class, String.class);            /＊lookup()方法来自于MethodHandles.lookup，这句的作用是在指定类中查找符合给定的方法名称、方法类型，并且符合调用权限的方法句柄＊/            /＊因为这里调用的是一个虚方法，按照Java语言的规则，方法第一个参数是隐式的，代表该方法的接收者，也即是this指向的对象，这个参数以前是放在参数列表中进行传递的，而现在提供了bindTo()方法来完成这件事情＊/                return lookup().findVirtual(reveiver.getClass(), &quot;println&quot;, mt).bindTo(reveiver);      }    }</code></pre><p>实际上，方法getPrintlnMH()中模拟了invokevirtual指令的执行过程，只不过它的分派逻辑并非固化在Class文件的字节码上，而是通过一个具体方法来实现。而这个方法本身的返回值（MethodHandle对象），可以视为对最终调用方法的一个“引用”。以此为基础，有了MethodHandle就可以写出类似于下面这样的函数声明：</p><pre><code>void sort(List list, MethodHandle compare)</code></pre><p>从上面的例子可以看出，使用MethodHandle并没有什么困难，不过看完它的用法之后，读者大概就会产生疑问，相同的事情，用反射不是早就可以实现了吗？</p><p>确实，仅站在Java语言的角度来看，MethodHandle的使用方法和效果与Reflection有众多相似之处，不过，它们还是有以下这些区别：</p><ul><li>从本质上讲，Reflection和MethodHandle机制都是在模拟方法调用，但Reflection是在模拟Java代码层次的方法调用，而MethodHandle是在模拟字节码层次的方法调用。在MethodHandles.lookup中的3个方法——findStatic()、findVirtual()、findSpecial()正是为了对应于invokestatic、invokevirtual &amp; invokeinterface和invokespecial这几条字节码指令的执行权限校验行为，而这些底层细节在使用Reflection API时是不需要关心的。</li><li>Reflection中的java.lang.reflect.Method对象远比MethodHandle机制中的java.lang. invoke.MethodHandle对象所包含的信息多。前者是方法在Java一端的全面映像，包含了方法的签名、描述符以及方法属性表中各种属性的Java端表示方式，还包含执行权限等的运行期信息。而后者仅仅包含与执行该方法相关的信息。用通俗的话来讲，Reflection是重量级，而MethodHandle是轻量级。</li><li>由于MethodHandle是对字节码的方法指令调用的模拟，所以理论上虚拟机在这方面做的各种优化（如方法内联），在MethodHandle上也应当可以采用类似思路去支持（但目前实现还不完善）。而通过反射去调用方法则不行。</li></ul><p>MethodHandle与Reflection除了上面列举的区别外，最关键的一点还在于去掉前面讨论施加的前提“仅站在Java语言的角度来看”：Reflection API的设计目标是只为Java语言服务的，而MethodHandle则设计成可服务于所有Java虚拟机之上的语言，其中也包括Java语言。</p><h3 id="invokedynamic指令">invokedynamic指令</h3><p>本节一开始就提到了JDK 1.7为了更好地支持动态类型语言，引入了第5条方法调用的字节码指令invokedynamic，之后一直没有再提到它，甚至把代码清单8-11中使用MethodHandle的示例代码反编译后也不会看见invokedynamic的身影，它的应用之处在哪里呢？</p><p>在某种程度上，invokedynamic指令与MethodHandle机制的作用是一样的，都是为了解决原有4条“invoke*”指令方法分派规则固化在虚拟机之中的问题，把如何查找目标方法的决定权从虚拟机转嫁到具体用户代码之中，让用户（包含其他语言的设计者）有更高的自由度。而且，它们两者的思路也是可类比的，可以把它们想象成为了达成同一个目的，一个采用上层Java代码和API来实现，另一个用字节码和Class中其他属性、常量来完成。因此，如果理解了前面的MethodHandle例子，那么理解invokedynamic指令也并不困难。</p><p>每一处含有invokedynamic指令的位置都称做“动态调用点”（Dynamic CallSite），这条指令的第一个参数不再是代表方法符号引用的CONSTANT_Methodref_info常量，而是变为JDK 1.7新加入的CONSTANT_InvokeDynamic_info常量，从这个新常量中可以得到3项信息：引导方法（Bootstrap Method，此方法存放在新增的BootstrapMethods属性中）、方法类型（MethodType）和名称。引导方法是有固定的参数，并且返回值是java.lang.invoke. CallSite对象，这个代表真正要执行的目标方法调用。根据CONSTANT_InvokeDynamic_info常量中提供的信息，虚拟机可以找到并且执行引导方法，从而获得一个CallSite对象，最终调用要执行的目标方法。我们还是举一个实际的例子来解释这个过程，如代码清单8-12所示。</p><p><strong>代码清单8-12 invokedynamic指令演示</strong></p><pre><code>import static java.lang.invoke.MethodHandles.lookup;import java.lang.invoke.CallSite;import java.lang.invoke.ConstantCallSite;import java.lang.invoke.MethodHandle;import java.lang.invoke.MethodHandles;import java.lang.invoke.MethodType;public class InvokeDynamicTest {    public static void main(String[] args) throws Throwable {        INDY_BootstrapMethod().invokeExact(&quot;icyfenix&quot;);    }    public static void testMethod(String s) {        System.out.println(&quot;hello String:&quot; + s);    }    public static CallSite BootstrapMethod(MethodHandles.Lookup lookup, String name, MethodType mt) throws Throwable {          return new ConstantCallSite(lookup.findStatic(InvokeDynamicTest.class, name, mt));    }    private static MethodType MT_BootstrapMethod() {        return MethodType.fromMethodDescriptorString(                              &quot;(Ljava/lang/invoke/MethodHandles$Lookup;Ljava/lang/String;Ljava/lang/invoke/MethodType;)Ljava/lang/invoke/CallSite;&quot;, null);    }    private static MethodHandle MH_BootstrapMethod() throws Throwable {          return lookup().findStatic(InvokeDynamicTest.class, &quot;BootstrapMethod&quot;, MT_BootstrapMethod());    }    private static MethodHandle INDY_BootstrapMethod() throws Throwable {        CallSite cs = (CallSite) MH_BootstrapMethod().invokeWithArguments(lookup(), &quot;testMethod&quot;, MethodType.fromMethodDescriptorString(&quot;(Ljava/lang/String;)V&quot;, null));        return cs.dynamicInvoker();    }}</code></pre><p>这段代码与前面MethodHandleTest的作用基本上是一样的，虽然笔者没有加以注释，但是阅读起来应当不困难。本书前面提到过，由于invokedynamic指令所面向的使用者并非Java语言，而是其他Java虚拟机之上的动态语言，因此仅依靠Java语言的编译器Javac没有办法生成带有invokedynamic 指令的字节码（曾经有一个java.dyn.InvokeDynamic的语法糖可以实现，但后来被取消了），所以要使用Java语言来演示invokedynamic指令只能用一些变通的办法。John Rose（DaVinci Machine Project的Leader）编写了一个把程序的字节码转换为使用invokedynamic的简单工具INDY[插图]来完成这件事情，我们要使用这个工具来产生最终要的字节码，因此这个示例代码中的方法名称不能随意改动，更不能把几个方法合并到一起写，因为它们是要被INDY工具读取的。</p><p>把上面代码编译、再使用INDY转换后重新生成的字节码如代码清单8-13所示（结果使用javap输出，因版面原因，精简了许多无关的内容）。</p><p><strong>代码清单8-13 invokedynamic指令演示（2）</strong></p><pre><code>Constant pool:  #121 = NameAndType        #33:#30     //testMethod:(Ljava/lang/String;)V  #123 = InvokeDynamic      #0:#121     //#0:testMethod:(Ljava/lang/String;)Vpublic static void main(java.lang.String[]) throws java.lang.Throwable;    Code:      stack=2, locals=1, args_size=1          0: ldc           #23          //String abc          2: invokedynamic #123, 0      // InvokeDynamic #0:testMethod:(Ljava/lang/String;)V          7: nop          8: returnpublic static java.lang.invoke.CallSite BootstrapMethod(java.lang.invoke.MethodHandles$Lookup, java.lang.String, java.lang.invoke.MethodType) throws java.lang.Throwable;    Code:      stack=6, locals=3, args_size=3          0: new           #63   //class java/lang/invoke/ConstantCallSite          3: dup          4: aload_0          5: ldc           #1    //class org/fenixsoft/InvokeDynamicTest          7: aload_1          8: aload_2          9: invokevirtual #65   //Method java/lang/invoke/MethodHandles$Lookup.findStatic:(Ljava/lang/Class;Ljava/lang/String;Ljava/lang/invoke/MethodType;)Ljava/lang/invoke/MethodHandle;         12: invokespecial #71    //Method java/lang/invoke/ConstantCallSite.&quot;&lt;in it&gt;&quot;:(Ljava/lang/invoke/MethodHandle;)V         15: areturn</code></pre><p>从main()方法的字节码可见，原本的方法调用指令已经替换为invokedynamic，它的参数为第123项常量（第二个值为0的参数在HotSpot中用不到，与invokeinterface指令那个值为0的参数一样都是占位的）。</p><pre><code>2: invokedynamic #123, 0        // InvokeDynamic #0:testMethod:(Ljava/lang/String;)V</code></pre><p>从常量池中可见，第123项常量显示“#123=InvokeDynamic #0:#121”说明它是一项CONSTANT_InvokeDynamic_info类型常量，常量值中前面的“#0”代表引导方法取BootstrapMethods属性表的第0项（javap没有列出属性表的具体内容，不过示例中仅有一个引导方法，即BootstrapMethod()），而后面的“#121”代表引用第121项类型为CONSTANT_NameAndType_info的常量，从这个常量中可以获取方法名称和描述符，即后面输出的“testMethod:(Ljava/lang/String;)V”。</p><p>再看一下BootstrapMethod()，这个方法Java源码中没有，是INDY产生的，但是它的字节码很容易读懂，所有逻辑就是调用MethodHandles$Lookup的findStatic()方法，产生testMethod()方法的MethodHandle，然后用它创建一个ConstantCallSite对象。最后，这个对象返回给invokedynamic指令实现对testMethod()方法的调用，invokedynamic指令的调用过程到此就宣告完成了。</p><h3 id="掌控方法分派规则">掌控方法分派规则</h3><p>nvokedynamic指令与前面4条“invoke*”指令的最大差别就是它的分派逻辑不是由虚拟机决定的，而是由程序员决定。在介绍Java虚拟机动态语言支持的最后一个小结中，笔者通过一个简单例子（如代码清单8-14所示），帮助读者理解程序员在可以掌控方法分派规则之后，能做什么以前无法做到的事情。</p><p>代码清单8-14 方法调用问题</p><pre><code>class GrandFather {    void thinking() {        System.out.println(&quot;i am grandfather&quot;);    }}class Father extends GrandFather {    void thinking() {        System.out.println(&quot;i am father&quot;);    }}class Son extends Father {    void thinking() {        //请读者在这里填入适当的代码（不能修改其他地方的代码）        //实现调用祖父类的thinking()方法，打印&quot;i am grandfather&quot;  }}</code></pre><p>在Java程序中，可以通过“super”关键字很方便地调用到父类中的方法，但如果要访问祖类的方法呢？读者在阅读本书下面提供的解决方案之前，不妨自己思考一下，在JDK 1.7之前有没有办法解决这个问题。</p><p>在JDK 1.7之前，使用纯粹的Java语言很难处理这个问题（直接生成字节码就很简单，如使用ASM等字节码工具），原因是在Son类的thinking()方法中无法获取一个实际类型是GrandFather的对象引用，而invokevirtual指令的分派逻辑就是按照方法接收者的实际类型进行分派，这个逻辑是固化在虚拟机中的，程序员无法改变。在JDK 1.7中，可以使用代码清单8-15中的程序来解决这个问题。</p><p>代码清单8-15 使用MethodHandle来解决相关问题</p><pre><code>import static java.lang.invoke.MethodHandles.lookup;import java.lang.invoke.MethodHandle;import java.lang.invoke.MethodType;class Test {class GrandFather {    void thinking() {        System.out.println(&quot;i am grandfather&quot;);    }}class Father extends GrandFather {    void thinking() {        System.out.println(&quot;i am father&quot;);    }}class Son extends Father {      void thinking() {            try {                  MethodType mt = MethodType.methodType(void.class);                  MethodHandle mh = lookup().findSpecial(GrandFather.class, &quot;thinking&quot;, mt, getClass());                  mh.invoke(this);              } catch (Throwable e) {              }        }    }    public static void main(String[] args) {        (new Test().new Son()).thinking();    }}</code></pre><p>运行结果：</p><pre><code>i am grandfather</code></pre>]]>
                </content>
            </entry>
            <entry>
                <title><![CDATA[JAVA-Benchmark-工具JMH]]></title>
                <link rel="alternate" type="text/html" href="https://www.wangdaye.net/archives/java-benchmark-工具jmh" />
                <id>tag:https://www.wangdaye.net,2024-01-22:java-benchmark-工具jmh</id>
                <published>2024-01-22T23:14:34+08:00</published>
                <updated>2024-01-22T23:20:08+08:00</updated>
                <author>
                    <name>王大爷</name>
                    <uri>https://www.wangdaye.net</uri>
                </author>
                <content type="html">
                        <![CDATA[<h2 id="为什么需要benchmark工具">为什么需要Benchmark工具</h2><p>如果想要知道一段代码的性能如何，一种常用的做法可能是这样的：</p><pre><code>long start = System.currentTimeMillis();// do something ...System.out.println(System.currentTimeMillis() - start);</code></pre><p>这样做，存在几个问题：</p><ul><li>结果不够精确<br />首先，System.currentTimeMillis() 的注释里明确表示了，根据操作系统的不同，会存在数十毫秒的误差。虽然这个问题比较容易解决，但是造成测试结果不精确的主要原因并不是时间函数的误差，而是JVM和JIT在运行时会对Java应用进行大量的优化，比如某个计算的结果并没有被使用，那么这段代码在执行时就会被忽略，这样的问题比较难察觉。</li><li>统计结果有限<br />如果需要打印多种类型的测试数据，就需要增加很多额外的代码。</li><li>配置不灵活<br />不容易修改测试的类型和条件。</li></ul><p>因此，使用一款靠谱的benchmark工具，既可以减少工作量，又可以确保性能优化过程不被错误的测试数据误导。<br />之前使用Golang开发的时候，SDK自带的benchmark就非常好用。转到Java栈之后，我也想找一款好用的benchmark工具，后来通过《Effective Java》了解到了JMH。</p><h2 id="jmh概述">JMH概述</h2><p>JMH 即Java Microbenchmark Harness，是用于代码微基准测试的工具套件，由JIT的开发人员编写，他们应该比任何人都了解JIT对于测试的影响。<br />JMH可以精确测量方法在不同输入参数情况下的执行时间和吞吐量。</p><h2 id="一个例子">一个例子</h2><p>这里使用Gradle来搭建测试环境，首先在build.gradle中添加依赖：</p><pre><code>    testCompile 'org.openjdk.jmh:jmh-core:jar:1.21'    testCompile 'org.openjdk.jmh:jmh-generator-annprocess:1.21'</code></pre><p>编写一个简单的类，测试两种字符串连接操作的性能：</p><pre><code>package com.dafengge0913.benchmark;import org.openjdk.jmh.annotations.*;import org.openjdk.jmh.infra.Blackhole;import org.openjdk.jmh.runner.Runner;import org.openjdk.jmh.runner.RunnerException;import org.openjdk.jmh.runner.options.Options;import org.openjdk.jmh.runner.options.OptionsBuilder;import java.util.concurrent.TimeUnit;@BenchmarkMode({Mode.AverageTime, Mode.Throughput})@Warmup(iterations = 1)@Measurement(iterations = 2, time = 1)@OutputTimeUnit(TimeUnit.MICROSECONDS)@Fork(value = 2)@Threads(8)@State(Scope.Benchmark)@OperationsPerInvocationpublic class StringBenchmark {    @Param({&quot;1&quot;, &quot;10&quot;, &quot;100&quot;})    private int n;    @Setup    public void setup() {    }    @TearDown    public void tearDown() {    }    @Benchmark    public void testStringAdd(Blackhole blackhole) {        String s = &quot;&quot;;        for (int i = 0; i &lt; n; i++) {            s += i;        }        blackhole.consume(s);    }    @Benchmark    public void testStringBuilderAdd(Blackhole blackhole) {        StringBuilder sb = new StringBuilder();        for (int i = 0; i &lt; n; i++) {            sb.append(i);        }        blackhole.consume(sb.toString());    }    public static void main(String[] args) throws RunnerException {        Options opt = new OptionsBuilder()                .include(StringBenchmark.class.getSimpleName())                .build();        new Runner(opt).run();    }}</code></pre><p>测试结果，这里截取了<strong>部分</strong>输出：</p><p><strong>testStringBuilderAdd 测试的某组执行结果:</strong></p><pre><code>Result &quot;com.dafengge0913.benchmark.StringBenchmark.testStringBuilderAdd&quot;:  1.317 ±(99.9%) 0.306 us/op [Average]  (min, avg, max) = (1.259, 1.317, 1.374), stdev = 0.047  CI (99.9%): [1.011, 1.622] (assumes normal distribution)</code></pre><p>每次操作平均耗时1.317 ± 0.306微秒<br />最小值：1.259<br />平均值：1.317<br />最大值：1.374<br />标准差：0.047<br />平均值的信赖区间：[1.011, 1.622]</p><p><strong>最后的统计结果：</strong></p><pre><code>Benchmark                             (n)   Mode  Cnt    Score    Error   UnitsStringBenchmark.testStringAdd           1  thrpt    4  260.743 ± 18.891  ops/usStringBenchmark.testStringAdd          10  thrpt    4   23.293 ±  0.865  ops/usStringBenchmark.testStringAdd         100  thrpt    4    0.565 ±  0.008  ops/usStringBenchmark.testStringBuilderAdd    1  thrpt    4  130.606 ±  6.957  ops/usStringBenchmark.testStringBuilderAdd   10  thrpt    4   74.840 ± 17.819  ops/usStringBenchmark.testStringBuilderAdd  100  thrpt    4    6.012 ±  0.520  ops/usStringBenchmark.testStringAdd           1   avgt    4    0.032 ±  0.004   us/opStringBenchmark.testStringAdd          10   avgt    4    0.355 ±  0.025   us/opStringBenchmark.testStringAdd         100   avgt    4   14.478 ±  1.119   us/opStringBenchmark.testStringBuilderAdd    1   avgt    4    0.062 ±  0.005   us/opStringBenchmark.testStringBuilderAdd   10   avgt    4    0.109 ±  0.038   us/opStringBenchmark.testStringBuilderAdd  100   avgt    4    1.317 ±  0.306   us/op</code></pre><p>(n)：参数n的取值<br />Mode: thrpt代表吞吐量，avgt代表平均运行时间。 对应<code>org.openjdk.jmh.annotations.Mode</code>中的<code>shortLabel</code><br />Cnt：iteration组数<br />Score：对应Units的值<br />Units：统计的单位<br />Error：误差</p><p>结果表明，在字符较多的情况下，StringBuilder的性能更好，完全符合预期。</p><h2 id="概念">概念</h2><h3 id="iteration">Iteration</h3><p>iteration是JMH进行测试的最小单位，包含一组invocations。</p><h3 id="invocation">Invocation</h3><p>一次benchmark方法调用。</p><h3 id="operation">Operation</h3><p>benchmark方法中，被测量操作的执行。如果被测试的操作在benchmark方法中循环执行，可以使用<code>@OperationsPerInvocation</code>表明循环次数，使测试结果为单次operation的性能。</p><h3 id="warmup">Warmup</h3><p>在实际进行benchmark前先进行预热。因为某个函数被调用多次之后，JIT会对其进行编译，通过预热可以使测量结果更加接近真实情况。</p><h2 id="注解">注解</h2><h3 id="benchmark">@Benchmark</h3><p>表示该方法需要被测量。</p><h3 id="benchmarkmode">@BenchmarkMode</h3><p>JMH进行benchmark时使用的模式。目前1.21版本共包含四种模式：</p><ul><li><p>Throughput： 每个单位时间执行的操作数</p></li><li><p>AverageTime： 每次执行消耗的平均时间</p></li><li><p>SampleTime： 每次执行时间，随机采样</p></li><li><p>SingleShotTime： 仅运行一次，用于测试冷启动时的性能</p></li></ul><p>几种模式可以相互组合，也可以设置为<code>Mode.All</code>来执行所有的模式。</p><h3 id="warmup-1">@Warmup</h3><p>实际进行benchmark前，预热的iteration配置。</p><h3 id="measurement">@Measurement</h3><p>实践测量的iteration配置。</p><h3 id="warmup和measurement的配置项相同">@Warmup和@Measurement的配置项相同：</h3><ul><li>iterations：iteration轮数</li><li>time：每次iteration的时间</li><li>timeUnit：iteration时间的单位，默认为秒</li><li>batchSize：每个operation中调用方法的次数</li></ul><h3 id="outputtimeunit">@OutputTimeUnit</h3><p>显示结果时使用的时间单位。</p><h3 id="fork">@Fork</h3><p>用于配置JMH运行时fork的Java进程。使用单独的进程可以避免测试结果之间互相影响。</p><ul><li>value： fork的进程数量</li><li>warmups： 每个进程执行Warmup的轮数</li><li>jvm：进程使用的JVM</li><li>jvm参数通过以下三个属性，按照从上到下的顺序拼接：<ul><li>jvmArgsPrepend</li><li>jvmArgs</li><li>jvmArgsAppend</li></ul></li></ul><p>默认fork的进程数配置在org.openjdk.jmh.runner.Defaults类中：</p><pre><code>    /**     * Number of forks in which we measure the workload.     */    public static final int MEASUREMENT_FORKS = 5;</code></pre><h3 id="threads">@Threads</h3><p>测试使用的线程数，默认为 <code>Runtime.getRuntime().availableProcessors()</code></p><h3 id="state">@State</h3><p>类实例的生命周期。</p><ul><li>Benchmark: 所有测试线程共享一个实例。</li><li>Group: 每个线程组内部使用一个实例。</li><li>Thread: 每个线程独占一个实例。</li></ul><h3 id="param">@Param</h3><p>在不同参数的情况下，分别测试。</p><h3 id="setup">@Setup</h3><p>在执行benchmark之前执行，用于初始化。</p><h3 id="teardown">@TearDown</h3><p>所有benchmark结束后执行，用于资源关闭。</p><h3 id="setup和teardown的调用时机">Setup和TearDown的调用时机</h3><p>根据Level配置</p><ul><li>Level.Trial： 每组iteration执行</li><li>Level.Iteration： 每组invocation执行</li><li>Level.Invocation： 每次invocation 即benchmark方法被调用时</li></ul><h2 id="blackhole">Blackhole</h2><p>为了避免JIT忽略未被使用的结果计算，可以使用<code>Blackhole.consume()</code>来保证方法被正确执行。</p><h2 id="将结果输出到日志文件中">将结果输出到日志文件中</h2><p>可以通过在<code>OptionsBuilder</code>中指定<code>output</code>属性，把测试结果输出到日志文件中。</p><pre><code>public static void main(String[] args) throws RunnerException {    Options opt = new OptionsBuilder()            .include(StringBenchmark.class.getSimpleName())            .output(&quot;D:/string_benchmark.log&quot;)            .build();    new Runner(opt).run();}</code></pre><h2 id="配置的优先级">配置的优先级</h2><p><a href="https://zhuanlan.zhihu.com/p/100574293">参考博客</a></p>]]>
                </content>
            </entry>
            <entry>
                <title><![CDATA[Lambda表达式是如何设计的]]></title>
                <link rel="alternate" type="text/html" href="https://www.wangdaye.net/archives/toleinj" />
                <id>tag:https://www.wangdaye.net,2024-01-12:toleinj</id>
                <published>2024-01-12T22:19:49+08:00</published>
                <updated>2024-01-12T22:30:17+08:00</updated>
                <author>
                    <name>王大爷</name>
                    <uri>https://www.wangdaye.net</uri>
                </author>
                <content type="html">
                        <![CDATA[<h1 id="lambda表达式是如何设计的">Lambda表达式是如何设计的</h1><blockquote><p>转载自布赖恩·戈茨的Translation of lambda expressions in javac</p></blockquote><h2 id="引言">引言</h2><p>阅读本文需要对invokedynamic指令知识有所了解：<a href="https://zhuanlan.zhihu.com/p/28124632?utm_medium=social">Invokedynamic</a></p><h2 id="转换策略">转换策略</h2><p>在字节码中表示Lambda表达式有多种方案，例如内部类、方法句柄、动态代理等，这些方案各有利弊。如何选择转换策略，有两个关键的衡量指标：</p><ul><li>是不引入特定策略，以期为将来的优化提供最大的灵活性；</li><li>是保持类文件格式的稳定。</li></ul><p>而invokedynamic指令可以同时满足这两个要求，即将Lambda在二进制字节码中的表达方式和其运行时的评估机制分开进行，而不是通过生成字节码的方式去创建一个实现了Lambda表达式的对象（例如为一个内部类调用构造方法)，通过这样的方式在编译的时候将方法需要的静态参数列表和动态参数列表与invokedynamic指令绑定，然后在运行的时候链接到指定的方法即可(此部分可以对比lambda$lambda$1()是如何被调用的)。</p><p>这么做的好处是invokedynamic指令使我们可以一直到运行时再去选择转换策略。运行时实现的方式是可以自由地选择转换策略，并且可以动态评估Lambda表达式。Invokedynamic允许这样做，且不需要付出为后续绑定方法可能强加的性能消耗。</p><p>具体的的转换策略是：当编译器遇到Lambda表达式的时候，它首先会将Lambda方法体内容脱糖到一个方法中(例如lambda$lambda$1()这样的方法)，此方法的参数列表和返回值类型与Lambda表达式的匹配，可能还会附加一些额外的参数（附加的参数来自外部作用域范围）。同时在遇到Lambda表达式的地方会生成一个invokedynamic调用点（CallSite对象），当调用点执行的时候会返回一个函数式接口的实例，这个转换后函数式接口的实现包含Lambda的内容（例如实现类LambdaMain$$Lambda$2）。</p><p>方法引用也会按照Lambda表达式一样的方式进行处理，但是大部分方法引用不需要被脱糖进到一个新方法中；我们可以简单地为一个引用的方法加载一个常量方法句柄，然后将其传给metafactory。</p><p>Lambda脱糖将Lambda表达式转换成字节码的第一步是将Lambda方法体脱糖到一个方法中。对于脱糖有以下几个问题需要考虑。</p><ul><li>将Lambda方法体脱糖到一个静态方法中还是一个实例方法中？</li><li>脱糖之后生成的方法应该放在哪一个类中？</li><li>脱糖之后生成的方法的可访问性应该是怎样的？</li><li>脱糖之后生成的方法的命名应该是怎样的？</li><li>如果需要一个适配器去桥接Lambda方法体的签名和函数式接口的签名（例如装箱、拆箱、基础类型的扩大和缩小转变、动态参数转换等），那么脱糖的方法是遵循Lambda方法体的签名还是函数式接口的签名，又或者是两者的结合呢？以及谁负责适配呢？</li><li>如果Lambda从外部作用域(enclosing scope)中获取参数，这些参数应该如何在脱糖的方法体的签名中表示呢？难道是将它们追加到参数列表的前面、后面，或者编译器可以将它们整合在一起，统一放到一个Bean对象里面？</li></ul><p>跟脱糖Lambda方法体时需要考虑的问题一样，我们也需要考虑方法引用是否需要一个适配器或者桥接方法。</p><p>对于以上问题，一般来说，在同等条件下，私有方法优于非私有方法，静态方法优于实例方法，最好的结果是Lambda方法体被脱糖在它所在的类里面，脱糖后的签名应该匹配Lambda方法体的签名，需要的额外参数应该被添加在参数列表的前面，而且完全不对方法引用进行脱糖。这些准则也不是一成不变的，在某些情况下，我们也不得不偏离这些基准策略。</p><p>接下来是关于Lambda脱糖的例子。</p><p>首先是无状态(stateless)Lambda，所谓无状态指的是Lambda方法体没有从外部作用域中捕捉任何状态，例如：</p><pre><code>class A {    public void foo() {        List&lt;String&gt; list = …        list.forEach( s -&gt; { System.out.println(s); } );    }}</code></pre><p>这个Lambda表达式对应的函数式接口的真实签名是 (String)V［其实这个Consumer接口中accept(T t)的真实签名，这种情况的签名通常称为naturesignature］，编译器会将Lambda方法体脱糖到一个静态方法，静态方法的签名与Lambda表达式的nature signature相同，然后为脱糖体生成一个方法，脱糖后的结果类似如下。</p><pre><code>class A {    public void foo() {        List&lt;String&gt; list = …        list.forEach( [lambda for lambda$1 as Block] );    }      //这个就是脱糖产生的方法    static void lambda$1(String s) {        System.out.println(s);    }}</code></pre><p>相比无状态Lambda，另外一种形式称为有状态Lambda，所谓有状态指的是Lambda方法体中使用了外部作用域的final局部变量、隐式是final的局部变量，或者外部实例(enclosing instance)的字段（这里可以看作捕获了外部作用域的this.xx字段），例如：</p><pre><code>class B {    public void foo() {        List&lt;Person&gt; list = …        final int bottom = …, top = …;        list.removeIf( p -&gt; (p.size &gt;= bottom &amp;&amp; p.size &lt;= top) );    }}</code></pre><p>上面这个例子，Lambda使用了外部作用域中final类型的局部变量bottom和top。脱糖之后的方法将使用的natural signature为 (Person)Z，并且在参数列表前面追加额外的参数。编译器有权决定这些额外的参数如何表示：参数可以逐个添加到参数列表的前面，或放在一个frame class中，或放在一个数组中。当然，最简单的方式是将参数逐个添加到参数列表的前面，如下面的例子所示。</p><pre><code>class B {    public void foo() {        List&lt;Person&gt; list = …        final int bottom = …, top = …;        list.removeIf( [ lambda for lambda$1 as Predicate capturing (bottom, top) ]);    }          //关注这个方法的签名    static boolean lambda$1(int bottom, int top, Person p) {        return (p.size &gt;= bottom &amp;&amp; p.size &lt;= top;    }}</code></pre><p>以上展示了Lambda如何脱糖，那么如何调用脱糖后的方法呢？关于这一部分的内容在前一节已经介绍了，接下来我们主要关注invokedynamic指令和脱糖方法之间参数是如何设定的。</p><h2 id="lambda-metafactory">Lambda Metafactory</h2><p>先来看看前面提到的例子。</p><pre><code>//lambda()方法public void lambda() {   Consumer&lt;String&gt; consumer2 = o -&gt; {       Object tmpObj = this.obj;       System.out.println(&quot;lambda2&quot;);   };} //对应的字节码public void lambda();    descriptor: ()V    flags: ACC_PUBLIC    Code:      stack=1, locals=3, args_size=1         …         6: aload_0         7: invokedynamic #7,  0  // InvokeDynamic #1:accept:\                     (Lcn/sensorsdata/lambda/LambdaMain;)\                     Ljava/util/function/Consumer;        12: astore_2        13: return</code></pre><p>上述代码的lambda()方法中使用了LambdaMain中的成员变量obj，观察其对应的字节码可以发现在执行invokedynamic指令之前先执行了aload_0，即this，该值作为参数会在前面提到的LambdaMetafactory.metafactory()方法中使用，下面我们看看这部分内容在Lambda设计参考中是如何介绍的。</p><p>首先看看什么是lambda metafactory：对给定的Lambda来说，这个调用点被称为lambda factory，lambda factory的动态参数是从外部作用域中捕获的，lambdafactory的引导方法是一个标准的方法，被称为lambda metafactory。虚拟机对每个invokedynamic只会调用一次这个metafactory，之后它会链接这个调用点然后退出。调用点的链接是懒加载的，所以factory sites不执行就不会被链接。基本的metafactory的静态参数如下。</p><pre><code>metaFactory(MethodHandles.Lookup caller,   // provided by VM            String invokedName,          // provided by VM            MethodType invokedType,      // provided by VM            MethodHandle descriptor,     // lambda descriptor            MethodHandle impl)           // lambda body</code></pre><p>前3个参数(caller、invokedName、invokedType)是在虚拟机调用链接的时候自动生成的。descripter参数确定了被转化的Lambda对应的函数式接口方法。impl参数确定了Lambda方法，要么是脱糖的Lambda方法体，要么是方法引用中的方法名。函数式接口方法的方法签名和实现方法有一些不同，实现方法可以有额外的参数，其余参数也可能不完全匹配。为方便展示，约定用一些符号来替换MethodHandle、MethodType与invokedynamic。</p><ul><li>method handle常量简写为MH（引用类型class-name.method-name）。</li><li>method type常量简写为MT(method-signature)。</li><li>invokedynamic简写为INDY((bootstrap, static args…)(dynamic args…))，注意这里的参数设定。</li></ul><p>对于前面脱糖的类A，可以使用如下方式来表示。</p><pre><code>class A {    public void foo() {            List&lt;String&gt; list = …            list.forEach(indy((MH(metaFactory), MH(invokeVirtual Block.apply),                               MH(invokeStatic A.lambda$1)( )));//注意此处的参数        }      private static void lambda$1(String s) {        System.out.println(s);    }}</code></pre><p>因为A中的Lambda是无状态的，所以lambda factory调用点的动态参数是空的。对于例子中的类B，动态参数并不为空，因为我们必须把bottom和top的值添加到lambda factory中。</p><pre><code>class B {    public void foo() {        List&lt;Person&gt; list = …        final int bottom = …, top = …;        list.removeIf(indy((MH(metaFactory), MH(invokeVirtual Predicate.apply),                            MH(invokeStatic B.lambda$1))( bottom, top ))));//注意此处的参数    }      private static boolean lambda$1(int bottom, int top, Person p) {        return (p.size &gt;= bottom &amp;&amp; p.size &lt;= top;    }}</code></pre><p>这就是LambdaMain的lambda()方法中会有6: aload_0的原因。</p><h2 id="静态方法还是实例方法">静态方法还是实例方法</h2><p>脱糖方法到底是静态方法还是实例方法呢？观察LambdaMain.class中的lambda$lambda$0()和lambda$lambda$1()两个方法，lambda$lambda$1()是实例方法的原因似乎与Lambda使用了LambdaMain中的字段obj有关，事实上确实如此。总体来说，我们将在Lambda中使用this、super或者外部实例的成员的情况称为instance-capturing lambdas，与其相对的是non-instance-capturinglambdas。</p><p>non-instance-capturing lambdas被脱糖成静态方法，instance-capturinglambdas被脱糖成实例的私有方法，当捕获instance-capturing lambdas的时候，this会被声明为第一个动态参数。</p><p>举个例子，考虑如下Lambda表达式中使用了一个minSize字段。</p><pre><code>list.filter(e -&gt; e.getSize() &lt; minSize )</code></pre><p>我们首先将上面的示例脱糖成一个实例方法，然后把接收者(this)作为第一个捕获的参数。结果如下：</p><pre><code>list.forEach(INDY((MH(metaFactory), MH(invokeVirtual Predicate.apply),                   MH(invokeVirtual B.lambda$1))( this ))));  private boolean lambda$1(Element e) {    return e.getSize() &lt; minSize;}</code></pre><p>因为Lambda方法体被转换成一个私有方法，所以metafactory中的调用点会加载一个常量池中的方法句柄。对示例方法来说，这个方法句柄的类型是REF_invokeSpecial（CONSTANT_MethodHandle_info结构的reference_index对应的值），而对静态方法来说，这个方法句柄的类型是REF_invokeStatic。脱糖成为一个私有方法是因为私有方法可以使用所在类的成员。</p><h2 id="方法引用">方法引用</h2><p>方法引用有多种写法，跟lambdas类似，也可以分成instance-capturing和non-instance-capturing两种。non-instance-capturing类型方法引用包括静态方法引用(Integer:: parseInt)、未绑定实例的方法引用(String::length)和构造方法引用(Foo::new)。当使用non-instance-capturing类型的方法引用时，动态参数列表总是空的，例如：</p><pre><code>list.filter(String::isEmpty)</code></pre><p>上面的例子会被转换成：</p><pre><code>list.filter(indy(MH(metaFactory), MH(invokeVirtual Predicate.apply),                 MH(invokeVirtual String.isEmpty))()))</code></pre><p>instance-capturing类型的方法引用形式包括绑定实例方法引用(s::length)、super()方法引用(super::foo)和内部类构造方法引用(Inner::new)。当捕获instance-capturing类型的方法引用，被捕获的参数列表总是有一个参数，就是this。</p>]]>
                </content>
            </entry>
            <entry>
                <title><![CDATA[字节序、大端字节序(Big Endian)、小端字节序(Little Endian)]]></title>
                <link rel="alternate" type="text/html" href="https://www.wangdaye.net/archives/大小端字节序" />
                <id>tag:https://www.wangdaye.net,2023-12-24:大小端字节序</id>
                <published>2023-12-24T12:33:12+08:00</published>
                <updated>2023-12-24T12:39:00+08:00</updated>
                <author>
                    <name>王大爷</name>
                    <uri>https://www.wangdaye.net</uri>
                </author>
                <content type="html">
                        <![CDATA[<div id="article_content" class="article_content clearfix"><div id="content_views" class="markdown_views prism-atom-one-dark"><h2 id="什么是字节序">什么是字节序？</h2><p>字节序，简单来说，指的是 超过一个字节的数据类型在[内存]中存储的顺序</p><h4 id="有几种字节序"><a name="t1"></a><a id="_6"></a>有几种字节序？</h4><h5 id="大端字节序big-endian"><a id="Big_Endian_8"></a>大端字节序(Big Endian)</h5><p>高位字节数据存放在内存低地址处，低位字节数据存放在内存高地址处。</p><h5 id="小端字节序little-endian"><a id="Little_Endian_11"></a>小端字节序(Little Endian)</h5><p>高位字节数据存放在内存高地址处，低位数据存放在内存低地址处。</p><p><img src="https://www.wangdaye.net/upload/2023/12/image-d1e146f2e8494e5eba5d272b5e2136b4.png" alt="image.png" /></p><p>如上图所示，<code>int32</code>类型的数值 <code>12345678</code>用一个字节<a href="https://blog.csdn.net/damanchen/article/details/112380741">表示不了</a>，需要用到4个字节，也就有了字节序的问题。</p><p>数值 <code>12345678</code>(一千两百三十四万五千六百七十八)，这里的最高位数据就是<code>1</code>，最低位数据就是<code>8</code>。</p><p>看大端序的存储方式，高位在前面的低地址，低位在后面的高地址，这也是人类读写数值的方式；而小端序正好相反。</p><p>那么问题来了，为什么还需要小端序的方式，只使用大端序的方式不是更方便吗？什么是内存高地址/低地址，为什么低地址在前，高地址在后呢？</p><h4 id="为什么需要小端字节序"><a name="t2"></a><a id="_25"></a>为什么需要小端字节序？</h4><blockquote><p>我们知道计算机正常的内存增长方式是从低到高(当然栈不是)，取数据方式是从基址根据偏移找到他们的位置。</p><p>从大/小端的存储方式可以看出，大端存储因为第一个字节就是高位，从而很容易知道它是正数还是负数，对于一些数值判断会很迅速。</p><p>而小端存储 第一个字节是它的低位，符号位在最后一个字节，这样在做数值四则运算时，从低位每次取出相应字节运算，最后直到高位，并且最终把符号位刷新，这样的运算方式会更高效。</p><p>所以大端和小端有其各自的优势。</p></blockquote><p>具体是使用大端序还是小端序跟处理器体系有关：</p><h5 id="处理器体系"><a id="_36"></a>处理器体系</h5><ul><li>x86、MOS Technology 6502、Z80、VAX、PDP-11等处理器为小端序；</li><li>Motorola 6800、Motorola 68000、PowerPC 970、System/370、SPARC（除V9外）等处理器为大端序；</li><li>ARM、PowerPC（除PowerPC 970外）、DEC Alpha、SPARC V9、MIPS、PA-RISC及IA64的字节序是可配置的。</li></ul><h4 id="字节序的处理"><a name="t3"></a><a id="_41"></a>字节序的处理</h4><p>计算机处理字节序的时候，不知道什么是高位字节，什么是低位字节。它只知道按顺序读取字节，先读第一个字节，再读第二个字节。</p><p>如果是大端字节序，先读到的就是高位字节，后读到的就是低位字节。小端字节序正好相反。</p><h4 id="网络字节序"><a name="t4"></a><a id="_48"></a>网络字节序</h4><p>前面的大端和小端都是在说计算机自己，也被称作主机字节序HBO(Host Byte Order)。其实，只要自己能够自圆其说是没啥问题的。问题是，网络的出现使得计算机可以通信了。通信，就意味着相处，相处必须得有共同语言啊，得说普通话，要不然就容易会错意，下了一个小时的小电影发现打不开，理解错误了！</p><p>但是每种计算机体系都有自己的主机字节序啊，还都不依不饶，坚持做自己，怎么办？</p><p><strong>TCP/IP协议隆重出场，RFC1700规定使用“大端”字节序为网络字节序NBO(Network Byte Order)</strong>。</p><p>其他不使用大端的计算机要注意了，发送数据的时候必须要将自己的主机字节序转换为网络字节序（即“大端”字节序），接收到的数据再转换为自己的主机字节序。这样就与CPU、操作系统无关了，实现了网络通信的标准化。</p><p>为了程序的兼容，你会看到，程序员们每次发送和接受数据都要进行转换，这样做的目的是保证代码在任何计算机上执行时都能达到预期的效果。</p><p>这么常用的操作，BSD Socket提供了封装好的转换接口，方便程序员使用。包括从主机字节序到网络字节序的转换函数：htons、htonl；从网络字节序到主机字节序的转换函数：ntohs、ntohl。当然，有了上面的理论基础，也可以编写自己的转换函数。</p><blockquote><p>网络字节顺序(NBO)<br />NBO(Network Byte Order)：按照从高到低的顺序存储，在网络上使用统一的网络字节顺序，可以避免兼容性问题。TCP/IP中规定好的一种数据表示格式，与具体的 CPU 类型、操作系统等无关。从而保证数据在不同主机之间传输时能够被正确解释。</p></blockquote><blockquote><p>主机字节顺序(HBO)<br />HBO(Host Byte Order)：不同机器 HBO 不相同，与 CPU 有关。计算机存储数据有两种字节优先顺序：Big Endian 和 Little Endian。Internet 以 Big Endian 顺序在网络上传输，所以对于在内部是以 Little Endian 方式存储数据的机器，在网络通信时就需要进行转换。</p></blockquote><blockquote><p>除了计算机的内部处理，其他的场合几乎都是大端字节序，比如<strong>网络传输和文件储存</strong>。</p></blockquote><h4 id="golang中验证系统的大小端"><a name="t5"></a><a id="golang_71"></a>golang中验证系统的大小端</h4><pre><code>package mainimport (&quot;fmt&quot;&quot;unsafe&quot;)func main() {a := int64(0x12345678)fmt.Printf(&quot;int64: %v bytes\n&quot;, unsafe.Sizeof(a))//用int8去转换int64，会只截取到第一个字节s := int8(a)if 0x12 == s {fmt.Println(&quot;Big-Endian&quot;)} else {fmt.Println(&quot;Little-Endian&quot;)}fmt.Printf(&quot;s: 0x%x&quot;, s)}输出如下:int64: 8 bytesLittle-Endians: 0x78</code></pre><p>参考：</p><blockquote><p>大小端字节序存在的意义，为什么不用一个标准呢？<a href="https://www.zhihu.com/question/25311159">https://www.zhihu.com/question/25311159</a></p><p><a href="http://www.ruanyifeng.com/blog/2016/11/byte-order.html">http://www.ruanyifeng.com/blog/2016/11/byte-order.html</a><br />你知道字节序吗 <a href="https://zhuanlan.zhihu.com/p/115135603">https://zhuanlan.zhihu.com/p/115135603</a></p><p>“字节序”是个什么鬼？ <a href="https://zhuanlan.zhihu.com/p/21388517">https://zhuanlan.zhihu.com/p/21388517</a></p><p>字节序及 Go encoding/binary 库 <a href="https://zhuanlan.zhihu.com/p/35326716">https://zhuanlan.zhihu.com/p/35326716</a></p><p>字节序：Big Endian 和 Little Endian <a href="https://songlee24.github.io/2015/05/02/endianess/">https://songlee24.github.io/2015/05/02/endianess/</a></p></blockquote></div><link href="https://csdnimg.cn/release/blogv2/dist/mdeditor/css/editerView/markdown_views-98b95bb57c.css" rel="stylesheet"><link href="https://csdnimg.cn/release/blogv2/dist/mdeditor/css/style-c216769e99.css" rel="stylesheet"></div>]]>
                </content>
            </entry>
            <entry>
                <title><![CDATA[一个优雅的环境管理工具--SDKMAN]]></title>
                <link rel="alternate" type="text/html" href="https://www.wangdaye.net/archives/sdkman简单使用" />
                <id>tag:https://www.wangdaye.net,2023-11-16:sdkman简单使用</id>
                <published>2023-11-16T21:49:25+08:00</published>
                <updated>2023-11-22T09:33:26+08:00</updated>
                <author>
                    <name>王大爷</name>
                    <uri>https://www.wangdaye.net</uri>
                </author>
                <content type="html">
                        <![CDATA[<p><a href="https://juejin.cn/post/7126033357405683748">SDKMAN详情介绍</a></p><h2 id="sdkman">SDKMAN!</h2><p>是在大多数基于Unix的系统上管理多个软件开发工具包的并行版本的工具。它提供了一个方便的命令行界面（CLI）和API来安装，切换，删除和列出sdk相关信息。以下是一些特性:</p><ul><li>By Developers, for Developers<br />安装SDK不再需要去各种下载页面去下载,解压,以及设置xxx_HOME或者PATH环境变量.</li><li>多平台<br />可以在任何基于UNIX的平台上运行：Mac OSX，Linux，Cygwin，Solaris和FreeBSD。 <a href="https://link.jianshu.com?t=https%3A%2F%2Fgithub.com%2Fflofreud%2Fposh-gvm">Powershell CLI</a>版本适用于Windows用户。</li><li>全套JAVA支持<br />为Java，Groovy，Scala，Kotlin和Ceylon等JVM安装软件开发工具包。 Ant，Gradle，Grails，Maven，SBT，Spark，Spring Boot，Vert.x以及其他许多支持。</li><li>APIs<br />使用开放的Broker REST API可以轻松地编写新的客户端。供应商可以通过安全的供应商API发布自己的版本。</li><li>轻量<br />只需要有<a href="https://link.jianshu.com?t=http%3A%2F%2Fcurl.haxx.se%2F">curl</a> \ <a href="https://link.jianshu.com?t=http%3A%2F%2Fwww.info-zip.org%2F">zip/unzip</a>就可以在<a href="https://link.jianshu.com?t=https%3A%2F%2Fwww.gnu.org%2Fsoftware%2Fbash%2F">bash</a>中通过命令使用.还可和<a href="https://link.jianshu.com?t=http%3A%2F%2Fwww.zsh.org%2F">ZSH</a>一起使用.</li></ul><h2 id="1安装">1.安装</h2><p>在终端中输入以下命令进行安装:</p><div class="_2Uzcx_"><pre><code>$ curl -s &quot;https://get.sdkman.io&quot; | bash</code></pre></div><p>如果提示缺少zip或unzip,安装后再次执行上面的命令即可.</p><div class="_2Uzcx_"><pre><code># 安装需要的组件,Ubuntu为例$ apt install zip$ apt install unzip</code></pre></div><p>安装完成后,在终端中输入:</p><div class="_2Uzcx_"><pre><code>$ source &quot;$HOME/.sdkman/bin/sdkman-init.sh&quot;</code></pre></div><p>输入以下命令查看安装情况:</p><div class="_2Uzcx_"><pre><code>$ sdk version# 以下为输出==== BROADCAST =================================================================* 09/01/18: Gradle 4.5-rc-1 released on SDKMAN! #gradle* 06/01/18: sbt 1.1.0 released on SDKMAN! #scala* 20/12/17: Gradle 4.4.1 released on SDKMAN! #gradle================================================================================SDKMAN 5.6.0+287</code></pre></div><h2 id="2安装到自定义位置">2.安装到自定义位置</h2><p>SDKMAN的默认安装位置为:$HOME/.sdkman.你可以通过设置SDKMAN_DIR环境变量来修改安装位置:</p><div class="_2Uzcx_"><pre><code>$ export SDKMAN_DIR=&quot;/usr/local/sdkman&quot; &amp;&amp; curl -s &quot;https://get.sdkman.io&quot; | bash</code></pre></div><h2 id="3beta通道">3.Beta通道</h2><p>SDKMAN的Bate版,包含一些cli的新功能,但是可能会不稳定.如果需要使用Bate版本,需要修改~/.sdkman/etc/config文件:</p><div class="_2Uzcx_"><pre><code>sdkman_beta_channel=true</code></pre></div><p>然后打开一个终端执行:</p><div class="_2Uzcx_"><pre><code>$ sdk selfupdate force</code></pre></div><p>如果不需要使用Bate版本了,将上面的配置修改为false,再执行一次更新即可.</p><h2 id="4卸载">4.卸载</h2><p>SDKMAN!没有提供自动化的卸载方法,可以通过以下命令进行卸载:</p><div class="_2Uzcx_"><pre><code>tar zcvf ~/sdkman-backup_$(date +%F-%kh%M).tar.gz -C ~/ .sdkman$ rm -rf ~/.sdkman</code></pre></div><p>然后从.bashrc，.bash_profile和/或.profile文件中编辑和删除初始化代码片段。如果您使用ZSH，请将其从.zshrc文件中删除。要删除的代码片段如下所示：</p><div class="_2Uzcx_"><pre><code>#THIS MUST BE AT THE END OF THE FILE FOR SDKMAN TO WORK!!![[ -s &quot;/home/dudette/.sdkman/bin/sdkman-init.sh&quot; ]] &amp;&amp; source &quot;/home/dudette/.sdkman/bin/sdkman-init.sh&quot;</code></pre></div><h2 id="5使用">5.使用</h2><h3 id="50-列出支持的软件">5.0 列出支持的软件</h3><div class="_2Uzcx_"><pre><code>$ sdk list# 执行命令后进入vi模式进行阅读,q退出阅读</code></pre></div><h3 id="51-列出软件的版本">5.1 列出软件的版本</h3><div class="_2Uzcx_"><pre><code>$ sdk list gradle================================================================================Available Gradle Versions================================================================================     4.5-rc-1             4.2.1                3.1                  2.11            &gt; * 4.4.1                4.2-rc-2             3.0                  2.10                4.4-rc-6             4.2-rc-1             2.9                  2.1                 4.4-rc-5             4.2                  2.8                  2.0                 4.4-rc-4             4.1                  2.7                  1.9                 4.4-rc-3             4.0.2                2.6                  1.8                 4.4-rc-2             4.0.1                2.5                  1.7                 4.4-rc-1             4.0                  2.4                  1.6                 4.4                  3.5.1                2.3                  1.5                 4.3.1                3.5                  2.2.1                1.4                 4.3-rc-4             3.4.1                2.2                  1.3                 4.3-rc-3             3.4                  2.14.1               1.2                 4.3-rc-2             3.3                  2.14                 1.12                4.3-rc-1             3.2.1                2.13                 1.11                4.3                  3.2                  2.12                 1.10           ================================================================================+ - local version* - installed&gt; - currently in use================================================================================</code></pre></div><h3 id="52-安装gradle">5.2 安装gradle</h3><div class="_2Uzcx_"><pre><code>$ sdk install gradleDownloading: gradle 4.4.1In progress...######################################################################## 100.0%Installing: gradle 4.4.1Done installing!Setting gradle 4.4.1 as default.</code></pre></div><h3 id="53-安装指定版本软件">5.3 安装指定版本软件</h3><div class="_2Uzcx_"><pre><code># 后面跟上版本号即可$ sdk install gradle 4.4.1</code></pre></div><h3 id="54-安装本地包">5.4 安装本地包</h3><div class="_2Uzcx_"><pre><code>$ sdk install groovy 3.0.0-SNAPSHOT /path/to/groovy-3.0.0-SNAPSHOT</code></pre></div><h3 id="55-卸载包">5.5 卸载包</h3><div class="_2Uzcx_"><pre><code>$ sdk uninstall scala 2.11.6</code></pre></div><h3 id="56-选择版本">5.6 选择版本</h3><p>选择一个版本用于当前终端:</p><div class="_2Uzcx_"><pre><code>$ sdk use scala 2.12.1</code></pre></div><h3 id="57-设置默认版本">5.7 设置默认版本</h3><div class="_2Uzcx_"><pre><code>$ sdk default scala 2.11.6</code></pre></div><h3 id="58-查看当前使用的版本">5.8 查看当前使用的版本</h3><div class="_2Uzcx_"><pre><code>$ sdk current java  Using java version 8u111#查看所有本地包的当前版本$ sdk current  Using:  groovy: 2.4.7  java: 8u111  scala: 2.12.1</code></pre></div><h3 id="59-sdk版本升级">5.9 sdk版本升级</h3><div class="_2Uzcx_"><pre><code>$ sdk upgrade springboot  Upgrade:  springboot (1.2.4.RELEASE, 1.2.3.RELEASE &lt; 1.2.5.RELEASE)# 本地所有sdk全部升级$ sdk upgrade  Upgrade:  gradle (2.3, 1.11, 2.4, 2.5 &lt; 2.6)  grails (2.5.1 &lt; 3.0.4)  springboot (1.2.4.RELEASE, 1.2.3.RELEASE &lt; 1.2.5.RELEASE)</code></pre></div><h3 id="510-离线模式">5.10 离线模式</h3><div class="_2Uzcx_"><pre><code>$ sdk offline enable  Forced offline mode enabled.$ sdk offline disable  Online mode re-enabled!</code></pre></div><p>当电脑没有网的时候,离线模式会进行自动切换.</p><h3 id="511-sdkman版本升级">5.11 SDKMAN!版本升级</h3><div class="_2Uzcx_"><pre><code>$ sdk selfupdate# 强制重新安装$ sdk selfupdate force</code></pre></div>]]>
                </content>
            </entry>
            <entry>
                <title><![CDATA[Function、Consumer、Supplier的妙用]]></title>
                <link rel="alternate" type="text/html" href="https://www.wangdaye.net/archives/java8functionconsumersupplier" />
                <id>tag:https://www.wangdaye.net,2023-11-14:java8functionconsumersupplier</id>
                <published>2023-11-14T11:13:31+08:00</published>
                <updated>2023-11-22T09:31:59+08:00</updated>
                <author>
                    <name>王大爷</name>
                    <uri>https://www.wangdaye.net</uri>
                </author>
                <content type="html">
                        <![CDATA[<h2 id="一概述">一、概述</h2><p>Jdk8之后新增的一个重要的包 : <strong>java.util.function</strong></p><p>该包下所有的接口都是函数式接口, 按分类主要分为四大接口类型: <code>Function</code>、<code>Consumer</code>、<code>Predicate</code>、<code>Supplier</code>。有关Predicate这里不再讲解，因为上面有单独写过一篇博客。</p><p><img src="https://img2018.cnblogs.com/blog/1090617/201912/1090617-20191216190024258-463349888.jpg" alt="" /></p><p>延伸如下</p><p><img src="https://img2018.cnblogs.com/blog/1090617/201912/1090617-20191216190111553-251022017.jpg" alt="" /></p><p>这里也仅仅是展示一部分，我们看看java.util.function包下</p><p><img src="https://img2018.cnblogs.com/blog/1090617/201912/1090617-20191216190211633-1473355636.jpg" alt="" /></p><h2 id="二consumer">二、Consumer</h2><p><code>作用</code> 一听这名字就知道是消费某个对象，没有返回值。</p><h4 id="1源码">1、源码</h4><p>在源码中只有两个方法，一个抽象方法，一个默认方法。</p><pre><code>@FunctionalInterfacepublic interface Consumer&lt;T&gt; {    /**     * 抽象方法：传入一个指定泛型的参数，无返回值     */    void accept(T t);    /**     * 如同方法名字一样andThen,类似一种相加的功能（下面会举例说明）     */    default Consumer&lt;T&gt; andThen(Consumer&lt;? super T&gt; after) {        Objects.requireNonNull(after);        return (T t) -&gt; { accept(t); after.accept(t); };    }}</code></pre><h4 id="2使用示例">2、使用示例</h4><pre><code>    public static void main(String[] args) {        testConsumer();        testAndThen();    }    /**     * 一个简单的平方计算     */    public static void testConsumer() {        //设置好Consumer实现方法        Consumer&lt;Integer&gt; square = x -&gt; System.out.println(&quot;平方计算 : &quot; + x * x);        //传入值        square.accept(2);    }    /**     * 定义3个Consumer并按顺序进行调用andThen方法     */    public static void testAndThen() {        //当前值        Consumer&lt;Integer&gt; consumer1 = x -&gt; System.out.println(&quot;当前值 : &quot; + x);        //相加        Consumer&lt;Integer&gt; consumer2 = x -&gt; { System.out.println(&quot;相加 : &quot; + (x + x)); };        //相乘        Consumer&lt;Integer&gt; consumer3 = x -&gt; System.out.println(&quot;相乘 : &quot; + x * x);        //andThen拼接        consumer1.andThen(consumer2).andThen(consumer3).accept(1);    }</code></pre><p><strong>运行结果</strong></p><p><img src="https://img2018.cnblogs.com/blog/1090617/201912/1090617-20191216190252957-1768982616.jpg" alt="" /></p><p>单个这样消费看去并没啥意义,但如果是集合操作就有意义了，所以Jdk8的Iterator接口就引入了Consumer。</p><h4 id="3jdk8使用">3、JDK8使用</h4><p>Iterable接口的forEach方法需要传入Consumer，大部分集合类都实现了该接口，用于返回Iterator对象进行迭代。</p><pre><code>public interface Iterable&lt;T&gt; {    //forEach方法传入的就是Consumer    default void forEach(Consumer&lt;? super T&gt; action) {        Objects.requireNonNull(action);        for (T t : this) {            action.accept(t);        }    }}</code></pre><p>我们在看给我们带来的便利</p><pre><code>    public static void main(String[] args) {        //假设这里有个集合,集合里的对象有个status属性,现在我想对这个属性赋值一个固定值        List&lt;Pension&gt; pensionList = new ArrayList&lt;&gt;();        //1、传统的通过for循环添加        for (Pension pension : pensionList) {            pension.setStatus(1);        }        //2、通过forEach的Consumer添加        pensionList.forEach(x -&gt; x.setStatus(1));    }</code></pre><p>这样一比较是不是代码简洁了点，这就是Consumer是我们代码带来简洁的地方。</p><h2 id="三supplier">三、Supplier</h2><p><code>作用</code> 提前定义可能返回的一个指定类型结果，等需要调用的时候再获取结果。</p><h4 id="1源码-1">1、源码</h4><pre><code>@FunctionalInterfacepublic interface Supplier&lt;T&gt; {    /**     * 只有这一个抽象类     */    T get();}</code></pre><p>源码非常简单。</p><h4 id="2jdk8使用">2、JDK8使用</h4><p>在JDK8中Optional对象有使用到</p><pre><code>Optional.orElseGet(Supplier&lt;? extends T&gt;) //当this对象为null，就通过传入supplier创建一个T返回。</code></pre><p>我们看下源码</p><pre><code> public &lt;X extends Throwable&gt; T orElseThrow(Supplier&lt;? extends X&gt; exceptionSupplier) throws X {        if (value != null) {            return value;        } else {            throw exceptionSupplier.get();        }    }</code></pre><p><code>使用示例</code></p><pre><code>  public static void main(String[] args) {        Person son = null;        //先判断son是否为null,如果为不为null则返回当前对象,如果为null则返回新创建的对象        BrandDTO optional = Optional.ofNullable(son).orElseGet(() -&gt; new Person());    }</code></pre><p>这样代码是不是又简单了。有关Optional这里就不多说,接下来会单独写一篇博客。</p><h2 id="四function">四、Function</h2><p><code>作用</code> 实现一个”一元函数“，即传入一个值经过函数的计算返回另一个值。</p><h4 id="1源码-2">1、源码</h4><pre><code>    @FunctionalInterface    public interface Function&lt;T, R&gt; {        /**         * 抽象方法: 根据一个数据类型T加工得到一个数据类型R         */        R apply(T t);        /**         * 组合函数，调用当前function之前调用         */        default &lt;V&gt; java.util.function.Function&lt;V, R&gt; compose(java.util.function.Function&lt;? super V, ? extends T&gt; before) {            Objects.requireNonNull(before);            return (V v) -&gt; apply(before.apply(v));        }        /**         * 组合函数，调用当前function之后调用         */        default &lt;V&gt; java.util.function.Function&lt;T, V&gt; andThen(java.util.function.Function&lt;? super R, ? extends V&gt; after) {            Objects.requireNonNull(after);            return (T t) -&gt; after.apply(apply(t));        }        /**         *  静态方法，返回与原函数参数一致的结果。x=y         */        static &lt;T&gt; java.util.function.Function&lt;T, T&gt; identity() {            return t -&gt; t;        }    }</code></pre><h4 id="2使用示例-1">2、使用示例</h4><pre><code>public static void main(String[] args) {        applyTest();        andThenTest();        composeTest();        test();    }    /**     * 1、apply 示例     */    private static void applyTest() {        //示例1：定义一个funciton,实现将String转换为Integer        Function&lt;String, Integer&gt; function = x -&gt; Integer.parseInt(x);        Integer a = function.apply(&quot;100&quot;);        System.out.println(a.getClass());        // 结果：class java.lang.Integer    }    /**     * 2、andThen 示例     */    private static void andThenTest() {        //示例2：使用andThen() 实现一个函数 y=10x + 10;        //先执行 10 * x        Function&lt;Integer, Integer&gt; function2 = x -&gt; 10 * x;        //通过andThen在执行 这里的x就等于上面的10 * x的值        function2 = function2.andThen(x -&gt; x + 10);        System.out.println(function2.apply(2));        //结果：30    }    /**     * 3、compose 示例     */    private static void composeTest() {        //示例3：使用compose() 实现一个函数 y=(10+x)2;        Function&lt;Integer, Integer&gt; function3 = x -&gt; x * 2;        //先执行 x+10 在执行(x+10)*2顺序与上面相反        function3 = function3.compose(x -&gt; x + 10);        System.out.println(function3.apply(3));        //结果：26    }    /**     * 4、综合示例     */    private static void test() {//示例4：使用使用compose()、andThen()实现一个函数 y=(10+x)*2+10;        //执行第二步        Function&lt;Integer, Integer&gt; function4 = x -&gt; x * 2;        //执行第一步        function4 = function4.compose(x -&gt; x + 10);        //执行第三步        function4 = function4.andThen(x -&gt; x + 10);        System.out.println(function4.apply(3));       //结果：36    }</code></pre><h4 id="3jdk8使用-1">3、JDK8使用</h4><p>有两个地方很常用</p><pre><code>1、V HashMap.computeIfAbsent(K , Function&lt;K, V&gt;) // 简化代码，如果指定的键尚未与值关联或与null关联，使用函数返回值替换。2、&lt;R&gt; Stream&lt;R&gt; map(Function&lt;? super T, ? extends R&gt; mapper); // 转换流</code></pre><p><code>computeIfAbsent使用示例</code></p><pre><code>Map&lt;String, List&lt;String&gt;&gt; map = new HashMap&lt;&gt;();List&lt;String&gt; list;// java8之前写法list = map.get(&quot;key&quot;);if (list == null) {    list = new LinkedList&lt;&gt;();    map.put(&quot;key&quot;, list);}list.add(&quot;11&quot;);// 使用 computeIfAbsent 可以这样写 如果key返回部位空则返回该集合 ，为空则创建集合后返回list = map.computeIfAbsent(&quot;key&quot;, k -&gt; new ArrayList&lt;&gt;());list.add(&quot;11&quot;);</code></pre><p><code>stream中map使用示例</code></p><pre><code>  public static void main(String[] args) {        List&lt;Person&gt; persionList = new ArrayList&lt;Person&gt;();        persionList.add(new Person(1,&quot;张三&quot;,&quot;男&quot;,38));        persionList.add(new Person(2,&quot;小小&quot;,&quot;女&quot;,2));        persionList.add(new Person(3,&quot;李四&quot;,&quot;男&quot;,65));        //1、只取出该集合中所有姓名组成一个新集合（将Person对象转为String对象）        List&lt;String&gt; nameList=persionList.stream().map(Person::getName).collect(Collectors.toList());        System.out.println(nameList.toString());        }</code></pre><p>代码是不是又简洁了。</p><p><code>总结</code> 这些函数式接口作用在我看来，就是定义一个方法,方法中有个参数是函数式接口，这样的话函数的具体实现则由调用者来实现。这就是函数式接口的意义所在。</p><p>一般我们也会很少去定义一个方法，方法参数包含函数接口。我们更重要的是学会使用JDk8中带有函数式接口参数的方法，来简化我们的代码。</p>]]>
                </content>
            </entry>
            <entry>
                <title><![CDATA[后端也可以数据绑定--BeanMap的使用]]></title>
                <link rel="alternate" type="text/html" href="https://www.wangdaye.net/archives/xiang-jie-b-e-a-n-m-a-p-jiang-j-a-v-a-dui-xiang-zhuan-wei-m-a-p-de-shi-yong" />
                <id>tag:https://www.wangdaye.net,2023-11-12:xiang-jie-b-e-a-n-m-a-p-jiang-j-a-v-a-dui-xiang-zhuan-wei-m-a-p-de-shi-yong</id>
                <published>2023-11-12T11:14:09+08:00</published>
                <updated>2023-11-22T09:34:07+08:00</updated>
                <author>
                    <name>王大爷</name>
                    <uri>https://www.wangdaye.net</uri>
                </author>
                <content type="html">
                        <![CDATA[<p><code>BeanMap</code> 是 Apache Commons BeanUtils 库中的一个类，它提供了一种将 Java 对象转换为 Map 的方式。<code>BeanMap</code> 可以将 Java 对象的属性作为键，属性值作为对应的值，存储在一个 Map 中。</p><p>BeanMap在后端最常用的场景就是读取配置文件的信息。</p><h4 id="一基本用法beanmap最基本的用法就是将java对象转成map"><a name="t1"></a><a id="BeanMapJavaMap_5"></a>一、基本用法：BeanMap最基本的用法就是将Java对象转成Map</h4><p>以下是使用 <code>BeanMap</code> 的基本示例：</p><p>首先，确保你已经引入了 Apache Commons BeanUtils 库。</p><pre><code>import org.apache.commons.beanutils.BeanMap;// 创建一个 Java 对象Person person = new Person();person.setName(&quot;John Doe&quot;);person.setAge(30);// 将 Java 对象转换为 BeanMapBeanMap beanMap = new BeanMap(person);// 通过键获取属性值String name = (String) beanMap.get(&quot;name&quot;);int age = (int) beanMap.get(&quot;age&quot;);System.out.println(&quot;Name: &quot; + name);System.out.println(&quot;Age: &quot; + age);</code></pre><p>在上面的示例中，我们创建了一个名为 <code>Person</code> 的 Java 对象，并设置了其名称和年龄属性。然后，我们使用 <code>BeanMap</code> 将 <code>Person</code> 对象转换为一个 Map。我们可以使用 <code>get</code> 方法通过属性名从 <code>BeanMap</code> 中获取属性值。</p><p>请注意，<code>BeanMap</code> 中的所有属性都作为字符串键存储在 Map 中。因此，在获取属性值时，需要进行适当的类型转换。</p><p>除了获取属性值之外，<code>BeanMap</code> 还提供了其他方法，如 <code>put</code>、<code>containsKey</code>、<code>size</code> 等，可以用于操作和查询属性值。</p><p>请注意，<code>BeanMap</code> 对象只是对 Java 对象属性的映射，不会创建新的对象。它允许您通过属性名称访问和操作 Java 对象的属性值。</p><h4 id="二beanmap的意义为什么要使用beanmap"><a name="t2"></a><a id="BeanMapBeanMap_39"></a>二、BeanMap的意义，为什么要使用BeanMap？</h4><p>引入BeanMap可以为开发人员提供一种方便的方式来操作Java对象的属性。以下是一些使用BeanMap的常见情况：</p><ol><li><p>简化对象到Map的转换：有时候，您可能需要将Java对象转换为Map来进行处理或传递数据。使用BeanMap，您可以轻松地将Java对象转换为一个可读写的Map，其中对象的属性作为Map的键，属性值作为Map的值。这样，就可以像操作Map一样访问和修改对象的属性。</p></li><li><p>动态访问和操作属性：BeanMap允许您在运行时动态地获取和设置Java对象的属性值，而无需显式调用对象的getter和setter方法。这对于需要动态操作属性的情况非常有用，例如在通用的数据绑定或动态配置场景中。</p><h5 id="以下是一个使用beanmap进行数据绑定和转换的示例"><a id="BeanMap_46"></a>以下是一个使用BeanMap进行数据绑定和转换的示例：</h5><p>假设我们有一个用户表单，其中包含姓名（name）、年龄（age）和电子邮件（email）字段。我们需要将表单数据绑定到一个User对象中。这时可以使用BeanMap来实现数据的绑定和转换。</p><p>首先，我们创建一个User类，具有与表单字段对应的属性：</p><pre><code>public class User {    private String name;    private int age;    private String email;    // 省略构造函数和其他方法    // Getter和Setter方法}</code></pre><p>接下来，我们获取表单数据并将其填充到BeanMap中：</p><pre><code>import org.apache.commons.beanutils.BeanMap;// 假设表单数据存储在一个Map中Map&lt;String, Object&gt; formData = new HashMap&lt;&gt;();formData.put(&quot;name&quot;, &quot;John Doe&quot;);formData.put(&quot;age&quot;, &quot;30&quot;);formData.put(&quot;email&quot;, &quot;john.doe@example.com&quot;);// 将表单数据填充到BeanMap中BeanMap beanMap = new BeanMap(new User());// 使用putAll方法将表单数据填充到BeanMap中beanMap.putAll(formData);</code></pre><p>现在，我们可以通过BeanMap方便地访问User对象的属性：</p><pre><code>// 获取User对象的姓名String name = (String) beanMap.get(&quot;name&quot;);System.out.println(&quot;Name: &quot; + name);// 修改User对象的年龄beanMap.put(&quot;age&quot;, 35);// 获取User对象的电子邮件String email = (String) beanMap.get(&quot;email&quot;);System.out.println(&quot;Email: &quot; + email);</code></pre><p>最后，我们可以使用BeanMap中的数据创建一个完整的User对象：</p><pre><code>User user = (User) beanMap.getBean();System.out.println(&quot;User: &quot; + user.getName() + &quot;, &quot; + user.getAge() + &quot;, &quot; + user.getEmail());</code></pre><p>上述示例演示了如何使用BeanMap将表单数据绑定到User对象，并通过BeanMap方便地访问和修改属性。这种方式可以简化数据绑定和转换的过程，将表单数据直接填充到BeanMap中，而无需分别调用每个属性的setter方法。这减少了冗余的代码，并提供了一种更简洁的方式来完成数据绑定。</p><h4 id="三beanmap和gettersetter各自适用于哪些场景"><a name="t3"></a><a id="BeanMapgettersetter_109"></a>三、BeanMap和getter、setter各自适用于哪些场景？</h4><p>BeanMap和getter、setter方法各自适用于不同的场景。以下是它们常见的应用场景：</p><h5 id="beanmap适用的场景"><a id="BeanMap_113"></a>BeanMap适用的场景：</h5><ol><li>动态属性访问。</li><li><em><strong>简化数据绑定</strong></em>：在数据绑定的场景中，BeanMap可以用于将Java对象转换为Map，使得属性的读取和设置更加简便。这对于处理表单数据、数据传输等场景非常有用。</li><li>*配置处理：*如果您需要从外部配置源（例如属性文件、数据库）加载属性并对其进行操作，BeanMap可以简化配置处理的过程。您可以将配置数据加载到BeanMap中，然后方便地读取和修改属性。</li></ol><h5 id="getter和setter方法适用的场景"><a id="GetterSetter_119"></a>Getter和Setter方法适用的场景：</h5><ol><li>封装和控制属性访问：通过使用getter和setter方法，您可以对属性的访问进行封装，控制读取和修改的逻辑。这使得您可以在属性级别上实施业务规则、验证和安全性控制。</li><li>IDE支持和自动生成：大多数集成开发环境（IDE）都支持自动生成getter和setter方法，使得编写这些方法变得非常简单和快捷。这样可以节省开发人员的时间和精力。</li><li>静态属性访问：如果属性的名称是固定的且在编译时已知，并且没有动态访问的需求，直接使用getter和setter方法是一种更直接和高效的方式。这种方式无需引入额外的工具或类库。</li></ol><h5 id="在spring框架中更常用的是使用getter和setter方法而不是beanmap下面是一些原因"><a id="SpringgettersetterBeanMap_126"></a>在Spring框架中，更常用的是使用getter和setter方法而不是BeanMap。下面是一些原因：</h5><ol><li>遵循JavaBean规范：Spring框架鼓励使用POJO（Plain Old Java Object）作为组件，它们通常遵循JavaBean规范。JavaBean规范要求使用getter和setter方法来访问和修改属性，这样可以更好地封装和控制属性的访问。因此，在Spring框架中，getter和setter方法是一种更常见和推荐的方式。</li><li>Spring的依赖注入（DI）机制：Spring框架的核心特性之一是依赖注入（Dependency Injection，DI）。通过DI，Spring可以自动将依赖注入到组件中，而无需显式调用setter方法。Spring框架通过使用反射和属性名称来自动查找和调用setter方法，并将依赖注入到属性中。</li><li>Spring表达式语言（SpEL）：Spring框架提供了表达式语言（SpEL），允许在XML配置文件和注解中直接访问对象的属性。SpEL支持直接访问getter和setter方法，使得在Spring框架中使用getter和setter更加方便和自然。</li></ol><p>尽管BeanMap在某些特定场景下可能有用，但在Spring框架中，通常更常见和推荐使用getter和setter方法。这样可以与Spring的约定和机制保持一致，并充分利用Spring框架提供的特性和功能。</p><h4 id="四比较一下beanmap和gettersetter"><a name="t4"></a><a id="BeanMapgettersetter_134"></a>四、比较一下BeanMap和getter、setter</h4><p>BeanMap和getter、setter是用于访问和操作Java对象属性的不同方法。下面是它们之间的比较：</p><h5 id="beanmap"><a id="BeanMap_138"></a>BeanMap：</h5><ul><li>优点：<ul><li>动态性：BeanMap允许在运行时动态地获取和设置Java对象的属性值，而无需在编译时确定。</li><li>简化操作：通过将Java对象转换为可读写的Map，BeanMap提供了一种简化的方式来访问和操作对象的属性，无需显式调用getter和setter方法。</li><li>减少重复代码：使用BeanMap可以减少编写大量的getter和setter方法的重复代码，从而提高代码的可维护性和简洁性。</li></ul></li><li>缺点：<ul><li>性能开销：相对于直接调用对象的getter和setter方法，BeanMap可能引入一些性能开销，因为它需要进行Map的操作和反射调用。</li></ul></li></ul><h5 id="getter和setter方法"><a id="GetterSetter_147"></a>Getter和Setter方法：</h5><ul><li>优点：<ul><li>直接访问：getter和setter方法提供了直接访问对象属性的方式，没有额外的性能开销。</li><li>封装性：通过使用getter和setter方法，可以实现对属性的封装，控制对属性的访问和修改。</li><li>IDE支持：IDE（集成开发环境）通常提供自动生成getter和setter方法的功能，方便快捷地生成这些方法。</li></ul></li><li>缺点：<ul><li>代码冗余：在类的属性较多时，需要编写大量的getter和setter方法，可能导致代码冗余和可读性下降。</li><li>静态定义：getter和setter方法在编译时就需要确定，并且一旦定义后，通常不能在运行时动态更改。</li></ul></li></ul><p>选择使用BeanMap或getter和setter方法取决于具体的需求和场景。如果需要动态访问和操作属性，或者希望减少重复代码，BeanMap可能是一个更好的选择。然而，如果性能是关键考虑因素，或者需要对属性进行封装和控制访问权限，getter和setter方法可能更适合。</p></li></ol>]]>
                </content>
            </entry>
            <entry>
                <title><![CDATA[TT命令实现简单压力测试--Arthas的妙用]]></title>
                <link rel="alternate" type="text/html" href="https://www.wangdaye.net/archives/arthastt简单压力测试发现问题" />
                <id>tag:https://www.wangdaye.net,2023-11-07:arthastt简单压力测试发现问题</id>
                <published>2023-11-07T10:18:05+08:00</published>
                <updated>2023-11-22T09:34:40+08:00</updated>
                <author>
                    <name>王大爷</name>
                    <uri>https://www.wangdaye.net</uri>
                </author>
                <content type="html">
                        <![CDATA[<h1 id="arthas-tt-简单压力测试发现问题">Arthas TT 简单压力测试发现问题</h1><h2 id="步骤1打开客户端一执行如下命令使用tt抓包">步骤1：打开客户端一，执行如下命令，使用TT抓包。</h2><p>￼￼<br /><img src="https://www.wangdaye.net/upload/2023/11/image-bc56325e06cf41c7a452cbb6c8e6ee43.png" alt="image.png" /><br /><img src="https://www.wangdaye.net/upload/2023/11/image-cbb5b16f63cb424c8f739a35493d4e2d.png" alt="image.png" /></p><h2 id="步骤2客户端一退出如上命令并且执行如下命令使用tt复现请求">步骤2：客户端一，退出如上命令，并且执行如下命令，使用TT复现请求。</h2><p><img src="https://www.wangdaye.net/upload/2023/11/image-54fd590302564bb08bafd926b041a479.png" alt="image.png" /></p><h2 id="步骤3打开客户端二执行trace命令">步骤3，打开客户端二。执行trace命令。</h2><p>￼<br /><img src="https://www.wangdaye.net/upload/2023/11/image-ffe919a1ea1c4bc9a6a813e31a3439b1.png" alt="image" /></p><h2 id="注意事项">注意事项</h2><ul><li>其中2,3步骤可能需要调换一下顺序，因为字节码执行顺序问题，需要先将trace命令执行修改完成字节码，然后执行TT复现命令，去调用新字节码。</li></ul>]]>
                </content>
            </entry>
            <entry>
                <title><![CDATA[从设计者的角度出发理解源码--Easy-Excel]]></title>
                <link rel="alternate" type="text/html" href="https://www.wangdaye.net/archives/easy-excel框架阅读笔记" />
                <id>tag:https://www.wangdaye.net,2023-11-05:easy-excel框架阅读笔记</id>
                <published>2023-11-05T19:39:09+08:00</published>
                <updated>2023-12-03T11:03:21+08:00</updated>
                <author>
                    <name>王大爷</name>
                    <uri>https://www.wangdaye.net</uri>
                </author>
                <content type="html">
                        <![CDATA[<p>都说阅读源码是一个程序员升级的必经之路，笔者也阅读了不少源码，但很长时间感受源码带给我的提升没有预期的那么大，所以我有了个新点子，假如我能知道一个开源项目作者是如何思考的，是否能更接近这个目标了呢？于是有了此文。</p><p>本文以比较流行的Excel解析框架，Easy-excel为基础，我尽量试着推倒作者在每个环节的思考，以此方式来阅读源码，和解析源码的设计思想。</p><h2 id="为什么要写easy-excel">为什么要写Easy-excel</h2><p>作者在写Easy-excel时，正是POI大行其道的时候，其令人诟病的特点就是难用，内存OOM，性能差等问题。所以笔者推测，作者应该当时正在做数据处理相关内容，且深受其害，于是，作者拍案而起，发誓要写一个，易用，高性能，内存使用率高的Excel处理框架，并且具备比较好的扩展性让使用者随意扩展能力。</p><p>贴一段使用POI解析Excel的代码，大家感受一下:</p><pre><code class="language-java">public void testReadUsersExcel() throws IOException {        // 指定excel文件，创建缓存输入流        BufferedInputStream inputStream = new BufferedInputStream(new FileInputStream(&quot;users.xlsx&quot;));        // 直接传入输入流即可，此时excel就已经解析了        XSSFWorkbook workbook = new XSSFWorkbook(inputStream);        // 选择要处理的sheet名称        XSSFSheet sheet = workbook.getSheet(&quot;my-sheet&quot;);        // 第一行表头，单独处理,迭代遍历sheet剩余的每一行        for (int rowNum = 0; rowNum &lt; sheet.getPhysicalNumberOfRows(); rowNum++) {            if (rowNum == 0) { // 读取第一行（表头）                XSSFRow head = sheet.getRow(rowNum);                String headColumn_1 = head.getCell(0).getStringCellValue();                String headColumn_2 = head.getCell(1).getStringCellValue();                String headColumn_3 = head.getCell(2).getStringCellValue();                String headColumn_4 = head.getCell(3).getStringCellValue();                String headStr = String.format(&quot;%s\t%s\t%s\t%s&quot;, headColumn_1, headColumn_2, headColumn_3, headColumn_4);                System.out.println(headStr);             } else { // 非表头（注意读取的时候要注意单元格内数据的格式，要使用正确的读取方法）                XSSFRow row = sheet.getRow(rowNum);                int id = (int) row.getCell(0).getNumericCellValue();                String name = row.getCell(1).getStringCellValue();                int age = (int) row.getCell(2).getNumericCellValue();                String addr = row.getCell(3).getStringCellValue();                String rowContent = String.format(&quot;%s\t%s\t%s\t%s&quot;, id, name, age, addr);                System.out.println(rowContent);            }        }        workbook.close();        inputStream.close();}</code></pre><p>概括一下作者写这个框架的可能原因如下：</p><ul><li><p>POI太难用：功能虽然齐全，但是太难用了。</p></li><li><p>POI扩展性不行：POI的扩展性也不太行，程序员想要在特定的生命周期里加点业务代码，很费劲，需要做很多遍历。</p></li><li><p>POI参数配置麻烦：使用者在使用的时候，需要像Spring框架那样创建一个非常大的Config对象，或者配置文件，太不友好。</p></li></ul><h2 id="读流程设计">读流程设计</h2><p>与原先的POI框架相比我需要解决三个问题</p><ul><li>API友好</li><li>内存使用率</li><li>业务扩展性</li></ul><h3 id="api友好">API友好</h3><p><strong>哎，POI最大的问题就是使用太麻烦了，想获取点数据不断for循环遍历！</strong></p><p>有了：</p><ul><li><p>构造流程，使用Builder设计模式，构造执行器对象。</p></li><li><p>业务流程，也通过Builder这种模式传入特定的扩展类，植入特定生命周期中，执行。</p></li></ul><p>这是之前看到的ZK使用时候的API交互，非常友好，将默认参数的设置自动设置掉，程序员只需要关注必要的参数的设置。</p><p>我可以学习一下这种模式。</p><p><img src="https://www.wangdaye.net/upload/2023/11/image-1e5ed45915a74f13856654dd47a72049.png" alt="image.png" /></p><blockquote><p>Builder设计模式的精髓，是将入参的填入与设置入参方法具体的业务含义分离开，让用户知道在做什么，但是不需要知道如何做的，做了哪些事，所以很多框架通常喜欢采用Builder设计模式，用户感知只调用了一次Build方法，却构造出了一个完整复杂的对象。</p></blockquote><p><strong>哎，使用起来还是不行，纯的builder设计好像不能从用户易用性角度给出使用者好的体验，因为参数太多了，他需要先从无数的参数中识别出那些他需要和不需要的，这该如何做？！</strong></p><p>有了：</p><ul><li><p>我可以将使用者对某个参数或者方法的的使用频次让其更接近使用者一点，换言之就是将不常用但是必不可少的参数设置放到抽象类，将常用的参数设置放到子类。</p></li><li><p>我可以多抽象几层，一个通用的抽象类，一个读流程抽象类，一个写流程抽象类，然后再往下读流程抽象类可以更具使用场景再实现几个具体子类，这些具体子类就是直接面向使用者的API。</p></li><li><p>然后使用一个工厂类，将所有的Builder子类封装为工厂方法统一提供API服务。</p></li></ul><p>如此一来，程序员最经常使用的API和入参均是对他们最友好的，一些不常用的参数和能力隐藏在最里面的抽象类。</p><pre><code>//最顶层Builder的抽象类com.alibaba.excel.metadata.AbstractParameterBuilder//writeBuilder抽象类，如下方法分别是：注册Handler方法com.alibaba.excel.write.builder.AbstractExcelWriterParameterBuildercom.alibaba.excel.write.builder.AbstractExcelWriterParameterBuilder#registerWriteHandler//readBuilder抽象类，如下俩个方法分别是：注册Listener实现类com.alibaba.excel.read.builder.AbstractExcelReaderParameterBuildercom.alibaba.excel.read.builder.AbstractExcelReaderParameterBuilder#registerReadListener// ReaderBuilder，如下的方法名称我不说你们应该也应该能能看得出来了com.alibaba.excel.read.builder.ExcelReaderBuildercom.alibaba.excel.read.builder.ExcelReaderBuilder#sheet()com.alibaba.excel.read.builder.ExcelReaderBuilder#doReadAll// WriteBuilder，如下的方法名称我不说你们也应该能看得出来了com.alibaba.excel.write.builder.ExcelWriterSheetBuildercom.alibaba.excel.write.builder.ExcelWriterSheetBuilder#sheetNocom.alibaba.excel.write.builder.ExcelWriterSheetBuilder#doWrite(java.util.Collection&lt;?&gt;)// 最后使用EasyExcelFactory，将如上Builder做统一入口的封装com.alibaba.excel.EasyExcelFactorycom.alibaba.excel.EasyExcelFactory#write(java.io.File)com.alibaba.excel.EasyExcelFactory#read(java.io.File)</code></pre><p>这样一来，越靠近使用者的API是他们最经常用的，我在命名和使用场景在这方面多下点功夫</p><p>最终API如下：</p><pre><code class="language-java">// 读流程List&lt;Map&lt;Integer, Object&gt;&gt; list = EasyExcel.read(TestFileUtil.getPath() + &quot;compatibility/t01.xls&quot;).sheet()            .doReadSync();// 写流程EasyExcel.write().file(file).head(AnnotationData.class).sheet().doWrite(dataStyle());</code></pre><p>UML图如下:</p><p><img src="https://www.wangdaye.net/upload/2023/12/288888-81981aedf8434cf2b9a27dc422c8237c.png" alt="288888.png" /></p><h3 id="内存使用率">内存使用率</h3><p><strong>POI框架经常被人诟病的是JVM内存溢出，以及内存使用率低</strong></p><blockquote><p>其实POI有俩种模式：usermodel，eventusermodel，默认使用的是usermodel，usermodel使用起来简单一些，但是它是一次性奖内容读取到JVM中，导致我们经常遇到的问题，而后者eventusermodel则是基于事件模式，一次读取一条数据，但是API复杂，但是它处理速度快，占用内存少。</p></blockquote><p>有了：</p><ul><li><p>我可以基于POI的eventusermodel模式，来对它进行封装抽象对使用者提供友好的API，这样内存OOM问题解决了。</p></li><li><p>使用Listener模式，读取具体数据时，触发Listener回调，这样内存里，只有当前使用的这条数据相关对象，其他对象都可以让JVM先回收掉。</p></li></ul><pre><code class="language-java">// 如下是POI框架的eventusermodel写法public void execute() {        XlsReadWorkbookHolder xlsReadWorkbookHolder = xlsReadContext.xlsReadWorkbookHolder();        MissingRecordAwareHSSFListener listener = new MissingRecordAwareHSSFListener(this);        xlsReadWorkbookHolder.setFormatTrackingHSSFListener(new FormatTrackingHSSFListener(listener));        EventWorkbookBuilder.SheetRecordCollectingListener workbookBuildingListener =            new EventWorkbookBuilder.SheetRecordCollectingListener(                xlsReadWorkbookHolder.getFormatTrackingHSSFListener());        xlsReadWorkbookHolder.setHssfWorkbook(workbookBuildingListener.getStubHSSFWorkbook());        HSSFEventFactory factory = new HSSFEventFactory();        HSSFRequest request = new HSSFRequest();        request.addListenerForAllRecords(xlsReadWorkbookHolder.getFormatTrackingHSSFListener());        try {            factory.processWorkbookEvents(request, xlsReadWorkbookHolder.getPoifsFileSystem());        } catch (IOException e) {            throw new ExcelAnalysisException(e);        }    }</code></pre><p><strong>哎，另一个读取性能问题，是由于反复创建临时对象，GC过于频繁导致的，这应该如何优化呢？</strong></p><p>有了：</p><ul><li><p>我可以使用一个Context对象，这个作为上下文，维护，管理整个执行流程中的可复用对象。</p></li><li><p>特殊业务中的复用象可以使用Map数据结构预加载。</p></li><li><p>POI是完全对Excel做的模型映射，有很多格式化的属性，在读取场景下是不需要的，我可以基于Excel设计一套精简的Bean，只包含数据，省去样式属性</p></li></ul><pre><code class="language-java">// xlsReadContext 是流程执行上下文，存储了流程执行中所有公共的复用对象。// XLS_RECORD_HANDLER_MAP 是一个业务工具的容器Map，采用预加载的方式加载。public class XlsSaxAnalyser implements HSSFListener, ExcelReadExecutor {    private final XlsReadContext xlsReadContext;    private static final Map&lt;Short, XlsRecordHandler&gt; XLS_RECORD_HANDLER_MAP = new HashMap&lt;Short, XlsRecordHandler&gt;(32);}</code></pre><p>自定义的ReadSheet，ReadWorkbook，ReadSheet类</p><pre><code class="language-java">// Read sheet, sheet类public class ReadSheet  {    private Integer sheetNo;    private String sheetName;}// 类似的还有如下classcom.alibaba.excel.read.metadata.ReadWorkbookcom.alibaba.excel.read.metadata.ReadSheet  </code></pre><p><strong>哎，执行读取写入逻辑需要大量判重key的逻辑，Map结构在数据量大的时候寻址速度好像下降比较厉害!</strong></p><p>有了：</p><ul><li>我可以现将Map的key转化为Set，需要判断Key是否存在，也基本使用Set，然后再进行接下来的业务</li></ul><pre><code class="language-java">// 由于Map Key方法的包含性能较差，所以在这里创建一个keySet Set&lt;String&gt; beanKeySet = new HashSet&lt;&gt;(beanMap.keySet());// 防止重复工作private Set&lt;Integer&gt; hasReadSheet;</code></pre><h3 id="业务扩展性">业务扩展性</h3><p>读数据和遍历语法树类似，都是访问某一块固定数据，我记得语法树遍历一般是用Vistor或者Listener设计模式，其中Listener无需设定返回值，灵活性更强一些，那我这边也用Listener设计模式来增加扩展性吧，这些用户实现的Listener，只要实现不同的生命周期接口，即可实现在特定实际触发特定类型的Listener实现类。</p><p>Listener的来源分为俩部分</p><ul><li>用户入参传入：一部分是由入参将实现类传入（使用者）</li><li>框架默认的Listener：比如ModelBuildEventListener（框架内）</li></ul><p>最终的所有的Listener类统一扭转到Holder的readListenerList属性中，事件的触发统一入口为:DefaultAnalysisEventProcessor，不同的生命周期触发不同函数</p><pre><code class="language-java">// 结束解析Sheet生命周期回调com.alibaba.excel.read.processor.DefaultAnalysisEventProcessor#endSheet//  触发所有readListener 的extra事件com.alibaba.excel.read.processor.DefaultAnalysisEventProcessor#dealExtra  </code></pre><p>如下就是实现之后的API使用</p><pre><code class="language-java"> EasyExcelFactory.read(file).registerReadListener(new ListHeadDataListener()).sheet().doRead();</code></pre><p><strong>哎，好像写法上还是差了一些，使用者每次都需要new 一个ListHeadDataListener.java文件，体验不太行！如果直接让程序员传入一个Lambda表达式就能够描述行为那就好了！</strong></p><p>有了：</p><ul><li>桥接设计模式+Consumer接口</li></ul><p>实现一个特殊的PageReadListener子类，该子类的其中一个属性是Consumer。</p><pre><code class="language-java">public class PageReadListener&lt;T&gt; implements ReadListener&lt;T&gt; {    // 消费者    private final Consumer&lt;List&lt;T&gt;&gt; consumer;    public PageReadListener(Consumer&lt;List&lt;T&gt;&gt; consumer) {        this(consumer, BATCH_COUNT);    }</code></pre><p>如此一来，使用者就可以用以下方式写业务代码了：</p><pre><code class="language-java">EasyExcelFactory.read(file07, CacheData.class, new PageReadListener&lt;DemoData&gt;(dataList -&gt; {                Assertions.assertNotNull(fieldThreadLocal.get());            })).sheet().doRead();</code></pre><h2 id="写流程设计">写流程设计</h2><p>对于写Excel流程，POI性能还好，就是API使用比较不友好，对于写方面，我的重心主要放在</p><ul><li>业务扩展性</li><li>API的友好</li></ul><p>这俩个方面就行了，API的设计思路，可以基于上面读取的Fluent思路，但是略有区别，写的流程与读不一样，要做的事也不一样，读流程只要单纯访问数据就可以了，但写流程，需要写格式，合并单元格，Excel样式编辑，表头设置，数据填充，等等，所以写流程这个动作扩展性需要考虑的点会更多。</p><h3 id="业务扩展性-1">业务扩展性</h3><p><strong>哎，怎么将写的业务的各个节点设计成方便拓展呢？！</strong></p><p>有了：</p><ul><li><p>使用责任链设计模式，将业务Handler实现类(扩展点)，依据不同的类型组装成不同的责任链，只需要在特定实际触发特定的责任链即可。</p></li><li><p>如果有新业务想增加新的生命周期时机，只需添加新的责任链即可。</p></li><li><p>使用统一的触发工具类，维护统一触发入口和触发执行逻辑。</p></li></ul><p>Handler是用来做数据处理的具体的实现类，数据处理工作和只是遍历数据的Listenner还是有区别的，前面Hander处理过后的结会直接对下面的Handler处理的数据产生影响。</p><p>所以它和Listener模式最大的区别是：<strong>直接影响原始数据</strong>，并且它对于生命周期的扩展性需要要高于Listenner模式。</p><p>它的来源也由俩部分组成</p><ul><li>用户入参传入：使用者传入入参。</li><li>框架默认的Handler：框架内部会有一些自生扩展实现，比如合并单元格，比如默认样式等等。</li></ul><p>同时在数据结构的处理上也不同，<strong>与Listener只会单纯的用一个List维护不同，Handler会依据不同的业务类型构造出不同类型的责任链</strong>，一个责任链对应于一个流程生命周期的时机，通过触发不同的责任链，实现触发不同时机的拓展，同时也方便生命周期的拓展。</p><p>如下不同生命周期的责任链:</p><pre><code class="language-java">// 如下是各个生命周期的责任链public WorkbookHandlerExecutionChain ownWorkbookHandlerExecutionChain;public SheetHandlerExecutionChain ownSheetHandlerExecutionChain;public WorkbookHandlerExecutionChain workbookHandlerExecutionChain;public SheetHandlerExecutionChain sheetHandlerExecutionChain;public RowHandlerExecutionChain rowHandlerExecutionChain;public CellHandlerExecutionChain cellHandlerExecutionChain;</code></pre><p>统一的触发入口：</p><pre><code class="language-java">com.alibaba.excel.util.WriteHandlerUtils// 创建Workbook之前com.alibaba.excel.util.WriteHandlerUtils#beforeWorkbookCreate(com.alibaba.excel.write.handler.context.WorkbookWriteHandlerContext)  // 创建Workbook之后  com.alibaba.excel.util.WriteHandlerUtils#afterWorkbookCreate(com.alibaba.excel.write.handler.context.WorkbookWriteHandlerContext)// 创建单元格之前  com.alibaba.excel.util.WriteHandlerUtils#beforeCellCreate // 创建单元格之后  com.alibaba.excel.util.WriteHandlerUtils#afterCellCreate  </code></pre><p>使用样例如下：</p><pre><code class="language-java">// 执行写入代码private void workbookWrite(File file) {        WriteHandler writeHandler = new WriteHandler();        EasyExcel.write(file).head(WriteHandlerData.class).registerWriteHandler(writeHandler).sheet().doWrite(data());        writeHandler.afterAll();}    </code></pre><h2 id="多种excel格式解析设计">多种Excel格式解析设计</h2><p><strong>哎，要读写的版本太多了，Excel版本有03，07版，还有CSV文件，不管在读还是写，都需要做特定的逻辑区分，以及为以后留足扩展空间，该如何做?</strong></p><p>有了：使用桥接模式，但需要抽象俩层</p><ul><li><p>解析逻辑所需要事情。</p></li><li><p>解析文件所需要做的事情。</p></li></ul><p>这俩者看起来相似，实则不同，第一个是解析器要做的事，第二个是解析一个文件要做的事。</p><pre><code class="language-java">// 如下分别对应如上俩个逻辑com.alibaba.excel.analysis.ExcelAnalysercom.alibaba.excel.analysis.ExcelReadExecutor</code></pre><pre><code class="language-java">/** * Excel file Executor * excel 文件执行器 * @author Jiaju Zhuang */public interface ExcelReadExecutor {    List&lt;ReadSheet&gt; sheetList();    void execute();}// 03,07,CSV版本分别实现自己的实现类com.alibaba.excel.analysis.v03.XlsSaxAnalyser com.alibaba.excel.analysis.v07.XlsxSaxAnalysercom.alibaba.excel.analysis.csv.CsvExcelReadExecutor</code></pre><p><strong>使用桥接思想</strong>，使文件执行器实现类织入ExcelAnalyser执行器</p><pre><code class="language-java">/** * @author jipengfei */public class ExcelAnalyserImpl implements ExcelAnalyser {    private static final Logger LOGGER = LoggerFactory.getLogger(ExcelAnalyserImpl.class);    private AnalysisContext analysisContext;    private ExcelReadExecutor excelReadExecutor;;}</code></pre><h2 id="数据类型转换设计">数据类型转换设计</h2><p><strong>哎，原先的PIO的JAVA类型太固定了，如果Excel表希望与JavaBean能一一对应起来，不管是写还是读，都以JavaBean的形式返回给程序员就好了！</strong></p><p>有了：</p><ul><li>我只需要给JavaBean上的具体属性通过注解打上标记。</li><li>在加载Bean的时机，使用注解来作为渲染参数寻找特定的渲染转化器，完成转化逻辑。</li></ul><pre><code class="language-java">// 类型转化器抽象接口com.alibaba.excel.converters.Converter// 转化器实现子类com.alibaba.excel.converters.bigdecimal.BigDecimalBooleanConvertercom.alibaba.excel.converters.bigdecimal.BigDecimalNumberConvertercom.alibaba.excel.converters.bigdecimal.BigDecimalStringConvertercom.alibaba.excel.converters.biginteger.BigIntegerBooleanConvertercom.alibaba.excel.converters.biginteger.BigIntegerNumberConverter// 转化器加载器，将所有的Converter，为了提升性能，在类加载阶段全部加载到内存Mapcom.alibaba.excel.converters.DefaultConverterLoader  // 转化器工具包（封装转化逻辑）com.alibaba.excel.util.ConverterUtils// 基础注解com.alibaba.excel.metadata.property.ExcelContentProperty com.alibaba.excel.annotation.ExcelProperty    </code></pre><pre><code class="language-java">// 如下为注解的关键属性@Target(ElementType.FIELD)@Retention(RetentionPolicy.RUNTIME)@Inheritedpublic @interface ExcelProperty {... 省略其他属性    // 返回转换器，定义一个默认值为 AutoConverter.class 的类作为注解的属性，用于强制当前字段使用这个转换器    Class&lt;? extends Converter&lt;?&gt;&gt; converter() default AutoConverter.class;   ...}</code></pre><p>UML图如下</p><p><img src="https://www.wangdaye.net/upload/2023/11/mermaid-diagram-2023-11-19-161642-1e2ea58f76c740efa7ef8f94173f9922.svg" alt="mermaiddiagram20231119161642.svg" /></p><h2 id="资源操作设计">资源操作设计</h2><p><strong>哎，我的输入源读取源，可能来自不同的渠道，有可能是文件，可能是网络资源，这些资源如果没有办法统一入口的话，我在内部逻辑封装会打折扣的，怎么办呢？</strong></p><p>有了：</p><ul><li>设计一个套接口命名为Holder，所有的资源读写，获取资源状态等操作都基于这个接口。</li><li>根据不同的操作需求将Holder派生出各种子类，WriteHolder，ReadHolder，这些子类根据对资源不同的使用场景场景按需实例化。</li><li>所有Holder是同一个统一配置资源入口，即用户在初始化时候只需要初始化该Holder的实例，根据不同的场景使用它作为构造函数参数去初始化其他类型的Holder。</li></ul><p>如此一来，对资源的读写和获取资源状态等属性，在我的代码中，可以统一使用接口来编写统一逻辑，也方便后续资源类型的拓展。</p><pre><code>// 这三个是最底层的Holder抽象类com.alibaba.excel.metadata.Holdercom.alibaba.excel.metadata.ConfigurationHoldercom.alibaba.excel.metadata.AbstractHolder//如下俩个是继承AbstractHolder的读写操作Holdercom.alibaba.excel.write.metadata.holder.AbstractWriteHoldercom.alibaba.excel.read.metadata.holder.AbstractReadHolder// 如下是是继承AbstractReadHolder的不同使用场景的读Holder（写Holder类似省略）com.alibaba.excel.read.metadata.holder.ReadSheetHoldercom.alibaba.excel.read.metadata.holder.ReadWorkbookHolder// 如下是继承ReadSheetHolder，ReadWorkbookHolder，各种不同类型文件的Holder的具体实现com.alibaba.excel.read.metadata.holder.csv.CsvReadSheetHoldercom.alibaba.excel.read.metadata.holder.xls.XlsReadSheetHoldercom.alibaba.excel.read.metadata.holder.xlsx.XlsxReadSheetHoldercom.alibaba.excel.read.metadata.holder.csv.CsvReadWorkbookHoldercom.alibaba.excel.read.metadata.holder.xls.XlsReadWorkbookHoldercom.alibaba.excel.read.metadata.holder.xlsx.XlsxReadWorkbookHolder</code></pre><p>UML图如下:</p><p><img src="https://www.wangdaye.net/upload/2023/12/288888-bcb194bdff77486b9203a148f5ecfa6f.svg" alt="288888.svg" /></p><h2 id="生命周期扩展设计">生命周期扩展设计</h2><p><strong>哎，代码用了这么多的设计，拆的太散了，这么散的代码，如何对特定的时机，进行统一生命周期的触发呢？！</strong></p><p>有了：</p><ul><li><p>所有的资源访问，都经过一层<strong>Holder（资源包裹对象）</strong> ，这样可以在资源操作(读，写)细节的前后进行一些特殊前置和后置的操作。</p></li><li><p>我可以将所有的需要回调的对象，维护在之前约定的全局上下文（Context）中，在任意地方的代码想要获取到这些Listener，Handler，都可以方便获取。</p></li><li><p>这些触发Listener,Handler的逻辑可以统一维护成一个 **流程执行器，实现这部分代码的复用，因为还涉及到异常处理，责任链节点后移等等操作。</p></li></ul><blockquote><p>其实，流程执行器+逻辑拓展实现（Handler+Listener）= 模板方法设计模式，只不过将传统模板方法设计模式中的通用业务逻辑，使用流程执行器来维护了而已，扩展灵活性更强一些。</p></blockquote><pre><code class="language-java">// 上下文抽象接口com.alibaba.excel.context.AnalysisContext// 上下文实现类com.alibaba.excel.context.AnalysisContextImpl// holder为资源访问器com.alibaba.excel.metadata.ConfigurationHoldercom.alibaba.excel.read.metadata.holder.ReadWorkbookHoldercom.alibaba.excel.read.metadata.holder.ReadSheetHoldercom.alibaba.excel.read.metadata.holder.ReadRowHoldercom.alibaba.excel.read.metadata.holder.ReadHolder  //生命周期流程执行器com.alibaba.excel.read.processor.AnalysisEventProcessor  </code></pre><p>UML图如下：</p><p><img src="https://www.wangdaye.net/upload/2023/11/mermaid-diagram-2023-11-19-151316-3a6e0ef4a40f4a9ebd8b1325eb1e9d38.svg" alt="mermaiddiagram20231119151316.svg" /></p><h2 id="关于框架命名">关于框架命名</h2><p><strong>哎：这个名字EasyExcelFactory，好像怪怪的，对使用者不太友好，这么怪的名字也不利于框架的传播，但是这个类的功能确实是工厂，直接改名也不太好！</strong></p><p>有了:</p><ul><li>实现一下子类叫EasyExcel，继承EasyExcelFactory</li></ul><pre><code class="language-java">//  @author jipengfeipublic class EasyExcel extends EasyExcelFactory {}</code></pre>]]>
                </content>
            </entry>
            <entry>
                <title><![CDATA[提问的智慧（转载）]]></title>
                <link rel="alternate" type="text/html" href="https://www.wangdaye.net/archives/ti-wen-de-zhi-hui" />
                <id>tag:https://www.wangdaye.net,2023-10-29:ti-wen-de-zhi-hui</id>
                <published>2023-10-29T16:32:35+08:00</published>
                <updated>2023-10-29T17:32:49+08:00</updated>
                <author>
                    <name>王大爷</name>
                    <uri>https://www.wangdaye.net</uri>
                </author>
                <content type="html">
                        <![CDATA[<h1 id="提问的智慧">提问的智慧</h1><p><strong>How To Ask Questions The Smart Way</strong></p><p>原文网址：<a href="http://www.catb.org/~esr/faqs/smart-questions.html">http://www.catb.org/~esr/faqs/smart-questions.html</a></p><h2 id="原文版本历史"><a href="history.md">原文版本历史</a></h2><h2 id="目录">目录</h2><ul><li><a href="#声明">声明</a></li><li><a href="#简介">简介</a></li><li><a href="#在提问之前">在提问之前</a></li><li><a href="#当你提问时">当你提问时</a><ul><li><a href="#慎选提问的论坛">慎选提问的论坛</a></li><li><a href="#stack-overflow">Stack Overflow</a></li><li><a href="#网站和-irc-论坛">网站和 IRC 论坛</a></li><li><a href="#第二步使用项目邮件列表">第二步，使用项目邮件列表</a></li><li><a href="#使用有意义且描述明确的标题">使用有意义且描述明确的标题</a></li><li><a href="#使问题容易回复">使问题容易回复</a></li><li><a href="#使用清晰、正确、精准且合乎语法的语句">使用清晰、正确、精准且合乎语法的语句</a></li><li><a href="#使用易于读取且标准的文件格式发送问题">使用易于读取且标准的文件格式发送问题</a></li><li><a href="#精确地描述问题并言之有物">精确地描述问题并言之有物</a></li><li><a href="#话不在多而在精">话不在多而在精</a></li><li><a href="#别动辄声称找到-bug">别动辄声称找到 Bug</a></li><li><a href="#低声下气不能代替你的功课">低声下气不能代替你的功课</a></li><li><a href="#描述问题症状而非你的猜测">描述问题症状而非你的猜测</a></li><li><a href="#按发生时间先后列出问题症状">按发生时间先后列出问题症状</a></li><li><a href="#描述目标而不是过程">描述目标而不是过程</a></li><li><a href="#别要求使用私人电邮回复">别要求使用私人电邮回复</a></li><li><a href="#清楚明确地表达你的问题以及需求">清楚明确地表达你的问题以及需求</a></li><li><a href="#询问有关代码的问题时">询问有关代码的问题时</a></li><li><a href="#别把自己家庭作业的问题贴上来">别把自己家庭作业的问题贴上来</a></li><li><a href="#去掉无意义的提问句">去掉无意义的提问句</a></li><li><a href="#即使你很急也不要在标题写紧急">即使你很急也不要在标题写<code>紧急</code></a></li><li><a href="#礼多人不怪而且有时还很有帮助">礼多人不怪，而且有时还很有帮助</a></li><li><a href="#问题解决后加个简短的补充说明">问题解决后，加个简短的补充说明</a></li></ul></li><li><a href="#如何解读答案">如何解读答案</a><ul><li><a href="#rtfm-和-stfw如何知道你已完全搞砸了">RTFM 和 STFW：如何知道你已完全搞砸了</a></li><li><a href="#如果还是搞不懂">如果还是搞不懂</a></li><li><a href="#处理无礼的回应">处理无礼的回应</a></li></ul></li><li><a href="#如何避免扮演失败者">如何避免扮演失败者</a></li><li><a href="#不该问的问题">不该问的问题</a></li><li><a href="#好问题与蠢问题">好问题与蠢问题</a></li><li><a href="#如果得不到回答">如果得不到回答</a></li><li><a href="#如何更好地回答问题">如何更好地回答问题</a></li><li><a href="#相关资源">相关资源</a></li><li><a href="#鸣谢">鸣谢</a></li></ul><h2 id="声明">声明</h2><p>许多项目在他们网站的帮助文档中链接了本指南。这很好，这正是我们想要的用途。但如果你是该项目管理员并试图创建指向本指南的超链接，请在超链接附近的显著位置注明：</p><p><strong>本指南不提供此项目的实际支持服务！</strong></p><p>我们已经深刻领教到缺少上述声明所带来的痛苦：我们将不停地被那些认为发布这本指南就意味着有责任解决世上所有技术问题的傻瓜苦苦纠缠。</p><p>如果你因寻求某些帮助而阅读本指南，并在离开时还觉得可以从本文作者这里得到直接帮助，那你就是我们之前说的那些傻瓜之一。别问我们问题，我们只会忽略你。我们在这本指南中想教你如何从那些真正懂得你所遇到的软件或硬件问题的人处取得协助，而 99% 的情况下那不会是我们。除非你确定本指南的作者之一刚好是你所遇到的问题领域的专家，否则请不要打扰我们，这样大家都会开心一点。</p><h2 id="简介">简介</h2><p>在<a href="http://www.catb.org/~esr/faqs/hacker-howto.html">黑客</a>的世界里，当你拋出一个技术问题时，最终是否能得到有用的回答，往往取决于你所提问和追问的方式。本指南将教你如何正确地提问以获得你满意的答案。</p><p>现在开源（Open Source）软件已经相当盛行，您通常可以从其他更有经验的用户那里获得与黑客一样好的答案，这是件<strong>好事</strong>；和黑客相比，用户们往往对那些新手常遇到的问题更宽容一些。尽管如此，以我们在此推荐的方式对待这些有经验的用户通常也是从他们那里获得有用答案的最有效方式。</p><p>首先你应该明白，黑客们喜爱有挑战性的问题，或者能激发他们思维的好问题。如果我们并非如此，那我们也不会成为你想询问的对象。如果你给了我们一个值得反复咀嚼玩味的好问题，我们自会对你感激不尽。好问题是激励，是厚礼。好问题可以提高我们的理解力，而且通常会暴露我们以前从没意识到或者思考过的问题。对黑客而言，“好问题！”是诚挚的大力称赞。</p><p>尽管如此，黑客们有着蔑视或傲慢面对简单问题的坏名声，这有时让我们看起来对新手、无知者似乎较有敌意，但其实不是那样的。</p><p>我们不讳言我们对那些不愿思考、或者在发问前不做他们该做的事的人的蔑视。那些人是时间杀手 —— 他们只想索取，从不付出，消耗我们可用在更有趣的问题或更值得回答的人身上的时间。我们称这样的人为 <code>失败者（撸瑟）</code> （由于历史原因，我们有时把它拼作 <code>lusers</code>）。</p><p>我们意识到许多人只是想使用我们写的软件，他们对学习技术细节没有兴趣。对大多数人而言，电脑只是种工具，是种达到目的的手段而已。他们有自己的生活并且有更要紧的事要做。我们认可这点，也从不指望每个人都对这些让我们着迷的技术问题感兴趣。尽管如此，我们只为那些真正有兴趣并愿意积极参与问题解决的人调整回答问题的风格。这点不会变，也不该变：否则，我们就是在最擅长的事情上降低效率。</p><p>我们（在很大程度上）是自愿的，从繁忙的生活中抽出时间来解答疑惑，而且时常被提问淹没。所以我们无情地滤掉一些话题，特别是拋弃那些看起来像失败者的家伙，以便更高效地利用时间来回答<code>赢家（winner）</code>的问题。</p><p>如果你厌恶我们的态度，高高在上，或过于傲慢，不妨也设身处地想想。我们并没有要求你向我们屈服 —— 事实上，我们大多数人非常乐意与你平等地交流，只要你付出小小努力来满足基本要求，我们就会欢迎你加入我们的文化。但让我们帮助那些不愿意帮助自己的人是没有效率的。无知没有关系，但装白痴就是不行。</p><p>所以，你不必在技术上很在行才能吸引我们的注意，但你必须表现出能引导你变得在行的特质 —— 机敏、有想法、善于观察、乐于主动参与解决问题。如果你做不到这些使你与众不同的事情，我们建议你花点钱找家商业公司签个技术支持服务合同，而不是要求黑客个人无偿地帮助你。</p><p>如果你决定向我们求助，当然你也不希望被视为失败者，更不愿成为失败者中的一员。能立刻得到快速并有效答案的最好方法，就是像赢家那样提问 —— 聪明、自信、有解决问题的思路，只是偶尔在特定的问题上需要获得一点帮助。</p><p>（欢迎对本指南提出改进意见。你可以把你的建议发送至 <a href="esr@thyrsus.com">esr@thyrsus.com</a> 或 <a href="respond-auto@linuxmafia.com">respond-auto@linuxmafia.com</a>。然而请注意，本文并非<a href="http://www.ietf.org/rfc/rfc1855.txt">网络礼节</a>的通用指南，而我们通常会拒绝无助于在技术论坛得到有用答案的建议）。</p><h2 id="在提问之前">在提问之前</h2><p>在你准备要通过电子邮件、新闻群组或者聊天室提出技术问题前，请先做到以下事情：</p><ol><li>尝试在你准备提问的论坛的旧文章中搜索答案。</li><li>尝试上网搜索以找到答案。</li><li>尝试阅读手册以找到答案。</li><li>尝试阅读常见问题文件（FAQ）以找到答案。</li><li>尝试自己检查或试验以找到答案。</li><li>向你身边的强者朋友打听以找到答案。</li><li>如果你是程序开发者，请尝试阅读源代码以找到答案。</li></ol><p>当你提出问题的时候，请先表明你已经做了上述的努力；这将有助于树立你并不是一个不劳而获且浪费别人的时间的提问者。如果你能一并表达在做了上述努力的过程中所<strong>学到</strong>的东西会更好，因为我们更乐于回答那些表现出能从答案中学习的人的问题。</p><p>运用某些策略，比如先用 Google 搜索你所遇到的各种错误信息（搜索 <a href="http://groups.google.com/">Google 论坛</a>和网页），这样很可能直接就找到了能解决问题的文件或邮件列表线索。即使没有结果，在邮件列表或新闻组寻求帮助时加上一句 <code>我在 Google 中搜过下列句子但没有找到什么有用的东西</code> 也是件好事，即使它只是表明了搜索引擎不能提供哪些帮助。这么做（加上搜索过的字串）也让遇到相似问题的其他人能被搜索引擎引导到你的提问来。</p><p>别着急，不要指望几秒钟的 Google 搜索就能解决一个复杂的问题。在向专家求助之前，再阅读一下常见问题文件（FAQ）、放轻松、坐得舒服一些，再花点时间思考一下这个问题。相信我们，他们能从你的提问看出你做了多少阅读与思考，如果你是有备而来，将更有可能得到解答。不要将所有问题一股脑拋出，只因你的第一次搜索没有找到答案（或者找到太多答案）。</p><p>准备好你的问题，再将问题仔细地思考过一遍，因为草率的发问只能得到草率的回答，或者根本得不到任何答案。越是能表现出在寻求帮助前你为解决问题所付出的努力，你越有可能得到实质性的帮助。</p><p>小心别问错了问题。如果你的问题基于错误的假设，某个普通黑客（J. Random Hacker）多半会一边在心里想着<code>蠢问题…</code>，一边用无意义的字面解释来答复你，希望着你会从问题的回答（而非你想得到的答案）中汲取教训。</p><p>绝不要自以为<strong>够格</strong>得到答案，你没有；你并没有。毕竟你没有为这种服务支付任何报酬。你将会是自己去<strong>挣到</strong>一个答案，靠提出有内涵的、有趣的、有思维激励作用的问题 —— 一个有潜力能贡献社区经验的问题，而不仅仅是被动地从他人处索取知识。</p><p>另一方面，表明你愿意在找答案的过程中做点什么是一个非常好的开端。<code>谁能给点提示？</code>、<code>我的这个例子里缺了什么？</code>以及<code>我应该检查什么地方</code>比<code>请把我需要的确切的过程贴出来</code>更容易得到答复。因为你表现出只要有人能指个正确方向，你就有完成它的能力和决心。</p><h2 id="当你提问时">当你提问时</h2><h3 id="慎选提问的论坛">慎选提问的论坛</h3><p>小心选择你要提问的场合。如果你做了下述的事情，你很可能被忽略掉或者被看作失败者：</p><ul><li>在与主题不合的论坛上贴出你的问题。</li><li>在探讨进阶技术问题的论坛张贴非常初级的问题；反之亦然。</li><li>在太多的不同新闻群组上重复转贴同样的问题（cross-post）。</li><li>向既非熟人也没有义务解决你问题的人发送私人电邮。</li></ul><p>黑客会剔除掉那些搞错场合的问题，以保护他们沟通的渠道不被无关的东西淹没。你不会想让这种事发生在自己身上的。</p><p>因此，第一步是找到对的论坛。再说一次，Google 和其它搜索引擎还是你的朋友，用它们来找到与你遭遇到困难的软硬件问题最相关的网站。通常那儿都有常见问题（FAQ）、邮件列表及相关说明文件的链接。如果你的努力（包括<strong>阅读</strong> FAQ）都没有结果，网站上也许还有报告 Bug（Bug-reporting）的流程或链接，如果是这样，链过去看看。</p><p>向陌生的人或论坛发送邮件最可能是风险最大的事情。举例来说，别假设一个提供丰富内容的网页的作者会想充当你的免费顾问。不要对你的问题是否会受到欢迎做太乐观的估计 —— 如果你不确定，那就向别处发送，或者压根别发。</p><p>在选择论坛、新闻群组或邮件列表时，别太相信它的名字，先看看 FAQ 或者许可书以弄清楚你的问题是否切题。发文前先翻翻已有的话题，这样可以让你感受一下那里的文化。事实上，事先在新闻组或邮件列表的历史记录中搜索与你问题相关的关键词是个极好的主意，也许这样就找到答案了。即使没有，也能帮助你归纳出更好的问题。</p><p>别像机关枪似的一次“扫射”所有的帮助渠道，这就像大喊大叫一样会使人不快。要一个一个地来。</p><p>搞清楚你的主题！最典型的错误之一是在某种致力于跨平台可移植的语言、套件或工具的论坛中提关于 Unix 或 Windows 操作系统程序界面的问题。如果你不明白为什么这是大错，最好在搞清楚这之间差异之前什么也别问。</p><p>一般来说，在仔细挑选的公共论坛中提问，会比在私有论坛中提同样的问题更容易得到有用的回答。有几个理由可以支持这点，一是看潜在的回复者有多少，二是看观众有多少。黑客较愿意回答那些能帮助到许多人的问题。</p><p>可以理解的是，老练的黑客和一些热门软件的作者正在接受过多的错发信息。就像那根最后压垮骆驼背的稻草一样，你的加入也有可能使情况走向极端 —— 已经好几次了，一些热门软件的作者由于涌入其私人邮箱的大量不堪忍受的无用邮件而不再提供支持。</p><h3 id="stack-overflow">Stack Overflow</h3><p>搜索，<em>然后</em>在 Stack Exchange 问。</p><p>近年来，Stack Exchange 社区已经成为回答技术及其他问题的主要渠道，尤其是那些开放源码的项目。</p><p>因为 Google 索引是即时的，在看 Stack Exchange 之前先在 Google 搜索。有很高的几率某人已经问了一个类似的问题，而且 Stack Exchange 网站们往往会是搜索结果中最前面几个。如果你在 Google 上没有找到任何答案，你再到特定相关主题的网站去找。用标签（Tag）搜索能让你更缩小你的搜索结果。</p><p>如果你还是找不到任何对你的问题有用的内容，请把你的问题发在与它最相关的网站上。提问的时候请善用格式化工具，尤其注意为代码添加格式，并且添加相关的标签（特别是编程语言、操作系统或库/包的名称）。当有人要求你提供更多相关信息时，请编辑你的贴子来补充它们[译注：而不是发一个回帖或回答！]。如果你觉得一个答案对你有帮助，点击向上的箭头来为它投票；如果一个答案提供了问题的正确解决方案，点击投票按钮下方的对勾来将它标记为正解。</p><p>Stack Exchange 已经成长到<a href="https://stackexchange.com/sites">超过一百个网站</a>，以下是最常用的几个站：</p><ul><li>Super User 是问一些通用的电脑问题，如果你的问题跟代码或是写程序无关，只是一些网络连线之类的，请到这里。</li><li>Stack Overflow 是问写程序有关的问题。</li><li>Server Fault 是问服务器和网管相关的问题。</li></ul><h3 id="网站和-irc-论坛">网站和 IRC 论坛</h3><p>本地的用户群组（user group），或者你所用的 Linux 发行版本也许正在宣传他们的网页论坛或 IRC 频道，并提供新手帮助（在一些非英语国家，新手论坛很可能还是邮件列表），这些都是开始提问的好地方，特别是当你觉得遇到的也许只是相对简单或者很普通的问题时。有广告赞助的 IRC 频道是公开欢迎提问的地方，通常可以即时得到回应。</p><p>事实上，如果程序出的问题只发生在特定 Linux 发行版提供的版本（这很常见），最好先去该发行版的论坛或邮件列表中提问，再到程序本身的论坛或邮件列表提问。（否则）该项目的黑客可能仅仅回复“使用<strong>我们的</strong>版本”。</p><p>在任何论坛发文以前，先确认一下有没有搜索功能。如果有，就试着搜索一下问题的几个关键词，也许这会有帮助。如果在此之前你已做过通用的网页搜索（你也该这样做），还是再搜索一下论坛，搜索引擎有可能没来得及索引此论坛的全部内容。</p><p>通过论坛或 IRC 频道来提供用户支持服务有增长的趋势，电子邮件则大多为项目开发者间的交流而保留。所以最好先在论坛或 IRC 中寻求与该项目相关的协助。</p><p>在使用 IRC 的时候，首先最好不要发布很长的问题描述，有些人称之为频道洪水。最好通过一句话的问题描述来开始聊天。</p><h3 id="第二步使用项目邮件列表">第二步，使用项目邮件列表</h3><p>当某个项目提供开发者邮件列表时，要向列表而不是其中的个别成员提问，即使你确信他能最好地回答你的问题。查一查项目的文件和首页，找到项目的邮件列表并使用它。有几个很好的理由支持我们采用这种办法：</p><ul><li>任何好到需要向个别开发者提出的问题，也将对整个项目群组有益。反之，如果你认为自己的问题对整个项目群组来说太愚蠢，那这也不能成为骚扰个别开发者的理由。</li><li>向列表提问可以分散开发者的负担，个别开发者（尤其是项目领导人）也许太忙以至于没法回答你的问题。</li><li>大多数邮件列表都会被存档，那些被存档的内容将被搜索引擎索引。如果你向列表提问并得到解答，将来其他人可以通过网页搜索找到你的问题和答案，也就不用再次发问了。</li><li>如果某些问题经常被问到，开发者可以利用此信息来改进说明文件或软件本身，以使其更清楚。如果只是私下提问，就没有人能看到最常见问题的完整场景。</li></ul><p>如果一个项目既有“用户”也有“开发者”（或“黑客”）邮件列表或论坛，而你又不会动到那些源代码，那么就向“用户”列表或论坛提问。不要假设自己会在开发者列表中受到欢迎，那些人多半会将你的提问视为干扰他们开发的噪音。</p><p>然而，如果你<strong>确信</strong>你的问题很特别，而且在“用户”列表或论坛中几天都没有回复，可以试试前往“开发者”列表或论坛发问。建议你在张贴前最好先暗地里观察几天以了解那里的行事方式（事实上这是参与任何私有或半私有列表的好主意）</p><p>如果你找不到一个项目的邮件列表，而只能查到项目维护者的电子邮件地址，尽管向他发信。即使是在这种情况下，也别假设（项目）邮件列表不存在。在你的电子邮件中，请陈述你已经试过但没有找到合适的邮件列表，也提及你不反对将自己的邮件转发给他人（许多人认为，即使没什么秘密，私人电子邮件也不应该被公开。通过允许将你的电子邮件转发他人，你给了相应人员处置你邮件的选择）。</p><h3 id="使用有意义且描述明确的标题">使用有意义且描述明确的标题</h3><p>在邮件列表、新闻群组或论坛中，大约 50 字以内的标题是抓住资深专家注意力的好机会。别用喋喋不休的<code>帮帮忙</code>、<code>跪求</code>、<code>急</code>（更别说<code>救命啊！！！！</code>这样让人反感的话，用这种标题会被条件反射式地忽略）来浪费这个机会。不要妄想用你的痛苦程度来打动我们，而应该是在这点空间中使用极简单扼要的描述方式来提出问题。</p><p>一个好标题范例是<code>目标 —— 差异</code>式的描述，许多技术支持组织就是这样做的。在<code>目标</code>部分指出是哪一个或哪一组东西有问题，在<code>差异</code>部分则描述与期望的行为不一致的地方。</p><blockquote><p>蠢问题：救命啊！我的笔记本电脑不能正常显示了！</p></blockquote><blockquote><p>聪明问题：X.org 6.8.1 的鼠标指针会变形，某牌显卡 MV1005 芯片组。</p></blockquote><blockquote><p>更聪明问题：X.org 6.8.1 的鼠标指针，在某牌显卡 MV1005 芯片组环境下 - 会变形。</p></blockquote><p>编写<code>目标 —— 差异</code> 式描述的过程有助于你组织对问题的细致思考。是什么被影响了？ 仅仅是鼠标指针或者还有其它图形？只在 X.org 的 X 版中出现？或只是出现在 6.8.1 版中？ 是针对某牌显卡芯片组？或者只是其中的 MV1005 型号？ 一个黑客只需瞄一眼就能够立即明白你的环境<strong>和</strong>你遇到的问题。</p><p>总而言之，请想像一下你正在一个只显示标题的存档讨论串（Thread）索引中查寻。让你的标题更好地反映问题，可使下一个搜索类似问题的人能够关注这个讨论串，而不用再次提问相同的问题。</p><p>如果你想在回复中提出问题，记得要修改内容标题，以表明你是在问一个问题， 一个看起来像 <code>Re: 测试</code> 或者 <code>Re: 新 bug</code> 的标题很难引起足够重视。另外，在不影响连贯性之下，适当引用并删减前文的内容，能给新来的读者留下线索。</p><p>对于讨论串，不要直接点击回复来开始一个全新的讨论串，这将限制你的观众。因为有些邮件阅读程序，比如 mutt ，允许用户按讨论串排序并通过折叠讨论串来隐藏消息，这样做的人永远看不到你发的消息。</p><p>仅仅改变标题还不够。mutt 和其它一些邮件阅读程序还会检查邮件标题以外的其它信息，以便为其指定讨论串。所以宁可发一个全新的邮件。</p><p>在网页论坛上，好的提问方式稍有不同，因为讨论串与特定的信息紧密结合，并且通常在讨论串外就看不到里面的内容，故通过回复提问，而非改变标题是可接受的。不是所有论坛都允许在回复中出现分离的标题，而且这样做了基本上没有人会去看。不过，通过回复提问，这本身就是暧昧的做法，因为它们只会被正在查看该标题的人读到。所以，除非你<strong>只想</strong>在该讨论串当前活跃的人群中提问，不然还是另起炉灶比较好。</p><h3 id="使问题容易回复">使问题容易回复</h3><p>以<code>请将你的回复发送到……</code>来结束你的问题多半会使你得不到回答。如果你觉得花几秒钟在邮件客户端设置一下回复地址都麻烦，我们也觉得花几秒钟思考你的问题更麻烦。如果你的邮件程序不支持这样做，<a href="http://linuxmafia.com/faq/Mail/muas.html">换个好点的</a>；如果是操作系统不支持这种邮件程序，也换个好点的。</p><p>在论坛，要求通过电子邮件回复是非常无礼的，除非你认为回复的信息可能比较敏感（有人会为了某些未知的原因，只让你而不是整个论坛知道答案）。如果你只是想在有人回复讨论串时得到电子邮件提醒，可以要求网页论坛发送给你。几乎所有论坛都支持诸如<code>追踪此讨论串</code>、<code>有回复时发送邮件提醒</code>等功能。</p><h3 id="使用清晰正确精准且合乎语法的语句"><a name="使用清晰、正确、精准且合乎语法的语句">使用清晰、正确、精准且合乎语法的语句</a></h3><p>我们从经验中发现，粗心的提问者通常也会粗心地写程序与思考（我敢打包票）。回答粗心大意者的问题很不值得，我们宁愿把时间耗在别处。</p><p>正确的拼写、标点符号和大小写是很重要的。一般来说，如果你觉得这样做很麻烦，不想在乎这些，那我们也觉得麻烦，不想在乎你的提问。花点额外的精力斟酌一下字句，用不着太僵硬与正式 —— 事实上，黑客文化很看重能准确地使用非正式、俚语和幽默的语句。但它<strong>必须很</strong>准确，而且有迹象表明你是在思考和关注问题。</p><p>正确地拼写、使用标点和大小写，不要将<code>its</code>混淆为<code>it's</code>，<code>loose</code>搞成<code>lose</code>或者将<code>discrete</code>弄成<code>discreet</code>。不要<strong>全部用大写</strong>，这会被视为无礼的大声嚷嚷（全部小写也好不到哪去，因为不易阅读。<a href="http://en.wikipedia.org/wiki/Alan_Cox">Alan Cox</a> 也许可以这样做，但你不行）。</p><p>更白话的说，如果你写得像是个半文盲[译注：<a href="http://zh.wikipedia.org/wiki/小白">小白</a>]，那多半得不到理睬。也不要使用即时通信中的简写或<a href="http://zh.wikipedia.org/wiki/火星文">火星文</a>，如将<code>的</code>简化为<code>d</code>会使你看起来像一个为了少打几个键而省字的小白。更糟的是，如果像个小孩似地鬼画符那绝对是在找死，可以肯定没人会理你（或者最多是给你一大堆指责与挖苦）。</p><p>如果在使用非母语的论坛提问，你可以犯点拼写和语法上的小错，但决不能在思考上马虎（没错，我们通常能弄清两者的分别）。同时，除非你知道回复者使用的语言，否则请使用英语书写。繁忙的黑客一般会直接删除用他们看不懂的语言写的消息。在网络上英语是通用语言，用英语书写可以将你的问题在尚未被阅读就被直接删除的可能性降到最低。</p><p>如果英文是你的外语（Second language），提示潜在回复者你有潜在的语言困难是很好的：<br />[译注：以下附上原文以供使用]</p><blockquote><p>English is not my native language; please excuse typing errors.</p></blockquote><ul><li>英文不是我的母语，请原谅我的错字或语法。</li></ul><blockquote><p>If you speak $LANGUAGE, please email/PM me;<br />I may need assistance translating my question.</p></blockquote><ul><li>如果你说<strong>某语言</strong>，请向我发电邮/私信；</li><li>我需要有人协助我翻译我的问题。</li></ul><blockquote><p>I am familiar with the technical terms,<br />but some slang expressions and idioms are difficult for me.</p></blockquote><ul><li>我对技术名词很熟悉，但对于俗语或是特别用法不甚了解。</li></ul><blockquote><p>I've posted my question in $LANGUAGE and English.<br />I'll be glad to translate responses, if you only use one or the other.</p></blockquote><ul><li>我把我的问题用<strong>某语言</strong>和英文写出来。</li><li>如果你只用其中的一种语言回答，我会乐意将回复翻译成为你使用的语言。</li></ul><h3 id="使用易于读取且标准的文件格式发送问题">使用易于读取且标准的文件格式发送问题</h3><p>如果你人为地将问题搞得难以阅读，它多半会被忽略，人们更愿读易懂的问题，所以：</p><ul><li>使用纯文字而不是 HTML (<a href="http://archive.birdhouse.org/etc/evilmail.html">关闭 HTML</a> 并不难）。</li><li>使用 MIME 附件通常是可以的，前提是真正有内容（譬如附带的源代码或 patch），而不仅仅是邮件程序生成的模板（譬如只是信件内容的拷贝）。</li><li>不要发送一段文字只是一行句子但自动换行后会变成多行的邮件（这使得回复部分内容非常困难）。设想你的读者是在 80 个字符宽的终端机上阅读邮件，最好设置你的换行分割点小于 80 字。</li><li>但是，对一些特殊的文件<strong>不要</strong>设置固定宽度（譬如日志文件拷贝或会话记录）。数据应该原样包含，让回复者有信心他们看到的是和你看到的一样的东西。</li><li>在英语论坛中，不要使用<code>Quoted-Printable</code> MIME 编码发送消息。这种编码对于张贴非 ASCII 语言可能是必须的，但很多邮件程序并不支持这种编码。当它们处理换行时，那些文本中四处散布的<code>=20</code>符号既难看也分散注意力，甚至有可能破坏内容的语意。</li><li>绝对，<strong>永远</strong>不要指望黑客们阅读使用封闭格式编写的文档，像微软公司的 Word 或 Excel 文件等。大多数黑客对此的反应就像有人将还在冒热气的猪粪倒在你家门口时你的反应一样。即便他们能够处理，他们也很厌恶这么做。</li><li>如果你从使用 Windows 的电脑发送电子邮件，关闭微软愚蠢的<code>智能引号</code>功能 （从[选项] &gt; [校订] &gt; [自动校正选项]，勾选掉<code>智能引号</code>单选框），以免在你的邮件中到处散布垃圾字符。</li><li>在论坛，勿滥用<code>表情符号</code>和<code>HTML</code>功能（当它们提供时）。一两个表情符号通常没有问题，但花哨的彩色文本倾向于使人认为你是个无能之辈。过滥地使用表情符号、色彩和字体会使你看来像个傻笑的小姑娘。这通常不是个好主意，除非你只是对性而不是对答案感兴趣。</li></ul><p>如果你使用图形用户界面的邮件程序（如微软公司的 Outlook 或者其它类似的），注意它们的默认设置不一定满足这些要求。大多数这类程序有基于选单的<code>查看源代码</code>命令，用它来检查发送文件夹中的邮件，以确保发送的是纯文本文件同时没有一些奇怪的字符。</p><h3 id="精确地描述问题并言之有物">精确地描述问题并言之有物</h3><ul><li>仔细、清楚地描述你的问题或 Bug 的症状。</li><li>描述问题发生的环境（机器配置、操作系统、应用程序、以及相关的信息），提供经销商的发行版和版本号（如：<code>Fedora Core 4</code>、<code>Slackware 9.1</code>等）。</li><li>描述在提问前你是怎样去研究和理解这个问题的。</li><li>描述在提问前为确定问题而采取的诊断步骤。</li><li>描述最近做过什么可能相关的硬件或软件变更。</li><li>尽可能地提供一个可以<code>重现这个问题的可控环境</code>的方法。</li></ul><p>尽量去揣测一个黑客会怎样反问你，在你提问之前预先将黑客们可能提出的问题回答一遍。</p><p>以上几点中，当你报告的是你认为可能在代码中的问题时，给黑客一个可以重现你的问题的环境尤其重要。当你这么做时，你得到有效的回答的机会和速度都会大大的提升。</p><p><a href="http://www.chiark.greenend.org.uk/~sgtatham/">Simon Tatham</a> 写过一篇名为《<a href="http://www.chiark.greenend.org.uk/~sgtatham/bugs-cn.html">如何有效地报告Bug</a>》的出色文章。强力推荐你也读一读。</p><h3 id="话不在多而在精">话不在多而在精</h3><p>你需要提供精确有内容的信息。这并不是要求你简单的把成堆的出错代码或者资料完全转录到你的提问中。如果你有庞大而复杂的测试样例能重现程序挂掉的情境，尽量将它剪裁得越小越好。</p><p>这样做的用处至少有三点。<br />第一，表现出你为简化问题付出了努力，这可以使你得到回答的机会增加；<br />第二，简化问题使你更有可能得到<strong>有用</strong>的答案；<br />第三，在精炼你的 bug 报告的过程中，你很可能就自己找到了解决方法或权宜之计。</p><h3 id="别动辄声称找到-bug">别动辄声称找到 Bug</h3><p>当你在使用软件中遇到问题，除非你非常、<strong>非常</strong>的有根据，不要动辄声称找到了 Bug。提示：除非你能提供解决问题的源代码补丁，或者提供回归测试来表明前一版本中行为不正确，否则你都多半不够完全确信。这同样适用在网页和文件，如果你（声称）发现了文件的<code>Bug</code>，你应该能提供相应位置的修正或替代文件。</p><p>请记得，还有其他许多用户没遇到你发现的问题，否则你在阅读文件或搜索网页时就应该发现了（你在抱怨前<a href="#在提问之前">已经做了这些，是吧</a>？）。这也意味着很有可能是你弄错了而不是软件本身有问题。</p><p>编写软件的人总是非常辛苦地使它尽可能完美。如果你声称找到了 Bug，也就是在质疑他们的能力，即使你是对的，也有可能会冒犯到其中某部分人。当你在标题中嚷嚷着有<code>Bug</code>时，这尤其严重。</p><p>提问时，即使你私下非常确信已经发现一个真正的 Bug，最好写得像是<strong>你</strong>做错了什么。如果真的有 Bug，你会在回复中看到这点。这样做的话，如果真有 Bug，维护者就会向你道歉，这总比你惹恼别人然后欠别人一个道歉要好一点。</p><h3 id="低声下气不能代替你的功课">低声下气不能代替你的功课</h3><p>有些人明白他们不该粗鲁或傲慢的提问并要求得到答复，但他们选择另一个极端 —— 低声下气：<code>我知道我只是个可悲的新手，一个撸瑟，但...</code>。这既使人困扰，也没有用，尤其是伴随着与实际问题含糊不清的描述时更令人反感。</p><p>别用原始灵长类动物的把戏来浪费你我的时间。取而代之的是，尽可能清楚地描述背景条件和你的问题情况。这比低声下气更好地定位了你的位置。</p><p>有时网页论坛会设有专为新手提问的版面，如果你真的认为遇到了初学者的问题，到那去就是了，但一样别那么低声下气。</p><h3 id="描述问题症状而非你的猜测">描述问题症状而非你的猜测</h3><p>告诉黑客们你认为问题是怎样造成的并没什么帮助。（如果你的推断如此有效，还用向别人求助吗？），因此要确信你原原本本告诉了他们问题的症状，而不是你的解释和理论；让黑客们来推测和诊断。如果你认为陈述自己的猜测很重要，清楚地说明这只是你的猜测，并描述为什么它们不起作用。</p><p><strong>蠢问题</strong></p><blockquote><p>我在编译内核时接连遇到 SIG11 错误，<br />我怀疑某条飞线搭在主板的走线上了，这种情况应该怎样检查最好？</p></blockquote><p><strong>聪明问题</strong></p><blockquote><p>我的组装电脑是 FIC-PA2007 主机板搭载 AMD K6/233 CPU（威盛 Apollo VP2 芯片组），<br />256MB Corsair PC133 SDRAM 内存，在编译内核时，从开机 20 分钟以后就频频产生 SIG11 错误，<br />但是在头 20 分钟内从没发生过相同的问题。重新启动也没有用，但是关机一晚上就又能工作 20 分钟。<br />所有内存都换过了，没有效果。相关部分的标准编译记录如下…</p></blockquote><p>由于以上这点似乎让许多人觉得难以配合，这里有句话可以提醒你：<code>所有的诊断专家都来自密苏里州。</code> 美国国务院的官方座右铭则是：<code>让我看看</code>（出自国会议员 Willard D. Vandiver 在 1899 年时的讲话：<code>我来自一个出产玉米，棉花，牛蒡和民主党人的国家，滔滔雄辩既不能说服我，也不会让我满意。我来自密苏里州，你必须让我看看。</code>） 针对诊断者而言，这并不是一种怀疑，而只是一种真实而有用的需求，以便让他们看到的是与你看到的原始证据尽可能一致的东西，而不是你的猜测与归纳的结论。所以，大方地展示给我们看吧！</p><h3 id="按发生时间先后列出问题症状">按发生时间先后列出问题症状</h3><p>问题发生前的一系列操作，往往就是对找出问题最有帮助的线索。因此，你的说明里应该包含你的操作步骤，以及机器和软件的反应，直到问题发生。在命令行处理的情况下，提供一段操作记录（例如运行脚本工具所生成的），并引用相关的若干行（如 20 行）记录会非常有帮助。</p><p>如果挂掉的程序有诊断选项（如 -v 的详述开关），试着选择这些能在记录中增加调试信息的选项。记住，<code>多</code>不等于<code>好</code>。试着选取适当的调试级别以便提供有用的信息而不是让读者淹没在垃圾中。</p><p>如果你的说明很长（如超过四个段落），在开头简述问题，接下来再按时间顺序详述会有所帮助。这样黑客们在读你的记录时就知道该注意哪些内容了。</p><h3 id="描述目标而不是过程">描述目标而不是过程</h3><p>如果你想弄清楚如何做某事（而不是报告一个 Bug），在开头就描述你的目标，然后才陈述重现你所卡住的特定步骤。</p><p>经常寻求技术帮助的人在心中有个更高层次的目标，而他们在自以为能达到目标的特定道路上被卡住了，然后跑来问该怎么走，但没有意识到这条路本身就有问题。结果要费很大的劲才能搞定。</p><p><strong>蠢问题</strong></p><blockquote><p>我怎样才能从某绘图程序的颜色选择器中取得十六进制的 RGB 值？</p></blockquote><p><strong>聪明问题</strong></p><blockquote><p>我正试着用替换一幅图片的色码（color table）成自己选定的色码，我现在知道的唯一方法是编辑每个色码区块（table slot），<br />但却无法从某绘图程序的颜色选择器取得十六进制的 RGB 值。</p></blockquote><p>第二种提问法比较聪明，你可能得到像是<code>建议采用另一个更合适的工具</code>的回复。</p><h3 id="别要求使用私人电邮回复">别要求使用私人电邮回复</h3><p>黑客们认为问题的解决过程应该公开、透明，此过程中如果更有经验的人注意到不完整或者不当之处，最初的回复才能够、也应该被纠正。同时，作为提供帮助者可以得到一些奖励，奖励就是他的能力和学识被其他同行看到。</p><p>当你要求私下回复时，这个过程和奖励都被中止。别这样做，让<strong>回复者</strong>来决定是否私下回答 —— 如果他真这么做了，通常是因为他认为问题编写太差或者太肤浅，以至于不可能使其他人产生兴趣。</p><p>这条规则存在一条有限的例外，如果你确信提问可能会引来大量雷同的回复时，那么这个神奇的提问句会是<code>向我发电邮，我将为论坛归纳这些回复</code>。试着将邮件列表或新闻群组从洪水般的雷同回复中解救出来是非常有礼貌的 —— 但你必须信守诺言。</p><h3 id="清楚明确地表达你的问题以及需求">清楚明确地表达你的问题以及需求</h3><p>漫无边际的提问是近乎无休无止的时间黑洞。最有可能给你有用答案的人通常也正是最忙的人（他们忙是因为要亲自完成大部分工作）。这样的人对无节制的时间黑洞相当厌恶，所以他们也倾向于厌恶那些漫无边际的提问。</p><p>如果你明确表述需要回答者做什么（如提供指点、发送一段代码、检查你的补丁、或是其他等等），就最有可能得到有用的答案。因为这会定出一个时间和精力的上限，便于回答者能集中精力来帮你。这么做很棒。</p><p>要理解专家们所处的世界，请把专业技能想像为充裕的资源，而回复的时间则是稀缺的资源。你要求他们奉献的时间越少，你越有可能从真正专业而且很忙的专家那里得到解答。</p><p>所以，界定一下你的问题，使专家花在辨识你的问题和回答所需要付出的时间减到最少，这技巧对你获得有用的答案相当有帮助 —— 但这技巧通常和简化问题有所区别。因此，问<code>我想更好地理解 X，可否指点一下哪有好一点说明？</code>通常比问<code>你能解释一下 X 吗？</code>更好。如果你的代码不能运作，通常请别人看看哪里有问题，比要求别人替你改正要明智得多。</p><h3 id="询问有关代码的问题时">询问有关代码的问题时</h3><p>如果没有提示别人应该从何入手，别要求他人帮你调试有问题的代码。张贴几百行的代码，然后说一声：<code>它不能工作</code>会让你完全被忽略。只贴几十行代码，然后说一句：<code>在第七行以后，我期待它显示 &lt;x&gt;，但实际出现的是 &lt;y&gt;</code>比较有可能让你得到回应。</p><p>最有效描述程序问题的方法是提供最精简的 Bug 展示测试用例（bug-demonstrating test case）。什么是最精简的测试用例？那是问题的缩影；一小个程序片段能<strong>刚好</strong>展示出程序的异常行为，而不包含其他令人分散注意力的内容。怎么制作最精简的测试用例？如果你知道哪一行或哪一段代码会造成异常的行为，复制下来并加入足够重现这个状况的代码（例如，足以让这段代码能被编译/直译/被应用程序处理）。如果你无法将问题缩减到一个特定区块，就复制一份代码并移除不影响产生问题行为的部分。总之，测试用例越小越好（查看<a href="#话不在多而在精">话不在多而在精</a>一节）。</p><p>一般而言，要得到一段相当精简的测试用例并不太容易，但永远先尝试这样做是一个好习惯。这种方式可以帮助你了解如何自行解决这个问题 —— 而且即使你的尝试不成功，黑客们也会看到你在尝试取得答案的过程中付出了努力，这可以让他们更愿意与你合作。</p><p>如果你只是想让别人帮忙审查（Review）一下代码，在信的开头就要说出来，并且一定要提到你认为哪一部分特别需要关注以及为什么。</p><h3 id="别把自己家庭作业的问题贴上来">别把自己家庭作业的问题贴上来</h3><p>黑客们很擅长分辨哪些问题是家庭作业式的问题；因为我们中的大多数都曾自己解决这类问题。同样，这些问题得由<strong>你</strong>来搞定，你会从中学到东西。你可以要求给点提示，但别要求得到完整的解决方案。</p><p>如果你怀疑自己碰到了一个家庭作业式的问题，但仍然无法解决，试试在用户群组，论坛或（最后一招）在项目的<strong>用户</strong>邮件列表或论坛中提问。尽管黑客们<strong>会</strong>看出来，但一些有经验的用户也许仍会给你一些提示。</p><h3 id="去掉无意义的提问句">去掉无意义的提问句</h3><p>避免用无意义的话结束提问，例如<code>有人能帮我吗？</code>或者<code>这有答案吗？</code>。</p><p>首先：如果你对问题的描述不是很好，这样问更是画蛇添足。</p><p>其次：由于这样问是画蛇添足，黑客们会很厌烦你 —— 而且通常会用逻辑上正确，但毫无意义的回答来表示他们的蔑视， 例如：<code>没错，有人能帮你</code>或者<code>不，没答案</code>。</p><p>一般来说，避免用 <code>是或否</code>、<code>对或错</code>、<code>有或没有</code>类型的问句，除非你想得到<a href="https://strcat.de/questions-with-yes-or-no-answers.html">是或否类型的回答</a>。</p><h3 id="即使你很急也不要在标题写紧急">即使你很急也不要在标题写<code>紧急</code></h3><p>这是你的问题，不是我们的。宣称<code>紧急</code>极有可能事与愿违：大多数黑客会直接删除无礼和自私地企图即时引起关注的问题。更严重的是，<code>紧急</code>这个字（或是其他企图引起关注的标题）通常会被垃圾信过滤器过滤掉 —— 你希望能看到你问题的人可能永远也看不到。</p><p>有半个例外的情况是，如果你是在一些很高调，会使黑客们兴奋的地方，也许值得这样去做。在这种情况下，如果你有时间压力，也很有礼貌地提到这点，人们也许会有兴趣回答快一点。</p><p>当然，这风险很大，因为黑客们兴奋的点多半与你的不同。譬如从 NASA 国际空间站（International Space Station）发这样的标题没有问题，但用自我感觉良好的慈善行为或政治原因发肯定不行。事实上，张贴诸如<code>紧急：帮我救救这个毛茸茸的小海豹！</code>肯定让你被黑客忽略或惹恼他们，即使他们认为毛茸茸的小海豹很重要。</p><p>如果你觉得这点很不可思议，最好再把这份指南剩下的内容多读几遍，直到你弄懂了再发文。</p><h3 id="礼多人不怪而且有时还很有帮助">礼多人不怪，而且有时还很有帮助</h3><p>彬彬有礼，多用<code>请</code>和<code>谢谢您的关注</code>，或<code>谢谢你的关照</code>。让大家都知道你对他们花时间免费提供帮助心存感激。</p><p>坦白说，这一点并没有比使用清晰、正确、精准且合乎语法和避免使用专用格式重要（也不能取而代之）。黑客们一般宁可读有点唐突但技术上鲜明的 Bug 报告，而不是那种有礼但含糊的报告。（如果这点让你不解，记住我们是按问题能教给我们什么来评价问题的价值的）</p><p>然而，如果你有一串的问题待解决，客气一点肯定会增加你得到有用回应的机会。</p><p>（我们注意到，自从本指南发布后，从资深黑客那里得到的唯一严重缺陷反馈，就是对预先道谢这一条。一些黑客觉得<code>先谢了</code>意味着事后就不用再感谢任何人的暗示。我们的建议是要么先说<code>先谢了</code>，<strong>然后</strong>事后再对回复者表示感谢，或者换种方式表达感激，譬如用<code>谢谢你的关注</code>或<code>谢谢你的关照</code>。）</p><h3 id="问题解决后加个简短的补充说明">问题解决后，加个简短的补充说明</h3><p>问题解决后，向所有帮助过你的人发个说明，让他们知道问题是怎样解决的，并再一次向他们表示感谢。如果问题在新闻组或者邮件列表中引起了广泛关注，应该在那里贴一个说明比较恰当。</p><p>最理想的方式是向最初提问的话题回复此消息，并在标题中包含<code>已修正</code>，<code>已解决</code>或其它同等含义的明显标记。在人来人往的邮件列表里，一个看见讨论串<code>问题 X</code>和<code>问题 X - 已解决</code>的潜在回复者就明白不用再浪费时间了（除非他个人觉得<code>问题 X</code>有趣），因此可以利用此时间去解决其它问题。</p><p>补充说明不必很长或是很深入；简单的一句<code>你好，原来是网线出了问题！谢谢大家 – Bill</code>比什么也不说要来的好。事实上，除非结论真的很有技术含量，否则简短可爱的小结比长篇大论更好。说明问题是怎样解决的，但大可不必将解决问题的过程复述一遍。</p><p>对于有深度的问题，张贴调试记录的摘要是有帮助的。描述问题的最终状态，说明是什么解决了问题，在此<strong>之后</strong>才指明可以避免的盲点。避免盲点的部分应放在正确的解决方案和其它总结材料之后，而不要将此信息搞成侦探推理小说。列出那些帮助过你的名字，会让你交到更多朋友。</p><p>除了有礼貌和有内涵以外，这种类型的补充也有助于他人在邮件列表/新闻群组/论坛中搜索到真正解决你问题的方案，让他们也从中受益。</p><p>至少，这种补充有助于让每位参与协助的人因问题的解决而从中得到满足感。如果你自己不是技术专家或者黑客，那就相信我们，这种感觉对于那些你向他们求助的大师或者专家而言，是非常重要的。问题悬而未决会让人灰心；黑客们渴望看到问题被解决。好人有好报，满足他们的渴望，你会在下次提问时尝到甜头。</p><p>思考一下怎样才能避免他人将来也遇到类似的问题，自问写一份文件或加个常见问题（FAQ）会不会有帮助。如果是的话就将它们发给维护者。</p><p>在黑客中，这种良好的后继行动实际上比传统的礼节更为重要，也是你如何透过善待他人而赢得声誉的方式，这是非常有价值的资产。</p><h2 id="如何解读答案">如何解读答案</h2><h3 id="rtfm-和-stfw如何知道你已完全搞砸了">RTFM 和 STFW：如何知道你已完全搞砸了</h3><p>有一个古老而神圣的传统：如果你收到<code>RTFM（Read The Fucking Manual）</code>的回应，回答者认为你<strong>应该去读他妈的手册</strong>。当然，基本上他是对的，你应该去读一读。</p><p>RTFM 有一个年轻的亲戚。如果你收到<code>STFW（Search The Fucking Web）</code>的回应，回答者认为你<strong>应该到他妈的网上搜索</strong>。那人多半也是对的，去搜索一下吧。（更温和一点的说法是 <strong><a href="http://lmgtfy.com/">Google 是你的朋友</a></strong>！）</p><p>在论坛，你也可能被要求去爬爬论坛的旧文。事实上，有人甚至可能热心地为你提供以前解决此问题的讨论串。但不要依赖这种关照，提问前应该先搜索一下旧文。</p><p>通常，用这两句之一回答你的人会给你一份包含你需要内容的手册或者一个网址，而且他们打这些字的时候也正在读着。这些答复意味着回答者认为：</p><ul><li><strong>你需要的信息非常容易获得</strong>；</li><li><strong>你自己去搜索这些信息比灌给你，能让你学到更多</strong>。</li></ul><p>你不应该因此不爽；<strong>依照黑客的标准，他已经表示了对你一定程度的关注，而没有对你的要求视而不见</strong>。你应该对他祖母般的慈祥表示感谢。</p><h3 id="如果还是搞不懂">如果还是搞不懂</h3><p>如果你看不懂回应，别立刻要求对方解释。像你以前试着自己解决问题时那样（利用手册，FAQ，网络，身边的高手），先试着去搞懂他的回应。如果你真的需要对方解释，记得表现出你已经从中学到了点什么。</p><p>比方说，如果我回答你：<code>看来似乎是 zentry 卡住了；你应该先清除它。</code>，然后，这是一个<strong>很糟的</strong>后续问题回应：<code>zentry 是什么？</code> <strong>好</strong>的问法应该是这样：<code>哦~~~我看过说明了但是只有 -z 和 -p 两个参数中提到了 zentries，而且还都没有清楚的解释如何清除它。你是指这两个中的哪一个吗？还是我看漏了什么？</code></p><h3 id="处理无礼的回应">处理无礼的回应</h3><p>很多黑客圈子中看似无礼的行为并不是存心冒犯。相反，它是直截了当，一针见血式的交流风格，这种风格更注重解决问题，而不是使人感觉舒服而却模模糊糊。</p><p>如果你觉得被冒犯了，试着平静地反应。如果有人真的做了出格的事，邮件列表、新闻群组或论坛中的前辈多半会招呼他。如果这<strong>没有</strong>发生而你却发火了，那么你发火对象的言语可能在黑客社区中看起来是正常的，而<strong>你</strong>将被视为有错的一方，这将伤害到你获取信息或帮助的机会。</p><p>另一方面，你偶尔真的会碰到无礼和无聊的言行。与上述相反，对真正的冒犯者狠狠地打击，用犀利的语言将其驳得体无完肤都是可以接受的。然而，在行事之前一定要非常非常的有根据。纠正无礼的言论与开始一场毫无意义的口水战仅一线之隔，黑客们自己莽撞地越线的情况并不鲜见。如果你是新手或外人，避开这种莽撞的机会并不高。如果你想得到的是信息而不是消磨时光，这时最好不要把手放在键盘上以免冒险。</p><p>（有些人断言很多黑客都有轻度的自闭症或亚斯伯格综合症，缺少用于润滑人类社会<strong>正常</strong>交往所需的神经。这既可能是真也可能是假的。如果你自己不是黑客，兴许你认为我们脑袋有问题还能帮助你应付我们的古怪行为。只管这么干好了，我们不在乎。我们<strong>喜欢</strong>我们现在这个样子，并且通常对病患标记都有站得住脚的怀疑。）</p><p>Jeff Bigler 的观察总结和这个相关也值得一读 (<strong><a href="http://www.mit.edu/~jcb/tact.html">tact filters</a></strong>)。</p><p>在下一节，我们会谈到另一个问题，当<strong>你</strong>行为不当时所会受到的<code>冒犯</code>。</p><h2 id="如何避免扮演失败者">如何避免扮演失败者</h2><p>在黑客社区的论坛中，你以本指南所描述的或类似的方式，可能会有那么几次搞砸了。而你会在公开场合中被告知你是如何搞砸的，也许攻击的言语中还会带点夹七夹八的颜色。</p><p>这种事发生以后，你能做的最糟糕的事莫过于哀嚎你的遭遇、宣称被言语攻击、要求道歉、高声尖叫、憋闷气、威胁诉诸法律、向其雇主报怨、不去关马桶盖等等。相反地，你该这么做：</p><p>熬过去，这很正常。事实上，它是有益健康且合理的。</p><p>社区的标准不会自行维持，它们是通过参与者积极而<strong>公开地</strong>执行来维持的。不要哭嚎所有的批评都应该通过私下的邮件传送，它不是这样运作的。当有人评论你的一个说法有误或者提出不同看法时，坚持声称受到个人攻击也毫无益处，这些都是失败者的态度。</p><p>也有其它的黑客论坛，受过高礼节要求的误导，禁止参与者张贴任何对别人帖子挑毛病的消息，并声称<code>如果你不想帮助用户就闭嘴。</code> 结果造成有想法的参与者纷纷离开，这么做只会使它们沦为毫无意义的唠叨与无用的技术论坛。</p><p>夸张的讲法是：你要的是“友善”（以上述方式）还是有用？两个里面挑一个。</p><p>记着：当黑客说你搞砸了，并且（无论多么刺耳）告诉你别再这样做时，他正在为关心<strong>你</strong>和<strong>他的社区</strong>而行动。对他而言，不理你并将你从他的生活中滤掉更简单。如果你无法做到感谢，至少要表现得有点尊严，别大声哀嚎，也别因为自己是个有戏剧性超级敏感的灵魂和自以为有资格的新来者，就指望别人像对待脆弱的洋娃娃那样对你。</p><p>有时候，即使你没有搞砸（或者只是在他的想像中你搞砸了），有些人也会无缘无故地攻击你本人。在这种情况下，抱怨倒是<strong>真的</strong>会把问题搞砸。</p><p>这些来找麻烦的人要么是毫无办法但自以为是专家的不中用家伙，要么就是测试你是否真会搞砸的心理专家。其它读者要么不理睬，要么用自己的方式对付他们。这些来找麻烦的人在给他们自己找麻烦，这点你不用操心。</p><p>也别让自己卷入口水战，最好不要理睬大多数的口水战 —— 当然，这是在你检验它们只是口水战，并且未指出你有搞砸的地方，同时也没有巧妙地将问题真正的答案藏于其后（这也是有可能的）。</p><h2 id="不该问的问题">不该问的问题</h2><p>以下是几个经典蠢问题，以及黑客没回答时心中所想的：</p><p>问题：<a href="#q1">我能在哪找到 X 程序或 X 资源？</a></p><p>问题：<a href="#q2">我怎样用 X 做 Y？</a></p><p>问题：<a href="#q3">如何设定我的 shell 提示？</a></p><p>问题：<a href="#q4">我可以用 Bass-o-matic 文件转换工具将 AcmeCorp 文件转换为 TeX 格式吗？</a></p><p>问题：<a href="#q5">我的程序/设定/SQL 语句没有用</a></p><p>问题：<a href="#q6">我的 Windows 电脑有问题，你能帮我吗？</a></p><p>问题：<a href="#q7">我的程序不会动了，我认为系统工具 X 有问题</a></p><p>问题：<a href="#q8">我在安装 Linux（或者 X ）时有问题，你能帮我吗？</a></p><p>问题：<a href="#q9">我怎么才能破解 root 帐号/窃取 OP 特权/读别人的邮件呢？</a></p><p>回答：想要这样做，说明了你是个卑鄙小人；想找个黑客帮你，说明你是个白痴！</p><h2 id="好问题与蠢问题">好问题与蠢问题</h2><p>最后，我将透过举一些例子，来说明怎样聪明的提问；同一个问题的两种问法被放在一起，一种是愚蠢的，另一种才是明智的。</p><p><strong>蠢问题</strong>：</p><blockquote><p>我可以在哪儿找到关于 Foonly Flurbamatic 的资料？</p></blockquote><p>这种问法无非想得到 <a href="#RTFM">STFW</a> 这样的回答。</p><p><strong>聪明问题</strong>：</p><blockquote><p>我用 Google 搜索过 &quot;Foonly Flurbamatic 2600&quot;，但是没找到有用的结果。谁知道上哪儿去找对这种设备编程的资料？</p></blockquote><p>这个问题已经 STFW 过了，看起来他真的遇到了麻烦。</p><p><strong>蠢问题</strong>：</p><blockquote><p>我从 foo 项目找来的源码没法编译。它怎么这么烂？</p></blockquote><p>他觉得都是别人的错，这个傲慢自大的提问者。</p><p><strong>聪明问题</strong>：</p><blockquote><p>foo 项目代码在 Nulix 6.2 版下无法编译通过。我读过了 FAQ，但里面没有提到跟 Nulix 有关的问题。这是我编译过程的记录，我有什么做的不对的地方吗？</p></blockquote><p>提问者已经指明了环境，也读过了 FAQ，还列出了错误，并且他没有把问题的责任推到别人头上，他的问题值得被关注。</p><p><strong>蠢问题</strong>：</p><blockquote><p>我的主机板有问题了，谁来帮我？</p></blockquote><p>某黑客对这类问题的回答通常是：<code>好的，还要帮你拍拍背和换尿布吗？</code>，然后按下删除键。</p><p><strong>聪明问题</strong>：</p><blockquote><p>我在 S2464 主机板上试过了 X 、 Y 和 Z ，但没什么作用，我又试了 A 、 B 和 C 。请注意当我尝试 C 时的奇怪现象。显然 florbish 正在 grommicking，但结果出人意料。通常在 Athlon MP 主机板上引起 grommicking 的原因是什么？有谁知道接下来我该做些什么测试才能找出问题？</p></blockquote><p>这个家伙，从另一个角度来看，值得去回答他。他表现出了解决问题的能力，而不是坐等天上掉答案。</p><p>在最后一个问题中，注意<code>告诉我答案</code>和<code>给我启示，指出我还应该做什么诊断工作</code>之间微妙而又重要的区别。</p><p>事实上，后一个问题源自于 2001 年 8 月在 Linux 内核邮件列表（lkml）上的一个真实的提问。我（Eric）就是那个提出问题的人。我在 Tyan S2464 主板上观察到了这种无法解释的锁定现象，列表成员们提供了解决这一问题的重要信息。</p><p>通过我的提问方法，我给了别人可以咀嚼玩味的东西；我设法让人们很容易参与并且被吸引进来。我显示了自己具备和他们同等的能力，并邀请他们与我共同探讨。通过告诉他们我所走过的弯路，以避免他们再浪费时间，我也表明了对他们宝贵时间的尊重。</p><p>事后，当我向每个人表示感谢，并且赞赏这次良好的讨论经历的时候，一个 Linux 内核邮件列表的成员表示，他觉得我的问题得到解决并非由于我是这个列表中的<strong>名</strong>人，而是因为我用了正确的方式来提问。</p><p>黑客从某种角度来说是拥有丰富知识但缺乏人情味的家伙；我相信他是对的，如果我<strong>像</strong>个乞讨者那样提问，不论我是谁，一定会惹恼某些人或者被他们忽视。他建议我记下这件事，这直接导致了本指南的出现。</p><h2 id="如果得不到回答">如果得不到回答</h2><p>如果仍得不到回答，请不要以为我们觉得无法帮助你。有时只是看到你问题的人不知道答案罢了。没有回应不代表你被忽视，虽然不可否认这种差别很难区分。</p><p>总的来说，简单地重复张贴问题是个很糟的点子。这将被视为无意义的喧闹。有点耐心，知道你问题答案的人可能生活在不同的时区，可能正在睡觉，也有可能你的问题一开始就没有组织好。</p><p>你可以通过其他渠道获得帮助，这些渠道通常更适合初学者的需要。</p><p>有许多网上的以及本地的用户群组，由热情的软件爱好者（即使他们可能从没亲自写过任何软件）组成。通常人们组建这样的团体来互相帮助并帮助新手。</p><p>另外，你可以向很多商业公司寻求帮助，不论公司大还是小。别为要付费才能获得帮助而感到沮丧！毕竟，假使你的汽车发动机汽缸密封圈爆掉了 —— 完全可能如此 —— 你还得把它送到修车铺，并且为维修付费。就算软件没花费你一分钱，你也不能强求技术支持总是免费的。</p><p>对像是 Linux 这种大众化的软件，每个开发者至少会对应到上万名用户。根本不可能由一个人来处理来自上万名用户的求助电话。要知道，即使你要为这些协助付费，和你所购买的同类软件相比，你所付出的也是微不足道的（通常封闭源代码软件的技术支持费用比开源软件的要高得多，且内容也没那么丰富）。</p><h2 id="如何更好地回答问题">如何更好地回答问题</h2><p><strong>态度和善一点。</strong> 问题带来的压力常使人显得无礼或愚蠢，其实并不是这样。</p><p><strong>对初犯者私下回复。</strong> 对那些坦诚犯错之人没有必要当众羞辱，一个真正的新手也许连怎么搜索或在哪找常见问题都不知道。</p><p><strong>如果你不确定，一定要说出来！</strong> 一个听起来权威的错误回复比没有还要糟，别因为听起来像个专家很好玩，就给别人乱指路。要谦虚和诚实，给提问者与同行都树个好榜样。</p><p><strong>如果帮不了忙，也别妨碍他。</strong> 不要在实际步骤上开玩笑，那样也许会毁了提问者的设置 —— 有些可怜的呆瓜会把它当成真的指令。</p><p><strong>试探性的反问以引出更多的细节。</strong> 如果你做得好，提问者可以学到点东西 —— 你也可以。试试将蠢问题转变成好问题，别忘了我们都曾是新手。</p><p>尽管对那些懒虫抱怨一声 RTFM 是正当的，但能给出文档的链接（即使只是建议个 Google 搜索关键词）会更好。</p>]]>
                </content>
            </entry>
</feed>
