<?xml version="1.0" encoding="utf-8"?>
<feed xmlns="http://www.w3.org/2005/Atom">
            <title type="text">个人文章分享</title>
            <subtitle type="text">我本微末凡尘，可也心向天空！</subtitle>
    <updated>2026-05-14T08:42:27+08:00</updated>
        <id>https://www.sxkawzp.cn</id>
        <link rel="alternate" type="text/html" href="https://www.sxkawzp.cn" />
        <link rel="self" type="application/atom+xml" href="https://www.sxkawzp.cn/atom.xml" />
    <rights>Copyright © 2026, 个人文章分享</rights>
    <generator uri="https://halo.run/" version="1.5.3">Halo</generator>
            <entry>
                <title><![CDATA[2026年书籍清单]]></title>
                <link rel="alternate" type="text/html" href="https://www.sxkawzp.cn/archives/2026-nian-shu-ji-qing-dan" />
                <id>tag:https://www.sxkawzp.cn,2026-02-25:2026-nian-shu-ji-qing-dan</id>
                <published>2026-02-25T08:52:30+08:00</published>
                <updated>2026-05-14T08:42:27+08:00</updated>
                <author>
                    <name>doMore</name>
                    <uri>https://www.sxkawzp.cn</uri>
                </author>
                <content type="html">
                        <![CDATA[<h3 id="%E5%B0%8F%E8%AF%B4" tabindex="-1">小说</h3><p>《仙者》     – 忘语  2026-03-03 开始看     2026-03-24 读完<br />《低智商犯罪》  – 紫金陈       2026-05-11 读完<br />《九州缥缈录》   – 江南     20260510 开始看</p><h3 id="%E6%8A%80%E6%9C%AF" tabindex="-1">技术</h3><p>《微信小程序开发实战（第2版） 》  – 黑马程序员<br /><a href="https://wmyskxz.cn/wiki/whats_ai/" target="_blank">《AI是怎么回事》</a> – 公众号 我没有三颗心脏</p>]]>
                </content>
            </entry>
            <entry>
                <title><![CDATA[ParallelStream终端操作阻塞详解]]></title>
                <link rel="alternate" type="text/html" href="https://www.sxkawzp.cn/archives/parallelstream-zhong-duan-cao-zuo-zu-sai-xiang-jie" />
                <id>tag:https://www.sxkawzp.cn,2025-06-13:parallelstream-zhong-duan-cao-zuo-zu-sai-xiang-jie</id>
                <published>2025-06-13T09:37:19+08:00</published>
                <updated>2025-06-13T09:37:19+08:00</updated>
                <author>
                    <name>doMore</name>
                    <uri>https://www.sxkawzp.cn</uri>
                </author>
                <content type="html">
                        <![CDATA[<h2 id="%E5%89%8D%E8%A8%80" tabindex="-1">前言</h2><p>最近在工作中发现有同事讲很多可以并行的任务放入一个List 集合中，然后 parallelStream().forEach ，并且会在循环过程中将每个任务的执行日志、执行结果返回到主线程中使用。我由此产生了一个疑问， forEach 是否会阻塞 主线程的执行，让其等待并行任务全部执行完成之后再执行后续的操作。</p><p>产生这样的疑惑是，我忘记了自己在哪儿看到过 forEach 是不会阻塞的，只有 forEachOrdered 才会阻塞。但是同时对我说这两个都会阻塞，带着这样的疑问，决定仔细研究一下 JDK 源码。如果文章中有不对的地方请看者指正，万分感谢。</p><h2 id="%E4%B8%BB%E7%BA%BF%E7%A8%8B%E9%98%BB%E5%A1%9E" tabindex="-1">主线程阻塞</h2><h3 id="%E7%A4%BA%E4%BE%8B%E4%BB%A3%E7%A0%81" tabindex="-1">示例代码</h3><pre><code class="language-java">public static void main(String[] args) {    List&lt;Runnable&gt; rl = new ArrayList&lt;&gt;();    rl.add(new Runn(5));    rl.add(new Runn(2));    rl.parallelStream().forEach(Runnable::run);    System.out.println(&quot;main end&quot;);}private static class Runn implements Runnable {    long seconds;    public Runn(long seconds) {        this.seconds = seconds;    }    @Override    public void run() {        try {            TimeUnit.SECONDS.sleep(seconds);            System.out.println(&quot;休眠&quot; + seconds + &quot;秒钟: &quot; + Thread.currentThread().getName());        } catch (InterruptedException e) {            e.printStackTrace();        }    }}// 执行结果如下：// 休眠2秒钟: main// 休眠5秒钟: ForkJoinPool.commonPool-worker-1// main end</code></pre><h3 id="%E8%AF%A6%E8%A7%A3" tabindex="-1">详解</h3><ol><li>parallelStream 只是构建一些实体类，并打一些标记，不会执行真正的操作，所以直接从 forEach 下手。</li></ol><pre><code class="language-java">    default Stream&lt;E&gt; parallelStream() {        return StreamSupport.stream(spliterator(), true);    }    public static &lt;T&gt; Stream&lt;T&gt; stream(Spliterator&lt;T&gt; spliterator, boolean parallel) {        Objects.requireNonNull(spliterator);        return new ReferencePipeline.Head&lt;&gt;(spliterator,                                            StreamOpFlag.fromCharacteristics(spliterator),                                            parallel);    }</code></pre><ol start="2"><li>通过步骤1 能看出，构建的实体类是 <strong>ReferencePipeline.Head</strong></li></ol><pre><code class="language-java">    @Override    public void forEach(Consumer&lt;? super E_OUT&gt; action) {        // 判断是否是并行流，是并行流的话直接调用父类 ReferencePipeline 的方法         if (!isParallel()) {            sourceStageSpliterator().forEachRemaining(action);        }        else {            super.forEach(action);        }    }    // java.util.stream.ReferencePipeline#forEach    @Override    public void forEach(Consumer&lt;? super P_OUT&gt; action) {        // 执行终端操作，并得出结果        evaluate(ForEachOps.makeRef(action, false));    }</code></pre><ol start="3"><li>执行操作，</li></ol><pre><code class="language-java">    // java.util.stream.AbstractPipeline#evaluate(java.util.stream.TerminalOp&lt;E_OUT,R&gt;)    // TerminalOp 也就是代表终端操作，是真的会处理数据    final &lt;R&gt; R evaluate(TerminalOp&lt;E_OUT, R&gt; terminalOp) {        assert getOutputShape() == terminalOp.inputShape();        if (linkedOrConsumed)            throw new IllegalStateException(MSG_STREAM_LINKED);        linkedOrConsumed = true;        // 是并行流的走 并行逻辑        return isParallel()               ? terminalOp.evaluateParallel(this, sourceSpliterator(terminalOp.getOpFlags()))               : terminalOp.evaluateSequential(this, sourceSpliterator(terminalOp.getOpFlags()));    }    // java.util.stream.ForEachOps.ForEachOp#evaluateParallel    @Override    public &lt;S&gt; Void evaluateParallel(PipelineHelper&lt;T&gt; helper,                                     Spliterator&lt;S&gt; spliterator) {        // 两个Task都继承了 ForkJoinTask ，但这里我们走 ForEachTask        if (ordered)            new ForEachOrderedTask&lt;&gt;(helper, spliterator, this).invoke();        else            new ForEachTask&lt;&gt;(helper, spliterator, helper.wrapSink(this)).invoke();        return null;    }    // java.util.concurrent.ForkJoinTask#invoke    public final V invoke() {        int s;        if ((s = doInvoke() &amp; DONE_MASK) != NORMAL)            reportException(s);        return getRawResult();    }    // java.util.concurrent.ForkJoinTask#doInvoke    private int doInvoke() {        int s; Thread t; ForkJoinWorkerThread wt;        // 这里判断是不是 ForkJoinWorkerThread ，主线程肯定不是，所有会走到 externalAwaitDone 这个方法后面再说。        return (s = doExec()) &lt; 0 ? s :            ((t = Thread.currentThread()) instanceof ForkJoinWorkerThread) ?            (wt = (ForkJoinWorkerThread)t).pool.            awaitJoin(wt.workQueue, this, 0L) :            externalAwaitDone();    }    // java.util.concurrent.ForkJoinTask#doExec    final int doExec() {        int s; boolean completed;        if ((s = status) &gt;= 0) {            try {                // 这里执行的是 CountedCompleter 的方法，ForEachTask继承了它                // class ForEachTask&lt;S, T&gt; extends CountedCompleter&lt;Void&gt;                 // java.util.concurrent.CountedCompleter#exec                completed = exec();            } catch (Throwable rex) {                return setExceptionalCompletion(rex);            }            if (completed)                s = setCompletion(NORMAL);        }        return s;    }    // java.util.concurrent.CountedCompleter#exec    protected final boolean exec() {        compute();        return false;    }</code></pre><ol start="4"><li>compute 方法说明，在该方法中会将任务拆分，并分配任务到线程池中。</li></ol><pre><code class="language-java">    public void compute() {        Spliterator&lt;S&gt; rightSplit = spliterator, leftSplit;        long sizeEstimate = rightSplit.estimateSize(), sizeThreshold;        if ((sizeThreshold = targetSize) == 0L)            targetSize = sizeThreshold = AbstractTask.suggestTargetSize(sizeEstimate);        boolean isShortCircuit = StreamOpFlag.SHORT_CIRCUIT.isKnown(helper.getStreamAndOpFlags());        boolean forkRight = false;        Sink&lt;S&gt; taskSink = sink;        ForEachTask&lt;S, T&gt; task = this;        while (!isShortCircuit || !taskSink.cancellationRequested()) {            // 判断是不是值得将任务分发到线程池中,如果只有一个任务，直接由主线程执行            // sizeEstimate : 1 ; sizeThreshold : 1            if (sizeEstimate &lt;= sizeThreshold ||                (leftSplit = rightSplit.trySplit()) == null) {                task.helper.copyInto(taskSink, rightSplit);                break;            }            ForEachTask&lt;S, T&gt; leftTask = new ForEachTask&lt;&gt;(task, leftSplit);            task.addToPendingCount(1);            ForEachTask&lt;S, T&gt; taskToFork;            if (forkRight) {                forkRight = false;                rightSplit = leftSplit;                taskToFork = task;                task = leftTask;            }            else {                forkRight = true;                taskToFork = leftTask;            }            // 剩下的任务 分发到 forkJoinPool 中            taskToFork.fork();            sizeEstimate = rightSplit.estimateSize();        }        task.spliterator = null;        task.propagateCompletion();    }</code></pre><ol start="5"><li>之前 4 步已经大致说了并行流的执行逻辑，现在回过头看一下 主线程的阻塞逻辑。</li></ol><pre><code class="language-java">    private int externalAwaitDone() {        // 这里会再次判断，需不需要 主线程帮助执行，如果需要 则会进一步执行任务（不只是执行一个任务）        int s = ((this instanceof CountedCompleter) ? // try helping                 ForkJoinPool.common.externalHelpComplete(                     (CountedCompleter&lt;?&gt;)this, 0) :                 ForkJoinPool.common.tryExternalUnpush(this) ? doExec() : 0);        // 如果不用主线程辅助，但是还有任务在执行，主线程进入 wait 状态，等待 ForkJoinThread 线程执行完成后来唤醒        if (s &gt;= 0 &amp;&amp; (s = status) &gt;= 0) {            boolean interrupted = false;            do {                if (U.compareAndSwapInt(this, STATUS, s, s | SIGNAL)) {                    synchronized (this) {                        if (status &gt;= 0) {                            try {                                wait(0L);                            } catch (InterruptedException ie) {                                interrupted = true;                            }                        }                        else                            notifyAll();                    }                }            } while ((s = status) &gt;= 0);            if (interrupted)                Thread.currentThread().interrupt();        }        return s;    }</code></pre><h2 id="%E6%80%BB%E7%BB%93" tabindex="-1">总结</h2><p>对于 Stream 来说，其实只要是终端操作 如：collect、foreach、find 。并行流操作都会阻塞主线成，如果使用 forEachOrdered 则会全部交给一个线程执行，主线程处于阻塞中。</p>]]>
                </content>
            </entry>
            <entry>
                <title><![CDATA[关于如何保存日常开发中的sql变更]]></title>
                <link rel="alternate" type="text/html" href="https://www.sxkawzp.cn/archives/guan-yu-ru-he-bao-cun-ri-chang-kai-fa-zhong-de-sql-bian-geng" />
                <id>tag:https://www.sxkawzp.cn,2025-05-20:guan-yu-ru-he-bao-cun-ri-chang-kai-fa-zhong-de-sql-bian-geng</id>
                <published>2025-05-20T11:34:44+08:00</published>
                <updated>2025-05-20T11:34:44+08:00</updated>
                <author>
                    <name>doMore</name>
                    <uri>https://www.sxkawzp.cn</uri>
                </author>
                <content type="html">
                        <![CDATA[<h2 id="%E5%89%8D%E8%A8%80" tabindex="-1">前言</h2><p>在项目开发过程中，不可避免的需要创建表，修改表结构。如果没有一个统一维护的地方，很容易造成 sql 丢失，上线的时候漏掉一部分，从而出现莫名的bug。为了能够方便管理这些数据库的变更，介绍几种在工作中曾经用过的方式。</p><h2 id="maven-dbdeploy-plugin" tabindex="-1">maven-dbdeploy-plugin</h2><h3 id="%E4%BD%BF%E7%94%A8%E6%96%B9%E5%BC%8F" tabindex="-1">使用方式</h3><p>maven-dbdeploy-plugin 是一个用于数据库版本控制和增量部署的 Maven 插件，通过管理 SQL 脚本的执行顺序，实现数据库结构的自动化升级。以下是其核心功能和基本用法：</p><pre><code class="language-xml">&lt;!-- 在项目 pom 中增加配置 --&gt;&lt;build&gt;        &lt;plugins&gt;            &lt;plugin&gt;                &lt;groupId&gt;com.dbdeploy&lt;/groupId&gt;                &lt;artifactId&gt;maven-dbdeploy-plugin&lt;/artifactId&gt;                &lt;version&gt;3.0M3&lt;/version&gt;                &lt;configuration&gt;                    &lt;scriptdirectory&gt;./src/main/sql&lt;/scriptdirectory&gt;                    &lt;name&gt;${dbchangefile.name}&lt;/name&gt;                    &lt;encoding&gt;GBK&lt;/encoding&gt;                    &lt;lineEnding&gt;lf&lt;/lineEnding&gt;                    &lt;outputfile&gt;./target/apply.sql&lt;/outputfile&gt;                    &lt;undoOutputfile&gt;./target/undo.sql&lt;/undoOutputfile&gt;                    &lt;driver&gt;com.mysql.cj.jdbc.Driver&lt;/driver&gt;                    &lt;url&gt;${db.url}&lt;/url&gt;                    &lt;userid&gt;${db.usr}&lt;/userid&gt;                    &lt;password&gt;${db.pwd}&lt;/password&gt;                    &lt;dbms&gt;mysql&lt;/dbms&gt;                    &lt;delimiter&gt;;&lt;/delimiter&gt;                    &lt;delimiterType&gt;row&lt;/delimiterType&gt;                &lt;/configuration&gt;                &lt;dependencies&gt;                    &lt;dependency&gt;                        &lt;groupId&gt;com.mysql&lt;/groupId&gt;                        &lt;artifactId&gt;mysql-connector-j&lt;/artifactId&gt;                        &lt;version&gt;8.0.33&lt;/version&gt;                    &lt;/dependency&gt;                &lt;/dependencies&gt;                &lt;executions&gt;                    &lt;execution&gt;                        &lt;id&gt;update-db&lt;/id&gt;                        &lt;phase&gt;verify&lt;/phase&gt;                        &lt;goals&gt;                            &lt;goal&gt;update&lt;/goal&gt;                        &lt;/goals&gt;                        &lt;configuration&gt;                            &lt;scriptdirectory&gt;${basedir}/target/sql-all&lt;/scriptdirectory&gt;                        &lt;/configuration&gt;                    &lt;/execution&gt;                &lt;/executions&gt;            &lt;/plugin&gt;            &lt;plugin&gt;                &lt;artifactId&gt;maven-resources-plugin&lt;/artifactId&gt;                &lt;version&gt;2.5&lt;/version&gt;                &lt;executions&gt;                    &lt;execution&gt;                        &lt;id&gt;copy-sql-to-source-dir&lt;/id&gt;                        &lt;phase&gt;process-resources&lt;/phase&gt;                        &lt;goals&gt;                            &lt;goal&gt;copy-resources&lt;/goal&gt;                        &lt;/goals&gt;                        &lt;configuration&gt;                            &lt;outputDirectory&gt;${basedir}/target/sql-all&lt;/outputDirectory&gt;                            &lt;resources&gt;                                &lt;resource&gt;                                    &lt;directory&gt;src/main/sql&lt;/directory&gt;                                &lt;/resource&gt;                            &lt;/resources&gt;                        &lt;/configuration&gt;                    &lt;/execution&gt;                &lt;/executions&gt;            &lt;/plugin&gt;        &lt;/plugins&gt;    &lt;/build&gt;</code></pre><pre><code class="language-sql">-- 需要初始化的 数据库表  表明可以在配置中修改，但是没必要CREATE TABLE changelog (  change_number BIGINT NOT NULL,  complete_dt TIMESTAMP NOT NULL,  applied_by VARCHAR(100) NOT NULL,  description VARCHAR(500) NOT NULL);ALTER TABLE changelog ADD CONSTRAINT Pkchangelog PRIMARY KEY (change_number);</code></pre><p>初始化文件的脚本：</p><pre><code class="language-shell"># createNewDDLChangeFile.batmvn dbdeploy:change-script -Ddbchangefile.name=DDL# createNewDMLChangeFile.batmvn dbdeploy:change-script -Ddbchangefile.name=DML# 生成文件名称效果  ：  20250520103541_DML.sql</code></pre><p>执行sql 的脚本：</p><pre><code class="language-shell"># -X 讲详细的执行日期打出来call mvn dbdeploy:update -X -Ddb.url=&quot;jdbc:mysql://localhost:3306/test&quot; -Ddb.usr=test -Ddb.pwd=123456 1&gt;run.log</code></pre><h3 id="%E4%B8%BB%E8%A6%81%E5%8A%9F%E8%83%BD" tabindex="-1">主要功能</h3><ol><li>脚本版本化：按时间顺序管理 SQL 脚本</li><li>防止重复执行：通过记录已执行脚本的元数据表（默认 changelog ）避免重复执行</li><li>回滚支持：部分支持脚本回滚（需在脚本中定义反向操作）</li><li>环境隔离：支持不同环境（开发 / 测试 / 生产）使用不同配置</li></ol><blockquote><p>注意： 若项目较简单，maven-dbdeploy-plugin 是轻量级选择。</p></blockquote><h2 id="sql-archery" tabindex="-1">Sql Archery</h2><p>Archery 是archer的分支项目，定位于SQL审核查询平台，旨在提升DBA的工作效率，支持多数据库的SQL上线和查询，同时支持丰富的MySQL运维功能，所有功能都兼容手机端操作。</p><p><a href="https://demo.archerydms.com/" target="_blank">在线体验</a></p><p>账号 密码<br />archer archer</p><blockquote><p>注意：如果说项目，数据库特别多的话，可以只用这种方式，这个有完善的审核机制，以及sql检测。能够快捷方便的去操作数据库，所有的sql都会被记录，即使后面上线或者恢复等，也不会因为时间太久而导致原本sql丢失。只是会增大运维工作量，小型的项目可以简简单单的，不必引入。</p></blockquote>]]>
                </content>
            </entry>
            <entry>
                <title><![CDATA[服务端与客户端交互加解密方案探究]]></title>
                <link rel="alternate" type="text/html" href="https://www.sxkawzp.cn/archives/fu-wu-duan-yu-ke-hu-duan-jiao-hu-jia-jie-mi-fang-an-tan-jiu" />
                <id>tag:https://www.sxkawzp.cn,2025-05-13:fu-wu-duan-yu-ke-hu-duan-jiao-hu-jia-jie-mi-fang-an-tan-jiu</id>
                <published>2025-05-13T16:50:19+08:00</published>
                <updated>2025-05-13T16:51:05+08:00</updated>
                <author>
                    <name>doMore</name>
                    <uri>https://www.sxkawzp.cn</uri>
                </author>
                <content type="html">
                        <![CDATA[<h2 id="%E7%AE%97%E6%B3%95%E7%AE%80%E4%BB%8B" tabindex="-1">算法简介</h2><p>SM2：基于椭圆曲线密码体制（ECC）的非对称加密算法，使用公钥和私钥对。公钥公开用于加密或验证签名，私钥保密用于解密或生成签名。数学原理基于椭圆曲线上的离散对数问题。主要用于数字签名、密钥交换等，能确保电子文档的真实性、完整性和不可否认性。其安全性高，同等安全强度下，密钥长度比 RSA 等非对称加密算法短，计算量小、效率高、存储成本低，但加密和解密计算过程相对复杂，处理大量数据时速度较慢。</p><p>SM3：密码哈希函数，将任意长度的数据映射为 256 位的固定长度哈希值。具有单向性，从哈希值很难反推出原始数据。主要用于数字签名中计算消息摘要以及数据完整性验证。具有高度的安全性和抗碰撞性，能有效抵御各种恶意攻击，保证数据的完整性和真实性，但原始数据丢失后无法通过哈希值恢复。</p><p>SM4：分组对称加密算法，分组长度和密钥长度均为 128 位。通过对明文分组和密钥进行多轮迭代运算实现加密，每轮运算依据特定置换和替换规则处理数据。常用于数据存储加密和数据传输加密，加密速度快，处理大量数据时效率高，128 位密钥能提供足够安全性，可抵御穷举攻击等常见攻击方式。</p><h2 id="%E6%96%B9%E6%A1%88" tabindex="-1">方案</h2><p>定义两套 密钥，用于 <strong>请求</strong> 和 <strong>响应</strong> 两种交互，定义 clientPublicKey、clientPrivateKey、servicePublicKey、servicePrivateKey 四个字段，两组 <strong>公钥和私钥</strong> 。</p><p>Client 请求的时候使用服务端的公钥(servicePublicKey)进行加密，Server端使用 服务端私钥（servicePrivateKey）进行解密。响应的时候则相反。这里并不会对数据进行 加/解密 ，而是类似于 SSL 协商后续 SM4（对称加密）所需要使用的密钥。</p><p>可以在 RequestHeader 和 ResponseHeader 中添加 <strong>X-Key</strong> 用于记录 SM4 加密所需要的密钥传输。</p><blockquote><p>无需纠结是服务端和客户端两个名词，只是为了区别是两套密钥。</p></blockquote><h2 id="%E5%AE%9E%E7%8E%B0" tabindex="-1">实现</h2><ol><li>外部依赖</li></ol><pre><code class="language-xml">        &lt;dependency&gt;            &lt;groupId&gt;cn.hutool&lt;/groupId&gt;            &lt;artifactId&gt;hutool-all&lt;/artifactId&gt;        &lt;/dependency&gt;</code></pre><ol start="2"><li>Java代码一端实现</li></ol><pre><code class="language-java">@Servicepublic class EncryptUtil {    /**     * 客户端公钥     */    @Value(&quot;${client.public.key:}&quot;)    private String clientPublicKey;    /**     * 服务端私钥     */    @Value(&quot;${service.private.key:}&quot;)    private String servicePrivateKey;    @Value(&quot;${service.salt:}&quot;)    private String salt;    /**     * SM2加密     *     * @param plaintext 明文数据     * @return 加密数据     */    public String sm2Encrypt(String plaintext) {        return SmUtil.sm2(null, clientPublicKey).encryptHex(plaintext.getBytes(), KeyType.PublicKey);    }    /**     * SM2解密     *     * @param encrypted 加密数据     * @return 明文     */    public String sm2Decrypt(String encrypted) {        return SmUtil.sm2(servicePrivateKey, null).decryptStr(encrypted, KeyType.PrivateKey);    }    /**     * SM4加密     *     * @param plaintext 明文     * @return 加密数据     */    public String sm4Encrypt(String plaintext) {        return SmUtil.sm4(SM4Util.hexToBytes(RandomUtil.randomNumbers(32))).encryptHex(plaintext.getBytes());    }    /**     * SM4解密     *     * @param encrypted 加密数据     * @return 明文     */    public String sm4Decrypt(String encrypted, String key) {        return SmUtil.sm4(SM4Util.hexToBytes(key)).decryptStr(encrypted);    }    /**     * SM3加密     *     * @param plaintext 需要HASH的明文数据     * @return Hash 数据     */    public String sm3(String plaintext) {        return SmUtil.sm3(plaintext);    }}</code></pre>]]>
                </content>
            </entry>
            <entry>
                <title><![CDATA[数据库树形结构设计]]></title>
                <link rel="alternate" type="text/html" href="https://www.sxkawzp.cn/archives/shu-ju-ku-shu-xing-jie-gou-she-ji" />
                <id>tag:https://www.sxkawzp.cn,2025-04-18:shu-ju-ku-shu-xing-jie-gou-she-ji</id>
                <published>2025-04-18T18:00:50+08:00</published>
                <updated>2025-04-21T13:50:09+08:00</updated>
                <author>
                    <name>doMore</name>
                    <uri>https://www.sxkawzp.cn</uri>
                </author>
                <content type="html">
                        <![CDATA[<h2 id="%E5%89%8D%E8%A8%80" tabindex="-1">前言</h2><p>本文基于mysql数据库探讨。</p><p>关于在数据库中存储树形结构，一直以来是一个比较头疼的问题。mysql 8中支持了 <strong>with recursive</strong> 语法，查询起来通过一个sql就能行，但是在 mysql 5.7 中还不支持，也就衍生了一些，方便 树形结构查询的方案，本文根据以往的经验做一个总结。探讨一下这些方案的优劣。</p><h2 id="%E5%87%86%E5%A4%87" tabindex="-1">准备</h2><blockquote><p>Tips： 日常最常见的树形结构有 行政区划、部门组织。此处以简单的部门组织为例，展开叙述。</p></blockquote><blockquote><p>Tips： 以下方案皆通过 mysql 5.7 进行优化。 mysql 8 可以使用 CTE 语法。</p></blockquote><pre><code class="language-sql">-- 此处为最初的为经过优化的 简化版本部门表create table sys_dept(    dept_id     bigint auto_increment comment &#39;部门id&#39; primary key,    parent_id   bigint      default 0   null comment &#39;父部门id&#39;,    dept_name   varchar(30) default &#39;&#39;  null comment &#39;部门名称&#39;,    status      char        default &#39;0&#39; null comment &#39;部门状态（0正常 1停用）&#39;,    del_flag    char        default &#39;0&#39; null comment &#39;删除标志（0代表存在 2代表删除）&#39;)    comment &#39;部门表&#39;;</code></pre><h2 id="%E8%AF%A6%E8%BF%B0" tabindex="-1">详述</h2><h3 id="%E7%AC%AC%E4%B8%80%E7%89%88%E6%9C%AC" tabindex="-1">第一版本</h3><p>在准备一节中，准备了一张表，该表中存储了上级部门的 ID ，以此来确定树形关系。这个版本中，有这非常明显的问题，比如：我们要查一个部门下所有的子级、查询指定级别的部门等等。查询起来都是比较麻烦的，只能在代码里面通过 <strong>递归</strong> 的方式，多次连接数据库才能达到目的，如果层级比较多的话，单单是数据库连接耗时也会是不小的开销（即使有连接池）。</p><h3 id="%E7%AC%AC%E4%BA%8C%E7%89%88%E6%9C%AC" tabindex="-1">第二版本</h3><p>根据第一版本中非常明显的问题，可以创建  <strong>ancestors（祖级列表），level（级别）</strong> 字段，使得查询更加方便，存储的体积会稍微增大，但是相较于在数据多的情况，这点可以忽略。表结构就变成了以下：</p><pre><code class="language-sql">create table sys_dept(    dept_id     bigint auto_increment comment &#39;部门id&#39; primary key,    parent_id   bigint      default 0   null comment &#39;父部门id&#39;,    dept_name   varchar(30) default &#39;&#39;  null comment &#39;部门名称&#39;,    ancestors   varchar(50) default &#39;&#39;  null comment &#39;祖级列表&#39;,    level       int         DEFAULT     NULL COMMENT &#39;层级&#39;,    status      char        default &#39;0&#39; null comment &#39;部门状态（0正常 1停用）&#39;,    del_flag    char        default &#39;0&#39; null comment &#39;删除标志（0代表存在 2代表删除）&#39;)    comment &#39;部门表&#39;;-- ancestors 存储数据示例：0,100,201,389,532 -- 可以利用 mysql 的函数 find_in_set 很方便的查询所有的下级</code></pre><p>这个版本已经足够应付大部分的场景。但如果需要查询 一个部门 从 root 到 自身的完整链路。 例如：查询后端组的全部名称（北京卡路里信息科技有限公司 - 研发部 - 后端组）。</p><h3 id="%E7%AC%AC%E4%B8%89%E7%89%88%E6%9C%AC" tabindex="-1">第三版本</h3><p>第三个版本就需要一个很巧妙的思想，就是将要查询的上级约束在一个范围以内，并且 level 是不一样的。也就是演变成如下表结构：</p><pre><code class="language-sql">create table sys_dept(    dept_id   bigint auto_increment comment &#39;部门id&#39; primary key,    parent_id bigint      default 0   null comment &#39;父部门id&#39;,    dept_name varchar(30) default &#39;&#39;  null comment &#39;部门名称&#39;,    ancestors varchar(50) default &#39;&#39;  null comment &#39;祖级列表&#39;,    level     int         DEFAULT NULL COMMENT &#39;层级&#39;,    &#96;left&#96;    int         DEFAULT NULL COMMENT &#39;左边界&#39;,    &#96;right&#96;   int         DEFAULT NULL COMMENT &#39;右边界&#39;,    status    char        default &#39;0&#39; null comment &#39;部门状态（0正常 1停用）&#39;,    del_flag  char        default &#39;0&#39; null comment &#39;删除标志（0代表存在 2代表删除）&#39;)    comment &#39;部门表&#39;;-- ancestors 字段在这个版本中其实是可以删除的，为了方便对比，在此处暂时留着</code></pre><p>为便于理解，给出数据表中数据示例：</p><table><thead><tr><th style="text-align:left">dept_id</th><th style="text-align:left">parent_id</th><th style="text-align:left">dept_name</th><th style="text-align:left">level</th><th style="text-align:left">left</th><th style="text-align:left">right</th></tr></thead><tbody><tr><td style="text-align:left">1</td><td style="text-align:left">0</td><td style="text-align:left">研发部</td><td style="text-align:left">1</td><td style="text-align:left">1</td><td style="text-align:left">14</td></tr><tr><td style="text-align:left">2</td><td style="text-align:left">1</td><td style="text-align:left">研发部-1</td><td style="text-align:left">2</td><td style="text-align:left">8</td><td style="text-align:left">13</td></tr><tr><td style="text-align:left">3</td><td style="text-align:left">2</td><td style="text-align:left">研发部-1-1</td><td style="text-align:left">3</td><td style="text-align:left">9</td><td style="text-align:left">10</td></tr><tr><td style="text-align:left">4</td><td style="text-align:left">2</td><td style="text-align:left">研发部-1-2</td><td style="text-align:left">3</td><td style="text-align:left">11</td><td style="text-align:left">12</td></tr><tr><td style="text-align:left">5</td><td style="text-align:left">1</td><td style="text-align:left">研发部-2</td><td style="text-align:left">2</td><td style="text-align:left">2</td><td style="text-align:left">7</td></tr><tr><td style="text-align:left">6</td><td style="text-align:left">5</td><td style="text-align:left">研发部-2-1</td><td style="text-align:left">3</td><td style="text-align:left">3</td><td style="text-align:left">4</td></tr><tr><td style="text-align:left">7</td><td style="text-align:left">5</td><td style="text-align:left">研发部-2-2</td><td style="text-align:left">3</td><td style="text-align:left">5</td><td style="text-align:left">6</td></tr></tbody></table><p>简单来说，这个版本的思想就是，将一个树形结构约束到一个范围以内。<br />再看一下插入流程（详细说明 left、right 的变化）：</p><ol><li>插入研发部</li></ol><pre><code class="language-sql">left  right 1      2</code></pre><ol start="2"><li>插入研发部-1</li></ol><pre><code class="language-sql">left  right 1      4 2      3</code></pre><ol start="3"><li>插入研发部-1-1</li></ol><pre><code class="language-sql">left  right 1      6 2      5 3      4</code></pre><p>…</p><p>在插入的时候每一次都需要变换 left、right。相应的在插入的时候会消耗一定的时间，但是相比较与每次查询所花费的时间，还是可以接受的。并且一般情况下，这种树形结构的数据变动的次数不会特别频繁。</p><blockquote><p>预排序遍历树（Modified Preorder Tree Traversal，MPTT）: 从根节点开始，沿着树的前序遍历方向，为每个节点分配一个唯一的左值（lft）和右值（rgt）。节点的左值小于其所有子孙节点的左值，右值大于其所有子孙节点的右值。通过这种方式，可以方便地通过lft和rgt字段的范围查询来确定节点的上下级关系，而无需使用递归查询，提高了查询效率</p></blockquote><h2 id="%E6%80%BB%E7%BB%93" tabindex="-1">总结</h2><p>希望大家能在设计的时候多一些选择，尽可能的优化自己项目的速度。合理利用，合理取舍。</p><p>本人写的一个简单示例：<a href="https://gitee.com/su_xing_kang/administrative_division.git" target="_blank">https://gitee.com/su_xing_kang/administrative_division.git</a></p>]]>
                </content>
            </entry>
            <entry>
                <title><![CDATA[Dubbo如何加载配置]]></title>
                <link rel="alternate" type="text/html" href="https://www.sxkawzp.cn/archives/dubbo-ru-he-jia-zai-pei-zhi" />
                <id>tag:https://www.sxkawzp.cn,2025-04-06:dubbo-ru-he-jia-zai-pei-zhi</id>
                <published>2025-04-06T18:14:04+08:00</published>
                <updated>2025-04-07T11:16:44+08:00</updated>
                <author>
                    <name>doMore</name>
                    <uri>https://www.sxkawzp.cn</uri>
                </author>
                <content type="html">
                        <![CDATA[<h2 id="%E5%89%8D%E8%A8%80" tabindex="-1">前言</h2><p>在实际开发过程中，使用 SpringBoot 结合 Dubbo ，在项目启动的时候总是会出现一个， zookeeper net connected 的错误，最后发现是在 org.apache.dubbo.remoting.zookeeper.curator.CuratorZookeeperClient 构造方法中会去连接 zk，并且是同步阻塞形式的（代码如下）。时间设置的过短就会报错。</p><pre><code class="language-java">    public CuratorZookeeperClient(URL url) {        super(url);        try {            // ......            client.getConnectionStateListenable().addListener(new CuratorConnectionStateListener(url));            client.start();            // 这里是一个同步阻塞等待，假如超过了 timeout 的时间，当前ZooKeeper客户端还是没有变成“已连接”状态，当前线程就会被唤醒，继续向下执行            boolean connected = client.blockUntilConnected(timeout, TimeUnit.MILLISECONDS);            // 判断当前客户端不是“已连接”状态，主动抛出异常            if (!connected) {                throw new IllegalStateException(&quot;zookeeper not connected&quot;);            }        } catch (Exception e) {            throw new IllegalStateException(e.getMessage(), e);        }    }</code></pre><p>本身我们项目中设置了 dubbo.consumer.timeout 但是这里的时间使用的却不是这个，而是 dubbo.registry.timeout 。由此引发了对该问题的探索，想要知道 dubbo 具体是怎么将配置文件加载的。</p><h2 id="%E8%AF%A6%E8%A7%A3" tabindex="-1">详解</h2><h3 id="pom-dependency" tabindex="-1">pom dependency</h3><p>在使用 dubbo-spring-boot-autoconfigure 时，会引入额外的 jar 包，以确保 自动配置能够正常识别使用。下面列出主要的：</p><pre><code class="language-xml">&lt;dependency&gt;  &lt;groupId&gt;org.apache.dubbo&lt;/groupId&gt;  &lt;artifactId&gt;dubbo-spring-boot-autoconfigure&lt;/artifactId&gt;  &lt;version&gt;2.7.8&lt;/version&gt;&lt;/dependency&gt;&lt;!-- 自动额外引入的主要的 jar --&gt;&lt;dependency&gt;  &lt;groupId&gt;org.apache.dubbo&lt;/groupId&gt;  &lt;artifactId&gt;dubbo-spring-boot-autoconfigure-compatible&lt;/artifactId&gt;  &lt;version&gt;2.7.8&lt;/version&gt;&lt;/dependency&gt;&lt;dependency&gt;  &lt;groupId&gt;org.apache.dubbo&lt;/groupId&gt;  &lt;artifactId&gt;dubbo&lt;/artifactId&gt;&lt;/dependency&gt;    &lt;dependency&gt;  &lt;groupId&gt;com.alibaba.spring&lt;/groupId&gt;  &lt;artifactId&gt;spring-context-support&lt;/artifactId&gt;&lt;/dependency&gt;</code></pre><h3 id="enabledubboconfig" tabindex="-1">EnableDubboConfig</h3><p>在 org.apache.dubbo.spring.boot.autoconfigure.DubboAutoConfiguration 类上使用了 org.apache.dubbo.config.spring.context.annotation.EnableDubboConfig 注解，该注解通过 @Import 引入了 org.apache.dubbo.config.spring.context.annotation.DubboConfigConfigurationRegistrar 类。由此开启了关于配置的一系列加载。</p><pre><code class="language-java">public class DubboConfigConfigurationRegistrar implements ImportBeanDefinitionRegistrar {    @Override    public void registerBeanDefinitions(AnnotationMetadata importingClassMetadata, BeanDefinitionRegistry registry) {        AnnotationAttributes attributes = AnnotationAttributes.fromMap(                importingClassMetadata.getAnnotationAttributes(EnableDubboConfig.class.getName()));        boolean multiple = attributes.getBoolean(&quot;multiple&quot;);        // Single Config Bindings        registerBeans(registry, DubboConfigConfiguration.Single.class);        if (multiple) {             // Since 2.6.6 https://github.com/apache/dubbo/issues/3193            registerBeans(registry, DubboConfigConfiguration.Multiple.class);        }        // Since 2.7.6        registerCommonBeans(registry);    }}@EnableConfigurationBeanBindings({        @EnableConfigurationBeanBinding(prefix = &quot;dubbo.applications&quot;, type = ApplicationConfig.class, multiple = true),        @EnableConfigurationBeanBinding(prefix = &quot;dubbo.modules&quot;, type = ModuleConfig.class, multiple = true),        @EnableConfigurationBeanBinding(prefix = &quot;dubbo.registries&quot;, type = RegistryConfig.class, multiple = true),        @EnableConfigurationBeanBinding(prefix = &quot;dubbo.protocols&quot;, type = ProtocolConfig.class, multiple = true),        @EnableConfigurationBeanBinding(prefix = &quot;dubbo.monitors&quot;, type = MonitorConfig.class, multiple = true),        @EnableConfigurationBeanBinding(prefix = &quot;dubbo.providers&quot;, type = ProviderConfig.class, multiple = true),        @EnableConfigurationBeanBinding(prefix = &quot;dubbo.consumers&quot;, type = ConsumerConfig.class, multiple = true),        @EnableConfigurationBeanBinding(prefix = &quot;dubbo.config-centers&quot;, type = ConfigCenterBean.class, multiple = true),        @EnableConfigurationBeanBinding(prefix = &quot;dubbo.metadata-reports&quot;, type = MetadataReportConfig.class, multiple = true),        @EnableConfigurationBeanBinding(prefix = &quot;dubbo.metricses&quot;, type = MetricsConfig.class, multiple = true)})public static class Multiple {}/** * 此处会把  com.alibaba.spring.beans.factory.annotation.EnableConfigurationBeanBindings  注解上的 @Import 引入类 com.alibaba.spring.beans.factory.annotation.ConfigurationBeanBindingsRegister 识别 */    public static void registerBeans(BeanDefinitionRegistry registry, Class&lt;?&gt;... annotatedClasses) {        if (!ObjectUtils.isEmpty(annotatedClasses)) {            Set&lt;Class&lt;?&gt;&gt; classesToRegister = new LinkedHashSet(Arrays.asList(annotatedClasses));            Iterator&lt;Class&lt;?&gt;&gt; iterator = classesToRegister.iterator();            while(iterator.hasNext()) {                Class&lt;?&gt; annotatedClass = (Class)iterator.next();                if (isPresentBean(registry, annotatedClass)) {                    iterator.remove();                }            }            AnnotatedBeanDefinitionReader reader = new AnnotatedBeanDefinitionReader(registry);            if (logger.isDebugEnabled()) {                logger.debug(registry.getClass().getSimpleName() + &quot; will register annotated classes : &quot; + Arrays.asList(annotatedClasses) + &quot; .&quot;);            }            reader.register((Class[])classesToRegister.toArray(com.alibaba.spring.util.ClassUtils.EMPTY_CLASS_ARRAY));        }    }/** * 需要注意 上述 是 Bindings 现在是 Binding . */public class ConfigurationBeanBindingsRegister implements ImportBeanDefinitionRegistrar, EnvironmentAware {    private ConfigurableEnvironment environment;    public ConfigurationBeanBindingsRegister() {    }    public void registerBeanDefinitions(AnnotationMetadata importingClassMetadata, BeanDefinitionRegistry registry) {        // 识别出 EnableConfigurationBeanBindings 中的数组        AnnotationAttributes attributes = AnnotationAttributes.fromMap(importingClassMetadata.getAnnotationAttributes(EnableConfigurationBeanBindings.class.getName()));        AnnotationAttributes[] annotationAttributes = attributes.getAnnotationArray(&quot;value&quot;);        // 实例化 ConfigurationBeanBindingRegistrar 去讲上面识别到的数组，一个一个注册        ConfigurationBeanBindingRegistrar registrar = new ConfigurationBeanBindingRegistrar();        registrar.setEnvironment(this.environment);        for(AnnotationAttributes element : annotationAttributes) {            // 根据注解中的值 进行初始化，并且讲配置文件或者配置中心的值赋值            registrar.registerConfigurationBeanDefinitions(element, registry);        }    }    public void setEnvironment(Environment environment) {        Assert.isInstanceOf(ConfigurableEnvironment.class, environment);        this.environment = (ConfigurableEnvironment)environment;    }}</code></pre><h3 id="configurationbeanbindingregistrar" tabindex="-1">ConfigurationBeanBindingRegistrar</h3><pre><code class="language-java">public class ConfigurationBeanBindingRegistrar implements ImportBeanDefinitionRegistrar, EnvironmentAware {    static final Class ENABLE_CONFIGURATION_BINDING_CLASS = EnableConfigurationBeanBinding.class;    private static final String ENABLE_CONFIGURATION_BINDING_CLASS_NAME;    private final Log log = LogFactory.getLog(this.getClass());    private ConfigurableEnvironment environment;    public ConfigurationBeanBindingRegistrar() {    }    // ......    // 1. 调用的是 这个方法    public void registerConfigurationBeanDefinitions(Map&lt;String, Object&gt; attributes, BeanDefinitionRegistry registry) {        String prefix = (String)AnnotationUtils.getRequiredAttribute(attributes, &quot;prefix&quot;);        prefix = this.environment.resolvePlaceholders(prefix);        Class&lt;?&gt; configClass = (Class)AnnotationUtils.getRequiredAttribute(attributes, &quot;type&quot;);        boolean multiple = (Boolean)AnnotationUtils.getAttribute(attributes, &quot;multiple&quot;, false);        boolean ignoreUnknownFields = (Boolean)AnnotationUtils.getAttribute(attributes, &quot;ignoreUnknownFields&quot;, true);        boolean ignoreInvalidFields = (Boolean)AnnotationUtils.getAttribute(attributes, &quot;ignoreInvalidFields&quot;, true);        this.registerConfigurationBeans(prefix, configClass, multiple, ignoreUnknownFields, ignoreInvalidFields, registry);    }    private void registerConfigurationBeans(String prefix, Class&lt;?&gt; configClass, boolean multiple, boolean ignoreUnknownFields, boolean ignoreInvalidFields, BeanDefinitionRegistry registry) {        Map&lt;String, Object&gt; configurationProperties = PropertySourcesUtils.getSubProperties(this.environment.getPropertySources(), this.environment, prefix);        if (CollectionUtils.isEmpty(configurationProperties)) {            if (this.log.isDebugEnabled()) {                this.log.debug(&quot;There is no property for binding to configuration class [&quot; + configClass.getName() + &quot;] within prefix [&quot; + prefix + &quot;]&quot;);            }        } else {            for(String beanName : multiple ? this.resolveMultipleBeanNames(configurationProperties) : Collections.singleton(this.resolveSingleBeanName(configurationProperties, configClass, registry))) {                // 2. 进而走到这里                this.registerConfigurationBean(beanName, configClass, multiple, ignoreUnknownFields, ignoreInvalidFields, configurationProperties, registry);            }            // 5. 注册 com.alibaba.spring.beans.factory.annotation.ConfigurationBeanBindingPostProcessor            this.registerConfigurationBindingBeanPostProcessor(registry);        }    }    private void registerConfigurationBean(String beanName, Class&lt;?&gt; configClass, boolean multiple, boolean ignoreUnknownFields, boolean ignoreInvalidFields, Map&lt;String, Object&gt; configurationProperties, BeanDefinitionRegistry registry) {        BeanDefinitionBuilder builder = BeanDefinitionBuilder.rootBeanDefinition(configClass);        AbstractBeanDefinition beanDefinition = builder.getBeanDefinition();        // 3. 关键点 讲 source 设置为 EnableConfigurationBeanBinding        this.setSource(beanDefinition);        // 注意： 环境中的配置属性 在此处会被绑定到 attributes （Map）的 configurationProperties 中。        Map&lt;String, Object&gt; subProperties = this.resolveSubProperties(multiple, beanName, configurationProperties);        ConfigurationBeanBindingPostProcessor.initBeanMetadataAttributes(beanDefinition, subProperties, ignoreUnknownFields, ignoreInvalidFields);        registry.registerBeanDefinition(beanName, beanDefinition);        if (this.log.isInfoEnabled()) {            this.log.info(&quot;The configuration bean definition [name : &quot; + beanName + &quot;, content : &quot; + beanDefinition + &quot;] has been registered.&quot;);        }    }    private Map&lt;String, Object&gt; resolveSubProperties(boolean multiple, String beanName, Map&lt;String, Object&gt; configurationProperties) {        if (!multiple) {            return configurationProperties;        } else {            MutablePropertySources propertySources = new MutablePropertySources();            propertySources.addLast(new MapPropertySource(&quot;_&quot;, configurationProperties));            return PropertySourcesUtils.getSubProperties(propertySources, this.environment, PropertySourcesUtils.normalizePrefix(beanName));        }    }    // 4. 将传进来的 beanDefinition 的source 设置为 com.alibaba.spring.beans.factory.annotation.EnableConfigurationBeanBinding 注解，source 在 com.alibaba.spring.beans.factory.annotation.ConfigurationBeanBindingPostProcessor 会使用到    private void setSource(AbstractBeanDefinition beanDefinition) {        beanDefinition.setSource(ENABLE_CONFIGURATION_BINDING_CLASS);    }    private void registerConfigurationBindingBeanPostProcessor(BeanDefinitionRegistry registry) {        BeanRegistrar.registerInfrastructureBean(registry, &quot;configurationBeanBindingPostProcessor&quot;, ConfigurationBeanBindingPostProcessor.class);    }    // ......    private Set&lt;String&gt; resolveMultipleBeanNames(Map&lt;String, Object&gt; properties) {        Set&lt;String&gt; beanNames = new LinkedHashSet();        for(String propertyName : properties.keySet()) {            int index = propertyName.indexOf(&quot;.&quot;);            if (index &gt; 0) {                String beanName = propertyName.substring(0, index);                beanNames.add(beanName);            }        }        return beanNames;    }    private String resolveSingleBeanName(Map&lt;String, Object&gt; properties, Class&lt;?&gt; configClass, BeanDefinitionRegistry registry) {        String beanName = (String)properties.get(&quot;id&quot;);        if (!StringUtils.hasText(beanName)) {            BeanDefinitionBuilder builder = BeanDefinitionBuilder.rootBeanDefinition(configClass);            beanName = BeanDefinitionReaderUtils.generateBeanName(builder.getRawBeanDefinition(), registry);        }        return beanName;    }    static {        ENABLE_CONFIGURATION_BINDING_CLASS_NAME = ENABLE_CONFIGURATION_BINDING_CLASS.getName();    }}</code></pre><h3 id="configurationbeanbindingpostprocessor" tabindex="-1">ConfigurationBeanBindingPostProcessor</h3><p>该类是实现了 org.springframework.beans.factory.config.BeanPostProcessor ，所以会在实例前触发方法，postProcessBeforeInitialization 。</p><pre><code class="language-java">    public Object postProcessBeforeInitialization(Object bean, String beanName) throws BeansException {        BeanDefinition beanDefinition = this.getNullableBeanDefinition(beanName);        // 这里就是看 beanDefinition 对象中的 source 属性是不是之前的 EnableConfigurationBeanBinding，是的话就直接进行属性绑定以及加载。        if (this.isConfigurationBean(bean, beanDefinition)) {            this.bindConfigurationBean(bean, beanDefinition);            this.customize(beanName, bean);        }        return bean;    }    private boolean isConfigurationBean(Object bean, BeanDefinition beanDefinition) {        return beanDefinition != null &amp;&amp; ConfigurationBeanBindingRegistrar.ENABLE_CONFIGURATION_BINDING_CLASS.equals(beanDefinition.getSource()) &amp;&amp; ObjectUtils.nullSafeEquals(this.getBeanClassName(bean), beanDefinition.getBeanClassName());    }    private void bindConfigurationBean(Object configurationBean, BeanDefinition beanDefinition) {        Map&lt;String, Object&gt; configurationProperties = getConfigurationProperties(beanDefinition);        boolean ignoreUnknownFields = getIgnoreUnknownFields(beanDefinition);        boolean ignoreInvalidFields = getIgnoreInvalidFields(beanDefinition);        // getConfigurationBeanBinder() 的结果为 org.apache.dubbo.spring.boot.autoconfigure.BinderDubboConfigBinder 实例        this.getConfigurationBeanBinder().bind(configurationProperties, ignoreUnknownFields, ignoreInvalidFields, configurationBean);        if (this.log.isInfoEnabled()) {            this.log.info(&quot;The configuration bean [&quot; + configurationBean + &quot;] have been binding by the configuration properties [&quot; + configurationProperties + &quot;]&quot;);        }    }/** *  次数运用到了spring的属性绑定功能，不再深入细究。 */class BinderDubboConfigBinder implements ConfigurationBeanBinder {    @Override    public void bind(Map&lt;String, Object&gt; configurationProperties, boolean ignoreUnknownFields,                     boolean ignoreInvalidFields, Object configurationBean) {        Iterable&lt;PropertySource&lt;?&gt;&gt; propertySources = asList(new MapPropertySource(&quot;internal&quot;, configurationProperties));        // Converts ConfigurationPropertySources        Iterable&lt;ConfigurationPropertySource&gt; configurationPropertySources = from(propertySources);        // Wrap Bindable from DubboConfig instance        Bindable bindable = Bindable.ofInstance(configurationBean);        Binder binder = new Binder(configurationPropertySources, new PropertySourcesPlaceholdersResolver(propertySources));        // Get BindHandler        BindHandler bindHandler = getBindHandler(ignoreUnknownFields, ignoreInvalidFields);        // Bind        binder.bind(&quot;&quot;, bindable, bindHandler);    }    private BindHandler getBindHandler(boolean ignoreUnknownFields,                                       boolean ignoreInvalidFields) {        BindHandler handler = BindHandler.DEFAULT;        if (ignoreInvalidFields) {            handler = new IgnoreErrorsBindHandler(handler);        }        if (!ignoreUnknownFields) {            UnboundElementsSourceFilter filter = new UnboundElementsSourceFilter();            handler = new NoUnboundElementsBindHandler(handler, filter);        }        return handler;    }}</code></pre><p>到这里，属性绑定就成功完成了。</p><h2 id="%E9%97%AE%E9%A2%98%E6%BA%90%E7%A0%81%E8%B7%9F%E8%B8%AA" tabindex="-1">问题源码跟踪</h2><p>说会一开始出现的问题，主要问题是 在启动的时候回去连接 zk ，启动是由 org.apache.dubbo.config.spring.context.DubboBootstrapApplicationListener#onApplicationContextEvent 触发 org.apache.dubbo.config.bootstrap.DubboBootstrap#start 方法，监听 spring 声明周期事件。</p><ol><li>org.apache.dubbo.config.bootstrap.DubboBootstrap#initialize</li><li>org.apache.dubbo.config.bootstrap.DubboBootstrap#startConfigCenter</li><li>org.apache.dubbo.config.bootstrap.DubboBootstrap#useRegistryAsConfigCenterIfNecessary(额外说明一下该方法，代码如下：)</li><li>org.apache.dubbo.config.bootstrap.DubboBootstrap#prepareEnvironment</li><li>org.apache.dubbo.common.config.configcenter.AbstractDynamicConfigurationFactory#getDynamicConfiguration</li><li>org.apache.dubbo.common.config.configcenter.AbstractDynamicConfigurationFactory#createDynamicConfiguration</li><li>org.apache.dubbo.configcenter.support.zookeeper.ZookeeperDynamicConfigurationFactory#createDynamicConfiguration （这里根据不同的实现类去到不同的方法，笔者这里是 zk）</li><li>org.apache.dubbo.remoting.zookeeper.ZookeeperTransporter#connect</li><li>org.apache.dubbo.remoting.zookeeper.curator.CuratorZookeeperTransporter#createZookeeperClient</li><li>org.apache.dubbo.remoting.zookeeper.curator.CuratorZookeeperClient#CuratorZookeeperClient</li><li>org.apache.curator.framework.CuratorFramework#blockUntilConnected(int, java.util.concurrent.TimeUnit)</li></ol><pre><code class="language-java">    private void useRegistryAsConfigCenterIfNecessary() {        // we use the loading status of DynamicConfiguration to decide whether ConfigCenter has been initiated.        if (environment.getDynamicConfiguration().isPresent()) {            return;        }        if (CollectionUtils.isNotEmpty(configManager.getConfigCenters())) {            return;        }        configManager                .getDefaultRegistries()                .stream()                .filter(this::isUsedRegistryAsConfigCenter)                .map(this::registryAsConfigCenter)                .forEach(configManager::addConfigCenter);    }    public List&lt;RegistryConfig&gt; getDefaultRegistries() {        return getDefaultConfigs(getConfigsMap(getTagName(RegistryConfig.class)));    }</code></pre><p>从 useRegistryAsConfigCenterIfNecessary 方法中可以看出，启动时获取的是 RegistryConfig 的配置，所以我们 consumer.timeout 是不生效的。</p><h2 id="%E6%80%BB%E7%BB%93" tabindex="-1">总结</h2><p>Dubbo 其实区分了很多的配置，这些配置也有默认值，在出现问题的时候可以查看一下自己的配置，有些关于时间的配置可以适当的延长，毕竟网络是不稳定的。</p><p>希望大家生活愉快！！</p>]]>
                </content>
            </entry>
            <entry>
                <title><![CDATA[Redis Lua脚本编写]]></title>
                <link rel="alternate" type="text/html" href="https://www.sxkawzp.cn/archives/redislua-jiao-ben-bian-xie" />
                <id>tag:https://www.sxkawzp.cn,2025-04-03:redislua-jiao-ben-bian-xie</id>
                <published>2025-04-03T10:19:41+08:00</published>
                <updated>2025-04-03T10:19:41+08:00</updated>
                <author>
                    <name>doMore</name>
                    <uri>https://www.sxkawzp.cn</uri>
                </author>
                <content type="html">
                        <![CDATA[<h2 id="%E5%89%8D%E8%A8%80" tabindex="-1">前言</h2><p>在实际的项目中引入了 redisson 这样优秀的开源框架，在研究其源码的时候，发现其使用了大量的 lua 脚本，用于处理一些比较复杂的功能。秉承着不能只知其一的原则，了解一些 redis 嵌入的 lua 引擎所支持的特性。</p><blockquote><p>因为之前学习过 lua 和 redis 源码，此处只做一些 api 方面的记录和在编写和 redis 交互的lua 脚本是需要注意的地方，防止踩坑。</p></blockquote><p>Redis包含一个嵌入式Lua 5.1解释器。解释器运行用户定义的临时脚本和函数。脚本在沙盒上下文中运行，并且只能访问特定的Lua包。</p><p>沙盒上下文的意思就是 <strong>脚本应该只对存储在Redis中的数据和作为执行参数提供的数据进行操作，不应该访问Redis服务器的底层主机系统（如：文件系统、网络和执行非API支持的系统调用的任何其他尝试）。</strong> 沙盒化的上下文也阻止了全局变量和函数的声明，在声明时都需要加 local 作为访问修饰符。 如下：</p><pre><code class="language-lua">local my_local_variable = &#39;some value&#39;local function my_local_function()  -- Do something else, but equally amazingend</code></pre><p>在沙盒上下文中不支持导入使用Lua模块。只能使用 redis 自带的库。因为禁用了Lua的require函数，以此来阻止加载模块。</p><h2 id="%E5%85%A8%E5%B1%80%E5%8F%98%E9%87%8F" tabindex="-1">全局变量</h2><ul><li>redis</li><li>KEYS</li><li>ARGV</li></ul><blockquote><p>上述变量从 2.6.0 版本就开始提供，所以只要不是远古时期的项目，都应该可用。</p></blockquote><h2 id="redis-api" tabindex="-1">redis api</h2><ol><li>redis.call(command [,arg…])</li></ol><p>该函数调用给定的Redis命令并返回其响应。它的输入是命令和参数，一旦被调用，它在Redis中执行命令并返回响应。<br />例如，我们可以从脚本中调用ECHO命令并返回它的响应，如下所示：</p><pre><code class="language-lua">return redis.call(&#39;ECHO&#39;, &#39;Echo, echo... eco... o...&#39;)</code></pre><p>如果触发运行时异常，则会自动将原始异常作为错误返回给用户。因此，尝试执行以下临时脚本将失败并生成运行时异常，因为ECHO只接受一个参数：</p><pre><code class="language-lua">redis&gt; EVAL &quot;return redis.call(&#39;ECHO&#39;, &#39;Echo,&#39;, &#39;echo... &#39;, &#39;eco... &#39;, &#39;o...&#39;)&quot; 0(error) ERR Wrong number of args calling Redis command from script script: b0345693f4b77517a711221050e76d24ae60b7f7, on @user_script:1.</code></pre><ol start="2"><li>redis.pcall(command [,arg…])</li></ol><pre><code class="language-lua">local reply = redis.pcall(&#39;ECHO&#39;, unpack(ARGV))if reply[&#39;err&#39;] ~= nil then  -- Handle the error sometime, but for now just log it  redis.log(redis.LOG_WARNING, reply[&#39;err&#39;])  reply[&#39;err&#39;] = &#39;ERR Something is wrong, but no worries, everything is under control&#39;endreturn reply</code></pre><p>此函数允许处理由Redis服务器引发的运行时错误,上述代码就是演示从临时脚本的上下文中拦截和处理运行时异常。</p><ol start="3"><li>redis.error_reply(x)</li></ol><p>这是一个返回错误响应的辅助函数。接受单个字符串参数并返回一个Lua表，其中err字段设置为该字符串。</p><pre><code class="language-lua">local text = &#39;ERR My very special error&#39;local reply1 = { err = text }local reply2 = redis.error_reply(text)-- 在这里 return reply1/reply2 效果是一样的</code></pre><ol start="4"><li>redis.status_reply(x)</li></ol><p>这里是返回正确回复的，使用方式和 第3点相同。</p><pre><code class="language-lua">local text = &#39;Frosty&#39;local status1 = { ok = text }local status2 = redis.status_reply(text)-- 在这里 return reply1/reply2 效果是一样的</code></pre><ol start="5"><li>redis.sha1hex(x)</li></ol><p>这个函数返回其单个字符串参数的SHA1十六进制摘要。</p><pre><code class="language-lua">-- 获取 空字符串的 sha1 摘要redis&gt; EVAL &quot;return redis.sha1hex(&#39;&#39;)&quot; 0&quot;da39a3ee5e6b4b0d3255bfef95601890afd80709&quot;</code></pre><ol start="6"><li>redis.log(level, message)</li></ol><p>该函数主要是可以输出 redis server 日志。</p><p>level 选项如下：</p><ul><li>redis.LOG_DEBUG</li><li>redis.LOG_VERBOSE</li><li>redis.LOG_NOTICE</li><li>redis.LOG_WARNING</li></ul><ol start="7"><li>redis.setresp(x)</li></ol><blockquote><p>version 6.0.0</p></blockquote><p>该函数允许执行脚本在 redis.call 和 redis.pcall 返回的响应之间切换 Redis Serialization Protocol（RESP）版本。它需要一个数字参数作为协议的版本。默认的协议版本是2，但可以切换到版本3。</p><pre><code class="language-lua">redis.setresp(3)</code></pre><ol start="8"><li>redis.register_function</li></ol><blockquote><p>version: 7.0.0</p></blockquote><p>此函数仅在function LOAD命令的上下文中可用。当被调用时，它将一个函数注册到加载的库中。可以使用位置参数或命名参数调用该函数。</p><p>Named arguments: redis.register_function{function_name=name, callback=callback, flags={flag1, flag2, …}, description=description}</p><p>The named arguments variant accepts the following arguments:<br />function_name: the function’s name.<br />callback: the function’s callback.<br />flags: an array of strings, each a function flag (optional).<br />description: function’s description (optional).</p><pre><code class="language-lua">redis&gt; FUNCTION LOAD &quot;#!lua name=mylib\n redis.register_function{function_name=&#39;noop&#39;, callback=function() end, flags={ &#39;no-writes&#39; }, description=&#39;Does nothing&#39;}&quot;</code></pre><p>更多详情请看：<a href="https://redis.io/docs/latest/develop/interact/programmability/lua-api/#redis.register_function" target="_blank">register_function</a></p><ol start="9"><li>redis.REDIS_VERSION</li></ol><blockquote><p>version: 7.0.0<br />以Lua字符串的形式返回当前Redis服务器版本。回复的格式是 MM.mm.PP 。</p></blockquote><ul><li>MM：主版本。</li><li>mm: 小版本。</li><li>PP：补丁级别。</li></ul><ol start="10"><li>select index</li></ol><p>可以在脚本中世界使用 select 命令已达到切换库的能力。</p><h2 id="%E6%95%B0%E6%8D%AE%E7%B1%BB%E5%9E%8B%E8%BD%AC%E6%8D%A2" tabindex="-1">数据类型转换</h2><p>Redis的响应和Lua的数据类型之间是一对一的映射，Lua的数据类型和Redis协议的数据类型之间是一对一的映射。从Redis协议响应（即 redis.call 和 redis.pcall ）到Lua数据类型的类型转换取决于脚本使用的Redis序列化协议版本。脚本执行期间的默认协议版本是RESP2。脚本可以通过调用 redis.setresp 函数来切换应答的协议版本。</p><h3 id="resp2-%E8%BD%AC%E6%8D%A2-lua-%E7%B1%BB%E5%9E%8B" tabindex="-1">RESP2 转换 lua 类型</h3><ul><li>RESP2 integer reply -&gt; Lua number</li><li>RESP2 bulk string reply -&gt; Lua string</li><li>RESP2 array reply -&gt; Lua table (may have other Redis data types nested)</li><li>RESP2 status reply -&gt; Lua table with a single ok field containing the status string</li><li>RESP2 error reply -&gt; Lua table with a single err field containing the error - string</li><li>RESP2 null bulk reply and RESP2 null multi-bulk reply -&gt; Lua false boolean type</li></ul><h3 id="lua-%E7%B1%BB%E5%9E%8B%E8%BD%AC%E6%8D%A2-resp2" tabindex="-1">lua 类型转换 RESP2</h3><ul><li>Lua number -&gt; RESP2 integer reply (the number is converted into an integer)</li><li>Lua string -&gt; RESP2 bulk string reply</li><li>Lua table (indexed, non-associative array) -&gt; RESP2 array reply (truncated at the first Lua nil value encountered in the table, if any)</li><li>Lua table with a single ok field -&gt; RESP2 status reply</li><li>Lua table with a single err field -&gt; RESP2 error reply</li><li>Lua boolean false -&gt; RESP2 null bulk reply</li><li>Lua Boolean true -&gt; RESP2 integer reply with value of 1.  这是一个额外的规则，RESP2 转换成 lua 的时候并没有。</li></ul><h3 id="%E7%89%B9%E6%AE%8A%E6%83%85%E5%86%B5" tabindex="-1">特殊情况</h3><pre><code class="language-lua">-- 正确的使用方式redis&gt; EVAL &quot;return 10&quot; 0(integer) 10redis&gt; EVAL &quot;return { 1, 2, { 3, &#39;Hello World!&#39; } }&quot; 01) (integer) 12) (integer) 23) 1) (integer) 3   1) &quot;Hello World!&quot;redis&gt; EVAL &quot;return redis.call(&#39;get&#39;,&#39;foo&#39;)&quot; 0&quot;bar&quot;-- 1. Lua只有一个数字类型，Lua数字。整数和浮点数之间没有区别。因此，直接将数字类型返回会丢失数字的小数部分（如果有的话）。如果你想返回一个Lua浮点数，它应该作为一个字符串返回 如下 3.3333 已经被丢弃。-- 2. 由于Lua的表语义，没有简单的方法可以在Lua数组中使用nil。因此，当Redis将Lua数组转换为 RESP2 时，当遇到Lua nil值时转换停止 如下： nil 后面的 bar 被丢弃。-- 3. 当一个Lua表是一个包含键和它们各自值的关联数组时，转换后的Redis响应将丢弃，如下： somekey = &#39;somevalue&#39;。redis&gt; EVAL &quot;return { 1, 2, 3.3333, somekey = &#39;somevalue&#39;, &#39;foo&#39;, nil , &#39;bar&#39; }&quot; 01) (integer) 12) (integer) 23) (integer) 34) &quot;foo&quot;</code></pre><p>注意： 在实际场景中并没有遇到使用 RESP3 的场景，有想法的可以继续了解，此处不在说明。 <a href="https://redis.io/docs/latest/develop/interact/programmability/lua-api/#resp3-to-lua-type-conversion" target="_blank">resp3-to-lua-type-conversion</a></p><h2 id="%E8%BF%90%E8%A1%8C%E6%97%B6%E5%BA%93" tabindex="-1">运行时库</h2><p>标准的lua库 <a href="https://www.lua.org/manual/5.1/manual.html#5.4" target="_blank">Lua 官方文档</a>：</p><ul><li>The String Manipulation (string) library</li><li>The Table Manipulation (table) library</li><li>The Mathematical Functions (math) library</li><li>The Operating System Facilities (os) library</li></ul><p>此外，脚本还可以加载并访问以下外部库 <a href="https://redis.io/docs/latest/develop/interact/programmability/lua-api/#struct-library" target="_blank">外部库文档说明</a>：</p><ul><li>The struct library</li><li>The cjson library</li><li>The cmsgpack library</li><li>The bitop library</li></ul><h2 id="%E6%80%BB%E7%BB%93" tabindex="-1">总结</h2><p>Redis保证脚本的原子执行。在执行脚本时，所有服务器活动在整个运行时期间都被阻塞。这些语义意味着脚本的所有效果要么尚未发生，要么已经发生。我们使用 lua 脚本的时可以根据这个特性以达到数据一致性的目的，但是脚本尽可能不要复杂，毕竟会影响性能，在网络开销和脚本执行之间平衡。毕竟在网络中，不可能百分之百的保证环境是不变的，只能在复杂的情况中选取对自己有利的。</p>]]>
                </content>
            </entry>
            <entry>
                <title><![CDATA[Jackson的奇技淫巧]]></title>
                <link rel="alternate" type="text/html" href="https://www.sxkawzp.cn/archives/jackson-de-qi-ji-yin-qiao" />
                <id>tag:https://www.sxkawzp.cn,2025-03-31:jackson-de-qi-ji-yin-qiao</id>
                <published>2025-03-31T11:47:57+08:00</published>
                <updated>2025-03-31T11:49:31+08:00</updated>
                <author>
                    <name>doMore</name>
                    <uri>https://www.sxkawzp.cn</uri>
                </author>
                <content type="html">
                        <![CDATA[<h2 id="%E6%8A%80%E5%B7%A7%E4%B8%80" tabindex="-1">技巧一</h2><p>如何使用Jackson处理Java中多态类型的序列化与反序列化问题。</p><p>通过 <strong>@JsonTypeInfo</strong> 注解和全局 <strong>DefaultTyping</strong> 机制确保多态类型的正确转换。</p><p>在将多态类型进行JSON序列化后，Jackson无法在反序列化期间找出正确的类型。</p><h3 id="%40jsontypeinfo" tabindex="-1">@JsonTypeInfo</h3><h4 id="%E5%9C%A8%E7%B1%BB%E4%B8%8A%E4%BD%BF%E7%94%A8%40jsontypeinfo" tabindex="-1">在类上使用@JsonTypeInfo</h4><pre><code class="language-java">// 配置指定使用的类名称（use = JsonTypeInfo.Id.CLASS），并将其作为JSON属性保留（include = JsonTypeInfo.As.PROPERTY）。 属性名称指定为’className’@JsonTypeInfo(    use = JsonTypeInfo.Id.CLASS,     include = JsonTypeInfo.As.PROPERTY,     property = &quot;className&quot;)public class Shape {}</code></pre><h4 id="%E5%9C%A8%E5%B1%9E%E6%80%A7%E4%B8%8A%E4%BD%BF%E7%94%A8%40jsontypeinfo" tabindex="-1">在属性上使用@JsonTypeInfo</h4><pre><code class="language-java">@Data@NoArgsConstructor@RequiredArgsConstructor(staticName = &quot;of&quot;)public class Containers {    @NonNull    @JsonTypeInfo(use = JsonTypeInfo.Id.CLASS)    private List&lt;Shape&gt; shapes;}</code></pre><blockquote><p>注意：如果该注解同时作用在类和属性上，则以使用在属性上的注解为准，因为它被认为更具体。需要特别说明的是， 当@JsonTypeInfo在属性（字段，方法）上使用时，此注解适用于值。 当在集合类型（List，Map，Array）上使用时，它将应用于元素，而不是集合本身。 对于非集合类型，没有区别。</p></blockquote><h3 id="defaulttyping" tabindex="-1">DefaultTyping</h3><ul><li>JAVA_LANG_OBJECT： 当对象属性类型为Object时生效</li><li>OBJECT_AND_NON_CONCRETE： 当对象属性类型为Object或者非具体类型（抽象类和接口）时生效</li><li>NON_CONCRETE_AND+_ARRAYS： 同上, 另外所有的数组元素的类型都是非具体类型或者对象类型</li><li>NON_FINAL： 对所有非final类型或者非final类型元素的数组</li></ul><blockquote><p>开启DefaultTyping，并且让所有的非final类型对象持久化时都存储类型信息显然能准确的反序列多态类型的数据。</p></blockquote><pre><code class="language-java">ObjectMapper objectMapper = new ObjectMapper();objectMapper.activateDefaultTyping(    LaissezFaireSubTypeValidator.instance,     ObjectMapper.DefaultTyping.NON_FINAL,     JsonTypeInfo.As.PROPERTY);</code></pre><h2 id="%E6%8A%80%E5%B7%A7%E4%BA%8C" tabindex="-1">技巧二</h2><p>对象是怎样被序列化的？序列化的时候字段是怎样的优先级？</p><p>对象的属性被初次确定的过程称为自动检测：所有的成员方法和字段被查找。</p><ul><li>“Getter”方法：所有public，带返回值，符合“getXxx”（“isXxx”，如果返回boolean,被称为“isgetter”）</li><li>field属性：所有public成员字段被推测要显示的属性，使用字段名字来序列化。</li></ul><p>在相同的逻辑属性中同时存在getter和field的情况下。getter方法优先被使用（field被忽略）。下面介绍一下可以改变自动检测的工具。</p><h3 id="com.fasterxml.jackson.annotation.jsonautodetect" tabindex="-1">com.fasterxml.jackson.annotation.JsonAutoDetect</h3><p>如果默认的自动检测（field和成员方法必须public）不是你想要的，可以下面的方法很容易的改变。</p><ul><li><p>@JsonAutoDetect 注解定义在class上；属性”fieldvisibility”，“gettervisibility”,“isgettervisibility”定义</p></li><li><p>ObjectMapper.setVisibilityChecker()可以被使用自定义最小化可见检测</p></li></ul><pre><code class="language-java">public class JacksonTest {    public static void main(String[] args) throws JsonProcessingException {        ObjectMapper mapper = new ObjectMapper();//        mapper.setVisibility(PropertyAccessor.FIELD, JsonAutoDetect.Visibility.ANY);//        mapper.setVisibility(PropertyAccessor.GETTER, JsonAutoDetect.Visibility.NONE);//        mapper.setVisibility(PropertyAccessor.IS_GETTER, JsonAutoDetect.Visibility.NONE);        Car car = new Car();        car.setName(&quot;奔驰&quot;);        car.setImg(&quot;123456.jpg&quot;);        String s = mapper.writeValueAsString(car);        System.out.println(s);    }//    @JsonAutoDetect(fieldVisibility = JsonAutoDetect.Visibility.ANY,//            getterVisibility = JsonAutoDetect.Visibility.NONE,//            isGetterVisibility = JsonAutoDetect.Visibility.NONE)    @Data    public static class Car {        private String name;        private String img;        public String getTest() {            return &quot;固定字段&quot;;        }        public Boolean isTestBoolean() {            return true;        }    }}// 上述代码输出内容如下：// {&quot;name&quot;:&quot;奔驰&quot;,&quot;img&quot;:&quot;123456.jpg&quot;,&quot;testBoolean&quot;:true,&quot;test&quot;:&quot;固定字段&quot;}// 放开代码中注释输出如下：// {&quot;name&quot;:&quot;奔驰&quot;,&quot;img&quot;:&quot;123456.jpg&quot;}</code></pre><blockquote><p>注意：上述两种方式，使用其中一种就可以，objectMapper 是一种全局的方式</p></blockquote><h2 id="%E6%8A%80%E5%B7%A7%E4%B8%89" tabindex="-1">技巧三</h2><p>明确的忽视属性</p><p>首先设置可能存在的自动检测属性，再通过每个属性注解来进一步修改序列化。</p><ul><li>@JsonProperty (@JsonGetter, @JsonAnyGetter)</li><li>@JsonIgnore</li><li>类注解 @JsonIgnoreProperties 被用于例举属性的逻辑名并不被序列化；</li></ul><pre><code class="language-java">  @JsonIgnoreProperties({ &quot;internal&quot; })  public class Bean {    public Settings getInternal() { ... } // ignored    @JsonIgnore     public Settinger getBogus(); // likewise ignored    public String getName(); // but this would be serialized  }</code></pre><h2 id="%E6%8A%80%E5%B7%A7%E5%9B%9B" tabindex="-1">技巧四</h2><p>定义profiles用于动态过滤，使用 <strong>com.fasterxml.jackson.annotation.JsonView</strong> 。</p><pre><code class="language-java">public class JacksonTest {    public static void main(String[] args) throws JsonProcessingException {        ObjectMapper mapper = new ObjectMapper();//        mapper.setVisibility(PropertyAccessor.FIELD, JsonAutoDetect.Visibility.ANY);//        mapper.setVisibility(PropertyAccessor.GETTER, JsonAutoDetect.Visibility.NONE);//        mapper.setVisibility(PropertyAccessor.IS_GETTER, JsonAutoDetect.Visibility.NONE);        Car car = new Car();        car.setName(&quot;奔驰&quot;);        car.setImg(&quot;123456.jpg&quot;);        car.setColor(&quot;#f40&quot;);        car.setBackgroundColor(&quot;#000000&quot;);        // springmvc/springboot如果标示为RestController，实际上是在后台了配置MappingJackson2HttpMessageConverter转换器，直接使用就好        // 使用接口来声明多个视图        // 在pojo的get方法上指定视图        // 在Controller方法上指定视图        String s = mapper.writerWithView(BackgroundColorView.class).writeValueAsString(car);        System.out.println(s);    }    @JsonAutoDetect(fieldVisibility = JsonAutoDetect.Visibility.ANY,            getterVisibility = JsonAutoDetect.Visibility.NONE,            isGetterVisibility = JsonAutoDetect.Visibility.NONE)    @Data    public static class Car {        private String name;        private String img;        @JsonView(ColorView.class)        private String color;        @JsonView(BackgroundColorView.class)        private String backgroundColor;        public String getTest() {            return &quot;固定字段&quot;;        }        public Boolean isTestBoolean() {            return true;        }    }    public interface ColorView {    }    public interface BackgroundColorView extends ColorView {    }</code></pre>]]>
                </content>
            </entry>
            <entry>
                <title><![CDATA[关于拉取Apollo配置中心Namespace逻辑探析]]></title>
                <link rel="alternate" type="text/html" href="https://www.sxkawzp.cn/archives/guan-yu-la-qu-apollo-pei-zhi-zhong-xin-namespace-luo-ji-tan-xi" />
                <id>tag:https://www.sxkawzp.cn,2025-03-07:guan-yu-la-qu-apollo-pei-zhi-zhong-xin-namespace-luo-ji-tan-xi</id>
                <published>2025-03-07T14:59:14+08:00</published>
                <updated>2025-03-07T15:09:13+08:00</updated>
                <author>
                    <name>doMore</name>
                    <uri>https://www.sxkawzp.cn</uri>
                </author>
                <content type="html">
                        <![CDATA[<h2 id="apollo-%E7%AE%80%E4%BB%8B" tabindex="-1">Apollo 简介</h2><p>Apollo（阿波罗）是一款可靠的分布式配置管理中心，诞生于携程框架研发部，能够集中化管理应用不同环境、不同集群的配置，配置修改后能够实时推送到应用端，并且具备规范的权限、流程治理等特性，适用于微服务配置管理场景。</p><h3 id="%E9%A1%B9%E7%9B%AE%E7%BB%93%E6%9E%84" tabindex="-1">项目结构</h3><ul><li>Config Service提供配置的读取、推送等功能，服务对象是Apollo客户端。</li><li>Admin Service提供配置的修改、发布等功能，服务对象是Apollo Portal（管理界面）。</li><li>Config Service和Admin Service都是多实例、无状态部署，所以需要将自己注册到Eureka中并保持心跳。</li><li>Client通过域名访问Meta Server获取Config Service服务列表（IP+Port），而后直接通过IP+Port访问服务，同时在Client侧会做load balance、错误重试。</li><li>Portal通过域名访问Meta Server获取Admin Service服务列表（IP+Port），而后直接通过IP+Port访问服务，同时在Portal侧会做load balance、错误重试。</li></ul><blockquote><p>注意：本次所谈内容默认已经成功部署 Apollo server 端，并且在项目中也成功结合了 Apollo Client。</p></blockquote><h2 id="client-%E6%8B%89%E5%8F%96" tabindex="-1">Client 拉取</h2><ol><li>入口类 com.ctrip.framework.apollo.spring.boot.ApolloApplicationContextInitializer 。该类由 spring.factories 进行加载，实现了 org.springframework.boot.env.EnvironmentPostProcessor ，所以在 spring 环境变量 Enviroment 加载的时候  会执行方法 org.springframework.boot.env.EnvironmentPostProcessor#postProcessEnvironment  。</li></ol><pre><code class="language-java">  @Override  public void postProcessEnvironment(ConfigurableEnvironment configurableEnvironment, SpringApplication springApplication) {    // should always initialize system properties like app.id in the first place    initializeSystemProperty(configurableEnvironment);    Boolean eagerLoadEnabled = configurableEnvironment.getProperty(PropertySourcesConstants.APOLLO_BOOTSTRAP_EAGER_LOAD_ENABLED, Boolean.class, false);    //EnvironmentPostProcessor should not be triggered if you don&#39;t want Apollo Loading before Logging System Initialization    // apollo.bootstrap.eagerLoad.enabled 是否 直接加载配置    if (!eagerLoadEnabled) {      return;    }    Boolean bootstrapEnabled = configurableEnvironment.getProperty(PropertySourcesConstants.APOLLO_BOOTSTRAP_ENABLED, Boolean.class, false);    if (bootstrapEnabled) {      DeferredLogger.enable();      // 远程配置的加载      initialize(configurableEnvironment);    }  }protected void initialize(ConfigurableEnvironment environment) {    if (environment.getPropertySources().contains(PropertySourcesConstants.APOLLO_BOOTSTRAP_PROPERTY_SOURCE_NAME)) {      //already initialized, replay the logs that were printed before the logging system was initialized      DeferredLogger.replayTo();      return;    }   // 从环境变量获取 apollo.bootstrap.namespaces 的值，可以获取到填写的application    String namespaces = environment.getProperty(PropertySourcesConstants.APOLLO_BOOTSTRAP_NAMESPACES, ConfigConsts.NAMESPACE_APPLICATION);    logger.debug(&quot;Apollo bootstrap namespaces: {}&quot;, namespaces);    // 因为namespaces可以配置多个，这里根据逗号转为集合列表    List&lt;String&gt; namespaceList = NAMESPACE_SPLITTER.splitToList(namespaces);    CompositePropertySource composite;    final ConfigUtil configUtil = ApolloInjector.getInstance(ConfigUtil.class);    if (configUtil.isPropertyNamesCacheEnabled()) {      composite = new CachedCompositePropertySource(PropertySourcesConstants.APOLLO_BOOTSTRAP_PROPERTY_SOURCE_NAME);    } else {      composite = new CompositePropertySource(PropertySourcesConstants.APOLLO_BOOTSTRAP_PROPERTY_SOURCE_NAME);    }    for (String namespace : namespaceList) {      // 获取config，这个config很关键，里面就包含了namespace文件的内容，下面要分析这个方法      Config config = ConfigService.getConfig(namespace);      // 根据config转换为spring environment需要的propertySource      composite.addPropertySource(configPropertySourceFactory.getConfigPropertySource(namespace, config));    }    // 加到所有PropertySources最前面，这里可以看到优先级就是最高的了    environment.getPropertySources().addFirst(composite);  }</code></pre><p>需要注意的是，很多时候项目中会使用 @EnableApolloConfig 去配置 Namespace ，该注解的配置内容会由 <strong>@Import(ApolloConfigRegistrar.class)</strong> 进行加载。 在上述 environment.getProperty 时已经转换完成。</p><ol start="2"><li>获取 Config 内容</li></ol><ul><li>com.ctrip.framework.apollo.ConfigService#getConfig</li><li><ul><li>com.ctrip.framework.apollo.internals.DefaultConfigManager#getConfig</li></ul></li><li><ul><li><ul><li>com.ctrip.framework.apollo.spi.DefaultConfigFactory#create</li></ul></li></ul></li></ul><pre><code class="language-java">  @Override  public Config create(String namespace) {    ConfigFileFormat format = determineFileFormat(namespace);    ConfigRepository configRepository = null;    // although ConfigFileFormat.Properties are compatible with themselves we    // should not create a PropertiesCompatibleFileConfigRepository for them    // calling the method &#96;createLocalConfigRepository(...)&#96; is more suitable    // for ConfigFileFormat.Properties    if (ConfigFileFormat.isPropertiesCompatible(format) &amp;&amp;        format != ConfigFileFormat.Properties) {      configRepository = createPropertiesCompatibleFileConfigRepository(namespace, format);    } else {      // 获取配置信息      configRepository = createConfigRepository(namespace);    }    logger.debug(&quot;Created a configuration repository of type [{}] for namespace [{}]&quot;,        configRepository.getClass().getName(), namespace);    // 这个是关键点，大部分核心代码都在这里    return this.createRepositoryConfig(namespace, configRepository);  }  ConfigRepository createConfigRepository(String namespace) {    // 判断是否配置只走本地缓存文件的方式，这里默认走这个分析    if (m_configUtil.isPropertyFileCacheEnabled()) {      return createLocalConfigRepository(namespace);    }    return createRemoteConfigRepository(namespace);  }    LocalFileConfigRepository createLocalConfigRepository(String namespace) {    if (m_configUtil.isInLocalMode()) {      logger.warn(          &quot;==== Apollo is in local mode! Won&#39;t pull configs from remote server for namespace {} ! ====&quot;,          namespace);      return new LocalFileConfigRepository(namespace);    }    // 这创建了一个LocalFileConfigRepository，里面又包装了一个RemoteConfigRepository，    return new LocalFileConfigRepository(namespace, createRemoteConfigRepository(namespace));  }  RemoteConfigRepository createRemoteConfigRepository(String namespace) {    return new RemoteConfigRepository(namespace);  }  protected Config createRepositoryConfig(String namespace, ConfigRepository configRepository) {    return new DefaultConfig(namespace, configRepository);  }    public DefaultConfig(String namespace, ConfigRepository configRepository) {    m_namespace = namespace;    m_resourceProperties = loadFromResource(m_namespace);    m_configRepository = configRepository;    m_configProperties = new AtomicReference&lt;&gt;();    m_warnLogRateLimiter = RateLimiter.create(0.017); // 1 warning log output per minute    initialize();  }  private void initialize() {    try {        // 获取远程的配置      updateConfig(m_configRepository.getConfig(), m_configRepository.getSourceType());    } catch (Throwable ex) {      Tracer.logError(ex);      logger.warn(&quot;Init Apollo Local Config failed - namespace: {}, reason: {}.&quot;,          m_namespace, ExceptionUtil.getDetailMessage(ex));    } finally {      //register the change listener no matter config repository is working or not      //so that whenever config repository is recovered, config could get changed      // 增加监听器      // 这里发现对传进来的LocalFileConfigRepository调用了addChangeListener，同样的方式，      // 让当前类来监听LocalFileConfigRepository，      // 所以监听可以传递到当前类DefaultConfig的onRepositoryChange方法了      m_configRepository.addChangeListener(this);    }  }// com.ctrip.framework.apollo.internals.RemoteConfigRepository#getConfig  @Override  public Properties getConfig() {    // 没有缓存信息就去同步远程配置    if (m_configCache.get() == null) {      this.sync();    }    return transformApolloConfigToProperties(m_configCache.get());  }  @Override  protected synchronized void sync() {    Transaction transaction = Tracer.newTransaction(&quot;Apollo.ConfigService&quot;, &quot;syncRemoteConfig&quot;);    try {      // 获取当前的ApolloConfig      ApolloConfig previous = m_configCache.get();      // 获取远程的ApolloConfig      // 该方法就是根据 appId、namespace、secret 等信息去请求 /configs/{{appId}}/{{clusterName}}/{{namespace}} 借口获取相应的配置信息，并且有重试机制等 。      // appId 在没有指定的情况在 会有一个默认值 ApolloNoAppIdPlaceHolder      ApolloConfig current = loadApolloConfig();      //reference equals means HTTP 304      if (previous != current) {        logger.debug(&quot;Remote Config refreshed!&quot;);        // 如果本地和远程不一样，则更新        m_configCache.set(current);        // (this.getConfig()是根据ApolloConfig获取一个Properties)        this.fireRepositoryChange(m_namespace, this.getConfig());      }      if (current != null) {        Tracer.logEvent(String.format(&quot;Apollo.Client.Configs.%s&quot;, current.getNamespaceName()),                current.getReleaseKey());      }      transaction.setStatus(Transaction.SUCCESS);    } catch (Throwable ex) {      transaction.setStatus(ex);      throw ex;    } finally {      transaction.complete();    }  }</code></pre><h2 id="server-%E7%AB%AF%E6%8E%A5%E5%8F%A3" tabindex="-1">Server 端接口</h2><ol><li>入口接口 com.ctrip.framework.apollo.configservice.controller.ConfigController#queryConfig 请求地址 -&gt; /configs/{{appId}}/{{clusterName}}/{{namespace}}</li></ol><pre><code class="language-java">@GetMapping(value = &quot;/{appId}/{clusterName}/{namespace:.+}&quot;)  public ApolloConfig queryConfig(@PathVariable String appId, @PathVariable String clusterName,                                  @PathVariable String namespace,                                  @RequestParam(value = &quot;dataCenter&quot;, required = false) String dataCenter,                                  @RequestParam(value = &quot;releaseKey&quot;, defaultValue = &quot;-1&quot;) String clientSideReleaseKey,                                  @RequestParam(value = &quot;ip&quot;, required = false) String clientIp,                                  @RequestParam(value = &quot;label&quot;, required = false) String clientLabel,                                  @RequestParam(value = &quot;messages&quot;, required = false) String messagesAsString,                                  HttpServletRequest request, HttpServletResponse response) throws IOException {    String originalNamespace = namespace;    //strip out .properties suffix    namespace = namespaceUtil.filterNamespaceName(namespace);    //fix the character case issue, such as FX.apollo &lt;-&gt; fx.apollo    namespace = namespaceUtil.normalizeNamespace(appId, namespace);    if (Strings.isNullOrEmpty(clientIp)) {      clientIp = tryToGetClientIp(request);    }    ApolloNotificationMessages clientMessages = transformMessages(messagesAsString);    List&lt;Release&gt; releases = Lists.newLinkedList();    String appClusterNameLoaded = clusterName;    // 提供了 APPID 的根据 APPID 去加载    if (!ConfigConsts.NO_APPID_PLACEHOLDER.equalsIgnoreCase(appId)) {      Release currentAppRelease = configService.loadConfig(appId, clientIp, clientLabel, appId, clusterName, namespace,          dataCenter, clientMessages);      if (currentAppRelease != null) {        releases.add(currentAppRelease);        //we have cluster search process, so the cluster name might be overridden        appClusterNameLoaded = currentAppRelease.getClusterName();      }    }    //if namespace does not belong to this appId, should check if there is a public configuration    // 如果当前 namespace 不属于 指定的 appId，则检查 namespace 是不是一个 public，是一个 public 也可以正常加载    if (!namespaceBelongsToAppId(appId, namespace)) {      Release publicRelease = this.findPublicConfig(appId, clientIp, clientLabel, clusterName, namespace,          dataCenter, clientMessages);      if (Objects.nonNull(publicRelease)) {        releases.add(publicRelease);      }    }    if (releases.isEmpty()) {      response.sendError(HttpServletResponse.SC_NOT_FOUND,          String.format(              &quot;Could not load configurations with appId: %s, clusterName: %s, namespace: %s&quot;,              appId, clusterName, originalNamespace));      Tracer.logEvent(&quot;Apollo.Config.NotFound&quot;,          assembleKey(appId, clusterName, originalNamespace, dataCenter));      return null;    }    auditReleases(appId, clusterName, dataCenter, clientIp, releases);    String mergedReleaseKey = releases.stream().map(Release::getReleaseKey)            .collect(Collectors.joining(ConfigConsts.CLUSTER_NAMESPACE_SEPARATOR));    if (mergedReleaseKey.equals(clientSideReleaseKey)) {      // Client side configuration is the same with server side, return 304      response.setStatus(HttpServletResponse.SC_NOT_MODIFIED);      Tracer.logEvent(&quot;Apollo.Config.NotModified&quot;,          assembleKey(appId, appClusterNameLoaded, originalNamespace, dataCenter));      return null;    }    ApolloConfig apolloConfig = new ApolloConfig(appId, appClusterNameLoaded, originalNamespace,        mergedReleaseKey);    apolloConfig.setConfigurations(mergeReleaseConfigurations(releases));    Tracer.logEvent(&quot;Apollo.Config.Found&quot;, assembleKey(appId, appClusterNameLoaded,        originalNamespace, dataCenter));    return apolloConfig;  }@GetMapping(value = &quot;/{appId}/{clusterName}/{namespace:.+}&quot;)  public ApolloConfig queryConfig(@PathVariable String appId, @PathVariable String clusterName,                                  @PathVariable String namespace,                                  @RequestParam(value = &quot;dataCenter&quot;, required = false) String dataCenter,                                  @RequestParam(value = &quot;releaseKey&quot;, defaultValue = &quot;-1&quot;) String clientSideReleaseKey,                                  @RequestParam(value = &quot;ip&quot;, required = false) String clientIp,                                  @RequestParam(value = &quot;label&quot;, required = false) String clientLabel,                                  @RequestParam(value = &quot;messages&quot;, required = false) String messagesAsString,                                  HttpServletRequest request, HttpServletResponse response) throws IOException {    String originalNamespace = namespace;    //strip out .properties suffix    namespace = namespaceUtil.filterNamespaceName(namespace);    //fix the character case issue, such as FX.apollo &lt;-&gt; fx.apollo    namespace = namespaceUtil.normalizeNamespace(appId, namespace);    if (Strings.isNullOrEmpty(clientIp)) {      clientIp = tryToGetClientIp(request);    }    ApolloNotificationMessages clientMessages = transformMessages(messagesAsString);    List&lt;Release&gt; releases = Lists.newLinkedList();    String appClusterNameLoaded = clusterName;    // 提供了 APPID 的根据 APPID 去加载    if (!ConfigConsts.NO_APPID_PLACEHOLDER.equalsIgnoreCase(appId)) {      Release currentAppRelease = configService.loadConfig(appId, clientIp, clientLabel, appId, clusterName, namespace,          dataCenter, clientMessages);      if (currentAppRelease != null) {        releases.add(currentAppRelease);        //we have cluster search process, so the cluster name might be overridden        appClusterNameLoaded = currentAppRelease.getClusterName();      }    }    //if namespace does not belong to this appId, should check if there is a public configuration    // 如果当前 namespace 不属于 指定的 appId，则检查 namespace 是不是一个 public，是一个 public 也可以正常加载    if (!namespaceBelongsToAppId(appId, namespace)) {      Release publicRelease = this.findPublicConfig(appId, clientIp, clientLabel, clusterName, namespace,          dataCenter, clientMessages);      if (Objects.nonNull(publicRelease)) {        releases.add(publicRelease);      }    }    if (releases.isEmpty()) {      response.sendError(HttpServletResponse.SC_NOT_FOUND,          String.format(              &quot;Could not load configurations with appId: %s, clusterName: %s, namespace: %s&quot;,              appId, clusterName, originalNamespace));      Tracer.logEvent(&quot;Apollo.Config.NotFound&quot;,          assembleKey(appId, clusterName, originalNamespace, dataCenter));      return null;    }    auditReleases(appId, clusterName, dataCenter, clientIp, releases);    String mergedReleaseKey = releases.stream().map(Release::getReleaseKey)            .collect(Collectors.joining(ConfigConsts.CLUSTER_NAMESPACE_SEPARATOR));    if (mergedReleaseKey.equals(clientSideReleaseKey)) {      // Client side configuration is the same with server side, return 304      response.setStatus(HttpServletResponse.SC_NOT_MODIFIED);      Tracer.logEvent(&quot;Apollo.Config.NotModified&quot;,          assembleKey(appId, appClusterNameLoaded, originalNamespace, dataCenter));      return null;    }    ApolloConfig apolloConfig = new ApolloConfig(appId, appClusterNameLoaded, originalNamespace,        mergedReleaseKey);    apolloConfig.setConfigurations(mergeReleaseConfigurations(releases));    Tracer.logEvent(&quot;Apollo.Config.Found&quot;, assembleKey(appId, appClusterNameLoaded,        originalNamespace, dataCenter));    return apolloConfig;  }</code></pre><h2 id="%E6%80%BB%E7%BB%93" tabindex="-1">总结</h2><p>在指定了 appId 的情况下，没有指定 namespace 获取不到配置，制定了 namespace，但是不属于当前 appId 的，则看是不是 public ，如果是 public 也可以正常获取配置信息。</p>]]>
                </content>
            </entry>
            <entry>
                <title><![CDATA[Mybatis-Plus优雅实现数据权限]]></title>
                <link rel="alternate" type="text/html" href="https://www.sxkawzp.cn/archives/mybatis-plus-you-ya-shi-xian-shu-ju-quan-xian" />
                <id>tag:https://www.sxkawzp.cn,2025-03-03:mybatis-plus-you-ya-shi-xian-shu-ju-quan-xian</id>
                <published>2025-03-03T16:57:19+08:00</published>
                <updated>2025-03-03T16:58:31+08:00</updated>
                <author>
                    <name>doMore</name>
                    <uri>https://www.sxkawzp.cn</uri>
                </author>
                <content type="html">
                        <![CDATA[<h2 id="%E5%89%8D%E8%A8%80" tabindex="-1">前言</h2><blockquote><p>笔者默认大伙已经了解 Mybatis 插件机制（org.apache.ibatis.plugin.Interceptor）</p></blockquote><h2 id="%E6%8E%A2%E6%9E%90" tabindex="-1">探析</h2><h3 id="mybatis-plus-extension-%E4%BB%8B%E7%BB%8D" tabindex="-1">mybatis-plus-extension 介绍</h3><p>mybatis-plus 扩展功能，包括分页，sql解析，spring集成。</p><blockquote><p>通常情况下，都会结合 spring 去使用。所以便以此前提去分析。</p></blockquote><p>在这个依赖中我们需要注意的是 com.baomidou.mybatisplus.extension.plugins.MybatisPlusInterceptor 类，他实现了 Mybatis 提供的 Interceptor，使它可以作为 Mybatis 的一个插件进行使用。</p><pre><code class="language-java">    // Mybatis-plus 中自己的拦截器，更加细致的区分了 查询、增加、修改所需要的权限    @Setter    private List&lt;InnerInterceptor&gt; interceptors = new ArrayList&lt;&gt;();    @Override    public Object intercept(Invocation invocation) throws Throwable {        Object target = invocation.getTarget();        Object[] args = invocation.getArgs();        if (target instanceof Executor) {            final Executor executor = (Executor) target;            Object parameter = args[1];            boolean isUpdate = args.length == 2;            MappedStatement ms = (MappedStatement) args[0];            if (!isUpdate &amp;&amp; ms.getSqlCommandType() == SqlCommandType.SELECT) {                RowBounds rowBounds = (RowBounds) args[2];                ResultHandler resultHandler = (ResultHandler) args[3];                BoundSql boundSql;                if (args.length == 4) {                    boundSql = ms.getBoundSql(parameter);                } else {                    // 几乎不可能走进这里面,除非使用Executor的代理对象调用query[args[6]]                    boundSql = (BoundSql) args[5];                }                for (InnerInterceptor query : interceptors) {                    if (!query.willDoQuery(executor, ms, parameter, rowBounds, resultHandler, boundSql)) {                        return Collections.emptyList();                    }                    // 进入查询前 Mybatis-plus 自己的拦截器中进行判断，这里也是实现数据权限的关键点                    // 官方注释： 改改sql啥的                    query.beforeQuery(executor, ms, parameter, rowBounds, resultHandler, boundSql);                }                CacheKey cacheKey = executor.createCacheKey(ms, parameter, rowBounds, boundSql);                return executor.query(ms, parameter, rowBounds, resultHandler, cacheKey, boundSql);            } else if (isUpdate) {                for (InnerInterceptor update : interceptors) {                    if (!update.willDoUpdate(executor, ms, parameter)) {                        return -1;                    }                    update.beforeUpdate(executor, ms, parameter);                }            }        } else {            // StatementHandler            final StatementHandler sh = (StatementHandler) target;            // 目前只有StatementHandler.getBoundSql方法args才为null            if (null == args) {                for (InnerInterceptor innerInterceptor : interceptors) {                    innerInterceptor.beforeGetBoundSql(sh);                }            } else {                Connection connections = (Connection) args[0];                Integer transactionTimeout = (Integer) args[1];                for (InnerInterceptor innerInterceptor : interceptors) {                    innerInterceptor.beforePrepare(sh, connections, transactionTimeout);                }            }        }        return invocation.proceed();    }</code></pre><h3 id="innerinterceptor-%E4%BB%A3%E7%A0%81%E5%B1%95%E7%A4%BA" tabindex="-1">InnerInterceptor 代码展示</h3><blockquote><p>此处就是 mybatis-plus 原样代码，未做任何修改</p></blockquote><pre><code class="language-JAVA">public interface InnerInterceptor {    /**     * 判断是否执行 {@link Executor#query(MappedStatement, Object, RowBounds, ResultHandler, CacheKey, BoundSql)}     * &lt;p&gt;     * 如果不执行query操作,则返回 {@link Collections#emptyList()}     *     * @param executor      Executor(可能是代理对象)     * @param ms            MappedStatement     * @param parameter     parameter     * @param rowBounds     rowBounds     * @param resultHandler resultHandler     * @param boundSql      boundSql     * @return 新的 boundSql     */    default boolean willDoQuery(Executor executor, MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler, BoundSql boundSql) throws SQLException {        return true;    }    /**     * {@link Executor#query(MappedStatement, Object, RowBounds, ResultHandler, CacheKey, BoundSql)} 操作前置处理     * &lt;p&gt;     * 改改sql啥的     *     * @param executor      Executor(可能是代理对象)     * @param ms            MappedStatement     * @param parameter     parameter     * @param rowBounds     rowBounds     * @param resultHandler resultHandler     * @param boundSql      boundSql     */    default void beforeQuery(Executor executor, MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler, BoundSql boundSql) throws SQLException {        // do nothing    }    /**     * 判断是否执行 {@link Executor#update(MappedStatement, Object)}     * &lt;p&gt;     * 如果不执行update操作,则影响行数的值为 -1     *     * @param executor  Executor(可能是代理对象)     * @param ms        MappedStatement     * @param parameter parameter     */    default boolean willDoUpdate(Executor executor, MappedStatement ms, Object parameter) throws SQLException {        return true;    }    /**     * {@link Executor#update(MappedStatement, Object)} 操作前置处理     * &lt;p&gt;     * 改改sql啥的     *     * @param executor  Executor(可能是代理对象)     * @param ms        MappedStatement     * @param parameter parameter     */    default void beforeUpdate(Executor executor, MappedStatement ms, Object parameter) throws SQLException {        // do nothing    }    /**     * {@link StatementHandler#prepare(Connection, Integer)} 操作前置处理     * &lt;p&gt;     * 改改sql啥的     *     * @param sh                 StatementHandler(可能是代理对象)     * @param connection         Connection     * @param transactionTimeout transactionTimeout     */    default void beforePrepare(StatementHandler sh, Connection connection, Integer transactionTimeout) {        // do nothing    }    /**     * {@link StatementHandler#getBoundSql()} 操作前置处理     * &lt;p&gt;     * 只有 {@link BatchExecutor} 和 {@link ReuseExecutor} 才会调用到这个方法     *     * @param sh StatementHandler(可能是代理对象)     */    default void beforeGetBoundSql(StatementHandler sh) {        // do nothing    }    default void setProperties(Properties properties) {        // do nothing    }}</code></pre><h3 id="%E6%AD%A5%E9%AA%A4" tabindex="-1">步骤</h3><ul><li>com.baomidou.mybatisplus.extension.plugins.inner.InnerInterceptor#beforeQuery</li><li><ul><li>com.baomidou.mybatisplus.extension.plugins.inner.DataPermissionInterceptor#beforeQuery （子类实现）</li></ul></li></ul><pre><code class="language-java">    @Override    public void beforeQuery(Executor executor, MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler, BoundSql boundSql) throws SQLException {        if (InterceptorIgnoreHelper.willIgnoreDataPermission(ms.getId())) {            return;        }        PluginUtils.MPBoundSql mpBs = PluginUtils.mpBoundSql(boundSql);        // com.baomidou.mybatisplus.extension.parser.JsqlParserSupport        // 当前类继承了 JsqlParserSupport，用于处理 sql 改写        mpBs.sql(parserSingle(mpBs.sql(), ms.getId()));    }</code></pre><ul><li>com.baomidou.mybatisplus.extension.parser.JsqlParserSupport#parserSingle</li></ul><pre><code class="language-java">    public String parserSingle(String sql, Object obj) {        if (logger.isDebugEnabled()) {            logger.debug(&quot;original SQL: &quot; + sql);        }        try {            Statement statement = JsqlParserGlobal.parse(sql);            // 执行 sql 解析            return processParser(statement, 0, sql, obj);        } catch (JSQLParserException e) {            throw ExceptionUtils.mpe(&quot;Failed to process, Error SQL: %s&quot;, e.getCause(), sql);        }    }        /**     * 执行 SQL 解析     *     * @param statement JsqlParser Statement     * @return sql     */    protected String processParser(Statement statement, int index, String sql, Object obj) {        if (logger.isDebugEnabled()) {            logger.debug(&quot;SQL to parse, SQL: &quot; + sql);        }        if (statement instanceof Insert) {            this.processInsert((Insert) statement, index, sql, obj);        } else if (statement instanceof Select) {            // 进入查询的数据权限            this.processSelect((Select) statement, index, sql, obj);        } else if (statement instanceof Update) {            this.processUpdate((Update) statement, index, sql, obj);        } else if (statement instanceof Delete) {            this.processDelete((Delete) statement, index, sql, obj);        }        sql = statement.toString();        if (logger.isDebugEnabled()) {            logger.debug(&quot;parse the finished SQL: &quot; + sql);        }        return sql;    }</code></pre><ul><li>com.baomidou.mybatisplus.extension.plugins.inner.DataPermissionInterceptor#processSelect</li></ul><pre><code class="language-java">protected void processSelect(Select select, int index, String sql, Object obj) {        if (dataPermissionHandler == null) {            return;        }        if (dataPermissionHandler instanceof MultiDataPermissionHandler) {            // 参照 com.baomidou.mybatisplus.extension.plugins.inner.TenantLineInnerInterceptor.processSelect 做的修改            final String whereSegment = (String) obj;            processSelectBody(select, whereSegment);            List&lt;WithItem&gt; withItemsList = select.getWithItemsList();            if (!CollectionUtils.isEmpty(withItemsList)) {                withItemsList.forEach(withItem -&gt; processSelectBody(withItem, whereSegment));            }        } else {            // 兼容原来的旧版 DataPermissionHandler 场景            if (select instanceof PlainSelect) {                this.setWhere((PlainSelect) select, (String) obj);            } else if (select instanceof SetOperationList) {                SetOperationList setOperationList = (SetOperationList) select;                List&lt;Select&gt; selectBodyList = setOperationList.getSelects();                selectBodyList.forEach(s -&gt; this.setWhere((PlainSelect) s, (String) obj));            }        }    }</code></pre><ul><li>com.baomidou.mybatisplus.extension.plugins.inner.BaseMultiTableInnerInterceptor#processSelectBody</li></ul><pre><code class="language-java">     protected void processSelectBody(Select selectBody, final String whereSegment) {        if (selectBody == null) {            return;        }        if (selectBody instanceof PlainSelect) {            // 根据判断，最终都会走到这里            processPlainSelect((PlainSelect) selectBody, whereSegment);        } else if (selectBody instanceof ParenthesedSelect) {            ParenthesedSelect parenthesedSelect = (ParenthesedSelect) selectBody;            processSelectBody(parenthesedSelect.getSelect(), whereSegment);        } else if (selectBody instanceof SetOperationList) {            SetOperationList operationList = (SetOperationList) selectBody;            List&lt;Select&gt; selectBodyList = operationList.getSelects();            if (CollectionUtils.isNotEmpty(selectBodyList)) {                selectBodyList.forEach(body -&gt; processSelectBody(body, whereSegment));            }        }    }        /**     * 处理 PlainSelect     */    protected void processPlainSelect(final PlainSelect plainSelect, final String whereSegment) {        //#3087 github        List&lt;SelectItem&lt;?&gt;&gt; selectItems = plainSelect.getSelectItems();        if (CollectionUtils.isNotEmpty(selectItems)) {            selectItems.forEach(selectItem -&gt; processSelectItem(selectItem, whereSegment));        }        // 处理 where 中的子查询        Expression where = plainSelect.getWhere();        processWhereSubSelect(where, whereSegment);        // 处理 fromItem        FromItem fromItem = plainSelect.getFromItem();        List&lt;Table&gt; list = processFromItem(fromItem, whereSegment);        List&lt;Table&gt; mainTables = new ArrayList&lt;&gt;(list);        // 处理 join        List&lt;Join&gt; joins = plainSelect.getJoins();        if (CollectionUtils.isNotEmpty(joins)) {            processJoins(mainTables, joins, whereSegment);        }        // 当有 mainTable 时，进行 where 条件追加，进行条件处理        if (CollectionUtils.isNotEmpty(mainTables)) {            plainSelect.setWhere(builderExpression(where, mainTables, whereSegment));        }    }        /**     * 处理条件     */    protected Expression builderExpression(Expression currentExpression, List&lt;Table&gt; tables, final String whereSegment) {        // 没有表需要处理直接返回        if (CollectionUtils.isEmpty(tables)) {            return currentExpression;        }        // 构造每张表的条件        List&lt;Expression&gt; expressions = tables.stream()        // 这里就要到自己实现的时候了，BaseMultiTableInnerInterceptor 中是一个抽象方法                .map(item -&gt; buildTableExpression(item, currentExpression, whereSegment))                .filter(Objects::nonNull)                .collect(Collectors.toList());        // 没有表需要处理直接返回        if (CollectionUtils.isEmpty(expressions)) {            return currentExpression;        }        // 注入的表达式        Expression injectExpression = expressions.get(0);        // 如果有多表，则用 and 连接        if (expressions.size() &gt; 1) {            for (int i = 1; i &lt; expressions.size(); i++) {                injectExpression = new AndExpression(injectExpression, expressions.get(i));            }        }        if (currentExpression == null) {            return injectExpression;        }        if (currentExpression instanceof OrExpression) {            return new AndExpression(new ParenthesedExpressionList&lt;&gt;(currentExpression), injectExpression);        } else {            return new AndExpression(currentExpression, injectExpression);        }    }</code></pre><ul><li>com.baomidou.mybatisplus.extension.plugins.inner.BaseMultiTableInnerInterceptor#buildTableExpression</li><li><ul><li>com.baomidou.mybatisplus.extension.plugins.inner.DataPermissionInterceptor#buildTableExpression</li></ul></li></ul><pre><code class="language-java">    @Override    public Expression buildTableExpression(final Table table, final Expression where, final String whereSegment) {        if (dataPermissionHandler == null) {            return null;        }        // 只有新版数据权限处理器才会执行到这里        final MultiDataPermissionHandler handler = (MultiDataPermissionHandler) dataPermissionHandler;        // 这里就是自己需要根据情况去实现的方法了。        return handler.getSqlSegment(table, where, whereSegment);    }</code></pre><blockquote><p>可以参考笔者的一个简单实现 <a href="https://gitee.com/su_xing_kang/leyan-data-permiison/" target="_blank">https://gitee.com/su_xing_kang/leyan-data-permiison/</a></p></blockquote><h2 id="%E6%80%BB%E7%BB%93" tabindex="-1">总结</h2><p>Mybatis-plus 是一款十分优秀的开源软件，能很方便的实现一些功能。在没有发现这个之前，我也自己基于 JdbcTemplate 、 JSqlParser 、 AspectJ 实现了一个数据权限，切面为 jsbcTemplate.query(…) 方法。但是比较难看，而且实现也比较难。做个记录让大家知道 mybatis-plus 还有这功能。需要注意的是，这个 com.baomidou.mybatisplus.extension.plugins.handler.MultiDataPermissionHandler 接口是在 3.5.2 才开始提供，旧版本的只能使用原来的，但实现大同小异，只是在功能上更加的丰富，使用上更加方便。</p><blockquote><p>鸣谢：<a href="https://gitee.com/baomidou/mybatis-plus" target="_blank">https://gitee.com/baomidou/mybatis-plus</a> 开源软件</p></blockquote>]]>
                </content>
            </entry>
            <entry>
                <title><![CDATA[Lazy 线段树浅学]]></title>
                <link rel="alternate" type="text/html" href="https://www.sxkawzp.cn/archives/lazy-xian-duan-shu-qian-xue" />
                <id>tag:https://www.sxkawzp.cn,2025-02-26:lazy-xian-duan-shu-qian-xue</id>
                <published>2025-02-26T09:38:17+08:00</published>
                <updated>2025-02-26T09:38:17+08:00</updated>
                <author>
                    <name>doMore</name>
                    <uri>https://www.sxkawzp.cn</uri>
                </author>
                <content type="html">
                        <![CDATA[<h2 id="%E4%BB%80%E4%B9%88%E6%98%AF-lazy-%E7%BA%BF%E6%AE%B5%E6%A0%91%EF%BC%9F" tabindex="-1">什么是 Lazy 线段树？</h2><p>Lazy 线段树（Lazy Propagation Segment Tree）是线段树（Segment Tree）的一种优化扩展，主要用于高效处理区间更新和区间查询问题。它在普通线段树的基础上引入了“延迟传播”（Lazy Propagation）的机制，通过推迟某些操作的实际执行时间，大幅减少了不必要的计算，从而优化时间复杂度。</p><h3 id="%E6%99%AE%E9%80%9A%E7%BA%BF%E6%AE%B5%E6%A0%91%E7%9A%84%E5%B1%80%E9%99%90%E6%80%A7" tabindex="-1">普通线段树的局限性</h3><p>普通线段树在处理区间查询（如求和、求最小值）时非常高效，时间复杂度为 O(logn)。然而，当涉及到区间更新（如将区间内所有元素加一个值）时，如果直接对每个叶子节点逐一修改，时间复杂度会退化为 O(nlogn)，这在数据规模较大时难以接受。</p><p>示例：<br />假设要对区间 [2,5] 的所有元素加 3。普通线段树需要递归分解区间，直到找到所有覆盖的叶子节点进行修改。如果区间长度为 k，则时间复杂度为 O(klogn)。</p><h3 id="lazy-%E7%BA%BF%E6%AE%B5%E6%A0%91%E7%9A%84%E6%A0%B8%E5%BF%83%E6%80%9D%E6%83%B3" tabindex="-1">Lazy 线段树的核心思想</h3><p>Lazy 线段树通过“延迟更新”来解决上述问题。其核心思想是：</p><ul><li><p>延迟标记（Lazy Tag）：在更新某个区间时，不立即更新所有子节点，而是将更新操作记录在当前节点的标记中。</p></li><li><p>按需传播（Propagation）：只有当后续操作（查询或更新）需要访问子节点时，才将标记中的更新操作应用到子节点，并清空当前节点的标记。</p></li></ul><p>类比：想象你有一个任务列表，但暂时不需要全部完成。你可以先记录下这些任务，等到真正需要结果时才去执行。</p><h3 id="lazy-%E7%BA%BF%E6%AE%B5%E6%A0%91%E7%9A%84%E5%B7%A5%E4%BD%9C%E6%B5%81%E7%A8%8B" tabindex="-1">Lazy 线段树的工作流程</h3><p>(1) 数据结构<br />每个线段树节点需要存储两类信息：</p><ul><li>值（Value）：当前节点覆盖区间的计算结果（如区间和、最小值等）。</li><li>延迟标记（Lazy Tag）：记录尚未传播到子节点的更新操作（如区间加、区间乘等）。</li></ul><p>(2) 关键操作</p><p>更新（Update）：<br />若当前节点区间完全包含在目标区间内，直接更新当前节点的值和延迟标记，无需修改子节点。否则，先将当前节点的延迟标记传播到子节点（称为 push down），再递归更新左右子树。</p><p>查询（Query）：<br />若当前节点区间完全包含在目标区间内，直接返回当前节点的值。否则，先 push down 延迟标记，再递归查询左右子树。</p><p>(3) Push Down 的具体步骤<br />当需要访问子节点时（无论是更新还是查询），必须先将父节点的延迟标记应用到子节点：<br />根据延迟标记更新子节点的值。<br />将延迟标记传递给子节点（叠加到子节点原有的标记上）。<br />清空父节点的延迟标记。</p><h3 id="lazy-%E7%BA%BF%E6%AE%B5%E6%A0%91%E7%9A%84%E9%80%82%E7%94%A8%E5%9C%BA%E6%99%AF" tabindex="-1">Lazy 线段树的适用场景</h3><p>Lazy 线段树适用于以下操作：</p><p>可合并的区间更新：<br />例如区间加法<br />a[l…r]+=x、区间乘法<br />a[l…r]∗=x，这些操作可以叠加，且顺序不影响结果。</p><p>可分解的区间查询：<br />例如区间求和、区间最小值，结果可以通过子区间的结果合并得到。</p><p>常见应用：<br />动态维护区间和/区间最值，支持区间加减。<br />区间赋值（如将区间内所有元素设为某个值）。<br />区间修改与查询的组合问题（如“先加后乘”的混合操作）。</p><h3 id="%E5%AE%9E%E7%8E%B0%E6%B3%A8%E6%84%8F%E4%BA%8B%E9%A1%B9" tabindex="-1">实现注意事项</h3><p>标记的设计<br />根据具体问题定义标记类型。例如：<br />区间加法：标记为一个累积的增量值。<br />区间赋值：标记为一个目标值和一个标志位。</p><p>标记的叠加规则：<br />若多个更新操作作用在同一节点，需定义标记如何合并。例如：<br />先加 3，再加 5 ⇒ 合并为加 8。<br />先加 3，再乘 2 ⇒ 需保留顺序，不能简单合并。</p><p>边界条件处理：<br />确保在叶子节点（区间长度为 1）时不再传播标记。</p><h3 id="%E6%80%BB%E7%BB%93" tabindex="-1">总结</h3><p>Lazy 线段树通过延迟传播机制，将区间更新的时间复杂度从 O(nlogn) 优化到 O(logn)，是处理动态区间问题的利器。其核心在于：</p><p>延迟标记记录未完成的操作。<br />按需传播仅在必要时更新子节点。</p><h2 id="lazy-%E9%A2%98%E7%9B%AE%E5%AE%9E%E8%B7%B5" tabindex="-1">Lazy 题目实践</h2><p><a href="https://leetcode.cn/problems/design-memory-allocator/solutions/2016010/bao-li-mo-ni-by-endlesscheng-bqba/" target="_blank">LeetCode 2502. 设计内存分配器</a></p><blockquote><p>参考 灵茶山艾府 的一篇题解</p></blockquote><pre><code class="language-JAVA">class SegTree {    private final int[] pre0; // 区间前缀连续 0 的个数    private final int[] suf0; // 区间后缀连续 0 的个数    private final int[] max0; // 区间最长连续 0 的个数    private final int[] todo; // 懒标记    public SegTree(int n) {        int size = 2 &lt;&lt; (32 - Integer.numberOfLeadingZeros(n - 1));        pre0 = new int[size];        suf0 = new int[size];        max0 = new int[size];        todo = new int[size];        build(1, 0, n - 1);    }    // 把 [ql, qr] 都置为 v    public void update(int o, int l, int r, int ql, int qr, int v) {        if (ql &lt;= l &amp;&amp; r &lt;= qr) {            do_(o, l, r, v);            return;        }        spread(o, l, r);        int m = (l + r) / 2;        int lo = o * 2;        int ro = lo + 1;        if (ql &lt;= m) {            update(lo, l, m, ql, qr, v);        }        if (m &lt; qr) {            update(ro, m + 1, r, ql, qr, v);        }        // 合并左右子树的信息        pre0[o] = pre0[lo];        if (pre0[lo] == m - l + 1) {            pre0[o] += pre0[ro]; // 和右子树的 pre0 拼起来        }        suf0[o] = suf0[ro];        if (suf0[ro] == r - m) {            suf0[o] += suf0[lo]; // 和左子树的 suf0 拼起来        }        max0[o] = Math.max(Math.max(max0[lo], max0[ro]), suf0[lo] + pre0[ro]);    }    // 线段树二分，找最左边的区间左端点，满足区间全为 0 且长度 &gt;= size    // 如果不存在这样的区间，返回 -1    public int findFirst(int o, int l, int r, int size) {        if (max0[o] &lt; size) {            return -1;        }        if (l == r) {            return l;        }        spread(o, l, r);        int m = (l + r) / 2;        int lo = o * 2;        int ro = lo + 1;        int idx = findFirst(lo, l, m, size); // 递归左子树        if (idx &lt; 0) {            // 左子树的后缀 0 个数 + 右子树的前缀 0 个数 &gt;= size            if (suf0[lo] + pre0[ro] &gt;= size) {                return m - suf0[lo] + 1;            }            idx = findFirst(ro, m + 1, r, size); // 递归右子树        }        return idx;    }    // 初始化线段树    private void build(int o, int l, int r) {        do_(o, l, r, -1);        if (l == r) {            return;        }        int m = (l + r) / 2;        build(o * 2, l, m);        build(o * 2 + 1, m + 1, r);    }    private void do_(int i, int l, int r, int v) {        int size = v &lt;= 0 ? r - l + 1 : 0;        pre0[i] = suf0[i] = max0[i] = size;        todo[i] = v;    }    // 下传懒标记    private void spread(int o, int l, int r) {        int v = todo[o];        if (v != -1) {            int m = (l + r) / 2;            do_(o * 2, l, m, v);            do_(o * 2 + 1, m + 1, r, v);            todo[o] = -1;        }    }}class Allocator {    private final int n;    private final SegTree tree;    private final Map&lt;Integer, List&lt;int[]&gt;&gt; blocks = new HashMap&lt;&gt;();    public Allocator(int n) {        this.n = n;        this.tree = new SegTree(n);    }    public int allocate(int size, int mID) {        int i = tree.findFirst(1, 0, n - 1, size);        if (i &lt; 0) { // 无法分配内存            return -1;        }        // 分配内存 [i, i+size-1]        blocks.computeIfAbsent(mID, k -&gt; new ArrayList&lt;&gt;()).add(new int[]{i, i + size - 1});        tree.update(1, 0, n - 1, i, i + size - 1, 1);        return i;    }    public int freeMemory(int mID) {        int ans = 0;        List&lt;int[]&gt; list = blocks.get(mID);        if (list != null) {            for (int[] range : list) {                ans += range[1] - range[0] + 1;                tree.update(1, 0, n - 1, range[0], range[1], 0); // 释放内存            }            blocks.remove(mID);        }        return ans;    }}作者：灵茶山艾府链接：https://leetcode.cn/problems/design-memory-allocator/description/来源：力扣（LeetCode）著作权归作者所有。商业转载请联系作者获得授权，非商业转载请注明出处。</code></pre>]]>
                </content>
            </entry>
            <entry>
                <title><![CDATA[Redis 6.0新特性客户端缓存]]></title>
                <link rel="alternate" type="text/html" href="https://www.sxkawzp.cn/archives/redis6-xin-te-xing-ke-hu-duan-huan-cun" />
                <id>tag:https://www.sxkawzp.cn,2025-02-17:redis6-xin-te-xing-ke-hu-duan-huan-cun</id>
                <published>2025-02-17T13:59:17+08:00</published>
                <updated>2025-02-17T14:04:21+08:00</updated>
                <author>
                    <name>doMore</name>
                    <uri>https://www.sxkawzp.cn</uri>
                </author>
                <content type="html">
                        <![CDATA[<blockquote><p>参考文章：  <a href="https://redis.io/docs/latest/develop/reference/client-side-caching/" target="_blank">https://redis.io/docs/latest/develop/reference/client-side-caching/</a></p></blockquote><h2 id="%E5%AE%A2%E6%88%B7%E7%AB%AF%E7%BC%93%E5%AD%98" tabindex="-1">客户端缓存</h2><p>顾名思义，客户端缓存就是Redis的客户端利用自身的存储缓存Redis的key，当需要读取某个key时，优先从自身的缓存中读取。Redis 6新增的客户端缓存功能是指Redis提供了一种机制，可以让Redis的客户端更好地实现自身的缓存。Redis通常作为数据库的缓存，用于降低数据库的压力和提高服务性能。对于一些高性能的服务而言，有时候也会将客户端的内存作为一级缓存，将Redis作为二级缓存。在这种模式下，客户端的一级缓存能够进一步缩短服务的处理时间，提高服务性能。对于Redis 6之前的版本而言，我们通常需要通过Pub/Sub来保证数据的一致性, Redis 6 能够更好地实现客户端缓存功能，本文将详细介绍Redis客户端缓存功能的实现。</p><h2 id="%E4%BD%BF%E7%94%A8" tabindex="-1">使用</h2><p>Tracking分为两种模式：默认模式和广播模式。<br />完整命令如下：<br />CLIENT TRACKING &lt;ON | OFF&gt; [REDIRECT client-id] [PREFIX prefix [PREFIX prefix …]] [BCAST] [OPTIN] [OPTOUT] [NOLOOP]</p><p>1)OPTIN：启用该选项后，默认所有的键都不会被追踪。需要配合 CLIENT CACHING yes 使用。</p><pre><code class="language-shell"># 表示 key3 将会被追踪CLIENT CACHING YESGET key3</code></pre><p>2)OPTOUT：与OPTIN相反，启用该选项后，默认所有的键都会被追踪，除非使用 <strong>CLIENT CACHING no</strong>。<br />3)NOLOOP：启用该选项后，客户端不再收到自己修改的key的失效消息。<br />4)REDIRECT：重定向模式。该模式兼容RESP 2，可将待发送的失效消息发给另一个指定客户端。</p><h3 id="%E9%BB%98%E8%AE%A4%E6%A8%A1%E5%BC%8F" tabindex="-1">默认模式</h3><p>在默认模式下，Redis Server会记录每个Redis Client访问的key，当key发生变更时，Redis Server便向Redis Client推送数据过期消息。很明显，当数据量比较大时，Redis Serve会有很大的存储压力。</p><p>在默认模式下，Redis Server会记录每个Redis Client访问的key，当key发生变更时，Redis Server便向Redis Client推送数据过期消息。很明显，当数据量比较大时，Redis Serve会有很大的存储压力：</p><pre><code class="language-shell"># 开启 resp 3 协议&gt; hello 3# 开启客户端缓存 tracking 功能&gt; client tracking on OK# 监听 test&gt; get test # 重新启动一个 redis 连接，修改 test 的值，就会收到推送的失效消息&gt;client 2# 注意：此处是 resp 3 协议内容$10invalidate*1$4test</code></pre><p>使用默认模式需要注意以下几点。<br />1)客户端监听的key如果在别处被修改为与原值一样，客户端也会收到失效消息。<br />2)监听后，客户端只会收到key的一次失效消息，即该key再被修改时，客户端不会再收到消息，客户端需要再次查询该key，才能继续监听该key。<br />3)当监听的key由于服务端触发过期淘汰策略而被清除时，客户端也会收到消息。</p><h3 id="%E5%B9%BF%E6%92%AD%E6%A8%A1%E5%BC%8F" tabindex="-1">广播模式</h3><p>默认模式可以让Redis Client跟踪特定的key，但是实际使用时，我们经常需要跟踪满足某个前缀条件的所有key。针对这种情况，我们可以使用广播模式。在广播模式下，客户端可以订阅匹配某一前缀的广播（也可订阅空串，表示订阅所有广播）​。在这种模式下，服务端只需要记录被订阅的广播的前缀与Redis Client的对应关系即可，当满足条件的key发生变化时就通知对应的Redis Client。相比于采用默认模式，采用广播模式，服务端不再需要消耗过多内存用于存储Redis Client访问的key，但是可能会发送给Redis Client并不关心的key。</p><p>广播模式使用示例：</p><pre><code class="language-shell">&gt; hello 3&gt; client tracking on bcast prefix xxx&gt; get xxx_test# 重新启动一个 redis 连接，修改 test 的值，就会收到推送的失效消息&gt;client 2# 注意：此处是 resp 3 协议内容$10invalidate*1$8xxx_test</code></pre><p>使用广播模式需要注意以下几点。<br />1)符合前缀的key出现新增、修改、删除、过期、淘汰等动作，客户端都会收到通知。<br />2)与默认模式不同，客户端可多次收到符合前缀的key的失效消息，无须反复监听。</p><h3 id="%E8%BD%AC%E5%8F%91%E5%8A%9F%E8%83%BD" tabindex="-1">转发功能</h3><p>默认模式及广播模式因为都需要Redis Server主动向Redis Client推送消息，所以都需要RESP 3协议。<br />对于使用 RESP 2 的用户可以通过客户端缓存的转发功能进行改造。<br />Redis Client 1启动Tracking重定向功能后，可以将后续消息转发给Redis Client 2，Redis Client 2需要订阅 <strong><em>redis</em>:invalidate</strong> 频道。之后当Redis Client 3修改Redis Client 1监听的key后，Redis Server就会向 <strong><em>redis</em>:invalidate</strong> 频道发送消息，Redis Client 2就可以接收到这个消息，进而更新自己的本地缓存。</p><p>使用转发功能需要注意以下几点。<br />1)转发功能只能指定一个Redis Client，这个Redis Client可以是自己。<br />2)转发功能可以基于默认模式，也可以基于广播模式。<br />3)转发功能需要Redis Client订阅  <strong><em>redis</em>:invalidate</strong> 频道</p><h2 id="java-%E5%AE%9E%E7%8E%B0" tabindex="-1">Java 实现</h2><blockquote><p>注意：从 Lettuce 6.0 版本开始，它支持 RESP 3 协议。Jedis 客户端尚未官方支持 RESP 3 协议。</p></blockquote><h3 id="1.-%E6%B7%BB%E5%8A%A0%E4%BE%9D%E8%B5%96" tabindex="-1">1. 添加依赖</h3><pre><code class="language-xml">&lt;dependencies&gt;    &lt;dependency&gt;        &lt;groupId&gt;org.springframework.boot&lt;/groupId&gt;        &lt;artifactId&gt;spring-boot-starter-data-redis&lt;/artifactId&gt;    &lt;/dependency&gt;    &lt;dependency&gt;        &lt;groupId&gt;io.lettuce&lt;/groupId&gt;        &lt;artifactId&gt;lettuce-core&lt;/artifactId&gt;    &lt;/dependency&gt;&lt;/dependencies&gt;</code></pre><h3 id="2.-%E9%85%8D%E7%BD%AE-redistemplate" tabindex="-1">2. 配置 RedisTemplate</h3><pre><code class="language-java">import io.lettuce.core.ClientOptions;import io.lettuce.core.RedisURI;import io.lettuce.core.api.StatefulRedisConnection;import io.lettuce.core.codec.StringCodec;import io.lettuce.core.protocol.ProtocolVersion;import io.lettuce.core.resource.ClientResources;import org.springframework.context.annotation.Bean;import org.springframework.context.annotation.Configuration;import org.springframework.data.redis.connection.RedisConnectionFactory;import org.springframework.data.redis.connection.lettuce.LettuceClientConfiguration;import org.springframework.data.redis.connection.lettuce.LettuceConnectionFactory;import org.springframework.data.redis.core.RedisTemplate;import org.springframework.data.redis.serializer.StringRedisSerializer;import java.time.Duration;@Configurationpublic class RedisConfig {    @Bean    public RedisConnectionFactory redisConnectionFactory() {        // 配置 Lettuce 客户端使用 RESP 3 协议        LettuceClientConfiguration clientConfig = LettuceClientConfiguration.builder()               .clientOptions(ClientOptions.builder()                       .protocolVersion(ProtocolVersion.RESP3)                       .build())               .commandTimeout(Duration.ofSeconds(10))               .build();        // 创建 Redis URI        RedisURI redisUri = RedisURI.create(&quot;redis://localhost:6379&quot;);        // 创建 Lettuce 连接工厂        return new LettuceConnectionFactory(redisUri, clientConfig);    }    @Bean    public RedisTemplate&lt;String, String&gt; redisTemplate(RedisConnectionFactory redisConnectionFactory) {        RedisTemplate&lt;String, String&gt; template = new RedisTemplate&lt;&gt;();        template.setConnectionFactory(redisConnectionFactory);        template.setKeySerializer(new StringRedisSerializer());        template.setValueSerializer(new StringRedisSerializer());        return template;    }}</code></pre><h3 id="%E5%AE%9E%E7%8E%B0-client-tracking" tabindex="-1">实现 CLIENT TRACKING</h3><pre><code class="language-java">import org.springframework.data.redis.connection.RedisConnection;import org.springframework.data.redis.core.RedisTemplate;import org.springframework.stereotype.Service;import java.nio.charset.StandardCharsets;@Servicepublic class RedisTrackingService {    private final RedisTemplate&lt;String, String&gt; redisTemplate;    public RedisTrackingService(RedisTemplate&lt;String, String&gt; redisTemplate) {        this.redisTemplate = redisTemplate;    }    /**     * 开启客户端跟踪功能     */    public void enableClientTracking() {        redisTemplate.execute((RedisConnection connection) -&gt; {            // 发送 CLIENT TRACKING ON 命令开启跟踪            byte[] command = &quot;CLIENT&quot;.getBytes(StandardCharsets.UTF_8);            byte[][] args = {                    &quot;TRACKING&quot;.getBytes(StandardCharsets.UTF_8),                    &quot;ON&quot;.getBytes(StandardCharsets.UTF_8)            };            connection.execute(command, args);            return null;        });    }    /**     * 处理跟踪通知     */    public void handleTrackingNotifications() {        redisTemplate.execute((RedisConnection connection) -&gt; {            while (true) {                // 从 Redis 接收消息                byte[] message = connection.receive();                if (message != null) {                    String notification = new String(message, StandardCharsets.UTF_8);                    System.out.println(&quot;Received tracking notification: &quot; + notification);                }            }        });    }}</code></pre><h3 id="4.-%E6%B5%8B%E8%AF%95" tabindex="-1">4. 测试</h3><pre><code class="language-java">import org.springframework.beans.factory.annotation.Autowired;import org.springframework.boot.CommandLineRunner;import org.springframework.boot.SpringApplication;import org.springframework.boot.autoconfigure.SpringBootApplication;@SpringBootApplicationpublic class RedisTrackingApplication implements CommandLineRunner {    @Autowired    private RedisTrackingService redisTrackingService;    public static void main(String[] args) {        SpringApplication.run(RedisTrackingApplication.class, args);    }    @Override    public void run(String... args) throws Exception {        // 开启客户端跟踪功能        redisTrackingService.enableClientTracking();        // 处理跟踪通知        redisTrackingService.handleTrackingNotifications();    }}</code></pre><h3 id="%E6%B3%A8%E6%84%8F%E4%BA%8B%E9%A1%B9" tabindex="-1">注意事项</h3><ul><li>确保你的 Redis 服务器版本为 6.0 或以上，因为 CLIENT TRACKING 是在 Redis 6.0 中引入的。</li><li>上述代码中的 handleTrackingNotifications 方法使用了一个无限循环来接收通知，实际应用中需要根据需求进行调整。</li></ul>]]>
                </content>
            </entry>
            <entry>
                <title><![CDATA[2025年书籍清单]]></title>
                <link rel="alternate" type="text/html" href="https://www.sxkawzp.cn/archives/2025-nian-shu-ji-qing-dan" />
                <id>tag:https://www.sxkawzp.cn,2025-02-11:2025-nian-shu-ji-qing-dan</id>
                <published>2025-02-11T15:13:33+08:00</published>
                <updated>2026-02-25T08:53:03+08:00</updated>
                <author>
                    <name>doMore</name>
                    <uri>https://www.sxkawzp.cn</uri>
                </author>
                <content type="html">
                        <![CDATA[<h2 id="%E5%B0%8F%E8%AF%B4" tabindex="-1">小说</h2><p>《你杀了谁》  – 东野圭吾   2025年2月11日 读完<br />《命运》  – 蔡崇达<br />《我是女刑警：追凶25年》   --庄梦、周飞  2025年6月26日 读完<br />《全球进化》   --咬狗  2025年7月14日 读完<br />《大唐辟珠记》  --饭卡   2025年8月14日  读完<br />《桃花源没事儿》  – 马伯庸   2025年9月28日 读完<br />《素食者》-- 韩江    2025年10月17日   读完<br />《异兽迷城》 – 澎湃   2025年11月15日 读完<br />《人偶游戏》  – 东野圭吾   2025年12月8日 读完</p><h2 id="%E6%8A%80%E6%9C%AF" tabindex="-1">技术</h2><p>《MyBatis 3源码深度解析》  – 江荣波   2025年1月24日 读完<br />《高效使用Redis：一书学透数据存储与高可用集群》  – 熊浩含   2025年2月17日 读完<br />《agentzh 的 Nginx 教程》   – openresty 官方教程    2025年6月17日 读完<br />《Java性能权威指南（第2版）》   – 斯科特·奥克斯   2025年6月6日 读完<br />《Git高效实践》  --吴子俊    2025年6月12日 读完<br />《15分钟了解Deepseek：DeepSeek R1轻松上手指南（轻科技）》  – 魔云兽    2025年6月18日 读完</p><h2 id="%E6%9D%82%E4%B9%A6" tabindex="-1">杂书</h2><p><a href="https://www.dev-life.site/" target="_blank">《自洽的程序员》</a>  – 毕业于 北大软件与微电子学院 的 <strong>公众号: 辣条加辣</strong></p><p>如果您刷到这篇文章，而您目前也正在读一本有趣的书，请添加评论告知笔者。不论是技术开发，还是文学类的书籍，笔者感激不尽。</p>]]>
                </content>
            </entry>
            <entry>
                <title><![CDATA[线段树浅学]]></title>
                <link rel="alternate" type="text/html" href="https://www.sxkawzp.cn/archives/xian-duan-shu-qian-xue" />
                <id>tag:https://www.sxkawzp.cn,2025-01-08:xian-duan-shu-qian-xue</id>
                <published>2025-01-08T17:18:48+08:00</published>
                <updated>2025-01-08T17:20:52+08:00</updated>
                <author>
                    <name>doMore</name>
                    <uri>https://www.sxkawzp.cn</uri>
                </author>
                <content type="html">
                        <![CDATA[<h2 id="%E5%AE%9A%E4%B9%89%E4%B8%8E%E6%A6%82%E5%BF%B5" tabindex="-1">定义与概念</h2><p>线段树是一种二叉树数据结构，用于高效地处理区间查询和区间修改操作。它将一个区间划分成多个子区间，每个节点都对应一个区间。<br />例如，对于区间 [1，10] , 线段树的根节点可能表示整个区间 [1,10] ，根节点的左子结点 可能表示 [1,5]， 右子节点表示区间 [6,10]，这种划分会持续下去，直到区间长度为 1（叶子结点）。叶子节点表示原始区间中的单个元素。</p><h2 id="%E6%9C%B4%E7%B4%A0%E6%80%9D%E6%83%B3" tabindex="-1">朴素思想</h2><p>假设我们现在有一个数组，我们想对其一个区间查询其区间和，那么对这个数组的查询操作，及找到该区间内所有元素的和的时间复杂度为O(n)。如果我们想要更新其数组内的一个数的值，这个更新操作的时间复杂度便为O(1)，可以直接根据下标进行修改。</p><p><img src="https://www.sxkawzp.cn/upload/2025/01/diyizhang.png" alt="diyizhang" /></p><h3 id="%E5%89%8D%E7%BC%80%E5%92%8C" tabindex="-1">前缀和</h3><p>提到和，尤其是对于查询区间和，我们容易想到的一个点就是使用前缀和，这样我们就可以将查询的操作提升到O(1), 但是使用前缀和会有一个问题，当我们的更新次数过多时，尤其是需要更新的元素比较靠前时，每一次更新的代价都会为O(n)，从而没有达到优化的效果。但是对于元素不变动的数组前缀和还是有很不错的优势！</p><p><img src="https://www.sxkawzp.cn/upload/2025/01/xianduanshu_dierzhang.png" alt="diyizhang" /></p><h3 id="%E7%BA%BF%E6%AE%B5%E6%A0%91" tabindex="-1">线段树</h3><p>线段树将上述问题的查询以及更新的时间复杂度都变成了O(logn)。当进行多次查询与更新时，线段树一定比上述两种方法更具优势。<br />首先我们先来看一下线段树是什么结构。线段树是一棵二叉树，每个非叶子节点都有左右子节点。用数组来表示树的结构，对于根树的根节点，它会在index=1的位置上(其实此处0也行，不过大家普遍用1,区别就是计算子节点的方式不同)，然后对于其节点的左右子节点的下标分别为 2*index 与 2*index+1。</p><ul><li>每个节点除了存储表示区间的左右端点和外，还存储与区间相关的信息。</li><li>例如，在求区间和的线段树中，节点存储该区间内所有元素的和。如果是求区间最大值的线段树，节点存储该区间内元素的最大值。</li></ul><p><img src="https://www.sxkawzp.cn/upload/2025/01/xianduanshu_disanzhang.png" alt="diyizhang" /></p><p>查询: 我们会根据区间<code>从根节点</code>向树的两边递归查寻。假设我们现在要查找此树的<code>[2,4]</code>的区间和，及<code>[50,50,1]</code>的和, 那么这个过程是什么样的呢？</p><p><img src="https://www.sxkawzp.cn/upload/2025/01/xianduanshu_disizhang.png" alt="diyizhang" /></p><h2 id="%E4%BB%A3%E7%A0%81%E5%AE%9E%E7%8E%B0" tabindex="-1">代码实现</h2><h3 id="%E5%BB%BA%E6%A0%91" tabindex="-1">建树</h3><pre><code class="language-java">public static void buildTree(int[] arr, int[] tree, int start, int end, int nodeIndex) {  // 找到叶子节点，不再递归    if (start == end) {          tree[nodeIndex] = arr[start];      } else {          // 找到树的左子节点(2*nodeIndex)          // 找到树的右子节点(2*nodeIndex +1)          int leftNode = 2 * nodeIndex;          int rightNode = 2 * nodeIndex + 1;          int mid = (start + end) / 2;          // 将树进行分割，左右递归建树          buildTree(arr, tree, start, mid, leftNode);          buildTree(arr, tree, mid + 1, end, rightNode);          tree[nodeIndex] = tree[leftNode] + tree[rightNode];      }  }</code></pre><h3 id="%E6%A3%80%E7%B4%A2" tabindex="-1">检索</h3><p>需要分情况进行讨论：</p><ul><li>情况一<br /><img src="https://www.sxkawzp.cn/upload/2025/01/xianduanshu_diwuzhang.png" alt="diyizhang" /></li><li>情况二则是当前区间被我们的检索区间包围，及蓝色区间在绿色区间里面时，因此不必继续往下递归，可以直接返回当前节点值。这里比较容易想，读者可参考之前的线段树查询。思考一下，每一个节点表示的都是一个区间内所有元素的和，那么当整个当前区间都被我们的检索区间包围了，证明我们需要所有的元素，因此不必继续往下递归查询，可以返回其节点值。</li></ul><pre><code class="language-java">public static int query(int[] arr, int[] tree, int start, int end, int l, int r, int nodeIndex) {      if (l &gt; end || r &lt; start) {          return 0;      }      if (l &lt;= start &amp;&amp; end &lt;= r) {          return tree[nodeIndex];      } else {          // 递归查询          int leftNode = 2 * nodeIndex;          int rightNode = 2 * nodeIndex + 1;          int mid = (start + end) / 2;          int leftSum = query(arr, tree, start, mid, l, r, leftNode);          int rightSum = query(arr, tree, mid + 1, end, l, r, rightNode);          return leftSum + rightSum;      }  }</code></pre><p>更新操作和建树操作很像，可以进一步思考。</p><h2 id="%E5%BA%94%E7%94%A8%E5%9C%BA%E6%99%AF" tabindex="-1">应用场景</h2><ul><li><strong>区间求和问题</strong>：高效地计算给定区间内元素的和。例如，给定一个数组，频繁查询数组中某个区间内元素的和。</li><li><strong>区间最大值 / 最小值问题</strong>：快速获取给定区间内元素的最大值或最小值。比如，在一个时间序列数据中，查询某段时间内的最高温度或最低温度。</li><li><strong>动态区间更新与查询</strong>：在一些动态的数据环境中，需要同时支持区间修改（如增加一个值、修改元素）和区间查询操作，线段树能够很好地应对这种情况。</li></ul>]]>
                </content>
            </entry>
            <entry>
                <title><![CDATA[Java CompletableFuture 异步超时实现探索]]></title>
                <link rel="alternate" type="text/html" href="https://www.sxkawzp.cn/archives/javacompletablefuture异步超时实现探索" />
                <id>tag:https://www.sxkawzp.cn,2024-12-30:javacompletablefuture异步超时实现探索</id>
                <published>2024-12-30T14:57:26+08:00</published>
                <updated>2024-12-30T14:58:18+08:00</updated>
                <author>
                    <name>doMore</name>
                    <uri>https://www.sxkawzp.cn</uri>
                </author>
                <content type="html">
                        <![CDATA[<h1 id="%E7%AE%80%E4%BB%8B" tabindex="-1"><strong>简介</strong></h1><p>JDK 8 中 CompletableFuture 没有超时中断任务的能力。现有做法强依赖任务自身的超时实现。本文提出一种异步超时实现方案，解决上述问题。</p><h1 id="%E5%89%8D%E8%A8%80" tabindex="-1"><strong>前言</strong></h1><p>JDK 8 是一个非常重要的版本，新增了非常多的特性，其中之一便是 <code>CompletableFuture</code>。自此从 JDK 层面真正意义上的支持了基于事件的异步编程范式，弥补了 <code>Future</code> 的缺陷。</p><p>在我们的日常优化中，最常用手段便是多线程并行执行。这时候就会涉及到 <code>CompletableFuture</code> 的使用。</p><h1 id="%E5%B8%B8%E8%A7%81%E4%BD%BF%E7%94%A8%E6%96%B9%E5%BC%8F" tabindex="-1"><strong>常见使用方式</strong></h1><p>下面举例一个常见场景。<br />假如我们有两个 RPC 远程调用服务，我们需要获取两个 RPC 的结果后，再进行后续逻辑处理。</p><pre><code class="language-java">public static void main(String[] args) {    // 任务 A，耗时 2 秒    int resultA = compute(1);    // 任务 B，耗时 2 秒    int resultB = compute(2);    // 后续业务逻辑处理    System.out.println(resultA + resultB);}</code></pre><p>可以预估到，串行执行最少耗时 4 秒，并且 B 任务并不依赖 A 任务结果。<br />对于这种场景，我们通常会选择并行的方式优化，Demo 代码如下：</p><pre><code class="language-java">public static void main(String[] args) {    // 仅简单举例，在生产代码中可别这么写！    // 统计耗时的函数    time(() -&gt; {        CompletableFuture&lt;Integer&gt; result = Stream.of(1, 2)                                                  // 创建异步任务                                                  .map(x -&gt; CompletableFuture.supplyAsync(() -&gt; compute(x), executor))                                                  // 聚合                                                  .reduce(CompletableFuture.completedFuture(0), (x, y) -&gt; x.thenCombineAsync(y, Integer::sum, executor));        // 等待结果        try {            System.out.println(&quot;结果：&quot; + result.get());        } catch (ExecutionException | InterruptedException e) {            System.err.println(&quot;任务执行异常&quot;);        }    });}输出：[async-1]: 任务执行开始：1[async-2]: 任务执行开始：2[async-1]: 任务执行完成：1[async-2]: 任务执行完成：2结果：3耗时：2 秒</code></pre><h1 id="%E5%AD%98%E5%9C%A8%E7%9A%84%E9%97%AE%E9%A2%98" tabindex="-1"><strong>存在的问题</strong></h1><h2 id="%E5%88%86%E6%9E%90" tabindex="-1"><strong>分析</strong></h2><p>看上去 <code>CompletableFuture</code> 现有功能可以满足我们诉求。但当我们引入一些现实常见情况时，一些潜在的不足便暴露出来了。</p><p><code>compute(x)</code> 如果是一个根据入参查询用户某类型优惠券列表的任务，我们需要查询两种优惠券并组合在一起返回给上游。假如上游要求我们 2 秒内处理完毕并返回结果，但 <code>compute(x)</code> 耗时却在 0.5 秒 ~ 无穷大波动。这时候我们就需要把耗时过长的 <code>compute(x)</code> 任务结果放弃，仅处理在指定时间内完成的任务，尽可能保证服务可用。</p><p>那么以上代码的耗时由耗时最长的服务决定，无法满足现有诉求。通常我们会使用 <code>get(long timeout, TimeUnit unit)</code> 来指定获取结果的超时时间，并且我们会给 <code>compute(x)</code> 设置一个超时时间，达到后自动抛异常来中断任务。</p><pre><code class="language-java">public static void main(String[] args) {    // 仅简单举例，在生产代码中可别这么写！    // 统计耗时的函数    time(() -&gt; {        List&lt;CompletableFuture&lt;Integer&gt;&gt; result = Stream.of(1, 2)                                                        // 创建异步任务，compute(x) 超时抛出异常                                                        .map(x -&gt; CompletableFuture.supplyAsync(() -&gt; compute(x), executor))                                                        .toList();        // 等待结果        int res = 0;        for (CompletableFuture&lt;Integer&gt; future : result) {            try {                res += future.get(2, SECONDS);            } catch (ExecutionException | InterruptedException | TimeoutException e) {                System.err.println(&quot;任务执行异常或超时&quot;);            }        }        System.out.println(&quot;结果：&quot; + res);    });}输出：[async-2]: 任务执行开始：2[async-1]: 任务执行开始：1[async-1]: 任务执行完成：1任务执行异常或超时结果：1耗时：2 秒</code></pre><p>可以看到，只要我们能够给 <code>compute(x)</code> 设置一个超时时间将任务中断，结合 <code>get</code>、<code>getNow</code> 等获取结果的方式，就可以很好地管理整体耗时。</p><p>那么问题也就转变成了，<strong>如何给任务设置异步超时时间呢</strong>？</p><h1 id="%E8%A7%A3%E5%86%B3%E6%96%B9%E5%BC%8F" tabindex="-1"><strong>解决方式</strong></h1><h2 id="jdk-9" tabindex="-1"><strong>JDK 9</strong></h2><p>这类问题非常常见，如大促场景，服务器 CPU 瞬间升高就会出现以上问题。</p><p>那么如何解决呢？其实 JDK 的开发大佬们早有研究。在 JDK 9，<code>CompletableFuture</code> 正式提供了 <code>orTimeout</code>、<code>completeOnTimeout</code> 方法，来准确实现异步超时控制。</p><pre><code class="language-java">public CompletableFuture&lt;T&gt; orTimeout(long timeout, TimeUnit unit) {    if (unit == null)        throw new NullPointerException();    if (result == null)        whenComplete(new Canceller(Delayer.delay(new Timeout(this), timeout, unit)));    return this;}</code></pre><p>JDK 9 <code>orTimeout</code> 其实现原理是通过一个定时任务，在给定时间之后抛出异常。如果任务在指定时间内完成，则取消抛异常的操作。</p><p>以上代码我们按执行顺序来看下：<br />首先执行 <code>new Timeout(this)</code>。</p><pre><code class="language-java">static final class Timeout implements Runnable {    final CompletableFuture&lt;?&gt; f;    Timeout(CompletableFuture&lt;?&gt; f) { this.f = f; }    public void run() {        if (f != null &amp;&amp; !f.isDone())            // 抛出超时异常            f.completeExceptionally(new TimeoutException());    }}</code></pre><p>通过源码可以看到，<code>Timeout</code> 是一个实现 Runnable 的类，<code>run()</code> 方法负责给传入的异步任务通过  <code>completeExceptionally</code>  CAS 赋值异常，将任务标记为异常完成。</p><p>那么谁来触发这个 <code>run()</code> 方法呢？我们看下 <code>Delayer</code> 的实现。</p><pre><code class="language-java">static final class Delayer {    static ScheduledFuture&lt;?&gt; delay(Runnable command, long delay,                                    TimeUnit unit) {        // 到时间触发 command 任务        return delayer.schedule(command, delay, unit);    }    static final class DaemonThreadFactory implements ThreadFactory {        public Thread newThread(Runnable r) {            Thread t = new Thread(r);            t.setDaemon(true);            t.setName(&quot;CompletableFutureDelayScheduler&quot;);            return t;        }    }    static final ScheduledThreadPoolExecutor delayer;    static {        (delayer = new ScheduledThreadPoolExecutor(            1, new DaemonThreadFactory())).            setRemoveOnCancelPolicy(true);    }}</code></pre><p><code>Delayer</code> 其实就是一个单例定时调度器，<code>Delayer.delay(new Timeout(this), timeout, unit)</code> 通过 <code>ScheduledThreadPoolExecutor</code> 实现指定时间后触发 <code>Timeout</code> 的 <code>run()</code> 方法。</p><p>到这里就已经实现了超时抛出异常的操作。但当任务完成时，就没必要触发 <code>Timeout</code> 了。因此我们还需要实现一个取消逻辑。</p><pre><code class="language-java">static final class Canceller implements BiConsumer&lt;Object, Throwable&gt; {    final Future&lt;?&gt; f;    Canceller(Future&lt;?&gt; f) { this.f = f; }    public void accept(Object ignore, Throwable ex) {        if (ex == null &amp;&amp; f != null &amp;&amp; !f.isDone())        // 3 未触发抛异常任务则取消            f.cancel(false);    }}</code></pre><p>当任务执行完成，或者任务执行异常时，我们也就没必要抛出超时异常了。因此我们可以把 <code>delayer.schedule(command, delay, unit)</code> 返回的定时超时任务取消，不再触发 <code>Timeout</code>。当我们的异步任务完成，并且定时超时任务未完成的时候，就是我们取消的时机。因此我们可以通过 <code>whenComplete(BiConsumer&lt;? super T, ? super Throwable&gt; action)</code> 来完成。</p><p><code>Canceller</code> 就是一个 <code>BiConsumer</code> 的实现。其持有了 <code>delayer.schedule(command, delay, unit)</code> 返回的定时超时任务，<code>accept(Object ignore, Throwable ex)</code> 实现了定时超时任务未完成后，执行 <code>cancel(boolean mayInterruptIfRunning)</code> 取消任务的操作。</p><h2 id="jdk-8" tabindex="-1"><strong>JDK 8</strong></h2><p>如果我们使用的是 JDK 9 或以上，我们可以直接用 JDK 的实现来完成异步超时操作。那么 JDK 8 怎么办呢？</p><p>其实我们也可以根据上述逻辑简单实现一个工具类来辅助。</p><pre><code class="language-java">  import java.util.concurrent.*;  import java.util.function.BiConsumer;    /**   * @author suxingkang * @version 1.0 * @since 2024/12/26 8:46  * */ public class CompletableFutureExpandUtils {          public static &lt;T&gt; CompletableFuture&lt;T&gt; orTimeOut(CompletableFuture&lt;T&gt; future, long timeout, TimeUnit unit) {            if (unit == null) {              throw new RuntimeException(&quot;时间的给定力度不能为空&quot;);          }          if (future == null) {              throw new RuntimeException(&quot;异步任务不能为空&quot;);          }            if (future.isDone()) {              return future;          }            return future.whenComplete(new Canceller(Delayer.delay(new TimeOut(future), timeout, unit)));      }          /**       * 超时时异常完成的操作       */      static final class TimeOut implements Runnable {            final CompletableFuture&lt;?&gt; completableFuture;            public TimeOut(CompletableFuture&lt;?&gt; completableFuture) {              this.completableFuture = completableFuture;          }            @Override          public void run() {              if (null != completableFuture &amp;&amp; !completableFuture.isDone()) {                  completableFuture.completeExceptionally(new TimeoutException());              }          }      }        /**       * 取消不需要的超时的操作       */      static final class Canceller implements BiConsumer&lt;Object, Throwable&gt; {          final Future&lt;?&gt; future;            public Canceller(Future&lt;?&gt; future) {              this.future = future;          }            @Override          public void accept(Object ignore, Throwable ex) {              if (null != ex &amp;&amp; null != future &amp;&amp; !future.isDone()) {                  future.cancel(false);              }          }      }          /**       * 单例延迟调度器，仅用于启动和取消任务，一个线程就足够       */      static final class Delayer {          static final ScheduledThreadPoolExecutor delayer;            static ScheduledFuture&lt;?&gt; delay(Runnable command, long delay, TimeUnit unit) {              return delayer.schedule(command, delay, unit);          }            static final class DaemonThreadFactory implements ThreadFactory {                @Override              public Thread newThread(Runnable r) {                  Thread thread = new Thread(r);                  thread.setDaemon(true);                  thread.setName(&quot;CompletableFutureExpandUtilsDelayScheduler&quot;);                  return thread;              }          }            static {              delayer = new ScheduledThreadPoolExecutor(1, new DaemonThreadFactory());              delayer.setRemoveOnCancelPolicy(true);          }        }    }</code></pre><h1 id="%E6%80%BB%E7%BB%93" tabindex="-1"><strong>总结</strong></h1><p>在 JDK 8 场景下，现有超时中断的做法依赖于任务本身的超时实现，当任务本身的超时失效，或者不够精确时，并没有很好的手段来中断任务。因此本文给出一种让 CompletableFuture 支持异步超时的实现方案实现思路，仅供大家参考。</p>]]>
                </content>
            </entry>
            <entry>
                <title><![CDATA[SpringBoot结合Redis作为Session管理浅析]]></title>
                <link rel="alternate" type="text/html" href="https://www.sxkawzp.cn/archives/springboot-jie-he-redis-zuo-wei-session-guan-li-qian-xi" />
                <id>tag:https://www.sxkawzp.cn,2024-11-27:springboot-jie-he-redis-zuo-wei-session-guan-li-qian-xi</id>
                <published>2024-11-27T10:21:12+08:00</published>
                <updated>2024-11-27T10:21:12+08:00</updated>
                <author>
                    <name>doMore</name>
                    <uri>https://www.sxkawzp.cn</uri>
                </author>
                <content type="html">
                        <![CDATA[<h2 id="%E4%BE%9D%E8%B5%96" tabindex="-1">依赖</h2><pre><code class="language-xml">&lt;dependencies&gt;&lt;dependency&gt;    &lt;groupId&gt;org.springframework.boot&lt;/groupId&gt;    &lt;artifactId&gt;spring-boot-autoconfigure&lt;/artifactId&gt;    &lt;version&gt;2.6.8&lt;/version&gt;  &lt;/dependency&gt;&lt;dependency&gt;&lt;groupId&gt;org.springframework.session&lt;/groupId&gt;  &lt;artifactId&gt;spring-session-data-redis&lt;/artifactId&gt;  &lt;version&gt;2.6.3&lt;/version&gt;&lt;/dependency&gt;&lt;/dependencies&gt;</code></pre><h2 id="%E5%90%AF%E5%8A%A8%E8%BF%87%E7%A8%8B" tabindex="-1">启动过程</h2><ol><li>加载 redis session 相关配置</li></ol><pre><code class="language-java">// org.springframework.boot.autoconfigure.session.RedisSessionConfiguration@Configuration(proxyBeanMethods = false)  @ConditionalOnClass({ RedisTemplate.class, RedisIndexedSessionRepository.class })  @ConditionalOnMissingBean(SessionRepository.class)  @ConditionalOnBean(RedisConnectionFactory.class)  @Conditional(ServletSessionCondition.class)  @EnableConfigurationProperties(RedisSessionProperties.class)  class RedisSessionConfiguration {        @Bean      @ConditionalOnMissingBean        ConfigureRedisAction configureRedisAction(RedisSessionProperties redisSessionProperties) {         switch (redisSessionProperties.getConfigureAction()) {         case NOTIFY_KEYSPACE_EVENTS:            return new ConfigureNotifyKeyspaceEventsAction();         case NONE:            return ConfigureRedisAction.NO_OP;         }         throw new IllegalStateException(               &quot;Unsupported redis configure action &#39;&quot; + redisSessionProperties.getConfigureAction() + &quot;&#39;.&quot;);      }        @Configuration(proxyBeanMethods = false)      public static class SpringBootRedisHttpSessionConfiguration extends RedisHttpSessionConfiguration {           @Autowired         public void customize(SessionProperties sessionProperties, RedisSessionProperties redisSessionProperties,               ServerProperties serverProperties) {            Duration timeout = sessionProperties                  .determineTimeout(() -&gt; serverProperties.getServlet().getSession().getTimeout());            if (timeout != null) {               setMaxInactiveIntervalInSeconds((int) timeout.getSeconds());            }            setRedisNamespace(redisSessionProperties.getNamespace());            setFlushMode(redisSessionProperties.getFlushMode());            setSaveMode(redisSessionProperties.getSaveMode());            setCleanupCron(redisSessionProperties.getCleanupCron());         }      }}</code></pre><ol start="2"><li>Redis Session 配置加载</li></ol><pre><code class="language-java">// org.springframework.session.data.redis.config.annotation.web.http.RedisHttpSessionConfiguration// 实际用来创建 session 和 管理session的对象// 这个类 并不能由我们自己去创建， 方法上只加了 @Bean，并没有条件注解，我们能做的处理就是修改 序列化类和在增加 org.springframework.session.config.SessionRepositoryCustomizer 定制化 操作。@Bean  public RedisIndexedSessionRepository sessionRepository() {      RedisTemplate&lt;Object, Object&gt; redisTemplate = createRedisTemplate();      RedisIndexedSessionRepository sessionRepository = new RedisIndexedSessionRepository(redisTemplate);      sessionRepository.setApplicationEventPublisher(this.applicationEventPublisher);      if (this.indexResolver != null) {         sessionRepository.setIndexResolver(this.indexResolver);      }      if (this.defaultRedisSerializer != null) {         sessionRepository.setDefaultSerializer(this.defaultRedisSerializer);      }      sessionRepository.setDefaultMaxInactiveInterval(this.maxInactiveIntervalInSeconds);      if (StringUtils.hasText(this.redisNamespace)) {         sessionRepository.setRedisKeyNamespace(this.redisNamespace);      }      sessionRepository.setFlushMode(this.flushMode);      sessionRepository.setSaveMode(this.saveMode);      int database = resolveDatabase();      sessionRepository.setDatabase(database);      this.sessionRepositoryCustomizers            .forEach((sessionRepositoryCustomizer) -&gt; sessionRepositoryCustomizer.customize(sessionRepository));      return sessionRepository;  }// 父类（org.springframework.session.config.annotation.web.http.SpringHttpSessionConfiguration）中初始化  SessionRepositoryFilter，这个会在拦截每一次请求，请求完成之后，都回去提交 session，session过期或创建一个新的session 返回去@Bean  public &lt;S extends Session&gt; SessionRepositoryFilter&lt;? extends Session&gt; springSessionRepositoryFilter(         SessionRepository&lt;S&gt; sessionRepository) {      SessionRepositoryFilter&lt;S&gt; sessionRepositoryFilter = new SessionRepositoryFilter&lt;&gt;(sessionRepository);      sessionRepositoryFilter.setHttpSessionIdResolver(this.httpSessionIdResolver);      return sessionRepositoryFilter;  }</code></pre><ol start="3"><li>实现了  org.springframework.session.Session 的 RedisSession 和 持有RedisSession 的包装类org.springframework.session.web.http.SessionRepositoryFilter.SessionRepositoryRequestWrapper.HttpSessionWrapper</li></ol><pre><code class="language-java">// org.springframework.session.data.redis.RedisIndexedSessionRepository.RedisSession// org.springframework.session.web.http.SessionRepositoryFilter.SessionRepositoryRequestWrapper.HttpSessionWrapper// org.springframework.session.web.http.SessionRepositoryFilter.SessionRepositoryRequestWrapper#getSession(boolean)@Override  public HttpSessionWrapper getSession(boolean create) {  // 当前请求中的 Attribute 属性中是否保存了session实体，避免与redis的多次交互，增加性能    HttpSessionWrapper currentSession = getCurrentSession();      if (currentSession != null) {         return currentSession;      }      // 通过请求Header Cookie 中 的session id 获取 session，获取到之后转换为包装类，放入 attribute 中    S requestedSession = getRequestedSession();      if (requestedSession != null) {         if (getAttribute(INVALID_SESSION_ID_ATTR) == null) {            requestedSession.setLastAccessedTime(Instant.now());            this.requestedSessionIdValid = true;            currentSession = new HttpSessionWrapper(requestedSession, getServletContext());            currentSession.markNotNew();            setCurrentSession(currentSession);            return currentSession;         }      }      else {         // This is an invalid session id. No need to ask again if         // request.getSession is invoked for the duration of this request       if (SESSION_LOGGER.isDebugEnabled()) {            SESSION_LOGGER.debug(                  &quot;No session found by id: Caching result for getSession(false) for this HttpServletRequest.&quot;);         }         setAttribute(INVALID_SESSION_ID_ATTR, &quot;true&quot;);      }      if (!create) {         return null;      }      if (SessionRepositoryFilter.this.httpSessionIdResolver instanceof CookieHttpSessionIdResolver            &amp;&amp; this.response.isCommitted()) {         throw new IllegalStateException(&quot;Cannot create a session after the response has been committed&quot;);      }      if (SESSION_LOGGER.isDebugEnabled()) {         SESSION_LOGGER.debug(               &quot;A new session was created. To help you troubleshoot where the session was created we provided a StackTrace (this is not an error). You can prevent this from appearing by disabling DEBUG logging for &quot;                     + SESSION_LOGGER_NAME,               new RuntimeException(&quot;For debugging purposes only (not an error)&quot;));      }      // 创建新的session    S session = SessionRepositoryFilter.this.sessionRepository.createSession();      session.setLastAccessedTime(Instant.now());      currentSession = new HttpSessionWrapper(session, getServletContext());      setCurrentSession(currentSession);      return currentSession;  }</code></pre><ol start="4"><li>HttpSessionWrapper 持有 RedisSession ，重点看一下 提交session的方法</li></ol><pre><code class="language-java">// org.springframework.session.web.http.SessionRepositoryFilter.SessionRepositoryRequestWrapper#commitSessionprivate void commitSession() {      HttpSessionWrapper wrappedSession = getCurrentSession();      if (wrappedSession == null) {         if (isInvalidateClientSession()) {            SessionRepositoryFilter.this.httpSessionIdResolver.expireSession(this, this.response);         }      }      else {         S session = wrappedSession.getSession();         clearRequestedSessionCache();         // sessionRepository 就是 RedisIndexedSessionRepository       SessionRepositoryFilter.this.sessionRepository.save(session);         String sessionId = session.getId();         if (!isRequestedSessionIdValid() || !sessionId.equals(getRequestedSessionId())) {            SessionRepositoryFilter.this.httpSessionIdResolver.setSessionId(this, this.response, sessionId);         }      }  }//  org.springframework.session.data.redis.RedisIndexedSessionRepository#save@Override  public void save(RedisSession session) {      session.save();      if (session.isNew) {         String sessionCreatedKey = getSessionCreatedChannel(session.getId());         this.sessionRedisOperations.convertAndSend(sessionCreatedKey, session.delta);         session.isNew = false;      }  }// org.springframework.session.data.redis.RedisIndexedSessionRepository.RedisSession#saveprivate void save() {// 该方法主要是看是不是要更换 session ID，    saveChangeSessionId();      // 主要是更新 session 失效时间，根据最后访问时间 lastAccessedTime    saveDelta();  }// org.springframework.session.data.redis.RedisIndexedSessionRepository.RedisSession#saveDeltaprivate void saveDelta() {      if (this.delta.isEmpty()) {         return;      }      String sessionId = getId();      getSessionBoundHashOperations(sessionId).putAll(this.delta);      // 省略部分代码    ......      this.delta = new HashMap&lt;&gt;(this.delta.size());  // 根据在 org.springframework.session.data.redis.RedisIndexedSessionRepository#getSession 中设置的 originalLastAccessTime 去更新缓存失效时间    Long originalExpiration = (this.originalLastAccessTime != null)            ? this.originalLastAccessTime.plus(getMaxInactiveInterval()).toEpochMilli() : null;      RedisIndexedSessionRepository.this.expirationPolicy.onExpirationUpdated(originalExpiration, this);  }// org.springframework.session.data.redis.RedisSessionExpirationPolicy#onExpirationUpdated// RedisSessionExpirationPolicy 过期策略，只有这一个实现void onExpirationUpdated(Long originalExpirationTimeInMilli, Session session) {      String keyToExpire = SESSION_EXPIRES_PREFIX + session.getId();      long toExpire = roundUpToNextMinute(expiresInMillis(session));        if (originalExpirationTimeInMilli != null) {         long originalRoundedUp = roundUpToNextMinute(originalExpirationTimeInMilli);         if (toExpire != originalRoundedUp) {            String expireKey = getExpirationKey(originalRoundedUp);            this.redis.boundSetOps(expireKey).remove(keyToExpire);         }      }        long sessionExpireInSeconds = session.getMaxInactiveInterval().getSeconds();      String sessionKey = getSessionKey(keyToExpire);        if (sessionExpireInSeconds &lt; 0) {         this.redis.boundValueOps(sessionKey).append(&quot;&quot;);         this.redis.boundValueOps(sessionKey).persist();         this.redis.boundHashOps(getSessionKey(session.getId())).persist();         return;      }        String expireKey = getExpirationKey(toExpire);      BoundSetOperations&lt;Object, Object&gt; expireOperations = this.redis.boundSetOps(expireKey);      expireOperations.add(keyToExpire);        long fiveMinutesAfterExpires = sessionExpireInSeconds + TimeUnit.MINUTES.toSeconds(5);        expireOperations.expire(fiveMinutesAfterExpires, TimeUnit.SECONDS);      if (sessionExpireInSeconds == 0) {         this.redis.delete(sessionKey);      }      else {         this.redis.boundValueOps(sessionKey).append(&quot;&quot;);         this.redis.boundValueOps(sessionKey).expire(sessionExpireInSeconds, TimeUnit.SECONDS);      }  // 刷新 session 失效时间，在原有基础上还加了 5分钟this.redis.boundHashOps(getSessionKey(session.getId())).expire(fiveMinutesAfterExpires, TimeUnit.SECONDS);  }</code></pre><h2 id="%E7%BB%93%E8%AF%AD" tabindex="-1">结语</h2><p>到这里，从请求开始 获取 session，到最后刷新session 失效时间，一整个流程就结束了。根据研究以及天马行空的想法，有一个骚操作，记录如下：</p><pre><code class="language-java">/**   * 项目中很多情况下都会有站内信功能，实时刷新的站内信 通常是轮询或者websocket，而在项目中又一般会设置 一定时间内不操作，自动登出（也就是session失效了）。 * 此处的骚操作就是，将这些方法在某次请求中不去刷新缓存失效的时间。 * 例如： 我在项目中设置session过期时间是 30分钟，但是由于我的项目在不停的请求消息列表接口，导致永远不会登出，以下是结合 spring-redis 作为session 的定制。 * 不刷新session的过期时间，可以用来过滤一些 轮询请求的接口   */  public static void noFlushSessionExpireTime() {      try {          HttpSession session = getHttpServletRequest().getSession();          Field f = session.getClass().getSuperclass().getDeclaredField(&quot;session&quot;);          f.setAccessible(true);          Session realSession = (Session) f.get(session);          Field delta = realSession.getClass().getDeclaredField(&quot;delta&quot;);          delta.setAccessible(true);          delta.set(realSession, new HashMap&lt;&gt;());      } catch (Exception e) {          // 捕获抛出的异常，不影响原有的业务逻辑      }  }</code></pre>]]>
                </content>
            </entry>
            <entry>
                <title><![CDATA[SpringBoot对于传输过程中的加解密方案]]></title>
                <link rel="alternate" type="text/html" href="https://www.sxkawzp.cn/archives/springboot对于传输过程中的加解密方案" />
                <id>tag:https://www.sxkawzp.cn,2024-11-14:springboot对于传输过程中的加解密方案</id>
                <published>2024-11-14T17:29:58+08:00</published>
                <updated>2024-11-14T17:37:20+08:00</updated>
                <author>
                    <name>doMore</name>
                    <uri>https://www.sxkawzp.cn</uri>
                </author>
                <content type="html">
                        <![CDATA[<h2 id="%E8%83%8C%E6%99%AF" tabindex="-1">背景</h2><p>在一次项目等保测评中，扫描出的问题有一项为：传输过程中未加密。为了使数据更加安全，决定将项目的请求参数和响应，全部采用对称加密之后传输，需要方根据指定的秘钥进行解密。</p><h2 id="%E9%A1%B9%E7%9B%AE%E7%AE%80%E4%BB%8B" tabindex="-1">项目简介</h2><p>项目基于 SpringBoot 2.5.x 进行构建。由于一些原因，项目中使用 Get、Post（form、json）方式请求。</p><h2 id="%E6%96%B9%E6%A1%88" tabindex="-1">方案</h2><blockquote><p>加密传输统一字段： payload 。字段值为，原有数据 json 化加密之后的字符串。对于地址栏参数不加密，文件上传接口也不加密。</p></blockquote><h3 id="%E8%A7%A3%E5%AF%86%E5%8F%82%E6%95%B0" tabindex="-1">解密参数</h3><ol><li>客户端端将所有的参数 json 化之后加密。</li><li>服务端将参数解密之后使用</li></ol><h4 id="form-%E8%A1%A8%E5%8D%95%E5%BD%A2%E5%BC%8F" tabindex="-1">form 表单形式</h4><ol><li>采用 Filter 的方式进行参数解析</li></ol><pre><code class="language-java">// org.springframework.web.filter.OncePerRequestFilter@Component  public class EncryptionRequestFilter extends OncePerRequestFilter {        @Value(&quot;${encryption.mark:true}&quot;)      private Boolean encryptionMark;        @Override      protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {          if (encryptionMark) {              filterChain.doFilter(new EncryptionHttpRequestWrapper(new CachedRequestWrapper(request)), response);          } else {              filterChain.doFilter(request, response);          }      }  }// 缓存 request 中的输入流 InputStream，因为这个使用过一次之后就会为空，后续不方便public class CachedRequestWrapper extends HttpServletRequestWrapper {        private byte[] requestBodyByte;        public CachedRequestWrapper(HttpServletRequest request) {          super(request);          try {              this.requestBodyByte = StreamUtils.copyToByteArray(request.getInputStream());          } catch (IOException e) {              // capture but do nothing              this.requestBodyByte = new byte[0];          }      }        @Override      public ServletInputStream getInputStream() {          ByteArrayInputStream bais = new ByteArrayInputStream(requestBodyByte);          return new ServletInputStream() {              @Override              public boolean isFinished() {                  return bais.available() == 0;              }              @Override              public boolean isReady() {                  return true;              }              @Override              public void setReadListener(ReadListener listener) {              }              @Override              public int read() throws IOException {                  return bais.read();              }          };      }      public byte[] getRequestBodyByte() {          return requestBodyByte;      }  }</code></pre><ol start="3"><li>参数解密，EncryptionHttpRequestWrapper 这种能够解决简单的一些参数，对与复杂形式结构的数据就无能为力</li></ol><pre><code class="language-java">// 无法解决的结构：/**   &quot;attachList[0].attachName&quot;: &quot;2024-11-14 11:46:55-2.jpg&quot;,&quot;attachList[0].attachAddress&quot;: &quot;/upload/jpg/20241114/367fe4ea04ed41fd92975b93e7abd722.jpg&quot;,&quot;attachList[0].attachSuffix&quot;: &quot;/jpg&quot;,&quot;attachList[0].attachSize&quot;: &quot;12709&quot;这种结构使用 json 的方式是很好解决的，但是这里无法去改。引入另一种 org.springframework.web.method.support.HandlerMethodArgumentResolver在执行 controller 方法之前进行参数解析。*/// 解密方式一// 解密 request 包装类public class EncryptionHttpRequestWrapper extends HttpServletRequestWrapper {        private Map&lt;String, String[]&gt; paramsMap = null;      private static final JSONConfig CREATE = JSONConfig.create().setDateFormat(DateUtil.YYYY_MM_DD_HH_MM_SS);        private final HttpServletRequest request;        public EncryptionHttpRequestWrapper(CachedRequestWrapper httpServletRequest) {          super(httpServletRequest);          this.request = httpServletRequest;          String encryption = request.getParameter(&quot;payload&quot;);          if (StrUtil.isBlank(encryption)) {              byte[] requestBodyByte = httpServletRequest.getRequestBodyByte();              String requestBodyString = URLDecoder.decode(StrUtil.str(requestBodyByte, StandardCharsets.UTF_8));              if (StrUtil.isBlank(requestBodyString)) {                  return;              }              List&lt;String&gt; paramSplit = StrUtil.split(requestBodyString, &quot;&amp;&quot;);              for (String param : paramSplit) {                  if (StrUtil.isNotBlank(param) &amp;&amp; param.startsWith(&quot;payload&quot;)) {                      encryption = param.substring(8);                      break;                  }              }          }          if (StrUtil.isBlank(encryption)) {              return;          }          String decrypt = AESUtil.decryptionLoginInfo(encryption);          JSONObject decryptParamMap = JSONUtil.parseObj(decrypt, CREATE);          this.paramsMap = new HashMap&lt;&gt;();          this.paramsMap.put(&quot;payload&quot;, new String[]{encryption});          Parameters parameters = new Parameters();          convert(decryptParamMap, &quot;&quot;, parameters);          Enumeration&lt;String&gt; parameterNames = parameters.getParameterNames();          while (parameterNames.hasMoreElements()) {              String parameterName = parameterNames.nextElement();              this.paramsMap.put(parameterName, parameters.getParameterValues(parameterName));          }      }        @Override      public String getParameter(String name) {          if (CollectionUtil.isEmpty(paramsMap)) {              return request.getParameter(name);          }          return Optional.ofNullable(paramsMap.get(name)).map(e -&gt; e[0]).orElse(null);      }        @Override      public Map&lt;String, String[]&gt; getParameterMap() {          if (CollectionUtil.isEmpty(paramsMap)) {              return request.getParameterMap();          }          return paramsMap;      }        @Override      public Enumeration&lt;String&gt; getParameterNames() {          if (CollectionUtil.isEmpty(paramsMap)) {              return request.getParameterNames();          }          Queue&lt;String&gt; queue = new LinkedBlockingQueue&lt;&gt;(paramsMap.keySet());          return new Enumeration&lt;String&gt;() {              @Override              public boolean hasMoreElements() {                  return !queue.isEmpty();              }                @Override              public String nextElement() {                  return queue.poll();              }          };      }        @Override      public String[] getParameterValues(String name) {          if (CollectionUtil.isEmpty(paramsMap)) {              return request.getParameterValues(name);          }          return paramsMap.get(name);      }          public void convert(JSONObject jsonObject, String preKey, Parameters parameters) {          for (Map.Entry&lt;String, Object&gt; entry : jsonObject.entrySet()) {              String key;              if (StrUtil.isBlank(preKey)) {                  key = entry.getKey();              } else {                  key = preKey + &quot;.&quot; + entry.getKey();              }              Object value = jsonObject.get(entry.getKey());              if (Objects.isNull(value)) {                  continue;              }              boolean primitive = value.getClass().isPrimitive();              if (primitive) {                  parameters.addParameter(key, String.valueOf(value));              } else if (value instanceof Byte || value instanceof Short || value instanceof Integer ||                      value instanceof Long || value instanceof Float || value instanceof Double ||                      value instanceof Character || value instanceof Boolean) {                  parameters.addParameter(key, String.valueOf(value));              } else if (value instanceof JSONObject) {                  convert((JSONObject) value, key, parameters);              } else if (value instanceof JSONArray) {                  JSONArray array = (JSONArray) value;                  for (int i = 0; i &lt; array.size(); i++) {                      Object obj = array.getObj(i);                      if (Objects.isNull(obj)) {                          continue;                      }                      if (obj instanceof Byte || obj instanceof Short || obj instanceof Integer ||                              obj instanceof Long || obj instanceof Float || obj instanceof Double ||                              obj instanceof Character || obj instanceof Boolean || obj.getClass().isPrimitive()) {                          parameters.addParameter(key + &quot;[&quot; + i + &quot;]&quot;, String.valueOf(obj));                      } else if (obj instanceof JSONObject) {                          JSONObject el = array.getJSONObject(i);                          convert(el, key + &quot;[&quot; + i + &quot;]&quot;, parameters);                      } else if (obj instanceof String) {                          parameters.addParameter(key + &quot;[&quot; + i + &quot;]&quot;, String.valueOf(obj));                      }                  }              } else {                  parameters.addParameter(key, value.toString());              }          }      }  }// 解密方式二@Configuration  public class ProjectWebConfigurer implements WebMvcConfigurer {      @Override      public void addArgumentResolvers(List&lt;HandlerMethodArgumentResolver&gt; resolvers) {          resolvers.add(new CustomMapArgumentResolver());      }  }public class CustomMapArgumentResolver implements HandlerMethodArgumentResolver {      // 判断是否要启用该参数解析实现，这里只对复杂的接口进行解析    @Override      public boolean supportsParameter(MethodParameter parameter) {          Method method = parameter.getMethod();          if (Objects.isNull(method)) {              return false;          }          Parameter[] parameters = method.getParameters();          if (parameters.length &gt; 1) {              return false;          }          Parameter realParameter = parameters[0];          Class&lt;?&gt; type = realParameter.getType();          boolean primitive = type.isPrimitive();          if (primitive) {              return false;          } else if (Byte.class.getName().equals(type.getName())                  || Short.class.getName().equals(type.getName())                  || Integer.class.getName().equals(type.getName())                  || Long.class.getName().equals(type.getName())                  || Float.class.getName().equals(type.getName())                  || Double.class.getName().equals(type.getName())                  || Character.class.getName().equals(type.getName())                  || String.class.getName().equals(type.getName())                  || Boolean.class.getName().equals(type.getName())) {              return false;          }          return true;      }  // 解析方案 读取到对应的数据流，然后 将 attachList[0].attachSize 形式的参数，转换为规范的 json 格式。这里偷个懒，只转换了一层，实际项目中也只有一层。    @Override      public Object resolveArgument(MethodParameter parameter, ModelAndViewContainer mavc, NativeWebRequest request, WebDataBinderFactory binderFactory) throws Exception {          String payload = request.getParameter(&quot;payload&quot;);          Class&lt;?&gt; parameterType = parameter.getParameterType();          if (StrUtil.isBlank(payload)) {              return BeanUtils.instantiateClass(parameterType);          }          JSONObject jsonObject = JSON.parseObject(AESUtil.decryptionLoginInfo(payload));          formatParamForJsonObj(jsonObject);          Object obj = jsonObject.toJavaObject(parameterType);          PropertyDescriptor payloadPd = BeanUtils.getPropertyDescriptor(parameterType, &quot;payload&quot;);          if (Objects.nonNull(payloadPd)) {              payloadPd.getWriteMethod().invoke(obj, payload);          }          return obj;      }  // 规范 json ，确保复杂结构形式的参数能正常被转化为实体    private void formatParamForJsonObj(JSONObject json) {          Set&lt;String&gt; needFormatParamNameSet = new HashSet&lt;&gt;();          Set&lt;String&gt; needFormatParamNameDotSet = new HashSet&lt;&gt;();          for (String key : json.keySet()) {              if (key.contains(&quot;[&quot;) &amp;&amp; key.contains(&quot;]&quot;)) {                  needFormatParamNameSet.add(key);              } else if (key.contains(&quot;.&quot;)) {                  needFormatParamNameDotSet.add(key);              }          }            for (String paramName : needFormatParamNameSet) {              int i = paramName.indexOf(&quot;[&quot;);              int j = paramName.indexOf(&quot;]&quot;);              String startPropertyName = paramName.substring(0, i);              JSONArray propertyValue = json.getJSONArray(startPropertyName);              if (Objects.isNull(propertyValue)) {                  propertyValue = new JSONArray();                  json.put(startPropertyName, propertyValue);              }              if (paramName.contains(&quot;.&quot;)) {                  int elIndex = Integer.parseInt(paramName.substring(i + 1, j));                  while (elIndex &gt;= propertyValue.size()) {                      propertyValue.add(new JSONObject());                  }                  JSONObject propertyElValue = propertyValue.getJSONObject(elIndex);                  propertyElValue.put(paramName.substring(paramName.indexOf(&quot;.&quot;) + 1), json.get(paramName));              } else {                  propertyValue.set(Integer.parseInt(paramName.substring(i + 1, j)), json.get(paramName));              }          }            for (String paramName : needFormatParamNameDotSet) {              int i = paramName.indexOf(&quot;.&quot;);              String startPropertyName = paramName.substring(0, i);              JSONObject propertyValue = json.getJSONObject(startPropertyName);              if (Objects.isNull(propertyValue)) {                  propertyValue = new JSONObject();                  json.put(startPropertyName, propertyValue);              }              propertyValue.put(paramName.substring(i + 1), json.get(paramName));          }      }    }</code></pre><h4 id="json-%E5%BD%A2%E5%BC%8F" tabindex="-1">json 形式</h4><p>使用 org.springframework.web.servlet.mvc.method.annotation.RequestBodyAdviceAdapter 处理 所有被 @RequestBody 注解标注的方法参数。</p><pre><code class="language-java">@RestControllerAdvice  @Component  public class EncryptionRequestBodyAdvice extends RequestBodyAdviceAdapter {        @Value(&quot;${encryption.mark:true}&quot;)      private Boolean encryptionMark;          @Override      public boolean supports(MethodParameter methodParameter, Type type, Class&lt;? extends HttpMessageConverter&lt;?&gt;&gt; aClass) {          return true;      }        @Override      public HttpInputMessage beforeBodyRead(HttpInputMessage inputMessage, MethodParameter parameter, Type targetType, Class&lt;? extends HttpMessageConverter&lt;?&gt;&gt; converterType) throws IOException {          if (!encryptionMark) {              return inputMessage;          }          return new EncryptionHttpInputMessageWrapper(inputMessage);      }  }public class EncryptionHttpInputMessageWrapper implements HttpInputMessage {        private final HttpInputMessage httpInputMessage;        public EncryptionHttpInputMessageWrapper(HttpInputMessage httpInputMessage) {          this.httpInputMessage = httpInputMessage;      }        @Override      public InputStream getBody() throws IOException {            String dataStr = null;          String decrypt;          try {              // 提取数据              InputStream is = httpInputMessage.getBody();              byte[] data = StreamUtils.copyToByteArray(is);              dataStr = new String(data, StandardCharsets.UTF_8);              JSONObject jsonObject = JSONUtil.parseObj(dataStr);              decrypt = AESUtil.decryptionLoginInfo(jsonObject.getStr(&quot;payload&quot;));          } catch (Exception e) {              if (dataStr == null) {                  dataStr = &quot;&quot;;              }              return new ByteArrayInputStream(dataStr.getBytes(StandardCharsets.UTF_8));          }          return new ByteArrayInputStream(decrypt.getBytes(StandardCharsets.UTF_8));      }        @Override      public HttpHeaders getHeaders() {          return httpInputMessage.getHeaders();      }  }</code></pre><h3 id="%E5%8A%A0%E5%AF%86%E5%93%8D%E5%BA%94" tabindex="-1">加密响应</h3><p>org.springframework.web.servlet.mvc.method.annotation.ResponseBodyAdvice 对所用 json 形式的响应加密。非json形式的不做处理</p><pre><code class="language-java">@RestControllerAdvice  @Component  public class EncryptionResponseBodyAdvice implements ResponseBodyAdvice&lt;Object&gt; {        @Resource      private ObjectMapper jacksonObjectMapper;        @Value(&quot;${encryption.mark:true}&quot;)      private Boolean encryptionMark;        @Override      public boolean supports(MethodParameter returnType, Class&lt;? extends HttpMessageConverter&lt;?&gt;&gt; converterType) {          return true;      }        @Override      public Object beforeBodyWrite(Object body, MethodParameter returnType, MediaType selectedContentType, Class&lt;? extends HttpMessageConverter&lt;?&gt;&gt; selectedConverterType, ServerHttpRequest request, ServerHttpResponse response) {          if (!encryptionMark) {              return body;          }          if (MediaType.APPLICATION_JSON.equals(selectedContentType) || MediaType.APPLICATION_JSON_UTF8.equals(selectedContentType)) {              String encryptHex;              try {                  encryptHex = AESUtil.encryptionLoginInfo(jacksonObjectMapper.writeValueAsString(body));              } catch (JsonProcessingException e) {                  encryptHex = &quot;&quot;;              }              return Collections.singletonMap(&quot;payload&quot;, encryptHex);          }          return body;      }  }</code></pre>]]>
                </content>
            </entry>
            <entry>
                <title><![CDATA[Manacher 算法简介]]></title>
                <link rel="alternate" type="text/html" href="https://www.sxkawzp.cn/archives/manacher算法简介" />
                <id>tag:https://www.sxkawzp.cn,2024-10-11:manacher算法简介</id>
                <published>2024-10-11T16:31:28+08:00</published>
                <updated>2024-10-11T16:38:13+08:00</updated>
                <author>
                    <name>doMore</name>
                    <uri>https://www.sxkawzp.cn</uri>
                </author>
                <content type="html">
                        <![CDATA[<h2 id="%E7%AE%80%E4%BB%8B" tabindex="-1">简介</h2><p>Manacher 算法是一种用于在线性时间内找到字符串中最长回文子串的算法。该算法的核心思想是利用回文的对称性，通过动态规划的方式避免重复计算，从而显著提高效率。下面我将详细解释这个算法，并给出一个 Java 示例。</p><h2 id="%E7%AE%97%E6%B3%95%E8%AF%A6%E8%A7%A3" tabindex="-1">算法详解</h2><h3 id="1.-%E9%A2%84%E5%A4%84%E7%90%86%E5%AD%97%E7%AC%A6%E4%B8%B2" tabindex="-1">1. 预处理字符串</h3><p>为了处理奇数长度和偶数长度的回文子串，Manacher 算法通过在每个字符之间（包括字符串首尾）插入一个特殊字符（如 <code>#</code>），并在字符串首尾添加不同的特殊字符（如 <code>^</code> 和 <code>$</code>），从而将所有情况统一为奇数长度处理。</p><blockquote><p>注意： 特殊字符 ^ 和 $ 也可以不加，算法需要特殊处理一下</p></blockquote><h3 id="2.-%E5%AE%9A%E4%B9%89%E8%BE%85%E5%8A%A9%E6%95%B0%E7%BB%84" tabindex="-1">2. 定义辅助数组</h3><ul><li>定义一个数组 p，其中 p[i] 表示 以 i 为中心的最长回文子串的半径（包括中心字符）</li><li>半径的定义使算法可以方便地利用回文的对称性</li></ul><h3 id="3.%E5%88%9D%E5%A7%8B%E5%8C%96" tabindex="-1">3.初始化</h3><ul><li>初始化 p 数组，并设置两个指针 c (当前回文中心) 和 r （当前回文边界）</li></ul><h3 id="4.-%E9%81%8D%E5%8E%86%E5%AD%97%E7%AC%A6%E4%B8%B2" tabindex="-1">4. 遍历字符串</h3><ul><li>对于每个字符 <code>i</code>，计算其对应的回文半径 <code>P[i]</code>。</li><li>如果 <code>i</code> 在 <code>R</code> 的左边（即 <code>i &lt; R</code>），则利用对称性找到 <code>i</code> 关于 <code>C</code> 的对称点 <code>j = 2*C - i</code>。<ul><li>如果 <code>P[j]</code> 不超过 <code>R - i</code>（即对称的回文部分完全在已知的回文内部），则 <code>P[i] = P[j]</code>。</li><li>否则，需要扩展 <code>i</code> 的回文半径，直到遇到不匹配的字符或到达字符串边界。</li></ul></li><li>更新 <code>C</code> 和 <code>R</code>，如果 <code>i + P[i] &gt; R</code>，则更新 <code>C = i</code> 和 <code>R = i + P[i]</code>。</li></ul><h3 id="5.-%E6%89%BE%E5%88%B0%E6%9C%80%E9%95%BF%E7%9A%84%E5%9B%9E%E6%96%87%E5%AD%90%E4%B8%B2" tabindex="-1">5.  找到最长的回文子串</h3><ul><li>遍历 <code>P</code> 数组，找到最大的 <code>P[i]</code>，其对应的回文子串即为原字符串中从 <code>i - P[i] / 2</code> 到 <code>i + P[i] / 2 - 1</code>（去除预处理字符）的子串。</li></ul><pre><code class="language-java">// 此处算法中没有 在开头结尾添加特殊字符。只用到分隔符 &quot;#&quot;public class L5 {        public static void main(String[] args) {          L5 l5 = new L5();          String r = &quot;babad&quot;;          String s = l5.longestPalindrome(r);          System.out.println(s);      }      public String longestPalindrome(String s) {        if (s == null || s.isEmpty()) {            return &quot;&quot;;        }        char[] str = marcherString(s);        int[] p = new int[str.length];        marcherFind(str, p, 0);                // 找到最长回文子串        int maxLen = 0;        int centerIndex = 0;        for (int i = 1; i &lt; p.length - 1; i++) {            if (p[i] &gt; maxLen) {                maxLen = p[i];                centerIndex = i;            }        }        // 提取原字符串中的最长回文子串        int start = (centerIndex - maxLen) / 2;        return s.substring(start, start + maxLen);    }    private void marcherFind(char[] s, int[] p, int l) {        int c = l - 1;        int r = l - 1;        int n = s.length;        for (int i = l; i &lt; n; i++) {            p[i] = r &gt; i ? Math.min(p[2 * c - i], r - i) : 1;            while (i + p[i] &lt; n &amp;&amp; i - p[i] &gt; l - 1 &amp;&amp; s[i + p[i]] == s[i - p[i]]) {                p[i]++;            }            p[i]--;            if (i + p[i] &gt; r) {                r = i + p[i];                c = i;            }        }    }    private char[] marcherString(String s) {        char[] chars = s.toCharArray();        char[] ans = new char[chars.length * 2 + 1];        int index = 0;        for (int i = 0; i != ans.length; i++) {            ans[i] = (i &amp; 1) == 0 ? &#39;#&#39; : chars[index++];        }        return ans;    }}</code></pre>]]>
                </content>
            </entry>
            <entry>
                <title><![CDATA[Spring Bean配置问题]]></title>
                <link rel="alternate" type="text/html" href="https://www.sxkawzp.cn/archives/springbean-pei-zhi-wen-ti" />
                <id>tag:https://www.sxkawzp.cn,2024-07-24:springbean-pei-zhi-wen-ti</id>
                <published>2024-07-24T15:06:58+08:00</published>
                <updated>2024-07-24T15:07:40+08:00</updated>
                <author>
                    <name>doMore</name>
                    <uri>https://www.sxkawzp.cn</uri>
                </author>
                <content type="html">
                        <![CDATA[<h1 id="%E9%97%AE%E9%A2%98" tabindex="-1">问题</h1><p>使用 @Autowire 自动装配时，注入的类型不是自己想要的类型。</p><h1 id="%E7%9B%B8%E5%85%B3%E8%B5%84%E6%96%99" tabindex="-1">相关资料</h1><h2 id="spring%E4%B8%AD%E6%B3%A8%E8%A7%A3%E8%87%AA%E5%8A%A8%E8%A3%85%E9%85%8D" tabindex="-1">Spring中注解自动装配</h2><h3 id="%40resource-%E5%92%8C-%40autowire" tabindex="-1">@Resource 和 @Autowire</h3><p>@Autowired: 用于构造器、方法、参数或字段上，表明需要自动注入一个Bean。Spring会自动装配匹配的Bean。</p><p>@Qualifier: 与@Autowired一起使用时，指定要注入的Bean的名称，以避免与其他Bean混淆。</p><p>@Resource: 来自JDK，类似于@Autowired，但默认是按名称装配，也可以混合使用。</p><p>@Inject: 来自javax.inject包，类似于@Autowired，属于JSR-330标准的一部分。</p><h3 id="%40resource%E5%92%8C%40autowired%E6%B3%A8%E8%A7%A3%E5%8C%BA%E5%88%AB" tabindex="-1">@Resource和@Autowired注解区别</h3><p>@Autowired 是Spring框架提供的注解，主要用于根据类型自动装配依赖项。</p><p>行为和特性：</p><ol><li><p>按类型装配：默认情况下，@Autowired按类型自动装配Bean。</p></li><li><p>可选依赖：如果你的依赖是可选的，可以使用required=false设置：</p></li><li><p>构造器、方法或字段：可以用在构造器，属性字段或Setter方法上。</p></li><li><p>结合@Qualifier：可以和@Qualifier结合使用以实现按名称装配。</p></li><li><p>作为Spring特有的注解，它更深度地集成在Spring的生态系统中，更适合与其他Spring注解一起使用。</p></li></ol><p>@Resource 是JDK提供的注解，属于Java依赖注入规范（JSR-250）的一部分。</p><p>行为和特性：</p><ol><li><p>按名称装配：默认情况下，@Resource按名称装配。如果没有匹配到名称，再按类型装配。</p></li><li><p>不支持required属性：与@Autowired不同，@Resource不支持required属性。</p></li><li><p>可以用于字段和Setter方法：虽然也可以用于构造器，但不常见。通常用在字段或Setter方法上。</p></li><li><p>由于是Java EE规范的一部分，它可以与其他Java EE注解（如@PostConstruct和@PreDestroy）更好地配合使用。</p></li></ol><h2 id="spring%E4%BE%9D%E8%B5%96%E6%B3%A8%E5%85%A5%E4%BC%98%E5%85%88%E7%BA%A7" tabindex="-1">Spring依赖注入优先级</h2><h3 id="%E4%BD%BF%E7%94%A8-%40resource" tabindex="-1">使用 @Resource</h3><p>在使用 @Resource 注解进行依赖注入时，优先级规则如下：</p><h4 id="%E6%98%8E%E7%A1%AE%E6%8C%87%E5%AE%9A%E5%90%8D%E7%A7%B0" tabindex="-1">明确指定名称</h4><ul><li><p>如果通过 @Resource(name=“beanName”) 明确指定了 Bean 的名称，那么 Spring 会首先按照名称匹配进行注入。</p></li><li><p>在这种情况下，@Primary 注解不会影响注入结果。</p></li></ul><h4 id="%E5%AD%97%E6%AE%B5%E6%88%96%E5%B1%9E%E6%80%A7%E5%90%8D%E7%A7%B0" tabindex="-1">字段或属性名称</h4><ul><li><p>如果没有通过 name 属性指定 Bean 的名称，Spring 会尝试按照字段或属性的名称进行匹配。</p></li><li><p>在这种情况下，@Primary 注解也不会影响注入结果。</p></li></ul><h4 id="%E7%B1%BB%E5%9E%8B%E5%8C%B9%E9%85%8D" tabindex="-1">类型匹配</h4><ul><li><p>如果按名称匹配失败（包括明确指定名称和按字段名称匹配都没有找到合适的 Bean），Spring 会按类型匹配。</p></li><li><p>在这种情况下，如果存在多个同类型的 Bean，则 @Primary 注解会起作用，标记为 @Primary 的 Bean 将被优先注入。</p></li></ul><h3 id="%E4%BD%BF%E7%94%A8%40autowired" tabindex="-1">使用@Autowired</h3><h4 id="%E7%B1%BB%E5%9E%8B%E5%8C%B9%E9%85%8D-1" tabindex="-1">类型匹配</h4><ul><li>Spring 首先通过类型匹配找到所有符合要求的候选 Bean。如果只有一个候选 Bean，那么该 Bean 会被注入。</li></ul><h4 id="%E5%90%8D%E7%A7%B0%E5%8C%B9%E9%85%8D%E7%BB%93%E5%90%88-%40qualifier" tabindex="-1">名称匹配结合 @Qualifier</h4><ul><li><p>如果有多个同类型的 Bean，可以使用 @Qualifier 注解来指定具体的 Bean。</p></li><li><p>@Qualifier 的值必须与一个候选 Bean 的名称匹配，匹配成功的 Bean 会被注入。</p></li></ul><h4 id="%E4%BD%BF%E7%94%A8-%40primary" tabindex="-1">使用 @Primary</h4><ul><li>如果仍存在多个符合要求的 Bean，并且其中一个 Bean 标记了 @Primary，Spring 会优先选择标记了 @Primary 的 Bean 进行注入。</li></ul><h4 id="%E5%90%8D%E7%A7%B0%E5%8C%B9%E9%85%8D%E5%AD%97%E6%AE%B5%E6%88%96%E5%B1%9E%E6%80%A7%E5%90%8D%E7%A7%B0" tabindex="-1">名称匹配字段或属性名称</h4><ul><li><p>在没有使用 @Qualifier 时，如果存在多个候选 Bean，Spring 会尝试通过字段或属性名称进行匹配。</p></li><li><p>如果找到名称匹配的 Bean，则该 Bean 会被注入。</p></li></ul><p><strong>NoUniqueBeanDefinitionException：</strong></p><ul><li>如果存在多个候选 Bean，但没有使用 @Qualifier 指定名称，且没有标记 @Primary，会抛出 NoUniqueBeanDefinitionException，表明有多个 Bean 类型匹配但无法确定注入哪个。</li></ul><h1 id="%E7%BB%93%E8%AE%BA" tabindex="-1">结论</h1><p>在使用配置类配置 Bean 时，@Bean 方法的参数，或者用@Autowired配置 Bean 时，最好使用@Qualifier 指定注入的bean，避免注入的bean不符合预期。<strong>@Resource 则通常不存在这种烦恼。而且</strong> <strong>@Resource 是 JDK 提供，以后如果要迁移到别的框架，也会方便一些。</strong></p>]]>
                </content>
            </entry>
            <entry>
                <title><![CDATA[Mysql OrderBy 乱序问题]]></title>
                <link rel="alternate" type="text/html" href="https://www.sxkawzp.cn/archives/mysqlorderby-luan-xu-wen-ti" />
                <id>tag:https://www.sxkawzp.cn,2024-07-12:mysqlorderby-luan-xu-wen-ti</id>
                <published>2024-07-12T15:51:08+08:00</published>
                <updated>2024-07-12T15:51:08+08:00</updated>
                <author>
                    <name>doMore</name>
                    <uri>https://www.sxkawzp.cn</uri>
                </author>
                <content type="html">
                        <![CDATA[<h2 id="%E5%89%8D%E8%A8%80" tabindex="-1">前言</h2><h2 id="%E9%97%AE%E9%A2%98" tabindex="-1">问题</h2><p>在执行各种列表等诸多操作时，都需要将从数据库中查出的结果按照<strong>时间（create_time）</strong> 做倒排处理和翻页处理。同一时间点（例如1s内），若包含的结果数量过多，将不可避免的出现前后页结果重复的问题。<br />例如：在一个 message 表中，有在同一时间有一部分消息的发送时间是相同的。</p><pre><code class="language-mysql">-- 第一页select message_id, create_time  from message  where message_id in (                       14128,                       14127,                       14126,                       14125,                       14124,                       14123,                       14122,                       14121      )  order by create_time limit 0,3  ;-- 结果-- 14127,2024-02-09 10:57:17-- 14126,2024-02-09 10:57:17-- 14125,2024-02-09 10:57:17-- 第二页 结果-- 14124,2024-02-09 10:57:17-- 14125,2024-02-09 10:57:17-- 14126,2024-02-09 10:57:17</code></pre><h2 id="%E4%BF%AE%E5%A4%8D" tabindex="-1">修复</h2><p>在原有的时间排序上增加 id 作为辅助排序。</p><pre><code class="language-mysql">select message_id, create_time  from message  where message_id in (                       14128,                       14127,                       14126,                       14125,                       14124,                       14123,                       14122,                       14121      )  order by create_time, message_id limit 0,3  ;-- 第一页-- 14121,2024-02-09 10:57:17-- 14122,2024-02-09 10:57:17-- 14123,2024-02-09 10:57:17-- 第二页-- 14124,2024-02-09 10:57:17-- 14125,2024-02-09 10:57:17-- 14126,2024-02-09 10:57:17</code></pre><ol><li>这个问题一般需要满足两个条件，一是要根据大量具有相同值的字段排序，一是需要使用到LIMIT关键字。</li><li>ORDER BY 的排序顺序对于无序列是非确定性的。</li></ol><h2 id="%E7%BB%93%E8%AE%BA" tabindex="-1">结论</h2><ol><li>当需要根据记录的创建时间排序时，不妨直接使用id排序，两者具有同等的效果。</li><li>如需要根据记录的更新时间排序，不妨先按照更新时间排序，再按照id排序。</li><li>所有的数据表一定要有一个主键，因为即使你不显示的创建主键，MySQL会判断表中是否有非NULL的整型唯一索引,如果有,则该列为主键，没有的话则会自动创建一个6字节的主键（很容易撑爆数据库）。</li></ol><h2 id="%E5%AE%98%E6%96%B9%E6%96%87%E6%A1%A3" tabindex="-1">官方文档</h2><p><a href="https://dev.mysql.com/doc/refman/8.0/en/limit-optimization.html" target="_blank">https://dev.mysql.com/doc/refman/8.0/en/limit-optimization.html</a></p>]]>
                </content>
            </entry>
</feed>
