<?xml version="1.0" encoding="utf-8"?>
<rss version="2.0"
        xmlns:atom="http://www.w3.org/2005/Atom"
        xmlns:content="http://purl.org/rss/1.0/modules/content/">
    <channel>
        <title>yww</title>
        <link>https://yww52.com</link>
        <image>
            <url>https://yww52.com/favicon.png</url>
            <title>yww</title>
            <link>https://yww52.com</link>
        </image>
        <atom:link href="https://yww52.com/rss2.xml" rel="self" type="application/rss+xml"/>
        <description>debug myself</description>
        <pubDate>Fri, 22 May 2026 00:00:00 +0000</pubDate>
        <generator>Astro</generator>
        <item>
            <title>记一次重写博客</title>
            <link>https://yww52.com/posts/ebb85548/</link>
            <guid isPermaLink="true">https://yww52.com/posts/ebb85548/</guid>
            <pubDate>Fri, 24 Apr 2026 00:00:00 +0000</pubDate>
            <description>我的博客已经使用butterfly这个主题很长一段时间了，虽然期间做了很多样式微调，但也难免产生了一点审美疲劳，一直想换一个新的风格。恰逢最近挺无聊的，于是重新搭建一套属于自己的博客系统。 其实重写博客的想法，早在两三年前就已经萌生。当时我特别倾向于前后端分离的动态博客，认为这种动态博客更加的灵活。为此我还专门用 SpringBoot3 + Vue3 实现了一版butterfly版本的动态博客与后台管理系统，但在实际体验了一会Halo这</description>
            <content:encoded><![CDATA[<img src="https://img.yww52.com/2026/ebb85548.png" alt="记一次重写博客" style="max-width:100%;height:auto;margin-bottom:15px;" /><h1>序言</h1><p>我的博客已经使用<code>butterfly</code>这个主题很长一段时间了，虽然期间做了很多样式微调，但也难免产生了一点审美疲劳，一直想换一个新的风格。恰逢最近挺无聊的，于是重新搭建一套属于自己的博客系统。</p><p>其实重写博客的想法，早在两三年前就已经萌生。当时我特别倾向于前后端分离的动态博客，认为这种动态博客更加的灵活。为此我还专门用 SpringBoot3 + Vue3 实现了一版butterfly版本的动态博客与后台管理系统，但在实际体验了一会Halo这种比我写的更完善的博客后，我发现这种动态博客并不完全适合我的使用场景。我只是想设计实现一套系统罢了，就下意识认为这种动态博客更好，却忽略了我的习惯和强行让自己自适应。最终就放弃了继续优化和完善这套方案，也一直使用butterfly至今。</p><h1>用AI重写博客</h1><p>过去一年多，自从DeepSeek-R1发布以后，AI就像打了鸡血一样，发展的更快了。相较于其他的生成式AI，在本就推崇开源的行业，大模型的编程能力更是迎来了爆发式的提升，大模型已经可以高效完成大量的编码工作，十几分钟就可以完成一天的代码量。于是这次重构，就打算尝试全程不手动修改代码，完全由AI完成开发。</p><p>在让 AI 编码之前，首先要确定技术栈，毕竟AI可以提供建议，但最终方向还需自己把控。</p><p>我最先确定的事情，就是刚刚提到的，继续采用纯前端静态博客，放弃带服务端的动态博客。</p><p>很多人都偏爱Halo，WordPress这类动态博客，甚至为静态博客额外接入服务端。更有甚者逐步把博客做成社区形态，引入登录、发帖、权限等复杂功能，不仅开发和维护成本高，备案也会变得麻烦。</p><p>有部分人是小白，这种动态博客适合他们，但是我更习惯用 Typora 本地写作，不喜欢在网页编辑器里写，因此静态博客是更适合我习惯的选择。</p><p>确定方向后，我看了很多静态站点生成器后，选择了扩展性更好的 Astro负责页面构建；样式与脚本则直接使用原生 CSS + TypeScript，没有引入 Tailwind CSS 这类框架。既然代码都是由 AI 完成，不如回归简洁，大道至简。</p><p>整个开发过程，除了反复思考设计风格耗费了太多的时间外，主体功能用几天就全部完成。</p><h1>实践</h1><h2>设计工具</h2><p>设计阶段我仅短暂试用了 Figma。它主要的功能就是把自然语言转换成可预览的界面框架，我觉得应该不是生成的代码在后台跑，就只是简单的框架加上图片交互，有点类似墨刀吧。我感觉更偏向产品与设计师的协作工具。</p><p>对我这种全干来说，先让 AI 生成设计图再实现，反而不如直接脑补设计，再让 AI 直接生成更高效，毕竟AI生成代码很快，不用考虑花半天时间实现代码后发现不符合心意的问题。而且这样也能更清楚实现成本与取舍，快速更改设计和技术方案吧。</p><h2>代码生成</h2><p>代码环节我主要使用了两款AI吧</p><ul><li>主力：codex (GPT-5.4)</li><li>简单任务和设计： Gemini3.1</li></ul><p>虽然很多人都说Gemini写前端最好，codex写前端一言难尽，但是就我的实际体验来看，Gemini仅仅在设计审美方向上更好，生成代码的稳定性、逻辑准确性等等很多方面都不如codex。使用Gemini的时候，我经常会碰到修改一个功能，组件的样式直接错乱，或者代码生成到一半就中断了，还说已经完成了。而我用codex，在使用同个提示词的情况下，基本可以一次通过，就算不通过，也不会出现样式错乱和逻辑冲突的问题。</p><h2>skill</h2><p>我不喜欢安装过多的skill，既占用上下文，又可能导致提示词之间互相干扰，导致一加一小于一的情况。本次只使用了两个：</p><ul><li>superpowers: 规范开发流程，提供实现方案与测试工作流</li><li>impeccable: 提高设计审美</li></ul><p>impeccable就不评价了，让你风格和色彩统一，设计审美更好一点。</p><p><code>superpowers</code> 能让简单项目的开发更规范，但缺点也很明显Token 消耗极高，很多输出冗长。最影响效率的是它会无差别触发完整工作流，哪怕只是修改一个颜色，也会自动生成一整套单元测试，大量时间被无效测试浪费，只能去更新他很多的提示词做矫正，但这也很难阻止大多情况的故意复杂化了。也不是说测试不好，但这个skill是无法判断是否需要启动这个工作流，也无法判断是否需要进行测试，容易导致大量Token被浪费掉。使用Gemini就是这样，有时候会假模假样的加个单元测试，测试成功了样式也会错落，功能也没有实现。</p><p>所以当发现AI输出的内容，处理问题的方向出错了，偏离了预期，就要立即中断，重新引导或者重新开另一个会话，避免白白浪费Token或者把逻辑思路引导到更难解决的方向。</p><h2>工程管理</h2><p>AI写代码一开始就要设计整体的架构和组件的设计和调用，最好就是有想法了，就让AI去判断这个设计行不行，不然越到后面越难改。总体架构设计好后，直接让AI协助生成一份项目的提示词，把架构，边界，代码处理都写好，避免AI乱改代码。经过大改动的话，就让AI优化一下AGENTS.md和GEMINI.md就行。</p><p>虽然这么说，但是开发到后面，代码写的太快，加上不经常写样式和脚本，于是review就及其痛苦，你也不知道写的是不是最好的方案，写的是不是有问题，看不过来的，即使是让AI开高推理去review，这个项目也难免变成半个黑盒，反正是没出现过怎么修都修不好的bug，只是有些心里负担罢了。</p><p>除了项目文档，还写了一些其他的辅组skill。比如风格色彩，git规范提交，文档处理等等，原本想写成command的，发现command已经被codex和claude code淘汰了，功能合并到skill里面，只有gemini还保存这个管理command的功能，没几个月就成为时代的眼泪了。</p><h1>结尾</h1><p>现在AI发展的很快，感觉博客和Stack Overflow一样，越来越少人写东西，问问题了，遇事不决就问AI。</p><p>搞的我也不知道要写什么了，除了比较冗长不好问到的东西，基本都是问AI了。</p><p>可能后面随缘写点其他东西吧hh。</p>]]></content:encoded>
            <category domain="https://yww52.com/categories/%25E6%2588%2591%25E7%259A%2584%25E6%258A%2580%25E6%259C%25AF%25E5%25AD%25A6%25E4%25B9%25A0%25E7%2589%25A9%25E8%25AF%25AD%25E6%259E%259C%25E7%2584%25B6%25E6%259C%2589%25E9%2597%25AE%25E9%25A2%2598/">我的技术学习物语果然有问题</category>
        </item>
        <item>
            <title>自建busuanzi网站计数服务</title>
            <link>https://yww52.com/posts/5315dfbb/</link>
            <guid isPermaLink="true">https://yww52.com/posts/5315dfbb/</guid>
            <pubDate>Sun, 10 Aug 2025 00:00:00 +0000</pubDate>
            <description>自建busuanzi网站计数服务</description>
            <content:encoded><![CDATA[<img src="https://img.yww52.com/2025/8/2025-8-10top_img.jpg" alt="自建busuanzi网站计数服务" style="max-width:100%;height:auto;margin-bottom:15px;" /><h1>序言</h1><p> <a href="https://busuanzi.ibruce.info/">不蒜子(busuanzi)</a> 是一个广受欢迎的免费网页计数服务，能够轻松实现网站的页面浏览量(PV)和独立访客数(UV)统计。许多基于Hexo的网站都在使用busuanzi来实现访问统计功能，我的博客当前使用的主题是<code>Butterfly</code>，这个主题也是通过集成busuanzi计数器来实现网页计数。</p><p>然而，随着使用人数的不断增多，这个由作者为爱发电的免费服务开始出现稳定性问题，访问速度也变得起伏不定。</p><p>因此，我决定自己开发一个服务来代替busuanzi，为我的网站提供稳定的计数服务。</p><h1>技术栈选择</h1><p>我简单分析了一下，发现busuanzi这个网页计数服务实现起来并不复杂，主要是通过获取访问请求的来源地址来实现PV和UV的计数增长，因此服务的核心就是一个统计接口。</p><p>基于这个分析，技术栈的选择也相对明确：</p><ol><li><p><strong>接口开发</strong>：由于我最熟悉Java，所以选择使用SpringBoot3和Java17来实现这个接口。（只实现一个接口的话，用SpringBoot感觉有些重，后续有时间可以考虑用Go重写一遍）</p></li><li><p><strong>数据存储</strong>：数据库选择很多，但考虑到服务需要记录的数据主要是网站的PV和UV，对于请求IP等信息并非必须存储，所以我选择使用Redis。Redis对于这类频繁操作的数据处理速度非常快，而且配置好Redis的持久化后，也不怎么需要担心数据丢失问题。如果还是不太放心，可以在增加一个定时任务，定时从Redis中读取数据，存储到关系型数据库当中去，但是我感觉没有必要。</p></li></ol><h1>服务分析</h1><h2>前端脚本</h2><p>为了避免修改<code>Butterfly</code>主题中集成busuanzi的代码（以免在后续主题升级时进行频繁修改），需要让自建服务的接口访问方式和数据显示方式与原版busuanzi完全一致。因此，首先需要分析当前busuanzi的具体工作流程。</p><p>访问官网，可以看到官网提示的信息，只需要两行代码即可搞定计数。</p><pre><code class="language-js">&lt;script async src=&quot;//busuanzi.ibruce.info/busuanzi/2.3/busuanzi.pure.mini.js&quot;&gt;&lt;/script&gt;&lt;span id=&quot;busuanzi_container_site_pv&quot;&gt;本站总访问量&lt;span id=&quot;busuanzi_value_site_pv&quot;&gt;&lt;/span&gt;次&lt;/span&gt;</code></pre><p>从这两行代码可以看出，busuanzi通过引入<code>busuanzi.pure.mini.js</code>脚本来修改<code>busuanzi_container_site_pv</code>标签的内容，从而实现计数显示。数据获取流程的核心就在这个脚本中。由于该脚本经过了代码压缩，我使用<code>claude code</code>对其进行了脚本还原，以提高代码可读性。</p><pre><code class="language-js">/** * Busuanzi 不蒜子统计脚本 * 用于网站PV/UV统计显示 */var bszCaller, bszTag;// DOM Ready 功能实现(function() {    var interval,         domReadyCallback,         executeCallbacks,        isReady = false,         readyCallbacks = [];    // 添加ready回调函数    ready = function(callback) {        if (isReady || document.readyState === &quot;interactive&quot; || document.readyState === &quot;complete&quot;) {            return callback.call(document);        } else {            readyCallbacks.push(function() {                return callback.call(this);            });        }        return this;    };    // 执行所有等待的回调函数    executeCallbacks = function() {        for (var i = 0, length = readyCallbacks.length; i &lt; length; i++) {            readyCallbacks[i].apply(document);        }        readyCallbacks = [];    };    // DOM准备完成后的处理    domReadyCallback = function() {        if (!isReady) {            isReady = true;            executeCallbacks.call(window);                        // 移除事件监听器            if (document.removeEventListener) {                document.removeEventListener(&quot;DOMContentLoaded&quot;, domReadyCallback, false);            } else if (document.attachEvent) {                document.detachEvent(&quot;onreadystatechange&quot;, domReadyCallback);                if (window == window.top) {                    clearInterval(interval);                    interval = null;                }            }        }    };    // 绑定DOM Ready事件    if (document.addEventListener) {        // 现代浏览器        document.addEventListener(&quot;DOMContentLoaded&quot;, domReadyCallback, false);    } else if (document.attachEvent) {        // IE浏览器兼容        document.attachEvent(&quot;onreadystatechange&quot;, function() {            if (/loaded|complete/.test(document.readyState)) {                domReadyCallback();            }        });        // IE浏览器特殊处理        if (window == window.top) {            interval = setInterval(function() {                try {                    if (!isReady) {                        document.documentElement.doScroll(&quot;left&quot;);                    }                } catch (error) {                    return;                }                domReadyCallback();            }, 5);        }    }})();// JSONP 调用器对象bszCaller = {    /**     * 发起JSONP请求获取统计数据     * @param {string} url - 请求URL     * @param {function} callback - 回调函数     */    fetch: function(url, callback) {        // 生成随机回调函数名        var callbackName = &quot;BusuanziCallback_&quot; + Math.floor(Math.random() * 1099511627776);                // 在全局作用域中注册回调函数        window[callbackName] = this.evalCall(callback);                // 替换URL中的回调函数名        url = url.replace(&quot;=BusuanziCallback&quot;, &quot;=&quot; + callbackName);                // 创建script标签进行JSONP请求        var scriptTag = document.createElement(&quot;SCRIPT&quot;);        scriptTag.type = &quot;text/javascript&quot;;        scriptTag.defer = true;        scriptTag.src = url;        scriptTag.referrerPolicy = &quot;no-referrer-when-downgrade&quot;;                // 将script标签添加到head中        document.getElementsByTagName(&quot;HEAD&quot;)[0].appendChild(scriptTag);    },    /**     * 包装回调函数，添加错误处理     * @param {function} callback - 原始回调函数     * @returns {function} 包装后的回调函数     */    evalCall: function(callback) {        return function(data) {            ready(function() {                try {                    // 执行回调并清理script标签                    callback(data);                    if (scriptTag &amp;&amp; scriptTag.parentElement) {                        scriptTag.parentElement.removeChild(scriptTag);                    }                } catch (error) {                    // 出错时隐藏统计显示                    bszTag.hides();                }            });        };    }};// 发起统计数据请求bszCaller.fetch(&quot;//busuanzi.ibruce.info/busuanzi?jsonpCallback=BusuanziCallback&quot;, function(data) {    // 更新页面统计数据并显示    bszTag.texts(data);    bszTag.shows();});// 页面标签操作对象bszTag = {    // 支持的统计类型    bszs: [&quot;site_pv&quot;, &quot;page_pv&quot;, &quot;site_uv&quot;],    /**     * 更新页面中的统计数字     * @param {object} data - 包含统计数据的对象     */    texts: function(data) {        this.bszs.map(function(type) {            var element = document.getElementById(&quot;busuanzi_value_&quot; + type);            if (element) {                element.innerHTML = data[type];            }        });    },    /**     * 隐藏所有统计容器     */    hides: function() {        this.bszs.map(function(type) {            var container = document.getElementById(&quot;busuanzi_container_&quot; + type);            if (container) {                container.style.display = &quot;none&quot;;            }        });    },    /**     * 显示所有统计容器     */    shows: function() {        this.bszs.map(function(type) {            var container = document.getElementById(&quot;busuanzi_container_&quot; + type);            if (container) {                container.style.display = &quot;inline&quot;;            }        });    }};</code></pre><p>虽然对JavaScript不是很熟悉，但这个脚本逻辑相对清晰，不难读懂。以下是该脚本的核心工作流程：</p><ol><li>等待浏览器DOM准备</li><li>DOM准备完成后，发起JSONP请求</li><li>解析请求返回的标签数据，如果请求错误，数据异常就隐藏所有统计容器</li><li>获取到计数的数据后，更新携带有ID为<code>busuanzi_value_</code>或<code>busuanzi_container_</code>统计标签的数据，然后修改容器的<code>display</code>属性，从而实现容器和内容的显示</li></ol><blockquote><p>JSONP其实就是GET请求，主要是利用<code>&lt;script&gt;</code>标签可以跨域加载资源的特性，将数据请求伪装成脚本加载，服务器返回的也不是纯数据，而是一个函数调用，从而绕过浏览器的同源策略限制。</p></blockquote><p>前端操作通过这个简单的脚本即可实现，接下来分析后端接口的实现方式。</p><h2>后端接口</h2><p>网页计数服务的核心功能是记录URL的UV和PV数据。这些指标又分为站点级别和页面级别，理论上应该有四个数据（但busuanzi不计算页面UV，因此只有三个），这里按四个指标数据计算。下面逐一分析这些数据的计算方法。</p><h3>PV</h3><p>PV，即浏览量，根据定义也不难知道计算的方法。当前端访问接口的时候，站点浏览量进行加1，页面浏览量进行加1既可以获得这两个PV数据量。</p><p>那么后端接口如何获知是哪个页面发起的请求呢？回头查看busuanzi脚本并未发现页面的相关信息，因此需要观察busuanzi脚本发送的具体HTTP请求信息。</p><pre><code>curl ^&quot;https://busuanzi.ibruce.info/busuanzi?jsonpCallback=BusuanziCallback_12592539439^&quot; ^  -H ^&quot;accept: */*^&quot; ^  -H ^&quot;accept-language: zh-CN,zh;q=0.9^&quot; ^  -H ^&quot;referer: https://yww52.com/^&quot; ^  -H ^&quot;sec-ch-ua: ^\^&quot;Not;A=Brand^\^&quot;;v=^\^&quot;99^\^&quot;, ^\^&quot;Google Chrome^\^&quot;;v=^\^&quot;139^\^&quot;, ^\^&quot;Chromium^\^&quot;;v=^\^&quot;139^\^&quot;^&quot; ^  -H ^&quot;sec-ch-ua-mobile: ?0^&quot; ^  -H ^&quot;sec-ch-ua-platform: ^\^&quot;Windows^\^&quot;^&quot; ^  -H ^&quot;sec-fetch-dest: script^&quot; ^  -H ^&quot;sec-fetch-mode: no-cors^&quot; ^  -H ^&quot;sec-fetch-site: cross-site^&quot; ^  -H ^&quot;sec-fetch-storage-access: active^&quot; ^  -H ^&quot;user-agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/139.0.0.0 Safari/537.36^&quot;</code></pre><p>经过仔细观察，容易发现请求头中的<code>referer</code>字段记录了当前页面的信息。切换到不同页面时，该字段也会相应变为相应页面的URL。基于这个信息，就可以计算出所需的两个PV数据：</p><ol><li><strong>site_pv</strong>（站点浏览量）：从<code>referer</code>请求头中提取站点Host地址，在Redis中存储一个键值对，并对该值进行递增操作</li><li><strong>page_pv</strong>（页面浏览量）：直接使用<code>referer</code>请求头信息作为键，在Redis中存储键值对，并对该值进行递增操作</li></ol><h3>UV</h3><p>UV（独立访客数）的统计重点在于两个关键概念：<code>每日</code>和<code>独立访客</code>。</p><p>对于每日统计，除了存储UV值外，还需要维护一个当日访客集合。当新请求到达时，通过查询该集合可以判断当前访客今日是否已经访问过。若已访问，则UV数据保持不变；若为首次访问，则UV数据递增。</p><p>独立访客的识别相对复杂，因为Hexo博客是纯静态页面，难以直接区分不同访客。前面的请求脚本中也未发现访客标识相关数据，于是我推测访客信息应该记录在HTTP请求中。为了对标busuanzi的实现机制，需要进一步分析具体请求。最好重新打开浏览器或清理缓存，以观察UV增长时的请求响应信息。</p><pre><code>content-length 109content-type application/jsondate Sun, 10 Aug 2025 07:55:37 GMTserver nginx/1.14.1set-cookie busuanziId=8CBB7C5BBCF54C86B90DAD230C7B5275; Path=/; httponly; secure; SameSite=None; Domain=busuanzi.ibruce.info; Secure</code></pre><p>从响应请求头可以看出，busuanzi采用了浏览器Cookie机制来识别独立访客。服务端在响应中设置Cookie，浏览器会在后续请求中自动携带该Cookie。通过检测请求中是否包含Cookie，服务端可以判断是否为首次访问。基于这个机制，可以得出UV数据的计算方法：</p><ol><li><strong>site_uv</strong>（站点每日独立访客数）：从<code>referer</code>请求头获取站点Host地址。若请求不包含Cookie，则生成新Cookie并添加至当日站点访客集合，同时返回Cookie并将UV数据递增。若请求已携带Cookie，则不进行任何增长操作</li><li><strong>page_uv</strong>（页面每日独立访客数）：使用<code>referer</code>请求头信息作为页面标识。若请求不包含Cookie，则生成新Cookie并添加至当日页面访客集合，同时返回Cookie并将UV数据递增。若请求已携带Cookie，则不进行任何增长操作</li></ol><h1>服务实现</h1><h2>接口实现</h2><p>这里我先展示所有使用到的Redis的键和类型，方便后续查找修改。</p><p><code>host</code>表示站点地址，<code>url</code>表示资源地址，<code>date</code>表示日期。</p><table><thead><tr><th align="center">数据名称</th><th align="center">Redis键名</th><th align="center">Redis类型</th></tr></thead><tbody><tr><td align="center">site_pv（站点浏览量）</td><td align="center">pv:{host}</td><td align="center">string</td></tr><tr><td align="center">page_pv（页面浏览量）</td><td align="center">pv:page:{url}</td><td align="center">string</td></tr><tr><td align="center">site_uv（站点每日独立访客数）</td><td align="center">uv:{host}</td><td align="center">string</td></tr><tr><td align="center">每日站点访客数（缓存7天）</td><td align="center">uvtime:{date}:{host}</td><td align="center">set</td></tr><tr><td align="center">page_uv（页面每日独立访客数）</td><td align="center">uv:page:{url}</td><td align="center">string</td></tr><tr><td align="center">每日页面访客数（缓存7天）</td><td align="center">uvtime:page:{date}:{url}</td><td align="center">set</td></tr></tbody></table><blockquote><p>对于Redis数据结构的选择，个人认为使用string和set这两种数据结构完全足够了，大部分博客不是什么高流量的网站，string结构的数据很好用而且也很准确，没有多大必要使用hyperloglog或者zset，特别是数据量少的情况。</p></blockquote><p>关于具体的实现，服务分析当中已经写的很明白了，这个服务其实就是一个核心的请求接口，逻辑和代码我就不一一贴出来了，我已经上传到Github`上面了，感兴趣可以自行研究，附带一下仓库地址。</p><p><a href="https://github.com/whyneh/busuanzi">https://github.com/whyneh/busuanzi</a></p><h2>优化部分</h2><p>服务代码除了适配原有的busuanzi的<code>JSONP</code>请求外，还自行进行了一些优化。以下是我进行优化的方向：</p><ol><li>因为后端是自己编写的服务，所以不需要前端使用JSONP的方式进行跨域处理，所以直接使用<code>fetch</code>的GET请求即可。</li><li>原版busuanzi是使用了cookie进行用户管理，我改为了使用浏览器<code>localStorage</code>进行存储管理用户信息，这样数据更加可以精准。</li></ol><p>如果需要进行修改，那么前端的请求脚本也需要修改，我根据原有的脚本进行了简单的优化。</p><pre><code>(function () {    &#39;use strict&#39;;    // 配置项    const CONFIG = {        // 统计项名称列表        STATISTICS: [&quot;site_pv&quot;, &quot;page_pv&quot;, &quot;site_uv&quot;, &quot;page_uv&quot;],        // API地址，请根据实际情况修改        API_URL: &quot;https://xxxx.com/busuanzi/api&quot;,        // localStorage中存储身份标识的key        LOCALSTORAGE_KEY: &quot;bsz-id&quot;,        // busuanzi的元素容器和元素值前缀        ITEM_VALUE_PREFIX: &quot;busuanzi_value_&quot;,        ITEM_CONTAINER_PREFIX: &quot;busuanzi_container_&quot;,        // 请求超时时间（毫秒）        REQUEST_TIMEOUT: 5000,        // 重试次数        RETRY_COUNT: 3    };    /**     * DOM Ready 检测函数     *     * @param {Function} callback - DOM准备完成后执行的回调函数     */    function domReady(callback) {        if (document.readyState === &quot;loading&quot;) {            document.addEventListener(&quot;DOMContentLoaded&quot;, callback, { once: true });        } else {            // DOM已经准备完成，立即执行            callback();        }    }    /**     * 创建带超时的fetch请求     *     * @param {string} url      请求URL     * @param {object} options  fetch选项     * @param {number} timeout  超时时间     * @returns {Promise}       fetch Promise     */    function fetchWithTimeout(url, options, timeout) {        return Promise.race([            fetch(url, options),            new Promise((_, reject) =&gt;                setTimeout(() =&gt; reject(new Error(&#39;Request timeout&#39;)), timeout)            )        ]);    }    /**     * 保存用户身份标识     * @param {string} userId - 用户身份标识     */    function saveUserId(userId) {        try {            if (userId &amp;&amp; userId !== &quot;&quot;) {                localStorage.setItem(CONFIG.LOCALSTORAGE_KEY, userId);            }        } catch (error) {            console.warn(&#39;无法保存到localStorage:&#39;, error);        }    }    /**     * 获取或生成用户身份标识     * @returns {string|null} 用户身份标识     */    function getUserId() {        try {            return localStorage.getItem(CONFIG.LOCALSTORAGE_KEY);        } catch (error) {            console.warn(&#39;无法访问localStorage:&#39;, error);            return null;        }    }    /**     * 隐藏所有统计容器（出错时调用）     */    function hideStatistics() {        CONFIG.STATISTICS.forEach(item =&gt; {            try {                const containerElement = document.getElementById(CONFIG.ITEM_CONTAINER_PREFIX + item);                if (containerElement) {                    containerElement.style.display = &quot;none&quot;;                }            } catch (error) {                console.warn(`隐藏统计项 ${item} 时出错:`, error);            }        });    }    /**     * 发送统计请求     *     * @param {number} retryCount - 剩余重试次数     */    function fetchStatistics(retryCount = CONFIG.RETRY_COUNT) {        // 构建请求头        const headers = new Headers();        headers.append(&quot;X-Referer&quot;, window.location.href);        const userId = getUserId();        if (userId) {            headers.append(&quot;Authorization&quot;, userId);        }        // 发送超时请求        fetchWithTimeout(CONFIG.API_URL, {            method: &quot;GET&quot;,            headers: headers        }, CONFIG.REQUEST_TIMEOUT)            // 请求成功处理            .then(response =&gt; {                if (!response.ok) {                    throw new Error(`HTTP error! status: ${response.status}`);                }                // 保存响应头中的身份标识                const bszId = response.headers.get(&quot;Authorization&quot;);                if (bszId) {                    saveUserId(bszId);                }                return response.json();            })            // 成功数据处理            .then(data =&gt; {                // 遍历每项统计数据，然后进行更改                CONFIG.STATISTICS.forEach(item =&gt; {                    try {                        // 更新统计数值                        const valueElement = document.getElementById(CONFIG.ITEM_VALUE_PREFIX + item);                        if (valueElement) {                            valueElement.innerHTML = data[item] || 0;                        }                        // 显示统计容器                        const containerElement = document.getElementById(CONFIG.ITEM_CONTAINER_PREFIX + item);                        if (containerElement) {                            containerElement.style.display = &quot;inline&quot;;                        }                    } catch (error) {                        console.warn(`更新统计项 ${item} 时出错:`, error);                    }                });            })            .catch(error =&gt; {                console.error(&#39;busuanzi统计请求失败:&#39;, error);                // 重试机制                if (retryCount &gt; 0) {                    console.log(`正在重试... 剩余重试次数: ${retryCount}`);                    setTimeout(() =&gt; {                        fetchStatistics(retryCount - 1);                    }, 1000); // 1秒后重试                } else {                    console.error(&#39;所有重试都失败，隐藏统计显示&#39;);                    hideStatistics();                }            });    }    // 等待DOM准备完成后初始化    domReady(fetchStatistics);})();</code></pre><h1>服务部署</h1><p>这次涉及的服务有两个，一个是<code>SpringBoot</code>的Java服务，一个是<code>Redis</code>服务，如果是要部署的话，就需要分开部署。</p><p>这里我使用了<code>docker-compose</code>来进行部署，也可以更加方便，以下是三个所需的配置文件。</p><ol><li><p>服务的Dockerfile</p><pre><code class="language-dockerfile">FROM eclipse-temurin:17-jre-alpineLABEL maintainer=&quot;yww@yww52.com&quot;# 设置时区ENV TZ=Asia/ShanghaiRUN apk add tzdata \    &amp;&amp; ln -snf /usr/share/zoneinfo/$TZ /etc/localtime \    &amp;&amp; echo $TZ &gt; /etc/timezone \    &amp;&amp; apk del tzdata \    &amp;&amp; rm -rf /var/cache/apk/*# 定义应用相关环境变量ENV JAVA_OPTS=&quot;-Xms512m -Xmx2048m -Xmn614m -Xss256k -XX:SurvivorRatio=6 -XX:+UseG1GC -XX:MetaspaceSize=256m -XX:MaxMetaspaceSize=512m&quot;ENV APP_HOME=/app# 创建非root用户并设置权限RUN adduser -D appuser &amp;&amp; \    mkdir -p $APP_HOME &amp;&amp; \    chown -R appuser:appuser $APP_HOMEUSER appuser# 设置工作目录和应用目录WORKDIR $APP_HOME# 复制已打包的 JAR 文件COPY busuanzi-1.0.jar $APP_HOME/app.jar# 暴露应用端口EXPOSE 10010# 启动命令（使用环境变量配置 JVM 参数）ENTRYPOINT [&quot;sh&quot;, &quot;-c&quot;, &quot;java $JAVA_OPTS -jar app.jar&quot;]</code></pre></li><li><p>redis的配置文件，这里的路径是容器内的路径，尽量不要修改</p></li></ol><pre><code class="language-redis.conf">dir /data/redisbind 0.0.0.0port 6379# 日志文件存放logfile &quot;/data/log/redis.log&quot;# 启用AOF持久化appendonly yesappendfilename &quot;appendonly.aof&quot;appendfsync everysec# RDB持久化配置（可选）save 900 1save 300 10save 60 10000# 日志级别loglevel notice</code></pre><ol start="3"><li><p>服务编排文件<code>docker-compose.yml</code></p><pre><code class="language-yaml">version: &#39;3.8&#39;services:  # 这个名字修改了，下面依赖服务名也要修改，springboot配置文件Redis连接服务名也要修改  busuanzi-redis:    image: redis:7-alpine    container_name: busuanzi-redis    # 可自行修改宿主机端口，同一个网络服务是联通的，不映射端口直接删除没有影响    ports:      - &quot;16379:6379&quot;    volumes:      # 持久化数据目录映射到宿主机      - /data/docker/busuanzi/data:/data/redis      # 日志映射到宿主机      - /data/docker/busuanzi/log:/data/log      # Redis配置文件映射到宿主机      - /data/docker/busuanzi/redis.conf:/usr/local/etc/redis/redis.conf    command: redis-server /usr/local/etc/redis/redis.conf --appendonly yes    restart: unless-stopped    networks:      - busuanzi-network  busuanzi:    build:       context: .      dockerfile: Dockerfile    container_name: busuanzi    ports:      - &quot;10010:10010&quot;    depends_on:      - busuanzi-redis    restart: unless-stopped    networks:      - busuanzi-networknetworks:  busuanzi-network:    driver: bridge</code></pre></li></ol><p>将上述三个配置文件和springboot打包生成的jar包放在一个文件夹中，然后执行<code>docker-compose up -d</code>即可运行整个编排服务。</p><p>服务部署好后，自行使用nginx进行反向代理，推荐使用<a href="https://www.lxware.cn/">1panel</a>,里面的网站部署使用了<code>OpenResty</code>，直接创建一个反向代理即可。如果安装了<code>1panel</code>，docker相关环境基本全部自动安装好，无需自己手动在安装环境，可以一步到位。</p><h1>busuanzi数据迁移</h1><p>不太想放弃原有的busuanzi数据，可以直接将原有的busuanzi数据迁移到服务部署的redis当中，我讲一下比较简单的一个流程。</p><ol><li>获取当前站点的sitemap</li><li>然后根据sitemap获取到站点所有的页面URL</li><li>然后根据每个页面的URL去请求原有的busuanzi接口</li><li>将得到的数据设置到服务的Redis当中即可</li></ol>]]></content:encoded>
            <category domain="https://yww52.com/categories/%25E6%2588%2591%25E7%259A%2584%25E6%258A%2580%25E6%259C%25AF%25E5%25AD%25A6%25E4%25B9%25A0%25E7%2589%25A9%25E8%25AF%25AD%25E6%259E%259C%25E7%2584%25B6%25E6%259C%2589%25E9%2597%25AE%25E9%25A2%2598/">我的技术学习物语果然有问题</category>
        </item>
        <item>
            <title>Java图片处理</title>
            <link>https://yww52.com/posts/91f4854c/</link>
            <guid isPermaLink="true">https://yww52.com/posts/91f4854c/</guid>
            <pubDate>Tue, 18 Apr 2023 00:00:00 +0000</pubDate>
            <description>最近经常接触Java的图片处理，所以特地来记录一下。</description>
            <content:encoded><![CDATA[<img src="https://img.yww52.com/2023/4/2023-4-18top_img.jpg" alt="Java图片处理" style="max-width:100%;height:auto;margin-bottom:15px;" /><h1>开发环境</h1><p>本次处理图片主要使用的依赖是<code>opencv</code>，具体的方法可以去参照官网。</p><ol><li><p>首先去<a href="https://opencv.org/releases/">opencv官网</a>去下载与系统对应的jar包和依赖文件。（这里以windows举例）</p></li><li><p>选择windows版本<code>opencv-4.7.0-windows.exe</code>，然后进行安装。(linux的版本需要先进行编译后获取动态库和jar包)</p></li><li><p>在安装目录的<code>opencv/build/java</code>的目录下，获得<code>jar</code>包和动态库文件。（注意是安装目录里）</p></li><li><p>在maven项目引入<code>opencv</code>依赖。</p><pre><code class="language-xml">        &lt;!-- OpenCv  --&gt;        &lt;dependency&gt;            &lt;groupId&gt;org.opencv&lt;/groupId&gt;            &lt;artifactId&gt;opencv&lt;/artifactId&gt;            &lt;version&gt;4.7.0&lt;/version&gt;            &lt;scope&gt;system&lt;/scope&gt;            &lt;systemPath&gt;${basedir}/src/main/resources/lib/opencv-470.jar&lt;/systemPath&gt;        &lt;/dependency&gt;</code></pre></li><li><p>加载<code>opencv</code>动态库</p><p>由于opencv的开发语言问题，所以Java在使用opencv相关类的时候，需要先加载对应的动态库才行，不然直接使用会出现异常。需要注意linux和windows的动态库是不一样的。</p></li></ol><blockquote><p>好像最新版本的opencv（4.7.0）不支持Java8，只能有Java11，如果用Java8的话，可以使用4.6.0的版本。</p><p>如果觉得opencv配置环境过于麻烦，可以使用Javacv，Javacv其实就是调用opencv进行操作的，虽然官方缺少文档（好像速度也慢一些，封装过的也正常），不过和opencv的调用方法都差不多。</p></blockquote><h1>linux的编译</h1><p>上述的动态库<code>dll</code>格式是windows上使用的，要想在对应的linux上使用，还得在linux上编译opencv的源码，获得对应的<code>jar包</code>和<code>so动态库</code>文件（jar包应该是不分系统的）。</p><h1>加载动态库</h1><p>因为Java调用的<code>opencv</code>都是原生<code>native</code>方法，所以使用opencv的方法之前，需要加载动态库。</p><pre><code class="language-Java">    /**     * 加载opencv动态库     * win: opencv_java470.dll     * linux: opencv_java470.so     */    public static void load() {        System.load(ResourceUtil.getResource(&quot;lib/opencv_java470.dll&quot;).getPath());    }</code></pre><p>每次都加载太过于麻烦，如果使用springboot服务的话，可以在启动服务时初始化导入。</p><pre><code class="language-Java">@Configurationpublic class InitConfig {    @PostConstruct    public void initDll() {        OpencvUtil.load();    }}</code></pre><h1>opencv基础操作</h1><h2>读入图片</h2><pre><code class="language-Java">public class Test {    public static void main(String[] args) {        OpencvUtil.load();        String filePath = &quot;C:/Users/11419/Desktop/project/image-process/src/main/resources/test/1.jpg&quot;;        // Mat是opencv表示一张图片的基础类        Mat mat = Imgcodecs.imread(filePath);        // Mat其实就是一个像素矩阵        System.out.println(mat.rows());        System.out.println(mat.cols());        for (int i = 0, rows = mat.rows(); i &lt; rows; i++) {            for (int j = 0, cols = mat.cols(); j &lt; cols; j++) {                double[] data = mat.get(i, j);                System.out.println(Arrays.toString(data));            }        }        // 释放内存        mat.release();    }}</code></pre><h2>写出图片</h2><pre><code class="language-Java">public class Test {    public static void main(String[] args) {        OpencvUtil.load();        String filePath = &quot;C:/Users/yww/Desktop/project/image-process/src/main/resources/test/1.jpg&quot;;        String output = &quot;C:/Users/yww/Desktop/project/image-process/src/main/resources/test/2.jpg&quot;;        Mat mat = Imgcodecs.imread(filePath);        // 写出图片        Imgcodecs.imwrite(output, mat);        // 释放内存        mat.release();    }}</code></pre><blockquote><p><code>Imgcodecs.imread()</code>这个方法，偶然碰见过一次保存失败的情况，成功运行却没有保存文件，所以还可以通过字节方式保存。</p></blockquote><pre><code class="language-java">    /**     * 保存图片到指定位置     *     * @param mat       图片矩阵     * @param filePath  文件路径     */    public static void saveImage(Mat mat, String filePath) {        saveImage(mat, filePath, &quot;png&quot;);    }        /**     * 保存图片到指定位置     *     * @param mat       图片矩阵     * @param filePath  文件路径     * @param ext       保存文件后缀     */    public static void saveImage(Mat mat, String filePath, String ext) {        MatOfByte matOfByte = new MatOfByte();        Imgcodecs.imencode(&quot;.&quot; + ext, mat, matOfByte);        byte[] byteArray = matOfByte.toArray();        FileUtil.writeBytes(byteArray, filePath);    }</code></pre><h2>释放图片</h2><p>在Java中使用opencv的话，这个操作必不可少。在opencv的库中，几乎都是<code>native</code>方法，所以导致对象的内存管理大多都不是由Java虚拟机管理，需要手动进行释放内存，不然会出现服务的使用内存不断升高，直至内存不够崩溃。而且期间不会出现内存溢出，dump文件也找不到占用很多内存的Mat类。所以释放图片内存是需要注意的。</p><pre><code class="language-Java">public class Test {    public static void main(String[] args) {        OpencvUtil.load();        String filePath = &quot;C:/Users/yww/Desktop/project/image-process/src/main/resources/test/1.jpg&quot;;        String output = &quot;C:/Users/yww/Desktop/project/image-process/src/main/resources/test/2.jpg&quot;;        Mat mat = Imgcodecs.imread(filePath);        // 写出图片        Imgcodecs.imwrite(output, mat);        // 释放内存        mat.release();    }}</code></pre><p>如果你使用了<code>lombok</code>，可以使用注解<code>@Cleanup</code>来进行释放内存的简化。</p><pre><code class="language-Java">public class Test {    public static void main(String[] args) {        OpencvUtil.load();        String filePath = &quot;C:/Users/yww/Desktop/project/image-process/src/main/resources/test/1.jpg&quot;;        String output = &quot;C:/Users/yww/Desktop/project/image-process/src/main/resources/test/2.jpg&quot;;        @Cleanup(value = &quot;release&quot;)        Mat mat = Imgcodecs.imread(filePath);        // 写出图片        Imgcodecs.imwrite(output, mat);    }}</code></pre><h1>获取图片基本信息</h1><p>获取图片的基本信息，使用opencv并不方便，所以可以使用其他的库来进行。</p><h2>获取图片DPI</h2><p>暂时有两种方法可以获取图片的DPI。</p><ol><li><p>commons-imaging依赖</p><pre><code class="language-Java">    /**     * 获取图片的DPI，获取不到返回-1     * 依赖于commons-imaging     * &lt;dependency&gt;     *   &lt;groupId&gt;org.apache.commons&lt;/groupId&gt;     *   &lt;artifactId&gt;commons-imaging&lt;/artifactId&gt;     *   &lt;version&gt;${commons-imaging.version}&lt;/version&gt;     * &lt;/dependency&gt;     *     * @param filePath  图片文件位置     * @return          图片DPI     */    public static int getDpi1(String filePath) {        ImageInfo imageInfo = null;        try {            imageInfo = Imaging.getImageInfo(FileUtil.file(filePath));        } catch (ImageReadException | IOException e) {            throw new RuntimeException(&quot;获取图片信息出错！&quot;);        }        // 水平分辨率和垂直都一样        if (null == imageInfo) {            return -1;        } else {            return imageInfo.getPhysicalWidthDpi();        }    }</code></pre></li><li><p>metadata-extractor依赖</p><pre><code class="language-Java">    /**     * 获取图片的DPI，获取不到返回-1     * 依赖于metadata-extractor     * &lt;dependency&gt;     *   &lt;groupId&gt;com.drewnoakes&lt;/groupId&gt;     *   &lt;artifactId&gt;metadata-extractor&lt;/artifactId&gt;     *   &lt;version&gt;${metadata.version}&lt;/version&gt;     * &lt;/dependency&gt;     *     * @param filePath  图片文件位置     * @return          图片DPI     */    public static int getDpi2(String filePath) {        Metadata metadata = null;        int res = -1;        try {            metadata = ImageMetadataReader.readMetadata(FileUtil.file(filePath));            for (Directory directory : metadata.getDirectories()) {                // 遍历图片信息，寻找水平分辨率                for (Tag tag : directory.getTags()) {                    if (&quot;X Resolution&quot;.equals(tag.getTagName())) {                        res = Convert.toInt(tag.getDescription());                    }                }            }        } catch (ImageProcessingException | IOException e) {            throw new RuntimeException(&quot;获取图片信息出错！&quot;);        }        return res;    }</code></pre></li></ol><h2>获取图片分辨率</h2><pre><code class="language-Java">    /**     * 获取图片的分辨率     *     * @param filePath  图片文件位置     * @return          [width, height] 水平分辨率 x 垂直分辨率     */    public static int[] getResolution(String filePath) {        @Cleanup(value = &quot;release&quot;)        Mat mat = Imgcodecs.imread(filePath);        int widthResolution = mat.width();        int heightResolution = mat.height();        return new int[]{widthResolution, heightResolution};    }</code></pre><blockquote><p>这些操作实质是获取图片的基本信息得到的，所以图片大部分的基本信息都可以通过这些方法修改得到。</p></blockquote><h2>图片旋转</h2><h3>Graphics2D旋转</h3><pre><code class="language-Java">    /**     * 使用Graphics2D进行旋转图片     *     * @param src       输入路径     * @param dst       输出路径     * @param degree    旋转角度     */    public static void rotateImage(String src, String dst, double degree) {        // 读取图片        BufferedImage image = ImgUtil.read(src);        // 获取图片宽，高，类型        int width = image.getWidth();        int height = image.getHeight();        int type = image.getType();        // 创建Graphics2D        BufferedImage res = new BufferedImage(width, height, type);        Graphics2D graphics  = res.createGraphics();        // 设置图形渲染选项，指定双线性插值算法作为图像缩放时的默认算法        graphics.setRenderingHint(RenderingHints.KEY_INTERPOLATION, RenderingHints.VALUE_INTERPOLATION_BILINEAR);        // 设置图片底色        graphics.setBackground(Color.WHITE);        // 填充底图        graphics.fillRect(0, 0, width, height);        // 按中心点旋转图片        graphics.rotate(Math.toRadians(degree), width &gt;&gt; 1, height &gt;&gt; 1);        graphics.drawImage(image, 0, 0, null);        // 关闭Graphics2D        graphics.dispose();        // 写出图片        ImgUtil.write(res, FileUtil.file(dst));    }</code></pre><h3>opencv旋转</h3><pre><code class="language-Java">    /**     * 图片旋转     *     * @param image 输入图片     * @param angle 旋转角度     * @return      输出图片     */    public static Mat rotate(Mat image, double angle) {        int w = image.cols();        int h = image.rows();&gt; 这种方法自定义程度很高，但是消耗的性能也会增加，暂无需求的话推荐使用opencv的方法。### opencv方法```Java    /**     * 图片旋转     *     * @param image 输入图片     * @param angle 旋转角度     * @return      输出图片     */    public static Mat rotate(Mat image, double angle) {        int w = image.cols();        int h = image.rows();        // 定义参数        double scale = 1.0;        Point center = new Point((double) w / 2, (double) h / 2);        // 定义旋转矩阵（需要注意的是此处正角度是逆时针旋转，所以取反）        @Cleanup(value = &quot;release&quot;)        Mat rotationMatrix = Imgproc.getRotationMatrix2D(center, -angle, scale);        // 旋转矩阵        Mat rotatedImage = new Mat();        Scalar backgroundColor = new Scalar(255, 255, 255);        Imgproc.warpAffine(image, rotatedImage, rotationMatrix, new Size(w, h), Imgproc.INTER_LINEAR, Core.BORDER_CONSTANT, backgroundColor);        return rotatedImage;    }</code></pre><pre><code>    if (angle &lt; 0) {        angle = 360 + angle;    }    Point center = new Point((double) w / 2, (double) h / 2);    double scale = 1.0;    Mat rotationMatrix = Imgproc.getRotationMatrix2D(center, angle, scale);    // 设置填充颜色为白色    Scalar backgroundColor = new Scalar(255, 255, 255);    Mat rotatedImage = new Mat();    Imgproc.warpAffine(image, rotatedImage, rotationMatrix, new Size(w, h), Imgproc.INTER_LINEAR, Core.BORDER_CONSTANT, backgroundColor);    rotationMatrix.release();    return rotatedImage;}</code></pre><pre><code>## 纠正图片旋转有些时候，电脑会自动对一些图片进行旋转显示（源文件不变），比如手机拍的一些图片，竖着拍的，电脑显示是竖着的，但是源文件其实是横着的，进行图片旋转，其实是按横着的源文件图片进行旋转，所以有时候会感觉旋转角度不对，所以需要先纠正图片旋转。```Java    /**     * 纠正图片旋转     * 有些图片手机拍摄，在电脑上看是竖着的，其实是电脑自动进行的转换     * 如果旋转这种图片，就会变成原来初始的效果进行旋转，和视图的旋转不一样     * 所以需要先进行纠正图片的旋转     *     * @param srcImgPath    图片路径     */    public static void correctImg(String srcImgPath) {        FileOutputStream fos = null;        try {            // 原始图片            File srcFile = new File(srcImgPath);            // 获取偏转角度            int angle = getAngle(srcFile);            if (angle != 90 &amp;&amp; angle != 270) {                return;            }            // 原始图片缓存            BufferedImage srcImg = ImageIO.read(srcFile);            // 宽高互换            // 原始宽度            int imgWidth = srcImg.getHeight();            // 原始高度            int imgHeight = srcImg.getWidth();            // 中心点位置            double centerWidth = ((double) imgWidth) / 2;            double centerHeight = ((double) imgHeight) / 2;            // 图片缓存            BufferedImage targetImg = new BufferedImage(imgWidth, imgHeight, BufferedImage.TYPE_INT_RGB);            // 旋转对应角度            Graphics2D graphics = targetImg.createGraphics();            graphics.rotate(Math.toRadians(angle), centerWidth, centerHeight);            graphics.drawImage(srcImg, (imgWidth - srcImg.getWidth()) / 2, (imgHeight - srcImg.getHeight()) / 2, null);            graphics.rotate(Math.toRadians(-angle), centerWidth, centerHeight);            graphics.dispose();            // 输出图片            fos = new FileOutputStream(srcFile);            ImageIO.write(targetImg, &quot;jpg&quot;, fos);        } catch (Exception e) {            e.printStackTrace();        } finally {            if (fos != null) {                try {                    fos.flush();                    fos.close();                } catch (Exception e) {                    e.printStackTrace();                }            }        }    }    /**     * 获取图片旋转角度     *     * @param file  上传图片     * @return      图片旋转角度     */    private static int getAngle(File file) throws Exception {        Metadata metadata = ImageMetadataReader.readMetadata(file);        for (Directory directory : metadata.getDirectories()) {            for (Tag tag : directory.getTags()) {                if (&quot;Orientation&quot;.equals(tag.getTagName())) {                    String orientation = tag.getDescription();                    if (orientation.contains(&quot;90&quot;)) {                        return 90;                    } else if (orientation.contains(&quot;180&quot;)) {                        return 180;                    } else if (orientation.contains(&quot;270&quot;)) {                        return 270;                    }                }            }        }        return 0;    }</code></pre><h2>灰度化</h2><p>大多数彩色图片的每个像素都是由红绿蓝三个通道组成的，为了更方便的处理图像，将这三个通道的值按一定比例进行加权平均，得到一个单通道的灰度值，用来表示该像素的颜色亮度。大多数图片处理都会先进行图片灰度化，因为只有一个通道，处理起来方便很多。opencv的灰度化操作，只需要调用方法即可。灰度化操作的话，主要是要考虑图片通道数量的问题。</p><pre><code class="language-Java">    /**     * 图片灰度化     *     * @param mat       图像矩阵     * @return          图像矩阵     */    public static Mat gray(Mat mat) {        Mat gray = new Mat();        // 获取图片的通道数，根据不同通道进行处理        int channel = mat.channels();        if (channel == 1) {            // 单通道图片无需进行灰度化操作            gray = mat.clone();        } else if (channel == 2) {            // 双通道图片，将两个通道的值相加再除以2，得到灰度值            @Cleanup(value = &quot;release&quot;)            Mat temp = new Mat();            Core.addWeighted(mat, 0.5, mat, 0.5, 0, temp);            Imgproc.cvtColor(temp, gray, Imgproc.COLOR_BGR2GRAY);        } else if (channel == 4 || channel == 3) {            // 三通道和四通道的图片，使用标准的灰度化方式，考虑Alpha分量            Imgproc.cvtColor(mat, gray, Imgproc.COLOR_BGR2GRAY);        } else {            // 其他通道，暂不支持灰度化操作            throw new UnsupportedOperationException(&quot;不支持灰度化的通道数： --&gt;&quot; + channel);        }        return gray;    }</code></pre><h2>高斯滤波</h2><p>处理图像时，往往需要对图像进行平滑操作以去除噪声，同时也可以模糊图像以达到柔化的效果。高斯滤波（Gaussian blur）就是一种常用的平滑滤波器，其通过对图像像素进行加权平均的方式来实现平滑和模糊的效果。高斯滤波的原理是使用一个高斯核（Gaussian kernel）对图像像素进行加权平均。一般来说，高斯核的大小或标准差越大，平滑效果越明显，图像的细节丢失也会越多。</p><pre><code class="language-Java">    /**     * 高斯滤波平滑     * 第三个参数为高斯核大小     * 第四个和第五个参数为标准差，设置为0，则根据核大小自动计算     *     * @param mat       图像矩阵     * @return          图像矩阵     */    public static Mat gaussianBlur(Mat mat) {        Mat blurred = mat.clone();        Imgproc.GaussianBlur(mat, blurred, new Size(3, 3), 0, 0);        return blurred;    }</code></pre><h2>图像二值化</h2><p>图像二值化很好理解，就是把一张彩色图片或者灰度图片转化为黑白二值图像，即每个像素只有两种可能的选值，通常是黑色（0）和白色（255）。主要用于分割某些图片的用途。</p><p>二值化具体步骤：</p><ol><li>灰度化。这一步是因为在大多数情况下，只考虑像素的亮度信息就足够了，灰度图像的每个像素值表示亮度的强度，通常在0（黑色）到255（白色）之间。</li><li>选择一个阈值，阈值是一个灰度值，用于判断像素应该被归类为黑色还是白色。选值方法有很多，大概有以下三种<ul><li>全局阈值：简单地选择一个固定的全局阈值，将所有像素比较与该阈值，高于阈值的像素设置为白色，低于阈值的像素设置为黑色。</li><li>局部阈值：根据像素的局部区域来选择阈值，每个像素的阈值根据其周围像素的统计特性（如均值、方差等）来确定。</li><li>自适应阈值：根据图像的不同区域自适应地选择阈值。常见的方法有基于局部均值、基于Otsu算法等。</li></ul></li><li>图片进行二值化处理，对于每个像素，如果其灰度值大于阈值，则将其设为白色（255），否则设为黑色（0）。</li></ol><pre><code class="language-Java">    /**     *  图片二值化处理，参数需要调整     *     * @param src       图片矩阵     * @return          图像矩阵     */    public static Mat threshold(Mat src) {        @Cleanup(value = &quot;release&quot;)        Mat gray = gray(src);        Mat threshold = new Mat();        Imgproc.threshold(gray, threshold, 120, 255, Imgproc.THRESH_BINARY);        return threshold;    }</code></pre><h2>图像膨胀与腐蚀</h2><p>腐蚀与膨胀主要用于处理二值化图像或者是灰度图像。</p><h3>膨胀</h3><p>膨胀主要作用是对二值化物体边界点进行扩充，将与物体接触的所有背景点合并到该物体中，使边界向外部扩张。</p><p>如果两个物体间隔较近，会将两物体连通在一起。对填补图像分割后物体的空洞有用。</p><pre><code>    /**     *  图片膨胀     *     * @param src       图片矩阵     * @return          图像矩阵     */    public static Mat erode(Mat src) {        Mat kernel = Imgproc.getStructuringElement(Imgproc.MORPH_RECT, new Size(5, 5));        @Cleanup(value = &quot;release&quot;)        Mat gray = gray(src);        Mat erode = new Mat();        Imgproc.erode(gray, erode, kernel);        return erode;    }</code></pre><h3>腐蚀</h3><p>腐蚀主要的作用就是消除物体的边界点，使边界向内收缩，可以把小于结构元素的物体去除。</p><p>可将两个有细小连通的物体分开，去除毛刺，小凸起等噪点。如果两个物体间有细小的连通，当结构足够大时，可以将两个物体分开。</p><pre><code class="language-Java">    /**     *  图片腐蚀     *     * @param src       图片矩阵     * @return          图像矩阵     */    public static Mat dilate(Mat src) {        Mat kernel = Imgproc.getStructuringElement(Imgproc.MORPH_RECT, new Size(5, 5));        @Cleanup(value = &quot;release&quot;)        Mat gray = gray(src);        Mat dilate = new Mat();        Imgproc.dilate(gray, dilate, kernel);        return dilate;    }</code></pre><h2>边缘检测</h2><p>边缘是指图像中颜色或亮度发生急剧变化的区域，通常是物体之间的边界或物体内部的纹理。边缘检测算法旨在在图像中找到这些边缘并将其标记出来。具体的步骤如下：</p><ol><li>图像灰度化</li><li>此处还可以进行（高斯模糊，二值化，图片膨胀，图片腐蚀等操作去除噪点）</li><li>进行边缘检测</li></ol><pre><code class="language-Java">    /**     * 边缘检测     *     * @param mat   图像矩阵     * @return      图像矩阵     */    public static Mat canny(Mat mat) {        return canny(mat, 60, 200, 3);    }    /**     *  边缘检测     *  检测图像中的边缘，并生成一个二值图像，其中边缘被表示为白色，背景为黑色     *  一般来说threshold2大于threshold1，保证能够检测到真正的边缘     *  threshold1一般设置为图像灰度级的20%-30%     *  threshold2一般设置为threshold1的三倍     *     * @param mat           图片矩阵     * @param threshold1    边缘的阈值，用于检测强边缘     * @param threshold2    边缘的阈值，用于检测弱边缘     * @param apertureSize  Sobel算子的大小，一般为3，5或7     * @return          图像矩阵     */    public static Mat canny(Mat mat, int threshold1, int threshold2, int apertureSize) {        // 进行高斯平滑        @Cleanup(value = &quot;release&quot;)        Mat blurred = new Mat();        Imgproc.GaussianBlur(mat, blurred, new Size(3, 3), 0, 0);        // 灰度化        @Cleanup(value = &quot;release&quot;)        Mat gray = new Mat();        Imgproc.cvtColor(blurred, gray, Imgproc.COLOR_BGR2GRAY);        // 进行边缘检测        Mat canny = new Mat();        Imgproc.Canny(gray, canny, threshold1, threshold2, apertureSize);        return canny;    }</code></pre><h1>一些图片的实际应用</h1><h2>检测图片亮度</h2><p>网上关于图片亮度检测的方法，大概有两种。</p><h3>图片平均亮度值</h3><p>使用图片的平均灰度值来代表图片的平均亮度。</p><pre><code class="language-Java">    /**     * 计算图片平均亮度     * mean 获取Mat中各个通道的均值     *     * @param filePath  图片路径     * @return          图片平均亮度值     */    public static double brightness(String filePath) {        Mat src = Imgcodecs.imread(filePath);        // 灰度化，转为灰度图        Mat gray = ImageUtil.gray(src.clone());        // 计算图像的平均亮度        Scalar mean = Core.mean(gray);        return mean.val[0];    }</code></pre><h3>图片亮度异常值</h3><pre><code class="language-Java">    /**     * 图片亮度检测     * cast为计算出的偏差值，小于1表示比较正常，大于1表示存在亮度异常；当cast异常时，da大于0表示过亮，da小于0表示过暗     * 标准可以自己定义     *     * @param   filePath    图片路径     * @return              [cast, da] [亮度值， 亮度异常值]     */    private double[] brightness2(String filePath) {        Mat src = Imgcodecs.imread(filePath);        // 灰度化，转为灰度图        Mat gray = ImageUtil.gray(src.clone());        double a = 0;        int[] hist = new int[256];        for (int i = 0; i &lt; gray.rows(); i++) {            for (int j = 0; j &lt; gray.cols(); j++) {                a += gray.get(i, j)[0] - 128;                int index = (int) gray.get(i, j)[0];                hist[index]++;            }        }        double da = a / (gray.rows() * gray.cols());        double ma = 0;        for (int i = 0; i &lt; hist.length; i++) {            ma += Math.abs(i - 128 - da) * hist[i];        }        ma = ma / (gray.rows() * gray.cols());        double cast = Math.abs(da) / Math.abs(ma);        return new double[] {cast, da};    }</code></pre><h2>检测图片的清晰度</h2><p>网上检测图片清晰度的方法有三种，需要注意的是，这些计算的指标在同一张图片里，越高代表这张图片越清晰。所以可能会出现一种情况，就是有一张模糊的图片指标比有一张清晰的指标高。</p><h3>Tenengrad梯度</h3><pre><code class="language-Java">    /**     * Tenengrad梯度方法计算清晰度     * Tenengrad梯度方法利用Sobel算子分别计算水平和垂直方向的梯度，同一场景下梯度值越高，图像越清晰。     *     * @param image  图片矩阵     * @return      图片清晰度     */    public static double tenengrad(Mat image) {        // 图片灰度化        Mat grayImage = image.clone();        Imgproc.cvtColor(image, grayImage, Imgproc.COLOR_BGR2GRAY);        // Sobel算子        Mat sobelImage = new Mat();        Imgproc.Sobel(grayImage, sobelImage, CvType.CV_16U, 1, 1);        Scalar mean = Core.mean(sobelImage);        double meanValue = mean.val[0];        // 释放内存        sobelImage.release();        grayImage.release();        return meanValue;    }</code></pre><h3>Laplacian方法计算清晰度</h3><pre><code class="language-Java">    /**     * Laplacian方法计算清晰度     *     * @param image  图片矩阵     * @return      图片清晰度     */    public static double laplacian(Mat image) {        // 图片灰度化        Mat grayImage = image.clone();        Imgproc.cvtColor(image, grayImage, Imgproc.COLOR_BGR2GRAY);        // Laplacian算子        Mat laplacian = new Mat();        Imgproc.Laplacian(grayImage, laplacian, CvType.CV_16U);        Scalar mean = Core.mean(laplacian);        double meanValue = mean.val[0];        // 释放内存        laplacian.release();        grayImage.release();        return meanValue;    }</code></pre><h3>灰度方差获取图片清晰度</h3><pre><code class="language-Java">    /**     * 通过灰度方差获取图片清晰度     * 对焦清晰的图像相比对焦模糊的图像，它的数据之间的灰度差异应该更大，即它的方差应该较大，可以通过图像灰度数据的方差来衡量图像的清晰度，方差越大，表示清晰度越好。     *     * @param image  图片矩阵     * @return      图片清晰度     */    public static double variance(Mat image) {        // 图片灰度化        Mat grayImage = image.clone();        Imgproc.cvtColor(image, grayImage, Imgproc.COLOR_BGR2GRAY);        // 计算灰度图像的标准差        MatOfDouble mean = new MatOfDouble();        MatOfDouble stdDev = new MatOfDouble();        Core.meanStdDev(grayImage, mean, stdDev);        double meanValue = stdDev.get(0, 0)[0];        // 释放内存        mean.release();        stdDev.release();        grayImage.release();        return meanValue;    }</code></pre><h2>图片亮度处理</h2><p>亮度调整主要是调整平均亮度值到一个自定义的值当中。</p><pre><code class="language-Java">    /**     * 图片亮度调整     *     * @param src   输入路径     * @param dst   输出路径     */    public void adjustBrightness(String src, String dst) {        // 读取图片        @Cleanup(value = &quot;release&quot;)        Mat mat = Imgcodecs.imread(src);        // 灰度化，转为灰度图        @Cleanup(value = &quot;release&quot;)        Mat gray = OpencvUtil.gray(mat.clone());        // 获取图片平均亮度值        Scalar mean = Core.mean(gray);        double brightness = mean.val[0];        // 图片亮度判断，需要根据具体情况进行判断        @Cleanup(value = &quot;release&quot;)        Mat adjustedImage = mat.clone();        if (brightness &lt; 100 || brightness &gt; 250) {            // 计算亮度调整值，可以根据具体情况选择参数调整            double alpha = 175 / brightness;            // 执行亮度调整            gray.convertTo(adjustedImage, -1, alpha, 0);        }        // 保存调整后的图像        Imgcodecs.imwrite(dst, adjustedImage);    }</code></pre><h2>图片纠偏</h2><p>图片纠偏是一个比较难的问题，主要的难点在于如何计算出图片的倾斜角度，最常见的方法就是霍夫变换检测直线了，以下是具体步骤。</p><ol><li>图片边缘检测</li><li>霍夫变换，获取检测的直线</li><li>计算每条直线的倾斜角度</li><li>获取所有直线倾斜角度的平均值或者是众数来作为图片的倾斜角度。（关于平均值和众数，总会在某些情况下十分异常，我认为平均数比较可靠，但需要严格筛选需要内容的直线才行。也可以根据某些算法排除掉异常值。）</li><li>图片灰度化。</li><li>高斯模糊，二值化，膨胀，腐蚀等操作处理图片，去除噪点，以免出现不需要的直线。</li><li>图片边缘检测，通过调整参数，把一些无用的边框去除。</li><li>霍夫变换检测直线，通过调整参数把一些无用的直线去除。</li><li>收集并计算每条直线的倾斜角度。</li><li>通过每条直线的倾斜角度，推测出整体图片的倾斜角度。这里有很多种方法，比如平均值，众数，出现次数最多的数，或者是附近角度最多的数等等方法。这里使用了出现次数最多的数用来代替图片整体倾斜角度。</li></ol><pre><code class="language-Java">    /**     * 图片进行纠偏     *     * @param src   图片路径     * @param dst   纠偏图片保存路径     */    public static void deskew(String src, String dst) {        @Cleanup(value = &quot;release&quot;)        Mat mat = Imgcodecs.imread(src);        // 计算图片倾斜角        double angle = getDeskewAngle(mat);        // 图片旋转        ImageUtil.rotateImage(src, dst, angle);    }    /**     *  通过霍夫变换后，获取直线并计算出整体的倾斜角度     *     * @param src       图片     * @return          倾斜角度     */    public static Integer getDeskewAngle(Mat src) {        // 图片灰度化        Mat gray = src.clone();        Imgproc.cvtColor(src, gray, Imgproc.COLOR_BGR2GRAY);                Mat kernel = Imgproc.getStructuringElement(Imgproc.MORPH_RECT, new Size(5, 5));        // 图片膨胀        Mat erode = gray.clone();        Imgproc.erode(gray, erode, kernel);        // 图片腐蚀        Mat dilate = erode.clone();        Imgproc.dilate(erode, dilate, kernel);    public static Integer getDeskewAngle(Mat src) {        // 图片灰度化        @Cleanup(value = &quot;release&quot;)        Mat gray = src.clone();        Imgproc.cvtColor(src, gray, Imgproc.COLOR_BGR2GRAY);        // 边缘检测        Mat canny = dilate.clone();        Imgproc.Canny(dilate, canny, 50, 150);        // 霍夫变换得到线条        @Cleanup(value = &quot;release&quot;)        Mat kernel = Imgproc.getStructuringElement(Imgproc.MORPH_RECT, new Size(5, 5));        // 图片膨胀        @Cleanup(value = &quot;release&quot;)        Mat erode = gray.clone();        Imgproc.erode(gray, erode, kernel);        // 图片腐蚀        @Cleanup(value = &quot;release&quot;)        Mat dilate = erode.clone();        Imgproc.dilate(erode, dilate, kernel);        // 边缘检测        @Cleanup(value = &quot;release&quot;)        Mat canny = dilate.clone();        Imgproc.Canny(dilate, canny, 50, 150);        // 霍夫变换得到线条        @Cleanup(value = &quot;release&quot;)        Mat lines = new Mat();        //累加器阈值参数，小于设置值不返回        int threshold = 90;        //最低线段长度，低于设置值则不返回        double minLineLength = 100;        //间距小于该值的线当成同一条线        double maxLineGap = 10;        // 霍夫变换，通过步长为1，角度为PI/180来搜索可能的直线        Imgproc.HoughLinesP(canny, lines, 1, Math.PI / 180, threshold, minLineLength, maxLineGap);        // 计算倾斜角度        List&lt;Integer&gt; angelList = new ArrayList&lt;&gt;();        for (int i = 0; i &lt; lines.rows(); i++) {            double[] line = lines.get(i, 0);            int k = calculateAngle(line[0], line[1], line[2], line[3]);            angelList.add(k);        }        if (angelList.isEmpty()) {            return 0;        }        gray.release();        kernel.release();        erode.release();        dilate.release();        canny.release();        lines.release();        // 可能还得需要考虑方差来决定选择平均数还是众数        return most(angelList);    }    /**     * 求数组众数     *     * @param angelList 数组     * @return          数组众数     */    private static int most(List&lt;Integer&gt; angelList) {        if (angelList.isEmpty()) {            return 0;        }        int res = 0;        int max = Integer.MIN_VALUE;        Map&lt;Integer, Integer&gt; map = new HashMap&lt;&gt;();        for (int i : angelList) {            map.put(i, map.getOrDefault(i, 0) + 1);        }        for (Integer i : map.keySet()) {            int count = map.get(i);            if (count &gt; max) {                max = count;                res = i;            }        }        return res;    }    /**     * 计算直线的倾斜角     *     * @param x1    点1的横坐标     * @param x2    点2的横坐标     * @param y1    点1的纵坐标     * @param y2    点2的纵坐标     * @return 直角的倾斜度     */    private static int calculateAngle(double x1, double y1, double x2, double y2) {        double dx = x2 - x1;        double dy = y2 - y1;        if (Math.abs(dx) &lt; 1e-4) {            return 90;        } else if (Math.abs(dy) &lt; 1e-4) {            return 0;        } else {            double radians = Math.atan2(dy, dx);            double degrees = Math.toDegrees(radians);            return Convert.toInt(Math.round(degrees));        }    }</code></pre><h2>去除红色印章</h2><p>有很多场景，比如说发票，白底黑字文件这种，在OCR识别的过程中，会受到红色印章的影响，所以需要先去除红色印章，在进行OCR识别，这样可以提高OCR的识别率。</p><h3>红色印章检测</h3><p>我碰到比较多的场景是白底黑字的图片，所以我这里就使用了检测红色像素，痛过红色像素的数量，来检测是否该图片有红色印章。</p><pre><code class="language-Java">    /**     * 遍历红色像素，根据像素数量判断是否存在红色印章     *     * @param filePath      图片路径     * @return              true表示可能存在红色印章     */    public static Boolean recognizeRed(String filePath) {        @Cleanup(value = &quot;release&quot;)        Mat mat = Imgcodecs.imread(filePath);        // 转为HSV空间        @Cleanup(value = &quot;release&quot;)        Mat hsv = new Mat();        Imgproc.cvtColor(mat, hsv, Imgproc.COLOR_BGR2HSV);        int nums = 0;        for (int i = 0; i &lt; hsv.rows(); i++) {            for (int j = 0; j &lt; hsv.cols(); j++) {                double[] clone = hsv.get(i, j).clone();                double h = clone[0];                double s = clone[1];                double v = clone[2];                // 红色的hsv范围判断                if ((h &gt; 0 &amp;&amp; h &lt; 10) || (h &gt; 156 &amp;&amp; h &lt; 180)) {                    if (s &gt; 43 &amp;&amp; s &lt; 255) {                        if (v &lt; 255 &amp;&amp; v &gt; 46) {                            nums++;                        }                    }                }            }        }        return nums &gt; 8000;    }</code></pre><p>有些图片看着是没有红色像素的，但是其实会存在有，所以先使用一个只有红色印章的图片进行检测，然后选择一个比较合理的值进行判断就可以。</p><p>这种方法不适合除了印章外还有很多红色的图片，那种情况可以考虑使用霍夫圆检测，那种可能会耗费很多时间，所以我没有使用。</p><h3>去除红色印章</h3><p>我这里去除红色印章的方法，主要的原理就是使用图片二值化，让印章的变成白色即可，但是会出现一个问题，就是红色比黑色更难二值化，所以需要先把图片的红色通道分离出来，红色通道的红色印章的颜色会变淡很多，从而二值化能去除掉红色印章。</p><pre><code class="language-Java">    /**     * 去除红色印章，只是红色通道中的去除，图片会有灰色变化     *     * @param filePath  图片路径     * @param dst       去除后图片保存地址     */    public static void removeRed(String filePath, String dst) {        @Cleanup(value = &quot;release&quot;)        Mat mat = Imgcodecs.imread(filePath);        // 分离红色通道        List&lt;Mat&gt; matList = new ArrayList&lt;&gt;();        Core.split(mat, matList);        @Cleanup(value = &quot;release&quot;)        Mat red = matList.get(2);        // 红色通道二值化        @Cleanup(value = &quot;release&quot;)        Mat redThresh = new Mat();        Imgproc.threshold(red, redThresh, 120, 255, Imgproc.THRESH_BINARY);        Imgcodecs.imwrite(dst, redThresh);    }</code></pre><p>这种方法的结果图片，因为去除了蓝绿通道，所以会变得灰色，不过因为主要的目的是给OCR识别，所以无所谓，如果想不改变图片的话，还是挺麻烦的，可能还得识别出椭圆印章，然后精确的去除。</p><h2>切边矫正</h2><p>图片的切边矫正还是使用深度学习模型比较准确，使用opencv局限性还是挺大的，简单使用还是可以的。以下的方法借鉴了网上流传比较广的一个关于截取发票的边缘的方法。</p><pre><code class="language-Java">    public static void remove(String src,String dst) {        Mat img = Imgcodecs.imread(src);        if(img.empty()){            return;        }        Mat greyImg = img.clone();        //1.彩色转灰色        Imgproc.cvtColor(img, greyImg, Imgproc.COLOR_BGR2GRAY);        OpencvUtil.saveImage(greyImg, &quot;C:\\Users\\11419\\Desktop\\test\\1.jpg&quot;);        Mat gaussianBlurImg = greyImg.clone();        // 2.高斯滤波，降噪        Imgproc.GaussianBlur(greyImg, gaussianBlurImg, new Size(3,3),0);        OpencvUtil.saveImage(greyImg, &quot;C:\\Users\\11419\\Desktop\\test\\2.jpg&quot;);        // 3.Canny边缘检测        Mat cannyImg = gaussianBlurImg.clone();        Imgproc.Canny(gaussianBlurImg, cannyImg, 50, 200);        OpencvUtil.saveImage(cannyImg, &quot;C:\\Users\\11419\\Desktop\\test\\3.jpg&quot;);        // 4.膨胀，连接边缘        Mat dilateImg = cannyImg.clone();        Imgproc.dilate(cannyImg, dilateImg, new Mat(), new Point(-1, -1), 3, 1, new Scalar(1));        OpencvUtil.saveImage(dilateImg, &quot;C:\\Users\\11419\\Desktop\\test\\4.jpg&quot;);        //5.对边缘检测的结果图再进行轮廓提取        List&lt;MatOfPoint&gt; contours = new ArrayList&lt;&gt;();        List&lt;MatOfPoint&gt; drawContours = new ArrayList&lt;&gt;();        Mat hierarchy = new Mat();        Imgproc.findContours(dilateImg, contours, hierarchy, Imgproc.RETR_EXTERNAL, Imgproc.CHAIN_APPROX_SIMPLE);        Mat linePic = Mat.zeros(dilateImg.rows(), dilateImg.cols(), CvType.CV_8UC3);        //6.找出轮廓对应凸包的四边形拟合        List&lt;MatOfPoint&gt; squares = new ArrayList&lt;&gt;();        List&lt;MatOfPoint&gt; hulls = new ArrayList&lt;&gt;();        MatOfInt hull = new MatOfInt();        MatOfPoint2f approx = new MatOfPoint2f();        approx.convertTo(approx, CvType.CV_32F);        for (MatOfPoint contour : contours) {            // 边框的凸包            Imgproc.convexHull(contour, hull);            // 用凸包计算出新的轮廓点            Point[] contourPoints = contour.toArray();            int[] indices = hull.toArray();            List&lt;Point&gt; newPoints = new ArrayList&lt;&gt;();            for (int index : indices) {                newPoints.add(contourPoints[index]);            }            MatOfPoint2f contourHull = new MatOfPoint2f();            contourHull.fromList(newPoints);            // 多边形拟合凸包边框(此时的拟合的精度较低)            Imgproc.approxPolyDP(contourHull, approx, Imgproc.arcLength(contourHull, true) * 0.02, true);            MatOfPoint mat = new MatOfPoint();            mat.fromArray(approx.toArray());            drawContours.add(mat);            // 筛选出面积大于某一阈值的，且四边形的各个角度都接近直角的凸四边形            MatOfPoint approxf = new MatOfPoint();            approx.convertTo(approxf, CvType.CV_32S);            if (approx.rows() == 4 &amp;&amp; Math.abs(Imgproc.contourArea(approx)) &gt; 40000 &amp;&amp;                    Imgproc.isContourConvex(approxf)) {                double maxCosine = 0;                for (int j = 2; j &lt; 5; j++) {                    double cosine = Math.abs(getAngle(approxf.toArray()[j % 4], approxf.toArray()[j - 2], approxf.toArray()[j - 1]));                    maxCosine = Math.max(maxCosine, cosine);                }                // 角度大概72度                if (maxCosine &lt; 0.3) {                    MatOfPoint tmp = new MatOfPoint();                    contourHull.convertTo(tmp, CvType.CV_32S);                    squares.add(approxf);                    hulls.add(tmp);                }            }        }        //这里是把提取出来的轮廓通过不同颜色的线描述出来，具体效果可以自己去看        Random r = new Random();        for (int i = 0; i &lt; drawContours.size(); i++) {            Imgproc.drawContours(linePic, drawContours, i, new Scalar(r.nextInt(255),r.nextInt(255), r.nextInt(255)));        }        OpencvUtil.saveImage(linePic, &quot;C:\\Users\\11419\\Desktop\\test\\5.jpg&quot;);        //7.找出最大的矩形        int index = findLargestSquare(squares);        MatOfPoint largest_square;        if(!squares.isEmpty()){            largest_square = squares.get(index);        }else{            System.out.println(&quot;图片无法识别&quot;);            return;        }        Mat polyPic = Mat.zeros(img.size(), CvType.CV_8UC3);        Imgproc.drawContours(polyPic, squares, index, new Scalar(0, 0,255), 2);        OpencvUtil.saveImage(polyPic, &quot;C:\\Users\\11419\\Desktop\\test\\6.jpg&quot;);        //存储矩形的四个凸点        hull = new MatOfInt();        Imgproc.convexHull(largest_square, hull, false);        List&lt;Integer&gt; hullList =  hull.toList();        List&lt;Point&gt; polyContoursList = largest_square.toList();        List&lt;Point&gt; hullPointList = new ArrayList&lt;&gt;();        for (Integer integer : hullList) {            Imgproc.circle(polyPic, polyContoursList.get(integer), 10, new Scalar(r.nextInt(255), r.nextInt(255), r.nextInt(255), 3));            hullPointList.add(polyContoursList.get(integer));        }        Core.addWeighted(polyPic, 1, img, 1, 0, img);        OpencvUtil.saveImage(img, &quot;C:\\Users\\11419\\Desktop\\test\\7.jpg&quot;);        List&lt;Point&gt; lastHullPointList = new ArrayList&lt;&gt;(hullPointList);        //dstPoints储存的是变换后各点的坐标，依次为左上，右上，右下， 左下        //srcPoints储存的是上面得到的四个角的坐标        Point[] dstPoints = {new Point(0,0), new Point(img.cols(),0), new Point(img.cols(),img.rows()), new Point(0,img.rows())};        Point[] srcPoints = new Point[4];        boolean sorted = false;        int n = 4;        //对四个点进行排序 分出左上 右上 右下 左下        while (!sorted &amp;&amp; n &gt; 0){            for (int i = 1; i &lt; n; i++){                sorted = true;                if (lastHullPointList.get(i - 1).x &gt; lastHullPointList.get(i).x){                    Point temp1 = lastHullPointList.get(i);                    Point temp2 = lastHullPointList.get(i-1);                    lastHullPointList.set(i, temp2);                    lastHullPointList.set(i - 1, temp1);                    sorted = false;                }            }            n--;        }        //即先对四个点的x坐标进行冒泡排序分出左右，再根据两对坐标的y值比较分出上下        if (lastHullPointList.get(0).y &lt; lastHullPointList.get(1).y){            srcPoints[0] = lastHullPointList.get(0);            srcPoints[3] = lastHullPointList.get(1);        }else{            srcPoints[0] = lastHullPointList.get(1);            srcPoints[3] = lastHullPointList.get(0);        }        if (lastHullPointList.get(2).y &lt; lastHullPointList.get(3).y){            srcPoints[1] = lastHullPointList.get(2);            srcPoints[2] = lastHullPointList.get(3);        }else{            srcPoints[1] = lastHullPointList.get(3);            srcPoints[2] = lastHullPointList.get(2);        }        List&lt;Point&gt; listSrcs = java.util.Arrays.asList(srcPoints[0], srcPoints[1], srcPoints[2], srcPoints[3]);        Mat srcPointsMat = Converters.vector_Point_to_Mat(listSrcs, CvType.CV_32F);        List&lt;Point&gt; dstSrcs = java.util.Arrays.asList(dstPoints[0], dstPoints[1], dstPoints[2], dstPoints[3]);        Mat dstPointsMat = Converters.vector_Point_to_Mat(dstSrcs, CvType.CV_32F);        //参数分别为输入输出图像、变换矩阵、大小。        //坐标变换后就得到了我们要的最终图像。        Mat transMat = Imgproc.getPerspectiveTransform(srcPointsMat, dstPointsMat);    //得到变换矩阵        Mat outPic = new Mat();        Imgproc.warpPerspective(img, outPic, transMat, img.size());        OpencvUtil.saveImage(outPic, dst);    }    // 根据三个点计算中间那个点的夹角   pt1 pt0 pt2    private static double getAngle(Point pt1, Point pt2, Point pt0) {        double dx1 = pt1.x - pt0.x;        double dy1 = pt1.y - pt0.y;        double dx2 = pt2.x - pt0.x;        double dy2 = pt2.y - pt0.y;        return (dx1*dx2 + dy1*dy2)/Math.sqrt((dx1*dx1 + dy1*dy1)*(dx2*dx2 + dy2*dy2) + 1e-10);    }    // 找到最大的正方形轮廓    private static int findLargestSquare(List&lt;MatOfPoint&gt; squares) {        if (squares.isEmpty()) {            return -1;        }        int maxWidth = 0;        int maxHeight = 0;        int maxSquareIdx = 0;        int currentIndex = 0;        for (MatOfPoint square : squares) {            Rect rectangle = Imgproc.boundingRect(square);            if (rectangle.width &gt;= maxWidth &amp;&amp; rectangle.height &gt;= maxWidth) {                maxWidth = rectangle.width;                maxHeight = rectangle.height;                maxSquareIdx = currentIndex;            }            currentIndex++;        }        return maxSquareIdx;    }</code></pre>]]></content:encoded>
            <category domain="https://yww52.com/categories/%25E6%2588%2591%25E7%259A%2584%25E6%258A%2580%25E6%259C%25AF%25E5%25AD%25A6%25E4%25B9%25A0%25E7%2589%25A9%25E8%25AF%25AD%25E6%259E%259C%25E7%2584%25B6%25E6%259C%2589%25E9%2597%25AE%25E9%25A2%2598/">我的技术学习物语果然有问题</category>
        </item>
        <item>
            <title>服务器软件</title>
            <link>https://yww52.com/posts/58d09dd7/</link>
            <guid isPermaLink="true">https://yww52.com/posts/58d09dd7/</guid>
            <pubDate>Tue, 11 Apr 2023 00:00:00 +0000</pubDate>
            <description>一些服务器软件的安装</description>
            <content:encoded><![CDATA[<img src="https://img.yww52.com/2023/4/2023-4-11top_img.jpg" alt="服务器软件" style="max-width:100%;height:auto;margin-bottom:15px;" /><h1>Java</h1><h2>卸载系统自带的JDK</h2><h3>查看系统安装的JDK</h3><p>使用<code>rpm -qa | grep jdk</code>命令查看当前系统安装的JDK。</p><pre><code class="language-bash">[root@192 ~]# rpm -qa | grep jdkjava-1.8.0-openjdk-headless-1.8.0.362.b08-1.el7_9.x86_64java-1.7.0-openjdk-1.7.0.261-2.6.22.2.el7_8.x86_64java-1.7.0-openjdk-headless-1.7.0.261-2.6.22.2.el7_8.x86_64copy-jdk-configs-3.3-11.el7_9.noarchjava-1.8.0-openjdk-1.8.0.362.b08-1.el7_9.x86_64</code></pre><h3>卸载安装的JDK</h3><p>使用<code>rpm -e --nodeps</code>的命令去卸载上述查询到的JDK。</p><pre><code class="language-bash">[root@192 ~]# rpm -e --nodeps java-1.8.0-openjdk-headless-1.8.0.362.b08-1.el7_9.x86_64[root@192 ~]# rpm -e --nodeps java-1.7.0-openjdk-1.7.0.261-2.6.22.2.el7_8.x86_64[root@192 ~]# rpm -e --nodeps java-1.7.0-openjdk-headless-1.7.0.261-2.6.22.2.el7_8.x86_64[root@192 ~]# rpm -e --nodeps copy-jdk-configs-3.3-11.el7_9.noarch[root@192 ~]# rpm -e --nodeps java-1.8.0-openjdk-1.8.0.362.b08-1.el7_9.x86_64[root@192 ~]# rpm -qa | grep jdk[root@192 ~]# java -versionbash: java: 未找到命令...</code></pre><h2>使用yum方式安装Java</h2><h3>查看可以安装的版本</h3><p>使用命令<code>yum -y list java*</code>查询可以安装的Java版本。</p><pre><code class="language-bash">[root@192 ~]# yum -y list java*已加载插件：fastestmirror, langpacksLoading mirror speeds from cached hostfile * base: mirrors.aliyun.com * extras: mirrors.aliyun.com * updates: mirrors.aliyun.com已安装的软件包javapackages-tools.noarch  3.4.1-11.el7  @anaconda可安装的软件包.......</code></pre><h3>安装JDK</h3><p>这里选择安装1.8的版本。</p><pre><code class="language-bash">yum -y install java-1.8.0-openjdk*</code></pre><h3>查看JDK安装的目录</h3><pre><code class="language-bash">[root@192 /]# cd /usr/lib/jvm/java-1.8.0-openjdk[root@192 java-1.8.0-openjdk]# ls ASSEMBLY_EXCEPTION  bin  demo  include  jre  lib  LICENSE  sample  src.zip  tapset  THIRD_PARTY_README</code></pre><h3>卸载yum下载的JDK</h3><pre><code class="language-bash">yum -y remove java-1.8.0-openjdk*</code></pre><h2>手动安装JDK</h2><h3>下载安装包</h3><p>安装包的下载可以去oracle的官网去下载，这里选择下载<code>Java8</code>的版本。</p><p><a href="https://www.oracle.com/java/technologies/downloads/#java8">下载地址</a></p><p>根据自己的系统，选择对应的版本即可，这里选择<code>x64 Compressed Archive</code>的安装包。</p><p>将<code>jdk-8u361-linux-x64.tar.gz</code>安装包上传到服务器。</p><h3>解压安装包</h3><p>这里将Java放在了<code>/usr/local/java</code>的文件夹中，把安装包解压到该文件夹即可。</p><pre><code class="language-bash">tar -zxvf jdk-8u361-linux-x64.tar.gz -C ./</code></pre><h3>配置环境变量</h3><p>在<code>/etc/profile</code>文件尾部，加入以下的JDK的环境配置。</p><pre><code>JAVA_HOME=/usr/local/java/jdk1.8.0_361CLASSPATH=$JAVA_HOME/lib/PATH=$PATH:$JAVA_HOME/binexport PATH JAVA_HOME CLASSPATH</code></pre><p>执行以下命令，使环境变量生效。</p><pre><code class="language-bash">source /etc/profile</code></pre><p>这条命令，在重启之后会失效，需要再次执行，不想再次执行，需要在<code>/etc/bashrc</code>尾部加入该条命令。</p><pre><code class="language-bash">source /etc/profile</code></pre><h3>卸载JDK</h3><p>想卸载的话，就将上述的压缩包的文件和环境变量删掉即可。</p><h1>Redis</h1><h2>源码编译</h2><h3>编译安装</h3><ol><li><p>下载Redis</p><p>Redis的安装包直接去官网下载即可。</p><p><a href="https://download.redis.io/releases/?_gl=1*1oe52s2*_gcl_au*MTAyNjg4NTk2Ny4xNzQ0MTY3NDU0">下载地址</a></p></li><li><p>解压安装包</p><p>这里放到<code>/usr/local/redis</code>下，并执行下面命令进行解压。</p><pre><code class="language-bash">tar -zxvf redis-7.0.10.tar.gz </code></pre></li><li><p>编译安装</p><p>进入redis源码文件夹，执行以下命令，编译redis并进行安装。</p><pre><code class="language-bash"># 编译安装makemake install# 官方建议使用test测试make test</code></pre></li><li><p>查看是否安装成功，在系统命令中出现redis的命令表示安装成功了。</p><pre><code class="language-bash">[root@192 redis-7.0.10]# cd /usr/local/bin/[root@192 bin]# ls redis-benchmark  redis-check-aof  redis-check-rdb  redis-cli  redis-sentinel  redis-server</code></pre></li><li><p>将Redis安装到系统服务</p><p>使用Redis的脚本工具，将Redis安装到系统服务上。</p><pre><code>[root@192 redis-7.0.10]# cd utils/[root@192 utils]# ./install_server.shWelcome to the redis service installerThis script will help you easily set up a running redis serverThis systems seems to use systemd.Please take a look at the provided example service unit files in this directory, and adapt and install them. Sorry!</code></pre><p>发现有问题，得换种方法。</p><p>将<code>utils</code>目录下的<code>systemd-redis_server.service </code>文件拷贝到系统配置目录下，并改名为<code>redis.server</code></p><pre><code class="language-bash">cp systemd-redis_server.service /usr/lib/systemd/systemcd /usr/lib/systemd/systemmv systemd-redis_server.service  redis.service</code></pre><p>编辑<code>redis.server</code>.</p><pre><code class="language-bash"># 原来为ExecStart=/usr/local/bin/redis-server --supervised systemd --daemonize no[Service]ExecStart=/usr/local/bin/redis-server /usr/local/redis/redis-7.0.10/redis.conf --supervised systemd# 不修改启动可能会超时#Type=notifyPrivateTmp=yesTimeoutStartSec=15TimeoutStopSec=15</code></pre></li><li><p>编辑redis的配置文件</p><pre><code class="language-bash">################################# GENERAL ###################################### 配置日志文件位置logfile /var/log/redis/redis.log################################ SNAPSHOTTING  ################################# 数据目录dir /var/redis/6379</code></pre></li><li><p>基础命令</p><pre><code class="language-bash"># 重新加载服务文件systemctl daemon-reload# 设置自启systemctl enable redis# 启动systemctl start redis# 停止systemctl stop redis# 重启systemctl restart redis# 查看状态systemctl status redis</code></pre></li></ol><h3>配置文件修改</h3><p>redis有些配置需要更改，不然使用起来会出现问题。配置文件就在源码文件夹中的<code>redis.conf</code></p><pre><code class="language-redis.conf"># 需设置为 0.0.0.0 以允许所有 IP 访问，默认只能本地127.0.0.1本地IP访问bind 0.0.0.0# 关闭protected-mode模式，此时外部网络可以直接访问protected-mode no# 设置密码，不设置就需要自己注意requirepass 123456# 是否以守护进程模式运行（后台运行），默认为nodaemonize yes# 日志文件存放logfile &quot;/data/redis/log/redis.log&quot;# 持久化存放文件的路径dir /data/redis/data# 启用 AOF 持久化appendonly yes# 指定 AOF 文件名appendfilename &quot;redis_data.aof&quot;</code></pre><h2>docker</h2><p>redis容器有个比较坑的地方，就是默认是找不到配置文件的，得自己参考配置文件，然后增加配置，映射到容器内部，覆盖掉默认的配置。</p><ol><li><p>拉取容器</p><pre><code class="language-bash">docker pull redis:7-alpine</code></pre></li><li><p>创建<code>redis.conf</code>配置文件</p><pre><code class="language-redis.conf"># 以下是容器内部路径，不需要修改dir /data/redisbind 0.0.0.0port 6379# 日志文件存放logfile &quot;/data/log/redis.log&quot;# 启用AOF持久化appendonly yesappendfilename &quot;appendonly.aof&quot;appendfsync everysec# RDB持久化配置（可选）save 900 1save 300 10save 60 10000# 日志级别loglevel notice</code></pre></li><li><p>启动容器</p><pre><code class="language-bash">  docker run -d \    --name redis \    -p 6379:6379 \    -v /data/redis/data:/data/redis \    -v /data/redis/log:/data/log \    -v ./redis.conf:/usr/local/etc/redis/redis.conf \    --restart unless-stopped \    redis:7-alpine \    redis-server /usr/local/etc/redis/redis.conf --appendonly yes</code></pre></li></ol><h2>docker-compose</h2><pre><code class="language-dockerfile">version: &#39;3.8&#39;services:  redis:    image: redis:7-alpine    container_name: chitanda-redis    ports:      - &quot;6379:6379&quot;    volumes:      # 持久化数据目录映射到宿主机      - /data/redis/data:/data/redis      # 日志映射到宿主机      - /data/redis/log:/data/log      # Redis配置文件映射到宿主机      - ./redis.conf:/usr/local/etc/redis/redis.conf    command: redis-server /usr/local/etc/redis/redis.conf --appendonly yes    restart: unless-stopped</code></pre><p>只部署一个服务的话，还是推荐用docker，有多个容器编排再用docker-compose</p><p>将配置文件和<code>docker-compose.yml</code>放在同一个目录，启动容器</p><pre><code class="language-bash">docker-compose up -d </code></pre><h1>Nginx</h1><h2>APT包管理</h2><pre><code class="language-bash"># 更新apt update# 下载nginxapt install nginx# 验证nginxnginx -v# nginx配置/etc/nginx </code></pre><h2>源码编译</h2><h3>Nginx依赖下载</h3><p>Nginx还需要下载一些依赖库。</p><pre><code class="language-bash"># PCRE https://github.com/PCRE2Project/pcre2/releases，例如pcre2-10.45.tar.gz# PCRE编译安装tar -zxvf pcre2-10.45.tar.gzcd  pcre2-10.45./configuremake make install</code></pre><pre><code class="language-bash"># Zlib https://www.zlib.net/，例如zlib-1.3.1.tar.gz# Zlib编译安装tar -zxvf zlib-1.3.1.tar.gzcd  zlib-1.3.1./configuremake make install</code></pre><pre><code class="language-bash"># OpenSSL https://openssl-library.org/source/，例如openssl-3.5.1.tar.gz# OpenSSL编译安装tar -zxvf openssl-3.5.1.tar.gzcd  openssl-3.5.1./config --prefix=/usr/local/openssl --openssldir=/usr/local/opensslmake make install</code></pre><blockquote><p>尽量添加sudo执行命令，避免权限出现的问题。</p></blockquote><h3>编译安装Nginx</h3><p>首先需要先去下载<code>nginx</code>的源码</p><p><a href="https://nginx.org/en/download.html">下载地址</a></p><p>比如这里的nginx-1.28.0.tar.gz。</p><pre><code class="language-bash"># 解压tar -zxvf nginx-1.28.0.tar.gzcd  nginx-1.28.0# 配置编译参数，请自行替换刚刚的依赖源码路径./configure \  --prefix=/etc/nginx \  --with-pcre=/data/nginx/pcre2-10.45 \  --with-zlib=/data/nginx/zlib-1.3.1 \  --with-openssl=/data/openssl-3.5.1 \  --with-http_ssl_module# 编译安装makemake install</code></pre><h3>配置 Systemd 服务</h3><p>创建服务文件<code>/etc/systemd/system/nginx.service</code></p><pre><code class="language-bash">[Unit]Description=The NGINX HTTP and reverse proxy serverAfter=syslog.target network.target [Service]Type=forkingPIDFile=/etc/nginx/logs/nginx.pidExecStartPre=/etc/nginx/sbin/nginx -tExecStart=/etc/nginx/sbin/nginxExecReload=/etc/nginx/sbin/nginx -s reloadExecStop=/bin/kill -s QUIT $MAINPIDPrivateTmp=true [Install]WantedBy=multi-user.target</code></pre><pre><code class="language-bash">sudo systemctl enable nginxsudo systemctl start nginx</code></pre><h2>docker</h2><p>拉取镜像</p><pre><code class="language-bash">docker pull nginx:latest</code></pre><p>配置文件，自用参考</p><p>nginx.conf</p><pre><code class="language-nginx.conf">user  nginx;worker_processes  auto;error_log  /var/log/nginx/error.log notice;pid        /var/run/nginx.pid;events {    worker_connections  1024;}http {    include       /etc/nginx/mime.types;    default_type  application/octet-stream;    log_format  main  &#39;$remote_addr - $remote_user [$time_local] &quot;$request&quot; &#39;                      &#39;$status $body_bytes_sent &quot;$http_referer&quot; &#39;                      &#39;&quot;$http_user_agent&quot; &quot;$http_x_forwarded_for&quot;&#39;;    access_log  /var/log/nginx/access.log  main;    sendfile        on;    #tcp_nopush     on;    keepalive_timeout  65;    client_max_body_size 0;    #gzip  on;    include /etc/nginx/conf.d/*.conf;}</code></pre><p>然后单独配置网页监听，/conf.d/test.conf</p><pre><code class="language-test.conf">server {    listen       10010;    server_name  localhost;        location / {      root /etc/nginx/html/dist;      index index.html index.htm;      try_files $uri $uri/ @router;    }    location /api {        proxy_pass http://192.168.1.1:9888/api;        proxy_set_header X-Forwarded-Proto $scheme;        proxy_set_header X-Forwarded-Port $server_port;        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;        proxy_set_header Upgrade $http_upgrade;        proxy_set_header Connection &quot;upgrade&quot;;    }    error_page 404 /404.html;    location = /404.html {    }    error_page 500 502 503 504 /50x.html;    location = /50x.html {    }}</code></pre><p>启动容器</p><pre><code class="language-bash">docker run -d \--name nginx \-p 10010:10010 \-v /data/nginx/nginx.conf:/etc/nginx/nginx.conf \-v /data/nginx/conf.d:/etc/nginx/conf.d \-v /data/nginx/logs:/var/log/nginx \-v /data/nginx/www:/etc/nginx/html \--restart unless-stopped \nginx</code></pre><h1>Mysql</h1><h2>官网DEB包安装</h2><p><a href="https://downloads.mysql.com/archives/community/">https://downloads.mysql.com/archives/community/</a></p><p>选择官网适配有的操作系统，这里就选择<code>Ubantu Linux</code>，版本自己选择。</p><p>要是没有对应的版本，就看看使用的操作系统是基于什么的，也可以尝试一下，实在不行就只能试试使用源码进行编译了。</p><p>我这里下载的就是第一个<code>Ubuntu Linux 24.10 (x86, 64-bit), DEB Bundle</code>，包含了所有的数据库依赖了。</p><pre><code class="language-bash"># 解压tar -xvf mysql-server_8.0.41-1ubuntu24.10_amd64.deb-bundle.tar# 里面包含这些依赖包libmysqlclient-dev_8.0.41-1ubuntu24.10_amd64.deblibmysqlclient21_8.0.41-1ubuntu24.10_amd64.debmysql-client_8.0.41-1ubuntu24.10_amd64.debmysql-common_8.0.41-1ubuntu24.10_amd64.debmysql-community-client-core_8.0.41-1ubuntu24.10_amd64.debmysql-community-client-plugins_8.0.41-1ubuntu24.10_amd64.debmysql-community-client_8.0.41-1ubuntu24.10_amd64.debmysql-community-server-core_8.0.41-1ubuntu24.10_amd64.debmysql-community-server-debug_8.0.41-1ubuntu24.10_amd64.debmysql-community-server_8.0.41-1ubuntu24.10_amd64.debmysql-community-test-debug_8.0.41-1ubuntu24.10_amd64.debmysql-community-test_8.0.41-1ubuntu24.10_amd64.debmysql-server_8.0.41-1ubuntu24.10_amd64.debmysql-testsuite_8.0.41-1ubuntu24.10_amd64.deb</code></pre><pre><code class="language-bash"># 依次按顺序安装依赖包# 基础组件sudo dpkg -i mysql-common_8.0.41-1ubuntu24.10_amd64.deb# 核心库sudo dpkg -i libmysqlclient21_8.0.41-1ubuntu24.10_amd64.debsudo dpkg -i libmysqlclient-dev_8.0.41-1ubuntu24.10_amd64.deb# 客户端组件sudo dpkg -i mysql-community-client-core_8.0.41-1ubuntu24.10_amd64.debsudo dpkg -i mysql-community-client-plugins_8.0.41-1ubuntu24.10_amd64.debsudo dpkg -i mysql-community-client_8.0.41-1ubuntu24.10_amd64.debsudo dpkg -i mysql-client_8.0.41-1ubuntu24.10_amd64.deb# 服务器组件sudo dpkg -i mysql-community-server-core_8.0.41-1ubuntu24.10_amd64.debsudo dpkg -i mysql-community-server_8.0.41-1ubuntu24.10_amd64.debsudo dpkg -i mysql-server_8.0.41-1ubuntu24.10_amd64.deb# 测试调试组件sudo dpkg -i mysql-community-test_8.0.41-1ubuntu24.10_amd64.debsudo dpkg -i mysql-community-test-debug_8.0.41-1ubuntu24.10_amd64.debsudo dpkg -i mysql-testsuite_8.0.41-1ubuntu24.10_amd64.deb</code></pre><h2>docker</h2><p>创建好存储目录</p><pre><code class="language-bash"># 日志目录mkdir -p /data/docker/mysql/log# 数据目录mkdir -p /data/docker/mysql/data# 配置文件目录mkdir -p /data/docker/mysql/conf</code></pre><p>创建配置文件<code>/data/docker/mysql/conf/custom.cnf</code>，具体配置可以自行修改</p><pre><code class="language-my.cnf">[client]default-character-set=utf8mb4[mysql]default-character-set=utf8mb4[mysqld]init_connect=&quot;SET collation_connection = utf8mb4_general_ci&quot;init_connect=&quot;SET NAMES utf8mb4&quot;character-set-server=utf8mb4collation-server=utf8mb4_general_ciskip-character-set-client-handshakeskip-name-resolve</code></pre><p>创建并运行容器</p><pre><code class="language-bash"> docker run  --name mysql --restart=always -p 3306:3306  -v /data/docker/mysql/log:/var/log/mysql  -v /data/docker/mysql/data:/var/lib/mysql  -v /data/docker/mysql/conf:/etc/mysql/conf.d  -e MYSQL_ROOT_PASSWORD=123456 -d mysql:5.7.44</code></pre><p>验证容器</p><pre><code class="language-bash"># 进入容器docker exec -it mysql bash# 进入容器后mysql -uroot -p123456</code></pre><blockquote><p>注意，MySQL容器初始化会在存储目录产生一些初始化文件，如果想重复初始化，得先把之前的配置删干净，不然在创建容器会初始化报错。</p></blockquote><h1>Docker</h1><h2>卸载旧版本</h2><pre><code class="language-bash">$ sudo yum remove docker \                  docker-client \                  docker-client-latest \                  docker-common \                  docker-latest \                  docker-latest-logrotate \                  docker-logrotate \                  docker-selinux \                  docker-engine-selinux \                  docker-engine</code></pre><h2>安装Docker</h2><h3>使用yum安装</h3><p>安装所需要的依赖包。</p><pre><code class="language-bash">$ sudo yum install -y yum-utils</code></pre><p>鉴于国内网络问题，强烈建议使用国内源，官方源请在注释中查看。</p><p>执行下面的命令添加 <code>yum</code> 软件源：</p><pre><code class="language-bash">$ sudo yum-config-manager \    --add-repo \    https://mirrors.aliyun.com/docker-ce/linux/centos/docker-ce.repo$ sudo sed -i &#39;s/download.docker.com/mirrors.aliyun.com\/docker-ce/g&#39; /etc/yum.repos.d/docker-ce.repo</code></pre><p>有了依赖包之后，就可以直接安装Docker了。</p><pre><code class="language-bash">$ sudo yum install docker-ce docker-ce-cli containerd.io</code></pre><h3>使用脚本自动安装</h3><p>在测试或开发环境中 Docker 官方为了简化安装流程，提供了一套便捷的安装脚本，CentOS 系统上可以使用这套脚本安装，另外可以通过 <code>--mirror</code> 选项使用国内源进行安装：</p><pre><code class="language-bash">$ curl -fsSL get.docker.com -o get-docker.sh$ sudo sh get-docker.sh --mirror Aliyun</code></pre><p>执行这个命令后，脚本就会自动的将一切准备工作做好，并且把 Docker 的稳定(stable)版本安装在系统中。</p><h2>启动Docker</h2><pre><code class="language-bash">$ sudo systemctl enable docker$ sudo systemctl start docker</code></pre><h2>建立docker用户组</h2><p>建立<code>docker</code>组</p><pre><code class="language-bash">$ sudo groupadd docker</code></pre><p>将当前用户加入<code>docker</code>组中</p><pre><code class="language-bash">$ sudo usermod -aG docker $USER</code></pre><h2>镜像加速</h2><h3>镜像加速器</h3><ol><li>阿里云加速器，可以去阿里云的关于镜像的管理控制台去获取。</li><li>网易云加速器，<code>https://hub-mirror.c.163.com</code></li><li>百度云加速器，<code>https://mirror.baidubce.com</code></li></ol><h3>配置docker加速器</h3><p>查看之前是否配置过镜像地址。</p><pre><code class="language-bash">$ systemctl cat docker | grep &#39;\-\-registry\-mirror&#39;</code></pre><p>如果该命令有输出，那么请执行 <code>$ systemctl cat docker</code> 查看 <code>ExecStart=</code> 出现的位置，修改对应的文件内容去掉 <code>--registry-mirror</code> 参数及其值，并按接下来的步骤进行配置。</p><p>如果以上命令没有任何输出，那么就可以在 <code>/etc/docker/daemon.json</code> 中写入如下内容（如果文件不存在请新建该文件）：</p><pre><code class="language-json">{  &quot;registry-mirrors&quot;: [    &quot;https://hub-mirror.c.163.com&quot;,    &quot;https://mirror.baidubce.com&quot;  ]}</code></pre><h3>重启docker服务</h3><pre><code class="language-bash">$ sudo systemctl daemon-reload$ sudo systemctl restart docker</code></pre><h2>测试Docker是否安装成功</h2><pre><code class="language-bash">[root@localhost ~]# docker run --rm hello-worldHello from Docker!This message shows that your installation appears to be working correctly.To generate this message, Docker took the following steps: 1. The Docker client contacted the Docker daemon. 2. The Docker daemon pulled the &quot;hello-world&quot; image from the Docker Hub.    (amd64) 3. The Docker daemon created a new container from that image which runs the    executable that produces the output you are currently reading. 4. The Docker daemon streamed that output to the Docker client, which sent it    to your terminal.To try something more ambitious, you can run an Ubuntu container with: $ docker run -it ubuntu bashShare images, automate workflows, and more with a free Docker ID: https://hub.docker.com/For more examples and ideas, visit: https://docs.docker.com/get-started/</code></pre><p>出现以上内容则表示安装成功。</p><h1>Docker离线安装</h1><p>下载Docker离线安装包。</p><p><a href="https://download.docker.com/linux/static/stable/x86_64/">下载地址</a></p><p>系统配置文件<code>docker.service</code></p><pre><code class="language-docker.service">[Unit]Description=Docker Application Container EngineDocumentation=https://docs.docker.comAfter=network - online.target firewalld.serviceWants=network - online.target[Service]Type=notify# the default is not to use systemd for cgroups because the delegate issues still# exists and systemd currently does not support the cgroup feature set required# for containers run by docker ExecStart=/usr/bin/dockerd  --ipv6=falseExecReload=/bin/kill -s HUP $MAINPID# Having non - zero Limit*s causes performance problems due to accounting overhead# in the kernel. We recommend using cgroups to do container - local accounting.LimitNOFILE=infinityLimitNPROC=infinityLimitCORE=infinity# Uncomment TasksMax if your systemd version supports it.# Only systemd 226 and above support this version.#TasksMax=infinityTimeoutStartSec=0# set delegate yes so that systemd does not reset the cgroups of docker containersDelegate=yes# kill only the docker process, not all processes in the cgroupKillMode=process# restart the docker process if it exits prematurelyRestart=on - failureStartLimitBurst=3StartLimitInterval=60s[Install]WantedBy=multi - user.target</code></pre><p>安装脚本。</p><pre><code class="language-bash">#!/bin/sh echo &#39;解压tar包&#39;tar -xvf $1echo &#39;将docker目录下所有文件复制到/usr/bin目录&#39;cp docker/* /usr/binecho &#39;将docker.service 复制到/etc/systemd/system/目录&#39;cp $2 /etc/systemd/system/echo &#39;添加文件可执行权限&#39;chmod +x /etc/systemd/system/docker.serviceecho &#39;重新加载配置文件&#39;systemctl daemon-reloadecho &#39;启动docker&#39;systemctl start dockerecho &#39;设置开机自启&#39;systemctl enable docker.serviceecho &#39;docker安装成功&#39;docker -v</code></pre><p>卸载脚本。</p><pre><code class="language-bash">#!/bin/sh echo &#39;停止docker&#39;systemctl stop dockerecho &#39;删除docker.service&#39;rm -f /etc/systemd/system/docker.serviceecho &#39;删除docker文件&#39;rm -rf /usr/bin/docker*echo &#39;重新加载配置文件&#39;systemctl daemon-reloadecho &#39;卸载成功&#39;</code></pre><p>通过安装包，执行安装脚本。</p><pre><code class="language-bash">sh install.sh docker-23.0.3.tgz docker.service</code></pre><h1>docker-compose</h1><p>docker-compose本质就是docker命令，所以安装了docker后，只需要把这个命令放到服务器环境中就能调用了</p><ol><li><p>根据架构下载对应二进制命令文件，下载地址<a href="https://github.com/docker/compose/releases">https://github.com/docker/compose/releases</a></p></li><li><p>比如x86架构，就下载<code>docker-compose-darwin-x86_64</code>，把二进制文件名字改成<code>docker-compose</code> ,然后将命令文件上传到服务器的<code>/usr/local/bin</code>路径中</p></li><li><p>给命令添加可执行权限</p><pre><code class="language-bash">sudo chmod +x /usr/local/bin/docker-compose</code></pre></li><li><p>测试命令</p><pre><code class="language-bash">docker-compose --version</code></pre></li></ol><h1>RAGFlow</h1><p>这里只记录<code>docker-compose</code>的安装方式，RafFlow涉及的服务有点多，使用docker-compose方便很多。</p><p>官方的仓库<a href="https://github.com/infiniflow/ragflow.git">https://github.com/infiniflow/ragflow.git</a></p><p>下载这个仓库，重点是<code>docker</code>目录</p><pre><code class="language-bash">git clone https://github.com/infiniflow/ragflow.gitcd ./ragflow/docker</code></pre><p>修改配置文件<code>.env</code>，可以根据官方文档自行修改，这里只修改RAGFlow的版本。</p><pre><code class="language-bash"># 默认镜像是slim的# RAGFLOW_IMAGE=infiniflow/ragflow:v0.20.5-slim# 修改成这个镜像RAGFLOW_IMAGE=infiniflow/ragflow:v0.20.5</code></pre><p>在<code>docker目录中</code>启动容器</p><pre><code class="language-bash"># 启动docker-compose up -d# 停止docker-compose stop# 如果要删除，尽量把数据卷也删掉docker-compose down -v</code></pre><p>可能会遇到的问题</p><ol><li><p>Mysql容器出现问题，起不来</p><p>这个容器很容易出问题，所以得检查数据卷是否已经存在，是否有初始化文件重复问题，请删干净</p><p>权限问题，在docker的Mysql配置文件中，即<code>docker/docker-compose-base.yml</code>，添加配置</p><pre><code>   mysql:       privileged: true       user: root</code></pre></li><li><p>有其他配套服务起不来</p><p>也尝试在配置中添加<code>privileged: ture</code>配置</p></li><li><p>ragflow-server报错</p><p>OPENBLAS blas_thread_init: pthread_create failed for thread x of 64: Operation not permitted</p><p>有可能是python的问题，详情参考<a href="https://stackoverflow.com/questions/52026652/openblas-blas-thread-init-pthread-create-resource-temporarily-unavailable%E8%BF%99%E4%B8%AA%E4%BF%AE%E6%94%B9%E6%96%B9%E6%A1%88">https://stackoverflow.com/questions/52026652/openblas-blas-thread-init-pthread-create-resource-temporarily-unavailable这个修改方案</a></p><p>修改了之后，启动可能还会报一个奇怪的错误，也可以尝试在配置文件<code>docker-compose.yml</code>中添加<code>privileged: ture</code>配置</p></li></ol><h1>声明</h1><p>以上基本都是网上搜集的教程，基本都是复制粘贴，有些已经找不到出处，有出处的我尽量标明在以下链接。</p><p><a href="https://yeasy.gitbook.io/docker_practice/">Docker</a></p><p><a href="https://chexl.blog.csdn.net/article/details/127932711?spm=1001.2101.3001.6650.1&utm_medium=distribute.pc_relevant.none-task-blog-2~default~CTRLIST~Rate-1-127932711-blog-122311325.235%5Ev28%5Epc_relevant_default&depth_1-utm_source=distribute.pc_relevant.none-task-blog-2~default~CTRLIST~Rate-1-127932711-blog-122311325.235%5Ev28%5Epc_relevant_default&utm_relevant_index=2">Docker离线安装</a></p><p><a href="https://blog.csdn.net/m0_61035257/article/details/125705400">Java安装</a></p><p><a href="https://blog.csdn.net/BodyandSoul/article/details/121297563">Redis离线安装</a></p><p><a href="https://blog.csdn.net/gogospecter/article/details/149069779">离线安装nginx</a></p>]]></content:encoded>
            <category domain="https://yww52.com/categories/%25E6%2588%2591%25E7%259A%2584%25E6%258A%2580%25E6%259C%25AF%25E5%25AD%25A6%25E4%25B9%25A0%25E7%2589%25A9%25E8%25AF%25AD%25E6%259E%259C%25E7%2584%25B6%25E6%259C%2589%25E9%2597%25AE%25E9%25A2%2598/">我的技术学习物语果然有问题</category>
        </item>
        <item>
            <title>MybatisPlus常用方法</title>
            <link>https://yww52.com/posts/4e5ad84e/</link>
            <guid isPermaLink="true">https://yww52.com/posts/4e5ad84e/</guid>
            <pubDate>Thu, 02 Feb 2023 00:00:00 +0000</pubDate>
            <description>一些MybatisPlus常见的使用方法。</description>
            <content:encoded><![CDATA[<img src="https://img.yww52.com/2023/2/2023-2-2top_img.jpg" alt="MybatisPlus常用方法" style="max-width:100%;height:auto;margin-bottom:15px;" /><h1>简介</h1><p>记录一下常用的一些使用方法。</p><h1>测试环境</h1><h2>表SQL</h2><pre><code class="language-sql">DROP TABLE IF EXISTS `user`;CREATE TABLE `user` (    `id`            CHAR(19)        NOT NULL                        COMMENT &#39;数据ID&#39;,  `username`      VARCHAR(200)    NOT NULL                        COMMENT &#39;用户名&#39;,  `password`      VARCHAR(200)    NOT NULL                        COMMENT &#39;密码&#39;,  `nickname`      VARCHAR(200)    NULL    DEFAULT &#39;用户昵称&#39;       COMMENT &#39;用户昵称&#39;,  `avatar`        VARCHAR(200)    NULL    DEFAULT &#39;&#39;              COMMENT &#39;用户头像地址&#39;,  `email`         VARCHAR(200)    NULL    DEFAULT &#39;123456@qq.com&#39; COMMENT &#39;用户邮箱地址&#39;,  `status`        BIT DEFAULT 1   NOT NULL                        COMMENT &#39;账号状态,0禁用|1正常&#39;,    `sex`        BIT DEFAULT 1   NOT NULL                        COMMENT &#39;性别,0是女|1是男&#39;,    `phone`         VARCHAR(200)    NULL    DEFAULT &#39;18888888888&#39; COMMENT &#39;用户邮箱地址&#39;,    `create_time`   DATETIME        NOT NULL                        COMMENT &#39;创建时间&#39;,    `create_by`     VARCHAR(200)    NOT NULL                        COMMENT &#39;创建人&#39;,    `update_time`   DATETIME        NOT NULL                        COMMENT &#39;更新时间&#39;,    `update_by`     VARCHAR(200)    NOT NULL                        COMMENT &#39;更新人&#39;,    PRIMARY KEY (`id`),    CONSTRAINT  username unique (username)  ) ENGINE = InnoDB DEFAULT CHARSET = utf8mb4 COMMENT = &#39;用户信息实体类&#39;;</code></pre><h2>实体类</h2><pre><code class="language-Java">package com.yww.demo.entity;import com.baomidou.mybatisplus.annotation.*;import io.swagger.v3.oas.annotations.media.Schema;import lombok.AllArgsConstructor;import lombok.Builder;import lombok.Data;import lombok.NoArgsConstructor;import java.io.Serializable;import java.time.LocalDateTime;/** * &lt;p&gt; *      (user)用户信息实体类 * &lt;/p&gt; * * @Author yww * @Date 2023-2-20 */@Data@Builder@NoArgsConstructor@AllArgsConstructor@TableName(&quot;user&quot;)@Schema(name = &quot;User&quot;, description = &quot;用户信息实体类&quot;)public class User implements Serializable {    private static final long serialVersionUID = 1L;    @Schema(description = &quot;数据ID&quot;)    @TableId(value = &quot;id&quot;, type = IdType.ASSIGN_ID)    private String id;    @Schema(description = &quot;用户名&quot;)    @TableField(&quot;username&quot;)    private String username;    @Schema(description = &quot;密码&quot;)    @TableField(&quot;password&quot;)    private String password;    @Schema(description = &quot;用户昵称&quot;)    @TableField(&quot;nickname&quot;)    private String nickname;    @Schema(description = &quot;用户头像地址&quot;)    @TableField(&quot;avatar&quot;)    private String avatar;    @Schema(description = &quot;用户邮箱地址&quot;)    @TableField(&quot;email&quot;)    private String email;    @Schema(description = &quot;性别,0是女|1是男&quot;)    @TableField(&quot;sex&quot;)    private Boolean sex;    @Schema(description = &quot;电话号码&quot;)    @TableField(&quot;phone&quot;)    private String phone;    @Schema(description = &quot;账号状态,0禁用|1正常&quot;)    @TableField(&quot;status&quot;)    private Boolean status;    @Schema(description = &quot;创建时间&quot;)    @TableField(value = &quot;create_time&quot;, fill = FieldFill.INSERT)    private LocalDateTime createTime;    @Schema(description = &quot;创建人&quot;)    @TableField(value = &quot;create_by&quot;, fill = FieldFill.INSERT)    private String createBy;    @Schema(description = &quot;更新时间&quot;)    @TableField(value = &quot;update_time&quot;, fill = FieldFill.INSERT_UPDATE)    private LocalDateTime updateTime;    @Schema(description = &quot;更新人&quot;)    @TableField(value = &quot;update_by&quot;, fill = FieldFill.INSERT_UPDATE)    private String updateBy;}</code></pre><h2>执行SQL分析打印</h2><p>使用<code>p6spy</code>来打印SQL。</p><pre><code class="language-xml">        &lt;dependency&gt;            &lt;groupId&gt;p6spy&lt;/groupId&gt;            &lt;artifactId&gt;p6spy&lt;/artifactId&gt;            &lt;version&gt;${p6spy.version}&lt;/version&gt;        &lt;/dependency&gt;</code></pre><p>配置文件</p><pre><code class="language-yaml">  # 数据库配置(p6spy)  datasource:    driver-class-name: com.p6spy.engine.spy.P6SpyDriver    url: jdbc:p6spy:mysql://localhost:3306/demo?userUnicode=true&amp;useSSL=false&amp;characterEncoding=utf-8&amp;serverTimezone=UTC    username: root    password: password</code></pre><h2>假数据的生成</h2><p>这里使用了<code>datafaker</code>这个库来生成假数据。</p><pre><code class="language-xml">        &lt;dependency&gt;            &lt;groupId&gt;net.datafaker&lt;/groupId&gt;            &lt;artifactId&gt;datafaker&lt;/artifactId&gt;            &lt;version&gt;1.8.0&lt;/version&gt;        &lt;/dependency&gt;</code></pre><p>随机生成600条数据。</p><pre><code class="language-Java">    @Override    public void random(Integer number) {        Faker cnFaker = new Faker(new Locale(&quot;zh-CN&quot;));        Faker enFaker = new Faker(new Locale(&quot;en&quot;));        Random random = new Random();        for (int i = 0; i &lt; number; i++) {            User user = User.builder()                    .username(enFaker.name().username() + i)                    .password(enFaker.passport().valid())                    .nickname(cnFaker.name().name())                    .avatar(enFaker.avatar().image())                    .email(StrUtil.cleanBlank(enFaker.name().fullName().toLowerCase()) + &quot;@qq.com&quot;)                    .sex(random.nextBoolean())                    .phone(StrUtil.cleanBlank(cnFaker.phoneNumber().phoneNumber()))                    .status(true)                    .build();            this.baseMapper.insert(user);        }    }</code></pre><h2>代码结构</h2><p>环境使用<code>MybatisPlus</code>代码生成器的结构。</p><blockquote><ol><li>service继承IService</li><li>serviceImpl继承ServiceImpl，实现service</li><li>mapper继承BaseMapper</li></ol></blockquote><h1>补充说明</h1><h2>关于服务实现类的API</h2><p>根据上述的代码结构，在服务的实现类中，会有两套API。</p><ol><li>Service CRUD接口</li><li>Mapper CRUD接口</li></ol><p>前者算是对Mapper接口的封装，所以前者会有Mapper接口里基本所有方法，也会添加很多封装后的方法。后者是基础的Mapper使用。</p><p>所以以下大多数使用都是基于前者，当有用到Mapper里的方法才会使用后者。</p><h2>关于条件构造器</h2><p>这里以查询的条件构造器为例子。</p><h3>QueryWrapper</h3><pre><code class="language-Java">        QueryWrapper&lt;User&gt; queryWrapper = new QueryWrapper&lt;&gt;();        queryWrapper.eq(&quot;id&quot;, 1);        this.getOne(queryWrapper);</code></pre><h3>LambdaQueryWrapper</h3><p>这个条件构造器使用了<code>Lambda</code>语法，使用起来更方便，而且使用这个方法不用构造条件，不用填写表的字段，因为字段这种不能修改的对开发来说这是十分不友好的，<code>LambdaQueryWrapper</code>可以直接使用实体类属性来构造条件。</p><ol><li><p>使用<code>QueryWrapper</code>的<code>lambda</code>方法获取</p><pre><code class="language-Java">        QueryWrapper&lt;User&gt; queryWrapper = new QueryWrapper&lt;&gt;();        queryWrapper.eq(&quot;id&quot;, 1);        LambdaQueryWrapper&lt;User&gt; lambda = queryWrapper.lambda();        this.getOne(lambda);</code></pre></li><li><p>使用<code>Wrappers.lambdaQuery()</code>获取（推荐）</p><pre><code class="language-Java">        LambdaQueryWrapper&lt;User&gt; lambda = Wrappers.lambdaQuery(User.builder().id(&quot;1&quot;).build());        this.getOne(lambda);</code></pre></li></ol><p>以下的条件构造器，都是基于第二种方法生成。</p><h2>防全表更新与删除插件</h2><p>这个插件我感觉还是很有必要的，要是开发出现一点错误，这个插件能多一份保障。当出现全表更新或者是全表删除的SQL时，<code>MybatisPlus</code>能帮我们拦截，并抛出异常。</p><p><a href="https://baomidou.com/pages/c571bc/#blockattackinnerinterceptor">官方插件说明</a></p><pre><code class="language-Java">    /**     * 插件配置     */    @Bean    public MybatisPlusInterceptor mybatisPlusInterceptor() {        MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor();        // 防全表更新与删除插件        interceptor.addInnerInterceptor(new BlockAttackInnerInterceptor());        return interceptor;    }</code></pre><h2>分页插件</h2><p>分页时需要用到的插件。</p><p><a href="https://baomidou.com/pages/97710a/#paginationinnerinterceptor">官方插件说明</a></p><pre><code class="language-Java">/**     * 插件配置     */    @Bean    public MybatisPlusInterceptor mybatisPlusInterceptor() {        MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor();        // 分页插件，指定数据库为MYSQL        interceptor.addInnerInterceptor(new PaginationInnerInterceptor(DbType.MYSQL));        return interceptor;    }</code></pre><blockquote><p>多个插件使用的情况，请将分页插件放到 <code>插件执行链</code> 最后面。</p></blockquote><h1>添加</h1><h2>添加单条数据</h2><pre><code class="language-Java">    @Override    public boolean insert(User user) {        User insertUser = User.builder()                .username(user.getUsername())                .password(user.getPassword())                .nickname(user.getNickname())                .avatar(user.getAvatar())                .email(user.getEmail())                .sex(user.getSex())                .phone(user.getPhone())                .status(user.getStatus())                .build();        // 可以进行一些操作校验数据，比如说username是唯一的等等        return this.save(insertUser);    }</code></pre><blockquote><ol><li>添加的数据尽量自己先提取在添加，避免添加了不该添加的数据，比如说ID，不建议传入ID，更新用户这些字段，尽量避免这些脏数据。</li><li>考虑添加数据的条件，比如<code>username</code>是唯一的，某些数据是不为空的，或者某些数据是有什么格式的，都可以先将提取的元素进行判断后在添加。</li><li>只通过前端校验参数是不合理的。</li></ol></blockquote><h2>批量添加</h2><p>这里就不从请求上输入用户列表了，通过<code>faker</code>模拟生成用户列表。</p><pre><code class="language-java">    /**     * 随机生成number条用户数据     *     * @param number    指定数量     * @return          用户数据列表     */    private List&lt;User&gt; getUsers(int number) {        Faker cnFaker = new Faker(new Locale(&quot;zh-CN&quot;));        Faker enFaker = new Faker(new Locale(&quot;en&quot;));        Random random = new Random();        List&lt;User&gt; userList = new ArrayList&lt;&gt;(number);        for (int i = 0; i &lt; number; i++) {            User user = User.builder()                    .username(enFaker.name().username() + i)                    .password(enFaker.passport().valid())                    .nickname(cnFaker.name().name())                    .avatar(enFaker.avatar().image())                    .email(StrUtil.cleanBlank(enFaker.name().fullName().toLowerCase()) + &quot;@qq.com&quot;)                    .sex(random.nextBoolean())                    .phone(StrUtil.cleanBlank(cnFaker.phoneNumber().phoneNumber()))                    .status(true)                    .build();            userList.add(user);        }        return userList;    }</code></pre><h3>for循环添加</h3><p>这是不推荐的方法，但也算是可行的一种方法，这种方法效率很低。</p><pre><code class="language-Java">    @Override@Transactional(rollbackFor = Exception.class)    public boolean insertBatch1(List&lt;User&gt; userList) {        userList = getUsers(10);        boolean res = true;        for (User user : userList) {            res = res &amp;&amp; this.insert(user);        }        return res;    }</code></pre><p>通过<code>p6spy</code>的打印，可以看到执行的其中一条插入语句。</p><pre><code> Consume Time：1 ms 2023-02-22 15:30:30 Execute SQL：INSERT INTO user ( id, username, password, nickname, avatar, email, sex, phone, status, create_time, create_by, update_time, update_by ) VALUES ( &#39;1628296156785729537&#39;, &#39;lonnie.walsh0&#39;, &#39;X75170066&#39;, &#39;弓远航&#39;, &#39;https://robohash.org/nnawetiq.png&#39;, &#39;mr.brandonthiel@qq.com&#39;, true, &#39;15024489626&#39;, true, &#39;2023-02-22T15:30:30.725&#39;, &#39;yww&#39;, &#39;2023-02-22T15:30:30.725&#39;, &#39;yww&#39; )</code></pre><h3>saveBatch的伪批量插入</h3><p>使用saveBatch最好先通过<code>@EnableTransactionManagement</code>注解开启<code>mybatis-plus</code>的事务管理，不然可能会出现警告。（虽然可能不会影响操作）</p><pre><code class="language-Java">    @Override    public boolean insertBatch2(List&lt;User&gt; userList) {        userList = getUsers(10);        return this.saveBatch(userList);    }</code></pre><p>通过<code>p6spy</code>的打印，可以看到执行的其中一条插入语句。</p><pre><code> Consume Time：0 ms 2023-02-22 15:32:05 Execute SQL：INSERT INTO user ( id, username, password, nickname, avatar, email, sex, phone, status, create_time, create_by, update_time, update_by ) VALUES ( &#39;1628296554896482305&#39;, &#39;aline.aufderhar0&#39;, &#39;296752366&#39;, &#39;商鑫鹏&#39;, &#39;https://robohash.org/zmvciuuf.png&#39;, &#39;randalwolfiii@qq.com&#39;, false, &#39;15067200564&#39;, true, &#39;2023-02-22T15:32:05.643&#39;, &#39;yww&#39;, &#39;2023-02-22T15:32:05.643&#39;, &#39;yww&#39; )</code></pre><blockquote><p>这里可以发现saveBatch的插入语句和for循环的插入语句是一样的，都是有10条INSERT语句，而不是真正的批量插入，只不过saveBatch是10条SQL语句一起提交，而for循环是10条语句10次提交，这里的效率差距还是很明显的，但由于这样也不算是批量插入，所以saveBatch被称为伪批量插入。</p></blockquote><p>由于是一次提交，如果一次性插入成千上万条数据，也会导致数据库响应缓慢，所以当插入的数据量很大时，还是建议先分片，在提交。</p><pre><code class="language-Java">    @Override    public boolean insertBatch2(List&lt;User&gt; userList) {        userList = UserUtil.getUsers(10);        // 按每次500条数据进行插入        return saveBatch(userList, 500);    }</code></pre><h3>批量插入</h3><p>这种方案效率是最高的，但是相应的需要去配置一下。</p><ol><li><p>创建批量插入SQL注入器</p><pre><code class="language-Java">/** * &lt;p&gt; *      批量插入SQL注入器 * &lt;/p&gt; * * @author yww * @since 2023/2/22 15:53 */public class InsertBatchSqlInjector extends DefaultSqlInjector {    @Override    public List&lt;AbstractMethod&gt; getMethodList(Class&lt;?&gt; mapperClass, TableInfo tableInfo) {        // 获取MybatisPlus的自带方法        List&lt;AbstractMethod&gt; methodList = super.getMethodList(mapperClass, tableInfo);        // 添加自定义批量插入方法，名称为insertBatchSomeColumn        methodList.add(new InsertBatchSomeColumn());        return methodList;    }}</code></pre></li><li><p>将SQL注入器添加到<code>mybatis-plus</code>配置中</p><pre><code class="language-Java">@Configuration@EnableTransactionManagement@MapperScan(&quot;com.yww.demo.mapper&quot;)public class MybatisPlusConfig {    /**     * 自定义批量插入 SQL 注入器     */    @Bean    public InsertBatchSqlInjector insertBatchSqlInjector() {        return new InsertBatchSqlInjector();    }}</code></pre></li><li><p>在Mapper类中添加方法</p><pre><code class="language-Java">@Repositorypublic interface UserMapper extends BaseMapper&lt;User&gt; {    /**     * 批量插入 仅适用于mysql     * @param batchList     实体列表     * @return              影响行数     */    @SuppressWarnings(&quot;MybatisMapperMethodInspection&quot;)    int insertBatchSomeColumn(@Param(&quot;list&quot;) List&lt;User&gt; batchList);}</code></pre></li></ol><blockquote><p>注意，insertBatchSomeColumn为内部指定的方法名字，不用在xml中写具体的SQL语句。IDE的警告可以忽略，上述添加的SQL注入器会帮我们注入具体的SQL语句，当然不用这么麻烦的配置，自己去xml中拼接SQL也是可以的。</p></blockquote><p>可以看看官方的一个方法解释。</p><pre><code class="language-Java">/** * 批量新增数据,自选字段 insert * &lt;p&gt; 不同的数据库支持度不一样!!!  只在 mysql 下测试过!!!  只在 mysql 下测试过!!!  只在 mysql 下测试过!!! &lt;/p&gt; * &lt;p&gt; 除了主键是 &lt;strong&gt; 数据库自增的未测试 &lt;/strong&gt; 外理论上都可以使用!!! &lt;/p&gt; * &lt;p&gt; 如果你使用自增有报错或主键值无法回写到entity,就不要跑来问为什么了,因为我也不知道!!! &lt;/p&gt; * &lt;p&gt; * 自己的通用 mapper 如下使用: * &lt;pre&gt; * int insertBatchSomeColumn(List&lt;T&gt; entityList); * &lt;/pre&gt; * &lt;/p&gt; * * &lt;li&gt; 注意: 这是自选字段 insert !!,如果个别字段在 entity 里为 null 但是数据库中有配置默认值, insert 后数据库字段是为 null 而不是默认值 &lt;/li&gt; * * &lt;p&gt; * 常用的 {@link Predicate}: * &lt;/p&gt; * * &lt;li&gt; 例1: t -&gt; !t.isLogicDelete() , 表示不要逻辑删除字段 &lt;/li&gt; * &lt;li&gt; 例2: t -&gt; !t.getProperty().equals(&quot;version&quot;) , 表示不要字段名为 version 的字段 &lt;/li&gt; * &lt;li&gt; 例3: t -&gt; t.getFieldFill() != FieldFill.UPDATE) , 表示不要填充策略为 UPDATE 的字段 &lt;/li&gt; * * @author miemie * @since 2018-11-29 */</code></pre><p>具体的使用。</p><pre><code class="language-java">    @Override    public int insertBatch3(List&lt;User&gt; userList) {        userList = getUsers(10);        return this.baseMapper.insertBatchSomeColumn(userList);    }</code></pre><p>SQL打印为</p><pre><code> Consume Time：3 ms 2023-02-22 16:04:37 Execute SQL：INSERT INTO user (id,username,password,nickname,avatar,email,sex,phone,status,create_time,create_by,update_time,update_by) VALUES (&#39;1628304743473872898&#39;,&#39;tod.jacobs0&#39;,&#39;Y19482857&#39;,&#39;房晟睿&#39;,&#39;https://robohash.org/fmcgfafy.png&#39;,&#39;marileemacejkovic@qq.com&#39;,false,&#39;13069884517&#39;,true,&#39;2023-02-22T16:04:37.951&#39;,&#39;yww&#39;,&#39;2023-02-22T16:04:37.951&#39;,&#39;yww&#39;) , (&#39;1628304743473872899&#39;,&#39;leo.wilderman1&#39;,&#39;X45343338&#39;,&#39;庾瑞霖&#39;,&#39;https://robohash.org/calkekge.png&#39;,&#39;jefferyarmstrong@qq.com&#39;,true,&#39;18603800047&#39;,true,&#39;2023-02-22T16:04:37.952&#39;,&#39;yww&#39;,&#39;2023-02-22T16:04:37.952&#39;,&#39;yww&#39;) , (&#39;1628304743473872900&#39;,&#39;dorris.kutch2&#39;,&#39;Y41776964&#39;,&#39;胥擎宇&#39;,&#39;https://robohash.org/cbrcnhor.png&#39;,&#39;darwindickinson@qq.com&#39;,false,&#39;15855129426&#39;,true,&#39;2023-02-22T16:04:37.952&#39;,&#39;yww&#39;,&#39;2023-02-22T16:04:37.952&#39;,&#39;yww&#39;) , (&#39;1628304743473872901&#39;,&#39;julio.dubuque3&#39;,&#39;529376562&#39;,&#39;辛鸿煊&#39;,&#39;https://robohash.org/atjuqhwo.png&#39;,&#39;annemariejerde@qq.com&#39;,true,&#39;13764269293&#39;,true,&#39;2023-02-22T16:04:37.952&#39;,&#39;yww&#39;,&#39;2023-02-22T16:04:37.952&#39;,&#39;yww&#39;) , (&#39;1628304743473872902&#39;,&#39;alvera.gerlach4&#39;,&#39;Z05050838&#39;,&#39;骆泽洋&#39;,&#39;https://robohash.org/kttvknqi.png&#39;,&#39;vincenzoortiz@qq.com&#39;,false,&#39;18764984449&#39;,true,&#39;2023-02-22T16:04:37.952&#39;,&#39;yww&#39;,&#39;2023-02-22T16:04:37.952&#39;,&#39;yww&#39;) , (&#39;1628304743473872903&#39;,&#39;robert.walter5&#39;,&#39;Z47884351&#39;,&#39;茅志强&#39;,&#39;https://robohash.org/gigofxue.png&#39;,&#39;missjosphinemarquardt@qq.com&#39;,false,&#39;16550365693&#39;,true,&#39;2023-02-22T16:04:37.952&#39;,&#39;yww&#39;,&#39;2023-02-22T16:04:37.952&#39;,&#39;yww&#39;) , (&#39;1628304743473872904&#39;,&#39;curtis.considine6&#39;,&#39;X36270777&#39;,&#39;满明哲&#39;,&#39;https://robohash.org/zkakbsnh.png&#39;,&#39;mitzidaugherty@qq.com&#39;,false,&#39;18605996014&#39;,true,&#39;2023-02-22T16:04:37.952&#39;,&#39;yww&#39;,&#39;2023-02-22T16:04:37.952&#39;,&#39;yww&#39;) , (&#39;1628304743473872905&#39;,&#39;stewart.mayert7&#39;,&#39;Z27480881&#39;,&#39;舒文轩&#39;,&#39;https://robohash.org/wnmekekg.png&#39;,&#39;pierreconsidine@qq.com&#39;,false,&#39;13988711400&#39;,true,&#39;2023-02-22T16:04:37.952&#39;,&#39;yww&#39;,&#39;2023-02-22T16:04:37.952&#39;,&#39;yww&#39;) , (&#39;1628304743473872906&#39;,&#39;winnie.mills8&#39;,&#39;Z66334442&#39;,&#39;刘擎苍&#39;,&#39;https://robohash.org/snbyikas.png&#39;,&#39;taneshapagac@qq.com&#39;,true,&#39;19603769365&#39;,true,&#39;2023-02-22T16:04:37.952&#39;,&#39;yww&#39;,&#39;2023-02-22T16:04:37.952&#39;,&#39;yww&#39;) , (&#39;1628304743473872907&#39;,&#39;lanita.walker9&#39;,&#39;Y43021930&#39;,&#39;边鸿煊&#39;,&#39;https://robohash.org/wgypkmjj.png&#39;,&#39;dorseyrath@qq.com&#39;,true,&#39;17297354287&#39;,true,&#39;2023-02-22T16:04:37.952&#39;,&#39;yww&#39;,&#39;2023-02-22T16:04:37.952&#39;,&#39;yww&#39;)</code></pre><blockquote><p>可以看到这个SQL的格式为</p><p>INSERT INTO user (..) VALUES (...),(...),(...),(...)</p><p>这种格式才算是批量插入，而且效率会高上很多。</p></blockquote><p>同理，一次性不建议插入太多条数据，所以还是需要分片。<code>insertBatchSomeColumn</code>这个mapper方法我找了一下源码，好像没见到自带的分片方法，所以需要手动分片然后进行插入。操作如下。</p><pre><code class="language-Java">    @Override@Transactional(rollbackFor = Exception.class)    public int insertBatch3(List&lt;User&gt; userList) {        userList = UserUtil.getUsers(10);        // 按每次500进行插入        List&lt;List&lt;User&gt;&gt; list = ListUtil.partition(userList, 500);        int res = 0;        for (List&lt;User&gt; i : list) {            res += this.baseMapper.insertBatchSomeColumn(i);        }        return res;    }</code></pre><h3>总结</h3><p>使用第三种发放是效率最高的，所以建议使用第三种，当然嫌麻烦配置，直接用第二种也可以。毕竟数据量不是特别大，差距可以忽略。</p><blockquote><p>尽量不要使用for循环的操作进行数据批量操作，消耗的时间是很多的。</p><p>所以之后的批量操作，就不记录使用for循环的方案了。</p></blockquote><h1>删除</h1><h2>根据ID删除单条数据</h2><p>直接调用API即可。</p><pre><code class="language-Java">    @Override    public boolean deleteById(String userId) {        // 有必要的话可以先查出数据，进行处理后在删除        // entity = select(userId);   this.removeById(entity)        this.removeById(userId);        return false;    }</code></pre><h2>根据其他条件删除</h2><pre><code class="language-java">    @Override    public boolean deleteByCondition(User user) {        return this.remove(Wrappers.lambdaQuery(user));    }</code></pre><blockquote><p>这个方法，这里只是用来记录一下，我感觉这个方法十分的危险，实际开发建议不要这样写，当user为空，或者是里面的字段全是空值，就会导致语句变成</p><p>DELETE FROM user</p><p>这样的SQL会导致全表数据删除，所以尽量使用ID进行删除。</p><p>实在需要用到这种方法，请先判定传入的实体对象条件不为空，而且里面的属性不能全部为空，避免出现全表删除的情况。</p><p>或者可以参考补充说明，添加防全表更新与删除插件的插件。</p></blockquote><h2>根据ID列表批量删除</h2><p>这里有两个方法，对应的SQL语句不太一样。</p><h3>removeByIds方法</h3><p>直接调用API即可。</p><pre><code class="language-Java">    @Override    public boolean deleteByIds(List&lt;String&gt; userIds) {        return this.removeByIds(userIds);    }</code></pre><p>对应的SQL语句如下。</p><pre><code> Consume Time：0 ms 2023-02-23 17:07:41 Execute SQL：DELETE FROM user WHERE id IN ( &#39;1&#39; , &#39;2&#39; , &#39;3&#39; , &#39;4&#39; )</code></pre><p>同样，尽量对ID列表分片，不能一次性删除太多数据。</p><pre><code class="language-Java">   @Override@Transactional(rollbackFor = Exception.class)    public boolean deleteByIds(List&lt;String&gt; userIds) {        // 按每次500条数据进行操作        List&lt;List&lt;String&gt;&gt; list = ListUtil.partition(userIds, 1000);        boolean res = true;        for (List&lt;String&gt; i : list) {            res = res &amp;&amp; this.removeByIds(i);        }        return this.removeByIds(userIds);    }</code></pre><h3>removeBatchByIds方法</h3><p>直接调用API即可。</p><pre><code class="language-Java">    @Override    public boolean deleteBatchByIds(List&lt;String&gt; userIds) {        // 每次最多进行1000条数据删除        return this.removeBatchByIds(userIds, 1000);    }</code></pre><p>对应的SQL语句如下。</p><pre><code> Consume Time：1 ms 2023-02-23 17:11:09 Execute SQL：DELETE FROM user WHERE id=&#39;1&#39;  Consume Time：0 ms 2023-02-23 17:11:09 Execute SQL：DELETE FROM user WHERE id=&#39;2&#39;  Consume Time：0 ms 2023-02-23 17:11:09 Execute SQL：DELETE FROM user WHERE id=&#39;3&#39;  Consume Time：0 ms 2023-02-23 17:11:09 Execute SQL：DELETE FROM user WHERE id=&#39;4&#39;</code></pre><p>这个有点类似与<code>saveBatch</code>的操作，多次删除一次提交。</p><h3>总结</h3><p>这两种方法我进行了简单的测试。测试结果打印如下。</p><pre><code>com.yww.demo.util.TestController         : 此处测试的两种方法，测试删除的数据数量为10600com.yww.demo.util.TestController         : deleteByIds测试时间为1349毫秒com.yww.demo.util.TestController         : deleteBatchByIds测试时间为12956毫秒</code></pre><blockquote><p>这两个差距还算很明显的了。测试中每次删除1000条数据，要是按更大的长度进行分片，我觉得这两种的方法差距会更加的大，所以尽量使用<code>removeByIds</code>这种方法。</p></blockquote><h2>根据其他条件批量删除</h2><p>这里以用户名举例，即传入用户名列表，批量进行删除。</p><p>由于<code>MybatisPlus</code>没有对应的API，所以需要手动去编写Mapper。</p><pre><code class="language-Java">    @Override    @Transactional(rollbackFor = Exception.class)    public int deleteBatchByUsernames(List&lt;String&gt; usernames) {        // 按每次1000条数据进行操作        List&lt;List&lt;String&gt;&gt; list = ListUtil.partition(usernames, 1000);        int count = 0;        for (List&lt;String&gt; i : list) {            count = count + baseMapper.deleteBatchByUsernames(i);        }        return count;    }</code></pre><p><code>deleteBatchByUsernames</code>方法的mapper可以参考批量删除ID的方法进行编写，这里就模仿<code>removeByIds</code>对应的SQL进行编写。</p><pre><code class="language-xml">    &lt;delete id=&quot;deleteBatchByUsernames&quot; parameterType=&quot;java.util.List&quot;&gt;        DELETE FROM user WHERE username in        &lt;foreach collection=&quot;usernames&quot; index=&quot;index&quot; item=&quot;username&quot; separator=&quot;,&quot; open=&quot;(&quot; close=&quot;)&quot; &gt;            #{username}        &lt;/foreach&gt;    &lt;/delete&gt;</code></pre><h1>更新</h1><h2>根据ID更新数据</h2><p>直接调用API即可。</p><pre><code class="language-Java">    @Operation(summary = &quot;根据ID更新数据&quot;)    @PutMapping(&quot;/updateById&quot;)    public Result&lt;?&gt; updateById(@RequestBody User user) {        if (service.updateById(user)) {            return Result.success(&quot;删除成功&quot;);        } else {            return Result.failure(&quot;删除失败&quot;);        }    }</code></pre><h2>根据ID批量更新数据</h2><p>直接调用API即可。</p><pre><code class="language-Java">    @Operation(summary = &quot;根据ID批量更新数据&quot;)    @PutMapping(&quot;/updateByIds&quot;)    public Result&lt;?&gt; updateByIds(@RequestBody List&lt;User&gt; users) {        if (service.updateBatchById(users, 1000)) {            return Result.success(&quot;更新成功&quot;);        } else {            return Result.failure(&quot;更新失败&quot;);        }    }</code></pre><h2>使用UpdateWrapper更新</h2><p>使用<code>UpdateWrapper</code>的条件去定位数据，更新条件需要设置sqlset。</p><p>以下是个简单的例子。</p><pre><code class="language-Java">    @Override    public boolean update1(User user) {        // 将名字设置为WHERE条件，将状态设置为SET语句        return this.update(            Wrappers.lambdaUpdate(User.class).eq(User::getNickname, user.getNickname())                    .set(User::getStatus, user.getStatus())        );    }</code></pre><h2>使用Wrapper更新</h2><p>以下是个简单的例子。</p><pre><code class="language-Java">    @Override    public boolean update2(User user) {        return this.update(user,                 Wrappers.lambdaQuery(User.class).eq(User::getNickname, user.getNickname())        );    }</code></pre><h2>saveOrUpdate</h2><p>这个方法是<code>MybatisPlus</code>里的一个方法，顾名思义，保存或者是更新，先去数据库根据ID查询是否存在该条数据，没有就插入数据，有的话就更新数据。大致用法和update或者是save差不多。</p><pre><code class="language-Java">// TableId 注解存在更新记录，否插入一条记录boolean saveOrUpdate(T entity);// 根据updateWrapper尝试更新，否继续执行saveOrUpdate(T)方法boolean saveOrUpdate(T entity, Wrapper&lt;T&gt; updateWrapper);// 批量修改插入boolean saveOrUpdateBatch(Collection&lt;T&gt; entityList);// 批量修改插入boolean saveOrUpdateBatch(Collection&lt;T&gt; entityList, int batchSize);</code></pre><blockquote><p>这方法确实也挺好用的，不过是更新和插入的混合方法，有点违和就是了，所以这里就不怎么探讨了。</p></blockquote><h1>查询</h1><h2>根据ID查询数据</h2><pre><code class="language-Java">    @Operation(summary = &quot;根据ID获取数据&quot;)    @GetMapping(&quot;/getById/{userId}&quot;)    public Result&lt;?&gt; getById(@PathVariable Integer userId) {        return Result.success(service.getById(userId));    }</code></pre><h2>根据条件查询单条数据</h2><pre><code class="language-Java">    @Override    public User getByUsername(String username) {        return this.getOne(                Wrappers.lambdaQuery(User.class).eq(User::getUsername, username)        );    }</code></pre><blockquote><p>这个方法只会返回一条数据，如果查询出多条数据会抛出异常，若是希望查出多条数据不抛出异常，可以自行设置。</p><p><code>getOne(queryWrapper, false)</code></p></blockquote><h2>根据条件查询数据</h2><p>假设根据用户名和昵称进行查询。</p><pre><code class="language-Java">    @Override    public List&lt;User&gt; getByUser(User user) {        // this.list() 查询所有数据        return this.list(                Wrappers.lambdaQuery(User.class).eq(User::getUsername, user.getUsername()).eq(User::getNickname, user.getNickname())        );    }</code></pre><h2>根据ID批量查询</h2><pre><code class="language-Java">    @Operation(summary = &quot;根据ID批量查询&quot;)    @GetMapping(&quot;/getByIds&quot;)    public Result&lt;?&gt; getByUser(@RequestBody List&lt;String&gt; userIds) {        return Result.success(service.listByIds(userIds));    }</code></pre><h2>根据其他条件批量查询</h2><p>这里需要自己手写SQL实现才行了，SQL语句和根据其他条件批量删除的差不多，也是通过<code>IN</code>关键字拼接实现。</p><pre><code class="language-java">    /**     * 批量根据用户名批量删除     *     * @param usernames 用户名列表     * @return 查询到的用户列表     */    List&lt;User&gt; listByUserNames(@Param(&quot;usernames&quot;) List&lt;String&gt; usernames);</code></pre><pre><code class="language-xml">    &lt;select id=&quot;listByUserNames&quot; resultType=&quot;com.yww.demo.entity.User&quot;&gt;        SELECT * FROM user WHERE username IN        &lt;foreach collection=&quot;usernames&quot; index=&quot;index&quot; item=&quot;username&quot; separator=&quot;,&quot; open=&quot;(&quot; close=&quot;)&quot; &gt;            #{username}        &lt;/foreach&gt;    &lt;/select&gt;</code></pre><h2>简单分页查询</h2><p><code>MybatisPlus</code>提供了一个分页插件，需要先去配置，配置参考上述的补充说明。</p><p><code>MybatisPlus</code>的分页方法需要<code>IPage</code>这分页模型，可以自己去实现这个分页模型，也可以使用官方提供的<code>Page</code>类（<code>Page</code>继承了<code>IPage</code>，实现了一个简单的分页模型）。<code>Page</code>的对象创建可以直接使用其<code>Page.of</code>方法。</p><pre><code class="language-Java">@Operation(summary = &quot;简单分页查询&quot;)@GetMapping(&quot;/page/{current}/{size}&quot;)public Result&lt;?&gt; page(@PathVariable Integer current,                      @PathVariable Integer size) {    Page&lt;User&gt; ipage = Page.of(current, size);    return Result.success(service.page(ipage));}</code></pre><blockquote><p>注意，<code>MybatisPlus</code>的分页方法是会先查询所有数据的数量，然后才会进行<code>LIMIT</code>分页，也就是说会有两条SQL语句，从打印的SQL就可以看到。</p><p> Consume Time：19 ms Execute SQL：SELECT COUNT(*) AS total FROM user</p><p> Consume Time：3 ms Execute SQL：SELECT id,username,password,nickname,avatar,email,sex,phone,status,create_time,create_by,update_time,update_by FROM user LIMIT 10</p><p>如果用不到总记录数，不想执行<code>count</code>查询，可以通过<code>Page.of(current, size, false)</code>方法去关掉。</p></blockquote><h2>根据条件分页查询</h2><p>根据条件分页查询，其实只需要添加一个条件构造器即可。</p><p>这里拿性别举例，分页查询性别为男的数据。</p><pre><code class="language-Java">    @Operation(summary = &quot;根据条件分页查询（查询性别）&quot;)    @GetMapping(&quot;/page/{current}/{size}/{sex}&quot;)    public Result&lt;?&gt; page(@PathVariable Integer current,                          @PathVariable Integer size,                          @PathVariable Boolean sex) {        Page&lt;User&gt; ipage = Page.of(current, size);        LambdaQueryWrapper&lt;User&gt; wrapper = Wrappers.lambdaQuery(User.builder().sex(sex).build());        return Result.success(service.page(ipage, wrapper));    }</code></pre><h1>篇末</h1><p>这篇文章主要是介绍一些<code>MybatisPlus</code>的API吧，其实刚刚开始写的时候，是想记录一些常用的用法的，但是后面写着写着，发现其实都是在写官方的API如何使用（确实这个项目考虑的很周全）。后来又怕别人看得更明白些，又写了很多介绍类和使用的注意事项。emm，怎么说的，是写的有些乱了，本来就是给自己看到，写写感觉就偏题了hh，不过还可以吧，将就看一下。</p>]]></content:encoded>
            <category domain="https://yww52.com/categories/%25E6%2588%2591%25E7%259A%2584%25E6%258A%2580%25E6%259C%25AF%25E5%25AD%25A6%25E4%25B9%25A0%25E7%2589%25A9%25E8%25AF%25AD%25E6%259E%259C%25E7%2584%25B6%25E6%259C%2589%25E9%2597%25AE%25E9%25A2%2598/">我的技术学习物语果然有问题</category>
        </item>
        <item>
            <title>2022年度总结</title>
            <link>https://yww52.com/posts/622a5765/</link>
            <guid isPermaLink="true">https://yww52.com/posts/622a5765/</guid>
            <pubDate>Sun, 01 Jan 2023 00:00:00 +0000</pubDate>
            <description>2022年度总结</description>
            <content:encoded><![CDATA[<img src="https://img.yww52.com/2023/1/2023-1-1/top_img.jpg" alt="2022年度总结" style="max-width:100%;height:auto;margin-bottom:15px;" /><h1>简介</h1><p>​刚打完游戏，发现2022年已经过去了。正好我这夜猫子也睡不着，就赶着2023年年初，来写了这篇年度总结。</p><blockquote><p>为什么2021年没有写？其实2022年1月1日的时候，我也是刚和朋友打完游戏，也差不多这个点，我已经写了一篇2021年的年度总结，但是后面写完之后，不记得为什么就删掉了，没发出来，所以就没了，也不想重新写了。</p></blockquote><p>这次没有鸽，肯定写出来hh。</p><h1>生活</h1><p>​2022年最重要的事应该是我大学毕业了吧。毕业的时候原本想写个大学四年的总结的，后面想想算了，在大学里也没有什么特别的事情，像很多人一样，平平凡凡的上了个大学，平平凡凡的大学毕业。</p><p>​其实大三大四过得不是很好，成天感觉到很焦虑，我都感觉心理状态有些亚健康了。每天都是上课，考试，刷题，还有那毕设的破事，大学后面两年其实还是有些遗憾的。不过应该也是我太懒了，学习的心情也没有大一大二那两年这么积极了，每天看那两三小时题目，效率也不高，所以后面也没有什么好结果，有些摆烂了。</p><p>​所以毕业后，就疯玩了一两个月后才找了个班上。</p><h1>coding</h1><p>2022年前期毫无开发的心情，所以也没开发什么有趣，好用的东西。上班之后，感觉到很无聊，所以有了很多想做的东西，也捡起了很多之前想弄有没有弄好的东西，继续努力写吧。</p><p>其实有些项目拖了很久没写好（拖延症后期的人是这样的），总是想弄得最好，用很多很新的技术，有些时候也增加了很多学习成本，不过毕竟不是很急的东西，都是兴趣爱好，所以就拖着慢慢写了。</p><p>就像是这个博客，用着hexo来搭建的，也不是说不好用，但是总是想自己写一个，所以就会考虑UI设计，学习前端项目，想要搭建一个自己的服务器，总之很多需要考虑的东西，还有自己很懒，所以就写的很慢了。（我这不算找借口吧hhh）</p><blockquote><p>虽然工作也是coding，但是我不是很想写到这里，我还记得高中的时候和朋友讨论的一个话题，当爱好变成工作的时候，爱好还能一直是爱好吗？这个问题其实我回答不出，等我以后讨厌了，可能我才能回答得出来。所以我不是很喜欢把工作和爱好联系起来，工作是工作，爱好是爱好，所以不在这里讨论工作了。</p></blockquote><p>今年学了很多技术，比如</p><ol><li>springcloud</li><li>typescript</li><li>vue3</li><li>....</li></ol><p>也加深了很多东西的理解，有些东西还是在用到的时候，才会发现这其实有很多讲究，没有想象中的那么简单。</p><p>为了更好的学习，在打折的时候，我还买了很多相关的书。</p><ol><li>MySQL必知必会</li><li>漫画算法2</li><li>编码</li><li>图解Java多线程设计模式</li><li>Redis深度历险：核心原理与应用实践</li><li>现代操作系统</li><li>图解TCP/IP</li><li>labuladong的算法小抄</li><li>图解HTTP</li><li>操作系统导论</li><li>逆流而上</li><li>技术之瞳</li><li>深入理解Java虚拟机</li><li>Java并发之美</li><li>Java编程思想</li><li>重学Java设计模式</li><li>数据结构与算法之美</li></ol><p>其实这些书我都有电子版，不过买纸质版，也算另一种督促我看书的方式，毕竟不是花钱的电子书，就会看的没有那么认真了hh。</p><blockquote><p>别问看了多少，问就是正在看着，没全部看完。</p></blockquote><h1>游戏</h1><p>游戏方面的话，还是和之前一样，玩来玩去还是那几个，对很多游戏都不感兴趣，不过我觉得也够了。</p><ol><li>英雄联盟</li><li>APEX</li><li>星际争霸2</li><li>公主连结R</li></ol><p>移动端现在就只玩公主连结了，之前会坑了战双一段时间，但是后面没这么多精力又再次退坑了。我还是喜欢像公主连结这种养生的游戏，虽然公会战的几天还是挺累的，不过毕竟一个月一次，还算是能接受。</p><p>其实我不怎么玩手游了，确实缺了点意思，但是也不舍得公会里的大家，所以拖着拖着，公主连结也玩了两年多了。在群里看到很多人都退坑了，也还是挺失落的，不过这游戏确实也就这样了，祝各位有缘再见吧。</p><h1>关于博客</h1><p>大三大四后，博客确实写得少了，没有一开始这么积极了，我也看到了友联里的很多人，虽然都在维护博客，但是发表的文章也肉眼可见的变少，甚至有些都不发了。前些天在github上看到一个记录独立博客的项目，确实挺有感慨的，地址如下。</p><blockquote><p> <a href="https://github.com/timqian/chinese-independent-blogs">https://github.com/timqian/chinese-independent-blogs</a></p></blockquote><p>当初我折腾这个博客的时候花了很多心思，也买了很久<code>yww52.com</code>的域名，现在喜欢折腾博客的人确实变少了很多，也有点感概吧。</p><p>今年发现了一些人的博客，看到他们充满生活气息的文章，也让我产生写东西的感觉了。</p><p>今年写得很少，大多都是在总结用法，写得少的原因大概有几个。</p><ol><li>有些不知道写什么了。</li><li>写东西就会想写最好，所以花的时间也很多。</li><li>想放弃hexo这个博客框架了，自己写的博客也没写好。</li><li>确实事情比较多，耗精力。</li></ol><h1>APP年度总结</h1><p>每个APP都有年度总结，这里相当于记录一下吧。</p><h2>QQ音乐</h2><p>今年没有上一年听歌听得这么多了，我发现一旦听歌，精力就不行，我总会去注意歌词，做的事情也会效率很低，所以听的就没这么多了。又是真夜陪伴的一年hh。</p><p>:::gallery<img src="https://img.yww52.com/2023/1/2023-1-1/1.jpg" alt=""><img src="https://img.yww52.com/2023/1/2023-1-1/2.jpg" alt=""><img src="https://img.yww52.com/2023/1/2023-1-1/3.jpg" alt=""><img src="https://img.yww52.com/2023/1/2023-1-1/4.jpg" alt=""><img src="https://img.yww52.com/2023/1/2023-1-1/5.jpg" alt=""><img src="https://img.yww52.com/2023/1/2023-1-1/6.jpg" alt=""><img src="https://img.yww52.com/2023/1/2023-1-1/7.jpg" alt="">:::</p><h2>bilibili</h2><p>:::gallery<img src="https://img.yww52.com/2023/1/2023-1-1/8.jpg" alt=""><img src="https://img.yww52.com/2023/1/2023-1-1/9.jpg" alt=""><img src="https://img.yww52.com/2023/1/2023-1-1/10.jpg" alt=""><img src="https://img.yww52.com/2023/1/2023-1-1/11.jpg" alt=""><img src="https://img.yww52.com/2023/1/2023-1-1/12.jpg" alt=""><img src="https://img.yww52.com/2023/1/2023-1-1/13.jpg" alt=""><img src="https://img.yww52.com/2023/1/2023-1-1/14.jpg" alt=""><img src="https://img.yww52.com/2023/1/2023-1-1/15.jpg" alt=""><img src="https://img.yww52.com/2023/1/2023-1-1/16.jpg" alt=""><img src="https://img.yww52.com/2023/1/2023-1-1/17.jpg" alt="">:::</p><p>自从国家对动漫的政策变了之后，就很少在B站看番了，大多都是学习和游戏。</p><blockquote><p>看看那个虫族教程，我看了多少遍，谁说学习游戏不是学习，我还是很好学的hh，不得不说一句星际争霸真难玩。扯到星际争霸，就想到暴雪了，该死的暴雪，可惜了这么多好游戏。</p></blockquote><h1>2023</h1><p>2023年就希望自己多看点书，多学习一点东西，多做一点项目，多写点文章，多勤奋一点吧。也没有很大的目标，没有很大的期待，可能我就是一条咸鱼吧。</p>]]></content:encoded>
            <category domain="https://yww52.com/categories/%25E4%25B8%25AA%25E4%25BA%25BA%25E6%2580%25BB%25E7%25BB%2593/">个人总结</category>
        </item>
        <item>
            <title>SpringBoot部署启动</title>
            <link>https://yww52.com/posts/e31003e0/</link>
            <guid isPermaLink="true">https://yww52.com/posts/e31003e0/</guid>
            <pubDate>Wed, 28 Sep 2022 00:00:00 +0000</pubDate>
            <description>SpringBoot部署启动</description>
            <content:encoded><![CDATA[<img src="https://img.yww52.com/2022/09/2022-9-28top_img.jpg" alt="SpringBoot部署启动" style="max-width:100%;height:auto;margin-bottom:15px;" /><h1>序言</h1><p>在开发中，经常需要部署SpringBoot服务，所以来简单记录一下我比较常用的一些部署方式。</p><h1>简单粗暴的脚本</h1><p>这种方法其实就是直接使用<code>java -jar</code>命令进行启动Jar包，所以得需要系统安装好Java的环境，然后打包好服务</p><pre><code class="language-sh">#!/bin/bashJAVAHOME=$JAVA_HOMEecho &quot;--------start--------&quot;PID=$(ps -ef|grep app.jar  |grep -v grep|awk &#39;{print $2}&#39;)if [ ! $PID ]; then    nohup java -jar -XX:MetaspaceSize=128m -XX:MaxMetaspaceSize=256m -Xms512m -Xmn768m -Xmx1024m -Xss256k -XX:SurvivorRatio=8 -XX:+UseConcMarkSweepGC app.jar &gt; app.log 2&gt;&amp;1 &amp;    echo &quot;--------------start success-----------&quot;    exitelse    kill -9 ${PID}    echo &quot;--------------kill success-----------&quot;    echo &quot;---------------now wait start-------&quot;    nohup java -jar -XX:MetaspaceSize=128m -XX:MaxMetaspaceSize=256m -Xms512m -Xmn768m -Xmx1024m -Xss256k -XX:SurvivorRatio=8 -XX:+UseConcMarkSweepGC app.jar &gt; app.log 2&gt;&amp;1 &amp;    echo &quot;--------------start success-----------&quot;    exitfi</code></pre><blockquote><p>注意这种方法，简化了一些重启的操作，会寻找是否有名字为app.jar的服务进程，所以服务器上如果部署有多个服务，那么jar包不要重名，或者修改成路径</p></blockquote><h1>参数版脚本文件</h1><p>这是我自己参考，然后修改的一个脚本文件，只要将该脚本文件和Jar包放入同一个目录下运行即可启动服务。</p><p>这种是本地启动的方式，所以需要自行配置服务器的Java环境。</p><pre><code class="language-bash">#!/bin/bash# 文件夹路径FOLDER_PATH=&#39;&#39;# 服务名称SERVER_NAME=&#39;&#39;# jar包名称JAR_NAME=&#39;&#39;# PID文件PID_FILE=&#39;&#39;# java -jar的参数OPTIONS=&#39;&#39;# log文件LOG_FILE=&#39;&#39;# 使用说明usage() {  echo &quot;Usage:  sh ****.sh [OPTION]... &quot;  echo -e &quot;\033[43;37m 注意: 需要添加参数运行,否则无法成功运行 \033[0m&quot;  echo -e &quot;\033[43;37m 注意: 需要将本sh文件和jar包放在同一个文件夹 \033[0m&quot;  echo &quot;当前OPTION参数有以下几种&quot;  echo &quot;  --info       查看当前服务信息&quot;  echo &quot;  --start      启动服务&quot;  echo &quot;  --stop       停止服务&quot;  echo &quot;  --restart    重启服务&quot;  echo &quot;  --status     查询服务运行状态&quot;  exit 1}# 初始化init() {  FOLDER_PATH=$( pwd )  JAR_NAME=$( ls -l |grep  .jar | awk &#39;{print$9}&#39; )  SERVER_NAME=$( basename ${JAR_NAME} .jar )  PID_FILE=${SERVER_NAME}\.pid  LOG_FILE=${SERVER_NAME}\.log  OPTIONS=&#39;-Xms256m -Xmx512m&#39;}# 获取环境信息info() {  echo &quot;----------检测是否有Java环境----------&quot;  if ! command -v java &amp;&gt; /dev/null  then    echo &quot;-bash: java: command not found&quot;    echo &quot;请检查是安装了Java环境,是否配置了环境变量&quot;  else    echo &quot;----------当前Java的环境为----------&quot;    echo &quot;&quot;    echo &quot;$( java -version )&quot;  fi  echo &quot;----------当前文件夹路径----------&quot;  echo &quot;当前文件夹路径为${FOLDER_PATH}&quot;  echo &quot;----------启动服务的信息----------&quot;  echo &quot;当前服务名称为${SERVER_NAME}&quot;  echo &quot;需要启动的jar包名称为${JAR_NAME}&quot;  echo &quot;保存的PID文件名称为${PID_FILE}&quot;  echo &quot;保存的LOG文件名称为${LOG_FILE}&quot;  echo &quot;----------java -jar的参数----------&quot;  echo &quot;当前参数为${OPTIONS},需要更改请执行 -setOptions参数进行修改&quot;}# 查询当前程序是否运行is_running() {  pid=`ps -ef|grep $JAR_NAME | grep -v grep | awk &#39;{print $2}&#39; `  # pid是否存在，存在返回1，不存在返回0  if [ -n &quot;${pid}&quot; ]; then    return 1  else    return 0  fi}# 启动服务start() {  is_running  # 如果服务没有运行，则启动服务，否则不启动  if [ $? -eq &quot;0&quot; ]; then    init    echo &quot;服务启动中,请稍后&quot;    nohup java ${OPTIONS} -jar ${JAR_NAME} &gt;${LOG_FILE} 2&gt;&amp;1 &amp;    echo $! &gt; ${PID_FILE}    echo &quot;服务启动成功,当前的PID为$( cat ${PID_FILE} )&quot;  else    echo &quot;目标服务正在运行，请勿再次启动&quot;  fi}# 停止服务stop() {  is_running  if [ $? -eq &quot;0&quot; ]; then    echo &quot;没有检测到服务启动,请启动之后在尝试&quot;    exit 2  fi  pid=$(cat ${PID_FILE})  kill ${pid}  rm -rf ${PID_FILE}  sleep 1  is_running  if [ $? -eq &quot;1&quot; ]; then    kill -9 ${pid}  fi  echo &quot;成功结束${SERVER_NAME}的进程&quot;}# 查询服务运行状态status() {  is_running  if [ $? -eq &quot;1&quot; ]; then    echo &quot;${SERVER_NAME}服务正在运行中,当前服务的PID为${pid}&quot;  else     echo &quot;${SERVER_NAME}服务没有在运行&quot;  fi  exit 0}# 重启服务restart() {  is_running  if [ $? -eq &quot;1&quot; ]; then    stop    start  else    echo &quot;当前服务并没有运行,请运行之后在尝试&quot;  fi  exit 0}initcase &quot;$1&quot; in  &quot;--info&quot;)    info    ;;  &quot;--start&quot;)    start    ;;  &quot;--stop&quot;)    stop    ;;  &quot;--status&quot;)    status    ;;  &quot;--restart&quot;)    restart    ;;  *)    usage    ;;esacexit 0</code></pre><p>以下是使用方法</p><pre><code class="language-bash"># 获取当前服务信息sh start.sh --info# 启动服务sh start.sh --start# 停止服务sh start.sh --stop# 查询服务状态sh start.sh --status# 重启服务sh start.sh --restart# 提示使用信息sh start</code></pre><blockquote><p>其实这个脚本文件多多少少还是有一点问题的，提示信息也不是很美观，但是自己使用还是没什么问题的。</p></blockquote><h1>自动maven打包的Dockerfile</h1><p>这种方法是通过Docker部署的方式，将源码使用maven打包成jar包，然后在容器内运行jar包。</p><p>步骤：</p><ol><li>拉取Springboot源代码</li><li>如果需要自定义maven配置，请添加maven配置文件在于源码目录，或自己添加</li><li>修改服务打包生成的JAR包文件名</li><li>制作镜像</li></ol><p>使用Docker的话，得自行选择需要的JDK镜像和Maven版本镜像，然后修改服务打包出来后的服务名即可。</p><pre><code class="language-dockerfile"># 多阶段构建：第一阶段用于编译 Spring Boot 项目FROM maven:3.8.1-openjdk-17 AS builderLABEL stage=builder# 设置工作目录WORKDIR /app# 拷贝项目文件COPY pom.xml .COPY src ./src# 如果需要自定义maven镜像仓库配置，请添加maven配置文件COPY settings.xml /root/.m2/settings.xml# 编译并打包（跳过测试）RUN mvn clean package -DskipTests# 第二阶段：运行阶段（基于轻量级 JRE 镜像）FROM eclipse-temurin:17-jre-alpineLABEL maintainer=&quot;yww@yww52.com&quot;# 设置时区ENV TZ=Asia/ShanghaiRUN apk add --no-cache tzdata &amp;&amp; \    ln -sf /usr/share/zoneinfo/$TZ /etc/localtime &amp;&amp; \    echo $TZ &gt; /etc/timezone &amp;&amp; \    rm -rf /var/cache/apk/*# 定义 JAR 名称变量（需自定义）ARG JAR_NAME=app.jar# 定义 JVM 参数和应用目录ARG JAR_NAME=app.jarENV JAVA_OPTS=&quot;-Xms512m -Xmx1536m -Xmn614m -Xss256k -XX:SurvivorRatio=6 -XX:+UseG1GC -XX:MetaspaceSize=256m -XX:MaxMetaspaceSize=512m&quot;ENV APP_HOME=/app# 创建非 root 用户并设置权限RUN adduser -D appuser &amp;&amp; \    mkdir -p $APP_HOME &amp;&amp; \    chown -R appuser:appuser $APP_HOMEUSER appuser# 设置工作目录WORKDIR $APP_HOME# 从构建阶段复制生成的 JAR 文件COPY --from=builder /app/target/*.jar ${JAR_NAME}# 暴露应用端口EXPOSE 9872# 启动命令（使用 exec 确保 Java 成为 PID 1）ENTRYPOINT [&quot;sh&quot;, &quot;-c&quot;, &quot;exec java $JAVA_OPTS -jar ${JAR_NAME}&quot;]</code></pre><blockquote><p>这个制作镜像的方式会有个问题，就是每次制作镜像都需要去跑maven下载依赖，然后打包，时间花费的很多，而且有时候有些maven依赖在仓库上获取不到，所以这种方式适合一些自建有maven仓库的项目，比如自建有Nexus，早就下载好依赖上传到仓库的项目。不然的话还是在本地打包后在制作镜像吧，这样也快速，就是会很麻烦，不够自动化。</p></blockquote><h1>打包后的Dockerfile</h1><p>步骤</p><ol><li>打包好的JAR包</li><li>准备Dockerfile</li><li>修改服务打包生成的JAR包文件名</li><li>制作镜像</li></ol><pre><code class="language-dockerfile">FROM eclipse-temurin:17-jre-alpineLABEL maintainer=&quot;yww@yww52.com&quot;# 定义应用相关环境变量ENV JAVA_OPTS=&quot;-Xms512m -Xmx2048m -Xmn614m -Xss256k -XX:SurvivorRatio=6 -XX:+UseG1GC -XX:MetaspaceSize=256m -XX:MaxMetaspaceSize=512m&quot;ENV APP_HOME=/appARG JAR_NAME=app.jar# 设置时区ENV TZ=Asia/ShanghaiRUN apk add tzdata \    &amp;&amp; ln -snf /usr/share/zoneinfo/$TZ /etc/localtime \    &amp;&amp; echo $TZ &gt; /etc/timezone \    &amp;&amp; apk del tzdata \    &amp;&amp; rm -rf /var/cache/apk/*# 创建非root用户并设置权限RUN adduser -D appuser &amp;&amp; \    mkdir -p $APP_HOME &amp;&amp; \    chown -R appuser:appuser $APP_HOMEUSER appuser# 设置工作目录和应用目录WORKDIR $APP_HOME# 复制已打包的 JAR 文件COPY ${JAR_NAME} $APP_HOME/${JAR_NAME}# 暴露应用端口EXPOSE 9872# 启动命令（使用环境变量配置 JVM 参数）ENTRYPOINT [&quot;sh&quot;, &quot;-c&quot;, &quot;java $JAVA_OPTS -jar ${JAR_NAME}&quot;]</code></pre>]]></content:encoded>
            <category domain="https://yww52.com/categories/%25E6%2588%2591%25E7%259A%2584%25E6%258A%2580%25E6%259C%25AF%25E5%25AD%25A6%25E4%25B9%25A0%25E7%2589%25A9%25E8%25AF%25AD%25E6%259E%259C%25E7%2584%25B6%25E6%259C%2589%25E9%2597%25AE%25E9%25A2%2598/">我的技术学习物语果然有问题</category>
        </item>
        <item>
            <title>RabbitMQ</title>
            <link>https://yww52.com/posts/b543ced0/</link>
            <guid isPermaLink="true">https://yww52.com/posts/b543ced0/</guid>
            <pubDate>Sun, 26 Dec 2021 00:00:00 +0000</pubDate>
            <description>RabbitMQ的学习笔记。</description>
            <content:encoded><![CDATA[<img src="https://img.yww52.com/2021/12/2021-12-26top_img.jpg" alt="RabbitMQ" style="max-width:100%;height:auto;margin-bottom:15px;" /><h1>什么是MQ</h1><p>MQ，全称为<code>message queue</code>，消息队列，本质就是一个先进先出的队列，是一种跨进程的通信机制，用于上下游传递信息。</p><h1>为什么要使用MQ</h1><p>MQ的优点</p><ol><li>流量削峰</li><li>异步处理</li><li>应用解耦</li></ol><h1>MQ分类</h1><ol><li><code>ActiveMQ</code><ul><li>优点。单机吞吐量万级，时效性为ms级，可用性高，基于主从架构实现高可用性，丢失数据的概率较低。</li><li>缺点。早期项目，目前社区活跃度低，高吞吐量场景较少使用。</li></ul></li><li><code>Kafka</code><ul><li>优点。在大数据的消息传输方面，举足轻重。方面性能卓越，单机写入TPS约在每秒百万条，最大的优点就是吞吐量很高，时效性为ms级，可用性很高。而且是Kafka是分布式的，一个数据多个副本。</li><li>缺点。Kafka单机超过64个队列（分区），Load就会发生明显的飙高，发送消息响应时间变长，消息失败不支持重试。支持消息顺序，但是一台代理宕机后，就会产生消息乱序，社区更新较慢。</li></ul></li><li><code>RocketMQ</code><ul><li>优点。RocketMQ出自阿里巴巴，而且用Java语言实现的，对于学习Java的人来说还能自己定制。单机吞吐量达十万级，可用性很高，分布式架构，消息可以做到零丢失，功能较为完善，扩展性也很好，支持10亿级别的消息堆积，不会因为消息堆积而导致性能下降。</li><li>缺点。支持的客户端语言不多，目前是Java和C++，而且C++方面也还不成熟，没有在MQ核心中去实现JMS等接口，有些系统要迁移需要修改大量的代码。</li></ul></li><li><code>RabbitMQ</code><ul><li>优点。RabbitMQ由erlang实现，由于erlang的高并发特性，性能较好，吞吐量到万级，功能比较完备，稳定易用跨平台，而且支持多种语言，社区活跃度也很高。</li><li>缺点。商业版需要收费，学习成本较高。</li></ul></li></ol><h1>RabbitMQ的概念</h1><p>RabbitMQ是一个消息中间件，接收并转发消息，它只接收存储和转发消息数据，并不会处理消息。</p><p>四大核心概念。</p><ol><li><p><code>生产者</code>。</p><p>产生数据并发送消息的程序。</p></li><li><p><code>交换机</code>。</p><p>交换机一方面接收来自生产者的消息，另一方面它将消息推送到队列中。交换机必须确切知道如何处理它接收到的消息，是将这些消息推送到特定队列还是推送到多个队列，亦或者是把消息丢弃，这个得有交换机类型决定。</p></li><li><p><code>队列</code></p><p>队列是RabbitMQ中的使用的一个数据结构，用于存储消息。队列仅受主机的内存和磁盘限制的约束，本质上是一个消息缓冲区。生产者将消息发送到队列，消费者从队列中接收消息。</p></li><li><p><code>消费者</code></p><p>接收并处理消息的程序。</p></li></ol><h1>RabbitMQ安装</h1><p>RabbitMQ的安装可以参考官方的文档。</p><p><a href="https://www.rabbitmq.com/download.html">https://www.rabbitmq.com/download.html</a></p><p>里面有多种安装的方式，建议使用docker来进行安装，不用考虑这么多环境的问题，只要安装了docker，就只需要一条命令即可。</p><pre><code class="language-bash">docker run -id --name rabbitmq \-e RABBITMQ_DEFAULT_USER=username \-e RABBITMQ_DEFAULT_PASS=password \-p 15672:15672 -p 5672:5672 \rabbitmq:3.8-management</code></pre><p>安装的是管理版本，所以可以访问地址</p><p><a href="http://IP:15672/">http://IP:15672/</a></p><p>来进行登录管理RabbitMQ的web界面。</p><p>服务器记得开放15672和5672这两个端口，15672是web管理界面的接口，5672是RabbitMQ的服务端口。</p><p>如果没设置参数，默认的账号和密码都是<code>guest</code></p><p>官方的教程也是挺好的，可以参考一下。</p><p><a href="https://www.rabbitmq.com/getstarted.html">https://www.rabbitmq.com/getstarted.html</a></p><h1>Hello World</h1><p>引入MQ客户端的依赖。</p><pre><code class="language-xml">            &lt;!-- RabbitMQ的客户端依赖 --&gt;            &lt;dependency&gt;                &lt;groupId&gt;com.rabbitmq&lt;/groupId&gt;                &lt;artifactId&gt;amqp-client&lt;/artifactId&gt;            &lt;/dependency&gt;</code></pre><p>首先先创建一个常量类，用于存储一些配置参数。</p><pre><code class="language-Java">package com.yww.rabbitmq;/** * &lt;p&gt; *     连接的参数 * &lt;/p&gt; * * @author yww **/public class Constant {        /**     * 队列名称     */    public static final String QUEUE_NAME = &quot;Hello&quot;;    /**     * 主机名     */    public static final String HOSTNAME = &quot;119.29.119.110&quot;;    /**     * 用户名     */    public static final String USERNAME = &quot;username&quot;;    /**     * 密码     */    public static final String PASSWORD = &quot;password&quot;;}</code></pre><p>生产者发送消息。</p><pre><code class="language-Java">package com.yww.rabbitmq;import com.rabbitmq.client.Channel;import com.rabbitmq.client.Connection;import com.rabbitmq.client.ConnectionFactory;import java.io.IOException;import java.util.concurrent.TimeoutException;/** * &lt;p&gt; *     生产者 * &lt;/p&gt; * * @author yww **/public class Producer {    public static void main(String[] args) throws IOException, TimeoutException {        // 创建连接工厂        ConnectionFactory factory = new ConnectionFactory();        // 设置连接的配置,IP,用户名，密码        factory.setHost(Constant.HOSTNAME);        factory.setUsername(Constant.USERNAME);        factory.setPassword(Constant.PASSWORD);        // 创建连接        Connection connection = factory.newConnection();        // 通过连接来创建一个信道        Channel channel = connection.createChannel();        /*         * 通过一个信道来生成一个队列         * 1. 队列的名称         * 2. true表示声明的是一个持久化队列，该队列会在服务器重启后依旧存活         * 3. true表示声明的是一个独占队列，被限制在这个连接中。         * 4. 最后一个消费者断开连接之后，该队列是否自动删除。true表示自动删除，false表示不自动删除         * 5. 其他参数         */        channel.queueDeclare(Constant.QUEUE_NAME, false, false, false, null);        String message = &quot;Hello World!&quot;;        /*         * 发送一个消息         * 1. 发送到哪个交换机         * 2. 路由的key值是哪个，本次是队列的名称         * 3. 其他参数信息         * 4. 发送消息的消息体         */        channel.basicPublish(&quot;&quot;, Constant.QUEUE_NAME, null, message.getBytes());        System.out.println(&quot;消息发送成功&quot;);    }}</code></pre><p>运行该程序就能发送一条消息到指定队列了。</p><p>消费者消费消息。</p><pre><code class="language-java">package com.yww.rabbitmq;import com.rabbitmq.client.*;import java.io.IOException;import java.util.concurrent.TimeoutException;/** * &lt;p&gt; *     消费者 * &lt;/p&gt; * * @author yww **/public class Consumer {    public static void main(String[] args) throws IOException, TimeoutException {        ConnectionFactory factory = new ConnectionFactory();        factory.setHost(Constant.HOSTNAME);        factory.setUsername(Constant.USERNAME);        factory.setPassword(Constant.PASSWORD);        Connection connection = factory.newConnection();        Channel channel = connection.createChannel();        // 消费者接收的回调        DeliverCallback deliverCallback = (consumerTag, message) -&gt; {            System.out.println(&quot;接收到的消息为：&quot;);            String messageMessage = new String(message.getBody());            System.out.println(messageMessage);        };        // 消费者被取消消费的回调        CancelCallback cancelCallback = consumerTag -&gt; {            System.out.println(&quot;消费者取消了消费！&quot;);            System.out.println(consumerTag);        };        /*         * 消费者消费消息         * 1. 队列的名称         * 2. 消费成功之后是否要自动应答。true表示自动应答，false表示要手动应答         * 3. 消费者接收的回调         * 4. 消费者被取消消费的回调         */        channel.basicConsume(Constant.QUEUE_NAME, true, deliverCallback, cancelCallback);    }}</code></pre><p>此时就能看到消费者获取到了这个消息。</p><h1>Work Queues</h1><p>Work Queues，工作队列或者说是任务队列，简单的理解为一个队列的任务可以分配给多个消费者消费。</p><p>它的主要思想是避免立即执行资源密集任务，而不得不等待它完成。相反我们安排任务在之后执行。我们把任务封装为消息并将其发送到队列。在后台运行的工作进 程将弹出任务并最终执行作业。当有多个工作线程时，这些工作线程将一起处理这些任务。</p><h2>循环调度</h2><p>生产者和消费者获取连接，信道的方式是一样的，所以可以抽取出来作为一个工具类。</p><pre><code class="language-Java">package com.yww.rabbitmq;import com.rabbitmq.client.Channel;import com.rabbitmq.client.Connection;import com.rabbitmq.client.ConnectionFactory;import java.io.IOException;import java.util.concurrent.TimeoutException;/** * &lt;p&gt; *     获取信道工具类 * &lt;/p&gt; * * @author yww **/public class MqUtil {    /**     * 队列名称     */    public static final String QUEUE_NAME = &quot;Hello&quot;;    /**     * 主机名     */    public static final String HOSTNAME = &quot;119.29.119.110&quot;;    /**     * 用户名     */    public static final String USERNAME = &quot;username&quot;;    /**     * 密码     */    public static final String PASSWORD = &quot;password&quot;;            /**     * 获取信道     * @return  Channel     */    public static Channel getChannel() throws IOException, TimeoutException {        ConnectionFactory factory = new ConnectionFactory();        factory.setHost(HOSTNAME);        factory.setUsername(USERNAME);        factory.setPassword(PASSWORD);        Connection connection = factory.newConnection();        return connection.createChannel();    }    }</code></pre><p>这次先编写两个消费者（工作线程），两个消费者除了打印不一样，其他都一样，所以就不写全了，启动两次或者分两个类启动都行。</p><pre><code class="language-Java">package com.yww.rabbitmq;import com.rabbitmq.client.CancelCallback;import com.rabbitmq.client.Channel;import com.rabbitmq.client.DeliverCallback;import java.io.IOException;import java.util.concurrent.TimeoutException;/** * &lt;p&gt; *     第一个工作线程 * &lt;/p&gt; * * @author yww * @version 1.0 * @date 2021/12/25 22:28 **/public class Worker1 {    public static void main(String[] args) throws IOException, TimeoutException {        Channel channel = MqUtil.getChannel();        DeliverCallback deliverCallback = (consumerTag, message) -&gt; {            System.out.println(&quot;接收到的消息：&quot;);            System.out.println(new String(message.getBody()));        };        CancelCallback cancelCallback = consumerTag -&gt; {            System.out.println(&quot;消费者取消消费这个消息&quot;);            System.out.println(consumerTag);        };        System.out.println(&quot;当前工作线程为**1**，正在等待接收消息--------&quot;);        // 接收消息        channel.basicConsume(MqUtil.QUEUE_NAME, true, deliverCallback, cancelCallback);    }}</code></pre><p>接下来编写生产者。</p><pre><code class="language-Java">package com.yww.rabbitmq;import com.rabbitmq.client.Channel;import java.io.IOException;import java.util.Scanner;import java.util.concurrent.TimeoutException;/** * &lt;p&gt; *     生产者 *     发送大量的消息 * &lt;/p&gt; * * @author yww **/public class Producer {    public static void main(String[] args) throws IOException, TimeoutException {        Channel channel = MqUtil.getChannel();        // 信道指定队列        channel.queueDeclare(MqUtil.QUEUE_NAME, false, false, false, null);        // 从控制台中接收信息        Scanner scanner = new Scanner(System.in);        while (scanner.hasNext()) {            String message = scanner.nextLine();            channel.basicPublish(&quot;&quot;, MqUtil.QUEUE_NAME, null, message.getBytes());            System.out.println(&quot;发送消息完成，本次的消息为----&quot; + message);        }    }}</code></pre><p>接下来进行发送测试，发送几个消息。</p><pre><code>AA发送消息完成，本次的消息为----AABB发送消息完成，本次的消息为----BBCC发送消息完成，本次的消息为----CCDD发送消息完成，本次的消息为----DD</code></pre><p>可以看到两个消费者的情况。 </p><pre><code>当前工作线程为**1**，正在等待接收消息--------接收到的消息：AA接收到的消息：CC当前工作线程为**2**，正在等待接收消息--------接收到的消息：BB接收到的消息：DD</code></pre><blockquote><p>从这里可以看出一些规律。</p><p>每个消息是只能被消费一次的。</p><p>消息的消费的轮询的，就是说每个消费者消费了一次消息之后，就轮到下一个消费者去消费，不会连续消费。</p></blockquote><h2>消息应答</h2><p>消费者完成一个任务需要一段时间，在这段时间内如果它挂掉了，由于RabbitMQ一旦向消费者传递消息后就会立即将该消息标记为删除，所以就会丢失了这段消息的处理。所以为了保证消息在发送过程中不丢失，RabbitMQ引入了消息应答机制。</p><p>消息应答就是说，消费者在接收到消息并处理了之后，在通知RabbitMQ处理完成，RabbitMQ才会把该消息删除。</p><h3>自动应答</h3><p>这种方式表示消息发送后就会立即被认为已经传输完成，就会有可能出现上述的消息丢失的情况。这种模式会出现消息丢失的情况，但是可以适用于高吞吐量的场景，毕竟速度和安全并不能全都要。上述的代码演示都是可以看到是自动应答的。</p><p>这种模式尽量不使用，因为在大多数情况下，每一个消息都是很重要的，所以尽量使用自定义的应答方式。</p><h3>消息应答的方法</h3><ul><li><p><code>Channel.basicAck()</code></p><p>肯定确认。表示MQ已经知道该消息已经被成功处理了，可以将其丢弃。</p><pre><code class="language-java">    /**     * 确认一个或多个收到的消息     * @param deliveryTag      * @param multiple 是否批量应答，true表示批量应答消息     * @throws java.io.IOException 如果遇到错误     */    void basicAck(long deliveryTag, boolean multiple) throws IOException;</code></pre></li><li><p><code>Channel.basicNack()</code></p><p>用于否定确认。</p><pre><code class="language-Java">    /**     * 拒绝接收到的一条或多条消息。     * @param deliveryTag      * @param multiple 是否批量应答，true表示批量应答消息     * @param requeue 如果被拒绝的消息应该被重新排队而不是丢弃/死信，则使用true     * @throws java.io.IOException 如果遇到错误     */    void basicNack(long deliveryTag, boolean multiple, boolean requeue) throws IOException;</code></pre></li><li><p><code>Channel.basicReject()</code></p><p>用于否定确认。</p><pre><code class="language-Java">    /**     * 拒绝接收到的一条消息。     * @param deliveryTag      * @param requeue true表示如果被拒绝的消息应该被重新排队而不是被丢弃/死信，则使用true     * @throws java.io.IOException 如果遇到错误     */    void basicReject(long deliveryTag, boolean requeue) throws IOException;</code></pre></li></ul><blockquote><p>关于参数multiple这个批量应答的处理。</p><p>不建议使用批量应答，因为不能保证之前的消息是完成的，批量应答可能会出现应答了没完成的消息。</p></blockquote><h3>消息自动重新入队</h3><p>当消费者由于一些特殊情况导致ACK确认未成功发送给RabbitMQ，RabbitMQ就会让该消息重新排队（上述的requeue参数选择了消息不被丢弃），这就可以解决消息丢失的问题了。</p><h3>消息应答的测试</h3><p>手动应答是消费者设置的，所以生产者不需要特殊的变化，跟上一例子一样。</p><pre><code class="language-Java">public class Producer {    public static void main(String[] args) throws IOException, TimeoutException {        Channel channel = MqUtil.getChannel();        // 信道指定队列        channel.queueDeclare(MqUtil.QUEUE_NAME, false, false, false, null);        // 从控制台中接收信息        Scanner scanner = new Scanner(System.in);        while (scanner.hasNext()) {            String message = scanner.nextLine();            channel.basicPublish(&quot;&quot;, MqUtil.QUEUE_NAME, null, message.getBytes());            System.out.println(&quot;发送消息完成，本次的发送的消息为----&quot; + message);        }    }    }</code></pre><p>接下来编写消费者。</p><pre><code class="language-Java">package com.yww.rabbitmq;import com.rabbitmq.client.CancelCallback;import com.rabbitmq.client.Channel;import com.rabbitmq.client.DeliverCallback;import java.io.IOException;import java.util.concurrent.TimeoutException;/** * &lt;p&gt; *     消费者1 * &lt;/p&gt; * * @author yww **/public class consumer1 {    public static void main(String[] args) throws IOException, TimeoutException {        Channel channel = MqUtil.getChannel();        System.out.println(&quot;消费者**1**等待接收消息， 该消费者处理消息时间较短&quot;);        DeliverCallback deliverCallback = (consumerTag, message) -&gt; {            System.out.println(&quot;消费者**1**开始消费消息&quot;);            String deliverMessage = new String(message.getBody());            // 模拟消费消息需要一秒钟            try {                Thread.sleep(1000);            } catch (InterruptedException e) {                e.printStackTrace();            }            System.out.println(&quot;消费者**1**接收到了消息，消息为： &quot; + deliverMessage);            // 手动应答(不批量)            channel.basicAck(message.getEnvelope().getDeliveryTag(), false);        };        CancelCallback cancelCallback = consumerTag -&gt; {            System.out.println(&quot;消费者**1**取消了消费消息，当前消息信息为：&quot;);            System.out.println(consumerTag);        };        // 第二个参数标识为false，表示不使用自动应答        channel.basicConsume(MqUtil.QUEUE_NAME, false, deliverCallback, cancelCallback);    }}</code></pre><p>消费者二就调整一下时间，设置一个较久的时间，便于我们在暂停期间停止该进程，使其消费失败。</p><pre><code class="language-Java">package com.yww.rabbitmq;import com.rabbitmq.client.CancelCallback;import com.rabbitmq.client.Channel;import com.rabbitmq.client.DeliverCallback;import java.io.IOException;import java.util.concurrent.TimeoutException;/** * &lt;p&gt; *     消费者2 * &lt;/p&gt; * * @author yww **/public class consumer2 {    public static void main(String[] args) throws IOException, TimeoutException {        Channel channel = MqUtil.getChannel();        System.out.println(&quot;消费者**2**等待接收消息， 该消费者处理消息时间较长&quot;);        DeliverCallback deliverCallback = (consumerTag, message) -&gt; {            System.out.println(&quot;消费者**2**开始消费消息&quot;);            String deliverMessage = new String(message.getBody());            // 模拟消费消息需要二十秒钟            try {                Thread.sleep(30000);            } catch (InterruptedException e) {                e.printStackTrace();            }            System.out.println(&quot;消费者**2**接收到了消息，消息为： &quot; + deliverMessage);            // 手动应答(不批量)            channel.basicAck(message.getEnvelope().getDeliveryTag(), false);        };        CancelCallback cancelCallback = consumerTag -&gt; {            System.out.println(&quot;消费者**2**取消了消费消息，当前消息信息为：&quot;);            System.out.println(consumerTag);        };        // 第二个参数标识为false，表示不使用自动应答        channel.basicConsume(MqUtil.QUEUE_NAME, false, deliverCallback, cancelCallback);    }}</code></pre><p>接下来开始测试。</p><blockquote><p>开启生产者和这两个消费者，当消费者2出现开始消费的字样，即进入了接收消费的回调之后，停止程序运行，模拟消息丢失。</p></blockquote><pre><code>Producer的控制台aa发送消息完成，本次的发送的消息为----aabb发送消息完成，本次的发送的消息为----bbcc发送消息完成，本次的发送的消息为----ccComsumer1的控制台消费者**1**等待接收消息， 该消费者处理消息时间较短消费者**1**开始消费消息消费者**1**接收到了消息，消息为： bb-------------------此时关闭消费者2-------------------------消费者**1**开始消费消息消费者**1**接收到了消息，消息为： ccComsumer2的控制台消费者**2**等待接收消息， 该消费者处理消息时间较长消费者**2**开始消费消息消费者**2**接收到了消息，消息为： aa消费者**2**开始消费消息-------------------此时关闭消费者2-------------------------</code></pre><p>从这个测试就可以看到，<code>cc</code>这个消息原本应该是在消费者2中消费的，但是我停下了消费者2的进程之后，会发现这个消息转交到了消费者1中执行。</p><h2>RabbitMQ持久化</h2><p>消息应答是消费者出现问题的时候的解决方法，但是当RabbitMQ服务宕机之后，消息还是可能会丢失，所以这就需要启动RabbitMQ的持久化机制了。</p><h3>队列实现持久化</h3><p>之前是有提到过的，队列不开启持久化的话，MQ服务宕机重启的时候，队列就会丢失（因为是临时存储的），所以需要在声明队列的时候就要开启持久化。</p><p>主要就是生产者存放消息时声明队列的那个方法。</p><pre><code class="language-Java">        /*         * 队列持久化的开启主要是第二个参数（durable）         * true表示声明的是一个持久化队列，该队列会在服务器重启后依旧存活         */        Queue.DeclareOk queueDeclare(String queue, boolean durable, boolean exclusive, boolean autoDelete,                             Map&lt;String, Object&gt; arguments) throws IOException;</code></pre><h3>消息实现持久化</h3><p>根据上述所说，当消息不开启持久化，服务宕机之后，即使队列不会丢失，消息也会丢失的，所以也需要开启持久化。</p><p>主要就是发送消息的那个方法。</p><pre><code class="language-Java">    /*     * 消息持久化的开启主要是第三个参数（props）     * 这个参数是比较多的，需要在这里配置消息是持久化的。     */    void basicPublish(String exchange, String routingKey, BasicProperties props, byte[] body) throws IOException;// 需要配置一个常数（MessageProperties.PERSISTENT_TEXT_PLAIN）channel.basicPublish(&quot;&quot;, Constant.QUEUE_NAME, MessageProperties.PERSISTENT_TEXT_PLAIN, message.getBytes());</code></pre><h3>持久化不代表不丢失</h3><p>队列和消息都实现持久化之后，并不能保证消息不会丢失，因为有很多特殊情况会影响结果，比如持久化到磁盘的过程中服务宕机，还是会可能丢失数据的，所以这种方法并不能避免一些特殊情况。所以还需要参考下述的发布确认。</p><h2>发布确认</h2><p>生产者将信道设置成 confirm 模式，一旦信道进入 confirm 模式，所有在该信道上面发布的消息都将会被指派一个唯一的 ID(从 1 开始)，一旦消息被投递到所有匹配的队列之后，broker 就会发送一个确认给生产者(包含消息的唯一 ID)，这就使得生产者知道消息已经正确到达目的队列了，如果消息和队列是可持久化的，那么确认消息会在将消息写入磁盘之后发出，broker 回传给生产者的确认消息中 delivery-tag 域包含了确认消息的序列号，此外 broker 也可以设置<code>basic.ack</code>的 multiple 域，表示到这个序列号之前的所有消息都已经得到了处理。</p><p>confirm 模式最大的好处在于他是异步的，一旦发布一条消息，生产者应用程序就可以在等信道返回确认的同时继续发送下一条消息，当消息最终得到确认之后，生产者应用便可以通过回调方法来处理该确认消息，如果 RabbitMQ 因为自身内部错误导致消息丢失，就会发送一条 nack 消息，生产者应用程序同样可以在回调方法中处理该 nack 消息。</p><h3>开启发布确认</h3><p>开启发布确认是在生产者端的，所以只需要在生产者端标记信道，开启即可，主要的方法就是这个。</p><pre><code class="language-java">    /**     * 在这个信道上开启发布确认。     */    Confirm.SelectOk confirmSelect() throws IOException;    /**     * 在生产者端开启即可     */    channel.confirmSelect();</code></pre><p>这个方法是将信道标记成<code>confirm</code>，还需要配合下面的方法实现确认。</p><pre><code class="language-java">    /**     * 开启等待直到上次调用依赖发布的所有消息都被broker返回ack或者是nack。     * 如果在非confirm信道上调用该方法，会抛出InterruptedException异常。     */    boolean waitForConfirms() throws InterruptedException;    /**     * 开启等待直到自上次调用以来所有发布的消息都被broker返回ack或者是nack，或者是直到超时结束。     * 如果超时过期就会抛出TimeoutException异常。     * 如果在非confirm信道上调用该方法，会抛出InterruptedException异常。     */    boolean waitForConfirms(long timeout) throws InterruptedException, TimeoutException;    /**      * 开启等待直到上次调用依赖发布的所有消息都被broker返回ack或者是nack。     * 如果消息被broker返回了nack，会抛出IOException异常。     * 如果在非confirm信道上调用该方法，会抛出InterruptedException异常。     */     void waitForConfirmsOrDie() throws IOException, InterruptedException;    /**      * 开启等待直到上次调用依赖发布的所有消息都被broker返回ack或者是nack，或者是直到超时结束。     * 如果消息被broker返回了nack，会抛出IOException异常。     * 如果超时过期就会抛出TimeoutException异常。     * 如果在非confirm信道上调用该方法，会抛出InterruptedException异常。     */    void waitForConfirmsOrDie(long timeout) throws IOException, InterruptedException, TimeoutException;</code></pre><p>一般都会使用</p><pre><code class="language-Java">boolean waitForConfirms(long timeout) throws InterruptedException, TimeoutException;</code></pre><h3>单个确认发布</h3><p>最简单的一种发布确认方式，是一种同步的确认发布，也就是说发布一个消息之后，只有它被确认的时候才会返回，如果在指定时间范围内没有被确认，就会抛出异常。</p><p>这种方式是最花费时间的，因为每一个发布的消息都要等待确认才能发下一个消息，吞吐量会小很多，不过这种方式也是最安全的，能保证消息不会丢失。</p><p>测试如下。</p><pre><code class="language-Java">package com.yww.rabbitmq.publish;import com.rabbitmq.client.Channel;import com.yww.rabbitmq.MqUtil;import java.io.IOException;import java.util.concurrent.TimeoutException;/** * &lt;p&gt; *     单个确认发布 * &lt;/p&gt; * * @author yww **/public class PublishOne {    public static void main(String[] args) throws IOException, TimeoutException, InterruptedException {        Channel channel = MqUtil.getChannel();        // 开启发布确认        channel.confirmSelect();        channel.queueDeclare(MqUtil.QUEUE_NAME, false, false, false, null);        int total = 0;        long beginTime = System.currentTimeMillis();        // 发布消息测试        for (int i = 0; i &lt; 100; i++) {            String msg = String.valueOf(i);            channel.basicPublish(&quot;&quot;, MqUtil.QUEUE_NAME, null, msg.getBytes());            // 发布确认，返回true则表示成功发布，返回false可以重新发布，也可以使用超时时间。            boolean flag = channel.waitForConfirms();            if (flag) {                total++;            }        }        long endTime = System.currentTimeMillis();        System.out.println(&quot;发布了100条单独确认消息耗时为：&quot; + (endTime - beginTime) + &quot;ms&quot;);        System.out.println(&quot;成功发布了消息数为：&quot; + total);    }}</code></pre><p>测试结果。</p><pre><code>发布了1000条单独确认消息耗时为：29474ms成功发布了消息数为：1000</code></pre><h3>批量发布确认</h3><p>单个确认的效率很慢，所以可以先发布一批消息然后一起确认可以极大提高吞吐量，这种方案也是同步的发布确认方式。</p><p>当然问题很明显，当出现故障的时候，不知道具体是哪一个消息出现了问题。</p><p>测试如下。</p><pre><code class="language-Java">package com.yww.rabbitmq.publish;import com.rabbitmq.client.Channel;import com.yww.rabbitmq.MqUtil;import java.io.IOException;import java.util.concurrent.TimeoutException;/** * &lt;p&gt; *     批量确认发布 * &lt;/p&gt; * * @author yww **/public class PublishBatch {    public static void main(String[] args) throws IOException, TimeoutException, InterruptedException {        Channel channel = MqUtil.getChannel();        // 开启发布确认        channel.confirmSelect();        channel.queueDeclare(MqUtil.QUEUE_NAME, false, false, false, null);        // 批量确认消息数        int batchSize = 100;        // 未确认消息个数        int total = 0;        long beginTime = System.currentTimeMillis();        // 发布消息测试        for (int i = 0; i &lt; 1000; i++) {            String msg = String.valueOf(i);            channel.basicPublish(&quot;&quot;, MqUtil.QUEUE_NAME, null, msg.getBytes());            total++;            // 如果发布的消息数等于设置的批量确认消息数，就进行发布确认            if (total == batchSize) {                channel.waitForConfirms();                total = 0;            }        }        //为了确保还有剩余没有确认消息 再次确认        if (total &gt; 0) {            channel.waitForConfirms();        }        long endTime = System.currentTimeMillis();        System.out.println(&quot;发布了1000条批量确认消息耗时为：&quot; + (endTime - beginTime) + &quot;ms&quot;);        System.out.println(&quot;未确认的消息数为：&quot; + total);    }}</code></pre><p>测试结果如下。</p><pre><code class="language-java">发布了1000条批量确认消息耗时为：292ms未确认的消息数为：0</code></pre><h3>异步确认发布</h3><p>异步确认发布在比同步的方式最重要的是效率能高很多，而且因为也是单个确认的，所以也是很可靠的。</p><p>这里就是通过监听器来帮忙监听哪些消息是成功的或者是失败的，而不用在逻辑里面等待确认。</p><p>添加监听器主要是下面这个方法。</p><pre><code class="language-java"> void addConfirmListener(ConfirmListener listener);// 一般使用下面这个方法，能知道哪些成功，哪些失败    ConfirmListener addConfirmListener(ConfirmCallback ackCallback, ConfirmCallback nackCallback);</code></pre><p>测试如下。</p><pre><code class="language-Java">package com.yww.rabbitmq.publish;import com.rabbitmq.client.Channel;import com.rabbitmq.client.ConfirmCallback;import com.yww.rabbitmq.MqUtil;import java.io.IOException;import java.util.concurrent.TimeoutException;/** * &lt;p&gt; *     异步确认发布 * &lt;/p&gt; * * @author yww **/public class PublishAsync {    public static void main(String[] args) throws IOException, TimeoutException, InterruptedException {        Channel channel = MqUtil.getChannel();        // 开启发布确认        channel.confirmSelect();        channel.queueDeclare(MqUtil.QUEUE_NAME, false, false, false, null);        // 监听器消息确认成功的回调        ConfirmCallback ackCallback = (deliveryTag, multiple) -&gt; {            System.out.println(&quot;这个确认消息为&quot; + deliveryTag);        };        // 监听器消息确认失败的回调（这里可以直接重发，或者是先记录下来）        ConfirmCallback nackCallback = (deliveryTag, multiple) -&gt; {            System.out.println(&quot;这个未确认消息为&quot; + deliveryTag);        };        // 准备一个监听器，监听哪些消息发布成功或者失败(异步通知)        channel.addConfirmListener(ackCallback, nackCallback);        long beginTime = System.currentTimeMillis();        // 发布消息测试        for (int i = 0; i &lt; 1000; i++) {            String msg = String.valueOf(i);            // 这里只负责发送消息，不负责确认，确认消息由监听器负责            channel.basicPublish(&quot;&quot;, MqUtil.QUEUE_NAME, null, msg.getBytes());        }        long endTime = System.currentTimeMillis();        System.out.println(&quot;发布了1000条批量确认消息耗时为：&quot; + (endTime - beginTime) + &quot;ms&quot;);    }}</code></pre><p>测试结果如下。</p><pre><code>发布了1000条批量确认消息耗时为：33ms(...省略了成功打印)</code></pre><h3>异步未确认消息的处理</h3><p>从上述的例子可以知道监听器和发布消息的线程不是同一个，所以这个未确认的消息的处理最好还是先存储起来。</p><p>一个简单的使用例子。</p><pre><code class="language-Java">package com.yww.rabbitmq.publish;import com.rabbitmq.client.Channel;import com.rabbitmq.client.ConfirmCallback;import com.yww.rabbitmq.MqUtil;import java.io.IOException;import java.util.concurrent.ConcurrentNavigableMap;import java.util.concurrent.ConcurrentSkipListMap;import java.util.concurrent.TimeoutException;/** * &lt;p&gt; *     处理异步方式未被确认的消息 * &lt;/p&gt; * * @author yww **/public class publishDeal {    public static void main(String[] args) throws IOException, TimeoutException, InterruptedException {        Channel channel = MqUtil.getChannel();        channel.confirmSelect();        channel.queueDeclare(MqUtil.QUEUE_NAME, false, false, false, null);        // 准备一个队列存储消息        ConcurrentSkipListMap&lt;Long, String&gt; map = new ConcurrentSkipListMap&lt;&gt;();        // 删除掉已经确认的消息        ConfirmCallback ackCallback = (deliveryTag, multiple) -&gt; {            // 如果是批量的就一起清理            if (multiple) {                // 获取到确认的消息                ConcurrentNavigableMap&lt;Long, String&gt; confirmed = map.headMap(deliveryTag);                confirmed.clear();            } else {                map.remove(deliveryTag);            }            System.out.println(&quot;确认的消息为：&quot; + deliveryTag);        };        ConfirmCallback nackCallback = (deliveryTag, multiple) -&gt; {            String nackMsg = map.get(deliveryTag);            System.out.println(&quot;这个未确认消息的内容为：&quot; + nackMsg);        };        channel.addConfirmListener(ackCallback, nackCallback);        long beginTime = System.currentTimeMillis();        for (int i = 0; i &lt; 1000; i++) {            String msg = String.valueOf(i);            long num = channel.getNextPublishSeqNo();            channel.basicPublish(&quot;&quot;, MqUtil.QUEUE_NAME, null, msg.getBytes());            // 记录发送的所有消息            map.put(num, msg);        }        long endTime = System.currentTimeMillis();        System.out.println(&quot;发布了1000条批量确认消息耗时为：&quot; + (endTime - beginTime) + &quot;ms&quot;);    }}</code></pre><h2>不公平调度</h2><p>刚刚学习的时候是可以知道RabbitMQ默认是循环调度，也就是轮询的，这种调度方法，有优点也有缺点，有时候公平并不是一件好事，所以我们可以设置这个消费者的工作内容。</p><p>主要是在消费者中设置信道的这个函数。</p><pre><code class="language-Java">    /**     * 设置工作量     * 可以设置0到65535之间的数     * @param prefetchCount 设置该线程最多持有的任务数，如果是0则代表无限制     */    void basicQos(int prefetchCount) throws IOException;/**     * 这样表示这个工作线程，在处理并确认一条消息的时间段     * 不能向该工作线程发送新的消息，将会把消息分派给不忙的工作线程     */channel.basicQos(1);</code></pre><blockquote><p>注意当所有工作线程都很忙，你的队列有可能会被填满。</p><p>所以需要你添加更多的工作线程，或者是使用其他的策略。</p></blockquote><p>上述只是简单实用，这个方法的完整形式如下（以下为机翻）</p><pre><code class="language-Java">    /**     * 请求特定的“服务质量”设置。     * &lt;p&gt; * 这些设置限制了服务器在需要确认之前将交付给消费者的数据量。 * 因此，它们提供了一种用户发起的流控制方法。     * &lt;p&gt;     * 注意预取计数必须在0到65535之间(unsigned short in AMQP 0-9-1)。     *     * @param prefetchSize  服务器将传递的最大内容量(以字节为单位)，如果无限制，则为0     * @param prefetchCount 服务器将传递的最大消息数，如果无限制，则为0     * @param global        如果设置应应用于整个通道而不是每个使用者，则为true     * @throws java.io.IOException 如果遇到错误     * @see com.rabbitmq.client.AMQP.Basic.Qos     */    void basicQos(int prefetchSize, int prefetchCount, boolean global) throws IOException;</code></pre><h1>Publish/Subscribe</h1><p>在上述的示例中，使用了工作队列，工作队列会将每个任务交付给一个工作线程。当然我们也可以实现向多个消费者传递一条消息，这种模式被称为<code>Publish/Subscribe</code>，也叫<code>发布/订阅</code>。</p><h2>交换机（Exchanges）</h2><p>RabbitMQ消息传递模型的核心思想是生产者从不直接向队列中发送任何消息。实际上生产者经常甚至根本不知道消息是否会被传送到哪个队列。</p><p>实际情况是生产者只能将消息发送到交换机中，交换机的任务也很简单，一方面他接收来自生产者的消息，另一方面将消息推入队列。</p><p>是应该把这些消息放到特定队列，还是说把他们丢到许多队列中，或者说是丢弃，这些就是由交换机的类型来决定了。</p><p>交换机有几种可用的类型。</p><ol><li>direct（直接）</li><li>topic（主题）</li><li>headers（标题）</li><li>fanout（扇出）</li></ol><h2>无名交换机</h2><p>之前使用的就是无名的交换机。</p><pre><code class="language-Java"> channel.basicPublish(&quot;&quot;, &quot;hello&quot;, null, &quot;hello world&quot;.getBytes());</code></pre><p>第一个参数是交换机的名称，空字符串就表示默认或者说是无名交换机。消息能路由发送到队列中其实就是由<code>routingKey</code>指定的。</p><h2>临时队列</h2><p>临时队列用于临时存储消息，一旦断开了消费者的连接，队列就会被自动删除。</p><p>创建一个随机名称的临时队列。</p><pre><code class="language-Java">String queueName = channel.queueDeclare().getQueue();</code></pre>]]></content:encoded>
            <category domain="https://yww52.com/categories/%25E6%2588%2591%25E7%259A%2584%25E6%258A%2580%25E6%259C%25AF%25E5%25AD%25A6%25E4%25B9%25A0%25E7%2589%25A9%25E8%25AF%25AD%25E6%259E%259C%25E7%2584%25B6%25E6%259C%2589%25E9%2597%25AE%25E9%25A2%2598/">我的技术学习物语果然有问题</category>
        </item>
        <item>
            <title>SpringCloud</title>
            <link>https://yww52.com/posts/96a41905/</link>
            <guid isPermaLink="true">https://yww52.com/posts/96a41905/</guid>
            <pubDate>Fri, 17 Dec 2021 00:00:00 +0000</pubDate>
            <description>SpringCloud的学习笔记。</description>
            <content:encoded><![CDATA[<img src="https://img.yww52.com/2021/12/2021-12-17top_img.jpg" alt="SpringCloud" style="max-width:100%;height:auto;margin-bottom:15px;" /><h1>服务注册中心</h1><h2>Eureka（停更）</h2><p>Eureka是Netflix开发的一个服务发现框架，SpringCloud将它集成在其子项目<code>spring-cloud-netflix</code>中，主要的功能是<code>服务注册</code>和<code>服务发现</code>。</p><p>Eureka有两个组件，对应了两个角色。服务中心和服务的客户端。</p><h3>Eureka Server</h3><p>引入依赖。</p><pre><code class="language-xml">&lt;dependency&gt;        &lt;groupId&gt;org.springframework.cloud&lt;/groupId&gt;        &lt;artifactId&gt;spring-cloud-starter-netflix-eureka-server&lt;/artifactId&gt;    &lt;/dependency&gt;</code></pre><p>编写配置文件。</p><pre><code class="language-yml">eureka:  instance:    # eureka服务端的实例名称    hostname: localhost:7001  client:    # false表示不向注册中心注册自己这个微服务    register-with-eureka: false    # false表示当前服务是注册中心，并不需要去检索服务    fetch-registry: false    service-url:      # 设置与Eureka Server交互的地址查询服务和注册服务依赖的地址      defaultZone: http://localhost:7001/eureka</code></pre><p>开启Eureka Server服务。</p><pre><code class="language-Java">@EnableEurekaServer@SpringBootApplicationpublic class EurekaMain {    public static void main(String[] args) {        SpringApplication.run(EurekaMain.class, args);    }}</code></pre><h3>Eureka Client</h3><p>引入依赖。</p><pre><code class="language-xml">        &lt;dependency&gt;            &lt;groupId&gt;org.springframework.cloud&lt;/groupId&gt;            &lt;artifactId&gt;spring-cloud-starter-netflix-eureka-client&lt;/artifactId&gt;        &lt;/dependency&gt;</code></pre><p>编写配置文件。</p><pre><code class="language-yml">eureka:  client:    # 表示是否将当前服务注册进注册中心，默认为true    register-with-eureka: true    # 是否从注册中心抓取已有的注册信息，默认为true。单节点无所谓，集群一定要为true才能配合ribbon使用负载均衡    fetch-registry: true    service-url:      # 服务中心的地址      defaultZone: http://localhost:7001/eureka  instance:    # 实例名称    instance-id: payment8001    # 访问路径显示IP地址    prefer-ip-address: true</code></pre><p>开启服务。</p><pre><code class="language-Java">@EnableEurekaClient@SpringBootApplicationpublic class PaymentMain {    public static void main(String[] args) {        SpringApplication.run(PaymentMain.class, args);    }}</code></pre><h3>Eureka的集群</h3><p>集群的配置主要就是要注意，两两注册。</p><p>两个Server端。</p><pre><code class="language-yml">eureka:  instance:    hostname: eureka7001.com  client:    register-with-eureka: false    fetch-registry: false    service-url:      # 集群就指向其他服务中心的地址，单机版就指向自己      defaultZone: http://eureka7002.com:7002/eureka</code></pre><pre><code class="language-yml">eureka:  instance:    hostname: eureka7002.com  client:    register-with-eureka: false    fetch-registry: false    service-url:      # 集群就指向其他服务中心的地址，单机版就指向自己      defaultZone: http://eureka7001.com:7001/eureka</code></pre><p>Client端的配置。</p><pre><code class="language-yml">eureka:  client:    register-with-eureka: true    fetch-registry: true    service-url:      # 将全部的注册中心的地址写入      defaultZone: http://eureka7001.com:7001/eureka,http://eureka7002.com:7002/eureka  instance:    instance-id: payment8001    prefer-ip-address: true</code></pre><h3>服务发现</h3><p>通过发现Client的服务发现，来获取Client端的服务信息。</p><p>主启动类中开启服务发现。</p><pre><code class="language-Java">@SpringBootApplication@EnableEurekaClient@EnableDiscoveryClientpublic class PaymentMain {    public static void main(String[] args) {        SpringApplication.run(PaymentMain.class, args);    }}</code></pre><p>服务发现的简单使用。</p><pre><code class="language-Java">@RestController@Slf4j@RequestMapping(&quot;/payment&quot;)public class PaymentController {    /**     * 引入服务发现     */    @Resource    private DiscoveryClient discoveryClient;    /**     * 简单地获取服务和实例信息     */    @GetMapping(&quot;/discovery&quot;)    public Result&lt;DiscoveryClient&gt; discovery() {        // 获取注册的微服务列表信息        List&lt;String&gt; services = discoveryClient.getServices();        for (String service : services) {            log.info(&quot;服务------&quot; + service);        }        // 根据具体的微服务ID获取其中所有的实例        List&lt;ServiceInstance&gt; instances = discoveryClient.getInstances(&quot;CLOUD-PAYMENT-PROVIDER&quot;);        for (ServiceInstance instance : instances) {            log.info(&quot;服务ID-----&quot; + instance.getServiceId());            log.info(&quot;主机名称-----&quot; + instance.getHost());            log.info(&quot;实例端口号-----&quot; + instance.getPort());            log.info(&quot;实例地址&quot; + instance.getUri());        }        return new Result&lt;&gt;(200,&quot;获取信息成功&quot;,this.discoveryClient);    }    }</code></pre><h3>Eureka的自我保护</h3><h4>自我保护机制</h4><p>在Eureka的服务注册中心会看到一行红字，看到了这段红字提示，就代表Eureka进入了自我保护机制。</p><blockquote><p>EMERGENCY! EUREKA MAY BE INCORRECTLY CLAIMING INSTANCES ARE UP WHEN THEY&#39;RE NOT. RENEWALS ARE LESSER THAN THRESHOLD AND HENCE THE INSTANCES ARE NOT BEING EXPIRED JUST TO BE SAFE.</p></blockquote><p>简单的机器翻译为。</p><blockquote><p>紧急情况！ EUREKA 可能不正确地声称实例已启动，但实际上并非如此。续订少于阈值，因此实例不会只是为了安全而过期。</p></blockquote><p>保护模式主要用于一组客户端和服务端之间存在网络分区场景下的保护，一旦进入保护模式，服务端将会尝试保护器服务注册表中的信息，不删除服务注册中的数据，也就是不会注销任何微服务。简单来说就是服务注册中心中的某一个Client微服务不可用了，Eureka并不会立刻清理该微服务信息，依旧会保存在服务注册中心。</p><blockquote><p>默认情况下，如果EurekaServer在一定时间内没有接收到某个微服务实例的心跳，Eureka会注销该实例（默认90秒）。</p><p>如果出现了网络分区的情况，Client与Server之间无法正常通信，这样的注销行为就会很不安全，因为有可能是因为网络问题导致无法接收到心跳，而不是该Client状态不健康，就不应该立刻注销该微服务。Eureka通过自我保护模式来解决了这个问题。</p><p>当EurekaServer节点短时间内丢失过多Client时，该节点就会进入自我保护模式。自我保护模式中，Server不会注销任何服务实例，即使是不健康的实例，也会保留。</p></blockquote><p>所以Eureka属于CAP分支中的<code>AP</code>分支。</p><h4>如何关闭自我保护</h4><p>修改配置就可以关闭自我保护。</p><pre><code class="language-yml">eureka:  instance:    hostname: eureka7001.com  server:    # 关闭自我保护机制,默认是开启的    enable-self-preservation: false    # 间隔时间    eviction-interval-timer-in-ms: 2000  client:    register-with-eureka: false    fetch-registry: false    service-url:      defaultZone: http://eureka7002.com:7002/eureka</code></pre><p>设置客户端向服务端发送心跳配置。</p><pre><code class="language-yml">eureka:  client:    register-with-eureka: true    fetch-registry: true    service-url:      defaultZone: http://eureka7001.com:7001/eureka,http://eureka7002.com:7002/eureka  instance:    instance-id: payment8001    prefer-ip-address: true    # EurekaClient向服务端发送心跳的时间间隔，默认为30秒，单位为秒    lease-renewal-interval-in-seconds: 30    # Eureka服务端在收到最后一次心跳后的等待时间上限，超时就剔除，默认为90秒，单位为秒    lease-expiration-duration-in-seconds: 90</code></pre><h2>Consul</h2><h3>简介</h3><p>官网地址<a href="https://www.consul.io/">https://www.consul.io/</a></p><p>官网的介绍。</p><blockquote><p>Consul 是一种服务网格解决方案，提供具有服务发现、配置和分段功能的全功能控制平面。这些功能中的每一个都可以根据需要单独使用，也可以一起使用以构建完整的服务网格。</p><p>Consul 需要一个数据平面并支持代理和本地集成模型。</p><p>Consul 附带一个简单的内置代理，因此一切都可以开箱即用，而且还支持 3rd 方代理集成，例如 Envoy。</p></blockquote><p>主要特点。</p><blockquote><ol><li><p>服务发现：Consul 的客户端可以注册一个服务，例如 api或mysql，其他客户端可以使用 Consul 来发现给定服务的提供者。使用 DNS 或 HTTP，应用程序可以轻松找到它们所依赖的服务。</p></li><li><p>健康检查：Consul 客户端可以提供任意数量的健康检查，要么与给定的服务相关联（“网络服务器是否返回 200 OK”），要么与本地节点（“内存利用率低于 90%”）相关联。操作员可以使用此信息来监视集群健康状况，并且服务发现组件可以使用它来将流量从不健康的主机路由出去。</p></li><li><p>KV 存储：应用程序可以将 Consul 的分层键/值存储用于多种目的，包括动态配置、功能标记、协调、领导选举等。简单的 HTTP API 使其易于使用。</p></li><li><p>安全服务通信：Consul 可以为服务生成和分发 TLS 证书，以建立相互的 TLS 连接。 意图 可用于定义允许哪些服务进行通信。可以通过实时更改意图轻松管理服务分段，而不是使用复杂的网络拓扑和静态防火墙规则。</p></li><li><p>多数据中心：Consul 支持开箱即用的多个数据中心。这意味着 Consul 的用户不必担心构建额外的抽象层以扩展到多个区域。</p></li></ol></blockquote><h3>下载使用</h3><p>win10下载之后就只有一个<code>consul.exe</code>文件，直接在文件夹中使用CMD即可。</p><pre><code class="language-cmd"># 查看版本信息consul -version# 使用开发模式启动consulconsul agent -dev</code></pre><p>然后访问consul的首页。<a href="http://localhost:8500">http://localhost:8500</a></p><h3>注册服务</h3><p>引入依赖。</p><pre><code class="language-xml">        &lt;dependency&gt;            &lt;groupId&gt;org.springframework.cloud&lt;/groupId&gt;            &lt;artifactId&gt;spring-cloud-starter-consul-discovery&lt;/artifactId&gt;        &lt;/dependency&gt;</code></pre><p>修改配置文件。</p><pre><code class="language-yml">server:  port: 8006spring:  application:    name: consul-provider-payment  cloud:    # consul注册中心地址    consul:      host: localhost      port: 8500      discovery:        service-name: ${spring.application.name}</code></pre><p>开启服务发现。</p><pre><code class="language-Java">@SpringBootApplication@EnableDiscoveryClientpublic class PaymentMain {    public static void main(String[] args) {        SpringApplication.run(PaymentMain.class, args);    }}</code></pre><p>在Consul的控制台就能查看到该服务的该实例。</p><h2>服务注册中心异同点</h2><table><thead><tr><th align="center">组件名</th><th align="center">实现语言</th><th align="center">CAP</th><th align="center">服务健康检查</th><th align="center">对外暴露接口</th><th align="center">SpringCloud集合</th></tr></thead><tbody><tr><td align="center">Eureka</td><td align="center">Java</td><td align="center">AP</td><td align="center">可配支持</td><td align="center">HTTP</td><td align="center">集成</td></tr><tr><td align="center">Consul</td><td align="center">GO</td><td align="center">CP</td><td align="center">支持</td><td align="center">HTTP/DNS</td><td align="center">集成</td></tr><tr><td align="center">Zookeeper</td><td align="center">Java</td><td align="center">CP</td><td align="center">支持</td><td align="center">客户端</td><td align="center">集成</td></tr></tbody></table><h2>Nacos</h2><p>Nacos可以实现服务注册中心，后续提出来专门学习。</p><h1>服务调用</h1><h2>Ribbon（维护模式）</h2><p>Ribbon是基于Neftlix Ribbon实现的一套客户端负载均衡工具，主要功能是提供客户端的软件负载均衡算法和服务调用。</p><p>工作流程。</p><ol><li>先选择EurekaServer，优先选择在同一个区域内负载较少的server</li><li>在根据用户指定的策略，再从server获取的服务注册列表选择一个地址</li></ol><p>Ribbon就是负载均衡加上RestTemplate调用。</p><h3>使用Ribbon</h3><p>引入依赖。</p><pre><code class="language-xml">    &lt;dependency&gt;      &lt;groupId&gt;org.springframework.cloud&lt;/groupId&gt;      &lt;artifactId&gt;spring-cloud-starter-netflix-ribbon&lt;/artifactId&gt;    &lt;/dependency&gt;&lt;!--  只要引入了这个client依赖，就不用引入上面的ribbon依赖了lient是包含了ribbon依赖的，不想用eureka就直接引入上面的ribbon依赖  --&gt;    &lt;dependency&gt;        &lt;groupId&gt;org.springframework.cloud&lt;/groupId&gt;        &lt;artifactId&gt;spring-cloud-starter-netflix-eureka-client&lt;/artifactId&gt;    &lt;/dependency&gt;</code></pre><p>Ribbon是基于RestTemplate调用实现的，所以要使用Ribbon还需要去RestTemplate配置类上开启。</p><pre><code class="language-Java">@Configurationpublic class RestConfig {    /**     *  LoadBalanced开启负载均衡     */    @Bean    @LoadBalanced    public RestTemplate getRestTemplate() {        return new RestTemplate();    }}</code></pre><h3>Ribbon常见的负载均衡实现</h3><p>Ribbon的核心接口IROLE，用于制定一个负载均衡规则的实现。</p><pre><code class="language-Java">public interface IRule{    /*     * choose one alive server from lb.allServers or     * lb.upServers according to key     *      * @return choosen Server object. NULL is returned if none     *  server is available      */    public Server choose(Object key);        public void setLoadBalancer(ILoadBalancer lb);        public ILoadBalancer getLoadBalancer();    }</code></pre><p>它的一些实现类就是一些常用的负载均衡规则。</p><ol><li><p><code>com.netflix.loadbalancer.RoundRobinRule</code></p><p>轮询</p></li><li><p><code>com.netflix.loadbalancer.RandomRule</code></p><p>随机</p></li><li><p><code>com.netflix.loadbalancer.RetryRule</code></p><p>先按照轮询的策略获取服务，如果获取服务失败则在指定时间内进行重试，获取可用的服务</p></li><li><p><code>WeightedResponseTimeRule</code></p><p>对轮询的扩展，响应速度越快的实例选择权重越大，越容易被选择</p></li><li><p><code>BestAvailableRule</code></p><p>会过滤调由于多次访问故障而处于断路器跳闸状态的服务，然后选择一个并发量最小的服务</p></li><li><p><code>AvailabilityFilteringRule</code></p><p>先过滤故障实例，再选择并发较小的实例</p></li><li><p><code>ZoneAvoidanceRule</code></p><p>默认规则，复合判断server所在区域的性能和server的可用性选择服务器</p></li></ol><h3>修改负载均衡规则</h3><p>官方说明不能被<code>@ComponentScan</code>注解给扫描到，而<code>@SpringBootApplication</code>启动类注解中是包含了<code>@ComponentScan</code>的，所以不能放在主启动类的包下。</p><p>创建配置类。</p><pre><code class="language-Java">/** * &lt;p&gt; *     自定义负载均衡规则类 * &lt;/p&gt; * * @author yww * @version 1.0 **/@Configuration// 使用MyRule类名可能会出现问题public class MySelfRule {    @Bean    public IRule myRule() {        return new RandomRule();    }}</code></pre><p>然后在主启动类下开启自定义规则。</p><pre><code class="language-Java">@SpringBootApplication@EnableEurekaClient@RibbonClient(name = &quot;CLOUD-PAYMENT-PROVIDER&quot;, configuration = MySelfRule.class)public class OrderMain {    public static void main(String[] args) {        SpringApplication.run(OrderMain.class,args);    }}</code></pre><h3>轮询算法的实现</h3><p>轮询的原理就是通过取模，确定请求的位置，类似于hash的实现。</p><p>实现的两个方法。（com.netflix.loadbalancer.RoundRobinRule）</p><pre><code class="language-Java">    public Server choose(ILoadBalancer lb, Object key) {        if (lb == null) {            log.warn(&quot;no load balancer&quot;);            return null;        }        Server server = null;        int count = 0;        while (server == null &amp;&amp; count++ &lt; 10) {            List&lt;Server&gt; reachableServers = lb.getReachableServers();            List&lt;Server&gt; allServers = lb.getAllServers();            int upCount = reachableServers.size();            int serverCount = allServers.size();            if ((upCount == 0) || (serverCount == 0)) {                log.warn(&quot;No up servers available from load balancer: &quot; + lb);                return null;            }            int nextServerIndex = incrementAndGetModulo(serverCount);            server = allServers.get(nextServerIndex);            if (server == null) {                /* Transient. */                Thread.yield();                continue;            }            if (server.isAlive() &amp;&amp; (server.isReadyToServe())) {                return (server);            }            // Next.            server = null;        }        if (count &gt;= 10) {            log.warn(&quot;No available alive servers after 10 tries from load balancer: &quot;                    + lb);        }        return server;    }    private int incrementAndGetModulo(int modulo) {        for (;;) {            int current = nextServerCyclicCounter.get();            int next = (current + 1) % modulo;            if (nextServerCyclicCounter.compareAndSet(current, next))                return next;        }    }</code></pre><h2>LoadBalancer</h2><p>可能以后会学习。</p><h2>Feign</h2><p>因为有Feign的替代版了，所以这个暂时不考虑，详情请参考OpenFeign。</p><h2>OpenFeign</h2><p>Feign是一个声明式WebService客户端，使用在服务消费端。</p><p>前面使用的是Ribbon+RestTemplate的服务调用方法，利用RestTemplate对http请求进行封装处理，调用其他服务的接口。Feign在此的基础上进一步封装，只需要添加一个注解，即可实现服务调用。</p><p>Feign是SpringCloud组件中一个轻量级的RESTful的HTTP服务客户端，Feign内置了Ribbon，用来做客户端负载均衡。</p><p>OpenFeign是在Feign的基础上支持了SpringMVC的注解，实现类中做负载均衡并调用其他服务。</p><h3>简单使用</h3><p>引入依赖。</p><pre><code>&lt;dependency&gt;            &lt;groupId&gt;org.springframework.cloud&lt;/groupId&gt;            &lt;artifactId&gt;spring-cloud-starter-openfeign&lt;/artifactId&gt;        &lt;/dependency&gt;</code></pre><p>这个依赖是包含了Ribbon依赖的，所以使用其中的负载均衡功能。</p><p>编写配置文件。</p><pre><code class="language-yml">server:  port: 80spring:  application:    name: feign-payment-consumereureka:  client:    register-with-eureka: false    service-url:      defaultZone: http://eureka7001.com:7001/eureka,http://eureka7002.com:7002/eureka</code></pre><p>开启使用。</p><pre><code class="language-Java">@SpringBootApplication@EnableFeignClientspublic class FeignOrderMain {    public static void main(String[] args) {        SpringApplication.run(FeignOrderMain.class, args);    }}</code></pre><p>创建业务服务接口。即通过这个接口，Feign就会调用指定服务的接口。</p><pre><code class="language-Java">/** * &lt;p&gt; *     业务服务接口 * &lt;/p&gt; * * @author yww * @version 1.0 * @date 2021/12/14 18:55 **/// 指定调用的服务（服务提供者）@FeignClient(&quot;CLOUD-PAYMENT-PROVIDER&quot;)public interface PaymentFeignService {        /**     * 通过ID获取订单（调用指定服务的指定接口）     * @param id    数据ID     * @return      订单信息     */    @GetMapping(&quot;/payment/getById/{id}&quot;)    Result&lt;Payment&gt; getById(@PathVariable(&quot;id&quot;) Long id);}</code></pre><p>前端控制器调用业务接口。</p><pre><code class="language-Java">@RestController@Slf4j@RequestMapping(&quot;/consumer&quot;)public class OrderController {    @Resource    private PaymentFeignService service;    @GetMapping(&quot;getById/{id}&quot;)    public Result&lt;Payment&gt; getById(@PathVariable(&quot;id&quot;)Long id) {        return service.getById(id);    }}</code></pre><p>总结。</p><ol><li>主启动类上开启使用Feign。(@EnableFeignClients)</li><li>编写service业务接口类，指定调用的服务提供者。(@FeignClient(&quot;CLOUD-PAYMENT-PROVIDER&quot;))</li><li>编写的抽象接口就使用服务提供者的接口。</li></ol><h3>超时控制</h3><p>因为是调用服务提供者的接口，所以就可能会出现超时的情况，可用通过配置进行超时控制。</p><p>就比如Feign默认的配置超时时间是1秒，即调用的接口1秒之内没有结果回应，Feign客户端就不会继续等待了，直接返回报错。</p><p>编写配置类</p><pre><code class="language-Java">feign:  client:    config:  # 单位ms       default:        # 建立连接后从服务器读取到可用资源所用的时间        connectTimeout: 5000        # 建立连接建立所用时间        readTimeout: 5000</code></pre><h3>日志打印功能</h3><p>OpenFeign日志级别。</p><ol><li><p>NONE</p><p>默认的，不显示任何日志</p></li><li><p>BASIC</p><p>仅记录请求方法，URL，响应状态码及执行时间</p></li><li><p>HEADERS</p><p>除了BASIC中定义的信息之外，还有请求和响应的头信息</p></li><li><p>FULL</p><p>除了HEADERS中定义的信息之外，还有请求和响应的正文及元数据</p></li></ol><p>编写配置类。</p><pre><code class="language-Java">/** * &lt;p&gt; *     Feign的配置类 * &lt;/p&gt; * * @author yww **/@Configurationpublic class FeignConfig {        @Bean    Logger.Level feignLoggerLevel() {        return Logger.Level.FULL;    }    }</code></pre><p>设置需要打印日志的接口。</p><pre><code class="language-yml">logging:  level:    # 业务接口类，debug是等级     com.yww.springcloud.service.PaymentFeignService: debug</code></pre><h1>服务降级</h1><h2>Hystrix（维护模式）</h2><blockquote><p>服务雪崩</p><p>多个微服务之间的调用的时候，假设微服务A调用微服务B和微服务C，微服务B和微服务C又调用其他的微服务，这就是所谓的“扇出”。</p><p>如果扇出的链路上某个微服务的调用响应时间过长或者不可用，对微服务A的调用就会占用越来越多的系统资源，进而引起系统崩溃，这就是所谓的雪崩效应。</p></blockquote><p>Hystrix是一个用于处理分布式系统的延迟和容错的开源库，比如在分布式系统里，许多依赖不可避免地会调用失败（超时，异常等），Hystrix就是用于保证不会因为一个依赖出现问题而导致整个服务失败地情况，避免级联故障。</p><p>当一个服务发生了故障，无法返回结果，就会通过断路器地故障监控，向调用方返回一个符合预期地备选响应，而不是直接导致整个服务调用的失败。</p><p>Hystrix的主要功能。</p><ol><li>服务降级</li><li>服务熔断</li><li>接近实时的监控</li></ol><h3>一些重要的概念</h3><ul><li><p>服务降级</p><p>服务的负载过大，但是资源是有限的，根据降级的策略牺牲一些服务或者是功能，保证核心服务的正常执行。</p></li><li><p>服务熔断</p><p>当上游服务因为某种原因而调用失败，下游服务为了保证整体服务的可用性，就会不再继续调用失败的上游服务，返回一个设定的结果，快速释放资源。</p></li><li><p>服务限流</p><p>服务限制请求的速率，比如服务设定了一个访问阈值，在一段时间内的访问次数超过了阈值，超过的部分就会直接拒绝访问。</p></li></ul><blockquote><p>降级和熔断</p><p>降级和熔断的概念很相似，但其实是不一样的。</p><p>降级强调的是整体架构的可用性，熔断强调的是当前服务的可用性。</p><p>降级的目的在于应对系统自身的故障，而熔断的目的在于应对当前系统依赖的外部系统或者第三方系统的故障。</p></blockquote><h3>前提业务</h3><p>引入依赖。</p><pre><code class="language-xml">        &lt;dependency&gt;            &lt;groupId&gt;org.springframework.cloud&lt;/groupId&gt;            &lt;artifactId&gt;spring-cloud-starter-netflix-hystrix&lt;/artifactId&gt;        &lt;/dependency&gt;</code></pre><p>编写业务实现类。</p><pre><code class="language-Java">@Servicepublic class PaymentService {    /**     * 模拟正常访问的情况     * @param id ID     * @return  返回OK     */    public String paymentInfoOk(Integer id) {        return &quot;线程池&quot; + Thread.currentThread().getName() + &quot;访问成功, id为: &quot; + id;    }    /**     * 模拟超时访问的情况     * @param id ID     * @return  返回Fault     */    public String paymentInfoTimeOut(Integer id) {        try {            TimeUnit.SECONDS.sleep(3);        } catch (InterruptedException e) {            e.printStackTrace();        }        return &quot;线程池&quot; + Thread.currentThread().getName() + &quot;超时访问, id为: &quot; + id;    }}</code></pre><p>编写前端控制器。</p><pre><code class="language-Java">@RestController@Slf4j@RequestMapping(&quot;payment&quot;)public class PaymentController {    @Resource    private PaymentService service;    @Value(&quot;${server.port}&quot;)    private String port;    @GetMapping(&quot;/ok/{id}&quot;)    public String infoOk(@PathVariable(&quot;id&quot;)Integer id) {        return service.paymentInfoOk(id);    }    @GetMapping(&quot;/timeout/{id}&quot;)    public String infoTimeOut(@PathVariable(&quot;id&quot;)Integer id) {        return service.paymentInfoTimeOut(id);    }}</code></pre><h3>高压力测试</h3><blockquote><p>前提</p><p>访问正常请求接口，基本都是秒回信息。</p><p>访问超时请求接口，则会等待三秒钟才返回信息。</p></blockquote><p>使用JMeter对超时请求接口进行压力测试。</p><p>模拟多线程多个请求访问超时请求接口，自己访问正常请求接口，会发现已经不是秒回信息了，可能会出现超时，停顿的情况。</p><p>因为tomcat的默认的工作线程数被打满了，没有多余的线程来分解压力和处理。</p><p>这还只是服务提供者的自测，若是还有服务消费者，会极大可能出现请求超时的情况，导致服务消费者的请求不能及时完成，拖死客户端。</p><p>正是因为这种情况的出现，所以需要我们进行降级，容错，限流等。</p><blockquote><p>解决的问题</p><ol><li><p>服务端出现超时或者宕机了，调用者不能一直卡死等待，需要服务降级</p></li><li><p>服务端成功返回，但是调用者自己出现故障或又自我要求，自己处理降级</p></li></ol></blockquote><h3>服务提供者的服务降级</h3><p>设置自身调用超时时间，不超过超时时间的就可以正常运行，超过了的需要用指定方法进行处理，作服务降级。</p><p>主启动类开启服务降级。</p><pre><code class="language-Java">@SpringBootApplication@EnableEurekaClient// @EnableCircuitBreaker@EnableCircuitBreakerpublic class HystrixPaymentMain {    public static void main(String[] args) {        SpringApplication.run(HystrixPaymentMain.class, args);    }}</code></pre><p>配置业务接口的超时设置和超时的处理方式。</p><pre><code class="language-Java">@Servicepublic class PaymentService {    /**     * 正常访问的情况     * @param id ID     * @return  返回OK     */    public String paymentInfoOk(Integer id) {        return &quot;线程池&quot; + Thread.currentThread().getName() + &quot;访问成功, id为: &quot; + id;    }    /**     * 超时访问的情况     * 设置2秒钟之内可以正常执行，逻辑里停了3秒，模拟降级情况     * 如果逻辑出现异常或者错误，不是超时的问题，也会进行降级     * @param id ID     * @return  返回Fault     */    @HystrixCommand(fallbackMethod = &quot;timeoutHandler&quot;, commandProperties = {            @HystrixProperty(name = &quot;execution.isolation.thread.timeoutInMilliseconds&quot;,value = &quot;2000&quot;)    })    public String paymentInfoTimeOut(Integer id) {        try {            TimeUnit.SECONDS.sleep(3);        } catch (InterruptedException e) {            e.printStackTrace();        }        return &quot;线程池&quot; + Thread.currentThread().getName() + &quot;超时访问, id为: &quot; + id;    }    public String timeoutHandler(Integer id) {        return &quot;超时请求，请稍后再试！&quot;;    }}</code></pre><h3>服务消费者的服务降级</h3><p>配置文件开启支持服务降级。</p><pre><code class="language-yml">feign:  hystrix:    enabled: true</code></pre><p>主启动类启用。</p><pre><code class="language-Java">@SpringBootApplication@EnableFeignClients@EnableHystrixpublic class FeignHystrixMain {    public static void main(String[] args) {        SpringApplication.run(FeignHystrixMain.class, args);    }}</code></pre><p>业务类接口处理。</p><pre><code class="language-Java">@RestController@Slf4j@RequestMapping(&quot;consumer&quot;)public class HystrixController {    @Resource    private FeignConsumer server;    @GetMapping(&quot;/ok/{id}&quot;)    public String ok(@PathVariable(&quot;id&quot;)Integer id) {        return server.infoOk(id);    }    @GetMapping(&quot;/timeout/{id}&quot;)    @HystrixCommand(fallbackMethod = &quot;timeoutHandler&quot;, commandProperties = {            @HystrixProperty(name = &quot;execution.isolation.thread.timeoutInMilliseconds&quot;,value = &quot;1000&quot;)    })    public String timeout(@PathVariable(&quot;id&quot;)Integer id) {        return server.infoTimeOut(id);    }        public String timeoutHandler(@PathVariable(&quot;id&quot;)Integer id) {        return &quot;消费端调用超时，请稍后再试！&quot; + id;    }}</code></pre><blockquote><p>一般是调用端配置超时设置，在哪里配置或者两端都配置还要根据具体业务需求。</p></blockquote><h3>全局服务降级</h3><p>刚刚演示的是某一个方法的服务降级，若是方法过多，就会导致降级处理的方法也会变多，所以大多数都是会用同一个降级处理方法。</p><ol><li>在controller的地方添加降级配置<code>@DefaultProperties</code></li><li>编写全局降级处理方法</li><li>给需要降级的接口添加<code>@HystrixCommand</code>注解，不指定降级处理方法就使用控制器全局的配置降级方法。</li></ol><pre><code class="language-Java">@RestController@Slf4j@RequestMapping(&quot;consumer&quot;)@DefaultProperties(defaultFallback = &quot;globalTimeout&quot;, commandProperties = {        @HystrixProperty(name = &quot;execution.isolation.thread.timeoutInMilliseconds&quot;,value = &quot;1000&quot;)})public class HystrixController {    @Resource    private FeignConsumer server;    @GetMapping(&quot;/ok/{id}&quot;)    public String ok(@PathVariable(&quot;id&quot;)Integer id) {        return server.infoOk(id);    }    @GetMapping(&quot;/timeout/{id}&quot;)    @HystrixCommand(fallbackMethod = &quot;timeoutHandler&quot;, commandProperties = {            @HystrixProperty(name = &quot;execution.isolation.thread.timeoutInMilliseconds&quot;,value = &quot;1000&quot;)    })    public String timeout(@PathVariable(&quot;id&quot;)Integer id) {        return server.infoTimeOut(id);    }    @GetMapping(&quot;/timeout/{id}&quot;)    // 不具体指定降级配置    @HystrixCommand    public String timeout2(@PathVariable(&quot;id&quot;)Integer id) {        return server.infoTimeOut(id);    }    /**     * 特定的方法的服务降级     */    public String timeoutHandler(@PathVariable(&quot;id&quot;)Integer id) {        return &quot;消费端调用超时，请稍后再试！&quot; + id;    }        /**     * 全局的降级处理     */    public String globalTimeout() {        return &quot;全局降级！&quot;;    }}</code></pre><h3>指定服务降级处理类</h3><p>因为一般不会在controller里进行处理异常情况，所以我们可以直接在业务接口处进行处理降级。</p><pre><code class="language-Java">// 指定服务降级的处理类@FeignClient(value = &quot;HYSTRIX-PROVIDER-PAYMENT&quot;,fallback = FeignConsumerImpl.class)public interface FeignConsumer {    /**     * 正常访问     * @param id    ID     * @return      响应     */    @GetMapping(&quot;/payment/ok/{id}&quot;)    String infoOk(@PathVariable(&quot;id&quot;)Integer id);    /**     * 超时访问     * @param id    ID     * @return      响应     */    @GetMapping(&quot;/payment/timeout/{id}&quot;)    String infoTimeOut(@PathVariable(&quot;id&quot;)Integer id);}</code></pre><pre><code class="language-Java">/** * &lt;p&gt; *     服务降级处理类 * 首先先实现业务接口，然后处理降级情况。 * &lt;/p&gt; * * @author yww **/@Componentpublic class FeignConsumerImpl implements FeignConsumer{    @Override    public String infoOk(Integer id) {        return &quot;系统繁忙，请稍后再试!&quot;;    }    @Override    public String infoTimeOut(Integer id) {        return &quot;系统繁忙，请稍后再试!&quot;;    }}</code></pre><blockquote><p>注意这两种降级的方法还是有点区别的。</p><p>在业务接口的降级处理其实是配置Feign的降级，也就是服务提供端出现问题的处理，在controller的降级处理其实算是在服务消费者的降级处理。</p></blockquote><h3>服务熔断</h3><p>服务熔断是一种保护机制，当扇出链路的某个微服务出错不可用或者响应时间太长，会进行服务的降级，进而熔断该节点微服务的调用，快速返回错误的响应信息，当检测到该节点微服务调用响应正常后，恢复调用链路。</p><p>简单的说，就是当一段时间内失败的调用达到指定的阈值后，就会触发熔断机制。</p><p>下面配置一下服务端的服务熔断。</p><p>在业务接口类配置服务熔断设置。</p><pre><code class="language-Java">@Servicepublic class PaymentService {    /**     *  服务熔断     *  circuitBreaker.enabled                    是否开启断路器     *  circuitBreaker.requestVolumeThreshold       请求的次数，默认20个请求     *  circuitBreaker.sleepWindowInMilliseconds    时间窗口期，默认十秒     *  circuitBreaker.errorThresholdPercentage     请求失败率的峰值，默认50%失败率     *  配置组合的意义     *  在10000ms内，不足20次，不会打开断路器     *在10000ms内，超过了20次，若是失败率达到了50%就会触发断路器     */    @HystrixCommand(fallbackMethod = &quot;paymentCircuitBreakerFallback&quot;, commandProperties = {            @HystrixProperty(name = &quot;circuitBreaker.enabled&quot;, value = &quot;true&quot;),            @HystrixProperty(name = &quot;circuitBreaker.requestVolumeThreshold&quot;, value = &quot;10&quot;),            @HystrixProperty(name = &quot;circuitBreaker.sleepWindowInMilliseconds&quot;, value = &quot;10000&quot;),            @HystrixProperty(name = &quot;circuitBreaker.errorThresholdPercentage&quot;, value = &quot;60&quot;)    })    public String paymentCircuitBreaker(@PathVariable(&quot;id&quot;) Integer id) {        if (id &lt; 0) {            throw new RuntimeException();        }        String number = IdUtil.simpleUUID();        return Thread.currentThread().getName() + &quot;\t&quot; + &quot;调用成功， UUID为&quot; + number;    }    public String paymentCircuitBreakerFallback(@PathVariable(&quot;id&quot;) Integer id) {        return &quot;id 不能为负数，请稍后再试！ 当前ID为：&quot; + id;    }    }</code></pre><blockquote><p>断路器开启之后，请求就不会调用主逻辑了，即不用判断请求正确还是错误，直接调用fallback，直接服务降级。</p><p>一段时间（默认是5秒），断路器是半开状态，会让其中一个请求进行转发，如果成功，断路器关闭，若失败，则继续开启</p><p>熔断类型。</p><ol><li>Open。请求不再调用当前服务，直接进行服务降级处理，打开时间达到设置时间，进入半熔断状态。</li><li>Half Open。部分请求根据规则调用当前服务，若是请求成功并且符合规则则认为服务恢复正常，关闭熔断。</li><li>Closed。不对服务进行熔断处理。</li></ol></blockquote><h3>服务熔断所有的配置</h3><pre><code class="language-Java">@HystrixCommand(fallbackMethod = &quot;fallbackMethod&quot;,                 groupKey = &quot;strGroupCommand&quot;,                 commandKey = &quot;strCommand&quot;,                 threadPoolKey = &quot;strThreadPool&quot;,                commandProperties = {                    // 设置隔离策略，THREAD 表示线程池 SEMAPHORE：信号池隔离                    @HystrixProperty(name = &quot;execution.isolation.strategy&quot;, value = &quot;THREAD&quot;),                    // 当隔离策略选择信号池隔离的时候，用来设置信号池的大小（最大并发数）                    @HystrixProperty(name = &quot;execution.isolation.semaphore.maxConcurrentRequests&quot;, value = &quot;10&quot;),                    // 配置命令执行的超时时间                    @HystrixProperty(name = &quot;execution.isolation.thread.timeoutinMilliseconds&quot;, value = &quot;10&quot;),                    // 是否启用超时时间                    @HystrixProperty(name = &quot;execution.timeout.enabled&quot;, value = &quot;true&quot;),                    // 执行超时的时候是否中断                    @HystrixProperty(name = &quot;execution.isolation.thread.interruptOnTimeout&quot;, value = &quot;true&quot;),                    // 执行被取消的时候是否中断                    @HystrixProperty(name = &quot;execution.isolation.thread.interruptOnCancel&quot;, value = &quot;true&quot;),                    // 允许回调方法执行的最大并发数                    @HystrixProperty(name = &quot;fallback.isolation.semaphore.maxConcurrentRequests&quot;, value = &quot;10&quot;),                    // 服务降级是否启用，是否执行回调函数                    @HystrixProperty(name = &quot;fallback.enabled&quot;, value = &quot;true&quot;),                    // 是否启用断路器                    @HystrixProperty(name = &quot;circuitBreaker.enabled&quot;, value = &quot;true&quot;),                    // 该属性用来设置在滚动时间窗中，断路器熔断的最小请求数。例如，默认该值为 20 的时候，如果滚动时间窗（默认10秒）内仅收到了19个请求， 即使这19个请求都失败了，断路器也不会打开。                    @HystrixProperty(name = &quot;circuitBreaker.requestVolumeThreshold&quot;, value = &quot;20&quot;),                    // 该属性用来设置在滚动时间窗中，表示在滚动时间窗中，在请求数量超过 circuitBreaker.requestVolumeThreshold 的情况下，如果错误请求数的百分比超过50, 就把断路器设置为 &quot;打开&quot; 状态，否则就设置为 &quot;关闭&quot; 状态。                    @HystrixProperty(name = &quot;circuitBreaker.errorThresholdPercentage&quot;, value = &quot;50&quot;),                    // 该属性用来设置当断路器打开之后的休眠时间窗。 休眠时间窗结束之后，会将断路器置为 &quot;半开&quot; 状态，尝试熔断的请求命令，如果依然失败就将断路器继续设置为 &quot;打开&quot; 状态，如果成功就设置为 &quot;关闭&quot; 状态。                    @HystrixProperty(name = &quot;circuitBreaker.sleepWindowinMilliseconds&quot;, value = &quot;5000&quot;),                    // 断路器强制打开                    @HystrixProperty(name = &quot;circuitBreaker.forceOpen&quot;, value = &quot;false&quot;),                    // 断路器强制关闭                    @HystrixProperty(name = &quot;circuitBreaker.forceClosed&quot;, value = &quot;false&quot;),                    // 滚动时间窗设置，该时间用于断路器判断健康度时需要收集信息的持续时间                    @HystrixProperty(name = &quot;metrics.rollingStats.timeinMilliseconds&quot;, value = &quot;10000&quot;),                    // 该属性用来设置滚动时间窗统计指标信息时划分&quot;桶&quot;的数量，断路器在收集指标信息的时候会根据设置的时间窗长度拆分成多个 &quot;桶&quot; 来累计各度量值，每个&quot;桶&quot;记录了一段时间内的采集指标。                    // 比如 10 秒内拆分成 10 个&quot;桶&quot;收集这样，所以 timeinMilliseconds 必须能被 numBuckets 整除。否则会抛异常                    @HystrixProperty(name = &quot;metrics.rollingStats.numBuckets&quot;, value = &quot;10&quot;),                    // 该属性用来设置对命令执行的延迟是否使用百分位数来跟踪和计算。如果设置为 false, 那么所有的概要统计都将返回 -1。                    @HystrixProperty(name = &quot;metrics.rollingPercentile.enabled&quot;, value = &quot;false&quot;),                    // 该属性用来设置百分位统计的滚动窗口的持续时间，单位为毫秒。                    @HystrixProperty(name = &quot;metrics.rollingPercentile.timeInMilliseconds&quot;, value = &quot;60000&quot;),                    // 该属性用来设置百分位统计滚动窗口中使用 “ 桶 ”的数量。                    @HystrixProperty(name = &quot;metrics.rollingPercentile.numBuckets&quot;, value = &quot;60000&quot;),                    // 该属性用来设置在执行过程中每个 “桶” 中保留的最大执行次数。如果在滚动时间窗内发生超过该设定值的执行次数，                    // 就从最初的位置开始重写。例如，将该值设置为100, 滚动窗口为10秒，若在10秒内一个 “桶 ”中发生了500次执行，                    // 那么该 “桶” 中只保留 最后的100次执行的统计。另外，增加该值的大小将会增加内存量的消耗，并增加排序百分位数所需的计算时间。                    @HystrixProperty(name = &quot;metrics.rollingPercentile.bucketSize&quot;, value = &quot;100&quot;),                                        // 该属性用来设置采集影响断路器状态的健康快照（请求的成功、 错误百分比）的间隔等待时间。                    @HystrixProperty(name = &quot;metrics.healthSnapshot.intervalinMilliseconds&quot;, value = &quot;500&quot;),                    // 是否开启请求缓存                    @HystrixProperty(name = &quot;requestCache.enabled&quot;, value = &quot;true&quot;),                    // HystrixCommand的执行和事件是否打印日志到 HystrixRequestLog 中                    @HystrixProperty(name = &quot;requestLog.enabled&quot;, value = &quot;true&quot;),                },                threadPoolProperties = {                    // 该参数用来设置执行命令线程池的核心线程数，该值也就是命令执行的最大并发量                    @HystrixProperty(name = &quot;coreSize&quot;, value = &quot;10&quot;),                    // 该参数用来设置线程池的最大队列大小。当设置为 -1 时，线程池将使用 SynchronousQueue 实现的队列，否则将使用 LinkedBlockingQueue 实现的队列。                    @HystrixProperty(name = &quot;maxQueueSize&quot;, value = &quot;-1&quot;),                    // 该参数用来为队列设置拒绝阈值。 通过该参数， 即使队列没有达到最大值也能拒绝请求。                    // 该参数主要是对 LinkedBlockingQueue 队列的补充,因为 LinkedBlockingQueue 队列不能动态修改它的对象大小，而通过该属性就可以调整拒绝请求的队列大小了。                    @HystrixProperty(name = &quot;queueSizeRejectionThreshold&quot;, value = &quot;5&quot;),                }               )public String doThing() {...}</code></pre><h3>HystrixDashboard</h3><p>HystrixDashboard准实时的调用监控，持续的通过记录所有通过Hystrix发起的请求的执行信息。</p><p>引入依赖。</p><pre><code class="language-xml">        &lt;dependency&gt;            &lt;groupId&gt;org.springframework.cloud&lt;/groupId&gt;            &lt;artifactId&gt;spring-cloud-starter-netflix-hystrix-dashboard&lt;/artifactId&gt;        &lt;/dependency&gt;</code></pre><p>主启动类开启支持。</p><pre><code class="language-java">@SpringBootApplication@EnableHystrixDashboardpublic class HystrixDashBoard {    public static void main(String[] args) {        SpringApplication.run(HystrixDashBoard.class, args);    }}</code></pre><p>然后启动就可以访问到页面了。<a href="http://localhost:9001/hystrix">http://localhost:9001/hystrix</a></p><p>被监控的服务也要设置一下。</p><pre><code class="language-Java">@SpringBootApplication@EnableEurekaClient@EnableCircuitBreakerpublic class HystrixPaymentMain {    public static void main(String[] args) {        SpringApplication.run(HystrixPaymentMain.class, args);    }   /**     * 此配置是为了服务监控而配置     */    @Bean    public ServletRegistrationBean getServlet() {        HystrixMetricsStreamServlet streamServlet = new HystrixMetricsStreamServlet();        ServletRegistrationBean registrationBean = new ServletRegistrationBean(streamServlet);        registrationBean.setLoadOnStartup(1);        registrationBean.addUrlMappings(&quot;/hystrix.stream&quot;);        registrationBean.setName(&quot;HystrixMetricsStreamServlet&quot;);        return registrationBean;    }}</code></pre><p>然后在<a href="http://localhost:9001/hystrix">http://localhost:9001/hystrix</a>填写监控的地址。</p><p><a href="http://localhost:8003/hystrix.stream">http://localhost:8003/hystrix.stream</a></p><h2>Sentinel</h2><p>后续单独提出，这里只需要知道能实现服务降级。</p><h1>服务网关</h1><h2>Zuul</h2><p>暂不考虑。</p><h2>Zuul2</h2><p>Zuul2是Zuul的替代版，但是由于开发组的情况复杂，所以暂不考虑。</p><h2>SpringCloudGateway</h2><p> SpringCloudGateway新一代的网关，主要当作为服务的入口。</p><h3>重要的概念</h3><ol><li><p>路由（Route）</p><p>路由是构建网关的基本模块，由ID，目标URI，一系列的断言和过滤器组成，如果断言为true则匹配该路由</p></li><li><p>断言（Predicate）</p><p>参考了Java8的一个重要的函数接口<code>Predicate</code>，开发人员可以匹配HTTP请求中的所有内容，如果请求与断言相匹配则进行路由</p></li><li><p>过滤（Filter）</p><p>使用过滤器，可以在请求被路由前或者请求之后对请求进行修改</p></li></ol><h3>简单的使用（yml配置）</h3><p>引入依赖。</p><pre><code class="language-xml">        &lt;dependency&gt;            &lt;groupId&gt;org.springframework.cloud&lt;/groupId&gt;            &lt;artifactId&gt;spring-cloud-starter-gateway&lt;/artifactId&gt;        &lt;/dependency&gt;</code></pre><p>编写配置文件。</p><pre><code class="language-yml">server:  port: 9527spring:  application:    name: cloud-gatewayeureka:  instance:    hostname: cloud-gateway  client:    register-with-eureka: true    fetch-registry: true    service-url:      defaultZone: http://eureka7001.com:7001/eureka</code></pre><p>主启动类。</p><pre><code class="language-Java">@SpringBootApplication@EnableEurekaClientpublic class GatewayMain {    public static void main(String[] args) {        SpringApplication.run(GatewayMain.class, args);    }}</code></pre><p>网关的配置主要是在yml配置文件。</p><pre><code class="language-yml">spring:  application:    name: cloud-gateway  cloud:    gateway:      routes:        - id: routh1                # 路由ID，需要唯一          uri: http://localhost:8001  # 匹配后提供的服务路由地址          predicates:               # 断言，路径匹配进行路由            - Path=/payment/**</code></pre><blockquote><p>启动网关服务的时候，出现了一个小错误。</p><p>原因是因为多添加了一个web依赖导致的，网关服务是不需要web依赖的，删除即可。</p></blockquote><p>此时网关服务启动了，只要符合断言的路径就会按照服务地址访问。</p><blockquote><p><a href="http://localhost:9527/payment/">http://localhost:9527/payment/</a>** </p><p>这样的路由就是符合路由的，经过网关转发就会转发到下面的路由。</p><p><a href="http://localhost:8001/payment/">http://localhost:8001/payment/</a>** </p></blockquote><h3>编码方式配置</h3><pre><code class="language-Java">package com.yww.springcloud.config;import org.springframework.cloud.gateway.route.RouteLocator;import org.springframework.cloud.gateway.route.builder.RouteLocatorBuilder;import org.springframework.context.annotation.Bean;import org.springframework.context.annotation.Configuration;/** * &lt;p&gt; *     网关配置 * &lt;/p&gt; * * @author yww * @version 1.0 **/@Configurationpublic class GatewayConfig {    @Bean    public RouteLocator customRouteLocator(RouteLocatorBuilder routeLocatorBuilder) {        RouteLocatorBuilder.Builder routes = routeLocatorBuilder.routes();        // 路由ID -&gt; 断言 -&gt; 提供的匹配路径        routes.route(&quot;route&quot;, r -&gt; r.path(&quot;/payment/**&quot;)                .uri(&quot;http://localhost:8001&quot;)        ).build();        return routes.build();    }}</code></pre><h3>动态路由</h3><p>我们在配置路由的时候就可以发现，路由地址是写死的，也就是指定了一个微服务，如果服务是集群的，就很麻烦了，所以可以考虑用微服务名来实现动态路由进行转发。</p><p>需要添加配置开启使用微服务名进行路由。</p><pre><code class="language-yml">spring:  application:    name: cloud-gateway  cloud:    gateway:      discovery:        locator:          # 自动以注册中心的服务名进行路由注册          enabled: true</code></pre><p>然后使用微服务名代替。</p><pre><code class="language-Java">@Configurationpublic class GatewayConfig {    @Bean    public RouteLocator customRouteLocator(RouteLocatorBuilder routeLocatorBuilder) {        RouteLocatorBuilder.Builder routes = routeLocatorBuilder.routes();        routes.route(&quot;route&quot;, r -&gt; r.path(&quot;/payment/**&quot;)                // lb开启负载均衡，不添加启动不了                .uri(&quot;lb://CLOUD-PAYMENT-PROVIDER&quot;)        ).build();        return routes.build();    }    }</code></pre><h3>常用Predicate</h3><p>断言其实是有很多种的，上述使用的都只是<code>path</code>，即路径符合就返回true，接下来了解一下常用的Predicate。</p><h4>Path断言</h4><p>Path断言表示路径符合断言即为成功。</p><pre><code class="language-yml">spring:  application:    name: cloud-gateway  cloud:    gateway:      routes:        - id: routh1          uri: http://localhost:8001          predicates:          # http://localhost:9527/payment/get符合断言          # http://localhost:9527/yww/payment/get不符合断言            - Path=/payment/**</code></pre><h4>关于时间的断言</h4><blockquote><p>时间的配置可以如下。</p><p>比如2021-12-24T11:14:05.346576Z[Etc/UTC]</p><p>可以通过ZonedDateTime.now()的方法生成。</p></blockquote><ul><li>After断言代表指定时间之后即为符合断言。（后面接一个时间）</li><li>Before断言代表指定时间之前即为符合断言。（后面接一个时间）</li><li>Between断言代表指定时间内即为符合断言。（后面接两个时间，配置文件隔开用逗号）</li></ul><pre><code class="language-yml">spring:  application:    name: cloud-gateway  cloud:    gateway:      routes:        - id: routh1          uri: http://localhost:8001          predicates:          # After时间之后            - After=2021-12-24T11:14:05.346576Z[Etc/UTC]            # Before时间之前            - Before=2021-12-24T12:14:05.346576Z[Etc/UTC]            # Between两个时间之间            - Between=2021-12-24T11:14:05.346576Z[Etc/UTC],2021-12-24T12:14:05.346576Z[Etc/UTC]</code></pre><h4>Cookie断言</h4><p>Cookie断言代表请求带了指定Cokkie后才符合断言。</p><pre><code class="language-yml">spring:  application:    name: cloud-gateway  cloud:    gateway:      routes:        - id: routh1          uri: http://localhost:8001          predicates:          # 携带了这个Cookie，之后接键值对，值可以是正则表达式            - Cookie=username,yww</code></pre><h4>Header断言</h4><p>Header断言代表请求带了指定请求头后才符合断言。</p><pre><code class="language-yml">spring:  application:    name: cloud-gateway  cloud:    gateway:      routes:        - id: routh1          uri: http://localhost:8001          predicates:          # 携带了这个Header请求头，之后接键值对，值可以是正则表达式          # 这里表示请求头中有X-Request-Id，且值为整数才符合断言            - Header=X-Request-Id, \d+</code></pre><h4>Host断言</h4><p>Host断言代表指定主机符合断言。</p><pre><code class="language-yml">spring:  application:    name: cloud-gateway  cloud:    gateway:      routes:        - id: routh1          uri: http://localhost:8001          predicates:          # Host后接一组匹配的域名列表            - Host=**.somehost.org,**.anotherhost.org</code></pre><h4>Method断言</h4><p>Header断言代表请求带了指定请求方法才符合断言。</p><pre><code class="language-yml">spring:  application:    name: cloud-gateway  cloud:    gateway:      routes:        - id: routh1          uri: http://localhost:8001          predicates:          # 使用了GET方法即为符合断言            - Method-GET</code></pre><h4>Query断言</h4><p>Query断言代表请求需要带指定参数。</p><pre><code class="language-yml">spring:  application:    name: cloud-gateway  cloud:    gateway:      routes:        - id: routh1          uri: http://localhost:8001          predicates:          # 携带了username参数，且值为整数（正则表达式）            - Query=username, \d+</code></pre><h3>过滤器</h3><p>Spring Cloud Gateway内置了多种路由过滤器，由<code>GatewayFilter</code>的工厂类来产生，主要用于添加或修改请求和响应。</p><p>自带的过滤器有几十种，所以只看一下一个简单的例子。</p><pre><code class="language-yml">spring:  application:    name: cloud-gateway  cloud:    gateway:      routes:        - id: routh1          uri: http://localhost:8001          filters:            # 过滤器工厂会在匹配的请求头上加上X-Request-Id这个请求头，且值为1024          - AddRequestParameter=X-Request-Id,1024</code></pre><p>自带的太多了，所以主要学习自定义的过滤器。</p><p>实现步骤。</p><ol><li><p>实现两个重要接口。<code>GlobalFilter</code>和<code>Ordered</code>。</p><pre><code class="language-Java">/** *     这个接口是代表优先级顺序的，一般是优先级越小越高。 **/public interface Ordered {    int HIGHEST_PRECEDENCE = -2147483648;    int LOWEST_PRECEDENCE = 2147483647;    int getOrder();}</code></pre><pre><code class="language-Java">/** *     这个接口是代表实现网关全局过滤器的主要实现。 **/public interface GlobalFilter {    Mono&lt;Void&gt; filter(ServerWebExchange exchange, GatewayFilterChain chain);}</code></pre></li><li><p>实现接口的方法。</p><pre><code class="language-Java">/** * &lt;p&gt; *     自定义网关过滤器 * &lt;/p&gt; * * @author yww **/@Componentpublic class GateWayFilter implements GlobalFilter, Ordered {        /**     * 过滤器的实现     */    @Override    public Mono&lt;Void&gt; filter(ServerWebExchange exchange, GatewayFilterChain chain) {        System.out.println(&quot;请求进入全局过滤器------&quot; + new Date());        // 获取请求中的username这个参数的值        String username = exchange.getRequest().getQueryParams().getFirst(&quot;username&quot;);        // 如果该值等于yww即为合法用户直接放行        if (&quot;yww&quot;.equals(username)) {            return chain.filter(exchange);        }        // 值不为yww则为非法用户，直接返回响应        System.out.println(&quot;非法用户&quot;);        exchange.getResponse().setStatusCode(HttpStatus.NOT_ACCEPTABLE);        return exchange.getResponse().setComplete();    }    /**     * 过滤器的优先级，值越小优先级越高     */    @Override    public int getOrder() {        return 0;    }}</code></pre></li></ol><h1>服务配置</h1><h2>Config</h2><p> SpringCloud Config为微服务架构中的微服务提供了集中化的外部配置支持，配置服务器为各个不同微服务应用的所有环境提供了一个中心化的外部配置。</p><p>SpringCloud Config分为服务端和客户端两部分。</p><ul><li>服务端也称为分布式配置中心，它是一个独立的微服务应用，用来连接配置服务器并为客户端提供获取配置信息。</li><li>客户端则是通过指定的配置中心来管理应用资源，以及与业务相关的配置内容，并在启动的时候从配置中心获取和加载配置信息配置服务器默认采用git来存储配置信息，这样就有助于对环境配置进行版本管理，并且可以通过git客户端工具来方便的管理和访问配置内容。</li></ul><blockquote><p>SrpingCloud Config默认是使用git来存储配置文件，使用的是http/https访问的形式</p></blockquote><h3>简单使用</h3><h4>服务端</h4><ol><li><p>因为使用的是git来存储配置文件，所以需要提前创建一个github仓库，并提前添加一些配置文件。</p></li><li><p>服务端主要依赖的引入。</p><pre><code class="language-xml">        &lt;dependency&gt;            &lt;groupId&gt;org.springframework.cloud&lt;/groupId&gt;            &lt;artifactId&gt;spring-cloud-config-server&lt;/artifactId&gt;        &lt;/dependency&gt;</code></pre></li><li><p>服务端的配置文件编写。</p><pre><code class="language-yml">server:  port: 3344spring:  application:    name: cloud-config-center  cloud:    config:      server:        git:          # 仓库地址（ssh方式连接不上就使用https的方式）          uri: git@github.com:whyneh/SpringCloudConfig.git          # 仓库的相对地址(即扫描仓库下的某个文件加)          search-paths:            - SpringCloudConfig          # 如果是私有的仓库，还需要添加github的用户名和密码          username:          password:       # 读取的分支      label: maineureka:  client:    service-url:      defaultZone: http://localhost:7001/eureka</code></pre></li><li><p>编写服务端的主启动类，开启注解支持。</p><pre><code class="language-Java">@SpringBootApplication@EnableConfigServerpublic class ConfigApplication {    public static void main(String[] args) {        SpringApplication.run(ConfigApplication.class, args);    }}</code></pre></li><li><p>可以通过URL<a href="localhost:3344/main/config-dev.yml">localhost:3344/main/config-dev.yml</a>就能读取的github仓库里的配置文件了，至此服务端已经完成建立。</p></li></ol><h4>客户端</h4><ol><li><p>引入客户端的主要依赖。</p><pre><code class="language-xml">        &lt;dependency&gt;            &lt;groupId&gt;org.springframework.cloud&lt;/groupId&gt;            &lt;artifactId&gt;spring-cloud-starter-config&lt;/artifactId&gt;        &lt;/dependency&gt;</code></pre></li><li><p>主要的核心就在于配置文件。</p><ul><li>application.yml是用户级别的配置文件</li><li>bootstrap.yml是系统级的配置文件，优先级更高。</li><li>它会先去加载bootstrap.yml的配置，然后在加载application.yml配置</li><li>在client模块就需要使用bootstrap.yml</li></ul><pre><code class="language-yml">server:  port: 3355spring:  application:    name: config-client  cloud:    config:      # 分支名称      label: main      # 配置文件名称      name: config      # 后缀名称      profile: dev      # 配置中心地址      uri: http://localhost:3344      # 上述四个配置结合起来就表示读取http://localhost:3344/main/config-dev.yml的文件eureka:  client:    service-url:      defaultZone: http://localhost:7001/eurek</code></pre></li><li><p>编写主启动类。（不用开启注解。）</p><pre><code class="language-Java">@SpringBootApplication@EnableEurekaClientpublic class ConfigClientMain {    public static void main(String[] args) {        SpringApplication.run(ConfigClientMain.class, args);    }}</code></pre></li><li><p>测试是否能够读取github上的配置文件。</p><pre><code class="language-Java">@RestControllerpublic class ConfigClientController {    // 读取配置文件中的config下的info信息    @Value(&quot;${config.info}&quot;)    private String configInfo;        @GetMapping(&quot;/configInfo&quot;)    public String getConfigInfo() {        return this.configInfo;    }}</code></pre></li><li><p>然后访问这个接口<a href="localhost:3355/configInfo">localhost:3355/configInfo</a>就能获取到这段配置文本了。</p></li></ol><h3>分布式配置的动态刷新问题</h3><p>当我们修改了github上的配置文件的内容后，服务端的配置中心读取到是新的配置文件，但是客户端怎么读取，都是之前的配置文件的内容，这就是分布式配置的动态刷新问题。（总不能重启客户端吧，那动态配置的好处就不能体现了）</p><h3>手动刷新</h3><p>避免客户端每次都要刷新，就需要解决中国刷新问题。</p><ol><li><p>引入一个重要的依赖。</p><pre><code class="language-xml">        &lt;dependency&gt;            &lt;groupId&gt;org.springframework.boot&lt;/groupId&gt;            &lt;artifactId&gt;spring-boot-starter-actuator&lt;/artifactId&gt;        &lt;/dependency&gt;</code></pre></li><li><p>修改配置文件，暴露监控的端口。</p><pre><code class="language-yml"># 暴露监控端口management:  endpoints:    web:      exposure:        include: &quot;*&quot;</code></pre></li><li><p>在业务类上面添加一个刷新的注解</p><pre><code class="language-Java">@RefreshScope@RestControllerpublic class ConfigClientController {    @Value(&quot;${config.info}&quot;)    private String configInfo;    @GetMapping(&quot;/configInfo&quot;)    public String getConfigInfo() {        return this.configInfo;    }}</code></pre></li><li><p>每次当github配置文件发生变更时，配置中心会跟着变，此时发送一个POST请求刷新客户端的配置即可生效。</p><pre><code class="language-bash">    curl -X POST &quot;http://localhost:3355/actuator/refresh&quot;</code></pre></li><li><p>这就成功刷新了客户端的配置内容到最新的版本。</p></li></ol><blockquote><p>从手动刷新这里也能看出来一个问题，那就是手动刷新只能刷新一个服务。</p><p>如果有很多个服务，怎样实现动态刷新全部的服务配置文件，还是需要思考的。</p><p>这里就可以配合SpringCloud Bus消息总线来实现这个功能。</p><p>SpringCloud Config大多数都是和SpringCloud Bus配合使用的。 </p></blockquote><h2>Nacos</h2><p>Nacos可以实现服务配置，后续会单独学习。</p><h1>消息总线</h1><h2>Bus</h2><p>SpringCloud Bus是用来将分布式系统的节点与轻量级消息系统链接起来的框架，整合了Java的事件处理机制和消息中间件的功能。</p><p>Bus目前只支持两种消息代理。</p><ol><li>RabbitMQ</li><li>Kafka</li></ol><p>SpringCloud Bus配合SpringCloud Config使用可以实现配置的动态刷新。</p>]]></content:encoded>
            <category domain="https://yww52.com/categories/%25E6%2588%2591%25E7%259A%2584%25E6%258A%2580%25E6%259C%25AF%25E5%25AD%25A6%25E4%25B9%25A0%25E7%2589%25A9%25E8%25AF%25AD%25E6%259E%259C%25E7%2584%25B6%25E6%259C%2589%25E9%2597%25AE%25E9%25A2%2598/">我的技术学习物语果然有问题</category>
        </item>
        <item>
            <title>lambda表达式与Stream流</title>
            <link>https://yww52.com/posts/8a8aa27b/</link>
            <guid isPermaLink="true">https://yww52.com/posts/8a8aa27b/</guid>
            <pubDate>Mon, 06 Dec 2021 00:00:00 +0000</pubDate>
            <description>Java8的新特性，优化代码。</description>
            <content:encoded><![CDATA[<img src="https://img.yww52.com/2021/12/2021-12-6top_img.jpg" alt="lambda表达式与Stream流" style="max-width:100%;height:auto;margin-bottom:15px;" /><h1>Lambda表达式</h1><p>Lambda表达式出现主要是为了简化代码，若某一个接口只有一个抽象方法，就能使用Lambda表达式来进行简化这个接口的实现类。</p><h2>Lambda表达式的六种情况</h2><pre><code class="language-Java">// 1 无参，无返回值        Runnable r = () -&gt; System.out.println(&quot;1&quot;);        // 2 一个参数，无返回值        Consumer&lt;String&gt; con = (String s) -&gt; {            System.out.println(s);        };        // 3 数据类型可以省略，编译器有类型推断        Consumer&lt;String&gt; con1 = (s) -&gt; {            System.out.println(s);        };        // 4 若只需要一个参数，小括号能省略        Consumer&lt;String&gt; con2 = s -&gt; {            System.out.println(s);        };        // 5 需要两个或以上的参数，可以有返回值,不只有一条语句        Comparator&lt;Integer&gt; com1 = (o1, o2) -&gt; {            System.out.println(o1);            System.out.println(o2);            return o1.compareTo(o2);        };        // 6 Lambda体只有一条语句，大括号能省略，return也可以省略        Consumer&lt;String&gt; con3 = s -&gt; System.out.println(s);</code></pre><h2>Java内置的四大核心函数式接口</h2><ol><li><p><code>Consumer&lt;T&gt;</code></p><p>消费型接口   对T进行操作,不返回结果，方法为 <code>void accept(T t)</code></p></li><li><p><code>Supplier&lt;T&gt;</code></p><p>供给型接口   不接收参数，返回类型T的数据，方法为  <code>T get()</code></p></li><li><p><code>Function&lt;T,R&gt;</code></p><p>函数型接口   对类型为T的对象操作，返回R类型的数据，方法为 <code>R apply(T t)</code></p></li><li><p><code>Predicate&lt;T&gt;</code></p><p>断定型接口   对类型T的数据进行判断，返回Boolean值，方法为 <code>boolean test(T t)</code></p></li></ol><h2>方法引用</h2><p>在lambda体中引用另一个方法，即用另一个方法来实现lambda体。</p><p>使用格式 <code>类（或对象）:: 方法名</code></p><ol><li>对象::非静态方法</li><li>类::静态方法</li><li>类::非静态方法</li></ol><p>方法引用的方法的形参列表和返回类型要与lambda一样才能使用该方法引用（针对于前两种格式）</p><p>主要是第三种格式有些难懂。</p><pre><code class="language-java">// 比如   Comparator 中的int compare(T1 t1, T2 t2)// String中的int t1.compareTo(t2)// 这种就是形参列表不一样，但是是由t1来调用一个t2参数的方法，所以就可以用第三种格式// t1作为方法的调用者，t2作为形参String::compareTo</code></pre><h2>构造器引用</h2><p>相当于lambda引用中创建一个对象，调用了一个构造方法。</p><pre><code class="language-Java">// lambda表达式写法Supplier&lt;String&gt; str = s -&gt; new String(s);// 构造器引用的写法Supplier&lt;String&gt; str = String::new;</code></pre><h1>Stream流</h1><p>Stream流的使用主要分为三个步骤。</p><ol><li>创建Stream流。从数据源（集合，数组等）转换成Stream流。</li><li>中间操作。对数据源的数据进行处理。</li><li>终止操作。执行终止操作，就会执行中间操作的处理，产生结果。</li></ol><h2>创建Stream流</h2><ol><li>集合获取Stream流的方法<ul><li><code>list.stream()</code>串行</li><li><code>list.paralleStream()</code> 并行</li></ul></li><li>数组获取Stream流的方法<ul><li>通过Arrays的静态方法stream()得到Stream流<code>Arrays.stream(int[])</code></li></ul></li><li>通过<code>Stream.of()</code>来获取Stream流</li><li>创建无限流（用于编造数据）<ul><li><code>Stream.iterate()</code></li><li><code>Stream.generate()</code></li></ul></li></ol><h2>Stream流的中间操作</h2><ol><li><p>筛选与切片</p><ul><li><p><code>filter(Predicate p)</code></p><p>从流中排除某些元素，即满足Predicate函数的元素</p></li><li><p><code>limit(n)</code></p><p>截断流，使其元素不超过给定的元素，即筛选出来的元素不超过n</p></li><li><p><code>skip(n)</code></p><p>跳过元素，返回一个除了前n个数据的流，若数据源不足n个，则返回空流</p></li><li><p><code>distinct()</code></p><p>通过流生成元素的hashCode和equals方法去除重复的元素</p></li></ul></li><li><p>映射</p><ul><li><p><code>map(Function f)</code></p><p>接收一个函数作为参数，将元素转换成其他形式或提取信息</p></li><li><p><code>flatMap(Function f)</code></p><p>将一个函数作为参数，将流中的每个值都换成另一个流，然后把所有流连接成一个流</p></li></ul></li><li><p>排序</p><ul><li><p><code>sorted()</code></p><p>没有参数，自然排序，默认从小到大的排序,如果操作的元素是类，就会抛出异常，需要该类实现Comparable接口</p></li><li><p><code>sorted(Comparator com)</code></p><p>一个Comparator实例，定制排序</p></li></ul></li></ol><h2>Stream流的终止操作</h2><ol><li><p>匹配与查找</p><ul><li><p><code>boolean allMatch(Predicate p)</code></p><p>检查是否所有元素是否匹配该断定函数接口</p></li><li><p><code>boolean anyMatch(Predicate p)</code>  </p><p>检查是否至少有一个元素匹配该断定函数接口</p></li><li><p><code>boolean noneMatch(Predicate p)</code> </p><p>检查是否没有元素匹配该断定函数接口</p></li><li><p><code>findFirst()</code></p><p>返回第一个元素</p></li><li><p><code>findAny()</code></p><p>返回任意一个元素</p></li><li><p><code>count()</code></p><p>返回流中的总个数</p></li><li><p><code>max(Comparator c)</code></p><p>返回流中的最大值</p></li><li><p><code>min(Comparator c)</code></p><p>返回流中的最小值</p></li><li><p><code>forEach(Consumer c)</code></p><p>内部迭代</p></li></ul></li><li><p>归约</p><ul><li><p><code>reduce(T identity, BinaryOperator&lt;T&gt; bin)</code></p><p>可以将流中元素反复结合起来得到一个值，返回这个T类型的值</p><p>比如这个求和的例子<code>list.stream.reduce(0, Integer::sum);</code></p></li><li><p><code>reduce(BinaryOperator&lt;T&gt; bin)</code></p><p>可以将流中元素反复结合起来得到一个值，返回这个<code>Optional&lt;T&gt;</code>类型的值</p></li></ul></li><li><p>收集</p></li></ol><p><code>collect(Collector c)</code></p><p>将流转换为其他形式的数据源,调用这个Collector接口的方法得到新建数据源</p><p>一般使用实现了接口的Collectors的类的方法</p><p>常用方法为</p><ul><li><p><code>List&lt;T&gt; toList() </code> </p><p>将元素收集到创建的List</p></li><li><p><code>Set&lt;t&gt; toSet()</code></p><p>将元素收集到创建的Set</p></li><li><p><code>Collection&lt;T&gt; toCollection()</code></p><p>将元素收集到创建的Collection</p></li><li><p><code>Long counting()</code></p><p>计算流中的个数</p></li><li><p><code>Integer summingInt()</code></p><p>对流中元素的整数属性求和</p></li><li><p><code>Double averagingInt()</code></p><p>求流中的元素Integer属性的平均值</p></li></ul><h1>Optional容器类</h1><p><code>Optional&lt;T&gt;</code>类是一个容器类，可以报错类型T的值，代表这个值存在。</p><p>或者仅仅保存null,表示这个值不存在，可以避免空指针异常</p><p>如果值存在，则isPresent()方法返回true,调用get()方法会返回该对象</p><h2>创建Optional类对象的方法</h2><ol><li><p><code>Optional.of(T t)</code></p><p>创建一个实例，t是非空的</p></li><li><p><code>Optional.isEmpty()</code></p><p>创建一个空的实例</p></li><li><p><code>Optional.ofNullable(T t)</code></p><p>创建一个实例，t是可以空的</p></li></ol><h2>判断Optional中是否包含对象</h2><ol><li><p><code>boolean isPresent()</code></p><p>判断是否包含对象</p></li><li><p><code>void ifPresent(Consumer&lt;? super T&gt; consumer)</code></p><p>如果有值就会执行Consumer接口的实现代码，并且会作为参数传给它</p></li></ol><h2>获取Optional容器的对象</h2><ol><li><p><code>T get()</code></p><p>如果调用对象包含值，则返回该值，否则抛异常</p></li><li><p><code>T orElse(T other)</code></p><p>如果有值则返回，没有返回指定的other值</p></li><li><p><code>T orElseGet(Supplier&lt;? extends T&gt; other) </code></p><p>如果有值就将其返回，否则返回由Supplier接口实现提供的对象</p></li><li><p><code>T orElseThrow(Supplier&lt;? extends X&gt; exceptionSupplier) </code></p><p>如果有值就将其返回，否则抛出由Supplier接口实现提供的异常</p></li></ol>]]></content:encoded>
            <category domain="https://yww52.com/categories/%25E6%2588%2591%25E7%259A%2584%25E6%258A%2580%25E6%259C%25AF%25E5%25AD%25A6%25E4%25B9%25A0%25E7%2589%25A9%25E8%25AF%25AD%25E6%259E%259C%25E7%2584%25B6%25E6%259C%2589%25E9%2597%25AE%25E9%25A2%2598/">我的技术学习物语果然有问题</category>
        </item>
        <item>
            <title>异常处理</title>
            <link>https://yww52.com/posts/28877bf/</link>
            <guid isPermaLink="true">https://yww52.com/posts/28877bf/</guid>
            <pubDate>Sun, 10 Oct 2021 00:00:00 +0000</pubDate>
            <description>常用的全局异常处理。</description>
            <content:encoded><![CDATA[<img src="https://img.yww52.com/2021/10/2021-10-10top_img.jpg" alt="异常处理" style="max-width:100%;height:auto;margin-bottom:15px;" /><h2>自定义异常类（配合结果类）</h2><pre><code class="language-Java">/** * @author yww * @since 2021/10/9 **/@Getter@AllArgsConstructorpublic class GlobalException extends RuntimeException {    /**     * 错误码     */    private Integer code = FAILED.getCode();    /**     * 错误信息     */    private final String message;    public GlobalException(String message) {        this.message = message;    }    public GlobalException(ResultCode resultCode) {        this.code = resultCode.getCode();        this.message = resultCode.getMessage();    }}</code></pre><blockquote><p>这里需要注意的是，自定义异常需要继承RuntimeException（运行时异常）。</p></blockquote><h2>定义全局异常处理</h2><pre><code class="language-Java">/** *  &lt;p&gt; *     全局异常处理 *  &lt;/p&gt; * * @author yww * @since 2021/10/10 **/@Slf4j@RestControllerAdvicepublic class ControllerAdviceHandler {    /**     * 处理自定义的服务异常信息     * 统一处理GlobalException异常，异常处理顺序是从小异常到大异常。     *     * @param e 服务异常     * @return 异常信息     */    @ExceptionHandler(value = GlobalException.class)    public &lt;T&gt; Result&lt;T&gt; globalExceptionHandler(GlobalException e, HttpServletRequest request) {        log.error(&quot;&gt;&gt; global exception: {}, {}, {}&quot;, request.getRequestURI(), e.getCode(), e.getMessage());        String errMessage = e.getMessage();        // 防止空的错误信息        if (StringUtils.isBlank(errMessage)) {            errMessage = &quot;服务器繁忙&quot;;        }        return Result.failure(e.getCode(), errMessage);    }    /**     * 异常信息     *     * @param e 服务异常     * @return 异常信息     */    @ExceptionHandler(value = Exception.class)    public &lt;T&gt; Result&lt;T&gt; defaultErrorHandler(Exception e, HttpServletRequest request) {        log.error(&quot;&gt;&gt; 服务器内部错误 &quot; + request.getRequestURI(), e.getMessage());        return Result.failure(500, &quot;服务器繁忙&quot;);    }}</code></pre><blockquote><p>@RestControllerAdvice注解其实是@ControllerAdvice和@ResponseBody的合并。</p><p>@ControllerAdvice通常配合@ExceptionHandler来捕抓异常信息。</p><p>@ControllerAdvice可以捕抓到系统抛出的异常，然后使用@ExceptionHandler匹配具体处理异常信息。</p></blockquote><h1>特殊的异常处理</h1><p>有些异常是不会走全局异常处理的，即不会被捕捉到，比如在过滤器中的异常，这种异常是没有经过<code>controller</code>的，所以发生了异常，也不会通过被上述的方式捕捉到。</p><p>以下是一个捕捉Token过滤器异常的例子。</p><pre><code class="language-Java">/** * &lt;p&gt; *      Token身份验证过滤器 * &lt;/p&gt; * * @author yww * @since 2022/10/20 */@Slf4jpublic class TokenAuthenticationFilter extends BasicAuthenticationFilter {    @Override    protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain chain) {        try {            .....            // 处理Token验证的异常        } catch (AlgorithmMismatchException | SignatureVerificationException | TokenExpiredException                 | MissingClaimException | IncorrectClaimException e) {            errorHandler(request, response, e);        }    }    /**     * 处理异常信息     *     * @param request  请求     * @param response 响应     * @param e        异常     */    private void errorHandler(HttpServletRequest request, HttpServletResponse response, Exception e) {        if (e instanceof AlgorithmMismatchException) {            resolver.resolveException(request, response, null, new AlgorithmMismatchException(e.getMessage()));            return;        }        if (e instanceof TokenExpiredException) {            resolver.resolveException(request, response, null, new TokenExpiredException(                    e.getMessage(),                    ((TokenExpiredException) e).getExpiredOn())            );            return;        }        if (e instanceof MissingClaimException) {            resolver.resolveException(request, response, null, new MissingClaimException(                    ((MissingClaimException) e).getClaimName())            );            return;        }        if (e instanceof IncorrectClaimException) {            resolver.resolveException(request, response, null, new IncorrectClaimException(                    e.getMessage(),                    ((IncorrectClaimException) e).getClaimName(),                    ((IncorrectClaimException) e).getClaimValue())            );            return;        }        if (e instanceof JWTVerificationException) {            resolver.resolveException(request, response, null, new JWTVerificationException(                    e.getMessage(),                    e.getCause())            );        }    }}</code></pre>]]></content:encoded>
            <category domain="https://yww52.com/categories/%25E6%2588%2591%25E7%259A%2584%25E6%258A%2580%25E6%259C%25AF%25E5%25AD%25A6%25E4%25B9%25A0%25E7%2589%25A9%25E8%25AF%25AD%25E6%259E%259C%25E7%2584%25B6%25E6%259C%2589%25E9%2597%25AE%25E9%25A2%2598/">我的技术学习物语果然有问题</category>
        </item>
        <item>
            <title>分布式session的解决方案</title>
            <link>https://yww52.com/posts/86ff1dad/</link>
            <guid isPermaLink="true">https://yww52.com/posts/86ff1dad/</guid>
            <pubDate>Thu, 10 Jun 2021 00:00:00 +0000</pubDate>
            <description>Session主要的目的是会话控制，但是在分布式环境下，Session就会出现一些问题，接下来就来探讨一下会出现的问题以及解决方案。</description>
            <content:encoded><![CDATA[<img src="https://img.yww52.com/2021/6/2021-6-10/top_img.jpg" alt="分布式session的解决方案" style="max-width:100%;height:auto;margin-bottom:15px;" /><h1>session</h1><blockquote><p> Session，在计算机领域，被称为“会话控制”。</p></blockquote><p>会话控制，这就提出了两个问题</p><ol><li>什么是会话</li><li>为什么要控制会话</li><li>怎样控制会话</li></ol><h2>什么是会话</h2><p>会话按字意来看就是进行对话，其实真正含义就差不多是这个意思。</p><blockquote><p>在计算机术语中，会话是指一个终端用户与交互系统进行通讯的过程，比如从输入账户密码进入网上的一个系统到退出系统就是一个会话过程。</p></blockquote><p>也可以理解为两个计算机之间的对话，这就是会话。</p><p>一个计算机的网络世界就像是被集体孤立的个人，虽然不能说孤立的个体是毫无作用的，但是不融入集体的个体，是不能发挥出个体的真正意义的，所以每个个体的交流是很重要的。所以在网络世界中，会话也是很重要的。</p><h2>为什么要控制会话</h2><p>会话大部分都是建立在HTTP的基础上的，但是HTTP是一个无状态的协议，于是就出现了一个很重要的问题，会话这么多，服务端该怎么区分这些会话是哪个用户的？所以就需要将这些会话进行控制区分。</p><h2>怎样控制会话</h2><p>服务端需要某种机制来将这些会话进行区分，这个机制就是session。</p><p>用户通过HTTP与服务端进行会话，服务端为每一个用户生成特定的sessionId来标记该用户，之后每个会话携带着特定sessionId，服务端就能确定该会话属于哪一个用户的了。</p><p>控制会话的大致思路就是这样了。</p><h1>分布式环境下的session</h1><p>在上述对session的描述中，会发现控制会话的方案是很好的。但是这仅限于单体架构，服务端只有一个服务器在提供服务，该服务存储着所有的sessionId，通过该服务器可以识别出所有被记录的用户。</p><p><img src="https://img.yww52.com/2021/6/2021-6-10/img1.png" alt=""></p><p>但是在如今这个大流量的时代，大多时候，单个服务器是不能承担大规模的请求，所以就出现了集群方案，将单个服务器拓展为多个服务器提供服务，减轻单个服务器的压力。</p><p><img src="https://img.yww52.com/2021/6/2021-6-10/img2.png" alt=""></p><p>但是从这个图中就会发现一个问题。</p><p>假设客户端A与服务端进行了一次会话，第一个请求发送到了服务器A，服务器A为客户端的这次会话生成了一个sessionId，但是客户端A的请求是有可能发送到服务器B的，但是此时的服务器B是没有记录这次会话的，所以也就是没有状态的，这违背了session的初心。其根本原因就在于session是没有共享。</p><p>接下来就探讨一下在分布式环境下的session怎样实现共享。</p><h1>客户端存储</h1><p>在分布式环境下，一个客户端的请求可能散落在不同的服务器上，但是我们可以发现客户端是唯一，所以将session直接存储在客户端不就可以实现session的共享了吗？这的确是一个方法，一个会话一定是会在同个客户端，这样就实现了session的共享。</p><p>那该怎么存储在客户端上呢？</p><p>可以借助浏览器的cookie来进行session信息的存储。在客户端向服务端第一次发送请求的时候，服务端生成一个特定的sessionId返回给客户端，浏览器收到了session信息之后，就将该信息存储到cookie中，每次发送请求都带上session信息，服务端就能识别出该会话。在这里服务端只提供生成session和识别seesion的服务，而不存储session信息。</p><p><img src="https://img.yww52.com/2021/6/2021-6-10/img3.png" alt=""></p><p>:::note{type=&quot;warning&quot;}</p><p>缺点: session存储在cookie中，安全隐患很大,很容易被盗取。另一方面，cookie对于数据类型及数据大小存在限制。</p><p>:::</p><h1>session复制</h1><p>第一种情况，在客户端存储，这种方案已经思考过了，那么接下来可以该在服务端想想方法了。</p><p>于是就出现了session复制这种方案。</p><p>将服务器A的sessionA给服务器B，服务器B的sessionB给服务器A，这样不就实现了session的共享了吗。</p><p>大致思路就是这样，在特定时间，服务器就会去复制其他服务器的session信息或者将自己的信息发送给其他服务器，从而实现session信息同步。</p><p>像tomcat等一些web容器就支持session的复制，在同一个局域网内发送该容器的session信息给其他容器。</p><p><img src="https://img.yww52.com/2021/6/2021-6-10/img4.png" alt=""></p><p>:::note{type=&quot;warning&quot;}</p><p>缺点: session需要消耗一定的资源，当集群有点规模，消耗的资源也是不可小觑。</p><p>:::</p><h1>绑定session</h1><p>第一种方法是在客户端，第二种方法是在服务端，接下来就可以考虑请求转发上想方法。</p><p>我们只要将同一个客户端的请求都转发到同一个服务器上不就好了吗。</p><p>这种想法也是可以行的通的。Nginx也提供了对应的方法。使用Nginx进行负载均衡，可以选择它的ip_hash的方案，将同一个IP的请求转发到相同的服务器，这样该次会话都只会在一个服务器上进行，宏观上就像是单体架构下使用的session。</p><p><img src="https://img.yww52.com/2021/6/2021-6-10/img5.png" alt=""></p><p>:::note{type=&quot;warning&quot;}</p><p>缺点: 降低了集群的可用性，若是单个服务器出现故障，那么存储的session的信息丢失，会话就会出现故障。</p><p>:::</p><h1>基于外部存储</h1><p>前三种方法在客户端，服务端，和负载均衡下思考的方案，除了这三者之外怎么进行session共享呢？</p><p>在计算机领域，没有什么问题是加一层不能解决的。</p><p>所以我们可以考虑给系统增加一个数据存储层，然后将seesion存储在该数据存储层上就好了，从而实现session的共享。</p><p>:::note{type=&quot;info&quot;}</p><p>事实上，每一个系统都会存在数据存储层的，只是上述解决思路没有将该数据存储层表示出来。没有表示出来的原因只是因为在session的机制中没有涉及到存储层，所以没有提及。</p><p>:::这里就提供一个基于Redis的sessioon存储方案。（只要是提供存储数据的都是可以实现的）</p><p><img src="https://img.yww52.com/2021/6/2021-6-10/img6.png" alt=""></p><p>:::note{type=&quot;success&quot;}</p><p>这种方法除了降低整个系统的可用性就没有什么很明显的缺点，系统可用性也是可以通过搭建Redis集群来进行提高。其实上述的绑定session的方案和session的方案，总体也是会降低整个系统的可用性，所以就不探讨该缺点。所以这种方案也是最经常使用的方案了。</p><p>:::</p>]]></content:encoded>
            <category domain="https://yww52.com/categories/%25E6%2588%2591%25E7%259A%2584%25E6%258A%2580%25E6%259C%25AF%25E5%25AD%25A6%25E4%25B9%25A0%25E7%2589%25A9%25E8%25AF%25AD%25E6%259E%259C%25E7%2584%25B6%25E6%259C%2589%25E9%2597%25AE%25E9%25A2%2598/">我的技术学习物语果然有问题</category>
        </item>
        <item>
            <title>MySQL必知必会</title>
            <link>https://yww52.com/posts/df083c4d/</link>
            <guid isPermaLink="true">https://yww52.com/posts/df083c4d/</guid>
            <pubDate>Fri, 04 Jun 2021 00:00:00 +0000</pubDate>
            <description>阅读MySQL必知必会的笔记，并记录一下书中的SQL语句，用于加深一下印象。</description>
            <content:encoded><![CDATA[<img src="https://img.yww52.com/2021/6/2021-6-4top_img.jpg" alt="MySQL必知必会" style="max-width:100%;height:auto;margin-bottom:15px;" /><h1>了解数据库和表</h1><h2>连接</h2><pre><code class="language-mysql">mysql -u root -p</code></pre><h2>选择数据库</h2><pre><code class="language-mysql">USE crashcourse;</code></pre><h2>了解数据库和表</h2><pre><code class="language-mysql"># 显示所有数据库SHOW DATABASES;# 显示一个数据库中的所有表SHOW TABLES;# 显示表中数据的列SHOW COLUMNS FROM customers;DESCRIBE customers;DESC cutomers;</code></pre><h2>其他一些语句</h2><pre><code class="language-mysql"># 用于显示服务器状态信息SHOW STATUS;# 显示创建特定数据库表MySQL语句SHOW CREATE DATABASE crashcourse;# 显示创建特定表MySQL语句SHOW CREATE TABLE customers;# 用来显示授予用户的安全权限SHOW GRANTS;# 显示服务器错误或警告信息SHOW ERRORS;SHOW WARNINGS;</code></pre><h1>检索数据</h1><h2>检索单个列</h2><pre><code class="language-mysql"># 从表products中检索一个名为prod_name的列SELECT prod_name FROM products;</code></pre><h2>检索多个列</h2><pre><code class="language-mysql"># 从表products中检索名为prod_id, prod_name, prod_price的列SELECT prod_id, prod_name, prod_price FROM products;</code></pre><h2>检索所有列</h2><pre><code class="language-mysql"># 使用通配符 * 返回表中所有列SELECT * FROM products;</code></pre><blockquote><p>最好不要使用通配符，检索不需要的列会江都检索和应用程序的性能。</p></blockquote><h2>检索不同的行</h2><pre><code class="language-mysql"># 使用DISTINCT关键字，会返回不同的行SELECT DISTINCT vend_id FROM products;</code></pre><blockquote><p>​DISTINCT必须放在列名的前面</p><p>​不能部分使用DISTINCT，这个关键字应用于所有列而不仅仅是前置它的列，所以除非指定的列都相同，否则所有行都将被检索出来。</p></blockquote><h2>限制结果</h2><pre><code class="language-mysql"># 返回前5行数据SELECT prod_name FROM products LIMIT 5;# 从第5行开始返回接下来5行的数据SELECT prod_name FROM products LIMIT 5, 5;SELECT prod_name FROM products LIMIT 5 OFFSET 5;</code></pre><blockquote><p>检索出来的行是从0开始，所以LIMIT 5 表示从第0行开始接下来的五行</p></blockquote><h2>使用完全限定的表名</h2><pre><code class="language-mysql">SELECT products.prod_name FROM products;</code></pre><h1>排序检索数据</h1><h2>排序数据</h2><pre><code class="language-mysql"># 使用ORDER BY子句取一个或多个列名字(使用未检索的列也是合法的)，对输出进行排序# 以prod_name列的字母顺序排序数据SELECT prod_name FROM products ORDER BY prod_name;</code></pre><h2>按多个列排序</h2><pre><code class="language-mysql"># 先以左边的列进行排序，若是相同再比较右边的列SELECT prod_id, prod_price, prod_name FROM products ORDER BY prod_price, prod_name;</code></pre><blockquote><p>ORDER BY 子句应该保证在FROM子句之后，LIMIT子句要在ORDER BY 子句之后。</p></blockquote><h2>指定排序顺序</h2><pre><code class="language-mysql"># 以价格降序进行排序SELECT prod_id, prod_price, prod_name FROM products ORDER BY prod_price DESC;# 以价格升序序进行排序SELECT prod_id, prod_price, prod_name FROM products ORDER BY prod_price ASC;# 先以价格降序，再以产品名排序SELECT prod_id, prod_price, prod_name FROM products ORDER BY prod_price DESC, prod_name;</code></pre><blockquote><p>如果想在多个列上进行降序排序，必须每一个列都要指定DESC关键字</p><p>ASC关键字其实没多大用，因为默认就是升序排序的，不指定DESC就一般是ASC排序</p></blockquote><h1>过滤数据</h1><h2>使用WHERE子句</h2><pre><code class="language-mysql"># 使用where子句指定搜索条件SELECT prod_name, prod_price FROM products WHERE prod_price = 2.5;</code></pre><blockquote><p>WHERE子句的位置应该在ORDER BY子句之前。</p></blockquote><h2>WHERE子句操作符</h2><table><thead><tr><th align="center">操作符</th><th align="center">说明</th></tr></thead><tbody><tr><td align="center">=</td><td align="center">等于</td></tr><tr><td align="center">&lt;&gt;</td><td align="center">不等于</td></tr><tr><td align="center">!=</td><td align="center">不等于</td></tr><tr><td align="center">&lt;</td><td align="center">小于</td></tr><tr><td align="center">&lt;=</td><td align="center">小于等于</td></tr><tr><td align="center">&gt;</td><td align="center">大于</td></tr><tr><td align="center">&gt;=</td><td align="center">大于等于</td></tr><tr><td align="center">BETWEEN</td><td align="center">在指定两个值之间</td></tr></tbody></table><h3>检查单个值</h3><pre><code class="language-mysql"># 执行匹配时默认不区分大小写SELECT prod_name, prod_price FROM products WHERE prod_name = &#39;fuses&#39;;SELECT prod_name, prod_price FROM products WHERE prod_price &lt; 10;</code></pre><h3>不匹配检查</h3><pre><code class="language-mysql"># 不是由供应商1003制造的产品SELECT vend_id, prod_name FROM products WHERE vend_id &lt;&gt; 1003;SELECT vend_id, prod_name FROM products WHERE vend_id != 1003;</code></pre><blockquote><p>单引号用来限定字符串。如果将值与串类型的列进行比较，则需要限定引号。用来与数值列进行比较的值不用引号。</p></blockquote><h3>范围值检查</h3><pre><code class="language-mysql"># 检索价格在5美元和10美元之间的产品SELECT prod_name, prod_price FROM products WHERE prod_price BETWEEN 5 AND 10;</code></pre><h3>空值检查</h3><pre><code class="language-mysql"># 查询价格是空值的产品,空值检查SELECT prod_name FROM products WHERE prod_price IS NULL;# 非空值检查SELECT prod_name FROM products WHERE prod_price IS NOT NULL;</code></pre><blockquote><p>NULL 无值，她与字段包括0，空字符串或仅仅包括空格不同。</p></blockquote><h1>数据过滤</h1><h2>组合WHERE子句</h2><blockquote><p>用来联结或改变WHERE子句的子句的关键字，被称为逻辑操作符或是操作符。</p></blockquote><h3>AND操作符</h3><pre><code class="language-mysql"># AND操作符给WHERE子句附加条件SELECT prod_id, prod_price, prod_name FROM products WHERE vend_id = 1003 AND prod_price &lt;= 10;</code></pre><blockquote><p>AND用在WHERE子句的关键字，用来指示检索满足所有给定条件的行。</p></blockquote><h3>OR操作符</h3><pre><code class="language-mysql"># OR操作符指示MySQL检索匹配任意条件的行SELECT prod_name, prod_price FROM products WHERE vend_id = 1002 OR vend_id = 1003;</code></pre><blockquote><p>ORWHERE子句中使用的关键字，用来表示检索匹配任一给定条件的行。</p></blockquote><h3>计算次序</h3><blockquote><p>WHERE可包括AND和OR的组合子句条件，但注意的是，MYSQL会优先处理AND的条件然后在处理OR条件。</p></blockquote><pre><code class="language-mysql"># 先处理AND在处理OR，所以表示的是vend_id等于1003并且价格大于等于10的产品或者是vend_id等于1002价格没限制的产品SELECT prod_name, prod_price FROM products WHERE vend_id = 1002 OR vend_id = 1003 AND prod_price &gt;= 10;# 正确顺序的话需要添加括号进行区分SELECT prod_name, prod_price FROM products WHERE (vend_id = 1002 OR vend_id = 1003) AND prod_price &gt;= 10;</code></pre><blockquote><p>任何时候使用具有AND和OR操作符的WHERE子句，都应该使用圆括号明确地分组操作符。</p></blockquote><h2>IN操作符</h2><blockquote><p>IN操作符用来指定条件返回，范围中地每个条件都可以进行匹配。</p></blockquote><pre><code class="language-mysql"># IN取合法值SELECT prod_name, prod_price FROM products WHERE vend_id IN (1002,1003)ORDER BY prod_name;p# 等价于SELECT prod_name, prod_price FROM products WHERE vend_id = 1002 OR vend_id = 1003ORDER BY prod_name;</code></pre><h2>NOT操作符</h2><blockquote><p>NOTWHERE子句中用来否定后跟条件地关键字。</p></blockquote><pre><code class="language-mysql">SELECT prod_name, prod_priceFROM productsWHERE vend_id NOT IN (1002,1003)ORDER BY prod_name;</code></pre><h1>用通配符进行过滤</h1><h2>LIKE操作符</h2><h3>百分号通配符</h3><pre><code class="language-mysql"># %通配符表示任何字符出现任意次数SELECT prod_id,prod_name FROM products WHERE prod_name LIKE &#39;jet%&#39;;</code></pre><blockquote><p>WHERE prod_name LIKE &#39;%&#39;也是不能匹配NULL值得，需要注意。</p></blockquote><h3>下划线通配符</h3><pre><code class="language-mysql"># _下划线只匹配一个字符,不能多也不能少SELECT prod_id, prod_name FROM productsWHERE prod_name LIKE &#39;_ton anvil&#39;;</code></pre>]]></content:encoded>
            <category domain="https://yww52.com/categories/%25E8%25AF%25BB%25E4%25B9%25A6%25E7%25AC%2594%25E8%25AE%25B0/">读书笔记</category>
        </item>
        <item>
            <title>漫画算法</title>
            <link>https://yww52.com/posts/998d504/</link>
            <guid isPermaLink="true">https://yww52.com/posts/998d504/</guid>
            <pubDate>Thu, 03 Jun 2021 00:00:00 +0000</pubDate>
            <description>记录书中面试算法题的解法，主要是书中思路。</description>
            <content:encoded><![CDATA[<img src="https://img.yww52.com/2021/6/2021-6-3top_img.jpg" alt="漫画算法" style="max-width:100%;height:auto;margin-bottom:15px;" /><h2>如何判断链表有环</h2><blockquote><p>给定一个链表，判断链表中是否有环。</p></blockquote><h3>循环判断</h3><p>从头节点开始，依次遍历单链表中的每一个节点。没遍历一个新节点，就从头检查新节点之前的所有节点，查看是否相同。若存在相同节点证明链表是有环的。</p><pre><code class="language-Java">public class Solution {    public boolean hasCycle(ListNode head) {        if (head == null || head.next == null) return false;        int total = 2;        ListNode cur = head.next;        while (cur != null) {            int start = 1;            ListNode node = head;            while (node != cur) {                node = node.next;                start++;            }            if (start != total) return true;            total++;            cur = cur.next;        }        return false;    }}</code></pre><p>该算法遍历一遍的同时，内部也在从头遍历一次，所以时间复杂度为O(n^2)</p><p>没有创造新的内存空间，空间复杂度为O(1)</p><h3>Set集合</h3><p>创建一个Set集合，遍历链表并将节点加入Set集合，当出现重复的元素，即加入Set集合失败即代表有环出现。</p><pre><code class="language-Java">public class Solution {    public boolean hasCycle(ListNode head) {        if (head == null || head.next == null) return false;        Set&lt;ListNode&gt; set = new HashSet&lt;&gt;();        while (head != null) {            if (!set.add(head)) return true;            head = head.next;        }        return false;    }}</code></pre><p>该算法是遍历一遍链表，所以时间复杂度为O(n)</p><p>将所有节点加入Set集合存储，空间复杂度也为O(n)</p><h3>双指针</h3><p>使用两个指针，一个每次前进两个，一个每次前进一个，若是出现两个指针指向一个节点，代表链表有环。因为快指针提前入环，总会追上慢指针。</p><pre><code class="language-Java">public class Solution {    public boolean hasCycle(ListNode head) {        if (head == null || head.next == null) return false;        ListNode fast = head;        ListNode slow = head;        while (fast != null &amp;&amp; fast.next != null) {            fast = fast.next.next;            slow = slow.next;            if (fast == slow) return true;        }        return false;    }}</code></pre><p>只用循环一次链表，所以时间复杂度为O(n)</p><p>不用创建额外的空间，所以空间复杂度为O(1)</p><h3>求出环的长度</h3><p>快慢节点重合的时候，在继续前进并记录前进的次数，下次重合，就是快指针（速度是慢指针的两倍）比慢指针多走了一圈，可以理解为前进的次数就是环的长度。</p><pre><code class="language-Java">public class Solution {    public ListNode getCycleLength(ListNode head) {        if (head == null || head.next == null) return false;        ListNode fast = head;        ListNode slow = head;        while (fast != null &amp;&amp; fast.next != null) {            fast = fast.next.next;            slow = slow.next;            if (fast == slow) break;        }        int length = 1;        fast = fast.next.next;        slow = slow.next;        while (fast != slow) {fast = fast.next.next;            slow = slow.next;            length++;        }        return length;    }}</code></pre><h3>判断入环点</h3><p>假设链表头节点到入环点的距离是<code>D</code>，从入环点到两个指针首次相遇点为<code>S1</code>，从两个指针首次相遇点到入环点的距离为<code>S2</code>。</p><p>设慢指针的移动速度为一个节点，快指针的移动速度为两个节点。</p><p>故两个指针首次相遇时，两个指针所走的距离如下</p><p>慢指针行走的距离为<code>D + S1</code></p><p>快指针的速度是慢指针的两倍，所以相当于比慢指针多走了<code>N</code>圈，故行走的距离为<code>D + S1 + N * (S1 + S2)</code></p><p>因为快指针的速度是慢指针的两倍，所以快指针的行走距离应该为慢指针的两倍，所以有</p><p><code>2(D + S1) = D + S1 + N * (S1 + S2)</code></p><p>经过整理可以得到</p><p><code>D = (N - 1)(S1 + S2) + S2</code></p><p>也就是说，链表头节点到入环点的的距离相当于从首次相遇点绕环<code>N - 1</code>圈后再回到入环点的距离。</p><p>故从头节点出发，与首次相遇点出发，两者均以一个节点的速度，两者相遇点即为入环点。</p><pre><code class="language-Java">public class Solution {    public ListNode detectCycle(ListNode head) {        if(head == null || head.next == null) return null;        ListNode fast = head;        ListNode slow = head;        while(fast != null &amp;&amp; fast.next != null) {            slow = slow.next;            fast = fast.next.next;            if(slow == fast) {                fast = head;                while(fast != slow) {                   fast = fast.next;                   slow = slow.next;                }                return fast;            }        }        return null;    }}</code></pre><h2>最小栈的实现</h2><blockquote><p>设计一个支持 <code>push</code> ，<code>pop</code> ，<code>top</code> 操作，并能在常数时间内检索到最小元素的栈。</p><ul><li><code>push(x)</code> —— 将元素 x 推入栈中。</li><li><code>pop()</code> —— 删除栈顶的元素。</li><li><code>top()</code> —— 获取栈顶元素。</li><li><code>getMin()</code> —— 检索栈中的最小元素。</li></ul></blockquote><p>这里需要使用到辅助栈来实现，将每次最小的元素都放到辅助栈中，若即入栈的元素比最小元素小，辅助栈中就放入该最小元素，否则就放入之前最小的元素。当栈出栈后，辅助栈也跟着出栈。</p><pre><code class="language-Java">class MinStack {    Stack&lt;Integer&gt; min;    Stack&lt;Integer&gt; stack;    public MinStack() {        stack = new Stack&lt;&gt;();        min = new Stack&lt;&gt;();    }        public void push(int val) {        if (stack.isEmpty()) {            stack.push(val);            min.push(val);        } else {            stack.push(val);            if (val &lt; min.peek()) {                min.push(val);            } else {                min.push(min.peek());            }        }    }        public void pop() {        if (stack.isEmpty()) {}        stack.pop();        min.pop();    }        public int top() {        return stack.peek();    }        public int getMin() {        return min.peek();    }}/** * Your MinStack object will be instantiated and called as such: * MinStack obj = new MinStack(); * obj.push(val); * obj.pop(); * int param_3 = obj.top(); * int param_4 = obj.getMin(); */</code></pre><h2>如何求出最大公约数</h2><blockquote><p>求出两个正整数的最大公约数。</p></blockquote><h3>枚举</h3><p>从较小整数的一半开始遍历到2，首次出现了两个正整数都整除的数，则该数就是两个正整数的最大公约数。</p><pre><code class="language-Java">public static int getGreatestCommonDivisor(int a, int b) {        int max = Math.max(a, b);        int min = Math.min(a, b);        if (max % min == 0) {            return min;        }        for (int i =  min / 2; i &gt; 1; i--) {            if (min % i == 0 &amp;&amp; max % i == 0) {                return i;            }        }        return 1;    }</code></pre><p>时间复杂度为O(min(a, b))</p><h3>辗转相除法（欧几里得算法）</h3><p>这个方法基于一个定理：</p><p>两个正整数a和b（a &gt; b），它们的最大公约数等于a除以b的余数c和b之间的最大公约数。</p><pre><code class="language-Java">    public static int getGreatestCommonDivisor(int a, int b) {        int max = Math.max(a, b);        int min = Math.min(a, b);        if (max % min == 0) {            return min;        }        return getGreatestCommonDivisor(max % min, min);    }</code></pre><p>时间复杂度近似为O(log(max(a, b))，当两个正整数很大的时候，a % b的计算性能很差。</p><h3>更相减损术</h3><p>两个正整数a和b（a &gt; b），它们的最大公约数等于a - b的差值c和较小数b的最大公约数。</p><pre><code class="language-Java">    public static int getGreatestCommonDivisor(int a, int b) {        if (a == b) {            return a;        }        int max = Math.max(a, b);        int min = Math.min(a, b);        return getGreatestCommonDivisor(max - min, min);    }</code></pre><p>避免了大数取模的计算，但是算法性能不稳定，最坏的时间复杂度为O(max(a, b))</p><h3>更优算法</h3><ul><li>当a和b均为偶数时，gcd(a,b) = 2 * gcd(a / 2, b / 2) = 2 * gcd(a &gt;&gt; 1, b &gt;&gt; 1) = gcd(a &gt;&gt; 1, b &gt;&gt; 1) &lt;&lt; 1;</li><li>当a为偶数，b为奇数时，gcd(a, b) = gcd(a / 2, b) = gcd(a &gt;&gt; 1, b);</li><li>当a为奇数，b为偶数时，gcd(a, b) = gcd(a, b / 2) = gcd(a, b &gt;&gt; 1);</li><li>当a和b均为奇数时，先利用更相减损术运算一次，gcd(a, b) = gcd(b, a - b)，此时a - b必是偶数，然后进行上面情况的运算。</li></ul><pre><code class="language-Java">    public static int getGreatestCommonDivisor(int a, int b) {        if (a == b) {            return a;        }        if ((a &amp; 1) == 0 &amp;&amp; (b &amp; 1) == 0) {            return getGreatestCommonDivisor(a &gt;&gt; 1, b &gt;&gt; 1) &lt;&lt; 1;        } else if ((a &amp; 1) == 0 &amp;&amp; (b &amp; 1) != 0) {            return getGreatestCommonDivisor(a &gt;&gt; 1, b);        } else if ((a &amp; 1) != 0 &amp;&amp; (b &amp; 1) == 0) {            return getGreatestCommonDivisor(a, b &gt;&gt; 1);        } else {            int max = Math.max(a, b);            int min = Math.min(a, b);            return getGreatestCommonDivisor(max - min, min);        }    }</code></pre><p>进行简化之后为</p><pre><code class="language-Java">    public static int getGreatestCommonDivisor(int a, int b) {        if (a == b) {            return a;        }        if ((a &amp; 1) == 0 &amp;&amp; (b &amp; 1) == 0) {            return getGreatestCommonDivisor(a &gt;&gt; 1, b &gt;&gt; 1) &lt;&lt; 1;        } else if ((a &amp; 1) == 0) {            return getGreatestCommonDivisor(a &gt;&gt; 1, b);        } else if ((b &amp; 1) == 0) {            return getGreatestCommonDivisor(a, b &gt;&gt; 1);        } else {            int max = Math.max(a, b);            int min = Math.min(a, b);            return getGreatestCommonDivisor(max - min, min);        }    }</code></pre><p>这是更相减损法与位运算的结合方法，算法性能稳定，而且使用了位运算，时间复杂度为O(log(max(a, b)))</p><h2>判断一个数是否为2的整数次幂</h2><blockquote><p>判断一个正整数是否为2的整数次幂</p></blockquote><h3>枚举</h3><p>计算出2的整数次幂，依次递增，若是出现了大于该数的2次幂（并没有等于的），就证明该数不是2的整数次幂。</p><pre><code class="language-java">class Solution {    public boolean isPowerOfTwo(int n) {        int num = 1;        while (num &lt;= n) {            if (num == n) return true;            num = num &lt;&lt; 1;        }        return false;    }}</code></pre><p>算法的时间复杂度为O(logn)。</p><h3>n &amp; (n - 1) == 0</h3><p>每一个2的整数次幂转换为二进制，都是1开头后面都是0的数串。</p><p>将该整数次幂减一，就得到二进制全为1的数串。</p><p>这两个数取按位与运算，就会出现每一位数都不一样，所以结果为0。</p><p>所以不为0的就不是2的整数次幂了。</p><pre><code class="language-Java">class Solution {    public boolean isPowerOfTwo(int n) {        return (n &amp; n - 1) == 0;    }}</code></pre><h3>n &amp; -n == n</h3><p><code>n &amp; -n</code>可以得到最低位的1的位置，即最低位为1其余全是0的数串，如果是2的整数次幂的话，那么该数串其实表示的就是本身。</p><pre><code class="language-Java">class Solution {    public boolean isPowerOfTwo(int n) {        return (n &amp; -n) == n;    }}</code></pre><h2>无序数组排序后的最大相邻差</h2><blockquote><p>有一个无序整数型数组，如何求出该数组排序后的任意两个相邻元素的最大差值？</p></blockquote><h3>排序后求最大差值</h3><pre><code class="language-Java">    public static int getMaxSortedDistance(int[] array) {        Arrays.sort(array);        int max = -1;        for (int i = 1; i &lt; array.length; i++) {            int temp = array[i] - array[i - 1];            if (temp &gt; max) {                max = temp;            }        }        return max;    }</code></pre><p>将数组进行快速排序O(nlogn)，然后进行遍历O(n)，所以总的时间复杂度为O(nlogn)</p><p>空间复杂度就是该数组O(n)</p><h3>计数排序</h3><p>使用计数排序，找出新数组最多连续出现0的次数加1，就是最大相邻差了。</p><p>当数组的数差值不是很大的情况下，效率很高，但是差值很大的情况，数组的长度会很大。</p><pre><code class="language-Java">    public static int getMaxSortedDistance(int[] array) {        // 求出数组的最大值和最小值        int max = array[0];        int min = array[0];        for (int i = 1; i &lt; array.length; i++) {            if (array[i] &gt; max) {                max = array[i];            }            if (array[i] &lt; min) {                min = array[i];            }        }        int d = max - min;        // 最大值和最小值相等，证明数组的所有数相等        if (d == 0) {            return 0;        }        // 计数排序        int[] arr = new int[d + 1];        for (int i : array) {            arr[i - min]++;        }        int maxDistance = 0;                // 寻找最长连续的0        for (int i = 0; i &lt; arr.length; i++) {            if (arr[i] == 0) {                int temp = i;                while (arr[temp] == 0) {                    temp++;                }                if (temp - i &gt; maxDistance) {                    maxDistance = temp - i;                }            }        }        return maxDistance + 1;    }</code></pre><h3>桶排序</h3><p>记录桶内的最大和最小值，统计出每一个桶的最大值，和这个桶右侧非空桶的最小值的差，数值最大的差即为题目所求，故时间复杂度为O(n)</p><pre><code class="language-Java">    public static int getMaxSortedDistance(int[] array) {        // 求出数组的最大值和最小值        int max = array[0];        int min = array[0];        for (int i = 1; i &lt; array.length; i++) {            if (array[i] &gt; max) {                max = array[i];            }            if (array[i] &lt; min) {                min = array[i];            }        }        int d = max - min;        // 最大值和最小值相等，证明数组的所有数相等        if (d == 0) {            return 0;        }        // 初始化桶        int bucketNum = array.length;        Bucket[] buckets = new Bucket[bucketNum];        Arrays.fill(buckets,new Bucket());        // 遍历原始数组，确定每个桶的最大值和最小值        for (int i : array) {            int index = ((i - min) * (bucketNum - 1) / d);            if (buckets[index].min == null || buckets[index].min &gt; i) {                buckets[index].min = i;            }            if (buckets[index].max == null || buckets[index].max &lt; i) {                buckets[index].max = i;            }        }        // 遍历桶，找到最大差值        int leftMax = buckets[0].max;        int maxDistance = 0;        for (int i = 1; i &lt; buckets.length; i++) {            if (buckets[i].min == null) {                continue;            }            if (buckets[i].min - leftMax &gt; maxDistance) {                maxDistance = buckets[i].min - leftMax;            }            leftMax = buckets[i].max;        }        return maxDistance;    }        private static class Bucket {        Integer min;        Integer max;    }</code></pre><h2>用栈实现队列</h2><blockquote><p>用栈来模拟一个队列，要求实现队列的两个基本操作：入队和出队。</p></blockquote><p>使用一个栈明显是不能实现队列的，所以需要一个辅助栈。设这两个栈为a和b。</p><p>a栈用来存储入队的元素，入队的元素直接压入a栈就好。</p><p>b栈用来存储出队的元素，出队的元素就直接推出b栈就好。</p><p>当b栈是空的，就将a栈的所有元素出栈压入a栈，这样出栈的顺序就相反了，即符合先进先出了。</p><pre><code class="language-Java">class CQueue {    Stack&lt;Integer&gt; stack1;    Stack&lt;Integer&gt; stack2;    public CQueue() {        stack1 = new Stack&lt;&gt;();        stack2 = new Stack&lt;&gt;();    }        public void appendTail(int value) {        stack1.push(value);    }        public int deleteHead() {        if(!stack2.isEmpty()) {            return stack2.pop();        }        if(stack1.isEmpty()) {            return -1;        }        while(!stack1.isEmpty()) {            stack2.push(stack1.pop());        }        return stack2.pop();    }    }/** * Your CQueue object will be instantiated and called as such: * CQueue obj = new CQueue(); * obj.appendTail(value); * int param_2 = obj.deleteHead(); */</code></pre><h2>删除k个数字后的最小值</h2><blockquote><p>给出一个整数，从该整数中去掉k个数字，要求剩下的数字形成的新整数尽可能小。应该如何选取被去掉的数字？</p></blockquote><p>从左往右，第一位大于它右边的数，将该数删了就会让整个数值降低。</p><pre><code class="language-Java">    public static String removeKDigits(String num, int k) {        int newLength = num.length() - k;        char[] stack = new char[num.length()];        int top = 0;        for (int i = 0; i &lt; num.length(); i++) {            char c = num.charAt(i);            while (top &gt; 0 &amp;&amp; stack[top - 1] &gt; c &amp;&amp; k &gt; 0) {                top -= 1;                k -= 1;            }            stack[top++] = c;        }        int offset = 0;        while (offset &lt; newLength &amp;&amp; stack[offset] == &#39;0&#39;) {            offset++;        }        return offset == newLength ? &quot;0&quot; : new String(stack,offset,newLength - offset);    }</code></pre><h2>如何实现大整数相加</h2><blockquote><p>给出两个很大的正整数，要求实现程序求出两个整数之和。</p></blockquote><pre><code class="language-Java">    public static String bigNumberSum(String bigNumberA, String bigNumberB) {        int maxLen = Math.max(bigNumberA.length(), bigNumberB.length()) + 1;        // 将两个大数逆序存储到数组中        int[] arrA = new int[maxLen];         for (int i = 0; i &lt; bigNumberA.length(); i++) {            arrA[i] = bigNumberA.charAt(bigNumberA.length() - i - 1) - &#39;0&#39;;        }        int[] arrB = new int[maxLen];        for (int i = 0; i &lt; bigNumberB.length(); i++) {            arrB[i] = bigNumberB.charAt(bigNumberB.length() - i - 1) - &#39;0&#39;;        }        // 进行运算        int[] res = new int[maxLen];        for (int i = 0; i &lt; res.length; i++) {            int temp = res[i];            temp += arrA[i] + arrB[i];            if (temp &gt;= 10) {                temp -= 10;                res[i + 1]++;            }            res[i] = temp;        }                // 将结果逆序        StringBuilder str = new StringBuilder();        boolean findFirst = false;        for (int i = res.length - 1; i &gt;= 0; i--) {            if (!findFirst) {                if (res[i] == 0) {                    continue;                }                findFirst = true;            }            str.append(res[i]);        }        return str.toString();    }</code></pre><h2>金矿问题</h2><blockquote><p>​很久很久以前，有一位国王拥有5座金矿，每座金矿的黄金储量不同，需要参与挖掘的工人人数也不同。例如又得金矿储量是500kg黄金，需要5个工人来挖掘；有的金矿储量是200kg黄金，需要3个工人来挖掘......</p><p>​如果参与挖矿的工人的总数是10.每座金矿要么全挖，要么不挖，不能派出一半人挖取一半的金矿。要用程序求出，要想得到尽可能多的黄金，应该选取挖取那几座金矿？</p></blockquote><p>这题就是一道背包问题，根据题意可知这是01背包的问题，有两种状态转移的情况，选或者是不选。</p><pre><code class="language-Java">    /**     * @param w 工人数量     * @param p 金矿开采所需的工人数量     * @param g 金矿储量     */    public static int getBestGoldMining(int w, int[] p, int[] g) {        int[] res = new int[w + 1];        for (int i = 1; i &lt;= g.length; i++) {            for (int j = w; j &gt;= 1; j--) {                if (j &gt;= p[i - 1]) {                    res[j] = Math.max(res[j],res[j - p[i - 1]] + g[i - 1]);                }            }        }        return res[w];    }</code></pre><h2>寻找缺少的整数</h2><blockquote><p>在一个无序数组里有99个不重复的正整数，范围从1到100，唯独缺少1个1~100中的整数。如何找出整个缺少的整数。</p></blockquote><h3>哈希表</h3><p>创建一个哈希表，将1到100这100个数全部存入key中，遍历整个数组，将存在的key都删掉，剩下的key就是缺少的整数。</p><pre><code class="language-Java">    public static int getBestGoldMining(int[] arr) {        Map&lt;Integer, Integer&gt; map = new HashMap&lt;&gt;(100);        for (int i = 1; i &lt;= 100; i++) {            map.put(i,i);        }        for (int i : arr) {            map.remove(i);        }        int res = 0;        for (int key : map.keySet()) {          res = key;          }        return res;    }</code></pre><h3>先排序后寻找</h3><p>将数组进行排序，然后逐个寻找。</p><pre><code class="language-Java">    public static int getBestGoldMining(int[] arr) {        Arrays.sort(arr);        if (arr[0] != 1) {            return 0;        }        for (int i = 0; i &lt; arr.length - 1; i++) {            if (arr[i] + 1 != arr[i + 1]) {                return arr[i] + 1;            }        }        return 100;    }</code></pre><h3>求和</h3><p>计算出1~100的和，然后减去数组中元素的和，剩下的就是所缺少的整数了。</p><pre><code class="language-Java">    public static int getBestGoldMining(int[] arr) {        int res = 5050;        for (int i : arr) {            res -= i;        }        return res;    }</code></pre><h3>求只出现一次的数</h3><blockquote><p>​一个无序数组里有若干个正整数，范围是1~100，其中99个整数都出现了偶数次，只有一个整数出现了奇书次，如何找到这个出现奇数次的整数？</p></blockquote><p>可以将数与数出现的次数当成Key和Value放入哈希表，然后寻找出现次数为1的，和上述哈希表的做法差不多，就不深入了。</p><p>这里可以使用异或运算，先了解异或运算的规律。</p><ol><li>两个相同的数异或，结果等于0</li><li>任何数与0异或，结果等于本身</li><li>异或运算符合交换律</li></ol><p>故将数组中的数进行异或，偶数次的数异或会变成0，奇数次的数就会留着。</p><pre><code class="language-Java">    public static int getBestGoldMining(int[] arr) {        int res = 0;        for (int i : arr) {            res ^= i;        }        return res;    }</code></pre><h2>LRU（最近最少使用算法）</h2><pre><code class="language-Java">package com.yww;import java.util.HashMap;/** * @ClassName LRUCache * @author yww */public class LRUCache {    /**     * 头节点     */    private Node head;    /**     * 尾节点     */    private Node end;    /**     * 缓存的个数     */    private int limit;    /**     * 哈希表缓存     */    private HashMap&lt;String, Node&gt; map;    class Node {        public Node pre;        public Node next;        public String key;        public String value;        Node(String key, String value) {            this.key = key;            this.value = value;        }    }    public LRUCache(int limit) {        this.limit = limit;        map = new HashMap&lt;&gt;();    }    /**     * 通过key获取缓存节点     */    public String get(String key) {        Node node = map.get(key);        if (node == null) {            return null;        }        refreshNode(node);        return node.value;    }    /**     * 放入缓存，若key存在，更新value值     */    public void put(String key, String value) {        Node node = map.get(key);        if (node == null) {            if (map.size() &gt;= limit) {                String oldKey = removeNode(head);                map.remove(oldKey);            }            node = new Node(key, value);            addNode(node);            map.put(key,node);        } else {            node.value = value;            refreshNode(node);        }    }    /**     * 通过key删除缓存     */    public void remove(String key) {        Node node = map.get(key);        removeNode(node);        map.remove(key);    }    /**     * 刷新被访问的节点     */    private void refreshNode(Node node) {        if (node == end) {            return;        }        removeNode(node);        addNode(node);    }    /**     * 删除节点     */    private String removeNode(Node node) {        if (node == head &amp;&amp; node == end) {            head = null;            end = null;        } else if (node == end) {            end = end.pre;            end.next = null;        } else if (node == head) {            head = head.next;            head.pre = null;        } else {            node.pre.next = node.next;            node.next.pre = node.pre;        }        return node.key;    }    /**     * 尾部插入节点     */    private void addNode(Node node) {        if (end != null) {            end.next = node;            node.pre = end;            node.next = null;        }        end = node;        if (head == null) {            head = node;        }    }}</code></pre>]]></content:encoded>
            <category domain="https://yww52.com/categories/%25E8%25AF%25BB%25E4%25B9%25A6%25E7%25AC%2594%25E8%25AE%25B0/">读书笔记</category>
        </item>
        <item>
            <title>码出高效Java开发手册</title>
            <link>https://yww52.com/posts/b93b977e/</link>
            <guid isPermaLink="true">https://yww52.com/posts/b93b977e/</guid>
            <pubDate>Sun, 30 May 2021 00:00:00 +0000</pubDate>
            <description>记录码出高效Java开发手册的读书笔记，主要是书中一些我觉得很有收获的观点，所以大多是原书内容，小部分是自己的总结。</description>
            <content:encoded><![CDATA[<img src="https://img.yww52.com/2021/5/2021-5-30top_img.jpg" alt="码出高效Java开发手册" style="max-width:100%;height:auto;margin-bottom:15px;" /><h2>面向对象的理念(OOP)</h2><blockquote><p>面向过程编程让计算机有步骤地顺序地做一件事情，是一种过程化地叙事思维。但是在大型软件开发过程中，使用面向过程语言开发，软件维护，软件复用存在着巨大的困难。</p></blockquote><p>面向过程是用来解决一个问题的最好的思想，面向过程最主要的是过程，好的过程就会让一个问题快速正确的获得正确的答案。</p><p>但在软件开发方面，会出现很多很多的功能，即出现很多很多的问题需要解决，对于面向对象思想无疑会产生很多问题。</p><ol><li>无法复用代码，在不同问题中出现相同的子问题，就需要又去复用之前出现的代码，无疑是很麻烦的。</li><li>耦合度高。面向过程的代码不断穿插，就会让软件的耦合度过高，这样只要一个小问题出现了错误，就有可能导致整个大的问题发生错误，这是我们不想看到的。</li></ol><p>面向对象思想的想法就是为了解决这样的问题。</p><h2>面向对象的特性</h2><blockquote><p>传统意义上，面向对象有三大特性：封装，继承，多态。本书明确将“抽象”作为面向对象的特性之一，支持面向对象“四大特性”的说法。</p></blockquote><blockquote><p>Java之父Gosling设计的Object类，是任何类的默认父类，是对万事万物的抽象，是在哲学方向上的延伸思考，高度概括了事务的自然行为和社会行为。Object还对哲学的三个经典问题进行了隐约的解答。</p><ol><li>我是谁？ getClass()说明本质上是谁，而toString()是当前我的名片。</li><li>我从哪里来？Object()构造方法是生产对象的基本方式，clone()是繁殖对象的另一种方式。</li><li>我到哪里去？finalize()是在对象销毁时触发的方法。</li></ol><p>Object还映射了社会科学领域的一些问题。</p><ol><li>世界是否与你而不同？hashCode()和equals()就是判断与其他元素是否相同的一组方法。</li><li>与他人如何协调？wait()和notify()是对象间通信与协作的一组方法。</li></ol></blockquote><p>Object的类是抽象的核心体现。他将万物抽象成一个Object，之后每一个类都继承于Object，将Object类当成一个模板，去将事物抽象成一个类。</p><ol><li><p>封装。封装是将一个类中的行为和属性进行的信息控制，决定是否要公开对类行为和属性的控制。外界只要知道调用这个行为方法，就可以实现一些功能，而不用知道如何去内部是怎样实现的。书中有句话说得很好，模块之间的协作只需要忠于接口，忠于功能实现即可。</p></li><li><p>继承。类将一些复用的行为和属性赋予子类，也就是子类继承了父类，从而复用了父类的代码，也为多态打下基础。</p><blockquote><p>继承的成本很低，一个关键字就可以使用别人的方法，所以继承像抗生素一样容易被滥用，我们传递的理念是谨慎使用继承，认清继承滥用的危险性，即方法污染和方法爆炸。</p><p>方法污染是指父类具备的行为，通过继承传递给子类，子类并不具备执行此行为的能力，这就是方法污染。（因为父类有些方法不希望子类使用）</p><p>方法爆炸是指继承树不断扩大，底层类拥有的方法虽然都能执行，但是由于方法众多，其中部分方法并非于当前类功能定位相关，多次继承后达到上百个方法，造成了方法爆炸。</p></blockquote></li><li><p>多态。根据运行时不同对象的类型，同一个方法产生不同的运行结果，使同一个行为具有不同的表现形式。</p></li></ol><h2>接口与抽象类</h2><blockquote><p>抽象类是模板式设计，而接口是契约式设计。</p></blockquote><p>抽象类和接口是对一个事物更高级的抽象，抽象类通常包括抽象方法，实体方法和属性变量，而接口一般只是一组行为，没有具体实现和属性。</p><p>抽象类是模板，意义是给子类提供一个抽象的模板，接口是一个契约，继承该接口就要实现里面的所有方法，即遵循这个契约。</p><h2>覆写</h2><blockquote><p>多态中的override，本书翻译成覆写。如果翻译成重写，那么与重构意思过于接近；如果翻译成覆盖，那么少了“写”这个核心动词。如果父类定义的方法达不到子类的预期，那么子类可以重新实现方法覆盖父类的实现。（实现多态的方式）父类引用执行子类方法时需要注意</p><ol><li>无法调用到子类中存在而父类本身不存在的方法。</li><li>可以调用到子类覆写了父类的方法，这是一种多态的实现。</li></ol></blockquote><p>覆写父类的方法，需要满足以下四个条件。</p><ol><li>访问权限不能变小 。</li><li>返回类型能够向上转型成父类的返回类型。这里的向上转型必须是严格的继承关系，数据类型基本不存在通过继承向上转型的问题。比如<code>int</code>和<code>Integer</code>,不会自动给装箱，是非兼容返回类型。</li><li>异常也要能向上转型成为父类的异常。</li><li>方法名，参数类型及个数必须严格一致。</li></ol><p>每一个覆写方法最好加上<code>@Override</code>注解，编译器会自动检查覆写方法签名是否一致。</p><h2>重载</h2><blockquote><p>在同一个类中，如果多个方法有相同的方法名称，不同的参数类型，参数个数，参数顺序，即为重载。（方法返回值不在考虑范围）</p></blockquote><p>JVM在重载方法中，选择合适的目标方法的顺序如下：</p><ol><li>精确匹配。</li><li>如果是基本数据类型，自动转换成更大表示范围的基本类型。</li><li>通过自动拆箱与装箱。</li><li>通过子类向上转型继承路线依次匹配。</li><li>通过可变参数匹配。</li></ol><blockquote><p>父类的公有实例方法与子类的公有实例方法可以存在重载关系。不管继承关系如何复杂，重载在编译时可以根据规则知道调用哪种目标方法。所以重载又称为静态绑定。</p></blockquote><h2>基本数据类型</h2><blockquote><p>虽然Java是面向对象编程语言，一切皆是对象，但是为了兼容人类根深蒂固的数据处理习惯，加快常规数据的处理速度，提供了9种基本数据类型，它们都不具备对象的特性，没有属性和行为。</p><p>9种数据类型包括：boolean,byte,char,short,int,long,float,double,refvar。</p><p>refvar是面向对象世界中的引用变量，也叫引用句柄。本书认为它也是一种基本数据类型。</p></blockquote><p>其中的boolean是一个比较特殊的存在。它的值为false或者是true。在计算机的世界，0表示false,1表示true。所以常规思想会认为booealn的大小为1字节或1位。</p><p>Java虚拟机描述，虽然定义了Boolean数据类型，但是对它的支持有限，所以在编译时用<code>int</code>数据类型来代替，所以会占用4个字节。但时Boolean数组在编译时会被编译成<code>byte</code>数组，所以数组中Boolean占用1个字节。</p><p>refvar无论指向什么，都是占用4个字节的空间。</p><h2>代码风格</h2><p>书的第三章一章书都是在讲代码风格。</p><p>对于我这种有一点代码洁癖的人来说，还是很建议去学习一套规范的代码风格的。</p><p>这里讲的不是很详细，详细的可以去看一下《阿里巴巴Java开发手册》那本书，一本小册子写了记录了很多的代码规范。</p><p>IDEA也有一个插件，用阿里巴巴的代码规范来规范你的代码，多写些规范的代码，利于别人也利于自己。</p><h2>字节码</h2><blockquote><p>​如果某个程序因为不同的硬件平台需要编写多套代码，这是十分令人崩溃的。Java的使命就是一次编写，到处执行。在不同的操作系统，不同的硬件平台上，均可以不用修改代码即可顺畅地执行，如何实现跨平台？有一个声音在天空中回响：计算机工程领域的任何问题都可以通过一个中间层来解决。因此，中间码应运而生，即“字节码”（Bytecode）。</p></blockquote><p>Java之所以会这么流行，离不开它的一次编写，到处执行的跨平台的这个特性。不同平台的Java文件（.java），都会编译产生字节码文件（.class）,从字节码文件这一中间层屏蔽了上层操作系统（平台）的差异性，只要安装了Java的环境，都能运行出Java文件，能解决很多平台之间的运行问题。</p><h2>Java内存结构</h2><h3>堆</h3><p>堆分成了两大块，新生代和老年代（它们之间的占比为1:2）。新生代又分为三个区域Eden区和两个Survivor区（它们占比为8:1:1）。</p><p>首先新创建的对象会放在Eden区上，等Eden区满了放不下新创建的对象了，就会进行YGC，将Eden区中和其中一个Survivor区（from区）活着的对象放入另一个一个Survivor区（to区）。以此循环。</p><p>每一个对象都会有一个计数器，经历了一次YGC之后，对象的计数器就会加一，当到达了一定阈值，就会进入老年代（Java默认为15）。</p><p>新生代进入老年代的两种方法：</p><ol><li>对象的计数器到达了阈值，进入老年代。</li><li>Eden区放不下新创建的对象，进行了YGC之后也放不下，就会提前将该对象晋升为老年代（过早老化）。</li></ol><h3>方法区</h3><p>方法区是JVM规定的一个区域，不是一个实在的区域，只是一个定义。</p><p>Java8之前是用永久代来实现了方法区，Java8之后是用元空间来实现方法区。</p><p>永久代包括了运行时常量池（比如String）和一些类元信息（类加载的文件，静态变量等）。</p><p>元空间只保存了类元信息，运行时常量池已经放入堆中进行存储。</p><p>不同与永久代，元空间在本地内存中分配。</p><h3>虚拟机栈和本地方法栈</h3><blockquote><p>​虚拟机栈是描述Java方法执行的内存区域，它是线程私有的。栈中的元素用与支持虚拟机进行方法调用，每个方法从开始调用到执行完成的过程，就是栈帧从入栈到出栈的过程。</p></blockquote><p>可以说，栈帧就是一个方法执行过程的具体体现。</p><p>栈帧主要包括四个部分。</p><ol><li>局部变量表。存放方法参数和局部变量的区域。</li><li>操作栈。操作栈是一个初始状态为空的桶式结构栈。在方法执行过程中，会有各种指令往栈中写入和提取信息。</li><li>动态连接。每个栈中中包含一个在常量池中对当前方法的引用，目的是支持方法调用过程的动态连接。</li><li>方法返回地址。方法执行有两种退出情况。一，正常退出。二，异常退出。无论何种退出情况，都将返回至方法被调用的位置。</li></ol><blockquote><p>虚拟机栈主内，本地方法栈主外。这个内外是针对JVM来说的，本地方法栈为Native方法服务的。</p></blockquote><h3>程序计数寄存器</h3><blockquote><p>​每个线程在创建后，都会产生自己的程序计数器和栈帧，程序计数器用来存放执行指令的偏移量和行号指示器等，线程执行或恢复都要依赖程序计数器。程序计数器在各个线程之间互不影响，此区域也不会发生内存溢出的异常。</p></blockquote><h2>try-catch-finally</h2><blockquote><ol><li><p>try代码块监视代码执行过程，一旦发现异常则直接跳转到catch，如果没有catch，就直接跳转到finally。</p></li><li><p>catch代码块可选执行的代码块，如果没有任何异常发生则不会执行；如果发现异常则进行处理或向上抛出。</p></li><li><p>finally代码块必选执行代码块，不管是否有异常产生，即使发售鞥OutOfMemoryError也会执行，通常用于处理善后清理工作。如果finally代码块没有执行，则会有三种情况。</p><ul><li>没有进入try代码块</li><li>进入try代码块，但是代码运行中出现了死循环或死锁状态</li><li>进入try代码块，但是执行了System.exit()操作</li></ul></li></ol></blockquote><h2>fail-fast机制</h2><p>fail-fast机制是几何世界中比较常见的错误检测机制，通常出现在遍历集合元素的过程中。</p><p>java.util下的所有集合类都是fail-fase机制的。</p><p>比如在这些集合中，使用foreach遍历元素时进行删除，就会出现异常。</p><h2>ConcurrentHashMap</h2><blockquote><p>​需要注意的时，无论是JDK7还是JDK8，ConcurrentHashMap的size()方法都只能返回一个大概数量，无法做到100%精确，因为已经统计过的槽在size()返回最终结果前可能又出现了变化，导致返回大小与实际大小存在些许差异。在多个槽的设计下，如果仅仅是为了统计元素数量而停下所有操作，又会显得因噎废食。因此，ConcurrentHashMap在涉及元素总数的相关更新和计算时，会最大限度地减少锁的使用，以减少线程间地竞争与互相等待。在这个设计思路上，JDK8的ConcurrentHashMap对元素总数的计算又做了进一步的优化，具体表现：在put()，remove()和size()方法中，涉及元素总数的更新和计算，都彻底避免了锁的使用，取而代之的是众多的CAS操作。</p></blockquote><h2>线程安全</h2><blockquote><p>线程安全问题只在多线程环境下才出现，单线程串行执行不存在此问题。保证高并发场景下的线程安全，可以从以下四个纬度考量</p><ol><li>数据单线程内可见。</li><li>只读对象</li><li>线程安全类</li><li>同步与锁机制</li></ol><p>线程安全的核心理念就是“要么只读，要么加锁”。</p></blockquote><p>该注意的是线程安全的主体是对象，而不是线程。</p><p>一般我们说线程安全的主语都是对象。</p><blockquote><p>Java并发编程实践中对线程安全的定义</p><p>​当多个线程访问一个对象时，如果不用考虑这些线程在运行时环境下的调度和交替执行，也不需要进行额外的同步，或者在调用方进行任何其他的协调操作，调用这个对象的行为都可以获得正确的结果，那这个对象就是线程安全的。</p></blockquote><h2>volatile</h2><blockquote><p>​volatile的英文本已是“挥发，不稳定的”，延申意义为敏感的。当使用volatile修饰变量时，意味着任何对此变量的操作都会在内存中进行，不会产生副本，以保证共享变量的可见性，局部阻止了指令重排的发生。</p><p>​“volatile是轻量级的同步方式”这种说法是错误的。它只是轻量级的线程操作可见方式，并非同步方式，如果是多写场景，一定会产生线程安全问题。</p></blockquote><p>volatile关键字的特性</p><ol><li>保证可见性</li><li>不保证原子性</li><li>禁止指令重排</li></ol><h2>读书感谢</h2><p>这本书的前面半部分讲的通俗易懂，讲的很好，但是到后面有几章通过源码来进行讲解部分类与方法，这里就开始接触到部分工具的底层，如果没有一定的基础去看的话会很累，而且也会很繁琐。这些知识点很重要，作者想尽量写全，但是书的篇幅是有限的，所以就会感觉后面讲源码的部分看的体验就不是很好，有些地方也没有更深入的去讲。总的来说还是一本很好的书，适合学习Java的人观看，但是后面的一些知识点需要更加了解的话，就还需要通过其他途径，作者只是给各位读者开个头，了解一下，也是查缺补漏。最后孤尽老师还写了段话送给大家。</p><blockquote><p>​最后，做一个有技术情怀的人。技术情怀总结成两个关键字：热爱，卓越。热爱是一种源动力，卓越是一种境界。兴趣是最好的老师，也是最好的动力。而热爱是一种信念，即使痛苦，也不会让你背离这份事业和内心的执着。对技术的热爱，让人用于归根究底，勇于坐冷板凳，勇于回馈别人。极致与卓越，似乎是一个意思，即出类拔萃，超出期望。技术情怀提倡我们追求极致式的卓越，把卓越在往前提升。不管一个人如何卓越与优秀，都要学会自我驱动，持续进步，追求个人内心的极致。因为卓越，所以经典，只有这样百尺竿头，才能更进一步。仰望星空的同时，是脚踏实地，这样才能不断地学习和打磨自己。</p></blockquote>]]></content:encoded>
            <category domain="https://yww52.com/categories/%25E8%25AF%25BB%25E4%25B9%25A6%25E7%25AC%2594%25E8%25AE%25B0/">读书笔记</category>
        </item>
        <item>
            <title>结果类封装</title>
            <link>https://yww52.com/posts/a3d4b987/</link>
            <guid isPermaLink="true">https://yww52.com/posts/a3d4b987/</guid>
            <pubDate>Sat, 22 May 2021 00:00:00 +0000</pubDate>
            <description>记录一下项目中常用的返回信息的封装类。</description>
            <content:encoded><![CDATA[<img src="https://img.yww52.com/2021/5/2021-5-22top_img.jpg" alt="结果类封装" style="max-width:100%;height:auto;margin-bottom:15px;" /><p>首先要封装一个响应的枚举，将一些常见的错误写入，一般状态码需要与前端一起制定，以便前端提示错误信息以及发生错误的处理逻辑。</p><pre><code class="language-java">package com.yww.demo.util;/** * 结果状态码枚举 * * @author yww */public enum ResultCode {    /**     * 成功     */    SUCCESS(200, &quot;成功&quot;),    /**     * 服务器错误     */    FAILED(500, &quot;服务器发生错误&quot;);    private Integer status;    private String message;    ResultCode(Integer status, String message) {        this.status = status;        this.message = message;    }    public Integer getStatus() {        return this.status;    }    public String getMessage() {        return this.message;    }}</code></pre><p>然后是数据返回的封装类。</p><pre><code class="language-java">package com.yww.demo.util;import lombok.Data;/** * @ClassName Result * @Descriprtion 结果类封装 * @Author yww * @Date 2021/5/21 23:22 **/@Datapublic class Result&lt;T&gt; {    /**     * 状态码     */    private Integer code;    /**     * 返回内容     */    private String message;    /**     * 返回数据     */    private T data;    /**     * 私有化无参构造函数     */    private Result() {}    /**     * 全参构造函数     *     * @param code    状态码     * @param message 返回内容     * @param data    返回数据     */    public Result(Integer code, String message, T data) {        this.code = code;        this.message = message;        this.data = data;    }    /**     * 返回一个响应成功的信息，不带数据     */    public static &lt;T&gt; Result&lt;T&gt; success() {        return new Result&lt;&gt;(ResultCode.SUCCESS.getStatus(), ResultCode.SUCCESS.getMessage(), null);    }    /**     * 返回一个响应成功的信息，带数据     *     * @param data    返回数据     */    public static &lt;T&gt; Result&lt;T&gt; success(T data) {        return new Result&lt;&gt;(ResultCode.SUCCESS.getStatus(), ResultCode.SUCCESS.getMessage(), data);    }    /**     *  返回一个响应错误的信息     */    public static &lt;T&gt; Result&lt;T&gt; failure() {        return new Result&lt;&gt;(ResultCode.FAILED.getStatus(), ResultCode.FAILED.getMessage(),null);    }    /**     *  返回一个响应错误的信息，可以自定义错误信息     *  @param message   返回的错误信息内容     */    public static &lt;T&gt; Result&lt;T&gt; failure(String message) {        return new Result&lt;&gt;(ResultCode.FAILED.getStatus(), message,null);    }    /**     *  根据结果状态码，返回一个响应错误的信息     *  @param resultCode  状态码     */    public static &lt;T&gt; Result&lt;T&gt; failure(ResultCode resultCode) {        return new Result&lt;&gt;(resultCode.getStatus(), resultCode.getMessage(),null);    }}</code></pre>]]></content:encoded>
            <category domain="https://yww52.com/categories/%25E6%2588%2591%25E7%259A%2584%25E6%258A%2580%25E6%259C%25AF%25E5%25AD%25A6%25E4%25B9%25A0%25E7%2589%25A9%25E8%25AF%25AD%25E6%259E%259C%25E7%2584%25B6%25E6%259C%2589%25E9%2597%25AE%25E9%25A2%2598/">我的技术学习物语果然有问题</category>
        </item>
        <item>
            <title>背包专题</title>
            <link>https://yww52.com/posts/4f2ce235/</link>
            <guid isPermaLink="true">https://yww52.com/posts/4f2ce235/</guid>
            <pubDate>Mon, 19 Apr 2021 00:00:00 +0000</pubDate>
            <description>记录一下学习背包问题时的思路和解法</description>
            <content:encoded><![CDATA[<img src="https://img.yww52.com/2021/4/2021-4-19top_img.jpg" alt="背包专题" style="max-width:100%;height:auto;margin-bottom:15px;" /><h1>01背包</h1><blockquote><p>有 N 件物品和一个容量是 V 的背包。每件物品只能使用一次。</p><p>第 i 件物品的体积是 v[i]，价值是 w[i]。</p><p>求解将哪些物品装入背包，可使这些物品的总体积不超过背包容量，且总价值最大。输出最大价值。</p></blockquote><p>01背部的特征是每个物品都只有一件，要么选择装进背包要么选择不装进背部。</p><p>首先是状态表示，一般都能想到的是二维数组来表示状态<code>dp[i][j]</code>，这个应该表示的是从前<code>i</code>个物品中选取，总体积是<code>j</code>的情况下，总价值最大是多少。</p><p>那么可以知道这道题的答案就是<code>dp[N][V]</code>了。</p><p>然后是状态转移了。</p><p>从题目就可以看出选择的方案，或者说是状态转移的方案。</p><ol><li><p>不选择第<code>i</code>件物品，即可以理解为从第<code>i - 1</code>件物品中选择，不选择第<code>i</code>件物品了，背包容量不变。</p><p><code>dp[i][j] = dp[i - 1][j]</code></p></li><li><p>选择第<code>i</code>件物品，即可以理解选择了第<code>i - 1</code>件物品之后，从第<code>i - 1</code>件物品中选择，背包容量变为原容量减去第<code>i</code>件物品的体积，这个方案的前提是当前背包容量大于物品的体积。</p><p><code>dp[i][j] = dp[i - 1][j - v[i]] + w[i]</code></p></li></ol><p>然后是结果，题目要求的是总价值最大，所以将两个方案取最大值就好。</p><pre><code class="language-java">import java.util.Scanner;public class Main{    public static void main(String[] args) {        Scanner sc = new Scanner(System.in);        int N = sc.nextInt();        int V = sc.nextInt();        int[] v = new int[N + 1];        int[] w = new int[N + 1];        for (int i = 1; i &lt;= N; i++) {            v[i] = sc.nextInt();            w[i] = sc.nextInt();        }        // 以上为输入        int[][] dp = new int[N + 1][V + 1];        // 物品是需要全部遍历的，所以是从第一件物品遍历到第N件物品        for (int i = 1; i &lt;= N; i++) {            // 体积是需要从0遍历到背包的体积            for (int j = 0; j &lt;= V; j++) {                // 如果当前背包容量大于该物品体积，就选取两个方案的最大值                // 如果背包容量小，那就选择不放入该物品的容量                if(j &gt;= v[i]){                    dp[i][j] = Math.max(dp[i - 1][j], dp[i - 1][j - v[i]] + w[i]);                }else{                    dp[i][j] = dp[i - 1][j];                }            }        }        System.out.println(dp[N][V]);    }    }</code></pre><p>优化dp数组。</p><p>这里使用的是二维的dp数组表示状态，但其实可以优化成一维的数组。</p><p>仔细观察这两个方程的状态转移方程。</p><ol><li><code>dp[i][j] = dp[i - 1][j]</code></li><li><code>dp[i][j] = dp[i - 1][j - v[i]] + w[i]</code></li></ol><p>第一个很好理解，将第<code>i - 1</code>的层<code>j</code>状态转移到第<code>i</code>层，但因为总体积是不变的，即<code>j</code>是不变的，所以这个转移相当于没有意义，所以遍历了<code>i</code>层这个转移就自动成立了。</p><p>第二个就会有点麻烦。当我们想要更新当前的状态，那么就需要第<code>i -1 </code>层的<code>[j - v[i]] + w[i]</code>数据来更新，如果我们直接使用<code>d[j] = dp[j - v[i]] + w[i]</code>是不对的。</p><p>这个式子其实等价于<code>dp[i][j] = dp[i][j - v[i]] + w[i]</code>，与之前的状态转移是不一样的，所以是不对的，要想这个式子与之前的状态转移方程等价，将体积从<code>V</code>到0遍历就可以了。</p><pre><code class="language-Java">for (int i = 1; i &lt;= N; i++) {            for (int j = V; j &gt;= 0; j--) {                // 这个条件放到for循环上                if(j &gt;= v[i]){                    dp[j] = Math.max(dp[j], dp[j-v[i]] + w[i]);                }else{                    // 这步直接省去                    dp[j] = dp[j];                }            }        }</code></pre><p>故完整的代码为</p><pre><code class="language-Java">import java.util.Scanner;public class Main{    public static void main(String[] args) {        Scanner sc = new Scanner(System.in);        int N = sc.nextInt();        int V = sc.nextInt();        int[] v = new int[N + 1];        int[] w = new int[N + 1];        for (int i = 1; i &lt;= N; i++) {            v[i] = sc.nextInt();            w[i] = sc.nextInt();        }        int[] dp = new int[V + 1];        for (int i = 1; i &lt;= N; i++) {            for (int j = V; j &gt;= v[i]; j--) {                    dp[j] = Math.max(dp[j], dp[j-v[i]] + w[i]);            }        }        System.out.println(dp[V]);    }}</code></pre><p>为什么逆序遍历可以让上述两个式子等价呢？</p><p>遍历第<code>i</code>层的时候，<code>dp[j-v[i]]</code>会用到之前的状态，这个式子其实等价于<code>dp[i][j-v[i]]</code>，在同一层里，用到了之前的状态，而不是上一层<code>i - 1</code>的状态，这就会导致<code>i</code>这个物品是被用到了第二次的，而01背包是每个物品只能用一次的，所以这个式子是不对的，逆序遍历体积容量的话就不会用到被<code>污染</code>的数据，就让式子成立了。（是有点绕，以后理解更深了在来修改一下说法）</p><p>还可以进一步优化输入，即便输入边处理数据。</p><pre><code class="language-Java">import java.util.Scanner;public class Main{    public static void main(String[] args) {        Scanner sc = new Scanner(System.in);        int N = sc.nextInt();        int V = sc.nextInt();        int[] dp = new int[V + 1];        for (int i = 1; i &lt;= N; i++) {            int vi = sc.nextInt();            int wi = sc.nextInt();            for (int j = V; j &gt;= vi; j--) {                    dp[j] = Math.max(dp[j], dp[j-vi] + wi);            }        }        System.out.println(dp[V]);    }}</code></pre><h1>完全背包</h1><blockquote><p>有 N 种物品和一个容量是 V 的背包，每种物品都有无限件可用。</p><p>第 i 种物品的体积是 vi，价值是 wi。</p><p>求解将哪些物品装入背包，可使这些物品的总体积不超过背包容量，且总价值最大。输出最大价值。</p></blockquote><p>完全背包与01背包不一样的是完全背包是可以选无限件的。</p><p>所以完全背包与01背包分析不一样的就是状态的转移方案。</p><p>01背包问题里，是有两种方案的，选一个或者是不选，完全背包就会有很多种方案。</p><ol><li><p>不选第<code>i</code>件物品</p><p><code>dp[i][j] = dp[i - 1][j]</code></p></li><li><p>选择<code>k</code>件第<code>i</code>个物品，容量要大于体积。</p><p><code>dp[i][j] = dp[i - 1][j - v[i]] + w[i]</code></p></li></ol><p>跟01背包差不多，直接遍历所有第<code>i</code>件物品。</p><pre><code class="language-Java">import java.util.Scanner;public class Main {    public static void main(String[] args) {        Scanner sc = new Scanner(System.in);        int N = sc.nextInt();        int V = sc.nextInt();        int[] v = new int[N + 1];        int[] w = new int[N + 1];        int[][] dp = new int[N + 1][V + 1];        for (int i = 1; i &lt;= N; i++) {            v[i] = sc.nextInt();            w[i] = sc.nextInt();        }        for (int i = 1;i &lt;= N; i++) {            for (int j = 0; j &lt;= V; j++) {                // 遍历选择第i件物品的选项                for(int k = 0 ; k * v[i] &lt;= j ; k++) {                    dp[i][j] = Math.max(dp[i][j], dp[i - 1][j - k * v[i]] + k * w[i]);                }            }        }        System.out.println(dp[N][V]);    }}</code></pre><p>这里用到了三重循环，这样的话时间复杂度会很高，所以应该要考虑进行优化。</p><p>仔细观察一下这个状态转移方程。</p><p><code>dp[i][j] = max(dp[i - 1][j], dp[i - 1][j - v] + w, dp[i - 1][j - 2v] + 2w....)</code></p><p>我们求<code>dp[i][j]</code>的最大值也就是求后面这些方案的最大值，我们可以注意到</p><p><code>dp[i][j - v] = max(dp[i - 1][j - v], dp[i - 1][j - 2v] + w)....</code></p><p>所以通过二式我们可以求出一式后面的除了第一项的最大值。</p><p>故进行优化之后的状态状态方程为</p><p><code>dp[i][j] = max(dp[i - 1][j], dp[i][j - v] + w)</code></p><p>这样就能省去一重循环，于是变成下面这样。</p><pre><code class="language-Java">import java.util.Scanner;public class Main {    public static void main(String[] args) {        Scanner sc = new Scanner(System.in);        int N = sc.nextInt();        int V = sc.nextInt();        int[] v = new int[N + 1];        int[] w = new int[N + 1];        int[][] dp = new int[N + 1][V + 1];        for (int i = 1; i &lt;= N; i++) {            v[i] = sc.nextInt();            w[i] = sc.nextInt();        }        for (int i = 1;i &lt;= N; i++) {            for (int j = 0; j &lt;= V; j++) {                dp[i][j] = dp[i - 1][j];                if (j &gt;= v[i]) {                    dp[i][j] = Math.max(dp[i][j], dp[i][j - v[i]] + w[i]);                }            }        }        System.out.println(dp[N][V]);    }}</code></pre><p>变成这样会发现和01背包十分相似，所以也可以根据01背包的优化思路进行优化，先将该状态数组优化为一维。</p><pre><code class="language-Java">import java.util.Scanner;public class Main {    public static void main(String[] args) {        Scanner sc = new Scanner(System.in);        int N = sc.nextInt();        int V = sc.nextInt();        int[] v = new int[N + 1];        int[] w = new int[N + 1];        int[] dp = new int[V + 1];        for (int i = 1; i &lt;= N; i++) {            v[i] = sc.nextInt();            w[i] = sc.nextInt();        }        for (int i = 1;i &lt;= N; i++) {            // 这里用正序遍历是可以实现的，01背包需要逆序            for (int j = v[i]; j &lt;= V; j++) {                dp[j] = Math.max(dp[j], dp[j - v[i]] + w[i]);            }        }        System.out.println(dp[V]);    }}</code></pre><p>在优化下输入，便输入边处理 ，最终可以为这样。</p><pre><code class="language-Java">import java.util.Scanner;public class Main {    public static void main(String[] args) {        Scanner sc = new Scanner(System.in);        int N = sc.nextInt();        int V = sc.nextInt();        int[] dp = new int[V + 1];        for (int i = 1; i &lt;= N; i++) {            int v = sc.nextInt();            int w = sc.nextInt();            for (int j = v; j &lt;= V; j++) {                dp[j] = Math.max(dp[j],dp[j - v] + w);            }        }        System.out.println(dp[V]);    }}</code></pre><h1>多重背包</h1><blockquote><p>有 N 种物品和一个容量是 V 的背包。</p><p>第 i 种物品最多有 si 件，每件体积是 vi，价值是 wi。</p><p>求解将哪些物品装入背包，可使物品体积总和不超过背包容量，且价值总和最大。输出最大价值。</p></blockquote><p>多重背包跟完全背包不一样的点是多重背包的每件物品是有个数限制的，不过大体是和完全背包差不多的解题思路。</p><h2>暴力三重循环</h2><p>首先是最暴力的方式，三重循环，多重背包的三重循环相对于完全背包就是为第三个循环加多一个条件就好了。</p><pre><code class="language-Java">import java.util.Scanner;public class Main {    public static void main(String[] args) {        Scanner sc = new Scanner(System.in);        int N = sc.nextInt();        int V = sc.nextInt();        int[] v = new int[N + 1];        int[] w = new int[N + 1];        int[] s = new int[N + 1];        int[][] dp = new int[N + 1][V + 1];        for (int i = 1; i &lt;= N; i++) {            v[i] = sc.nextInt();            w[i] = sc.nextInt();            s[i] = sc.nextInt();        }        for (int i = 1;i &lt;= N; i++) {            for (int j = 0; j &lt;= V; j++) {                // 加多一个条件，个数小于或等于限制的条件                for(int k = 0 ; k * v[i] &lt;= j &amp;&amp; k &lt;= s[i] ; k++) {                    dp[i][j] = Math.max(dp[i][j], dp[i - 1][j - k * v[i]] + k * w[i]);                }            }        }        System.out.println(dp[N][V]);    }}</code></pre><p>三重循环的时间复杂度为O(n^3)，很大概率会超时，所以要进行优化才行。</p><h2>二进制优化</h2><p>其实多重背包可以转换成01背包的问题，只要将背包的个数拆分开来就可以了。</p><p>最容易想到的是将<code>n</code>件商品拆分成<code>n</code>个商品，这样将所有商品个数大于1的商品全部拆分，就可以理解为01背包的问题了，选或者是不选。</p><p>但是这种方案是行不通的。因为将<code>n</code>件商品拆分成<code>n</code>个，时间复杂度依旧很大，依然很容易超时，所以我们得考虑出另一种拆分方法，怎样拆分能用拆分后得数字表示出<code>0 ~ n</code>呢？</p><p>这就用到了一种二进制的拆分方法。</p><p>将数字转换成类似二进制的表示方法。</p><blockquote><p>比如要拆分9这个数字</p><p>我们可以拆分为 1 + 2 + 4 + 2</p><p>最后一个数小于2的n次方就使用剩余的数</p><p>这样多重背包的一个9件的商品问题就可以拆分为01背包的四个不同的商品的问题</p><p>使用这四个数选或者不选两种方案就能遍历出0 ~ 9十种方案</p></blockquote><p>接下来看代码。</p><pre><code class="language-Java">import java.util.*;public class Main {    public static void main(String[] args) {        Scanner sc = new Scanner(System.in);        int N = sc.nextInt();        int V = sc.nextInt();        List&lt;Integer&gt; vi = new ArrayList&lt;&gt;();        List&lt;Integer&gt; wi = new ArrayList&lt;&gt;();        // 将多重背包拆分成01背包        for (int i = 0; i &lt; N; i++) {            int v = sc.nextInt();            int w = sc.nextInt();            int s = sc.nextInt();            for (int k = 1; k &lt; s; k *= 2) {                vi.add(k * v);                wi.add(k * w);                s = s - k;            }            if (s &gt; 0) {                vi.add(s * v);                wi.add(s * w);            }        }        // 接下来是01背包解法        int[] dp = new int[V + 1];        for (int i = 0; i &lt; vi.size(); i++) {            for (int j = V; j &gt;= vi.get(i); j--) {                dp[j] = Math.max(dp[j],dp[j - vi.get(i)] + wi.get(i));            }        }        System.out.println(dp[V]);    }}</code></pre><h1>参考链接</h1><ul><li><a href="https://github.com/tianyicui/pack">https://github.com/tianyicui/pack</a></li><li><a href="https://www.acwing.com/problem/">https://www.acwing.com/problem/</a></li></ul>]]></content:encoded>
            <category domain="https://yww52.com/categories/%25E6%2588%2591%25E7%259A%2584%25E6%258A%2580%25E6%259C%25AF%25E5%25AD%25A6%25E4%25B9%25A0%25E7%2589%25A9%25E8%25AF%25AD%25E6%259E%259C%25E7%2584%25B6%25E6%259C%2589%25E9%2597%25AE%25E9%25A2%2598/">我的技术学习物语果然有问题</category>
        </item>
        <item>
            <title>整合Redis</title>
            <link>https://yww52.com/posts/3cd0c9b6/</link>
            <guid isPermaLink="true">https://yww52.com/posts/3cd0c9b6/</guid>
            <pubDate>Fri, 02 Apr 2021 00:00:00 +0000</pubDate>
            <description>SpringBoot整合Redis的Demo</description>
            <content:encoded><![CDATA[<img src="https://img.yww52.com/2021/4/2021-4-2top_img.jpg" alt="整合Redis" style="max-width:100%;height:auto;margin-bottom:15px;" /><h1>Jedis</h1><p>Jedis是官网推荐的Java连接工具，类似就是在Java和Redis中增加一层辅助层，辅助你用Java使用Redis。</p><p>简单的测试一下使用。</p><p>首先创建一个maven项目，导入相关的依赖。</p><pre><code class="language-xml">    &lt;!-- Jedis --&gt;    &lt;dependency&gt;        &lt;groupId&gt;redis.clients&lt;/groupId&gt;        &lt;artifactId&gt;jedis&lt;/artifactId&gt;        &lt;version&gt;3.5.1&lt;/version&gt;    &lt;/dependency&gt;</code></pre><p>连接使用Redis</p><pre><code class="language-Java">package com.yww;import redis.clients.jedis.Jedis;public class config {    public static void main(String[] args) {        // 创建Jedis对象，连接Redis,jedis对象相当于客户端        // Redis命令就是使用该对象的方法        Jedis jedis = new Jedis(&quot;127.0.0.1&quot;, 6379);        // 测试连接        System.out.println(jedis.ping());        // 关闭连接        jedis.close();    }}</code></pre><p>事务的使用。</p><pre><code class="language-Java">package com.yww;import redis.clients.jedis.Jedis;import redis.clients.jedis.Transaction;public class config {    public static void main(String[] args) {        Jedis jedis = new Jedis(&quot;127.0.0.1&quot;, 6379);        // 创建事务        Transaction multi = jedis.multi();        try{            // 组成事务的命令            // 执行事务            multi.exec();        } catch(Exception e) {            // 放弃事务            multi.discard();            e.printStackTrace();        } finally {            // 关闭连接            jedis.close();        }    }}</code></pre><h1>Lettuce</h1><p>整合就先创建一个SpringBoot的项目，导入SpringData项目的redis依赖。</p><pre><code class="language-xml">        &lt;dependency&gt;            &lt;groupId&gt;org.springframework.boot&lt;/groupId&gt;            &lt;artifactId&gt;spring-boot-starter-data-redis&lt;/artifactId&gt;            &lt;version&gt;2.4.2&lt;/version&gt;        &lt;/dependency&gt;</code></pre><p>:::note{type=&quot;info&quot;}</p><p>在比较新的SpringBoot版本中，放弃了Jedis对redis的直接连接，因为多个线程会出现线程不安全的情况，所以官方已经替换成了lettuce。    lettuce采用netty，实例可以在多个线程中进行共享，减少线程数量，不存在线程不安全的情况。</p><p>:::在配置文件中，配置Redis的连接。</p><pre><code class="language-properties"># 配置Redisspring.redis.host=127.0.0.1spring.redis.port=6379</code></pre><p>简单的测试。</p><pre><code class="language-Java">package com.yww;import org.junit.jupiter.api.Test;import org.springframework.beans.factory.annotation.Autowired;import org.springframework.boot.test.context.SpringBootTest;import org.springframework.data.redis.core.RedisTemplate;@SpringBootTestclass RedisTestApplicationTests {    @Autowired    private RedisTemplate redisTemplate;    @Test    void contextLoads() {        /*        *       数据类型的操作        *   1. opsForValue  操作字符串        *   2. opsForList   操作list        *   3. opsForSet    操作set        *   4. opsForHash   操作hash        *   5. opsForZSet   操作Zest        *   6. opsForGeo    操作geospatial        *   7. opsForHyperLogLog    操作Hyperloglog        *        */        redisTemplate.opsForValue().set(&quot;name&quot;,&quot;yww&quot;);        System.out.println(redisTemplate.opsForValue().get(&quot;name&quot;));        // 获取Redis的连接对象        RedisConnection connection = redisTemplate.getConnectionFactory().getConnection();        connection.flushDb();    }}</code></pre><p>上述的测试其实是没有经过序列化的，这是一个很不安全的操作，特别是传输对象的时候，没有序列化就会报错。</p><p>先简单的创建一个对象来测试。</p><pre><code class="language-Java">package com.yww;import lombok.AllArgsConstructor;import lombok.Data;import lombok.NoArgsConstructor;import org.springframework.stereotype.Component;@Component@AllArgsConstructor@NoArgsConstructor@Data// 实现Serializable序列化public class User implements Serializable {    private String name;    private int age;}</code></pre><pre><code class="language-Java">package com.yww;import com.fasterxml.jackson.core.JsonProcessingException;import com.fasterxml.jackson.databind.ObjectMapper;import org.junit.jupiter.api.Test;import org.springframework.beans.factory.annotation.Autowired;import org.springframework.boot.test.context.SpringBootTest;import org.springframework.data.redis.connection.RedisConnection;import org.springframework.data.redis.core.RedisTemplate;@SpringBootTestclass RedisTestApplicationTests {    @Autowired    private RedisTemplate redisTemplate;    @Test    void contextLoads() throws JsonProcessingException {        User user = new User(&quot;yww&quot;, 20);        //  转换为JSON字符串，传输的对象要经过序列化，不然会报错        String json = new ObjectMapper().writeValueAsString(user);        redisTemplate.opsForValue().set(&quot;user&quot;,json);        System.out.println(redisTemplate.opsForValue().get(&quot;user&quot;));    }}</code></pre><p>:::note{type=&quot;info&quot;}</p><p>默认是会使用jdk的序列化方式，在后期使用会出现一些麻烦，所以开发中一般会自己定义RedisTemplate的序列化方式，所以就需要自己定义一个RedisTemplate来使用。</p><p>:::</p><pre><code class="language-Java">/** * &lt;p&gt; *     Redis的配置类 * &lt;/p&gt; * @ClassName RedisConfig * @Author yww **/@EnableCaching@Configurationpublic class RedisConfig extends CachingConfigurationSelector {    /**     * 自定义RedisTemplate     * @return RedisTemplate     */    @Bean    @SuppressWarnings(value = { &quot;unchecked&quot;, &quot;rawtypes&quot;, &quot;deprecation&quot; })    public RedisTemplate&lt;Object, Object&gt; redisTemplate(RedisConnectionFactory redisConnectionFactory) {        RedisTemplate&lt;Object, Object&gt; redisTemplate = new RedisTemplate();        redisTemplate.setConnectionFactory(redisConnectionFactory);        Jackson2JsonRedisSerializer jackson2JsonRedisSerializer = new Jackson2JsonRedisSerializer(Object.class);        ObjectMapper mapper = new ObjectMapper();        mapper.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.ANY);        mapper.enableDefaultTyping(ObjectMapper.DefaultTyping.NON_FINAL);        jackson2JsonRedisSerializer.setObjectMapper(mapper);        StringRedisSerializer stringRedisSerializer = new StringRedisSerializer();        // key采用String的序列化方式        redisTemplate.setKeySerializer(stringRedisSerializer);        // hash的key也采用String的序列化方式        redisTemplate.setHashKeySerializer(stringRedisSerializer);        // value序列化方式采用jackson        redisTemplate.setValueSerializer(jackson2JsonRedisSerializer);        // hash的value序列化方式采用jackson        redisTemplate.setHashValueSerializer(jackson2JsonRedisSerializer);        redisTemplate.afterPropertiesSet();        return redisTemplate;    }    /**     * 缓存管理     * @param factory Redis连接工厂     * @return CacheManager     */    @Bean    @SuppressWarnings(value = { &quot;unchecked&quot;, &quot;rawtypes&quot; })    public CacheManager cacheManager(RedisConnectionFactory factory) {        RedisSerializer&lt;String&gt; redisSerializer = new StringRedisSerializer();        Jackson2JsonRedisSerializer jackson2JsonRedisSerializer = new Jackson2JsonRedisSerializer(Object.class);        //解决查询缓存转换异常的问题        ObjectMapper om = new ObjectMapper();        om.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.ANY);        om.activateDefaultTyping(LaissezFaireSubTypeValidator.instance ,                ObjectMapper.DefaultTyping.NON_FINAL, JsonTypeInfo.As.PROPERTY);        jackson2JsonRedisSerializer.setObjectMapper(om);        // 配置序列化（解决乱码的问题）,过期时间600秒        RedisCacheConfiguration config =                RedisCacheConfiguration.defaultCacheConfig()                        .entryTtl(Duration.ofSeconds(600))                        .serializeKeysWith(RedisSerializationContext.SerializationPair.fromSerializer(redisSerializer))                        .serializeValuesWith(RedisSerializationContext.SerializationPair.fromSerializer(jackson2JsonRedisSerializer))                        .disableCachingNullValues();        return RedisCacheManager.builder(factory).cacheDefaults(config).build();    }}</code></pre><p>测试。</p><pre><code class="language-Java">@SpringBootTestclass RedisTestApplicationTests {    // 注意要注入的对象是配置类的对象，要是使用了原生的就配置无效了,或者使用resource    @Autowired    @Qualifier(&quot;redisTemplate&quot;)    private RedisTemplate redisTemplate;    @Test    void contextLoads() throws JsonProcessingException {        User user = new User(&quot;yww&quot;, 20);        redisTemplate.opsForValue().set(&quot;user&quot;,json);        System.out.println(redisTemplate.opsForValue().get(&quot;user&quot;));    }}</code></pre><h1>使用注解</h1><p>注解的使用会更加方便的为接口返回的数据添加到缓存里。</p><h2>Cacheable</h2><p>这个注解有几个参数。</p><pre><code class="language-Java">@Target({ElementType.TYPE, ElementType.METHOD})@Retention(RetentionPolicy.RUNTIME)@Inherited@Documentedpublic @interface Cacheable {    // 缓存名（注意这个不是键，类似一个文件夹名）@AliasFor(&quot;cacheNames&quot;)String[] value() default {};@AliasFor(&quot;value&quot;)String[] cacheNames() default {};// 缓存的自定义键String key() default &quot;&quot;;String keyGenerator() default &quot;&quot;;String cacheManager() default &quot;&quot;;String cacheResolver() default &quot;&quot;;// 缓存的条件String condition() default &quot;&quot;;String unless() default &quot;&quot;;boolean sync() default false;}</code></pre><p>首先在这个注解中，<code>value</code>或者是<code>cacheNames</code>这个参数是必须的。</p><pre><code class="language-java">    @Cacheable(&quot;yww&quot;)// 这样就是value的属性    @GetMapping(&quot;/&quot;)    public Result getInfo() {        List&lt;User&gt; list = service.list();        return Result.ok().data(&quot;list&quot;,list);    }</code></pre><p>最经常用的组合。</p><pre><code class="language-Java">    @Cacheable(value = &quot;yww&quot;,key = &quot;111&quot;)    @GetMapping(&quot;/&quot;)    public Result getInfo() {        List&lt;User&gt; list = service.list();        return Result.ok().data(&quot;list&quot;,list);    }</code></pre><p><code>condition</code>这个参数是用来定义缓存的条件的，比如下面这个例子。</p><pre><code class="language-Java">    // 这样表示id大于50的时候取出的数据才会放入缓存@Cacheable(value = &quot;yww&quot;,condition = &quot;#id &gt; 50&quot;)    @GetMapping(&quot;/{id}&quot;)    public Result getInfo(@PathVariable(&quot;id&quot;) int id) {    }</code></pre><h2>CachePut</h2><p>这个注解和<code>Cacheable</code>作用是一样的，参数也是一样的，不一样的是这个注解是只负责将返回的结果存入缓存，再次查询的时候，是不走缓存的。</p><h2>CacheEvict</h2><p>这个注解主要是用来清除缓存的。</p><p>主要使用的参数是<code>allEntries</code>，让它为true会将缓存清空</p><pre><code class="language-Java">// value,key,condition和Cacheable一样@Cacheable(value = &quot;yww&quot;,key = &quot;111&quot;,condition = &quot;#id &gt; 50&quot;,allEntries = true)    public void clear() {    }</code></pre><h1>比较标准的一个配置</h1><h2>依赖</h2><ol><li>redis的官方starter</li><li>fastjson2，用于配置文件中的序列化，当然用其他的也可能可以</li><li>log4j，用于记录日志，当然不用也可以</li><li>hutool，这里用于提供sha256Hex加密</li></ol><pre><code class="language-xml">    &lt;!--  redis  --&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;version&gt;${redis.version}&lt;/version&gt;    &lt;/dependency&gt;    &lt;!--  搭配redis的对象池依赖  --&gt;    &lt;dependency&gt;        &lt;groupId&gt;org.apache.commons&lt;/groupId&gt;        &lt;artifactId&gt;commons-pool2&lt;/artifactId&gt;        &lt;version&gt;${commons-pool2.version}&lt;/version&gt;    &lt;/dependency&gt;    &lt;!--  fastjson2  --&gt;    &lt;dependency&gt;        &lt;groupId&gt;com.alibaba.fastjson2&lt;/groupId&gt;        &lt;artifactId&gt;fastjson2&lt;/artifactId&gt;        &lt;version&gt;${fastjson2.version}&lt;/version&gt;    &lt;/dependency&gt;    &lt;!--  log4j日志  --&gt;    &lt;dependency&gt;        &lt;groupId&gt;org.apache.logging.log4j&lt;/groupId&gt;        &lt;artifactId&gt;log4j-core&lt;/artifactId&gt;        &lt;version&gt;${log4j.version}&lt;/version&gt;    &lt;/dependency&gt;    &lt;!--  hutool  --&gt;    &lt;dependency&gt;        &lt;groupId&gt;cn.hutool&lt;/groupId&gt;        &lt;artifactId&gt;hutool-all&lt;/artifactId&gt;        &lt;version&gt;${hutool.version}&lt;/version&gt;    &lt;/dependency&gt;</code></pre><h2>application.yaml</h2><pre><code class="language-yaml">spring:  # redis配置  redis:    database: 3    host: 127.0.0.1    port: 6379    #连接超时时间（毫秒）    timeout: 5000    lettuce:      pool:        #连接池最大连接数（使用负值表示没有限制）        max-active: 8        #连接池最大阻塞等待时间（使用负值表示没有限制）        max-wait: 3000        #连接池中的最大空闲连接        max-idle: 10        #连接池中的最小空闲连接        min-idle: 2</code></pre><h2>Redis.config</h2><p>借鉴几个开源项目整合的配置文件。</p><pre><code class="language-java">import cn.hutool.core.lang.Assert;import cn.hutool.crypto.digest.DigestUtil;import com.alibaba.fastjson2.JSON;import com.alibaba.fastjson2.JSONReader;import com.alibaba.fastjson2.JSONWriter;import lombok.extern.slf4j.Slf4j;import org.apache.commons.lang3.SerializationException;import org.apache.commons.lang3.StringUtils;import org.springframework.boot.autoconfigure.AutoConfigureBefore;import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;import org.springframework.boot.autoconfigure.data.redis.RedisAutoConfiguration;import org.springframework.cache.Cache;import org.springframework.cache.annotation.CachingConfigurerSupport;import org.springframework.cache.annotation.EnableCaching;import org.springframework.cache.interceptor.CacheErrorHandler;import org.springframework.cache.interceptor.KeyGenerator;import org.springframework.context.annotation.Bean;import org.springframework.context.annotation.Configuration;import org.springframework.data.redis.cache.RedisCacheConfiguration;import org.springframework.data.redis.connection.RedisConnectionFactory;import org.springframework.data.redis.core.RedisTemplate;import org.springframework.data.redis.serializer.RedisSerializationContext;import org.springframework.data.redis.serializer.RedisSerializer;import reactor.util.annotation.Nullable;import java.nio.charset.Charset;import java.nio.charset.StandardCharsets;import java.time.Duration;import java.util.HashMap;import java.util.Map;/** * &lt;p&gt; *      Redis配置类 * &lt;/p&gt; * * @ClassName RedisConfig * @Author yww * @Date 2022/10/12 21:10 */@Slf4j@Configuration@EnableCaching@AutoConfigureBefore(RedisAutoConfiguration.class)public class RedisConfig extends CachingConfigurerSupport {    /**     *  RedisTemplate的配置     */    @Bean(name = &quot;redisTemplate&quot;)    @ConditionalOnMissingBean(name = &quot;redisTemplate&quot;)    @SuppressWarnings(value = { &quot;unchecked&quot;, &quot;rawtypes&quot; })    public RedisTemplate&lt;String, Object&gt; redisTemplate(RedisConnectionFactory connectionFactory) {        RedisTemplate&lt;String, Object&gt; template = new RedisTemplate&lt;&gt;();        template.setConnectionFactory(connectionFactory);        // 使用FastJson2重写序列化        FastJson2JsonRedisSerializer serializer = new FastJson2JsonRedisSerializer(Object.class);        // value值的序列化采用FastJson2重写的序列化        template.setValueSerializer(serializer);        template.setHashValueSerializer(serializer);        // key的序列化采用StringRedisSerializer        template.setKeySerializer(new StringRedisSerializer());        template.setHashKeySerializer(new StringRedisSerializer());        template.afterPropertiesSet();        return template;    }    /**     * 缓存配置     * 设置 redis 数据默认过期时间，默认3小时     */    @Bean    public RedisCacheConfiguration redisCacheConfiguration() {        FastJson2JsonRedisSerializer&lt;Object&gt; fastJsonRedisSerializer = new FastJson2JsonRedisSerializer&lt;&gt;(Object.class);        RedisCacheConfiguration configuration = RedisCacheConfiguration.defaultCacheConfig();        configuration = configuration.serializeValuesWith(RedisSerializationContext.                SerializationPair.fromSerializer(fastJsonRedisSerializer)).entryTtl(Duration.ofHours(3));        return configuration;    }    /**     * 自定义缓存key生成策略，默认将使用该策略     */    @Bean    @Override    public KeyGenerator keyGenerator() {        return (target, method, params) -&gt; {            Map&lt;String, Object&gt; container = new HashMap&lt;&gt;(4);            Class&lt;?&gt; targetClassClass = target.getClass();            // 类地址            container.put(&quot;class&quot;, targetClassClass.toGenericString());            // 方法名称            container.put(&quot;methodName&quot;, method.getName());            // 包名称            container.put(&quot;package&quot;, targetClassClass.getPackage());            // 参数列表            for (int i = 0; i &lt; params.length; i++) {                container.put(String.valueOf(i), params[i]);            }            // 转为JSON字符串            String jsonString = JSON.toJSONString(container);            // 做SHA256 Hash计算，得到一个SHA256摘要作为Key            return DigestUtil.sha256Hex(jsonString);        };    }    /**     *  Redis的异常处理，当Redis发生异常时，打印日志     */    @Bean    @Override    @SuppressWarnings(value = {&quot;all&quot;})    public CacheErrorHandler errorHandler() {        log.info(&quot;初始化 -&gt; [{}]&quot;, &quot;Redis CacheErrorHandler&quot;);        return new CacheErrorHandler() {            @Override            public void handleCacheGetError(RuntimeException e, Cache cache, Object key) {                log.error(&quot;Redis occur handleCacheGetError：key -&gt; [{}]&quot;, key, e);            }            @Override            public void handleCachePutError(RuntimeException e, Cache cache, Object key, Object value) {                log.error(&quot;Redis occur handleCachePutError：key -&gt; [{}]；value -&gt; [{}]&quot;, key, value, e);            }            @Override            public void handleCacheEvictError(RuntimeException e, Cache cache, Object key) {                log.error(&quot;Redis occur handleCacheEvictError：key -&gt; [{}]&quot;, key, e);            }            @Override            public void handleCacheClearError(RuntimeException e, Cache cache) {                log.error(&quot;Redis occur handleCacheClearError：&quot;, e);            }        };    }}/** * Value 序列化 * * @param &lt;T&gt; */class FastJson2JsonRedisSerializer&lt;T&gt; implements RedisSerializer&lt;T&gt; {    public static final Charset DEFAULT_CHARSET = StandardCharsets.UTF_8;    private final Class&lt;T&gt; clazz;    public FastJson2JsonRedisSerializer(Class&lt;T&gt; clazz) {        super();        this.clazz = clazz;    }    @Override    public byte[] serialize(T t) throws SerializationException {        if (t == null) {            return new byte[0];        }        return JSON.toJSONString(t, JSONWriter.Feature.WriteClassName).getBytes(DEFAULT_CHARSET);    }    @Override    public T deserialize(byte[] bytes) throws SerializationException {        if (bytes == null || bytes.length == 0) {            return null;        }        String str = new String(bytes, DEFAULT_CHARSET);        return JSON.parseObject(str, clazz, JSONReader.Feature.SupportAutoType);    }}/** * 重写 String序列化器 */class StringRedisSerializer implements RedisSerializer&lt;Object&gt; {    private final Charset charset;    StringRedisSerializer() {        this(StandardCharsets.UTF_8);    }    private StringRedisSerializer(Charset charset) {        Assert.notNull(charset, &quot;Charset must not be null!&quot;);        this.charset = charset;    }    @Override    public String deserialize(byte[] bytes) {        return (bytes == null ? null : new String(bytes, charset));    }    @Override    public @Nullable    byte[] serialize(Object object) {        String string = JSON.toJSONString(object);        if (StringUtils.isBlank(string)) {            return null;        }        string = string.replace(&quot;\&quot;&quot;, &quot;&quot;);        return string.getBytes(charset);    }}</code></pre><h2>Redis工具类</h2><pre><code class="language-java">import com.alibaba.fastjson2.JSON;import lombok.extern.slf4j.Slf4j;import org.springframework.beans.factory.annotation.Autowired;import org.springframework.data.redis.connection.RedisConnection;import org.springframework.data.redis.connection.RedisConnectionFactory;import org.springframework.data.redis.core.*;import org.springframework.stereotype.Component;import java.util.*;import java.util.concurrent.TimeUnit;/** * &lt;p&gt; *      Redis的工具类 * &lt;/p&gt; * * @ClassName RedisUtil * @Author yww * @Date 2022/10/15 10:40 */@Slf4j@Componentpublic class RedisUtil {    private final StringRedisTemplate stringRedisTemplate;    private final RedisTemplate&lt;String, Object&gt; redisTemplate;    @Autowired    public RedisUtil(StringRedisTemplate stringRedisTemplate, RedisTemplate&lt;String, Object&gt; redisTemplate) {        this.stringRedisTemplate = stringRedisTemplate;        this.redisTemplate = redisTemplate;    }    /**     * 普通缓存     *     * @param key 键     */    public void setStr(String key, String value) {        stringRedisTemplate.opsForValue().set(key, value);    }    /**     * 普通缓存     *     * @param key 键     */    public void setStr(String key, String value, long time) {        stringRedisTemplate.opsForValue().set(key, value, time, TimeUnit.MINUTES);    }    /**     * 普通缓存     *     * @param key 键     */    public void setStr(String key, String value, long time, TimeUnit timeUnit) {        stringRedisTemplate.opsForValue().set(key, value, time, timeUnit);    }    /**     * 普通缓存获取     *     * @param key 键     * @return 值     */    public String getStr(String key) {        return key == null ? null : stringRedisTemplate.opsForValue().get(key);    }    /**     * 普通缓存获取     *     * @param key 键     * @return 值     */    public &lt;T&gt; T getStr(String key, Class&lt;T&gt; clazz) {        return key == null ? null : JSON.parseObject(stringRedisTemplate.opsForValue().get(key), clazz);    }    /**     * 普通缓存删除     *     * @param key 键     */    public void deleteStr(String key) {        stringRedisTemplate.delete(key);    }    /**     * Object缓存     *     * @param key 键     */    public void setObj(String key, Object value) {        redisTemplate.opsForValue().set(key, value);    }    /**     * Object缓存     *     * @param key 键     */    public void setObj(String key, Object value, long time) {        redisTemplate.opsForValue().set(key, value, time, TimeUnit.MINUTES);    }    /**     * Object缓存     *     * @param key 键     */    public void setObj(String key, Object value, long time, TimeUnit timeUnit) {        redisTemplate.opsForValue().set(key, value, time, timeUnit);    }    /**     * Object缓存获取     *     * @param key 键     * @return 值     */    public Object getObj(String key) {        return key == null ? null : redisTemplate.opsForValue().get(key);    }    /**     * Object缓存获取     *     * @param key 键     * @return 值     */    public &lt;T&gt; T getObj(String key, Class&lt;T&gt; clazz) {        return key == null ? null : (T) redisTemplate.opsForValue().get(key);    }    /**     * Object缓存删除     *     * @param key 键     * @return 值     */    public void deleteObj(String key) {        redisTemplate.delete(key);    }    /**     * 查找匹配key     *     * @param pattern key     * @return /     */    public List&lt;String&gt; scan(String pattern) {        ScanOptions options = ScanOptions.scanOptions().match(pattern).build();        RedisConnectionFactory factory = redisTemplate.getConnectionFactory();        RedisConnection rc = Objects.requireNonNull(factory).getConnection();        Cursor&lt;byte[]&gt; cursor = rc.scan(options);        List&lt;String&gt; result = new ArrayList&lt;&gt;();        while (cursor.hasNext()) {            result.add(new String(cursor.next()));        }        try {            RedisConnectionUtils.releaseConnection(rc, factory);        } catch (Exception e) {            e.printStackTrace();        }        return result;    }    /**     * 指定缓存失效时间     *     * @param key  键     * @param time 时间(秒)     */    public boolean expire(String key, long time) {        try {            if (time &gt; 0) {                redisTemplate.expire(key, time, TimeUnit.SECONDS);            }        } catch (Exception e) {            e.printStackTrace();            return false;        }        return true;    }    /**     * 指定缓存失效时间     *     * @param key      键     * @param time     时间(秒)     * @param timeUnit 单位     */    public boolean expire(String key, long time, TimeUnit timeUnit) {        try {            if (time &gt; 0) {                redisTemplate.expire(key, time, timeUnit);            }        } catch (Exception e) {            e.printStackTrace();            return false;        }        return true;    }    /**     * 根据 key 获取过期时间     *     * @param key 键 不能为null     * @return 时间(秒) 返回0代表为永久有效     */    public long getExpire(String key) {        return redisTemplate.getExpire(key, TimeUnit.SECONDS);    }    /**     * 分页查询 key     *     * @param patternKey key     * @param page       页码     * @param size       每页数目     * @return /     */    public List&lt;String&gt; findKeysForPage(String patternKey, int page, int size) {        ScanOptions options = ScanOptions.scanOptions().match(patternKey).build();        RedisConnectionFactory factory = redisTemplate.getConnectionFactory();        RedisConnection rc = Objects.requireNonNull(factory).getConnection();        Cursor&lt;byte[]&gt; cursor = rc.scan(options);        List&lt;String&gt; result = new ArrayList&lt;&gt;(size);        int tmpIndex = 0;        int fromIndex = page * size;        int toIndex = page * size + size;        while (cursor.hasNext()) {            if (tmpIndex &gt;= fromIndex &amp;&amp; tmpIndex &lt; toIndex) {                result.add(new String(cursor.next()));                tmpIndex++;                continue;            }            // 获取到满足条件的数据后,就可以退出了            if (tmpIndex &gt;= toIndex) {                break;            }            tmpIndex++;            cursor.next();        }        try {            RedisConnectionUtils.releaseConnection(rc, factory);        } catch (Exception e) {            e.printStackTrace();        }        return result;    }    /**     * 判断key是否存在     *     * @param key 键     * @return true 存在 false不存在     */    public boolean hasKey(String key) {        try {            return redisTemplate.hasKey(key);        } catch (Exception e) {            e.printStackTrace();            return false;        }    }    /**     * 删除缓存     *     * @param keys 可以传一个值 或多个     */    public void del(String... keys) {        if (keys != null &amp;&amp; keys.length &gt; 0) {            if (keys.length == 1) {                boolean result = redisTemplate.delete(keys[0]);                log.debug(&quot;--------------------------------------------&quot;);                log.debug(&quot;删除缓存：{}，结果：{}&quot;, keys[0], result);                log.debug(&quot;--------------------------------------------&quot;);            } else {                Set&lt;String&gt; keySet = new HashSet&lt;&gt;();                for (String key : keys) {                    keySet.addAll(redisTemplate.keys(key));                }                long count = redisTemplate.delete(keySet);                log.debug(&quot;--------------------------------------------&quot;);                log.debug(&quot;成功删除缓存：{}&quot;, keySet.toString());                log.debug(&quot;缓存删除数量：{}个&quot;, count);                log.debug(&quot;--------------------------------------------&quot;);            }        }    }    // ============================String=============================    /**     * 普通缓存获取     *     * @param key 键     * @return 值     */    public Object get(String key) {        return key == null ? null : redisTemplate.opsForValue().get(key);    }    /**     * 批量获取     *     * @param keys     * @return     */    public List&lt;Object&gt; multiGet(List&lt;String&gt; keys) {        return redisTemplate.opsForValue().multiGet(keys);    }    /**     * 普通缓存放入     *     * @param key   键     * @param value 值     * @return true成功 false失败     */    public boolean set(String key, Object value) {        try {            redisTemplate.opsForValue().set(key, value);            return true;        } catch (Exception e) {            e.printStackTrace();            return false;        }    }    /**     * 普通缓存放入并设置时间     *     * @param key   键     * @param value 值     * @param time  时间(秒) time要大于0 如果time小于等于0 将设置无限期     * @return true成功 false 失败     */    public boolean set(String key, Object value, long time) {        try {            if (time &gt; 0) {                redisTemplate.opsForValue().set(key, value, time, TimeUnit.SECONDS);            } else {                set(key, value);            }            return true;        } catch (Exception e) {            e.printStackTrace();            return false;        }    }    /**     * 普通缓存放入并设置时间     *     * @param key      键     * @param value    值     * @param time     时间     * @param timeUnit 类型     * @return true成功 false 失败     */    public boolean set(String key, Object value, long time, TimeUnit timeUnit) {        try {            if (time &gt; 0) {                redisTemplate.opsForValue().set(key, value, time, timeUnit);            } else {                set(key, value);            }            return true;        } catch (Exception e) {            e.printStackTrace();            return false;        }    }    // ================================Map=================================    /**     * HashGet     *     * @param key  键 不能为null     * @param item 项 不能为null     * @return 值     */    public Object hget(String key, String item) {        return redisTemplate.opsForHash().get(key, item);    }    /**     * 获取hashKey对应的所有键值     *     * @param key 键     * @return 对应的多个键值     */    public Map&lt;Object, Object&gt; hmget(String key) {        return redisTemplate.opsForHash().entries(key);    }    /**     * HashSet     *     * @param key 键     * @param map 对应多个键值     * @return true 成功 false 失败     */    public boolean hmset(String key, Map&lt;String, Object&gt; map) {        try {            redisTemplate.opsForHash().putAll(key, map);            return true;        } catch (Exception e) {            e.printStackTrace();            return false;        }    }    /**     * HashSet 并设置时间     *     * @param key  键     * @param map  对应多个键值     * @param time 时间(秒)     * @return true成功 false失败     */    public boolean hmset(String key, Map&lt;String, Object&gt; map, long time) {        try {            redisTemplate.opsForHash().putAll(key, map);            if (time &gt; 0) {                expire(key, time);            }            return true;        } catch (Exception e) {            e.printStackTrace();            return false;        }    }    /**     * 向一张hash表中放入数据,如果不存在将创建     *     * @param key   键     * @param item  项     * @param value 值     * @return true 成功 false失败     */    public boolean hset(String key, String item, Object value) {        try {            redisTemplate.opsForHash().put(key, item, value);            return true;        } catch (Exception e) {            e.printStackTrace();            return false;        }    }    /**     * 向一张hash表中放入数据,如果不存在将创建     *     * @param key   键     * @param item  项     * @param value 值     * @param time  时间(秒) 注意:如果已存在的hash表有时间,这里将会替换原有的时间     * @return true 成功 false失败     */    public boolean hset(String key, String item, Object value, long time) {        try {            redisTemplate.opsForHash().put(key, item, value);            if (time &gt; 0) {                expire(key, time);            }            return true;        } catch (Exception e) {            e.printStackTrace();            return false;        }    }    /**     * 删除hash表中的值     *     * @param key  键 不能为null     * @param item 项 可以使多个 不能为null     */    public void hdel(String key, Object... item) {        redisTemplate.opsForHash().delete(key, item);    }    /**     * 判断hash表中是否有该项的值     *     * @param key  键 不能为null     * @param item 项 不能为null     * @return true 存在 false不存在     */    public boolean hHasKey(String key, String item) {        return redisTemplate.opsForHash().hasKey(key, item);    }    /**     * hash递增 如果不存在,就会创建一个 并把新增后的值返回     *     * @param key  键     * @param item 项     * @param by   要增加几(大于0)     * @return     */    public double hincr(String key, String item, double by) {        return redisTemplate.opsForHash().increment(key, item, by);    }    /**     * hash递减     *     * @param key  键     * @param item 项     * @param by   要减少记(小于0)     * @return     */    public double hdecr(String key, String item, double by) {        return redisTemplate.opsForHash().increment(key, item, -by);    }    // ============================set=============================    /**     * 根据key获取Set中的所有值     *     * @param key 键     * @return     */    public Set&lt;Object&gt; sGet(String key) {        try {            return redisTemplate.opsForSet().members(key);        } catch (Exception e) {            e.printStackTrace();            return null;        }    }    /**     * 根据value从一个set中查询,是否存在     *     * @param key   键     * @param value 值     * @return true 存在 false不存在     */    public boolean sHasKey(String key, Object value) {        try {            return redisTemplate.opsForSet().isMember(key, value);        } catch (Exception e) {            e.printStackTrace();            return false;        }    }    /**     * 将数据放入set缓存     *     * @param key    键     * @param values 值 可以是多个     * @return 成功个数     */    public long sSet(String key, Object... values) {        try {            return redisTemplate.opsForSet().add(key, values);        } catch (Exception e) {            e.printStackTrace();            return 0;        }    }    /**     * 将set数据放入缓存     *     * @param key    键     * @param time   时间(秒)     * @param values 值 可以是多个     * @return 成功个数     */    public long sSetAndTime(String key, long time, Object... values) {        try {            Long count = redisTemplate.opsForSet().add(key, values);            if (time &gt; 0) {                expire(key, time);            }            return count;        } catch (Exception e) {            e.printStackTrace();            return 0;        }    }    /**     * 获取set缓存的长度     *     * @param key 键     * @return     */    public long sGetSetSize(String key) {        try {            return redisTemplate.opsForSet().size(key);        } catch (Exception e) {            e.printStackTrace();            return 0;        }    }    /**     * 移除值为value的     *     * @param key    键     * @param values 值 可以是多个     * @return 移除的个数     */    public long setRemove(String key, Object... values) {        try {            Long count = redisTemplate.opsForSet().remove(key, values);            return count;        } catch (Exception e) {            e.printStackTrace();            return 0;        }    }    // ===============================list=================================    /**     * 获取list缓存的内容     *     * @param key   键     * @param start 开始     * @param end   结束 0 到 -1代表所有值     * @return     */    public List&lt;Object&gt; lGet(String key, long start, long end) {        try {            return redisTemplate.opsForList().range(key, start, end);        } catch (Exception e) {            e.printStackTrace();            return null;        }    }    /**     * 获取list缓存的长度     *     * @param key 键     * @return     */    public long lGetListSize(String key) {        try {            return redisTemplate.opsForList().size(key);        } catch (Exception e) {            e.printStackTrace();            return 0;        }    }    /**     * 通过索引 获取list中的值     *     * @param key   键     * @param index 索引 index&gt;=0时， 0 表头，1 第二个元素，依次类推；index&lt;0时，-1，表尾，-2倒数第二个元素，依次类推     * @return     */    public Object lGetIndex(String key, long index) {        try {            return redisTemplate.opsForList().index(key, index);        } catch (Exception e) {            e.printStackTrace();            return null;        }    }    /**     * 将list放入缓存     *     * @param key   键     * @param value 值     * @return     */    public boolean lSet(String key, Object value) {        try {            redisTemplate.opsForList().rightPush(key, value);            return true;        } catch (Exception e) {            e.printStackTrace();            return false;        }    }    /**     * 将list放入缓存     *     * @param key   键     * @param value 值     * @param time  时间(秒)     * @return     */    public boolean lSet(String key, Object value, long time) {        try {            redisTemplate.opsForList().rightPush(key, value);            if (time &gt; 0) {                expire(key, time);            }            return true;        } catch (Exception e) {            e.printStackTrace();            return false;        }    }    /**     * 将list放入缓存     *     * @param key   键     * @param value 值     * @return     */    public boolean lSet(String key, List&lt;Object&gt; value) {        try {            redisTemplate.opsForList().rightPushAll(key, value);            return true;        } catch (Exception e) {            e.printStackTrace();            return false;        }    }    /**     * 将list放入缓存     *     * @param key   键     * @param value 值     * @param time  时间(秒)     * @return     */    public boolean lSet(String key, List&lt;Object&gt; value, long time) {        try {            redisTemplate.opsForList().rightPushAll(key, value);            if (time &gt; 0) {                expire(key, time);            }            return true;        } catch (Exception e) {            e.printStackTrace();            return false;        }    }    /**     * 根据索引修改list中的某条数据     *     * @param key   键     * @param index 索引     * @param value 值     * @return /     */    public boolean lUpdateIndex(String key, long index, Object value) {        try {            redisTemplate.opsForList().set(key, index, value);            return true;        } catch (Exception e) {            e.printStackTrace();            return false;        }    }    /**     * 移除N个值为value     *     * @param key   键     * @param count 移除多少个     * @param value 值     * @return 移除的个数     */    public long lRemove(String key, long count, Object value) {        try {            return redisTemplate.opsForList().remove(key, count, value);        } catch (Exception e) {            e.printStackTrace();            return 0;        }    }    /**     * @param prefix 前缀     * @param ids    id     */    public void delByKeys(String prefix, Set&lt;Long&gt; ids) {        Set&lt;String&gt; keys = new HashSet&lt;&gt;();        for (Long id : ids) {            keys.addAll(redisTemplate.keys(new StringBuffer(prefix).append(id).toString()));        }        long count = redisTemplate.delete(keys);        // 此处提示可自行删除        log.debug(&quot;--------------------------------------------&quot;);        log.debug(&quot;成功删除缓存：{}&quot;, keys.toString());        log.debug(&quot;缓存删除数量：{}个&quot;, count);        log.debug(&quot;--------------------------------------------&quot;);    }}</code></pre>]]></content:encoded>
            <category domain="https://yww52.com/categories/%25E6%2588%2591%25E7%259A%2584%25E6%258A%2580%25E6%259C%25AF%25E5%25AD%25A6%25E4%25B9%25A0%25E7%2589%25A9%25E8%25AF%25AD%25E6%259E%259C%25E7%2584%25B6%25E6%259C%2589%25E9%2597%25AE%25E9%25A2%2598/">我的技术学习物语果然有问题</category>
        </item>
        <item>
            <title>剑指Offer</title>
            <link>https://yww52.com/posts/dd921d98/</link>
            <guid isPermaLink="true">https://yww52.com/posts/dd921d98/</guid>
            <pubDate>Fri, 19 Mar 2021 00:00:00 +0000</pubDate>
            <description>剑指Offer上面的题目，记录一下解题的过程</description>
            <content:encoded><![CDATA[<img src="https://img.yww52.com/2021/3/2021-3-19top_img.jpg" alt="剑指Offer" style="max-width:100%;height:auto;margin-bottom:15px;" /><h1>3.数组中重复的数字</h1><h2>排序遍历</h2><p>可以直接将数组排序，然后进行循环遍历，就可以找到数组中重复的数字了。</p><pre><code class="language-Java">class Solution {    public int findRepeatNumber(int[] nums) {        Arrays.sort(nums);        for (int i = 1;i &lt; nums.length; i++) {            if (nums[i] == nums[i - 1]) {                return nums[i];            }        }        return 0;    }}</code></pre><h2>Set去重</h2><p>可以将该数组丢到Set集合里去重，若是添加失败就代表该数字重复，直接返回就好了。</p><pre><code class="language-Java">class Solution {    public int findRepeatNumber(int[] nums) {        Set&lt;Integer&gt; set = new HashSet&lt;&gt;();        for (int i : nums) {            if (!set.add(i)) {                return i;            }        }        return 0;    }}</code></pre><h2>哈希表</h2><p>跟Set差不多，添加到哈希表之前，先判断一下是否有该键，然后要是有就直接返回。</p><pre><code class="language-Java">class Solution {    public int findRepeatNumber(int[] nums) {        HashMap&lt;Integer,Integer&gt; map = new HashMap&lt;&gt;();        for (int i : nums) {            if (map.containsKey(i)) {                return i;            }            map.put(i,i);        }        return 0;    }}</code></pre><h2>原地置换（类似桶排序）</h2><p>遍历数组，将每一个数放在与它等值的下标下，因为题目说了数字都在1~n - 1，所以当该下标出现了与它一样的数字，代表这个数字是重复的。</p><pre><code class="language-Java">class Solution {    public int findRepeatNumber(int[] nums) {        for (int i = 0; i &lt; nums.length; i++) {           while(nums[i] != i) {                if(nums[i] == nums[nums[i]]) return nums[i];                int temp = nums[i];                nums[i] = nums[temp];                nums[temp] = temp;           }        }        return 0;    }}</code></pre><h2>衍生题</h2><p>题目详情可以参考<a href="https://www.acwing.com/problem/content/description/15/">https://www.acwing.com/problem/content/description/15/</a>。</p><p>这里可以使用上述的哈希和Set集合来进行查找，或者在创建一个数组，然后使用桶排序找出重复的元素。</p><p>因为必定会有一个重复的，可以理解为数组中间会出现一个环，所以可以当成寻找链表的环。</p><pre><code class="language-Java">class Solution {    public int duplicateInArray(int[] nums) {        // 快慢指针寻找相遇点        int fast = 0；        int slow = 0;        while (fast == 0 || fast != slow) {            fast = nums[nums[fast]];            slow = nums[slow];        }        // 寻找入环点        fast = 0;        while (fast != slow) {            fast = nums[fast];            slow = nums[slow];        }        return slow;    }}</code></pre><p>还有就是利用分治的思想来解。类似二分查找的思想。</p><p>利用数值将数组分成两半，如果一边坑的个数小于数的个数，代表重复的数字就在那半边，所以缩小区间，直到找到。</p><pre><code class="language-Java">class Solution {    public int duplicateInArray(int[] nums) {        int l = 1;        int r = nums.length - 1;        while (l &lt; r) {            int mid = l + r &gt;&gt; 1;                // 记录小于中间值的数的个数            int count = 0;              for(int i : nums) {                if(l &lt;= i &amp;&amp; i &lt;= mid) count++;            }            // 个数大于坑数就进入左区间，直到找到重复的数字            if(count &gt; mid - l + 1) r = mid;            else l = mid + 1;        }        return l;    }}</code></pre><h1>4.二维数组中的查找</h1><h2>数组遍历</h2><p>直接将这个二维数组遍历，暴力破解。</p><pre><code class="language-Java">class Solution {    public boolean findNumberIn2DArray(int[][] matrix, int target) {        for (int[] i : matrix) {            for (int j : i) {                if (j == target) return true;            }        }        return false;    }}</code></pre><h2>二维查找</h2><p>根据递增的单调性，可以从最右上角一个个进行排除，寻找对应的值。可以从右下角向左下角进行遍历（反过来也可以）。</p><pre><code class="language-Java">class Solution {    public boolean findNumberIn2DArray(int[][] matrix, int target) {        if (matrix.length == 0 || matrix[0].length == 0) return false;        int i = 0;        // 从最后一列开始，排除最后一列小于给定值的行        int j = matrix[0].length - 1;        while(i &lt; matrix.length &amp;&amp; j &gt;= 0) {            if (matrix[i][j] == target) return true;            // 如果值大于给定值，代表当前行不存在给定值，所以遍历下一行            if (matrix[i][j] &gt; target) j--;            else i++;        }        return false;    }}</code></pre><h1>5.替换空格</h1><h2>replaceAll</h2><p>String类里就自带一个替换的方法了，所以可以直接进行替换。</p><pre><code class="language-Java">class Solution {    public String replaceSpace(String s) {        return s.replaceAll(&quot; &quot;,&quot;%20&quot;);    }}</code></pre><h2>遍历字符串</h2><p>直接遍历字符串，出现了空格就替换，不然就不替换。</p><pre><code class="language-Java">class Solution {    public String replaceSpace(String s) {        StringBuilder res = new StringBuilder();        char[] str = s.toCharArray();        for (char c : str) {            if (c == &#39; &#39;) res.append(&quot;%20&quot;);            else res.append(c);        }        return res.toString();    }}</code></pre><p>要是不能用动态的增加字符串，就只能先遍历一遍字符串，然后判断答案的长度，然后在创建一个新的数组，然后依次放入新数组就好了。</p><h1>6. 从尾到头打印链表</h1><h2>反转链表后输出</h2><p>这里最简单的解法就是将链表反转，然后在打印链表了。</p><pre><code class="language-Java">class Solution {    public int[] reversePrint(ListNode head) {        if (head == null) return new int[0];        ListNode newhead = reverse(head);        List&lt;Integer&gt; list = new ArrayList&lt;&gt;();        while(newhead != null) {            list.add(newhead.val);            newhead = newhead.next;        }        int[] res = new int[list.size()];        for (int i = 0; i&lt; list.size(); i++) {            res[i] = list.get(i);        }        return res;    }    public ListNode reverse(ListNode head) {        if (head.next == null) return head;        ListNode newhead = reverse(head.next);        head.next.next = head;        head.next = null;        return newhead;    }}</code></pre><h2>递归遍历</h2><p>这里可以使用类似后序遍历的操作，先递归进去然后添加值，这样就类似栈了，直接从后到前输出。或者这里定义一个栈也可以进行。</p><pre><code class="language-Java">class Solution {    List&lt;Integer&gt; list = new ArrayList&lt;&gt;();    public int[] reversePrint(ListNode head) {        if (head == null) return new int[0];        reverse(head);        int[] res = new int[list.size()];        for (int i = 0; i&lt; list.size(); i++) {            res[i] = list.get(i);        }        return res;    }    public void reverse(ListNode head) {        if (head == null) return;        reverse(head.next);        list.add(head.val);    }}</code></pre><h2>反向输出</h2><p>其实可以看到，其实从前遍历，然后反向输出就行了，其实并不用一定是正向的数据。</p><pre><code class="language-Java">class Solution {        public int[] reversePrint(ListNode head) {        if (head == null) return new int[0];        List&lt;Integer&gt; list = new ArrayList&lt;&gt;();        while (head != null) {            list.add(head.val);            head = head.next;        }        int[] res = new int[list.size()];        for (int i = 0; i&lt; list.size(); i++) {            res[i] = list.get(list.size() - i - 1);        }        return res;    }}</code></pre><h2>两次遍历</h2><p>第一次遍历链表，判断数组的个数，然后从后往前赋值就好了。之前的解法也可以改为先进行一次遍历后再赋值，这样就不用使用list集合了，效率太低。</p><pre><code class="language-Java">class Solution {        public int[] reversePrint(ListNode head) {        if (head == null) return new int[0];        ListNode node = head;        int size = 0;        while(node != null) {            size++;            node = node.next;        }        int[] res = new int[size];        for (int i = size - 1; i &gt; -1;i--) {             res[i] = head.val;             head = head.next;        }        return res;    }}</code></pre><h2>利用栈的先进后出</h2><pre><code class="language-Java">class Solution {    public int[] reversePrint(ListNode head) {        if (head == null) return new int[0];        Stack&lt;ListNode&gt; stack = new Stack&lt;&gt;();        ListNode cur = head;        while (cur != null) {            stack.push(cur);            cur = cur.next;        }        int[] res = new int[stack.size()];        int i = 0;        while (!stack.isEmpty()) {            res[i++] = stack.pop().val;        }        return res;    }}</code></pre><h1>7.重建二叉树</h1><p>这题主要是要划分区间，划分好左子树和右子树的区间。</p><p>前序遍历的区间可以分为三个。根的值，左子树的值，右子树的值。</p><p>中序遍历的区间可以分为三个。左子树的值，根的值，右子树的值。</p><p>所以可以用前序遍历的第一个值创建节点，然后遍历前序遍历的数组，用中序遍历的数组来判断是否达到根的值。</p><pre><code class="language-Java">class Solution {    private int ino = 0;    private int pre = 0;    public TreeNode buildTree(int[] preorder, int[] inorder) {        return build(preorder, inorder, Integer.MIN_VALUE - 1);    }    private TreeNode build(int[] preorder, int[] inorder, long stop) {        if (pre &gt;= preorder.length) return null;        // 判断该子树是否碰到根        if (inorder[ino] == stop) {            ino++;            return null;        }        TreeNode node = new TreeNode(preorder[pre++]);        // 左子树的范围，到当前节点值，即到子树的根值(按前序遍历的数组)        node.left = build(preorder,inorder,node.val);        // 右子树的范围，从当前节点到最后        node.right = build(preorder,inorder,stop);        return node;    }}</code></pre><p>上面是通过前序遍历和中序遍历去创建一颗二叉树，接下来是通过中序遍历和后序遍历来创建一个二叉树，其实差不多。</p><pre><code class="language-Java">class Solution {         private int ino = 0;    private int pos = 0;        public TreeNode buildTree(int[] inorder, int[] postorder) {        pos = postorder.length - 1;        ino = inorder.length - 1;        return build(inorder,postorder,Integer.MIN_VALUE - 1);    }        public TreeNode build(int[] inorder, int[] postorder,long stop) {        if (pos == -1) return null;        if (inorder[ino] == stop) {            ino--;            return null;        }        TreeNode node = new TreeNode(postorder[pos--]);        node.right = build(inorder,postorder,node.val);        node.left = build(inorder,postorder,stop);        return node;    }    }</code></pre><h1>8.二叉树的下一个节点</h1><p>题目详情可以参考<a href="https://www.acwing.com/problem/content/31/">https://www.acwing.com/problem/content/31/</a></p><p>这里就是直接找下一个节点就好了。</p><p>于是会有两种情况。</p><ol><li>当前节点有右子树。那么该节点的下一个节点就是右子树里的最左子节点。</li><li>当前节点没有右子树，那就要追溯回原来第一个是其father左儿子的节点。<ul><li>该节点是父节点的左孩子，这样下一个节点就是它的father</li><li>该节点是父孩子的右孩子，这样下一个节点就是该左子树的父节点</li></ul></li></ol><pre><code class="language-java">class Solution {    public TreeNode inorderSuccessor(TreeNode p) {        // 存在右子树的情况        if (p.right != null) {            p = p.right;            while(p.left != null) p = p.left;            return p;        }        // 不存在右子树的情况        while(p.father != null &amp;&amp; p == p.father.right) p = p.father;                return p.father;    }}</code></pre><h1>9.用两个栈实现队列</h1><p>这里先来分析一下这两个栈的作用。栈是先进后出的数据结构，而队列是先进先出的，所以无法进行互通，引入第二个栈是用来存储第一个栈中出栈的元素，这样第二个栈的元素就是第一个栈元素的倒序了，直接出栈就是最先进来的元素了。</p><p>入队就直接压入第一个栈中就可以了。</p><p>主要是要探讨出队的情况。</p><ol><li>当第二个栈中还有元素的时候，代表这些元素是最先进入的元素，所以直接出栈就是之前最先入队的元素了。</li><li>当第二个栈和第一个栈都没有元素了，那就证明队列为空。</li><li>当第二个栈没有元素，第一个栈有元素，那就直接将第一个栈的元素全部出栈压入第二栈，实现倒序。</li></ol><pre><code class="language-Java">class CQueue {    Stack&lt;Integer&gt; stack1;    Stack&lt;Integer&gt; stack2;    public CQueue() {        stack1 = new Stack&lt;&gt;();        stack2 = new Stack&lt;&gt;();    }        public void appendTail(int value) {        stack1.push(value);    }        public int deleteHead() {        if (!stack2.isEmpty()) return stack2.pop();        if (stack1.isEmpty()) return -1;        while (!stack1.isEmpty()) {            stack2.push(stack1.pop());        }        return stack2.pop();    }    }/** * Your CQueue object will be instantiated and called as such: * CQueue obj = new CQueue(); * obj.appendTail(value); * int param_2 = obj.deleteHead(); */</code></pre><h1>10.斐波那契数列</h1><p>剑指Offer这题的斐波那契数列是结果取模的，因为返回的是int，除了这个需要注意的，其他的和经常的解法差不多了。</p><h2>递归</h2><p>可以考虑直接递归，但一般都会超时。</p><pre><code class="language-Java">class Solution {    public int fib(int n) {        if (n &lt; 0) return 0;        if (n == 1 || n == 2) return 1;        return (fib(n - 1) + fib(n - 2)) % 1000000007;    }}</code></pre><h2>动态规划</h2><pre><code class="language-Java">class Solution {    public int fib(int n) {        if (n == 0 || n == 1) return n;        int[] dp = new int[n + 1];        dp[1] = 1;        for (int i = 2; i &lt;= n; i++) {            dp[i] = dp[i - 1] + dp[i - 2];            if (dp[i] &gt;= 1000000007) dp[i] -= 1000000007;        }        return dp[n];    }}</code></pre><h2>迭代解法</h2><p>这里是可以考虑dp数组的迭代解法的，但是这里只是一次性达到答案，不会回头了，所以记录之前的状态并没有什么意思，所以可以创建临时变量保存需要的临时变量就可以了。</p><pre><code class="language-Java">class Solution {    public int fib(int n) {        if (n == 0 || n == 1) return n;        int a = 0;        int b = 1;        for (int i = 2; i &lt;= n; i++) {            int temp = b;            b = a + b;            a = temp;            if (b &gt;= 1000000007) b -= 1000000007;        }        return b;    }}</code></pre><h2>青蛙跳台阶</h2><pre><code class="language-Java">class Solution {    public int numWays(int n) {        if (n &lt;= 1) return 1;        if (n == 2) return 2;        int a = 1;        int b = 1;        for (int i = 2;i &lt;= n;i++) {            int c = (a + b) % 1000000007;            a = b;            b = c;        }        return b;    }}</code></pre><h1>11.旋转数组的最小数字</h1><h2>排序</h2><p>可以直接排序在取第一个。</p><pre><code class="language-Java">class Solution {    public int minArray(int[] numbers) {        Arrays.sort(numbers);        return numbers[0];    }}</code></pre><h2>不递增</h2><p>找到第一个不递增的数字那就是最小的数字了。</p><pre><code class="language-Java">class Solution {    public int minArray(int[] numbers) {        for (int i = 0;i &lt; numbers.length - 1; i++) {            if (numbers[i] &gt; numbers[i + 1]) return numbers[i + 1];        }        return numbers[0];    }}</code></pre><h2>二分查找</h2><p>二分查找最主要的就是划分区间，需要先分析一下怎样确定好区间。</p><ol><li>中间的数大于最右边的数，代表期间不会单调递增，直接进入右边的区间</li><li>中间的数小于最右边的数，代表期间是单调递增的，直接进入左边的区间</li><li>要是中间的数与最右边的数相等，那就缩进一下区间</li></ol><pre><code class="language-Java">class Solution {    public int minArray(int[] numbers) {        int l = 0;        int r = numbers.length - 1;        while (l &lt; r) {            int mid = l + r &gt;&gt; 1;            if (numbers[mid] &gt; numbers[r]) {                l = mid + 1;            } else if (numbers[mid] &lt; numbers[r]) {                r = mid;            } else {                r -= 1;            }        }        return numbers[l];    }}</code></pre><h1>12.矩阵中的路径</h1><p>经典的dfs和回溯。</p><pre><code class="language-Java">class Solution {    public boolean exist(char[][] board, String word) {        if (board == null) return false;        int row = board.length;        int col = board[0].length;        // 标记是否经过的数组        boolean[][] isVisited = new boolean[row][col];        for (int i = 0; i &lt; row; i++) {            for (int j = 0; j &lt; col; j++) {                // 遍历每个点，确定dfs的起点                if (board[i][j] == word.charAt(0) &amp;&amp; dfs(board,word,0,i,j,isVisited)) return true;            }        }        return false;    }    public boolean dfs(char[][] board,String word,int n,int x,int y,boolean[][] isVisited){        if (board[x][y] != word.charAt(n)) return false;        if (n == word.length() - 1) return true;        // 标记经过        isVisited[x][y] = true;        // 偏移数组，上下左右        int[] dx = {0,0,-1,1};        int[] dy = {-1,1,0,0};        // 按上下左右开始偏移        for (int i = 0; i &lt; 4; i++) {            int lx = x + dx[i];            int ly = y + dy[i];            // 保证下一步没有越界而且没有经过            if (lx &gt;= 0 &amp;&amp; lx &lt; board.length &amp;&amp; ly &gt;= 0 &amp;&amp; ly &lt; board[0].length &amp;&amp; !isVisited[lx][ly]) {                if (dfs(board,word,n + 1,lx,ly,isVisited)) return true;            }        }        // 取消标记，回溯        isVisited[x][y] = false;        return false;    }}</code></pre><h1>13.机器人的运动范围</h1><h2>dfs</h2><pre><code class="language-Java">class Solution {    public int movingCount(int m, int n, int k) {        if (m == 0 || n == 0) return 0;        boolean[][] isVisited = new boolean[m][n];        return dfs(0,0,k,isVisited);    }    public int dfs(int i,int j,int k,boolean[][] isVisited) {        if (i &lt; 0 || i &gt;= isVisited.length || j &lt; 0 || j &gt;= isVisited[0].length || isVisited[i][j] || !isAllowed(i,j,k)) return 0;        isVisited[i][j] = true;        // 上下左右        return dfs(i-1,j,k,isVisited) + dfs(i+1,j,k,isVisited) + dfs(i,j-1,k,isVisited) + dfs(i,j+1,k,isVisited) + 1;    }// 判断是否合法    public boolean isAllowed(int m, int n, int k) {        int sum = 0;        while (m != 0) {            sum += m % 10;            m = m / 10;        }        while (n != 0) {            sum += n % 10;            n = n / 10;        }        if (sum &lt;= k) return true;        else return false;    }}</code></pre><p>看看别人的发现判断是否合法不用这么麻烦，还可以优化一下，所以变成下面这样。</p><pre><code class="language-Java">class Solution {    public int movingCount(int m, int n, int k) {        if (m == 0 || n == 0) return 0;        boolean[][] isVisited = new boolean[m][n];        return dfs(0,0,k,isVisited);    }    public int dfs(int i,int j,int k,boolean[][] isVisited) {        if (i &lt; 0 || i &gt;= isVisited.length || j &lt; 0 || j &gt;= isVisited[0].length || isVisited[i][j] || (i/10+i%10+j/10+j%10)&gt;k) return 0;        isVisited[i][j] = true;        return dfs(i-1,j,k,isVisited) + dfs(i+1,j,k,isVisited) + dfs(i,j-1,k,isVisited) + dfs(i,j+1,k,isVisited) + 1;    }}</code></pre><p>从0,0开始，其实只用向右和向下就可以走完全部的格子了，所以可以去掉向左和向上的dfs。</p><pre><code class="language-Java">class Solution {    public int movingCount(int m, int n, int k) {        if (m == 0 || n == 0) return 0;        boolean[][] isVisited = new boolean[m][n];        return dfs(0,0,k,isVisited);    }    public int dfs(int i,int j,int k,boolean[][] isVisited) {        if (i &gt;= isVisited.length || j &gt;= isVisited[0].length || isVisited[i][j] || (i/10+i%10+j/10+j%10)&gt;k) return 0;        isVisited[i][j] = true;        return dfs(i+1,j,k,isVisited) + dfs(i,j+1,k,isVisited) + 1;    }}</code></pre><h1>14.1 剪绳子</h1><h2>动态规划</h2><p>这一题刚开始给我的想法是使用动态规划去解。</p><pre><code class="language-Java">class Solution {    public int cuttingRope(int n) {        if (n == 1 || n == 2) return 1;        if (n == 3) return 2;        int[] dp = new int[n + 1];        dp[1] = 1;        dp[2] = 2;        dp[3] = 3;        for (int i = 4; i &lt;= n; i++) {            int max = 0;            // j &gt;i / 2就会出现重复了，所以就不需要那部分的数据比较了            for (int j = 1; j &lt;= i / 2; j++) {                max = Math.max(max,dp[j] * dp[i - j]);            }            dp[i] = max;        }        return dp[n];    }}</code></pre><h2>贪婪算法</h2><p>搬用题解关于该题的解释。</p><ol><li>任何大于1的数都可由2和3相加组成。</li><li>因为都能有2和3组成，所以当3的数量越多，乘积越大。当剩余的绳子长度大于等于5，尽可能剪长度为3的绳子。</li><li>有一个特例。当剩余的绳子长度等于4，就将绳子剪为两段2。</li></ol><pre><code class="language-Java">class Solution {    public int cuttingRope(int n) {        if (n == 1 || n == 2) return 1;        if (n == 3) return 2;        int res = 1;        while (n &gt; 4) {            res *= 3;            n -= 3;        }        return res * n;    }}</code></pre><h1>14.2 剪绳子2</h1><p>这题将数据量变大了很多，使用动态规划就会很麻烦，所以用刚刚的贪婪算法，修改一下。</p><pre><code class="language-Java">class Solution {    public int cuttingRope(int n) {        if (n == 1 || n == 2) return 1;        if (n == 3) return 2;        long res = 1;        while (n &gt; 4) {            res *= 3;            res %= 1000000007;            n -= 3;        }        return (int)(res * n % 1000000007);    }}</code></pre><h1>15.二进制中1的个数</h1><h2>用方法</h2><p>有个方法是可以算出二进制中1的个数。</p><pre><code class="language-Java">public class Solution {    public int hammingWeight(int n) {        return Integer.bitCount(n);    }}</code></pre><h2>位运算</h2><p>通过数字与1的与运算，来判断最后一个二进制的数是不是1，是的话与运算会返回1，就这样逐个判断二进制1的个数。</p><pre><code class="language-Java">public class Solution {    public int hammingWeight(int n) {        int res = 0;        while(n != 0) {            res += n &amp; 1;            n &gt;&gt;&gt;= 1;        }        return res;    }}</code></pre><p>还有一个就是用<code>n &amp; (n - 1)</code>来判断，这样的与运算会消去二进制中最右边的1。</p><pre><code class="language-Java">public class Solution {        public int hammingWeight(int n) {        int res = 0;        while (n != 0) {            n = n &amp; (n - 1);            res++;        }        return res;    }}</code></pre><p><code>n &amp; (n - 1)</code>还能用来判断是不是2的幂。</p><h1>16.数值的整数次方</h1><p>这里直接使用相乘的算法会超时，所以用到了快速幂的写法。这里需要注意的点是n为负数的时候，<code>int</code>的负数会比正数多一个，力扣上正好用到了这个边界<code>−2147483648</code>，所以需要用<code>long</code>类型来存储。</p><h2>快速幂的迭代写法</h2><pre><code class="language-Java">class Solution {    public double myPow(double x, int n) {        // 每个数的0次方都等于1        if (n == 0) return 1;        // 因为负数是比正数多一个数的，如果取得是负数的最小值，取反就会超出正数的最大值，所以使用long        long num = n;        double res = 1.0;        // 如果是负次方，就转换为正数次方        if (n &lt; 0) {            x = 1 / x;            num = -num;        }        // 若num还大于0代表运算还没有结束        while (num &gt; 0) {            // 当次方数为奇数的时候，就乘底数，次方数减一，转换为次方数为偶数的情况            if ((num &amp; 1) == 1) res *= x;            // 次方数为偶数的时候，底数平方，次方数除以2，缩短运算            x = x * x;            num &gt;&gt;= 1;        }        return res;    }}</code></pre><h2>快速幂的递归写法</h2><pre><code class="language-Java">class Solution {    public double myPow(double x, int n) {        if (n == 0) return 1;        if (n == -1) return 1 / x;        if ((n &amp; 1) == 1) return myPow(x * x,n &gt;&gt; 1) * x;        else return myPow(x * x,n &gt;&gt; 1);    }}</code></pre><h1>18.删除链表的节点</h1><p>这里可以直接查到那个需要删除的节点，然后改变链表的指向就好了。</p><h2>迭代写法</h2><pre><code class="language-Java">class Solution {    public ListNode deleteNode(ListNode head, int val) {        if (head == null) return head;        if (head.val == val) return head.next;        ListNode newHead = new ListNode(0);        newHead.next = head;        ListNode pre = newHead;        ListNode cur = head;        while (cur != null) {            if (cur.val == val) {                pre.next = cur.next;                cur.next = null;                break;            }            pre = pre.next;            cur = cur.next;        }        return newHead.next;    }}</code></pre><h2>递归写法</h2><pre><code class="language-Java">class Solution {    public ListNode deleteNode(ListNode head, int val) {        if (head == null) return null;        if (head.val == val) return head.next;        head.next = deleteNode(head.next,val);        return head;    }}</code></pre><h1>21.调整数组顺序使奇数位于偶数前面</h1><p>这题一看题目就是有点像快排的思想，双指针遍历就好了。</p><pre><code class="language-Java">class Solution {    public int[] exchange(int[] nums) {        int l = 0, r = nums.length - 1;        while (l &lt; r) {            while (nums[l] % 2 != 0 &amp;&amp; l &lt; r) l++;            while (nums[r] % 2 == 0 &amp;&amp; l &lt; r) r--;            int temp = nums[l];            nums[l] = nums[r];            nums[r] = temp;        }        return nums;    }}</code></pre><h1>22.链表中倒数第K个节点</h1><h2>快慢指针</h2><p>这题用快慢指针还是很简单的，先让快指针走N步，然后快慢指针一起走，当快指针都到链表尾部，那么慢指针就是倒数第K个节点了。</p><pre><code class="language-Java">class Solution {    public ListNode getKthFromEnd(ListNode head, int k) {        ListNode fast = head;        ListNode slow = head;        while (k-- &gt; 0) fast = fast.next;        while (fast != null) {            fast = fast.next;            slow = slow.next;        }        return slow;    }}</code></pre><h1>24.反转链表</h1><h2>迭代</h2><pre><code class="language-Java">class Solution {    public ListNode reverseList(ListNode head) {        if (head == null || head.next == null) return head;        ListNode pre = null;        ListNode cur = head;        while (cur != null) {            ListNode node = cur.next;            cur.next = pre;            pre = cur;            cur = node;        }        return pre;    }}</code></pre><h2>递归</h2><pre><code class="language-Java">class Solution {    public ListNode reverseList(ListNode head) {        return reverse(head);    }    public ListNode reverse(ListNode head) {        if (head == null || head.next == null) return head;        ListNode last = reverse(head.next);        head.next.next = head;        head.next = null;        return last;    }}</code></pre><h1>25.合并两个排序的链表</h1><h2>迭代</h2><p>这个只要遍历就好了。</p><pre><code class="language-Java">class Solution {    public ListNode mergeTwoLists(ListNode l1, ListNode l2) {        ListNode head = new ListNode(1);        ListNode cur = head;        while (l1 != null &amp;&amp; l2 != null) {            if (l1.val &gt; l2.val) {                cur.next = l2;                l2 = l2.next;            } else {                cur.next = l1;                l1 = l1.next;            }            cur = cur.next;        }        if (l1 == null) cur.next = l2;        else cur.next = l1;        return head.next;    }}</code></pre><h2>递归</h2><p>递归的解法挺简洁的，但是可能会有点绕。</p><pre><code class="language-Java">class Solution {    public ListNode mergeTwoLists(ListNode l1, ListNode l2) {        // 要不要这行都可以        if (l1 == null &amp;&amp; l2 == null) return null;        if (l1 == null) return l2;        if (l2 == null) return l1;        if (l1.val &lt; l2.val) {            l1.next = mergeTwoLists(l1.next, l2);            return l1;        } else {            l2.next = mergeTwoLists(l1, l2.next);            return l2;        }    }}</code></pre><h1>26. 树的子结构</h1><p>首先要先找到A中与B节点的头节点相等的点，然后递归判断A与B的左子树与右子树，看是否结构与节点值相等。</p><pre><code class="language-Java">class Solution {         public boolean isSubStructure(TreeNode A, TreeNode B) {        if (A == null || B == null) return false;        return check(A, B) || isSubStructure(A.left, B) || isSubStructure(A.right, B);    }        // 判断a树是否和b树是一样的    public boolean check(TreeNode A, TreeNode B) {        // 若B节点为空，无论A节点是否为空，都是A树的子结构        if (B == null) return true;        // 因为此时B节点不为空，但是如果A节点为空，那么B树就不为A树的子结构了        if (A == null) return false;        // 两个值不相同，代表树种该位置两个节点不一样        if (A.val != B.val) return false;        // 判断左子树与右子树        return check(A.left, B.left) &amp;&amp; check(A.right, B.right);    }}</code></pre><h1>27.二叉树的镜像</h1><p>转换镜像，那就是将左子树变为右子树，右子树变为左子树就可以了。</p><pre><code class="language-Java">class Solution {    public TreeNode mirrorTree(TreeNode root) {        if (root == null) return null;        TreeNode node = new TreeNode(root.val);        node.left = mirrorTree(root.right);        node.right = mirrorTree(root.left);        return node;    }}</code></pre><h1>28.对称的二叉树</h1><p>这题递归主要是要理清思路。将左子树和右子树传入，然后比较值，在将左子树的左子树和右子树的右子树，右子树的左子树和左子树的右子树放入递归判断就好了。</p><pre><code class="language-Java">class Solution {    public boolean isSymmetric(TreeNode root) {        if (root == null) return true;        return check(root.left, root.right);    }        public boolean check(TreeNode left, TreeNode right) {        if (left == null &amp;&amp; right == null) return true;        if (left == null || right == null) return false;        if (left.val != right.val) return false;        return check(left.left, right.right) &amp;&amp; check(left.right, right.left);    }}</code></pre><h1>29.顺时针打印矩阵</h1><p>这题和那题螺旋矩阵差不多，最主要的就是遍历的边界的界定，这还是有点麻烦的。</p><pre><code class="language-Java">class Solution {    public int[] spiralOrder(int[][] matrix) {        int rows = matrix.length;        if (rows == 0) return new int[0];        int cols = matrix[0].length;        if (cols == 0) return new int[0];        int count = 0;        int[] res = new int[rows * cols];        int up = 0, down = rows - 1, left = 0, right = cols - 1;        // 注意调整边界时要注意不可过界        while (count &lt; rows * cols) {            // 向右，遍历完，上边界下移            for (int j = left; j &lt;= right &amp;&amp; left &lt;= right; j++) {                res[count++] = matrix[up][j];            }            up++;            // 向下，遍历完，右边界左移            for (int i = up; i &lt;= down &amp;&amp; up &lt;= down; i++) {                res[count++] = matrix[i][right];            }            right--;            // 向左，遍历完，下边界上移            for (int j = right; j &gt;= left &amp;&amp; up &lt;= down; j--) {                res[count++] = matrix[down][j];            }            down--;            // 向上，遍历完，左边界右移            for (int i = down; i &gt;= up &amp;&amp; left &lt;= right; i--) {                res[count++] = matrix[i][left];            }            left++;        }        return res;    }}</code></pre><h1>30. 包含min函数的栈</h1><p>找到最小元素其实并不难，这题难的地方是调用<code>min</code>，<code>push</code>，<code>pop</code>的时间复杂度都要为O(1)。</p><p>使用辅助栈来存取最小的数据，只要出现比辅助栈的栈顶元素还小的数，那就压入辅助栈，这样辅助栈就会是至今为止最小的元素的排行。</p><pre><code class="language-Java">class MinStack {    Stack&lt;Integer&gt; stack1;    Stack&lt;Integer&gt; stack2;    /** initialize your data structure here. */    public MinStack() {        stack1 = new Stack&lt;&gt;();        stack2 = new Stack&lt;&gt;();    }        public void push(int x) {        stack1.push(x);        if (stack2.isEmpty()) {            stack2.push(x);        } else {            stack2.push(Math.min(x, stack2.peek()));        }    }        public void pop() {        stack1.pop();        stack2.pop();    }        public int top() {        return stack1.peek();    }        public int min() {        return stack2.peek();    }}</code></pre><h1>31. 栈的压入和弹出序列</h1><p>这个问题就可以创建一个栈，然后进行模拟压入和弹出就可以了。</p><p>如果压入了弹出序列的数，就弹出就好了。</p><pre><code class="language-Java">class Solution {    public boolean validateStackSequences(int[] pushed, int[] popped) {        Stack&lt;Integer&gt; stack = new Stack&lt;&gt;();        int start = 0;        for (int i : pushed) {            stack.push(i);            while (!stack.isEmpty() &amp;&amp; stack.peek() == popped[start]) {                stack.pop();                start++;            }        }        return stack.isEmpty();    }}</code></pre><h1>32.1.从上到下打印二叉树</h1><p>这题就是层序遍历就可以了，要注意的就是确定数组的长度。</p><pre><code class="language-Java">class Solution {    public int[] levelOrder(TreeNode root) {        if (root == null) return new int[0];        List&lt;Integer&gt; list = new ArrayList&lt;&gt;();        Deque&lt;TreeNode&gt; deque = new LinkedList&lt;&gt;();        deque.offer(root);        while (!deque.isEmpty()) {            TreeNode node = deque.poll();            list.add(node.val);            if (node.left != null) deque.offer(node.left);            if (node.right != null) deque.offer(node.right);        }        int[] res = new int[list.size()];        for (int i = 0; i &lt; res.length; i++) {            res[i] = list.get(i);        }        return res;    }}</code></pre><h1>32.2.从上到下打印二叉树</h1><p>这个也是层序遍历，与上一题不一样的是要先确定每层的节点数然后将每层的节点加入到临时链表存储，之后在加入到答案列表。</p><pre><code class="language-Java">class Solution {    public List&lt;List&lt;Integer&gt;&gt; levelOrder(TreeNode root) {        List&lt;List&lt;Integer&gt;&gt; res = new ArrayList&lt;&gt;();        if (root == null) return res;        Deque&lt;TreeNode&gt; deque = new LinkedList&lt;&gt;();        deque.offer(root);        while (!deque.isEmpty()) {            int size = deque.size();            List&lt;Integer&gt; list = new ArrayList&lt;&gt;();            while (size-- != 0) {                TreeNode node = deque.poll();                list.add(node.val);                if (node.left != null) deque.offer(node.left);                if (node.right != null) deque.offer(node.right);            }            res.add(list);                    }        return res;    }}</code></pre><h1>32.3从上到下打印二叉树</h1><p>这个就是要记录一下层数，这里的倒序可以分为两种解法。</p><ol><li>倒着加入到列表</li><li>加入到列表之后再倒序</li></ol><p>两种方法哪个都可以，我就使用了第二种方法。</p><pre><code class="language-Java">class Solution {    public List&lt;List&lt;Integer&gt;&gt; levelOrder(TreeNode root) {        List&lt;List&lt;Integer&gt;&gt; res = new ArrayList&lt;&gt;();        if (root == null) return res;        Deque&lt;TreeNode&gt; deque = new LinkedList&lt;&gt;();        deque.offer(root);        while (!deque.isEmpty()) {            List&lt;Integer&gt; list = new ArrayList&lt;&gt;();            int size = deque.size();            while (size-- != 0) {                TreeNode node = deque.poll();                list.add(node.val);                if (node.left != null) deque.offer(node.left);                if (node.right != null) deque.offer(node.right);            }            if (res.size() % 2 != 0) Collections.reverse(list);            res.add(list);        }        return res;    }}</code></pre><h1>33.二叉搜索树的后序遍历序列</h1><p>后序遍历的遍历序列大致可以分成这样[左子树 | 右子树 | 根节点]</p><p>又因为是二叉搜索树，故左子树的所有节点应该比根节点小，右子树的所有节点应该比根节点大</p><pre><code class="language-Java">class Solution {        public boolean verifyPostorder(int[] postorder) {        return check(postorder,0,postorder.length - 1);    }    // 判断该树是否正确    public boolean check(int[] postorder, int start, int end) {        // start &gt;= end代表该树只有小于等于1的节点，所以不用判断，返回true        if (start &gt;= end) return true;        int p = start;        // mid 为根节点的值        int mid = postorder[end];        // 检查左子树区间节点是否正确        while (postorder[p] &lt; mid) {            p++;        }        int lr = p;        // 检查右子树区间节点是否正确        while (postorder[p] &gt; mid) {            p++;        }        // 两个区间正确的话，p会遍历到根节点的位置，即p == end的话该树正确        if (p != end) return false;        // 然后遍历左子树和右子树是否正确        return check(postorder, start, lr - 1) &amp;&amp; check(postorder, lr, end - 1);    }}</code></pre><h1>34. 二叉树中和为某一值的路径</h1><h2>回溯</h2><p>直接进行回溯就可以了。</p><pre><code class="language-java">class Solution {        List&lt;List&lt;Integer&gt;&gt; res = new LinkedList&lt;&gt;();    LinkedList&lt;Integer&gt; path = new LinkedList&lt;&gt;();    public List&lt;List&lt;Integer&gt;&gt; pathSum(TreeNode root, int target) {        dfs(root, target);        return res;    }    public void dfs(TreeNode node, int target) {        if (node == null) return;        // 先进行添加在判断条件，如果是先判断条件的话，target的判断就不能使用减法了，需要多添加一个b        path.addFirst(node.val);        target -= node.val;        // 若此时当前节点是子节点，并且target为0了，就代表是一条路径        if(target == 0 &amp;&amp; node.left == null &amp;&amp; node.right == null) {            res.add(new LinkedList(path));        }        // 遍历左子树和右子树        dfs(node.left, target);        dfs(node.right, target);        // 状态回溯        path.removeLast();    }}</code></pre><h1>35. 复杂链表的复制</h1><h2>哈希表</h2><p>直接先将链表复制下来，然后逐个给复制随机指针指向就可以了。</p><p>链表复制很简单。</p><pre><code class="language-java">class Solution {    public Node copyRandomList(Node head) {        if (head == null) return null;        Node newhead = new Node(head.val);        Node newcur = newhead;        Node cur = head.next;        while (cur != null) {            Node node = new Node(cur.val);            newcur.next = node;            newcur = node;            cur = cur.next;        }        return newhead;    }    }</code></pre><p>到这就会发现，这题的难点是怎样复制随机指针的指向。</p><p>这里可以用哈希表存储链表节点，键为原链表节点，值为新链表节点。</p><p>所以根据原链表节点的随机指针就能确定新链表的随机指针了。</p><pre><code class="language-Java">class Solution {    public Node copyRandomList(Node head) {        if (head == null) return null;        Map&lt;Node, Node&gt; map = new HashMap&lt;&gt;();        Node newHead = new Node(head.val);        map.put(head, newHead);        // 链表复制节点，并存储节点到哈希表        Node newCur = newHead;        Node cur = head.next;        while (cur != null) {            Node node = new Node(cur.val);            newCur.next = node;            newCur = node;            map.put(cur, newCur);            cur = cur.next;        }        // 给节点的随机指针赋值        while (head != null) {            map.get(head).random = map.get(head.random);            head = head.next;        }        return newHead;    }}</code></pre><h2>拼接加拆分</h2><p>将原链表和新链表拼接在一起</p><p>原节点1 --&gt; 新节点1 --&gt; 原节点2 --&gt; 新节点2 ...........</p><p>这样的话，新节点的随机指针指向的前一个节点就是原节点的随机指针指向了。</p><p>将随机指针赋值完之后，将这个整合的链表中的新节点提取出来，拆分成一个新的链表就可以得到复制的链表了。</p><pre><code class="language-Java">class Solution {    public Node copyRandomList(Node head) {        if (head == null) return head;        // 为链表的每个节点复制一个节点并接在该节点后面        Node cur = head;        while (cur != null) {            Node node = new Node(cur.val);            node.next = cur.next;            cur.next = node;            cur = node.next;        }        // 为新的复制节点的随机指针赋值        cur = head;        while (cur != null) {            if (cur.random != null) {                cur.next.random = cur.random.next;            }            cur = cur.next.next;        }        // 链表拆分        Node newHead = head.next;        Node newCur = newHead;        cur = head;        while (newCur.next != null) {            cur.next = cur.next.next;            newCur.next = newCur.next.next;            cur = cur.next;            newCur = newCur.next;        }        // 如果不加这句，系统判定是原来的节点，d        cur.next = null;        return newHead;            }}</code></pre><h1>39. 数组中出现次数超过一半的数字</h1><h2>排序</h2><p>先排序，因为出现的次数超过一半，所以数组中间的数一定是超过一半的数字。</p><pre><code class="language-Java">class Solution {    public int majorityElement(int[] nums) {        if (nums.length &lt;= 2) return nums[0];        Arrays.sort(nums);        return nums[nums.length / 2];    }}</code></pre><h2>摩尔投票法</h2><pre><code class="language-Java">class Solution {    public int majorityElement(int[] nums) {        if (nums.length &lt;= 2) return nums[0];        int count = 0;        int temp = 0;        for (int num : nums) {            if (count == 0) temp = num;            if (num == temp) {                count++;            } else {                count --;            }        }        return temp;    }}</code></pre><h1>40. 最小的k个数</h1><h2>暴力解法</h2><p>直接排序取前k个数行了。</p><pre><code class="language-java">class Solution {    public int[] getLeastNumbers(int[] arr, int k) {        if (k == 0) return new int[0];        Arrays.sort(arr);        int[] mink = new int[k];        for (int i = 0; i &lt; k; i++) {            mink[i] = arr[i];        }        return mink;    }}</code></pre><h2>快速排序</h2><p>因为要返回的最小的K个数，并没有说是有序的k个数，所以，快速排序的时候，排到第k个其实就可以停止了。</p><pre><code class="language-Java">class Solution {    public int[] getLeastNumbers(int[] arr, int k) {        if (k &gt;= arr.length) return arr;        return quickSort(arr, 0, arr.length - 1, k);    }    public int[] quickSort(int[] arr, int left, int right, int k) {        int i = left, j = right, x = arr[left];        while (i &lt; j) {            while (i &lt; j &amp;&amp; arr[j] &gt;= x) j--;            while (i &lt; j &amp;&amp; arr[i] &lt;= x) i++;            if (i &lt; j) {                int temp = arr[j];                arr[j] = arr[i];                arr[i] = temp;            }        }        arr[left] = arr[i];        arr[i] = x;        if (i &gt; k) return quickSort(arr, left, i - 1, k);        if (i &lt; k) return quickSort(arr, i + 1, right, k);        return Arrays.copyOf(arr, k);    }}</code></pre><h1>45.把数组排成最小的数</h1><p>最简单的方法找出数组的全排序，得到所有数字的可能性，然后比较每个数字的大小就可以了。</p><p>不过这会很麻烦，暂时不考虑这种解法。</p><p>解法的关键是这个数组该怎么排序。使用普通的大于小于关系当然是不行的，所以需要自定义一个大小关系。</p><p>这里就定义了一个关系。要是<code>ab &gt; ba</code>，那就设为<code>a &gt; b</code>。</p><p>这里就不多加证明了。</p><pre><code class="language-Java">class Solution {    public String minNumber(int[] nums) {        if (nums.length == 1) return String.valueOf(nums[0]);        sort(nums, 0, nums.length - 1);        StringBuilder res = new StringBuilder();        for (int num : nums) {            res.append(num);        }        return res.toString();    }    // 快排    public void sort(int[] nums, int l , int r) {        if (l &gt;= r) return;        int x = nums[l], i = l - 1, j = r + 1;        while (i &lt; j) {            do i++; while (check(x,nums[i]));            do j--; while (check(nums[j],x));            if (i &lt; j) swap(nums, i, j);        }        sort(nums, l, j);        sort(nums, j + 1, r);    }// 判断i是否大于j(自定义的大于)    public boolean check(int i, int j) {        StringBuilder a = new StringBuilder();        StringBuilder b = new StringBuilder();        a.append(i).append(j);        b.append(j).append(i);        return a.toString().compareTo(b.toString()) &gt; 0;    }    public void swap(int[] nums, int i, int j) {        int t = nums[i];        nums[i] = nums[j];        nums[j] = t;    }    }</code></pre><p>Java还有更简单的排序方法。</p><pre><code class="language-Java">class Solution {    public String minNumber(int[] nums) {        if (nums.length == 1) return String.valueOf(nums[0]);        List&lt;String&gt; list = new ArrayList&lt;&gt;();        for (int num : nums) {            list.add(String.valueOf(num));        }        // 这个排序虽然简单，但是效率好像没有快排高        list.sort((o1,o2) -&gt; (o1 + o2).compareTo(o2 + o1));        return String.join(&quot;&quot;,list);    }}</code></pre><h1>46.把数字翻译成字符串</h1><h2>递归</h2><pre><code class="language-java">class Solution {    public int translateNum(int num) {        if (num &lt;= 9) return 1;        int ba = num % 100;        if (ba &lt;= 9 || ba &gt;= 26) {            return translateNum(num / 10);        } else {            return translateNum(num / 10) + translateNum(num / 100);        }    }}</code></pre><h1>47.礼物的最大价值</h1><h2>回溯</h2><p>这种题目回溯一般会超时，因为会有很多分支，把所有可能都计算出来了，如果题目需要遍历到所有可能，回溯会是一种很好的方法。</p><pre><code class="language-Java">class Solution {    private int res = -1;    public int maxValue(int[][] grid) {        int rows = grid.length;        int cols = grid[0].length;        boolean[][] visited = new boolean[rows][cols];        dfs(grid, 0, 0, grid[0][0], visited);        return res;    }    public void dfs(int[][] grid, int x, int y, int count, boolean[][] visited) {        if (x == grid.length - 1 &amp;&amp; y == grid[0].length - 1) {            res = Math.max(count, res);        }        if (x + 1 &lt; grid.length &amp;&amp; !visited[x + 1][y]) {            visited[x + 1][y] = true;            dfs(grid, x + 1, y, count + grid[x + 1][y],visited);            visited[x + 1][y] = false;        }        if (y + 1 &lt; grid[0].length &amp;&amp; !visited[x][y + 1]) {            visited[x][y + 1] = true;            dfs(grid, x, y + 1, count + grid[x][y + 1],visited);            visited[x][y + 1] = false;        }    }}</code></pre><h2>动态规划</h2><p>回溯减枝就可以想到动态规划了，存储最大的情况。</p><pre><code class="language-Java">class Solution {    public int maxValue(int[][] grid) {        int rows = grid.length;        int cols = grid[0].length;        int[][] dp = new int[rows + 1][cols + 1];        for (int i = 1; i &lt;= rows; i++) {            for (int j = 1; j &lt;= cols; j++) {                dp[i][j] = Math.max(dp[i - 1][j], dp[i][j - 1]) + grid[i - 1][j - 1];            }        }        return dp[rows][cols];    }}</code></pre><p>这里其实可以优化为一维数组的。</p><pre><code class="language-Java">class Solution {    public int maxValue(int[][] grid) {        int rows = grid.length;        int cols = grid[0].length;        int[] dp = new int[cols + 1];        for (int i = 1; i &lt;= rows; i++) {            for (int j = 1; j &lt;= cols; j++) {                dp[j] = Math.max(dp[j], dp[j - 1]) + grid[i - 1][j - 1];            }        }        return dp[cols];    }}</code></pre><h1>48. 最长不含重复字符的子字符串</h1><p>这题两重循环的暴力解很大概率会超时，所以就不用暴力解了。</p><p>这里最好的方法就是滑动窗口。</p><pre><code class="language-Java">class Solution {    public int lengthOfLongestSubstring(String s) {        int len = s.length();        if (len &lt;= 1) return len;        int l = 0, max = 0;        Map&lt;Character,Integer&gt; map = new HashMap&lt;&gt;();        for (int i = 0; i &lt; len; i++) {            // 如果已经存在重复的字符，就更新窗口的左边界让字符串不重复            if (map.containsKey(s.charAt(i))) {                // 重新定义窗口左边界                l = Math.max(l, map.get(s.charAt(i)) + 1);            }            // 更新（添加）map里字符的坐标            map.put(s.charAt(i),i);            // 计算最大值            max = Math.max(max, i - l + 1);        }        return max;    }}</code></pre><h1>49. 丑数</h1><p>使用动态规划，主要是凑成最小的丑数，然后一个个凑到第N个，就是用2,3,5的公倍数从小凑到N。</p><pre><code class="language-Java">class Solution {    public int nthUglyNumber(int n) {        int[] dp = new int[n];        dp[0] = 1;        int p2 = 0, p3 = 0, p5 = 0;        for (int i = 1; i &lt; n; i++) {            int min = Math.min(Math.min(dp[p2] * 2,dp[p3] * 3),dp[p5] * 5);            if (min % 2 == 0) p2++;            if (min % 3 == 0) p3++;            if (min % 5 == 0) p5++;            dp[i] = min;        }        return dp[n - 1];    }}</code></pre><h1>50.第一个只出现一次的字符</h1><p>这题用正常思维就可以解决了，暂时找不到更优的解法，都是先存储出现的次数，然后在遍历找出第一个次数为1的字符。</p><p>我这里就是用了哈希来进行存储，用数组也是可以的，因为题目标明了只有小写字母。</p><pre><code class="language-Java">class Solution {    public char firstUniqChar(String s) {        Map&lt;Character,Integer&gt; map = new HashMap&lt;&gt;();        for (char c : s.toCharArray()) {            map.put(c,map.getOrDefault(c,0) + 1);        }        for (char c : s.toCharArray()) {            if (map.get(c) == 1) return c;        }         return &#39; &#39;;    }}</code></pre><h1>51.数组中的逆序对</h1><h2>暴力破解</h2><pre><code class="language-java">class Solution {    public int reversePairs(int[] nums) {        int[] sum = new int[nums.length];        for (int i = 0; i &lt; sum.length; i++) {            for (int j = i + 1; j &lt; sum.length; j++) {                if (nums[i] &gt; nums[j]) sum[i]++;            }        }        int res = 0;        for (int s : sum) {            res += s;        }        return res;    }}</code></pre><p>超时了。。O(n^2)的确很容易超时。</p><h2>归并排序</h2><p>这个真没想出来。不过这题的确是归并排序的经典题，求出当前数与前面的数或者是后面的数的大小关系的个数的题基本都是归并排序来进行实现的。</p><p>归并排序主要分为两个个部分，递归划分，排序合并。</p><p>所以在排序合并的时候就能记录出逆序对的个数了。</p><pre><code class="language-Java">class Solution {    private int res = 0;    private int[] temp;    public int reversePairs(int[] nums) {        temp = new int[nums.length];        return mergeSort(nums, 0, nums.length - 1);    }    public int mergeSort(int[] nums, int l, int r) {        // 划分终止        if (l &gt;= r) {            return 0;        }// 递归划分        int mid = l + (r - l) / 2;        mergeSort(nums, l, mid);        mergeSort(nums, mid + 1, r);        // 排序合并        int k = 0, i = l, j = mid + 1;        while (i &lt;= mid &amp;&amp; j &lt;= r) {            if (nums[i] &lt;= nums[j]) {                temp[k++] = nums[i++];            } else {                temp[k++] = nums[j++];                // 理解了这个结果的计算就容易了                //如果j位置小于i位置，那么j位置小于i位置后所有的右半边的数                res += mid - i + 1;            }        }        while (i &lt;= mid) {            temp[k++] = nums[i++];        }        while (j &lt;= r) {            temp[k++] = nums[j++];        }        // 排序结果放入原数组，完成排序        for (i = l, j = 0; i &lt;= r; i++, j++) {            nums[i] = temp[j];        }        return res;    }}</code></pre><h1>52.两个链表的第一个公共节点</h1><p>这题每个节点的数字都不是唯一的，所以不能用普通存储的方式来查找第一个公共节点。</p><p>所以需要考虑一下直接遍历，唯一的问题就是，这两个链表不一定是等长的。所以我们需要想办法弄成等长的。</p><pre><code class="language-Java">public class Solution {    public ListNode getIntersectionNode(ListNode headA, ListNode headB) {        ListNode n1 = headA;        ListNode n2 = headB;        // 都遍历了一段公共的节点与两个链表不公共的部分，凑成了等长的        while (n1 != n2) {            n1 = n1 == null ? headB : n1.next;            n2 = n2 == null ? headA : n2.next;        }        return n1;    }}</code></pre><h1>53.1在排序数组中查找数字1</h1><h2>暴力破解</h2><p>直接循环一遍即可。</p><pre><code class="language-java">class Solution {    public int search(int[] nums, int target) {        int res = 0;        for (int n : nums) {            if (n == target) res++;        }        return res;    }}</code></pre><h2>二分查找</h2><p>因为数组本身就是排好序的，所以很容易联想到二分查找。查找到第一个小于目标数字的数，然后循环找出有多少个目标数字就可以了。</p><pre><code class="language-Java">class Solution {    public int search(int[] nums, int target) {        int l = 0, r = nums.length;        int res = 0;        while (l &lt; r) {            int mid = l + (r - l) / 2;            if (nums[mid] &gt;= target) {                r = mid;            } else {                l = mid + 1;            }        }        while (l &lt; nums.length &amp;&amp; nums[l++] == target) {            res++;        }        return res;    }}</code></pre><h1>53.20~n-1中缺少的数字</h1><h2>暴力破解</h2><p>数组本身是递增的，直接递增，就能直接查找到缺少的数字。</p><pre><code class="language-Java">class Solution {    public int missingNumber(int[] nums) {        int res = 0;        for (int num : nums) {            if (num != res) return res;            res++;        }        return res;    }}</code></pre><h2>求和</h2><p>求和然后逐个减去数组中的数字，这样就能找出缺少的数字了。</p><pre><code class="language-java">class Solution {    public int missingNumber(int[] nums) {        int res = 0;        for (int i = 0; i &lt;= nums.length; i++) {            res += i;        }        for (int num : nums) {            res -= num;        }        return res;    }}</code></pre><h2>二分查找</h2><p>数组有序，那就是二分查找。</p><p>最主要的问题是查找什么。</p><p>若是每个数字都不缺少，那n这个数字应该会在数组的n的下标的位置。</p><p>要是有个数字缺少了，数组就会乱序，所以我们只要找到第一个乱序的数字，即为答案。</p><pre><code class="language-Java">class Solution {    public int missingNumber(int[] nums) {        int l = 0, r = nums.length;        while (l &lt; r) {            int mid = l + (r - l) / 2;            if (nums[mid] == mid) l = mid + 1;            else r = mid;        }        return l;    }}</code></pre><h1>56.1 数组中数字出现的次数</h1><h2>位运算</h2><p>首先看到这题就想到之前写的找出唯一一个不是两个数的数字，使用异或很快就解决了问题。</p><p>不过这里的数组有两个这样的数，所以就无法直接异或得到答案，需要进行一些处理。</p><h1>58.2 左旋转字符串</h1><h2>切片</h2><p>直接截出这两段字符串然后换位置就可以了。</p><pre><code class="language-Java">class Solution {    public String reverseLeftWords(String s, int n) {        return s.substring(n) + s.substring(0,n);    }}</code></pre><h2>顺序处理</h2><p>遍历字符串一个个加入到结果字符中也是可以的。</p><pre><code class="language-Java">class Solution {    public String reverseLeftWords(String s, int n) {        StringBuilder res = new StringBuilder();        char[] arr = s.toCharArray();        for (int i = n; i &lt; arr.length; i++) res.append(arr[i]);        for (int i = 0; i &lt; n; i++) res.append(arr[i]);        return res.toString();    }}</code></pre><h1>60. n个筛子的点数</h1><p>这题是动态规划问题，状态转移方程有点类似多重背包。</p><p><code>dp[i][j] += dp[i - 1][j - k] * dp[1][k]</code></p><pre><code class="language-Java">class Solution {    public double[] dicesProbability(int n) {        double[][] dp = new double[n + 1][6 * n + 1];        // 初始化一颗筛子的情况        for (int i = 1; i &lt;= 6; i++) dp[1][i] = (double) 1 / 6;        for (int i = 2; i &lt;= n; i++) {           // i颗筛子的最大点数            int max = 6 * i;                // 遍历i颗筛子的所有点数求出概率            for (int j = i; j &lt;= max; j++) {                for (int k = 1; k &lt;= 6; k++) {                    if (j - k &lt;= 0) {                           continue;                    }                    dp[i][j] += dp[i - 1][j - k] * dp[1][k];                }            }        }// 存储答案        double[] res = new double[5 * n + 1];        int value = n;        for (int i = 0; i &lt; res.length; i++) {            res[i] = dp[n][value++];        }        return res;    }}</code></pre><h1>61. 扑克牌的顺子</h1><p>首先确定答案的约束。</p><ol><li>抽到的五张牌是不是连续的。</li><li>抽到的五张牌是不重复的。</li><li>大小王可以换为任意一张牌，可以存在重复的0</li></ol><p>这里可以找出一定的规律，抽到的五张牌中最大的减去最小的小于5就可以说明这五张牌是重复的了。</p><h2>Set去重</h2><pre><code class="language-Java">class Solution {    public boolean isStraight(int[] nums) {        Set&lt;Integer&gt; res = new HashSet&lt;&gt;();        int max = -1, min = 14;        for (int i : nums) {            if (i == 0) continue;            max = Math.max(max,i);            min = Math.min(min,i);            // 如果有重复的就返回false            if (!res.add(i)) return false;        }        return max - min &lt; 5;    }}</code></pre><h2>排序遍历</h2><pre><code class="language-Java">class Solution {    public boolean isStraight(int[] nums) {        Arrays.sort(nums);        int count = 0;        for(int i = 0; i &lt; 4; i++) {            // 统计大小王的个数，用于寻找最小的牌            if (nums[i] == 0) count++;            // 若是重复了就直接返回false            else if (nums[i] == nums[i + 1]) return false;        }        // 最大的牌减去最小的牌小于5d        return nums[4] - nums[count] &lt; 5;    }}</code></pre><h1>62.圆圈中最后剩下的数字</h1><h2>模拟转圈</h2><p>直接按照题意进行转圈删除，最后一个剩下没被删除的就是答案了。</p><pre><code class="language-Java">class Solution {    public int lastRemaining(int n, int m) {        if (n == 1) return 1;        List&lt;Integer&gt; list = new ArrayList&lt;&gt;(n);        for (int i = 0; i &lt; n; i++) {            list.add(i);        }        int k = 0;        while (n &gt; 1) {            k = (k + m - 1) % n;            list.remove(k);            n--;        }        return list.get(0);    }}</code></pre><h2>约瑟夫环公式</h2><p>这题就算典序的约瑟夫环的问题。</p><p>约瑟夫环有一个递推公式。</p><p><code>F(n,m) = (f(n - 1,m) + m) % n</code></p><p>F(n,m)表示的是n个人报数，报到m的人会被杀掉，最终胜利者的编号，也就是这题的答案。</p><pre><code class="language-Java">class Solution {    public int lastRemaining(int n, int m) {        int res = 0;        // f(2,m)是最初的状态，需要加到F(n,m)才是最终答案        for (int i = 2; i &lt;= n; i++) {            res = (res + m) % i;        }        return res;    }}</code></pre><h1>63. 股票的最大利润</h1><p>这题是经典的动态规划题，不过这一题只交易一次。所以只需要找到一个最小的数买进，最大的数卖出就行了。</p><pre><code class="language-Java">class Solution {    public int maxProfit(int[] prices) {        if (prices.length &lt;= 1) return 0;        int res = 0, min = prices[0];        for (int i = 1; i &lt; prices.length; i++) {            // 找到一个更小的数就替换            if (prices[i] &lt;= min) {                min = prices[i];            } else {                res = Math.max(res,prices[i] - min);            }        }        return res;    }}</code></pre><h1>64. 求1+2+...+n</h1><p>这题题目是不难的，但是题目的要求让这题变难了，不能使用乘除法、for、while、if、else、switch、case等关键字及条件判断语句（A?B:C）。</p><p>这里运用到了<code>&amp;&amp;</code>短路与的特性，判断了第一个子式后如果是<code>false</code>就不会进行判断第二个子式了。</p><pre><code class="language-java">class Solution {    public int sumNums(int n) {        int sum = n;        boolean flag = n &gt; 0 &amp;&amp; (sum += sumNums(n - 1)) &gt; 0;        return sum;    }}</code></pre><h1>65. 不用加减乘除做加法</h1><p>不用加减乘除，那就肯定是位运算了。然后就只能看题解了。</p><p><code>^</code>表示两个数无进位的求和，比如<code>16 + 4</code>,无进位求和就是等于<code>10</code>的，相当于进位没有了。</p><p><code>&amp;</code>表示两个数进位的个数，比如<code>6 + 4</code> ，答案是等于<code>1</code>的，因为进了一位，需要在左移一位才能等于<code>10</code>。</p><pre><code class="language-Java">class Solution {    public int add(int a, int b) {        // 直到进位数为0，就停止循环        while(b != 0) {             int c = (a &amp; b) &lt;&lt; 1;              a ^= b;             b = c;         }        return a;    }}</code></pre><h1>66. 构建乘积数组</h1><p>这题主要是除了当前元素其他元素都相乘，其实就算数左边的元素的乘积与右边元素的乘积的乘积。</p><pre><code class="language-Java">class Solution {    public int[] constructArr(int[] a) {        if (a.length &lt;= 1) return a;        int[] res = new int[a.length];        int left = 1;        int right = 1;        for(int i = 0; i &lt; a.length; i++) {            res[i] = left;            left *= a[i];        }        for(int i = a.length - 1; i &gt; -1; i--) {            res[i] *= right;            right *= a[i];        }        return res;    }}</code></pre><h1>68.1 二叉搜索树的最近公共祖先</h1><p>这题主要总结节点的情况，该题的节点应该是分为三种的。</p><ol><li>p和q都在左子树</li><li>p和q都在右子树</li><li>p和q一个在左子树，一个在右子树（这种情况的节点就算二叉树的最近公共祖先）</li></ol><p>了解了状态这题应该就不难了。</p><pre><code class="language-Java">class Solution {    public TreeNode lowestCommonAncestor(TreeNode root, TreeNode p, TreeNode q) {        if (root == null) return null;        if (root.val &gt; p.val &amp;&amp; root.val &gt; q.val)            return lowestCommonAncestor(root.left,p,q);        if (root.val &lt; p.val &amp;&amp; root.val &lt; q.val)            return lowestCommonAncestor(root.right,p,q);        return root;    }}</code></pre><h1>68.2 二叉树的最近公共祖先</h1><p>这题的分析跟上面的情况一样，只是判断子树的情况不太一样。</p><pre><code class="language-Java">class Solution {    public TreeNode lowestCommonAncestor(TreeNode root, TreeNode p, TreeNode q) {        if (root == p || root == q || root == null) return root;        TreeNode left = lowestCommonAncestor(root.left,p,q);        TreeNode right = lowestCommonAncestor(root.right,p,q);        if (left == null) return right;        if (right == null) return left;        if (left != null &amp;&amp; right != null) return root;        return null;    }}</code></pre>]]></content:encoded>
            <category domain="https://yww52.com/categories/%25E6%2588%2591%25E7%259A%2584%25E6%258A%2580%25E6%259C%25AF%25E5%25AD%25A6%25E4%25B9%25A0%25E7%2589%25A9%25E8%25AF%25AD%25E6%259E%259C%25E7%2584%25B6%25E6%259C%2589%25E9%2597%25AE%25E9%25A2%2598/">我的技术学习物语果然有问题</category>
        </item>
        <item>
            <title>Redis</title>
            <link>https://yww52.com/posts/bae4ff13/</link>
            <guid isPermaLink="true">https://yww52.com/posts/bae4ff13/</guid>
            <pubDate>Mon, 15 Feb 2021 00:00:00 +0000</pubDate>
            <description>学习Redis的笔记</description>
            <content:encoded><![CDATA[<img src="https://img.yww52.com/2021/2/2021-2-15/top_img.jpg" alt="Redis" style="max-width:100%;height:auto;margin-bottom:15px;" /><h1>什么是NoSQL</h1><p>先来看看百度百科对NoSQL的描述。</p><blockquote><p>NoSQL，泛指非关系型的数据库。随着互联网web2.0网站的兴起，传统的关系数据库在处理web2.0网站，特别是超大规模和高并发的SNS类型的web2.0纯动态网站已经显得力不从心，出现了很多难以克服的问题，而非关系型的数据库则由于其本身的特点得到了非常迅速的发展。NoSQL数据库的产生就是为了解决大规模数据集合多重数据种类带来的挑战，特别是大数据应用难题。</p></blockquote><p>NoSQL维基百科称其为<code>Not Only SQL</code>,在当今的大数据年代，仅仅是关系型数据库已经不能满足当前一些流量较大的Web应用，所以NoSQL就应运而生，就是用来解决数据量较大的问题。</p><p>NoSQL可以分成四个大类。</p><ol><li><p>键值存储数据库</p></li><li><p>列存储数据库</p></li><li><p>文档型数据库</p></li><li><p>图形数据库</p></li></ol><p>这里我就用百度百科上的表格，来简单了解一下这四个分类。</p><table><thead><tr><th align="center">分类</th><th align="center">Examples举例</th><th align="center">典型应用场景</th><th align="center">数据模型</th><th align="center">优点</th><th align="center">缺点</th></tr></thead><tbody><tr><td align="center">键值存储数据库</td><td align="center">Tokyo Cabinet/Tyrant， Redis， Voldemort， Oracle BDB</td><td align="center">内容缓存，主要用于处理大量数据的高访问负载，也用于一些日志系统等等。</td><td align="center">Key 指向 Value 的键值对，通常用hash table来实现</td><td align="center">查找速度快</td><td align="center">数据无结构化，通常只被当作字符串或者二进制数据</td></tr><tr><td align="center">列存储数据库</td><td align="center">Cassandra， HBase， Riak</td><td align="center">分布式的文件系统</td><td align="center">以列簇式存储，将同一列数据存在一起</td><td align="center">查找速度快，可扩展性强，更容易进行分布式扩展</td><td align="center">功能相对局限</td></tr><tr><td align="center">文档型数据库</td><td align="center">CouchDB， MongoDb</td><td align="center">Web应用（与Key-Value类似，Value是结构化的，不同的是数据库能够了解Value的内容）</td><td align="center">Key-Value对应的键值对，Value为结构化数据</td><td align="center">数据结构要求不严格，表结构可变，不需要像关系型数据库一样需要预先定义表结构</td><td align="center">查询性能不高，而且缺乏统一的查询语法。</td></tr><tr><td align="center">图形(Graph)数据库</td><td align="center">Neo4J， InfoGrid， Infinite Graph</td><td align="center">社交网络，推荐系统等。专注于构建关系图谱</td><td align="center">图结构</td><td align="center">利用图结构相关算法。比如最短路径寻址，N度关系查找等</td><td align="center">很多时候需要对整个图做计算才能得出需要的信息，而且这种结构不太好做分布式的集群方案。</td></tr></tbody></table><p>这次学习的就是键值存储数据库中的Redis。</p><h1>什么是Redis</h1><p>Redis是<code>Remote Dictionary Server </code>的缩写，翻译为远程字典调用，是一个开源的使用ANSI C语言编写、支持网络、可基于内存亦可持久化的日志型、Key-Value数据库，并提供多种语言的API。</p><ul><li><a href="https://redis.io/">官网</a></li><li><a href="https://github.com/redis/redis">Github</a></li></ul><h1>安装</h1><h2>windows</h2><p>windows的只需要下载他们提供的压缩包使用就好了。</p><p><img src="https://img.yww52.com/2021/2/2021-2-15/img1.png" alt=""></p><p>打开服务后就能看到服务的信息，默认端口为6379</p><p><img src="https://img.yww52.com/2021/2/2021-2-15/img2.png" alt=""></p><p>接下来来确认一下服务是否真的开启了，打开客户端，使用<code>ping</code>命令来测试，出现<code>PONG</code>表示已经连接到了Redis的服务。</p><p><img src="https://img.yww52.com/2021/2/2021-2-15/img3.png" alt=""></p><p>:::note{type=&quot;info&quot;}</p><p>windows的版本活跃度很低，而且官网也不推荐使用windows，所以还是尽量使用Linux，之后的使用我也是使用Linux来测试。</p><p>:::</p><h2>Linux</h2><p>去到redis的github就能下载到压缩包使用。</p><p>因为Redis是使用C/C++写的，所以要想使用，还是需要先安装一下C/C++的环境。</p><pre><code class="language-bash"># 安装环境yum install gcc-c++# 配置makemake install</code></pre><blockquote><p>要是make命令时出现了很多的error，大概率就是因为redis6版本是需要gcc版本是5以上的才行，所以需要先升级gcc的版本，这里是升级到了9。</p></blockquote><pre><code class="language-bash">    yum -y install centos-release-scl    yum -y install devtoolset-9-gcc devtoolset-9-gcc-c++ devtoolset-9-binutils     scl enable devtoolset-9 bash# scl命令是临时启动，长期使用就需要配置环境echo &quot;source /opt/rh/devtoolset-9/enable&quot; &gt;&gt;/etc/profile</code></pre><p>Redis默认会安装到<code>/usr/local/bin</code>的目录里。</p><p>Redis不是默认启动的，为了方便学习，要设置成默认启动的，所以去修改<code>redis.conf</code>这个文件。</p><pre><code class="language-conf"># 将这个选项设置为yes,默认为nodaemonize yes</code></pre><p>启动Redis的服务。</p><pre><code class="language-bash"># 以哪个配置文件来启动redis的服务redis-server [conf的路径]# 打开redis的客户端，连接6379端口redis-cli -p 6379# 出现PONG表示连通ping</code></pre><p>关闭Redis的服务</p><pre><code class="language-bash"># 在客户端连接6379端口中SHUTDOWN# 退出客户端exit</code></pre><h1>性能测试</h1><p>Redis的目录下还会有一个命令<code>redis-benchmark</code>。</p><p>这个命令是官方自带的性能测试的命令，用来测试Redis的一些基础命令读写的速度，以下是命令的参数。</p><table><thead><tr><th align="center">参数选项</th><th align="center">描述</th><th align="center">默认值</th></tr></thead><tbody><tr><td align="center">-h</td><td align="center">指定服务器主机名</td><td align="center">127.0.0.1</td></tr><tr><td align="center">-p</td><td align="center">指定服务器端口</td><td align="center">6379</td></tr><tr><td align="center">-s</td><td align="center">指定服务器 socket</td><td align="center"></td></tr><tr><td align="center">-c</td><td align="center">指定并发连接数</td><td align="center">50</td></tr><tr><td align="center">-n</td><td align="center">指定请求数</td><td align="center">10000</td></tr><tr><td align="center">-d</td><td align="center">以字节的形式指定 SET/GET 值的数据大小</td><td align="center">2</td></tr><tr><td align="center">-k</td><td align="center">1=keep alive 0=reconnect</td><td align="center">1</td></tr><tr><td align="center">-r</td><td align="center">SET/GET/INCR 使用随机 key, SADD 使用随机值</td><td align="center"></td></tr><tr><td align="center">-P</td><td align="center">通过管道传输 <numreq> 请求</td><td align="center">1</td></tr><tr><td align="center">-q</td><td align="center">强制退出 redis。仅显示 query/sec 值</td><td align="center"></td></tr><tr><td align="center">--csv</td><td align="center">以 CSV 格式输出</td><td align="center"></td></tr><tr><td align="center">-l</td><td align="center">生成循环，永久执行测试</td><td align="center"></td></tr><tr><td align="center">-t</td><td align="center">仅运行以逗号分隔的测试命令列表</td><td align="center"></td></tr><tr><td align="center">-I</td><td align="center">Idle 模式。仅打开 N 个 idle 连接并等待。</td><td align="center"></td></tr></tbody></table><h1>数据类型</h1><p>Redis支持的数据类型有很多，官网也写的很清楚了。</p><blockquote><p>Redis provides data structures such as strings, hashes, lists, sets, sorted sets with range queries, bitmaps, hyperloglogs, geospatial indexes, and streams.</p></blockquote><h2>String(字符串)</h2><p>:::note{type=&quot;info&quot;}</p><p>String一般使用是用来计数，比如用户的访问次数，热点文章的点击数，转发数等。</p><p>:::</p><pre><code class="language-bash"># 值为一个字符串# 设置键值对set [key] [value]# 获取值get [key]# 查看所有的键值对keys *# 切换数据库（redis默认16个数据库，从0开始）select [index]# 清空数据库flushdb # 当前数据库flushall # 所有的数据库 # 判断键值对是否存在exists [key]# 往值追加字符串append [key] [value]# 获取字符串的长度strlen [key]# 自增,注意该字符串是要是数字incr [key]# 步长为1incrby [key] [length]# 自减decr [key]decrby [key] [length]# 获取区间的值getrange [key] [index1] [index2]# 替换值,从第几个开始替换值setrange [key] [index] [value]# 给给定键设置过期时间expire [key] [秒]# 创建一个键值对并设置过期时间setex [key] [秒] [value]# 查看是否过期，负数表示已过期的时间ttl [key]#  如果不存在该键就设置键值对setnx [key] [value]# 批量创建键值对mset [key1] [value1] [key2] [value3] ...# 批量获取值mget [key1] [key2] ...# 批量创建键值对,原子操作，若是有一个键已经存在，那就全部都会创建失败msetnx [key1] [value1] [key2] [value3] ...# 先获取键值对在创建键值对getset [key] [value]</code></pre><h2>List(列表)</h2><p>:::note{type=&quot;info&quot;}</p><p>List一般用来发布与订阅或者说消息队列、慢查询等场景。</p><p>:::</p><pre><code class="language-bash"># 值为类似一个双向的链表# lpush表示从头部增加值，rpush从韦部增加值lpush [key] [value]rpush [key] [value]# 获取列表中的值,从index1到index2区间获取值lrange [key] [index1] [index2]lrange [key] 0 -1 # 表示查询列表所有值# lpop移除第一个值，rpop移除最后一个值lpop [key]rpop [key]# 按下标获取值lindex [key] [index]# 查询键的长度，即有多少个值llen [key]# 移除指定的值lrem [key] [个数] [value]# 截取列表,即修改列表中的值ltrim [key] [index1] [index2]# 移除列表最后一个值，移动到另一个列表中rpoplpush [key1] [value] [key2]# 替换列表中指定的值lset [key] [index] [value]# 判断列表是否存在exists [key]# 插入值# 在列表的value1值的前面插入一个value2的值linsert [key] before [value1] [value2]# 在列表的value1值的后面插入一个value2的值linsert [key] after [value1] [value2]</code></pre><h2>Set(集合)</h2><p>:::note{type=&quot;info&quot;}</p><p>Set的使用场景一般为，需要存放的数据不能重复以及需要获取多个数据源交集和并集等场景</p><p>:::</p><pre><code class="language-bash"># set不能重复添加相同的元素，类似于Java的HashSet，是一种无序集合# 往集合中添加值sadd [key] [value]# 查看集合的所有值smembers [key]# 判断集合是否存在该元素sismember [key] [value]# 查看集合中元素的个数scard [key]# 移除指定元素srem [key] [value]# 随机抽选元素srandmember [key] # 默认一个srandmember [key] [length]# 随机移除一个元素spop [key]# 将一个元素移动到另一个集合smove [key1] [value] [key2]# 差集sdiff [key1] [key2]# 交集sinter [key1] [key2]# 并集sunion [key1] [key2]</code></pre><h2>Hash(哈希)</h2><p>:::note{type=&quot;info&quot;}</p><p>Hash一般是用来进行对象的存储</p><p>:::</p><pre><code class="language-bash"># hash的结构类似于Java8时候的HashMap# 设置一个键值对hset [key] [key1] [value1]# 获取值hget [key] [key1]# 批量设置键值对hmset [key] [key1] [value1] [key2] [value2]...# 批量获取值hmget [key] [key1] [key2]...# 获取全部键值对hgetall [key]# 删除指定键值对hdel [key] [key1]# 查看键值对的个数hlen [key]# 判断一个键值对是否存在hexists [key] [key1]# 获取所有的键hkeys [key]# 获取所有的值hvals [key]# 不存在可以创建hsetnx [key] [key1] [value1]</code></pre><h2>Zset(有序的集合)</h2><p>:::note{type=&quot;info&quot;}</p><p>Zset一般使用在那些需要排序的场景。比如礼物排行榜，弹幕消息等。</p><p>:::</p><pre><code class="language-bash"># 在Set的基础上增加了权重参数，从而可以通过权重来进行排序# 在指定位置增加值zadd [key] [index] [value]zadd [key] [index1] [value1] [index2] [value2]..# 有顺序地显示区间内的值，可以说是增序排序 zrangebyscore [key] [min] [max]zrangebyscore [key] -inf +inf # 表示按增序顺序显示所有的值# 有顺序地显示区间内的值，可以说是降序排序zrevrange [key] 0 -1# 表示降序顺序显示所有的值# 获取有序集合的个数zcrad [key]# 获取指定区间的元素的个数zount [key] [index1] [index2]</code></pre><h2>geospatial(地理位置)</h2><pre><code class="language-bash"># 用来表示地理位置，经纬度，实现底层就是Zset，所以一些命令也适用# 添加一个地理位置geoadd [key] [经度] [纬度] [value]# 获取指定值的经纬度geopos [key] [value]# 获取两个位置之间的距离geodist [key] [value1] [value2] [单位]# 指定某个位置和某个半径，返回半径内的值georadius [key] [经度] [纬度] [半径] [单位]georadius [key] [经度] [纬度] [半径] [单位] withdist# 还会显示到中心位置的距离georadius [key] [经度] [纬度] [半径] [单位] withcoord# 会显示值当前的经纬度# 指定一个元素和某个半径，返回离这个元素位置半径内的值georadiusbymember [key] [value] [半径] [单位]# 将指定位置经纬度转换为字符串geohash [key] [value]</code></pre><h2>Hyperloglog(基数统计)</h2><pre><code class="language-bash"># 基数，表示不重复元素的个数，存在误差# 添加键值对,值为一个集合pfadd [key] [value1] [value2] [value3]...# 合并集合,不会重复pfmerge [key] [key1] [key2]# 统计基数数量pfcount [key]</code></pre><h2>Bitmaps(位图)</h2><p>:::note{type=&quot;info&quot;}</p><p>一般用来用户签到，统计活跃的用户和用户在线情况。</p><p>:::</p><pre><code class="language-bash"># 使用二进制来表示状态，用于那些只有两个状态# 添加键值对setbit [key] [index] [1或0]# 获取值的状态getbit [key] [index]# 统计1的个数bitcount sign</code></pre><h2>总结</h2><p>对Redis的数据类型进行一个小的总结。</p><p>Redis的数据类型其实大概分为两类。</p><ul><li><p>五大数据类型</p><ol><li>String</li><li>Set</li><li>Hash</li><li>List</li><li>Zset</li></ol></li><li><p>三个特殊的数据类型</p><ol><li>geospatial</li><li>hyperloglog</li><li>bitmaps</li></ol></li></ul><h1>事务</h1><ul><li><p>Redis的事务其实就是一组命令的队列</p></li><li><p>Redis事务没有隔离级别的概念</p></li><li><p>一次性，顺序性，排他性</p></li><li><p>Redis的单条命令存在原子性，但是事务是多条命令的集合，所以事务不具备原子性</p></li><li><p>Redis的事务分为三个阶段</p><ol><li>开启事务</li><li>命令入队</li><li>执行事务</li></ol></li><li><p>在开启事务后，输入的命令会进入队列，但是没有直接执行，要等所有命令入队后，发出执行命令才会一次性，依次执行</p></li></ul><pre><code class="language-bash"># 开启事务multi# 输入multi命令出现ok，表示开启事务，之后输入命令，命令会入队# 执行队列中的命令exec# 要是想放弃执行事务，可以直接放弃事务discard</code></pre><p>:::note{type=&quot;warning&quot;}</p><ol><li>若是事务中存在错误的命令，那么整个事务的命令都不会被执行</li><li>若是事务中只是命令语法有问题，那么该命令会抛出异常，但是其他命令会正常执行</li></ol><p>:::</p><h1>监视</h1><p>首先了解悲观锁和乐观锁这两个概念。</p><ul><li><p>悲观锁</p><p>悲观锁，顾名思义，什么时候都很悲观，每次访问公共数据，都会觉得别人会进行修改，所以每次获取该数据的时候，都会对数据加锁</p></li><li><p>乐观锁</p><p>乐观锁，顾名思义，什么时候都很乐观，每次访问公共数据，都觉得别人不会进行修改，所以每次都不会去上锁，只有在更新该数据后会判断一下使用期间其他线程有没有修改该数据。可以使用版本号来实现。每次更新数据后，判断该数据的版本号是否发生变更，发生了变更，就会导致修改失效，若是没有就会修改成功并修改版本号。</p></li></ul><pre><code class="language-bash"># Redis的监控就是使用了乐观锁的操作watch [key]multi ... exec# 使用watch来监视键值，一组事务执行后查看数据是否变更# 若是已经变更，该事务就会失效，即执行失败# 失败后就需要放弃监视,然后重新监视，执行事务，保证上锁的值是最新的值unwatch [key]watch [key]</code></pre><h1>持久化</h1><p>存储在内存中的数据，一旦机器出现问题，那么内存中的数据就会丢失，所以为了尽量避免这个问题，Redis提供了两种持久化的方法，接下来来了解一下这两种持久化的方式。</p><h2>RDB</h2><p>RDB持久化机制是将某个时刻的数据快照写入磁盘，也就是将某个时刻的数据保存下来，等到Redis服务启动，就会自动加载这个快照文件进行数据恢复。</p><h3>手动触发</h3><ul><li><p>save命令（不建议使用）</p><p>save是一个同步的命令，也就是说执行save命令，会让Redis的服务器发生阻塞，直到RDB持久化完成，其他命令才能正常进行。</p><p><img src="https://img.yww52.com/2021/2/2021-2-15/img4.png" alt=""></p></li><li><p>bgsave命令</p><p>bgsave是一个异步的命令，执行bgsave命令，Redis会<code>fork</code>一个子进程来进行RDB持久化，只有fork时才会阻塞，其他时间Redis正常运行。</p><p><img src="https://img.yww52.com/2021/2/2021-2-15/img5.png" alt=""></p></li></ul><h3>自动触发</h3><p><img src="https://img.yww52.com/2021/2/2021-2-15/img6.png" alt=""></p><ul><li><p>满足配置文件中save配置的文件，默认配置如下，可以自己配置</p><pre><code class="language-bash"># 当900秒内至少有1个键值对发生变动，触发持久化save 900 1# 当300秒内至少有10个键值对发生变动，触发持久化save 300 10# 当60秒内至少有10000个键值对发生变动，触发持久化save 60 10000</code></pre></li><li><p>执行<code>flushall</code>命令清空数据库时，触发持久化</p></li><li><p>执行<code>shutdown</code>命令等手段退出Redis时，触发持久化</p></li></ul><h3>RDB文件</h3><p>从配置文件可以看到配置文件的默认路径和默认名。</p><pre><code class="language-bash"># rdb文件的默认文件名dbfilename dump.rdb# rdb文件保存的目录dir ./</code></pre><h2>AOF</h2><p>AOF持久化全称<code>Append Only File</code>，当我们执行的改变数据的操作时就会将该命令追加到一个AOF文件的末尾，当Redis服务重新启动的时候，就会重新执行AOF文件内的命令，用来同步数据。</p><p>:::note{type=&quot;info&quot;}</p><p>AOF不是默认的持久化方式，故默认关闭的，需要去配置文件手动开启。</p><p>:::</p><p><img src="https://img.yww52.com/2021/2/2021-2-15/img7.png" alt=""></p><h3>AOF触发策略</h3><pre><code class="language-bash"># appendfsync alwaysappendfsync everysec# appendfsync no</code></pre><ul><li><p>always</p><p>每次有新的修改数据的命令，就会将缓冲区内的命令同步追加到AOF文件，十分安全，但是效率低</p></li><li><p>everysec</p><p>默认的策略，每秒将缓冲区内的命令同步追加到AOF文件，但是无法做到实时持久化，还是会可能丢失一秒的数据</p></li><li><p>no</p><p>交给操作系统来决定什么时候去同步追加数据</p></li></ul><h3>重写机制</h3><p>当命令不断被追加到AOF文件内，文件会越来越大，这对使用来说很不好，所以Redis提供了一个AOF的重写机制来解决这个问题，将AOF文件内的命令优化，重写为可以恢复到当前数据的最小指令集，从而减少文件的大小，达到压缩AOF文件的目的。</p><p>触发流程如下。</p><p><img src="https://img.yww52.com/2021/2/2021-2-15/img8.png" alt=""></p><h3>手动触发</h3><p>手动输入<code>bgrewriteaof</code>命令触发重写机制。</p><h3>自动触发</h3><p>自动触发就需要自行修改配置文件内AOF重写的配置。</p><pre><code class="language-bash"># 重写触发条件# 当AOF文件大于64mb而且比上一次重写的文件体积大了至少一倍，就会AOF重写auto-aof-rewrite-percentage 100auto-aof-rewrite-min-size 64mb</code></pre><h2>持久化流程</h2><p><img src="https://img.yww52.com/2021/2/2021-2-15/img9.png" alt=""></p><h1>过期Key</h1><h2>为什么要设置过期Key</h2><p>之前有提到过设置过期时间，那么就会有一个问题，为什么我们要多此一举去设置一个过期时间呢？</p><p>简而言之那就是，内存有限。</p><p>如果所有的Key都存在内存，数据量一大就会内存溢出了。</p><p>还有一个原因是用户希望存一个有过期时间的Key，就比如在一些场景下，过期Key就会显示很方便。Token的状态登陆或者是验证码的使用，都可以在Redis存储一个过期时间从而达到过期了就无法使用的目的。</p><p>不适用过期时间又想要实现这个功能，那就只能查询数据库获得记录，然后在判断是否过期，那也会有点麻烦。</p><h2>如何判断Key过期</h2><p>Redis 通过一个叫做过期字典（可以看作是hash表）来保存数据过期的时间。过期字典的键指向Redis数据库中的某个key(键)，过期字典的值是一个<code>long long</code>类型的整数，这个整数保存了key所指向的数据库键的过期时间（毫秒精度的UNIX时间戳）。</p><h2>过期Key的删除策略</h2><ol><li><p>定期删除</p><p>每隔一段时间就会抽取一批Key来进行过期检查，过期了就进行删除。</p></li><li><p>惰性删除</p><p>每次从数据库获取Key时会判断该Key是否过期，要是过期了就会进行删除，没有过期就会返回数据给用户。</p></li><li><p>定时删除</p><p>在设置key的过期时间的同时，为该key创建一个定时器，让定时器在key的过期时间来临时，对key进行删除。</p></li></ol><p>Redis默认使用的就是定期删除和惰性删除的配合。</p><h1>Redis的内存淘汰策略</h1><p>刚才有提到过过期Key，从Redis的过期Key删除策略中可以看出这些删除策略并不能将每一个过期的Key都删掉，所以在数据量大的时候又加上过期的Key，就会出现内存满了的情况，那这种时候就需要淘汰掉一些内存数据了。</p><p>Redis提供有8种淘汰的策略。</p><ol><li><strong>volatile-lru</strong>：淘汰掉设置过期时间的数据集里最近最少使用的数据</li><li><strong>volatile-ttl</strong>：淘汰掉设置过期时间的数据集里的将要过期的数据</li><li><strong>volatile-random</strong>：随机选择设置过期时间的数据集里的数据来进行淘汰</li><li><strong>volatile-lfu</strong>：淘汰掉设置过期时间的数据集里最不经常使用的数据</li><li><strong>allkeys-lru</strong>：移除空间里最近最少使用的 key（这个是最常用的）</li><li><strong>allkeys-random</strong>：从空间中任意选择数据来进行淘汰</li><li><strong>allkeys-lfu</strong>：移除空间里最不经常使用的数据</li><li><strong>no-eviction</strong>：不允许淘汰数据，当内存满了之后在进行新数据的写入就会报错</li></ol><h1>发布订阅</h1><p>发布订阅是一种消息通信模式。</p><p>发送者发送消息，订阅者接受发送者的消息。</p><p>接下来进行一个简单的测试。</p><ol><li><p>订阅一个频道，这里订阅<code>yww</code>这个频道</p><pre><code class="language-bash">127.0.0.1:6379&gt; subscribe ywwReading messages... (press Ctrl-C to quit)1) &quot;subscribe&quot;2) &quot;yww&quot;3) (integer) 1</code></pre></li><li><p>在打开一个Redis的客户端，然后往<code>yww</code>频道发送两条消息</p><pre><code class="language-bash">127.0.0.1:6379&gt; publish yww &quot;Hello Redis&quot;(integer) 1127.0.0.1:6379&gt; publish yww &quot;Hello World&quot;(integer) 1127.0.0.1:6379&gt; </code></pre></li><li><p>在订阅频道的订阅者的客户端就能接收到频道的消息了</p><pre><code class="language-bash">127.0.0.1:6379&gt; subscribe ywwReading messages... (press Ctrl-C to quit)1) &quot;subscribe&quot;2) &quot;yww&quot;3) (integer) 11) &quot;message&quot;2) &quot;yww&quot;3) &quot;Hello Redis&quot;1) &quot;message&quot;2) &quot;yww&quot;3) &quot;Hello World&quot;</code></pre></li></ol><p>以下是一些常用的API</p><pre><code class="language-bash"># 订阅一个或多个符和给定模式的频道psubscribe pattern[pattern...]# 退订一个或多个给定模式的频道punsubscribe [pattern[pattern]]# 查看订阅与发布系统状态pubsub subcommand [argument[argument...]]# 将消息发送到指定的频道publish channel message# 订阅一个或多个频道的信息subscribe channel[channel ...]# 退订一个或多个频道的信息unsubscribe [channel[channel...]]</code></pre><h1>主从复制，读写分离</h1><p>当数据量过大的时候，服务器的压力就会提高，为了解决这个问题，Redis提供了<code>主从复制，读写分离</code>的方案，因为大部分的压力是读操作，所以可以搭建一个Redis集群（最低要求三台，一主二从)，主节点负责写操作，从节点用来提供读的服务，这样就能减少服务器的压力了。</p><p><img src="https://img.yww52.com/2021/2/2021-2-15/img10.png" alt=""></p><p>当然还可以有以下这种情况</p><p><img src="https://img.yww52.com/2021/2/2021-2-15/img11.png" alt=""></p><p>节点是既可以当主节点，又可以当子节点的。</p><p>Redis的主从复制表示只有主节点能进行写操作，从节点是不能进行写操作的，只能进行读操作，主节点的数据会同步到子节点，达到整个集群的数据一致。</p><h2>同步机制</h2><p>上边说到的同步方式根据是否是全量来分为<code>全量同步</code>和<code>增量同步</code>。</p><ul><li><p>全量同步</p><p>全量同步的数据复制一般只会发生在<code>slave从节点</code>连接主节点的时候，这时从节点会将主节点中的所有数据都复制到从节点中，从而从节点连接主节点后数据通同步。</p><p>期间发生的具体步骤如下。</p><ol><li>从节点连接主节点后，向主节点发出<code>SYNC</code>命令</li><li>主节点收到<code>SYNC</code>命令后，开始执行<code>BGSAVE</code>生成RDB文件，因为是异步的操作，所以主节点继续处理命令，并将被执行的命令放入缓冲区</li><li>生成RDB文件后，就会向从节点发送快照文件</li><li>从节点收到主节点发送的RDB文件后，就会放弃原来存在的旧数据，然后载入RDB文件同步数据</li><li>触发增量同步</li></ol><p>这样主节点的数据旧全量同步复制到了从节点上。</p></li><li><p>增量同步</p><p>除了首次从节点的全量同步，一般数据的同步都是使用增量同步的方式</p><ol><li>主节点将缓冲区中的命令发送给从节点</li><li>从节点接收到命令请求，就会执行这些命令，完成部分写命令带来的数据同步</li></ol></li></ul><h2>模拟Redis集群</h2><p>这里使用同一个服务器，不同端口搭建的Redis集群进行学习。</p><p>当然你有多台服务器可以使用不同服务器来搭建集群来学习。</p><p>再不然可以直接开三个容器来搭建Redis集群来学习。</p><p>首先是了解一个基本的命令。</p><pre><code class="language-bash"># 查看当前服务的信息info replication127.0.0.1:6379&gt; info replication# Replicationrole:master# 角色为主节点，没有配置，默认每个服务都是主机connected_slaves:0# 连接从节点的个数master_replid:fb2dce24da3d78a04f01a6f55abdd48e1751dda0master_replid2:0000000000000000000000000000000000000000master_repl_offset:0second_repl_offset:-1repl_backlog_active:0repl_backlog_size:1048576repl_backlog_first_byte_offset:0repl_backlog_histlen:0127.0.0.1:6379&gt; </code></pre><p>创建三个配置文件，用来开启三个服务。</p><pre><code class="language-bash">redis6379.conf  redis6380.conf  redis6381.conf</code></pre><p>然后配置这三个文件。</p><pre><code class="language-bash"># redis6379.conf主节点的配置logfile &quot;6379log.log&quot;dbfilename dump6379.rdb# redis6380.confport 6380pidfile /var/run/redis_6380.pidlogfile &quot;6380log.log&quot;dbfilename dump80.rdb# redis6381.confport 6381pidfile /var/run/redis_6381.pidlogfile &quot;6381log.logdbfilename dump6381.rdb# 开启redis集群服务# redis-server ./myconf/redis6379.conf # redis-server ./myconf/redis6380.conf # redis-server ./myconf/redis6381.conf # ps -ef |grep redisroot     19536 19481  0 20:19 pts/1    00:00:00 redis-cliroot     32040     1  0 22:30 ?        00:00:00 redis-server 127.0.0.1:6379root     32052     1  0 22:30 ?        00:00:00 redis-server 127.0.0.1:6380root     32062     1  0 22:30 ?        00:00:00 redis-server 127.0.0.1:6381root     32106 30264  0 22:30 pts/2    00:00:00 grep --color=auto redis# 开启三个客户端，这里使用主节点的客户端演示，子节点客户端另开窗口# redis-cli -p 6379</code></pre><p>然后是建立主从联系，这里使用6379的服务当成主节点，建立联系有两种方法。</p><ul><li><p>在从节点中使用命令连接主机，这是简单的连接，要是从节点服务关闭了，之后重启旧连接不到主节点了，可以说是一次性的</p><pre><code class="language-bash">    SLAVEOF [host] [port]</code></pre></li><li><p>使用配置设置，这种情况，从节点服务重启后就会继续连接主机</p><pre><code class="language-bash">################################# REPLICATION ################################## 在这下面配置主节点服务地址就好了，样例已经给出# replicaof &lt;masterip&gt; &lt;masterport&gt;# 要是主节点服务有密码可以在这里配置# masterauth &lt;master-password&gt;</code></pre></li></ul><p>这里连接6379当成主节点。</p><pre><code class="language-bash"># 配置6380从节点127.0.0.1:6380&gt; SLAVEOF 127.0.0.1 6379OK127.0.0.1:6380&gt; info replication# Replicationrole:slavemaster_host:127.0.0.1master_port:6379master_link_status:upmaster_last_io_seconds_ago:3master_sync_in_progress:0slave_repl_offset:28slave_priority:100slave_read_only:1connected_slaves:0master_replid:8ed59f23d41ec553e33eae6b5fb13cfd1d807e80master_replid2:0000000000000000000000000000000000000000master_repl_offset:28second_repl_offset:-1repl_backlog_active:1repl_backlog_size:1048576repl_backlog_first_byte_offset:1repl_backlog_histlen:28# 配置6381从节点127.0.0.1:6381&gt; SLAVEOF 127.0.0.1 6379OK127.0.0.1:6381&gt; info replication# Replicationrole:slavemaster_host:127.0.0.1master_port:6379master_link_status:upmaster_last_io_seconds_ago:9master_sync_in_progress:0slave_repl_offset:42slave_priority:100slave_read_only:1connected_slaves:0master_replid:8ed59f23d41ec553e33eae6b5fb13cfd1d807e80master_replid2:0000000000000000000000000000000000000000master_repl_offset:42second_repl_offset:-1repl_backlog_active:1repl_backlog_size:1048576repl_backlog_first_byte_offset:15repl_backlog_histlen:28 # 查看6379主节点信息127.0.0.1:6379&gt; info replication# Replicationrole:masterconnected_slaves:2slave0:ip=127.0.0.1,port=6380,state=online,offset=70,lag=1slave1:ip=127.0.0.1,port=6381,state=online,offset=70,lag=1master_replid:8ed59f23d41ec553e33eae6b5fb13cfd1d807e80master_replid2:0000000000000000000000000000000000000000master_repl_offset:70second_repl_offset:-1repl_backlog_active:1repl_backlog_size:1048576repl_backlog_first_byte_offset:1repl_backlog_histlen:70</code></pre><p>然后在主机设置的数据，可以在从机中获取数据。</p><h2>哨兵模式（Sentinel）</h2><p>通过配置连接的子节点出现故障，重启服务后，因为配置连接的缘故那还是子节点，所以子节点出现故障的情况很简单。</p><p>:::note{type=&quot;info&quot;}</p><p>要是这个子节点有子节点，那么主机故障后，子节点的role依旧是salve，所以还是不能进行写操作。</p><p>:::</p><p>接下来重点了解一下主节点出现故障的情况。</p><p>当主节点出现故障后，它的子节点身份是不会变的，当主节点的服务重新启动后，集群依旧存在。</p><p>当我们不清楚主节点何时恢复，总不能一直不进行写操作，所以就需要重新推出一个主节点。</p><pre><code class="language-bash"># 令从节点恢复masterSLAVEOF no one# 然后在配置其他节点连接至该节点</code></pre><p> 这是手动配置主节点，这其实还是挺麻烦的，所以为了解决这个问题，Redis从2.8的版本后就提供了<code>哨兵模式</code>这个方案。</p><hr><p>哨兵模式其实就是自动推选主节点的一种方案。</p><p>先来简单了解一下哨兵模式。</p><p>哨兵是一个独立的进程，在Redis的命令中也能看到它<code>redis-sentinel</code>。</p><p>该进程通过向各个节点发送命令，通过节点返回的信息来监控节点的使用情况。</p><p><img src="https://img.yww52.com/2021/2/2021-2-15/img12.png" alt=""></p><p>当主节点宕机了，哨兵向主节点发送命令却得不到返回信息，过段时间哨兵确认主节点宕机后，就会随机给从节点随机投票，获得投票的节点就会当选为主节点，然后通过发布订阅模式通知其他的从节点修改配置，让它们切换主节点对象，从而实现集群正常服务。</p><p>那要是哨兵宕机了怎么办呢？这样的设置还是会出现问题。所以哨兵不能只有一台，哨兵也要形成一个集群（最好三个起步），于是最终的解决方案就如下图。</p><p><img src="https://img.yww52.com/2021/2/2021-2-15/img13.png" alt=""></p><p>多个哨兵进行监控，当有一个哨兵检测到主节点宕机，并不会马上切换，因为有可能是哨兵的问题，所以哨兵只会主观认为主节点宕机（这种情况叫主观下线）。</p><p>当一定个数（在哨兵的配置文件可以配置）的哨兵都认为主节点宕机后，那大概率就不是哨兵的问题了，那么哨兵之前就会对剩余的从节点投票，投票结束后，随机一个哨兵进行<code>failover</code>操作（这种情况叫客观下线）。</p><p><code>failover</code>又称故障转移，它会从从节点中挑选一个作为Redis集群中的新的主节点。</p><ol><li>选择票数高的从节点当为主节点，若是不存在（同票的情况），就继续判断。</li><li>选择主从复制，同步数据最完整的节点成为主节点，若是不存在就继续判断。</li><li>选择启动最早的子节点当为新的主节点。</li></ol><p>对选出来的主节点执行<code>slaveof no one</code>将身份转换成主节点，然后向其他的从节点发送订阅模式通知，各个哨兵就会让他们的节点切换主节点的对象为新的主节点。最后更新之前宕掉的节点的身份为从节点，当宕机恢复后，就自动成为该集群的从节点。</p><hr><p>哨兵的启动需要先配置哨兵启动的配置文件<code>sentinel.conf</code>。</p><pre><code class="language-bash"># Example sentinel.conf # 哨兵sentinel运行的端口，默认26379port 26379 # 哨兵sentinel的工作目录dir /tmp # 最主要的一个配置，这个是必定配置的# master-name为主节点名称,ip为主节点的ip，port主节点Redis服务断开# 哨兵对主节点主观下线的个数达到quorum个后，就会执行故障转移。(三个节点就用2)# sentinel monitor &lt;master-name&gt; &lt;ip&gt; &lt;redis-port&gt; &lt;quorum&gt;sentinel monitor mymaster 127.0.0.1 6379 2 # 设置哨兵sentinel 连接主从的密码 注意必须为主从设置一样的验证密码# sentinel auth-pass &lt;master-name&gt; &lt;password&gt; # 当哨兵向主节点发送命令后，超过times毫秒没得到回应，该哨兵就会认为主节点宕机，默认30秒# sentinel down-after-milliseconds &lt;master-name&gt; &lt;milliseconds&gt; # 这个配置项指定了在发生failover主备切换时最多可以有多少个slave同时对新的master进行同步# sentinel parallel-syncs &lt;master-name&gt; &lt;numslaves&gt; # 故障转移的超时时间 failover-timeout，默认三分钟# sentinel failover-timeout &lt;master-name&gt; &lt;milliseconds&gt; # SCRIPTS EXECUTION# 配置当某一事件发生时所需要执行的脚本，对于脚本的运行结果有以下规则：# 若脚本执行后返回1，那么该脚本稍后将会被再次执行，重复次数默认为10# 若脚本执行后返回2，或者比2更高的一个返回值，脚本将不会重复执行。# 如果脚本在执行过程中由于收到系统中断信号被终止了，则同返回值为1时的行为相同。# 一个脚本的最大执行时间为60s，如果超过这个时间，脚本将会被一个SIGKILL信号终止，之后重新执行。# ## 通知型脚本，主要用于通知故障的情况# sentinel notification-script &lt;master-name&gt; &lt;script-path&gt;# 客户端重新配置主节点参数脚本# 当一个master由于failover而发生改变时，这个脚本将会被调用，通知相关的客户端关于master地址已经发生改变的信息。# 以下参数将会在调用脚本时传给脚本:# &lt;master-name&gt; &lt;role&gt; &lt;state&gt; &lt;from-ip&gt; &lt;from-port&gt; &lt;to-ip&gt; &lt;to-port&gt;# 目前&lt;state&gt;总是“failover”,# &lt;role&gt;是“leader”或者“observer”中的一个。 # 参数 from-ip, from-port, to-ip, to-port是用来和旧的master和新的master(即旧的slave)通信的# 这个脚本应该是通用的，能被多次调用，不是针对性的。# sentinel client-reconfig-script &lt;master-name&gt; &lt;script-path&gt;</code></pre><pre><code class="language-bash"># 开启哨兵线程redis-sentinel ./sentinel.conf</code></pre><h1>redis.conf</h1><pre><code class="language-bash">################################## INCLUDES #################################### include /path/to/local.conf# include /path/to/other.conf# 包含多个配置文件################################## NETWORK ###################################### 绑定IPbind 127.0.0.1# 打开安全模式，默认是打开的protected-mode yes# 绑定端口port 6379################################# GENERAL ###################################### 是否以守护线程方式开启服务，默认为no,建议改为yesdaemonize no# 如果以后台方式运行，需要指定pid文件pidfile /var/run/redis_6379.pid# 日志形式# This can be one of:# debug (a lot of information, useful for development/testing)# verbose (many rarely useful info, but not a mess like the debug level)# notice (moderately verbose, what you want in production probably)# warning (only very important / critical messages are logged)loglevel notice# 指定日志文件路径logfile &quot;&quot;# 数据库数量databases 16################################# REPLICATION ################################## 在这下面配置主节点服务地址就好了，样例已经给出# replicaof &lt;masterip&gt; &lt;masterport&gt;# 要是主节点服务有密码可以在这里配置# masterauth &lt;master-password&gt;################################ SNAPSHOTTING  ################################# 持久化设置# save s k# 表示在s秒内，若是有至少修改了k个键值对，就会进行持久化操作save 900 1save 300 10save 60 10000# 持久化出错是否继续工作stop-writes-on-bgsave-error yes# 是否压缩rdb文件rdbcompression yes# 对保存的rdb文件时进行校验检测rdbchecksum yes# rdb文件的默认文件名dbfilename dump.rdb# rdb文件保存的目录dir ./################################## SECURITY #################################### 设置密码，默认是没有设置该选项的# requirepass foobared################################### CLIENTS ##################################### 设置最大连接的客户端数# maxclients 10000############################## MEMORY MANAGEMENT ################################# 设置redis最大的内存容量# maxmemory &lt;bytes&gt;# 内存满了的处理策略# maxmemory-policy noeviction# 1. noeviction：当内存使用超过配置的时候会返回错误，不会驱逐任何键# 2. allkeys-lru：加入键的时候，如果过限，首先通过LRU算法驱逐最久没有使用的键# 3. volatile-lru：加入键的时候如果过限，首先从设置了过期时间的键集合中驱逐最久没有使用的键# 4. allkeys-random：加入键的时候如果过限，从所有key随机删除# 5. volatile-random：加入键的时候如果过限，从过期键的集合中随机驱逐# 6. volatile-ttl：从配置了过期时间的键中驱逐马上就要过期的键# 7. volatile-lfu：从所有配置了过期时间的键中驱逐使用频率最少的键# 8. allkeys-lfu：从所有键中驱逐使用频率最少的键############################## APPEND ONLY MODE ################################ 是否开启aof模式，默认不开启，使用rdb方式持久化appendonly no# 持久化的文件名字appendfilename &quot;appendonly.aof&quot;# 同步策略# 每次都会同步# appendfsync always# 每秒进行一次同步appendfsync everysec# 不执行同步，交给操作系统自己同步# appendfsync no# 执行重写时同步数据到AOF文件，默认为no，yes表示不同步直接写入新的AOF文件no-appendfsync-on-rewrite no# 重写触发条件# 当AOF文件大于64mb而且比上一次重写的文件体积大了至少一倍，就会AOF重写auto-aof-rewrite-percentage 100auto-aof-rewrite-min-size 64mb</code></pre><h1>一些参考链接</h1><ul><li><a href="https://zhuanlan.zhihu.com/p/105587132">https://zhuanlan.zhihu.com/p/105587132</a></li><li><a href="https://www.bilibili.com/video/BV1S54y1R7SB?p=1">https://www.bilibili.com/video/BV1S54y1R7SB?p=1</a></li><li><a href="https://baijiahao.baidu.com/s?id=1654694618189745916&wfr=spider&for=pc">https://baijiahao.baidu.com/s?id=1654694618189745916&amp;wfr=spider&amp;for=pc</a></li><li><a href="https://blog.csdn.net/weixin_39040059/article/details/79120444">https://blog.csdn.net/weixin_39040059/article/details/79120444</a></li><li><a href="https://www.runoob.com/redis/redis-tutorial.html">https://www.runoob.com/redis/redis-tutorial.html</a></li><li><a href="https://www.cnblogs.com/daofaziran/p/10978628.html">https://www.cnblogs.com/daofaziran/p/10978628.html</a></li><li><a href="https://www.jianshu.com/p/06ab9daf921d">https://www.jianshu.com/p/06ab9daf921d</a></li></ul>]]></content:encoded>
            <category domain="https://yww52.com/categories/%25E6%2588%2591%25E7%259A%2584%25E6%258A%2580%25E6%259C%25AF%25E5%25AD%25A6%25E4%25B9%25A0%25E7%2589%25A9%25E8%25AF%25AD%25E6%259E%259C%25E7%2584%25B6%25E6%259C%2589%25E9%2597%25AE%25E9%25A2%2598/">我的技术学习物语果然有问题</category>
        </item>
        <item>
            <title>常用的工具类</title>
            <link>https://yww52.com/posts/b0ab3ebd/</link>
            <guid isPermaLink="true">https://yww52.com/posts/b0ab3ebd/</guid>
            <pubDate>Wed, 03 Feb 2021 00:00:00 +0000</pubDate>
            <description>记录一下开发中经常使用的工具类。</description>
            <content:encoded><![CDATA[<img src="https://img.yww52.com/2021/2/2021-2-3top_img.jpg" alt="常用的工具类" style="max-width:100%;height:auto;margin-bottom:15px;" /><h1>前言</h1><p> 总结记录一下在开发中经常使用到的一些工具类。</p><h1>MD5加密</h1><p>用户的密码一般都会进行加密后在存储到数据库当中，所以一般用到MD5这种加密算法。</p><p>这种加密算法最大的特点就是不可逆，只能加密不能解密。</p><p>这是挺简单的一种加密算法了，之后还可以考虑加盐和哈希散列，进行更复杂的加密手段。</p><pre><code class="language-Java">public class Md5Utils {    public static String encrypt(String strSrc) {        try {            char hexChars[] = { &#39;0&#39;, &#39;1&#39;, &#39;2&#39;, &#39;3&#39;, &#39;4&#39;, &#39;5&#39;, &#39;6&#39;, &#39;7&#39;, &#39;8&#39;,                    &#39;9&#39;, &#39;a&#39;, &#39;b&#39;, &#39;c&#39;, &#39;d&#39;, &#39;e&#39;, &#39;f&#39; };            byte[] bytes = strSrc.getBytes();            MessageDigest md = MessageDigest.getInstance(&quot;MD5&quot;);            md.update(bytes);            bytes = md.digest();            int j = bytes.length;            char[] chars = new char[j * 2];            int k = 0;            for (int i = 0; i &lt; bytes.length; i++) {                byte b = bytes[i];                chars[k++] = hexChars[b &gt;&gt;&gt; 4 &amp; 0xf];                chars[k++] = hexChars[b &amp; 0xf];            }            return new String(chars);        } catch (NoSuchAlgorithmException e) {            e.printStackTrace();            throw new RuntimeException(&quot;MD5加密出错+&quot; + e);        }    }}</code></pre><h1>雪花算法</h1><p>对于MySQL的主键问题，一般都需要使用自增的主键。但是只是从1自增就会因为分库分表产生主键相同的情况。</p><p>所以这次使用雪花算法生成一个分布式全局唯一ID</p><p>首先要知道雪花算法生成的ID是怎么样的。</p><p>分布式ID一般都是Long类型，所以会有64位。</p><ul><li>第一部分是一个bit位，是一个标识部分，在java中由于long的最高位是符号位，正数是0，负数是1，一般生成的ID为正数，所以固定为0</li><li>第二部分是时间戳，该部分占41bit，这个是毫秒级的时间</li><li>第三部分是工作机器ID，占10bit</li><li>第四部分是序列号，占12bit，支持同一毫秒内同一个节点可以生成4096个ID</li></ul><pre><code class="language-Java">/** * &lt;p&gt; *     雪花算法，分布式全局唯一ID * &lt;/p&gt; * @ClassName SnowFlake * @Author yww * @date 2021/4/24 18:39 **/public class SnowFlake {    /**     *  起始的时间戳     */    private final static long START_STMP = 1480166465631L;    /**     *  序列号占用的位数     */    private final static long SEQUENCE_BIT = 12;    /**     *  机器标识占用的位数     */    private final static long MACHINE_BIT = 5;    /**     *  数据中心占用的位数     */    private final static long DATACENTER_BIT = 5;    /**     *  每一部分的最大值     */    private final static long MAX_DATACENTER_NUM = -1L ^ (-1L &lt;&lt; DATACENTER_BIT);    private final static long MAX_MACHINE_NUM = -1L ^ (-1L &lt;&lt; MACHINE_BIT);    private final static long MAX_SEQUENCE = -1L ^ (-1L &lt;&lt; SEQUENCE_BIT);    /**     *  每一部分向左的位移     */    private final static long MACHINE_LEFT = SEQUENCE_BIT;    private final static long DATACENTER_LEFT = SEQUENCE_BIT + MACHINE_BIT;    private final static long TIMESTMP_LEFT = DATACENTER_LEFT + DATACENTER_BIT;    /**     *  数据中心     */    private long datacenterId;    /**     *  机器标识     */    private long machineId;    /**     *  序列号     */    private long sequence = 0L;    /**     *  上一次时间戳     */    private long lastStmp = -1L;    public SnowFlake(long datacenterId, long machineId) {        if (datacenterId &gt; MAX_DATACENTER_NUM || datacenterId &lt; 0) {            throw new IllegalArgumentException(&quot;datacenterId can&#39;t be greater than MAX_DATACENTER_NUM or less than 0&quot;);        }        if (machineId &gt; MAX_MACHINE_NUM || machineId &lt; 0) {            throw new IllegalArgumentException(&quot;machineId can&#39;t be greater than MAX_MACHINE_NUM or less than 0&quot;);        }        this.datacenterId = datacenterId;        this.machineId = machineId;    }    /**     *  产生下一个ID     */    public synchronized long nextId() {        long currStmp = getNewstmp();        if (currStmp &lt; lastStmp) {            throw new RuntimeException(&quot;Clock moved backwards.  Refusing to generate id&quot;);        }        if (currStmp == lastStmp) {            //相同毫秒内，序列号自增            sequence = (sequence + 1) &amp; MAX_SEQUENCE;            //同一毫秒的序列数已经达到最大            if (sequence == 0L) {                currStmp = getNextMill();            }        } else {            //不同毫秒内，序列号置为0            sequence = 0L;        }        lastStmp = currStmp;        return          // 时间戳部分                (currStmp - START_STMP) &lt;&lt; TIMESTMP_LEFT                        // 数据中心部分                | datacenterId &lt;&lt; DATACENTER_LEFT                        // 机器标识部分                | machineId &lt;&lt; MACHINE_LEFT                        // 序列号部分                | sequence;    }    private long getNextMill() {        long mill = getNewstmp();        while (mill &lt;= lastStmp) {            mill = getNewstmp();        }        return mill;    }    private long getNewstmp() {        return System.currentTimeMillis();    }    /**     *  测试     */    public static void main(String[] args) {        SnowFlake snowFlake = new SnowFlake(2, 3);        for (int i = 0; i &lt; (1 &lt;&lt; 12); i++) {            System.out.println(snowFlake.nextId());        }    }}</code></pre><p>参考于:<a href="https://github.com/beyondfengyu/SnowFlake">https://github.com/beyondfengyu/SnowFlake</a></p><h1>JWT工具类</h1><h2>引入依赖</h2><pre><code class="language-xml">    &lt;!--  jwt  --&gt;    &lt;dependency&gt;        &lt;groupId&gt;com.auth0&lt;/groupId&gt;        &lt;artifactId&gt;java-jwt&lt;/artifactId&gt;        &lt;version&gt;${jwt.version}&lt;/version&gt;    &lt;/dependency&gt;</code></pre><h2>工具类</h2><pre><code class="language-java">package com.yww.management.utils;import com.auth0.jwt.JWT;import com.auth0.jwt.algorithms.Algorithm;import com.auth0.jwt.exceptions.*;import com.auth0.jwt.interfaces.DecodedJWT;import com.auth0.jwt.interfaces.JWTVerifier;import java.util.Date;import java.util.HashMap;import java.util.Map;/** * &lt;p&gt; *      Token工具类 * 1. Header,记录令牌类型和签名算法 * 2. payload,携带用户信息 *      (1) iss(issuer), 签发者 *      (2) sub(subject), 面向的主体 *      (3) aud(audience), 接收方 *      (4) nbf(notBefore), 开始生效生效时间戳 *      (5) exp(expiresAt), 过期时间戳 *      (6) iat(issuedAt ), 签发时间 *      (7) jti(jwtId), 唯一标识 * 3. signature, 签名，防止Token被篡改 * &lt;/p&gt; * * @ClassName TokenUtil * @Author yww * @Date 2022/10/15 14:31 */public class TokenUtil {    /**     * 生成Token     *     * @return  Token     */    public static String genToken(String secret) {        // 设置Token头部（不设置也会默认有这两个值）        Map&lt;String, Object&gt; header = new HashMap&lt;String, Object&gt;(2) {            private static final long serialVersionUID = 1L;            {                put(&quot;alg&quot;, &quot;HMAC512&quot;);                put(&quot;typ&quot;, &quot;JWT&quot;);            }        };        // 设置需要放在Token的有效载荷        Map&lt;String, Object&gt; payload = new HashMap&lt;String, Object&gt;(3) {            private static final long serialVersionUID = 1L;            {                put(&quot;name&quot;, &quot;yww&quot;);                put(&quot;num&quot;, 1141950370);            }        };        Date now = new Date();        Date exp = new Date(now.getTime() + 10);        return JWT.create()                // 设置header                .withHeader(header)                // 设置payload                .withIssuer(&quot;yww&quot;)                .withSubject(&quot;management&quot;)                .withAudience(&quot;vue-management&quot;)                .withNotBefore(now)                .withExpiresAt(exp)                .withIssuedAt(now)                .withJWTId(&quot;123456&quot;)                .withPayload(payload)                // 签名                .sign(Algorithm.HMAC512(secret));    }    /**     * 解析Token     *     * @param token Token     */    public static void parse(String token) {        try {            JWTVerifier jwtVerifier = JWT.require(Algorithm.HMAC512(&quot;1&quot;)).build();            DecodedJWT decode = jwtVerifier.verify(token);            System.out.println(decode.getHeader());            System.out.println(decode.getPayload());            System.out.println(decode.getSignature());        } catch (AlgorithmMismatchException e) {            System.out.println(&quot;签名算法不匹配&quot;);        } catch (SignatureVerificationException e) {            System.out.println(&quot;签名无效&quot;);        } catch (TokenExpiredException e) {            System.out.println(&quot;令牌已过期&quot;);        } catch (MissingClaimException e) {            System.out.println(&quot;缺少要验证的声明&quot;);        } catch (IncorrectClaimException e) {            System.out.println(&quot;声明包含的值和预期不符合&quot;);        } catch (JWTVerificationException e) {            System.out.println(&quot;验证中的某一个步骤失败&quot;);        }    }}</code></pre><h1>响应工具类</h1><pre><code class="language-Java">import cn.hutool.json.JSONUtil;import com.yww.management.common.Result;import javax.servlet.http.HttpServletResponse;import java.io.IOException;/** * &lt;p&gt; *      用于接口外响应JSON的响应工具类 * &lt;/p&gt; * * @ClassName ResponseUtil * @Author yww * @Date 2022/10/12 21:30 */public class ResponseUtil {    /**     * 用于返回JSON数据     * @param response 请求响应     * @param result   响应封装     */    public static void response(HttpServletResponse response, Result&lt;Object&gt; result) throws IOException {        // 设置响应的Header        response.setHeader(&quot;Access-Control-Allow-Origin&quot;, &quot;*&quot;);        response.setHeader(&quot;Cache-Control&quot;,&quot;no-cache&quot;);        response.setCharacterEncoding(&quot;UTF-8&quot;);        response.setContentType(&quot;application/json&quot;);        // 设置响应码        response.setStatus(result.getCode());        // 设置响应内容        response.getWriter().write(JSONUtil.toJsonStr(result));        // 将缓存信息刷新到页面        response.getWriter().flush();    }}</code></pre><h1>IP工具类</h1><p>这个工具类主要是用于获取请求的IP地址。</p><pre><code class="language-java">import cn.hutool.core.util.StrUtil;import javax.servlet.http.HttpServletRequest;/** * &lt;p&gt; *      IP相关工具类 * &lt;/p&gt; * * @ClassName IpUtil * @Author yww * @Date 2022/10/16 14:27 */public class IpUtil {    /**     * 获取客户端IP     *     * @param request   请求     * @return          IP     */    public static String getIpAddr(HttpServletRequest request) {        if (request == null) {            return &quot;unknown&quot;;        }        // 判断x-forwarded-for是否有IP        String ip = request.getHeader(&quot;x-forwarded-for&quot;);        if (isNotUnknown(ip)) {            ip = request.getHeader(&quot;Proxy-Client-IP&quot;);        }        // 判断Proxy-Client-IP是否有IP        if (isNotUnknown(ip)) {            ip = request.getHeader(&quot;X-Forwarded-For&quot;);        }        // 判断X-Forwarded-For是否有IP        if (isNotUnknown(ip)) {            ip = request.getHeader(&quot;WL-Proxy-Client-IP&quot;);        }        // 判断WL-Proxy-Client-IP是否有IP        if (isNotUnknown(ip)) {            ip = request.getHeader(&quot;X-Real-IP&quot;);        }        // 判断X-Real-IP是否有IP        if (isNotUnknown(ip)) {            ip = request.getRemoteAddr();        }        // 若是IP最后为0:0:0:0:0:0:0:1，就是本地回环地址，否则第一个非unknown的IP地址就是真实地址        return &quot;0:0:0:0:0:0:0:1&quot;.equals(ip) ? &quot;127.0.0.1&quot; : getMultistageReverseProxyIp(ip);    }    /**     * 从多级反向代理中获得第一个非unknown IP地址     *     * @param ip 获得的IP地址     * @return 第一个非unknown IP地址     */    @SuppressWarnings(&quot;all&quot;)    public static String getMultistageReverseProxyIp(String ip) {        // 多级反向代理检测        if (ip != null &amp;&amp; ip.indexOf(&quot;,&quot;) &gt; 0) {            final String[] ips = ip.trim().split(&quot;,&quot;);            for (String subIp : ips) {                if (isNotUnknown(subIp)) {                    ip = subIp;                    break;                }            }        }        return ip;    }    /**     * 检测给定字符串是否为未知，多用于检测HTTP请求相关     *     * @param checkString 被检测的字符串     * @return 是否未知     */    public static boolean isNotUnknown(String checkString) {        return StrUtil.isNotBlank(checkString) &amp;&amp; StrUtil.isNotEmpty(checkString) &amp;&amp; !&quot;unknown&quot;.equalsIgnoreCase(checkString);    }}</code></pre><h1>BigDecimal工具类</h1><pre><code class="language-Java">import java.math.BigDecimal;import java.math.RoundingMode;/** * &lt;p&gt; *      BigDecimal工具类 *  用来提供浮点数的精确运算的工具类 * &lt;/p&gt; * * @ClassName BigDecimalUtil * @Author yww * @Date 2022/10/25 16:47 */public class BigDecimalUtil {    /**     *  除法运算默认精度     */    private static final int DEFAULT_PRECISION = 10;    /**     *  私有化无参构造     */    private BigDecimalUtil() {}    /**     *  用于浮点数的精确加法     *     * @param value1    浮点数1     * @param value2    浮点数2     * @return          浮点数1 + 浮点数2     */    public static double addUp(double value1, double value2) {        BigDecimal b1 = BigDecimal.valueOf(value1);        BigDecimal b2 = BigDecimal.valueOf(value2);        return b1.add(b2).doubleValue();    }    /**     *  精确减法     *     * @param value1    浮点数1     * @param value2    浮点数2     * @return          浮点数1 - 浮点数2     */    public static double subtract(double value1, double value2) {        BigDecimal b1 = BigDecimal.valueOf(value1);        BigDecimal b2 = BigDecimal.valueOf(value2);        return b1.subtract(b2).doubleValue();    }    /**     *  精确乘法     *     * @param value1    浮点数1     * @param value2    浮点数2     * @return          浮点数1 * 浮点数2     */    public static double multiply(double value1, double value2) {        BigDecimal b1 = BigDecimal.valueOf(value1);        BigDecimal b2 = BigDecimal.valueOf(value2);        return b1.multiply(b2).doubleValue();    }    /**     *  精确除法(使用默认精度)     *     * @param value1    浮点数1     * @param value2    浮点数2     * @return          浮点数1 / 浮点数2     */    public static double div(double value1, double value2) throws IllegalAccessException {        return div(value1, value2, DEFAULT_PRECISION);    }    /**     *  精确除法(指定精度)     *  四舍五入     *     * @param value1        浮点数1     * @param value2        浮点数2     * @param precision     精度     * @return              浮点数1 / 浮点数2     */    public static double div(double value1, double value2, int precision) throws IllegalAccessException {        if (precision &lt; 0) {            throw new IllegalAccessException(&quot;除法的精度不能小于0&quot;);        }        return div(value1, value2, precision, RoundingMode.HALF_UP);    }    /**     *  精确除法(指定精度)     *  指定舍入的策略     *     * @param value1        浮点数1     * @param value2        浮点数2     * @param precision     精度     * @param mode          舍入策略     * @return              浮点数1 / 浮点数2     */    public static double div(double value1, double value2, int precision, RoundingMode mode) throws IllegalAccessException {        if (precision &lt; 0) {            throw new IllegalAccessException(&quot;除法的精度不能小于0&quot;);        }        BigDecimal b1 = BigDecimal.valueOf(value1);        BigDecimal b2 = BigDecimal.valueOf(value2);        return b1.divide(b2, precision, mode).doubleValue();    }    }</code></pre><h1>断言工具类</h1><blockquote><p>hutool里也有很多断言的工具类，但是抛出的异常不是自己定义的，所以为了指定抛出的异常，需要自己在定义一个断言工具类。</p></blockquote><pre><code class="language-Java">import cn.hutool.core.util.StrUtil;import com.yww.management.common.exception.GlobalException;/** * &lt;p&gt; *      断言工具类 * &lt;/p&gt; * * @ClassName AssertUtils * @Author yww * @Date 2022/10/20 11:27 */public class AssertUtils {    /**     * 私有化无参构造器     */    private AssertUtils() {}    /**     * 如果对象为{@code null}, 则抛出异常     *     * @param object 要判断的对象     * @throws GlobalException  全局异常类     */    public static void notNull(Object object) throws GlobalException {        notNull(object, &quot;不能处理空对象&quot;);    }    /**     * 如果对象为{@code null}, 则抛出异常     *     * @param object  要判断的对象     * @param message 断言失败时的错误信息     * @throws GlobalException  全局异常类     */    public static void notNull(Object object, String message) throws GlobalException {        if (object == null) {            throw new GlobalException(message);        }    }    /**     * 如果字符串为{@code null}、空字符串或仅包含空白字符, 则抛出异常     *     * @param text 要进行检查的字符串     * @throws GlobalException  全局异常类     */    public static void hasText(String text) throws GlobalException {        hasText(text, &quot;此参数不能为空字符串&quot;);    }    /**     * 如果字符串为{@code null}、空字符串或仅包含空白字符, 则抛出异常     *     * @param text    要进行检查的字符串     * @param message 断言失败时的错误信息     * @throws GlobalException  全局异常类     */    public static void hasText(String text, String message) throws GlobalException {        if (StrUtil.isBlank(text)) {            throw new GlobalException(message);        }    }    }</code></pre><h1>ThreadLocal工具类</h1><pre><code class="language-Java">import java.util.HashMap;import java.util.Map;/** * &lt;p&gt; *      ThreadLocal工具类 * &lt;/p&gt; * * @ClassName ThreadLocalUtil * @Author yww * @Date 2022/10/21 14:27 */@SuppressWarnings(&quot;unchecked&quot;)public class ThreadLocalUtil {    /**     *  ThreadLocalMap     */    private static final ThreadLocal&lt;Map&lt;String, Object&gt;&gt; THREAD_LOCAL_MAP = ThreadLocal.withInitial(HashMap::new);    /**     * 私有化无参构造器     */    private ThreadLocalUtil() {}    /**     * 根据指定的key获取当前线程相关的变量值     *     * @param key 变量的key     * @param &lt;T&gt; 变量值的具体类型     * @return 若无此key对应的变量值, 则返回{@code null}     * @throws ClassCastException 若接收此返回值的变量类型与上下文保存的值的实际类型不匹配, 则抛出异常     */    public static &lt;T&gt; T get(String key) {        return (T) THREAD_LOCAL_MAP.get().get(key);    }    /**     * 根据指定的key获取当前线程相关的变量值, 若为{@code null}则返回指定的默认值     *     * @param key          变量的key     * @param defaultValue 默认值     * @param &lt;T&gt;          变量值的具体类型     * @return 若无此key对应的变量值, 则返回defaultValue     * @throws ClassCastException 若接收此返回值的变量类型与上下文保存的值的实际类型不匹配, 则抛出异常     */    public static &lt;T&gt; T get(String key, T defaultValue) {        T value = get(key);        return value == null ? defaultValue : value;    }    /**     * 设置线程相关上下文的变量值     *     * @param key   变量的key     * @param value 变量值     */    public static void set(String key, Object value) {        THREAD_LOCAL_MAP.get().put(key, value);    }    /**     * 删除指定key的变量     *     * @param key 变量的key     */    public static void remove(String key) {        THREAD_LOCAL_MAP.get().remove(key);    }    /**     * 清除当前线程相关的上下文     */    public static void close() {        THREAD_LOCAL_MAP.remove();    }}</code></pre><h1>分页工具类</h1><h2>封装分页请求对象</h2><blockquote><p>如果需要添加其他的属性筛选，只需要继承该封装对象即可。</p></blockquote><pre><code class="language-Java">import com.fasterxml.jackson.annotation.JsonIgnore;import com.fasterxml.jackson.annotation.JsonIgnoreProperties;/** * &lt;p&gt; *      分页查询请求对象 * &lt;/p&gt; * * @ClassName PageVo * @Author yww * @Date 2022/10/24 21:18 */@SuppressWarnings(&quot;all&quot;)@JsonIgnoreProperties(ignoreUnknown = true)public class PageVo {    /**     * 每页显示行数默认为10     */    private static final int DEFAULT_SIZE = 10;    /**     *  当前页码, 首页为1     */    private int page = 1;    /**     *  每页记录条数     */    private int size = DEFAULT_SIZE;    /**     *  排序字段名,asc,desc     */    private String sort;    /**     *  排序方向     */    private String dir;    public static PageVo of(int page, int size) {        PageVo pageReqVo = new PageVo();        pageReqVo.setPage(page);        pageReqVo.setSize(size);        return pageReqVo;    }    public PageVo() {}    @JsonIgnore    public int getOffset() {        return (getPage() - 1) * getSize();    }    public int getPage() {        return page &gt; 0 ? page : 1;    }    public void setPage(int page) {        this.page = page;    }    public int getSize() {        return size &gt; 0 ? size : DEFAULT_SIZE;    }    public void setSize(int size) {        this.size = size;    }    public String getSort() {        return sort;    }    public void setSort(String sort) {        this.sort = sort;    }    public String getDir() {        return dir;    }    public void setDir(String dir) {        this.dir = dir;    }}</code></pre><h2>分页结果类</h2><pre><code class="language-Java">import com.fasterxml.jackson.annotation.JsonPropertyOrder;import java.util.List;/** * &lt;p&gt; *      分页查询结果 * &lt;/p&gt; * * @ClassName PageResultVo * @Author yww * @Date 2022/10/24 21:15 */@JsonPropertyOrder({&quot;start&quot;, &quot;size&quot;, &quot;total&quot;, &quot;rows&quot;})public class PageResultVo&lt;T&gt; {    /**     *  本页记录在所有记录中的起始位置     */    private int start;    /**     *  每页记录条数     */    private int size;    /**     *  总记录条数     */    private int total;    /**     *  当前页数据     */    private List&lt;T&gt; rows;    public static &lt;E&gt; PageResultVo&lt;E&gt; ofReqVo(PageVo reqVo, List&lt;E&gt; rows, int total) {        PageResultVo&lt;E&gt; pageResultVo = new PageResultVo&lt;&gt;();        pageResultVo.setSize(reqVo.getSize());        pageResultVo.setStart(reqVo.getOffset());        pageResultVo.setTotal(total);        pageResultVo.setRows(rows);        return pageResultVo;    }    public PageResultVo() {    }    public int getStart() {        return start;    }    public void setStart(int start) {        this.start = start;    }    public int getSize() {        return size;    }    public void setSize(int size) {        this.size = size;    }    public int getTotal() {        return total;    }    public void setTotal(int total) {        this.total = total;    }    public List&lt;T&gt; getRows() {        return rows;    }    public void setRows(List&lt;T&gt; rows) {        this.rows = rows;    }}</code></pre><h2>分页工具类</h2><pre><code class="language-Java">import cn.hutool.core.bean.BeanUtil;import cn.hutool.core.util.StrUtil;import com.baomidou.mybatisplus.core.conditions.ISqlSegment;import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;import com.baomidou.mybatisplus.core.conditions.segments.MergeSegments;import com.baomidou.mybatisplus.core.conditions.segments.NormalSegmentList;import com.baomidou.mybatisplus.core.mapper.BaseMapper;import com.baomidou.mybatisplus.core.metadata.IPage;import com.baomidou.mybatisplus.core.toolkit.Wrappers;import com.baomidou.mybatisplus.extension.plugins.pagination.Page;import com.yww.management.common.exception.GlobalException;import java.util.ArrayList;import java.util.List;/** * &lt;p&gt; *      分页工具类 * &lt;/p&gt; * * @ClassName PageUtil * @Author yww * @Date 2022/10/24 21:21 */@SuppressWarnings(&quot;all&quot;)public class PageUtil {    /**     * mybatis-plus自带分页插件     *     * @param pageVo     * @param mapper     * @param clazz     * @param &lt;R&gt;     * @param &lt;Q&gt;     * @return     */    public static &lt;R, Q extends PageVo&gt; PageResultVo&lt;R&gt; paging(Q pageVo, BaseMapper&lt;R&gt; mapper, Class&lt;R&gt; clazz) {        R entity;        try {            entity = clazz.newInstance();        } catch (InstantiationException | IllegalAccessException e) {            throw new GlobalException(&quot;分页类型转换错误&quot;);        }        BeanUtil.copyProperties(pageVo, entity);        IPage&lt;R&gt; page = new Page&lt;&gt;(pageVo.getPage(), pageVo.getSize());        page = mapper.selectPage(page, Wrappers.lambdaQuery(entity));        return PageResultVo.ofReqVo(pageVo, page.getRecords(), Long.valueOf(page.getTotal()).intValue());    }    /**     * mybatis-plus自带分页插件     *     * @param pageVo     * @param mapper     * @param &lt;R&gt;     * @param &lt;Q&gt;     * @return     */    public static &lt;R, Q extends PageVo&gt; PageResultVo&lt;R&gt; paging(Q pageVo, BaseMapper&lt;R&gt; mapper, LambdaQueryWrapper&lt;R&gt; lambdaQuery, Class&lt;R&gt; clazz) {        R entity;        try {            entity = clazz.newInstance();        } catch (InstantiationException | IllegalAccessException e) {            throw new GlobalException(&quot;分页类型转换错误&quot;);        }        // 已设置在lambdaQuery的字段实体类中不应该重复设置        String[] whereFields = getWhereFields(lambdaQuery);        BeanUtil.copyProperties(pageVo, entity, whereFields);        lambdaQuery.setEntity(entity);        IPage&lt;R&gt; page = new Page&lt;&gt;(pageVo.getPage(), pageVo.getSize());        page = mapper.selectPage(page, lambdaQuery);        return PageResultVo.ofReqVo(pageVo, page.getRecords(), Long.valueOf(page.getTotal()).intValue());    }    /**     * 获取设置的条件字段名     *     * @param lambdaQuery     * @param &lt;R&gt;     * @return     */    public static &lt;R&gt; String[] getWhereFields(LambdaQueryWrapper&lt;R&gt; lambdaQuery) {        List&lt;String&gt; fields = new ArrayList&lt;&gt;();        MergeSegments expression = lambdaQuery.getExpression();        NormalSegmentList normal = expression.getNormal();        int index = 0;        for (ISqlSegment iSqlSegment : normal) {            if (index % 4 == 0) {                String field = StrUtil.toCamelCase(iSqlSegment.getSqlSegment());                fields.add(field);            }            index++;        }        return fields.toArray(new String[]{});    }}</code></pre><h1>文件工具类</h1><pre><code class="language-Java">package com.yww.filebackend.utils;import cn.hutool.core.util.IdUtil;import cn.hutool.core.util.StrUtil;import com.yww.filebackend.entity.SysFile;import lombok.extern.slf4j.Slf4j;import org.springframework.web.multipart.MultipartFile;import java.io.File;import java.io.IOException;import java.io.InputStream;import java.security.MessageDigest;import java.security.NoSuchAlgorithmException;import java.text.SimpleDateFormat;import java.util.Date;/** * &lt;p&gt; *      文件工具类 * &lt;/p&gt; * * @author yww */@Slf4jpublic class FileUtil {    /**     * 文件保存路径（需要注意）     */    private final static String FILE_SAVE_PATH = &quot;D:\\file&quot;;    /**     * 文件日期部分路径格式     */    public final static String DATE_FORMAT = &quot;/yyyy/MM/dd/&quot;;    /**     * windows下文件日期部分路径格式     */    private final static String DATE_FORMAT_WIN = &quot;\\yyyy\\MM\\dd\\&quot;;    /**     * 保存文件     *     * @param file  文件     * @return      文件信息     */    public static SysFile saveFile(MultipartFile file) {        String originalFilename = file.getOriginalFilename();        // 获取文件扩展名        String ext = getExtName(originalFilename);        // 生成新的文件名称        String newName = IdUtil.fastSimpleUUID() + &quot;.&quot; + ext;        String dateFormat = new SimpleDateFormat(getDateFormat()).format(new Date());        String path = FILE_SAVE_PATH + dateFormat + newName;        File dest = new File(path);        // 若是路径不存在就先创建父级目录        if (!dest.exists()) {            cn.hutool.core.io.FileUtil.touch(dest);        }        // 保存文件        try {            file.transferTo(dest);        } catch (IOException e) {            log.warn(&quot;保存文件出错，保存的文件名称为：&quot; + originalFilename);            log.warn(&quot;保存文件出错，保存的路径为：&quot; + path);            throw new RuntimeException(e);        }        return SysFile.builder()                .fileName(originalFilename)                .path(path)                .size(file.getSize())                .build();    }    /**     * 获取文件后缀     * 参考自hutool的FileUtil.extName()方法     *     * @param fileName  文件名称     * @return          文件后缀     */    public static String getExtName(String fileName) {        if (fileName == null) {            return null;        }        int index = fileName.lastIndexOf(&quot;.&quot;);        if (index == -1) {            return &quot;&quot;;        }        // 可能会有两个.符号        int secondToLastIndex = fileName.substring(0, index).lastIndexOf(&quot;.&quot;);        return fileName.substring(secondToLastIndex == -1 ? index + 1 : secondToLastIndex + 1);    }    /**     * 获取日期格式     * 由于操作系统不一样，所以可能会有不同文件路径格式     *     * @return  日期格式     */    public static String getDateFormat() {        String osName = System.getProperty(&quot;os.name&quot;).toLowerCase();        if (StrUtil.isNotBlank(osName) &amp;&amp; osName.contains(&quot;win&quot;)) {            return DATE_FORMAT_WIN;        }        return DATE_FORMAT;    }}</code></pre>]]></content:encoded>
            <category domain="https://yww52.com/categories/%25E6%2588%2591%25E7%259A%2584%25E6%258A%2580%25E6%259C%25AF%25E5%25AD%25A6%25E4%25B9%25A0%25E7%2589%25A9%25E8%25AF%25AD%25E6%259E%259C%25E7%2584%25B6%25E6%259C%2589%25E9%2597%25AE%25E9%25A2%2598/">我的技术学习物语果然有问题</category>
        </item>
        <item>
            <title>Shiro</title>
            <link>https://yww52.com/posts/c0010af8/</link>
            <guid isPermaLink="true">https://yww52.com/posts/c0010af8/</guid>
            <pubDate>Mon, 25 Jan 2021 00:00:00 +0000</pubDate>
            <description>Apache Shiro（发音为“ shee-roh”，日语为“ castle”）是一种功能强大且易于使用的Java安全框架，它执行身份验证，授权，加密和会话管理，可用于保护任何应用程序的安全-从命令行应用程序，移动应用程序到最大的Web和企业应用程序。</description>
            <content:encoded><![CDATA[<img src="https://img.yww52.com/2021/1/2021-1-25/top_img.jpg" alt="Shiro" style="max-width:100%;height:auto;margin-bottom:15px;" /><h1>简介</h1><p>网上最流行的两个Java安全框架就是Shiro和spring security。spring security功能更加强大，而Shiro是更加简单。这次就先来学习一下Shiro。首先我们来看看官网上对Shiro的描述。</p><blockquote><p><strong>Apache Shiro™</strong> is a powerful and easy-to-use Java security framework that performs authentication, authorization, cryptography, and session management. With Shiro’s easy-to-understand API, you can quickly and easily secure any application – from the smallest mobile applications to the largest web and enterprise applications.</p><p>Apache Shiro是一个功能强大且易于使用的Java安全框架，用于执行身份验证，授权，加密和会话管理。 使用Shiro易于理解的API，您可以快速轻松地保护任何应用程序-从最小的移动应用程序到最大的Web和企业应用程序。</p></blockquote><h1>核心架构</h1><p>在官网中的参考文档可以看到Shiro的框架架构图和一些解释。</p><p><img src="https://img.yww52.com/2021/1/2021-1-25/img1.png" alt=""></p><ul><li><p>Subject<code>Subject</code>表示需要认证的数据，可以理解为登陆的用户，一般有一个用户表示身份的信息和验证身份的密码，表示身份的信息一定要由唯一性。</p></li><li><p>Security Manager</p><p><code>Security Manager</code>是Shiro框架的核心，该对象会协调其内部的安全组件。</p></li><li><p>Realm</p><p>可以以称为域，充当Shiro和程序安全数据的桥梁，也就是匹配的数据，用来确定Subject登陆是否能够成功。</p></li></ul><blockquote><p>下面这张就是更加详细的架构图。</p></blockquote><p><img src="https://img.yww52.com/2021/1/2021-1-25/img2.png" alt=""></p><ol><li><p>上面的部分其实就是上图的<code>Subject</code>，可以Shiro是可以支持不同语言的服务的。</p></li><li><p>中间蓝色的就是框架的核心<code>Security Manager</code>。</p><ul><li><p>Authenticator（认证）</p><p>用来负责认证用户登陆是否成功。当用户尝试登陆时，会通过<code>Authenticator</code>去与单个或多个的<code>Realms</code>去验证该用户是否为合法用户。</p></li><li><p>Authenticator（授权）</p><p><code>Authenticator</code>是用来确定用户在应用程序中的访问控制。即表示该用户经过认证后，在程序中被允许做什么事情。</p></li><li><p>Session Manager</p><p>用来创建和管理用户<code>Session</code>的生命周期，管理会话。<strong>SessionDAO</strong>算是实现会话管理操作的接口。</p></li><li><p>Cache Manager</p><p>用来管理缓存，如果每次执行操作都要去认证授权，那会大幅降低效率，加入了缓存，那就可以不用每次都进行认证和授权了。</p></li></ul></li><li><p>Cryptography</p><p>翻译为密码术或者称密码学。其实就是用来加密和解密的。</p></li><li><p>下面的部分就为<code>Realms</code>。可以看到可以支持多种方式来验证用户的合法性。</p></li></ol><h1>认证</h1><p>从上面的架构图的各部分的功能可以了解了用户认证的过程。</p><ol><li><code>Subject</code>从应用程序过来寻求认证。</li><li>Shiro将<code>Subject</code>交给<code>Security Manager</code>去管理。</li><li>然后就会交给<code>Authenticator</code>去认证，<code>Authenticator</code>又会从<code>Realms</code>去获取安全数据与<code>Subject</code>去比较，确认<code>Subject</code>的合法性。</li></ol><p>现在就来简单的实现一下这个认证的流程。</p><h2>ini配置文件</h2><p>首先是创建一个简单的Maven项目，并导入依赖。</p><pre><code class="language-Java">    &lt;dependency&gt;      &lt;groupId&gt;org.apache.shiro&lt;/groupId&gt;      &lt;artifactId&gt;shiro-core&lt;/artifactId&gt;      &lt;version&gt;1.7.0&lt;/version&gt;    &lt;/dependency&gt;</code></pre><p>在resources文件夹下创建<code>shiro.ini</code>配置文件。</p><pre><code class="language-Java">[users]yww=1141</code></pre><p>这里就是用户的数据，用户名为<code>yww</code>，密码为<code>1141</code>。</p><p>然后就是实现了。</p><pre><code class="language-Java">package com.yww;import org.apache.shiro.SecurityUtils;import org.apache.shiro.authc.UsernamePasswordToken;import org.apache.shiro.mgt.DefaultSecurityManager;import org.apache.shiro.realm.text.IniRealm;import org.apache.shiro.subject.Subject;public class test {    public static void main(String[] args) {        // 1. 创建Security Manager对象        DefaultSecurityManager securityManager = new DefaultSecurityManager();        // 2. 给Security Manager对象设置Realms，这里就为resources文件夹下的shiro.ini配置文件        securityManager.setRealm(new IniRealm(&quot;classpath:shiro.ini&quot;));        // 3. 为SecurityUtils工具类绑定一个Security Manager对象        SecurityUtils.setSecurityManager(securityManager);        // 4. 使用工具类为Security Manager对象绑定一个Subject        Subject subject = SecurityUtils.getSubject();        // 5. 创建token，需要包括用户        UsernamePasswordToken token = new UsernamePasswordToken(&quot;yww&quot;,&quot;1141&quot;);        // 6. 用户登陆        try{            System.out.println(&quot;认证状态---&gt;&quot; + subject.isAuthenticated());            subject.login(token);            System.out.println(&quot;认证状态---&gt;&quot; + subject.isAuthenticated());        } catch (Exception e) {            e.printStackTrace();        }    }}</code></pre><pre><code class="language-运行结果">认证状态---false认证状态---true</code></pre><p>现在修改一下登陆用户的信息。</p><pre><code class="language-Java">        UsernamePasswordToken token = new UsernamePasswordToken(&quot;yw&quot;,&quot;1141&quot;);</code></pre><pre><code class="language-java">认证状态---falseorg.apache.shiro.authc.UnknownAccountException: Realm [org.apache.shiro.realm.text.IniRealm@573fd745] was unable to find account data for the submitted AuthenticationToken [org.apache.shiro.authc.UsernamePasswordToken - yw, rememberMe=false].at org.apache.shiro.authc.pam.ModularRealmAuthenticator.doSingleRealmAuthentication(ModularRealmAuthenticator.java:184)at org.apache.shiro.authc.pam.ModularRealmAuthenticator.doAuthenticate(ModularRealmAuthenticator.java:273)at org.apache.shiro.authc.AbstractAuthenticator.authenticate(AbstractAuthenticator.java:198)at org.apache.shiro.mgt.AuthenticatingSecurityManager.authenticate(AuthenticatingSecurityManager.java:106)at org.apache.shiro.mgt.DefaultSecurityManager.login(DefaultSecurityManager.java:275)at org.apache.shiro.subject.support.DelegatingSubject.login(DelegatingSubject.java:260)at com.yww.test.main(test.java:31)</code></pre><p>发现了这个<code>UnknownAccountException</code>异常，表示找不到该用户，即没有该用户。</p><p>在修改一下登陆用户的信息。</p><pre><code class="language-Java">        UsernamePasswordToken token = new UsernamePasswordToken(&quot;yww&quot;,&quot;555&quot;);</code></pre><pre><code class="language-java">认证状态---falseorg.apache.shiro.authc.IncorrectCredentialsException: Submitted credentials for token [org.apache.shiro.authc.UsernamePasswordToken - yww, rememberMe=false] did not match the expected credentials.at org.apache.shiro.realm.AuthenticatingRealm.assertCredentialsMatch(AuthenticatingRealm.java:603)at org.apache.shiro.realm.AuthenticatingRealm.getAuthenticationInfo(AuthenticatingRealm.java:581)at org.apache.shiro.authc.pam.ModularRealmAuthenticator.doSingleRealmAuthentication(ModularRealmAuthenticator.java:180)at org.apache.shiro.authc.pam.ModularRealmAuthenticator.doAuthenticate(ModularRealmAuthenticator.java:273)at org.apache.shiro.authc.AbstractAuthenticator.authenticate(AbstractAuthenticator.java:198)at org.apache.shiro.mgt.AuthenticatingSecurityManager.authenticate(AuthenticatingSecurityManager.java:106)at org.apache.shiro.mgt.DefaultSecurityManager.login(DefaultSecurityManager.java:275)at org.apache.shiro.subject.support.DelegatingSubject.login(DelegatingSubject.java:260)at com.yww.test.main(test.java:31)</code></pre><p>会发现另一个异常<code>IncorrectCredentialsException</code>，表示认证不通过，一般就是密码不正确。</p><p>常见的就是这两个异常了，现在完善一下认证的程序。</p><pre><code class="language-Java">package com.yww;import org.apache.shiro.SecurityUtils;import org.apache.shiro.authc.IncorrectCredentialsException;import org.apache.shiro.authc.UnknownAccountException;import org.apache.shiro.authc.UsernamePasswordToken;import org.apache.shiro.mgt.DefaultSecurityManager;import org.apache.shiro.realm.text.IniRealm;import org.apache.shiro.subject.Subject;public class test {    public static void main(String[] args) {        // 1. 创建securityManager对象        DefaultSecurityManager securityManager = new DefaultSecurityManager();        // 2. 给securityManager设置realm        securityManager.setRealm(new IniRealm(&quot;classpath:shiro.ini&quot;));        // 3. 为SecurityUtils工具类设置一个安全管理器        SecurityUtils.setSecurityManager(securityManager);        // 4. 设置subject主体        Subject subject = SecurityUtils.getSubject();        // 5. 创建令牌        UsernamePasswordToken token = new UsernamePasswordToken(&quot;yww&quot;,&quot;555&quot;);        // 6. 用户登陆        try{            System.out.println(&quot;认证状态---&quot; + subject.isAuthenticated());            subject.login(token);            System.out.println(&quot;认证状态---&quot; + subject.isAuthenticated());        } catch (IncorrectCredentialsException e) {            System.out.println(&quot;认证不通过，密码不正确&quot;);        } catch (UnknownAccountException e) {            System.out.println(&quot;认证不通过，该用户不存在&quot;);        } catch (Exception e) {            e.printStackTrace();        }    }}</code></pre><h2>自定义Realms</h2><p>导入依赖后，创建Realms的实现类，继承<code>AuthorizingRealm</code>,并重写其中的两个方法。</p><pre><code class="language-Java">public class CustomRealms extends AuthorizingRealm {    // 授权    @Override    protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principalCollection) {        return null;    }    // 认证    @Override    protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken authenticationToken) throws AuthenticationException {        // 1. 从token中获取subject的用户名        String principal = (String) authenticationToken.getPrincipal();        // 2. 通过用户名从数据库获取用户密码，这里使用假数据模拟        // 3. 比较用户名和密码        if (&quot;yww&quot;.equals(principal)) {            //第一个参数为需要认证的用户名，第二个是数据库存储的真正密码，第三个realm名字一般为this.getName()            SimpleAuthenticationInfo simpleAuthenticationInfo = new SimpleAuthenticationInfo(principal,&quot;555&quot;,this.getName());            return simpleAuthenticationInfo;        }        return null;    }}</code></pre><p>认证过程主要是<code>doGetAuthenticationInfo</code>这个方法，上面的授权方法先不重写。</p><p>然后是测试类。</p><pre><code class="language-Java">package com.yww;import org.apache.shiro.SecurityUtils;import org.apache.shiro.authc.IncorrectCredentialsException;import org.apache.shiro.authc.UnknownAccountException;import org.apache.shiro.authc.UsernamePasswordToken;import org.apache.shiro.mgt.DefaultSecurityManager;import org.apache.shiro.subject.Subject;public class test {    public static void main(String[] args) {        // 1. 创建securityManager对象        DefaultSecurityManager securityManager = new DefaultSecurityManager();        // 2. 给securityManager设置realm        securityManager.setRealm(new CustomRealms());        // 3. 为SecurityUtils工具类设置一个安全管理器        SecurityUtils.setSecurityManager(securityManager);        // 4. 设置subject主体        Subject subject = SecurityUtils.getSubject();        // 5. 创建令牌        UsernamePasswordToken token = new UsernamePasswordToken(&quot;yww&quot;, &quot;555&quot;);        // 6. 认证        try{            System.out.println(&quot;认证状态---&quot; + subject.isAuthenticated());            subject.login(token);            System.out.println(&quot;认证状态---&quot; + subject.isAuthenticated());        } catch (IncorrectCredentialsException e) {            System.out.println(&quot;认证不通过，密码不正确&quot;);        } catch (UnknownAccountException e) {            System.out.println(&quot;认证不通过，该用户不存在&quot;);        } catch (Exception e) {            e.printStackTrace();        }    }}</code></pre><h1>加密</h1><p>这里就使用到了三种加密方法，md5,salt和hash散列，具体是怎么加密的就不详细了解了，大概知道就好了。</p><ol><li>MD5信息摘要算法，一种被广泛使用的密码散列函数，可以产生出一个128位（16字节）的散列值，用于确保信息传输完整一致。</li><li>salt就是使用随机的字符串加到密码后进行md5加密，加的位置不知道也会让解密困难。</li><li>Hash算法可以将一个数据转换为一个标志，这个标志和源数据的每一个字节都有十分紧密的关系。Hash算法还具有一个特点，就是很难找到逆向规律。</li></ol><p>下面简单的测试下加密方法。</p><pre><code class="language-Java">public class Encryption {    public static void main(String[] args) {        // 假设密码为123456        String password = &quot;123456&quot;;        // 使用md5加密        Md5Hash md5Hash1 = new Md5Hash(password);        System.out.println(md5Hash1.toHex());        // 使用MD5 + salt加密，这里的salt默认加在前面        // 这里用固定字符串，用随机字符串更加安全，但要保存salt值        Md5Hash md5Hash2 = new Md5Hash(password,&quot;k*63&quot;);        System.out.println(md5Hash2.toHex());        // 使用MD5 + salt + hash散列加密        Md5Hash md5Hash3 = new Md5Hash(password, &quot;k*63&quot;, 1024);        System.out.println(md5Hash3.toHex());   }}</code></pre><pre><code class="language-运行结果">e10adc3949ba59abbe56e057f20f883ea57ba7884ad33d967db809552d96c6f6c9222f707633cc9a650286ca3bbadbe7</code></pre><p>接下来就来测试一下如何使用加密。</p><p>首先也是先创建自定义的realms。</p><pre><code class="language-Java">public class CustomRealms extends AuthorizingRealm {    // 授权    @Override    protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principalCollection) {        return null;    }    // 认证    @Override    protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken authenticationToken) throws AuthenticationException {        // 1. 从token中获取subject的用户名        String principal = (String) authenticationToken.getPrincipal();        // 2. 通过用户名从数据库获取用户密码，这里使用假数据模拟        // 3. 比较用户名和密码        if (&quot;yww&quot;.equals(principal)) {            SimpleAuthenticationInfo simpleAuthenticationInfo = new SimpleAuthenticationInfo(principal,&quot;555&quot;,this.getName());            return simpleAuthenticationInfo;        }        return null;    }}</code></pre><ol><li><p>解决MD5加密，先创建一个hash凭证匹配器，在将该匹配器给realms。</p><pre><code class="language-java">package com.yww;import org.apache.shiro.SecurityUtils;import org.apache.shiro.authc.IncorrectCredentialsException;import org.apache.shiro.authc.UnknownAccountException;import org.apache.shiro.authc.UsernamePasswordToken;import org.apache.shiro.authc.credential.HashedCredentialsMatcher;import org.apache.shiro.mgt.DefaultSecurityManager;import org.apache.shiro.subject.Subject;public class test {    public static void main(String[] args) {        // 1. 创建securityManager对象        DefaultSecurityManager securityManager = new DefaultSecurityManager();        // 2. 设置hash凭证匹配器        HashedCredentialsMatcher hashedCredentialsMatcher = new HashedCredentialsMatcher();        //     表示使用md5加密后在匹配认证        hashedCredentialsMatcher.setHashAlgorithmName(&quot;md5&quot;);        // 3. 给securityManager设置realm        CustomRealms realms = new CustomRealms();        //  为该realms设置凭证匹配器        realms.setCredentialsMatcher(hashedCredentialsMatcher);        securityManager.setRealm(realms);        // 4. 为SecurityUtils工具类设置一个安全管理器        SecurityUtils.setSecurityManager(securityManager);        // 5. 设置subject主体        Subject subject = SecurityUtils.getSubject();        // 6. 创建令牌        UsernamePasswordToken token = new UsernamePasswordToken(&quot;yww&quot;, &quot;555&quot;);        // 7. 认证        try{            System.out.println(&quot;认证状态---&quot; + subject.isAuthenticated());            subject.login(token);            System.out.println(&quot;认证状态---&quot; + subject.isAuthenticated());        } catch (IncorrectCredentialsException e) {            System.out.println(&quot;认证不通过，密码不正确&quot;);        } catch (UnknownAccountException e) {            System.out.println(&quot;认证不通过，该用户不存在&quot;);        } catch (Exception e) {            e.printStackTrace();        }    }}</code></pre></li><li><p>salt的处理，在匹配的时候加上salt就好。</p><pre><code class="language-Java">    // 认证    @Override    protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken authenticationToken) throws AuthenticationException {        // 1. 从token中获取subject的用户名        String principal = (String) authenticationToken.getPrincipal();        // 2. 通过用户名从数据库获取用户密码，这里使用假数据模拟        // 3. 比较用户名和密码        if (&quot;yww&quot;.equals(principal)) {            //    加上salt值，这里使用固定值，密码是md5+salt处理后的值            SimpleAuthenticationInfo simpleAuthenticationInfo =                 new SimpleAuthenticationInfo(principal,&quot;a57ba7884ad33d967db809552d96c6f6&quot;,                                             ByteSource.Util.bytes(&quot;k*63&quot;)                                             this.getName());            return simpleAuthenticationInfo;        }        return null;    }</code></pre></li><li><p>hash散列的处理，设置匹配器的时候加上散列的次数。</p><pre><code class="language-Java">package com.yww;import org.apache.shiro.SecurityUtils;import org.apache.shiro.authc.IncorrectCredentialsException;import org.apache.shiro.authc.UnknownAccountException;import org.apache.shiro.authc.UsernamePasswordToken;import org.apache.shiro.authc.credential.HashedCredentialsMatcher;import org.apache.shiro.mgt.DefaultSecurityManager;import org.apache.shiro.subject.Subject;public class test {    public static void main(String[] args) {        // 1. 创建securityManager对象        DefaultSecurityManager securityManager = new DefaultSecurityManager();        // 2. 设置hash凭证匹配器        HashedCredentialsMatcher hashedCredentialsMatcher = new HashedCredentialsMatcher();        // 表示使用md5加密后在匹配认证        hashedCredentialsMatcher.setHashAlgorithmName(&quot;md5&quot;);        // 表示散列多少次后在匹配认证        hashedCredentialsMatcher.setHashIterations(1024);        // 3. 给securityManager设置realm        CustomRealms realms = new CustomRealms();        //  为该realms设置凭证匹配器        realms.setCredentialsMatcher(hashedCredentialsMatcher);        securityManager.setRealm(realms);        // 4. 为SecurityUtils工具类设置一个安全管理器        SecurityUtils.setSecurityManager(securityManager);        // 5. 设置subject主体        Subject subject = SecurityUtils.getSubject();        // 6. 创建令牌        UsernamePasswordToken token = new UsernamePasswordToken(&quot;yww&quot;, &quot;555&quot;);        // 7. 认证        try{            System.out.println(&quot;认证状态---&quot; + subject.isAuthenticated());            subject.login(token);            System.out.println(&quot;认证状态---&quot; + subject.isAuthenticated());        } catch (IncorrectCredentialsException e) {            System.out.println(&quot;认证不通过，密码不正确&quot;);        } catch (UnknownAccountException e) {            System.out.println(&quot;认证不通过，该用户不存在&quot;);        } catch (Exception e) {            e.printStackTrace();        }    }}</code></pre></li></ol><h1>授权</h1><h2>对于角色的授权</h2><pre><code class="language-Java">    // 授权    @Override    protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principalCollection) {        // 获取主身份信息，这里即为用户名        String primaryPrincipal = (String)principalCollection.getPrimaryPrincipal();        // 根据身份信息获取当前用户的权限        SimpleAuthorizationInfo simpleAuthorizationInfo = new SimpleAuthorizationInfo();        // 将数据库中查询的角色信息赋值给权限对象，这里的admin为设定的角色，具体权限角色需要自定义        simpleAuthorizationInfo.addRole(&quot;admin&quot;);   // 添加admin的角色权限        return simpleAuthorizationInfo;    }</code></pre><pre><code class="language-Java">package com.yww;import org.apache.shiro.SecurityUtils;import org.apache.shiro.authc.IncorrectCredentialsException;import org.apache.shiro.authc.UnknownAccountException;import org.apache.shiro.authc.UsernamePasswordToken;import org.apache.shiro.authc.credential.HashedCredentialsMatcher;import org.apache.shiro.mgt.DefaultSecurityManager;import org.apache.shiro.subject.Subject;import java.util.Arrays;public class test {    public static void main(String[] args) {        // 1. 创建securityManager对象        DefaultSecurityManager securityManager = new DefaultSecurityManager();        // 2. 设置hash凭证匹配器        HashedCredentialsMatcher hashedCredentialsMatcher = new HashedCredentialsMatcher();        // 表示使用md5加密后在匹配认证        hashedCredentialsMatcher.setHashAlgorithmName(&quot;md5&quot;);        // 表示散列多少次后在匹配认证        hashedCredentialsMatcher.setHashIterations(1024);        // 3. 给securityManager设置realm        CustomRealms realms = new CustomRealms();        //  为该realms设置凭证匹配器        realms.setCredentialsMatcher(hashedCredentialsMatcher);        securityManager.setRealm(realms);        // 4. 为SecurityUtils工具类设置一个安全管理器        SecurityUtils.setSecurityManager(securityManager);        // 5. 设置subject主体        Subject subject = SecurityUtils.getSubject();        // 6. 创建令牌        UsernamePasswordToken token = new UsernamePasswordToken(&quot;yww&quot;, &quot;555&quot;);        // 7. 认证        try{            subject.login(token);        } catch (IncorrectCredentialsException e) {            System.out.println(&quot;认证不通过，密码不正确&quot;);        } catch (UnknownAccountException e) {            System.out.println(&quot;认证不通过，该用户不存在&quot;);        } catch (Exception e) {            e.printStackTrace();        }                // 认证通过，便进行授权操作，然后就进行判断权限        if(subject.isAuthenticated()) {            // 判断subject主体是否由admin权限，结果为true            System.out.println(subject.hasRole(&quot;admin&quot;));            // 判断subject主体是否同时具有admin和user权限，结果为false            System.out.println(subject.hasRoles(Arrays.asList(&quot;admin&quot;,&quot;user&quot;)));            // 判断主体具有的权限,结果集中为true和false            boolean[] booleans = subject.hasRoles(Arrays.asList(&quot;admin&quot;, &quot;user&quot;));        }    }}</code></pre><h2>对决资源的授权</h2><blockquote><p>这里涉及到权限字符串，大概了解一下字符串的写法和格式。</p><p>资源标识符:操作:资源类型</p><p>可以使用*号代表所有</p><p>admin:create:*</p><p>这个权限代表当前角色对admin中所有资源由创建的权限</p><p>admin:*:01</p><p>这个权限代表当前角色对admin中01用户存在所有权限</p></blockquote><pre><code class="language-Java">    // 授权    @Override    protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principalCollection) {        // 获取主身份信息，这里即为用户名        String primaryPrincipal = (String)principalCollection.getPrimaryPrincipal();        // 根据身份信息获取当前用户的权限        SimpleAuthorizationInfo simpleAuthorizationInfo = new SimpleAuthorizationInfo();        // 为该用户赋予对admin中的所有角色由创建权限        simpleAuthorizationInfo.addStringPermission(&quot;admin:create:*&quot;);        return simpleAuthorizationInfo;    }</code></pre><pre><code class="language-Java">package com.yww;import org.apache.shiro.SecurityUtils;import org.apache.shiro.authc.IncorrectCredentialsException;import org.apache.shiro.authc.UnknownAccountException;import org.apache.shiro.authc.UsernamePasswordToken;import org.apache.shiro.authc.credential.HashedCredentialsMatcher;import org.apache.shiro.mgt.DefaultSecurityManager;import org.apache.shiro.subject.Subject;public class test {    public static void main(String[] args) {        // 1. 创建securityManager对象        DefaultSecurityManager securityManager = new DefaultSecurityManager();        // 2. 设置hash凭证匹配器        HashedCredentialsMatcher hashedCredentialsMatcher = new HashedCredentialsMatcher();        // 表示使用md5加密后在匹配认证        hashedCredentialsMatcher.setHashAlgorithmName(&quot;md5&quot;);        // 表示散列多少次后在匹配认证        hashedCredentialsMatcher.setHashIterations(1024);        // 3. 给securityManager设置realm        CustomRealms realms = new CustomRealms();        //  为该realms设置凭证匹配器        realms.setCredentialsMatcher(hashedCredentialsMatcher);        securityManager.setRealm(realms);        // 4. 为SecurityUtils工具类设置一个安全管理器        SecurityUtils.setSecurityManager(securityManager);        // 5. 设置subject主体        Subject subject = SecurityUtils.getSubject();        // 6. 创建令牌        UsernamePasswordToken token = new UsernamePasswordToken(&quot;yww&quot;, &quot;555&quot;);        // 7. 认证        try{            subject.login(token);        } catch (IncorrectCredentialsException e) {            System.out.println(&quot;认证不通过，密码不正确&quot;);        } catch (UnknownAccountException e) {            System.out.println(&quot;认证不通过，该用户不存在&quot;);        } catch (Exception e) {            e.printStackTrace();        }        // 认证通过，便进行授权操作        if(subject.isAuthenticated()) {            // 判断当前主体是否对该资源存在权限,结果为true            System.out.println(subject.isPermitted(&quot;admin:create:*&quot;));            // 判断当前主体是否对该资源存在所有的权限,结果为false            System.out.println(subject.isPermittedAll(&quot;admin:create:*&quot;,&quot;admin:update:*&quot;));            // 判断当前主体分别存在哪些权限,结果为true和true            boolean[] booleans = subject.isPermitted(&quot;admin:create:*&quot;,&quot;admin:create:01&quot;);        }    }}</code></pre><h1>参考链接</h1><ul><li><a href="http://shiro.apache.org/">官网</a></li><li><a href="https://www.bilibili.com/video/BV1uz4y197Zm?p=1">shiro视频</a></li></ul>]]></content:encoded>
            <category domain="https://yww52.com/categories/%25E6%2588%2591%25E7%259A%2584%25E6%258A%2580%25E6%259C%25AF%25E5%25AD%25A6%25E4%25B9%25A0%25E7%2589%25A9%25E8%25AF%25AD%25E6%259E%259C%25E7%2584%25B6%25E6%259C%2589%25E9%2597%25AE%25E9%25A2%2598/">我的技术学习物语果然有问题</category>
        </item>
        <item>
            <title>单例模式</title>
            <link>https://yww52.com/posts/f1601c3e/</link>
            <guid isPermaLink="true">https://yww52.com/posts/f1601c3e/</guid>
            <pubDate>Sat, 23 Jan 2021 00:00:00 +0000</pubDate>
            <description>单例模式是设计模式中十分经典的一个模式。</description>
            <content:encoded><![CDATA[<img src="https://img.yww52.com/2021/1/2021-1-23top_img.jpg" alt="单例模式" style="max-width:100%;height:auto;margin-bottom:15px;" /><h1>前言</h1><p>单例模式是一个很简单的设计模式，当然这里的简单说的是理解起来很简单，其实单例模式中有很多细节值得我们去深究。</p><h1>什么是单例模式</h1><p>单例模式顾名思义就是<code>只有一个实例</code>。</p><p>每一个单例的类都只会存在一个实例，需要使用到该类时，就需要使用该实例，不得在创建另一个实例对象。</p><p>为什么会需要单例模式呢？每次使用类的时候就直接创建对象就好了，何必这么麻烦。</p><p>的确在大多数使用类的情况下，单例是没有必要的。但是在某些情况下单例还是很有必要的。</p><p>比如需要重复用到的类，就会不断重复创建实例，这个实例完全可以复用，但是还是重新创建了，这样是十分浪费系统资源和性能的。</p><p>再比如数据库的连接，如果每次使用都需要连接一次，使用完在断开连接，要是这样的开销完全大于一直连接，那为什么不一直连接直到程序结束呢？</p><p>所以单例模式在一些情况下显得十分必要。</p><h1>单例模式的实现</h1><p>单例模式实现的方式有很多种，大多都是围绕两个核心的问题来变化的。</p><ol><li>何时生成实例，也就是是否是懒加载</li><li>是否线程安全</li></ol><p>接下来就围绕这两个问题进行探讨。</p><h2>饿汉式</h2><p>首先是最简单的一种单例模式的实现方法。</p><pre><code class="language-Java">public class Singleton {      private static Singleton instance = new Singleton();        // 要使用实例，只能用该方法获取    public static Singleton getInstance() {      return instance;      }    // 重写构造方法，使其不能再外部创建实例    private Singleton (){}}</code></pre><p>这种方式很简单，这也就是网上提到的饿汉式的单例模式。</p><p>现在回到刚刚那两个核心问题来看看这个饿汉式的单例模式。</p><ol><li>这是线程安全的</li><li>不是懒加载的</li></ol><p>这种方式简单，其实就简答在一开始就创建了对象，所以就是线程安全的，多个对象用<code>getInstance</code>方法，都是调用同一个对象，不会出现线程安全的问题。</p><h2>懒汉式</h2><p>懒汉式其实就是使用了懒加载的。现在来看看最简单的一种写法。</p><pre><code class="language-Java">public class Singleton {    private static Singleton instance = null;    public static Singleton getInstance(){        if(instance == null) {            instance = new Singleton();        }        return instance;    }    private Singleton(){};}</code></pre><p>因为使用了懒加载，即直到要使用该类了才会创建实例，但是就是因为懒加载，所以出现了线程安全的问题。</p><p>这其实不难理解，单个线程该类不会出现任何的问题，但是在多线程环境下，当多个线程同时进入到判断语句，此时都是<code>null</code>，那就会导致多个线程会创建多个实例，那就不是只有一个实例了，这就出现了线程不安全的问题。</p><p>于是为了实现线程安全就可以改成下面这样。</p><pre><code class="language-Java">public class Singleton {    private static Singleton instance = null;    public static  synchronized Singleton getInstance(){        if(instance == null) {            instance = new Singleton();        }        return instance;    }    private Singleton(){};}</code></pre><p>直接在<code>getInstance</code>方法上增加<code>synchronized</code>关键字，就能保证只有一个方法进入到该线程，从而就能保证线程的安全。</p><h2>双检锁</h2><p>上面的线程安全的懒汉式已经解决了之前提到的两个问题，懒加载和线程安全。</p><p>但是在方法上增加<code>synchronized</code>，会显得加锁粒度太重，我们只需要在创建对象的时候加锁就好了，何必在方法上加锁呢？所以就能再次优化。</p><pre><code class="language-Java">public class Singleton {    private static Singleton instance = null;    public static  synchronized Singleton getInstance(){        if(instance == null) {            synchronized (Singleton.class) {                instance = new Singleton();            }        }        return instance;    }    private Singleton() {};}</code></pre><p>但仔细思考之后，就会发现这段代码虽然降低了加锁粒度，但是并没有保证线程安全，因为也是存在有多个线程同时通过了判断语句，每次封锁就只能封锁住创建的顺序而已，并不能保证只创建一个实例。为了解决这个问题，我们可以在为这段代码增加一个判断语句。</p><pre><code class="language-Java">public class Singleton {    private static Singleton instance = null;    public static Singleton getInstance(){        if(instance == null) {            synchronized (Singleton.class) {                if(instance == null) {                    instance = new Singleton();                }            }        }        return instance;    }    private Singleton(){};}</code></pre><p>这就是双检锁的单例模式了，这段代码看上去已经很完美了，但是这其实并没有实现线程安全。</p><p>因为<code>instance = new Singleton();</code>这个创建对象的语句并不是原子性的操作，这个语句在底层其实分成了三步。</p><ol><li>分配实例的内存地址</li><li>在该内存地址初始化实例对象</li><li>将<code>instance</code>的地址指向这个内存地址。</li></ol><p>之前我们学习多线程的时候有提到过指令重排。这三个指令经过优化可能会重排，会导致执行出现以下几种顺序</p><ul><li><p>123，正常顺序</p></li><li><p>132，结果与正常顺序一样的顺序</p></li></ul><p>这个不正常的顺序在单线程虽然结果与正常顺序一样，但是在多线就会出现一些线程不安全的问题。</p><p>比如存在两个线程A和B调用了<code>getInstance</code>方法，就会出现这种情况。当线程A进入到同步代码块，并通过了第二个判断语句，进入到创建实例的阶段，经过指令重排，执行132这个顺序，当执行完3时，B也到了第一个判断语句，此时<code>instance != null</code>，所以就会直接返回<code>instance</code>，但是线程A还没执行初始化对象，所以这个内存地址其实还是没有被使用的，这样线程B拿到的实例就会是没有被初始化的空的内存地址。</p><p>解决这个问题也很简单，之前有提到过<code>volatile</code>是可以禁止指令重排的，所以为该实例加上<code>volatile</code>关键字就好了，所以代码如下。</p><pre><code class="language-Java">public class Singleton {    private static volatile Singleton instance = null;    public static  synchronized Singleton getInstance(){        if(instance == null) {            synchronized (Singleton.class) {                if(instance == null) {                    instance = new Singleton();                }            }        }        return instance;    }    private Singleton(){};}</code></pre><p>这样即实现了懒加载，又实现了线程安全，而且封锁的粒度还小。</p><p>这种方法就一个缺点，写起来很麻烦，下面探讨一下更简单的写法。</p><h2>静态内部类</h2><pre><code class="language-Java">public class Singleton {    private static class SingletonHolder {        private static final Singleton INSTANCE = new Singleton();    }    public static final Singleton getInstance() {        return SingletonHolder.INSTANCE;    }    private Singleton() {}}</code></pre><p>这种单例模式就使用到了静态内部类了，它是可以实现懒加载和线程安全的。因为静态内部类在调用的时候才会加载，也就会在调用到的时候才会创建对象，从而实现懒加载。静态内部类在类加载机制中也会实现线程安全，所以这种方法是比较简单的实现这两个问题的。</p><h2>枚举</h2><p>上面提到的方法其实都还存在一个问题，可以被反射机制所破坏。</p><p>下面我就使用双检索的方式创建一个单例，看看反射机制是否能破环该单例模式。</p><pre><code class="language-Java">public class Test{    public static void main(String[] args) throws NoSuchMethodException, IllegalAccessException, InvocationTargetException, InstantiationException {        Constructor constructor = Singleton.class.getDeclaredConstructor();        constructor.setAccessible(true);        Singleton demo1 = (Singleton) constructor.newInstance();        Singleton demo2 = (Singleton) constructor.newInstance();        System.out.println(demo1);        System.out.println(demo2);        System.out.println(demo1.equals(demo2));    }}</code></pre><pre><code class="language-运行结果">com.yw.Singleton@1b6d3586com.yw.Singleton@4554617cfalse</code></pre><p>内存地址并不一样，这并不是同一个对象，所以可以看出反射机制是能够破坏上面的方法的。这种情况基本都是人为破坏的，所以不怎么需要考虑，不过实在要解决也是有方法的，那就是使用枚举。</p><pre><code class="language-Java">public enum SingletonEnum {    INSTANCE;}</code></pre><p>因为枚举是可以防止反射的，所以使用枚举可以防止别人使用反射，不过这样就和饿汉式差不多了，所以并不怎么推荐使用。</p><h1>总结</h1><p>因为反射破坏基本都是人为因素，所以不怎么需要考虑，所以单例模式一般使用双检锁或者是静态内部类的方法就好，如果懒加载并不是必须的，可以考虑使用饿汉式的单例模式。</p><p>:::note{type=&quot;info&quot;}</p><p>单例模式的使用需要经过认真思考，滥用单例模式就是造成内存泄漏的原因之一，所以要在项目中使用单例模式，就需要确定该实例一直存在的开销是否要比使用完就销毁的开销要大。</p><p>:::</p>]]></content:encoded>
            <category domain="https://yww52.com/categories/%25E6%2588%2591%25E7%259A%2584%25E6%258A%2580%25E6%259C%25AF%25E5%25AD%25A6%25E4%25B9%25A0%25E7%2589%25A9%25E8%25AF%25AD%25E6%259E%259C%25E7%2584%25B6%25E6%259C%2589%25E9%2597%25AE%25E9%25A2%2598/">我的技术学习物语果然有问题</category>
        </item>
        <item>
            <title>前后端分离demo</title>
            <link>https://yww52.com/posts/ee7e4b9/</link>
            <guid isPermaLink="true">https://yww52.com/posts/ee7e4b9/</guid>
            <pubDate>Wed, 20 Jan 2021 00:00:00 +0000</pubDate>
            <description>简单的体验一下前后端分离。</description>
            <content:encoded><![CDATA[<img src="https://img.yww52.com/2021/1/2021-1-20top_img.jpg" alt="前后端分离demo" style="max-width:100%;height:auto;margin-bottom:15px;" /><h1>开篇</h1><p>当下前后端分离的趋势很明显，的确这也是很有必要的。前后端分离让前端专注于前端，后端专注于后端，而不是哪一方都要学习前后端。学习过JSP的都知道，前后端之前都是套模板，这会让项目很繁琐，而且也提高了项目的耦合性，不利于二次开发，所以现在来通过一个简单的demo来简单的体验一下前后端分离。</p><h1>涉及技术</h1><ul><li><p>Vue</p></li><li><p>SprtingBoot</p></li><li><p>Mybatis-plus</p></li></ul><h1>准备数据</h1><p>这里的数据我就使用<code>Mybatis-plus</code>官方提供样例的数据了。</p><pre><code class="language-mysql">CREATE DATABASE IF NOT EXISTS Company;USE `Company`;DROP TABLE IF EXISTS user;CREATE TABLE user(id BIGINT(20) NOT NULL COMMENT &#39;主键ID&#39;,name VARCHAR(30) NULL DEFAULT NULL COMMENT &#39;姓名&#39;,age INT(11) NULL DEFAULT NULL COMMENT &#39;年龄&#39;,email VARCHAR(50) NULL DEFAULT NULL COMMENT &#39;邮箱&#39;,PRIMARY KEY (id));DELETE FROM user;INSERT INTO user (id, name, age, email) VALUES(1, &#39;Jone&#39;, 18, &#39;test1@baomidou.com&#39;),(2, &#39;Jack&#39;, 20, &#39;test2@baomidou.com&#39;),(3, &#39;Tom&#39;, 28, &#39;test3@baomidou.com&#39;),(4, &#39;Sandy&#39;, 21, &#39;test4@baomidou.com&#39;),(5, &#39;Billie&#39;, 24, &#39;test5@baomidou.com&#39;);</code></pre><h1>前端部分</h1><blockquote><p>前端就随便展示一下好了。</p></blockquote><ol><li><p>创建一个Vue的项目</p></li><li><p>在项目的<code>views</code>的文件夹下编写<code>test.vue</code>页面用来展示数据</p><pre><code>&lt;template&gt;  &lt;div&gt;      &lt;table&gt;          &lt;tr&gt;              &lt;td&gt;主键ID&lt;/td&gt;              &lt;td&gt;姓名&lt;/td&gt;              &lt;td&gt;年龄&lt;/td&gt;              &lt;td&gt;邮箱&lt;/td&gt;          &lt;/tr&gt;          &lt;tr v-for=&quot;item in user&quot; :key=&quot;item.id&quot;&gt;              &lt;td&gt;{{item.id}}&lt;/td&gt;              &lt;td&gt;{{item.name}}&lt;/td&gt;              &lt;td&gt;{{item.age}}&lt;/td&gt;              &lt;td&gt;{{item.email}}&lt;/td&gt;          &lt;/tr&gt;      &lt;/table&gt;  &lt;/div&gt;&lt;/template&gt;&lt;script&gt;export default {    name: &quot;test&quot;,    data(){        return{            user: [{                id : 1,                name : &#39;yww&#39;,                age : 10,                email : &#39;1141@qq.com&#39;            }]        }    },}&lt;/script&gt;&lt;style&gt;&lt;/style&gt;</code></pre><blockquote><p>前端开始主要注重页面，所以先用假数据。</p></blockquote></li><li><p>编写路由，在<code>router</code>的文件下编写index.js文件</p><pre><code class="language-js">import Vue from &#39;vue&#39;import VueRouter from &#39;vue-router&#39;import Home from &#39;../views/Home.vue&#39;Vue.use(VueRouter)const routes = [  {    path: &#39;/&#39;,    name: &#39;Home&#39;,    component: Home  },// 添加test的路由  {    path: &#39;/test&#39;,    name: &#39;Test&#39;,    component: () =&gt; import(&#39;../views/test.vue&#39;)  },  {    path: &#39;/about&#39;,    name: &#39;About&#39;,    // route level code-splitting    // this generates a separate chunk (about.[hash].js) for this route    // which is lazy-loaded when the route is visited.    component: () =&gt; import(/* webpackChunkName: &quot;about&quot; */ &#39;../views/About.vue&#39;)  }]const router = new VueRouter({  mode: &#39;history&#39;,  base: process.env.BASE_URL,  routes})export default router</code></pre></li><li><p>在页面上添加路由跳转，编写<code>App.vue</code></p><pre><code class="language-vue">&lt;template&gt;  &lt;div id=&quot;app&quot;&gt;    &lt;div id=&quot;nav&quot;&gt;      &lt;router-link to=&quot;/&quot;&gt;Home&lt;/router-link&gt; |      &lt;router-link to=&quot;/about&quot;&gt;About&lt;/router-link&gt; |      &lt;router-link to=&quot;/test&quot;&gt;Test&lt;/router-link&gt;    &lt;/div&gt;    &lt;router-view/&gt;  &lt;/div&gt;&lt;/template&gt;</code></pre></li><li><p>运行vue项目，查看<a href="http://localhost:8080/test">http://localhost:8080/test</a>是否出现假数据，出现则为成功</p></li></ol><h1>后端部分</h1><ol><li><p>首先先创建一个SpringBoot的项目，我的版本是<code>2.4.2</code>。</p></li><li><p>然后在<code>pom.xml</code>导入依赖</p><pre><code class="language-xml">&lt;dependencies&gt;        &lt;!--web启动器--&gt;        &lt;dependency&gt;            &lt;groupId&gt;org.springframework.boot&lt;/groupId&gt;            &lt;artifactId&gt;spring-boot-starter-web&lt;/artifactId&gt;        &lt;/dependency&gt;        &lt;!--数据库驱动--&gt;        &lt;dependency&gt;            &lt;groupId&gt;mysql&lt;/groupId&gt;            &lt;artifactId&gt;mysql-connector-java&lt;/artifactId&gt;            &lt;scope&gt;runtime&lt;/scope&gt;        &lt;/dependency&gt;        &lt;!--lombok--&gt;        &lt;dependency&gt;            &lt;groupId&gt;org.projectlombok&lt;/groupId&gt;            &lt;artifactId&gt;lombok&lt;/artifactId&gt;            &lt;optional&gt;true&lt;/optional&gt;        &lt;/dependency&gt;        &lt;!--mybatis-plus--&gt;        &lt;dependency&gt;            &lt;groupId&gt;com.baomidou&lt;/groupId&gt;            &lt;artifactId&gt;mybatis-plus-boot-starter&lt;/artifactId&gt;            &lt;version&gt;3.4.1&lt;/version&gt;        &lt;/dependency&gt;        &lt;!--单元测试--&gt;        &lt;dependency&gt;            &lt;groupId&gt;org.springframework.boot&lt;/groupId&gt;            &lt;artifactId&gt;spring-boot-starter-test&lt;/artifactId&gt;            &lt;scope&gt;test&lt;/scope&gt;        &lt;/dependency&gt;&lt;/dependencies&gt;</code></pre></li><li><p>创建一个实体类，该类在<code>pojo</code>的包下</p><pre><code class="language-Java">package com.yww.springboottest.pojo;import lombok.Data;@Datapublic class User {    private Long id;    private String name;    private Integer age;    private String email;}</code></pre></li><li><p>创建一个Mapper接口，该接口在<code>Mapper</code>的包下</p><pre><code class="language-Java">package com.yww.springboottest.Mapper;import com.baomidou.mybatisplus.core.mapper.BaseMapper;import com.yww.springboottest.pojo.User;import org.springframework.stereotype.Repository;// 这里只需要基础BaseMapper接口，泛型放入实体类就好了@Repositorypublic interface UserMapper extends BaseMapper&lt;User&gt; {}</code></pre></li><li><p>配置SpringBoot，这里使用的配置文件是<code>application.yaml</code></p><pre><code class="language-yaml"># 配置MySQLspring:  datasource:    username: root    password: password    url: jdbc:mysql://localhost:3306/Company?userUnicode=true&amp;characterEncoding=utf-8&amp;serverTimezone=UTC    driver-class-name: com.mysql.cj.jdbc.Driver# 配置日志，以便更好查看结果 mybatis-plus:  configuration:    log-impl: org.apache.ibatis.logging.stdout.StdOutImpl# 端口自定义，注意和Vue的端口分开，Vue使用了8080，这里就使用8081好了server:  port: 8081</code></pre></li><li><p>扫描注册Mapper接口</p><pre><code class="language-Java">package com.yww.springboottest;import org.mybatis.spring.annotation.MapperScan;import org.springframework.boot.SpringApplication;import org.springframework.boot.autoconfigure.SpringBootApplication;@SpringBootApplication@MapperScan(&quot;com.yww.springboottest.Mapper&quot;)public class SpringboottestApplication {    public static void main(String[] args) {        SpringApplication.run(SpringboottestApplication.class, args);    }</code></pre></li><li><p>查询测试</p><pre><code class="language-Java">package com.yww.springboottest;import com.yww.springboottest.Mapper.UserMapper;import com.yww.springboottest.pojo.User;import org.junit.jupiter.api.Test;import org.springframework.beans.factory.annotation.Autowired;import org.springframework.boot.test.context.SpringBootTest;import java.util.List;@SpringBootTestclass SpringboottestApplicationTests {    @Autowired    private UserMapper userMapper;    @Test    public void FindAll() {        List&lt;User&gt; userList = userMapper.selectList(null);        System.out.println(userList);    }}</code></pre></li><li><p>得到结果</p><pre><code>==&gt;  Preparing: SELECT id,name,age,email FROM user==&gt; Parameters: &lt;==    Columns: id, name, age, email&lt;==        Row: 1, Jone, 18, test1@baomidou.com&lt;==        Row: 2, Jack, 20, test2@baomidou.com&lt;==        Row: 3, Tom, 28, test3@baomidou.com&lt;==        Row: 4, Sandy, 21, test4@baomidou.com&lt;==        Row: 5, Billie, 24, test5@baomidou.com&lt;==        Row: 1327829723124920321, lili, 10, sssss&lt;==        Row: 1327839061415620609, lili, 10, sssss&lt;==        Row: 1327839257776095234, yww, 20, 1141950370@qq.com&lt;==      Total: 8</code></pre><p>由此可以看到查询结果没有什么问题，然后就可以写接口返回数据了。</p></li><li><p>编写接口，该接口在<code>Controller</code>的包下</p><pre><code class="language-Java">package com.yww.springboottest.Controller;import com.yww.springboottest.Mapper.UserMapper;import com.yww.springboottest.pojo.User;import org.springframework.beans.factory.annotation.Autowired;import org.springframework.web.bind.annotation.GetMapping;import org.springframework.web.bind.annotation.RestController;import java.util.List;@RestControllerpublic class UserController {    @Autowired    private UserMapper userMapper;    @GetMapping(&quot;/findall&quot;)    public List&lt;User&gt; FindAll() {        return userMapper.selectList(null);    }}</code></pre><p>访问<a href="http://localhost:8081/findall">http://localhost:8081/findall</a>即可查询到查询返回的数据。</p></li></ol><h1>前后端交互</h1><ol><li><p>在vue项目里添加<code>axios</code>插件，用来请求接口</p><pre><code class="language-bash">    vue add axios</code></pre></li><li><p>修改<code>test.vue</code>里的数据</p><pre><code class="language-Java">&lt;template&gt;  &lt;div&gt;      &lt;table&gt;          &lt;tr&gt;              &lt;td&gt;主键ID&lt;/td&gt;              &lt;td&gt;姓名&lt;/td&gt;              &lt;td&gt;年龄&lt;/td&gt;              &lt;td&gt;邮箱&lt;/td&gt;          &lt;/tr&gt;          &lt;tr v-for=&quot;item in user&quot; :key=&quot;item.id&quot;&gt;              &lt;td&gt;{{item.id}}&lt;/td&gt;              &lt;td&gt;{{item.name}}&lt;/td&gt;              &lt;td&gt;{{item.age}}&lt;/td&gt;              &lt;td&gt;{{item.email}}&lt;/td&gt;          &lt;/tr&gt;      &lt;/table&gt;  &lt;/div&gt;&lt;/template&gt;&lt;script&gt;export default {    name: &quot;test&quot;,    data(){        return{            user: []        }    },    created() {        //    请求端口数据，并将数据赋值给user数组        axios.get(&#39;http://localhost:8081/findall&#39;).then(response =&gt; (this.user = response.data))    }}&lt;/script&gt;&lt;style&gt;&lt;/style&gt;</code></pre></li><li><p>在后端部分增加一个工具类，用于解决跨域问题（因为是不同端口的请求），该类在<code>config</code>包下</p><pre><code class="language-Java">package com.yww.springboottest.config;import org.springframework.context.annotation.Configuration;import org.springframework.web.servlet.config.annotation.CorsRegistry;import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;@Configurationpublic class CrosConfig implements WebMvcConfigurer {    @Override    public void addCorsMappings(CorsRegistry registry) {        registry.addMapping(&quot;/**&quot;)                .allowCredentials(false)                .allowedMethods(&quot;POST&quot;, &quot;GET&quot;, &quot;PUT&quot;, &quot;OPTIONS&quot;, &quot;DELETE&quot;)                .allowedOrigins(&quot;*&quot;);    }}</code></pre></li><li><p>然后访问前端项目的页面<a href="http://localhost:8080/test">http://localhost:8080/test</a>，就能看到后端查询并返回的数据了</p></li></ol><h1>结尾</h1><p>这次是一个简单的demo，实际项目还要复杂很多，这个demo虽然简单，但是也是能让你简单了解一下前后端分离的开发流程。</p>]]></content:encoded>
            <category domain="https://yww52.com/categories/%25E6%2588%2591%25E7%259A%2584%25E6%258A%2580%25E6%259C%25AF%25E5%25AD%25A6%25E4%25B9%25A0%25E7%2589%25A9%25E8%25AF%25AD%25E6%259E%259C%25E7%2584%25B6%25E6%259C%2589%25E9%2597%25AE%25E9%25A2%2598/">我的技术学习物语果然有问题</category>
        </item>
        <item>
            <title>synchronized</title>
            <link>https://yww52.com/posts/edea11bc/</link>
            <guid isPermaLink="true">https://yww52.com/posts/edea11bc/</guid>
            <pubDate>Mon, 11 Jan 2021 00:00:00 +0000</pubDate>
            <description>synchronized是Java提供的一个关键字，是一种同步锁。</description>
            <content:encoded><![CDATA[<img src="https://img.yww52.com/2021/1/2021-1-11/top_img.jpg" alt="synchronized" style="max-width:100%;height:auto;margin-bottom:15px;" /><h1>应用方式</h1><p>首先要知道synchronized实现同步的基础为Java中每一个对象都可以作为锁，synchronized一般的使用方法有以下三个。</p><ol><li>在普通方法上加synchronized，为方法的对象加锁。</li><li>在静态方法上加synchronized，为该方法的类模板加锁。</li><li>同步方法块，为给定对象加锁。</li></ol><p>每一个线程执行到同步块或者是同步方法（对共享内存进行访问的程序片段称为临界区），就会去申请获得锁。当同步的操作执行完或者是抛出异常时就会把该锁给释放掉。</p><h1>简单应用</h1><p>之前的volatile文章有提过，volatile是不保证原子性的，现在就用synchronized来保证那个例子的原子性。</p><pre><code class="language-Java">package com.yw;import java.util.concurrent.CountDownLatch;public class Test{    public static void main(String[] args) throws InterruptedException {        Data data = new Data();        //用来辅助确保二十个线程全部执行完毕        CountDownLatch countDownLatch = new CountDownLatch(20);        for (int i = 0; i &lt; 20; i++) {            new Thread(() -&gt; {                //设置次数要多，太少结果不明显                for (int j = 0; j &lt; 1000; j++) {                    data.addData();                }                countDownLatch.countDown();            }).start();        }        countDownLatch.await();        System.out.println(&quot;当前资源类的值为-----&gt;&quot; + data.datanum);    }}class Data {    int datanum = 0;    // 为该方法加锁，锁的是对象    public synchronized void addData() {        datanum++;    }    //与上面等价，锁的是this对象    public  void addData1() {        synchronized(this) {            datanum++;        }    }}</code></pre><p>由此可以看见测试了很多次，答案都是20000，从而用synchronized可以保证了这个例子的原子性。下面我用图来简单讲解一下为什么synchronized能够保持这个例子的原子性。</p><p><img src="https://img.yww52.com/2021/1/2021-1-11/img1.png" alt=""></p><h1>Monitor</h1><p>介绍synchronized的原理之前先了解一个重要的概念，Monitor。</p><p>Monitor又称监视器，是一种同步机制，synchronized就是通过这个机制来实现方法或者代码块的同步的。</p><blockquote><p>实际上任何一个Java对象都可以作为Monitor机制的monitor object，每一个Object的类都在底层实现了ObjectMonitor模式（这是底层用C来实现的），我们一般看不到，只要知道每一个Object类的对象都可以作为monitor对象</p></blockquote><p>同步代码块会在代码块开始位置插入一条<code>monitorenter</code>在代码块结尾插入一条<code>monitorexit</code>，用这两条指令来实现Monitor机制。</p><p>同步方法是通过一个<code>ACC_SYNCHRONIZED</code>标志来确定，执行方法时判断有无该标志位，然后会隐式调用上面两条指令。</p><p>起初在没有线程执行到临界区时，monitor的大概情况是这样子的。</p><p><img src="https://img.yww52.com/2021/1/2021-1-11/img2.png" alt=""></p><p>当有一个线程进入临界区，Monitor中的Owner就会指向该线程。</p><p><img src="https://img.yww52.com/2021/1/2021-1-11/img3.png" alt=""></p><p>此时要是有其他线程也执行到了临界区的范围，就会进入Enter Set，即将线程的状态转变为阻塞状态，故Enter Set也可以称为阻塞队列。</p><p><img src="https://img.yww52.com/2021/1/2021-1-11/img4.png" alt=""></p><p>当Owner的线程1离开了临界区就会出现三种情况。</p><ol><li><p>线程1因某些原因发生了<code>wait</code>等待，Owner恢复为null，并进入<code>Wait set</code>,然后阻塞队列中的线程开始竞争Owner的位置。（这里假设线程2竞争到了）</p><p><img src="https://img.yww52.com/2021/1/2021-1-11/img5.png" alt=""></p></li><li><p>线程1正常离开临界区，Owner恢复为null，线程1之后不再进入临界区，阻塞队列中的线程就会开始竞争Owner的位置。（这里假设线程2竞争到了）</p><p><img src="https://img.yww52.com/2021/1/2021-1-11/img6.png" alt=""></p></li><li><p>线程1正常离开临界区，Owner恢复为null，线程1之后还会进入临界区，那线程1就会与阻塞队列中的线程竞争Owner的位置。（这里假设线程2竞争到了）</p><p><img src="https://img.yww52.com/2021/1/2021-1-11/img7.png" alt=""></p></li></ol><p>:::note{type=&quot;info&quot;}</p><p>Monitor是synchronized实现线程同步的原理（当初只有重量级锁，锁的优化靠其他的机制）。   从图中也可以看出为什么wait这些方法要放在synchronized中使用，因为都需要借助到Monitor的机制来进行实现。</p><p>:::</p><h1>Java对象头</h1><p>Java对象头是存储对象的一些基础信息的集合。</p><table><thead><tr><th align="center">长度</th><th align="center">内容</th><th align="center">说明</th></tr></thead><tbody><tr><td align="center">32bit</td><td align="center">Mark Word</td><td align="center">存储对象的hashCode或锁信息等</td></tr><tr><td align="center">32bit</td><td align="center">Class Metadata Address</td><td align="center">存储到对象类型数据的指针</td></tr><tr><td align="center">32bit</td><td align="center">Arraylength</td><td align="center">数组的长度（要是对象不是数组则没有该项）</td></tr></tbody></table><p>这里就不详细探讨后面两个了，这里的重点是<code>Mark Word</code>，现在我们来看看这里面具体存储了什么。</p><table style="text-align:center;vertical-align:middle;"><tr>    <th rowspan="2">锁状态</th>    <th colspan="2">25bit</th>      <th rowspan="2">4bit</th>     <th>1bit</th>     <th>2bit</th></tr><tr>    <th>23bit</th>     <th>2bit</th>     <th>是否是偏向锁</th>    <th>锁的标志位</th></tr><tr>     <td>无锁</td>    <td colspan="2">对象的hashCode</td>    <td>分代年龄</td>    <td>0</td>    <td>01</td></tr><tr>     <td>偏向锁</td>    <td>线程ID</td>    <td>Epoch</td>    <td>对象分代年龄</td>    <td>1</td>    <td>01</td></tr><tr>     <td>轻量级锁</td>    <td colspan="4">指向栈中锁记录的指针</td>    <td>00</td></tr><tr>     <td>重量级锁</td>    <td colspan="4">指向互斥量（重量级锁）的指针</td>    <td>10</td></tr><tr>    <td>GC标志</td>    <td colspan="4">空</td>    <td>11</td></tr></table><p><img src="https://img.yww52.com/2021/1/2021-1-11/img8.png" alt=""></p><p>在JDK1.6之前，synchronized只是一个重量级的锁，当JDK1.6进行更新之后，引入了偏向锁和轻量级锁的概念，所以锁的状态变成了以上的四种。</p><h1>偏向锁</h1><p>偏向锁是JDK1.6之后引进的，HotSpot作者发现大多数情况锁不仅存在多线程竞争，而且总是由同一线程多次获得。如果是之前，该线程每次都要获得锁，这样代价也未免太大了一点，所以为了优化这种情况，就出现了偏向锁。</p><p>偏向锁，顾名思义，偏爱某个线程的锁。当一个线程访问同步块并获取锁，会在对象头和栈帧中的锁记录里存储偏向的线程ID。以后该线程再次来获取锁，就只需测试一下对象头的Mark Word里是否存储着指向当前线程的偏向锁就好了，不必再次进行CAS来操作加锁和解锁。</p><blockquote><ol><li>偏向锁在Java6中是默认开启的，但是要在应用程序启动几秒之后才会激活，也就是说是有延迟的，有必要也可以通过设置参数来设置延迟 -XX:BiasedLockingStartupDelay=0</li><li>当不希望使用偏向锁，也可以通过设置参数来关闭,默认进入轻量级锁 -XX:-UseBiasedLocking=false</li></ol></blockquote><h1>参考资料</h1><ul><li><a href="https://book.douban.com/subject/26591326/">Java并发编程的艺术</a></li><li><a href="https://juejin.cn/post/6844903640197513230#heading-6">https://juejin.cn/post/6844903640197513230#heading-6</a></li><li><a href="https://segmentfault.com/a/1190000016417017">https://segmentfault.com/a/1190000016417017</a></li></ul>]]></content:encoded>
            <category domain="https://yww52.com/categories/%25E6%2588%2591%25E7%259A%2584%25E6%258A%2580%25E6%259C%25AF%25E5%25AD%25A6%25E4%25B9%25A0%25E7%2589%25A9%25E8%25AF%25AD%25E6%259E%259C%25E7%2584%25B6%25E6%259C%2589%25E9%2597%25AE%25E9%25A2%2598/">我的技术学习物语果然有问题</category>
        </item>
        <item>
            <title>2020年度总结</title>
            <link>https://yww52.com/posts/276e22e/</link>
            <guid isPermaLink="true">https://yww52.com/posts/276e22e/</guid>
            <pubDate>Fri, 08 Jan 2021 00:00:00 +0000</pubDate>
            <description>2020年度总结</description>
            <content:encoded><![CDATA[<img src="https://img.yww52.com/2020/2020%25E5%25B9%25B4%25E5%25BA%25A6%25E6%2580%25BB%25E7%25BB%2593/top_img.jpg" alt="2020年度总结" style="max-width:100%;height:auto;margin-bottom:15px;" /><h1>开篇</h1><p>刚刚结束了地狱级别难度的期末考试，终于有时间来写这篇总结（不是真的想鸽）。</p><p>为什么之前没有写？</p><p>那是因为自己真的没什么可以说的（其实是太混了，虽然现在也很混）。</p><h1>疫情</h1><p>2020年一开头就不太平，爆发了疫情。新冠的爆发让我被迫在家呆了好几个月，我到现在都记得刚刚爆发时的情景，第一次感觉到生命是如此的脆弱。</p><p>不仅如此，新冠的爆发也让我看到了很多魔幻的事情，网上的事情我就不说了，大家懂的都懂，说说我碰上的。</p><p>稍微了解我的都知道我家是开药店的，疫情的爆发我相对很多人都更有体会，全镇都勒令关门禁止营业，除了药店，因为还要提供一些药物和口罩。虽说我们这边没有病例，可依旧是一件冒风险的事情。即使这样还会有人向政府举报我们家高价卖口罩（经过政府调查其实并没有），真的是让人心寒。</p><p>只能说医疗行业真的不容易。这其实也是为什么全家都叫我学医我还是选择学了计算机的原因（虽然最后进了数学院，有点遗憾）。</p><p>到现在疫情都还没有完全结束，总有死灰复燃的苗头，不过还好已经好转很多了，相信一切都会好起来的。</p><h1>游戏</h1><h2>PC端</h2><p>PC端我就只玩一款游戏，那就是LOL。这款游戏从我小学六年级一直陪伴我到现在，想想也是玩了好多年了。</p><p>不过今年玩的也少很多了，一部分是因为学习上的压力，另一方面就是网络风气。LOL的风气懂得都懂，其实每个游戏都这样，不过这游戏玩家多，现象超级明显，对新人极不友好。</p><h2>移动端</h2><p>移动端我就玩的还算多了，毕竟比较方便。</p><p>不过也只是玩了两款，那就是明日方舟和公主连结。</p><p>不过现在方舟也不玩了，虽然是一个好游戏，但是随着我学习上压力的递增，我也没有这么多时间来去通关了（想到第一次的危机合约，能玩上一天），到后面就都是抄作业了，时间久了就感受不到方舟的乐趣了，我也没有精力一起玩两款游戏，于是我就卸了（所以也没有截图了）。</p><p>说到公主连结就精神了，这游戏更加休闲，纯属一个养花瓶游戏，每天十分钟完成日常就可以了，十分适合我这些懒得玩的人，而且角色多好看啊（看看这个万圣忍，这不可爱吗？）。</p><p><img src="https://img.yww52.com/2020/2020%E5%B9%B4%E5%BA%A6%E6%80%BB%E7%BB%93/img1.jpg" alt=""></p><p>这游戏最大的内容就是公会战（虽然公会战总是在摸鱼，但有时间还是会很努力的打的），因此我还认识了很多的志同道合的朋友，十分感谢各位的陪伴。</p><p><img src="https://img.yww52.com/2020/2020%E5%B9%B4%E5%BA%A6%E6%80%BB%E7%BB%93/img2.jpg" alt=""></p><h1>学习</h1><h2>专业学习</h2><p>因为精力总不花在专业学习上，导致学分绩也就排在中游。（老混子了）</p><p>身为数学院的学子没学好数学还是挺惭愧的，以后有空研究深度学习了在认真看一下。（老师也都是研究深度学习的，开发在数学院地位低下hh）</p><h2>开发</h2><p>这方面其实也没什么说的了，直接看<a href="https://github.com/whyneh?tab=overview&from=2020-12-01&to=2020-12-31">github</a>吧（不要问什么有两段时间空了，在家是真的无法学习，以后要好好改正了）。</p><p><img src="https://img.yww52.com/2020/2020%E5%B9%B4%E5%BA%A6%E6%80%BB%E7%BB%93/img3.png" alt=""></p><p>20年也没什么成就，花了挺多时间在这个博客上了。</p><p>我从大一下学期就接触了hexo，也就那时候就搭建好了这个博客，当时还是用的是next主题，什么都没整明白。</p><p>不过虽然搭建好了，但是没怎么管理，文章也就没怎么写，当时是觉得反正没什么人看，没怎么有动力弄得更好。</p><p>今年我就改变了这个想法。毕竟这写的文章不给别人看也是要给自己看的，所以要尽量写好，而且弄得更顺眼。</p><p>搭建这个博客的确是很能提高能力的一件事情，当然是尽量完善之后。简单说说搭建这个博客学到了什么。</p><ol><li>hexo与github pages（coding等）</li><li>域名解析，也因此买了自己的域名，并备了案</li><li>html + css + javascript三件套，算是加深一点印像吧</li><li>github action等一系列技术</li><li>使用CDN来加速自己的网站，怎样进行云存储</li><li>很多网络和排错的知识，比如Chrome的F12中的Network。</li><li>Linux基本命令和使用一些基本的服务器软件，比如什么Nginx等</li></ol><p>不得不说开发真的是一步步上云，上云是必然的趋势了。</p><h2>读书方面</h2><p>20年下半年我又重新拾起了书本，因为之前我实在看不下书，一看就困。但是书上会有很多知识比网上的资料要详细（因为又很多书都很经典），只有读书才能学到更多的东西，有很多书还是很有趣的。让我想想我20年看过什么书（当然除去课本）。</p><ul><li>网络是怎样连接的</li><li>C++ Primer Plus</li><li>鸟哥的私房菜</li><li>小灰的算法之旅</li><li>Java并发编程艺术</li><li>大话数据结构</li><li>哈喽算法</li></ul><p>说了这么多其实有些没完全读完，就别如那本C++ Primer Plus，有这本书的都懂，而且我学的Java，有时间在看吧。</p><p>毕竟是从这个学期才开始认真好好读点书，所以看起来很少，21年尽量会看多一点的。</p><h1>身体</h1><p>啊这 。这个其实也就老样子了，依旧的不健康作息。</p><p>特别是前两三周，每天复习到凌晨，以后需要改了。</p><p>还有就是运动方面，不是不想跑步，因为疫情，好不容易来到学校前期又太忙后期风吹的我脑袋疼（找借口还是有一手的）。</p><p>不行了，真的要好好锻炼了，不然整天都是没精神的。</p><h1>总结</h1><p>20年懒散了，21年就必须更加努力才行了。说到努力，其实看到很多网上和现实的大佬，真的是强的人还很努力，对比下自己就完爆了。</p><p>的确人不能一直对一件事情保持热情（可能只是我不能吧），所以总会有些时候会丧失对学习的热情，总会颓废几天，不过之后还是要努力学习啊。</p><p>想到了DIYgod的签名，写代码是热爱，写到世界充满爱。这可能就是强者吧，越学习越热爱，我什么时候也能这么强。</p><p>emm,不说这么多了，21年也希望各位能变得更好吧，毕竟21年对于我们这一届来说还是很重要的，送各位一句话，也是小米的标语，永远相信美好的事情即将发生。</p>]]></content:encoded>
            <category domain="https://yww52.com/categories/%25E4%25B8%25AA%25E4%25BA%25BA%25E6%2580%25BB%25E7%25BB%2593/">个人总结</category>
        </item>
        <item>
            <title>volatile</title>
            <link>https://yww52.com/posts/fe4c9cb6/</link>
            <guid isPermaLink="true">https://yww52.com/posts/fe4c9cb6/</guid>
            <pubDate>Fri, 08 Jan 2021 00:00:00 +0000</pubDate>
            <description>volatile是Java提供的一个关键字，是一个轻量级的同步机制。</description>
            <content:encoded><![CDATA[<img src="https://img.yww52.com/2021/1/2021-1-8/top_img.jpg" alt="volatile" style="max-width:100%;height:auto;margin-bottom:15px;" /><h1>什么是volatile</h1><p>volatile是Java提供的一个关键字，是一种轻量级的同步方式，也可以理解为轻量级的synchronized。</p><p>:::note{type=&quot;error&quot;}</p><p>码出高效Java开发手册中是这样说道。  volatile是轻量级的同步方式，这种说法是错误的。它只是轻量级的线程操作可见方式，并非同步方式，如果是多写场景，一定会产生线程安全问题。</br>  这里的错误就不修改了，以便提醒自己。</p><p>:::</p><h1>Java内存结构</h1><p>Java的内存结构其实说的是JVM的运行时数据区。</p><p><img src="https://img.yww52.com/2021/1/2021-1-8/img1.png" alt=""></p><blockquote><p>绿色代表线程私有的内存区域，紫色的代表所有线程共享的内存区域。</p></blockquote><p>Java内存结构在这里就不展开详细说了，这算是JVM的内容了，这里提前主要是提醒一下，不要与下面学习的Java的内存模型混淆，这两个概念说的并不是同一个东西。</p><h1>JMM</h1><p>JMM，全称为<code>Java Memory Model</code>，Java内存模型。</p><p>Java内存模型是一组规范或者说是规则，每个线程执行都要遵循这个规范，是用来解决在线程的通信问题的。</p><p>JMM是一种规范，是一个抽象的概念，并不真实存在，内存结构才是真实存在的。</p><p>在讲解JMM之前先要理解两个概念，主内存和工作内存。</p><blockquote><p>《Java虚拟机规范》试图这样进行定义Java内存模型这个概念。</p><p>Java内存模型是用来屏蔽各种硬件和操作系统的内存访问的差异，以实现让Java程序在各种平台下都能达到一致的内存访问效果。</p></blockquote><h2>主内存</h2><p>主内存是Java运行时在计算机存储数据的地方，是所有线程共享的，同时多个线程对这个主内存进行修改，就会出现很多的问题，这就是并发操作的问题，需要我们去解决。</p><h2>工作内存</h2><p>每个线程都有一个存储数据的地方，用来存储线程需要操作的数据，为什么要这样呢？</p><p>因为线程是不能直接对主内存中的数据进行修改的，只能修改线程工作内存中的数据，所以线程修改主内存中的数据时就会将主内存中的数据保存在自己的工作内存，然后在进行操作。</p><p>这样就会存在一个问题，每个线程都会对自己的工作内存进行操作，所以每一个线程都无法得知其他线程工作内存中的数据是怎么样的，这就是一个可见性的问题。</p><h2>JMM的抽象结构</h2><p><img src="https://img.yww52.com/2021/1/2021-1-8/img2.png" alt=""></p><p>以下是JMM对主内存数据操作时会执行的八个操作。（按顺序）</p><ul><li><p>lock将主内存中的数据变量标识为线程独占状态，即对该变量进行加锁操作，其他线程不能对其操作。</p></li><li><p>read读取主内存中需要修改的变量，即上个经过加锁操作的变量。</p></li><li><p>load</p><p>将读取到的数据变量载入到线程的工作内存之中。</p></li><li><p>use</p><p>把工作内存中的变量传输给执行引擎，即对该变量进行操作。</p></li><li><p>assign执行引擎对变量进行操作之后，将得到的变量的值放回工作内存。</p></li><li><p>store将线程工作内存中的变量存储好。</p></li><li><p>write将上述存储的变量写入主内存，实现刷新主内存中的值。</p></li><li><p>unlock将该变量的锁释放，使其能让其他线程进行操作。</p></li></ul><p>:::note{type=&quot;info&quot;}</p><p>Java内存模型的结构大概率是参考了CPU基于高速缓存的存储交互的设计。</br>  为了提高CPU的效率，添加了高速缓存的概念，用于解决处理器总是等待缓慢的内存读写的问题。在Java内存模型中，工作内存其实就像是一层高速缓存。</p><p>:::</p><h2>JMM的三个特性</h2><p>Java内存模型就是为了解决对共享数据中的可见性，原子性和有序性问题的一组规则。</p><p>即JMM的存在就是为了 保证这三个特性，现在具体来看看这三个特性。</p><h3>可见性</h3><p>可见性刚刚也讲工作内存的时候也是有提到的，这个其实很好理解，每个线程中的工作内存经过修改写回主内存之后，其他线程都可以看见主内存中的值发生变化，从而解决一些缓存不一致的情况。</p><h3>原子性</h3><p>原子性表示一个操作在执行中是不可以被中断的，有点类似事务的原子性，要么成功完成，要么直接失败。</p><h3>有序性</h3><p>有序性表示JMM会保证操作是有序执行的。或许有人会感到疑惑，难道程序不都是有序执行的吗？</p><p>这就要说到处理器的指令重排了，这涉及到了一些汇编的知识，所以不怎么展开了，大概了解一下。</p><p>为了提高CPU的使用率，在程序编译执行的时候，处理器会将指令进行重排优化，一般分为以下三个。</p><ol><li>编译器优化的重排</li><li>指令并行的重排</li><li>内存系统的重排</li></ol><p>指令重排使得语句不一定是按从上到下执行的，可能会是乱序执行的，有些语句是存在数据依赖性的才会保持前后顺序。</p><p>为什么单线程的时候没有感觉呢？这是因为指令重排不会干扰到单线程执行的结果的，但是在多线程中乱序执行就会出现一些问题，导致得到的结果不一样。</p><h1>volatile的特点</h1><p>根据上述JMM的三个特性来说说volatile的特点。</p><ol><li>保证可见性</li><li>不保证原子性</li><li>禁止指令重排</li></ol><p>下面就仔细说说volatile的特点。</p><h2>保证可见性</h2><p>volatile是可以保证可见性的，现在来验证一下volatile的可见性。</p><pre><code class="language-Java">package com.yw;import java.util.concurrent.TimeUnit;public class Test{    public static void main(String[] args)  {        Data data = new Data();System.out.println(Thread.currentThread().getName() + &quot;线程开启&quot;);        new Thread(() -&gt; {            System.out.println(Thread.currentThread().getName() + &quot;线程开启！&quot;);            // 保证main线程已经执行到循环的部分            try {                TimeUnit.SECONDS.sleep(3);            } catch (InterruptedException e) {                e.printStackTrace();            }            data.modifydata();            System.out.println(Thread.currentThread().getName() + &quot;修改了资源值，当前值为&quot; + data.datanum);        },&quot;第二个线程&quot;).start();        while(data.datanum == 0) {            // datanum等于0就一直循环        }        System.out.println(Thread.currentThread().getName() + &quot;线程结束&quot;);    }}class Data {    int datanum = 0;    public void modifydata() {        this.datanum = 100;    }}</code></pre><pre><code class="language-运行结果">main线程开启第二个线程线程开启！第二个线程修改了资源值，当前值为100</code></pre><p>这个程序的运行结果可以看到，main线程一直没有结束，一直在while循环里出不来。</p><p>因为main线程工作内存中<code>datanum</code>是一开始从主内存中拿到的0，在第二个线程将值改为100并刷新之后，由于main线程没收到通知主内存中的值发生了变化，故一直使用工作内存中的值，故一直会进行循环。</p><p>那接下来为这个资源变量增加一个volatile之后看看结果。</p><pre><code class="language-Java">package com.yw;import java.util.concurrent.TimeUnit;public class Test{    public static void main(String[] args)  {        Data data = new Data();System.out.println(Thread.currentThread().getName() + &quot;线程开启&quot;);        new Thread(() -&gt; {            System.out.println(Thread.currentThread().getName() + &quot;线程开启！&quot;);            try {                TimeUnit.SECONDS.sleep(3);            } catch (InterruptedException e) {                e.printStackTrace();            }            data.modifydata();            System.out.println(Thread.currentThread().getName() + &quot;修改了资源值，当前值为&quot; + data.datanum);        },&quot;第二个线程&quot;).start();        while(data.datanum == 0) {            // datanum等于0就一直循环        }        System.out.println(Thread.currentThread().getName() + &quot;线程结束&quot;);    }}class Data {    volatile int datanum = 0;    public void modifydata() {        this.datanum = 100;    }}</code></pre><pre><code class="language-运行结果">main线程开启第二个线程线程开启！第二个线程修改了资源值，当前值为100main线程结束</code></pre><p>发现main线程是正常结束了，故可以知道main线程知道主内存中的值发生了改变，故该资源类对所有线程具有了可见性，故可以知道volatile是可以保证可见性的。</p><p>那么问题来了，volatile是怎么保证可见性的。</p><ol><li>用volatile修饰的变量在工作内存中被修改之后，就会将该值强制刷新到主内存当中去</li><li>每个线程在修改volatile修饰的变量前，都会触发总线嗅探，保证工作内存中的值与主内存一样在进行修改。</li></ol><p>:::note{type=&quot;info&quot;}</p><p>每个处理器通过嗅探总线上的资源类来确保自己工作内存的缓存值是否已经过期，如果过期就会将该缓存行设置为无效，然后重修读取主内存中的自己过期的值进行修改，这就是总线嗅探机制，当高并发多个线程不断进行总线嗅探就会导致总线风暴。（我是这么理解的）</p><p>:::</p><h2>不保证原子性</h2><p>这个其实很好验证，看以下代码。</p><pre><code class="language-Java">package com.yw;import java.util.concurrent.CountDownLatch;public class Test{    public static void main(String[] args) throws InterruptedException {        Data data = new Data();        //用来辅助确保二十个线程全部执行完毕        CountDownLatch countDownLatch = new CountDownLatch(20);        for (int i = 0; i &lt; 20; i++) {            new Thread(() -&gt; {                //设置次数要多，太少结果不明显                for (int j = 0; j &lt; 1000; j++) {                    data.addData();                }                countDownLatch.countDown();            }).start();        }        countDownLatch.await();        System.out.println(&quot;当前资源类的值为-----&gt;&quot; + data.datanum);    }}class Data {    volatile int datanum = 0;    public void addData() {        datanum++;    }}</code></pre><p>得到的结果按理说应该是20000,但是执行了很多次出来的结果都不是20000，都是比20000少的。这显而易见，每个线程并不能全部完成自己的任务，从而可知volatile不保证原子性。为什么不保证原子性呢？</p><p>首先要知道的一点是，自增或者自减其实并不是一个原子性的操作，我们总觉得它是一个语句所以就把它当成了一个原子性操作，这是不对的，在底层<code>i++</code>是有几个指令组成的，现在来探究一下。</p><p>首先写一个简单的自增语句。</p><pre><code class="language-Java">package com.yw;public class Test{    public static void main(String[] args) throws InterruptedException {        int i = 0;        i++;        System.out.println(i);    }}</code></pre><p>对该程序进行<code>javap -c</code>的反汇编得到以下的指令。</p><pre><code class="language-Java">Compiled from &quot;Test.java&quot;public class com.yw.Test {  public com.yw.Test();    Code:       0: aload_0       1: invokespecial #1                  // Method java/lang/Object.&quot;&lt;init&gt;&quot;:()V       4: return  public static void main(java.lang.String[]) throws java.lang.InterruptedException;    Code:       0: iconst_0       1: istore_1       2: iinc          1, 1       5: getstatic     #2                  // Field java/lang/System.out:Ljava/io/PrintStream;       8: iload_1       9: invokevirtual #3                  // Method java/io/PrintStream.println:(I)V      12: return}</code></pre><p>于是就可以得到<code>i++</code>的指令。</p><pre><code class="language-Java">       1: istore_1//将栈顶int型数值存入第一个本地变量       2: iinc          1, 1//将指定int型变量增加指定值       5: getstatic     #2//获取指定类中的静态域，并将其压入栈顶</code></pre><p>所以可以看到自增的指令并不是一条，所以就会有可能出现一些情况导致出现错误。</p><p>比如存在两个线程A和B。这两个线程都在各自线程的工作内存获取了相同并正确的值，并执行完<code>iinc</code>的自增指令，并将其压入栈中，但是由于存在线程竞争的情况，将其写回主内存时就会出现这种情况，A抢到时间片将自增写回主内存，时间片结束轮到B线程进行写入，可能会出现执行速度过快，导致没有进行嗅探将线程错误的初始值确认为无效，然后也将工作内存中的值写回主内存，于是就出现了丢失修改的错误。</p><p>所以说valatile并不能代替锁进行线程的同步，只要不是原子性的操作，就不能保证线程安全,如果需要保证原子性那就需要用到锁了。</p><h2>禁止指令重排</h2><p>指令重排涉及到汇编，结果也会有随机性，只能进行云分析了，先看以下样例。</p><p>假设有a,b,x,y四个变量的值为0，然后两个线程对该变量进行修改。</p><table><thead><tr><th align="center">线程A</th><th align="center">线程B</th></tr></thead><tbody><tr><td align="center">x = a</td><td align="center">y = b</td></tr><tr><td align="center">b = 10</td><td align="center">a = 10</td></tr></tbody></table><p>正常的结果是得到<code>x=0,y=0</code>。</p><p>可是由于指令重排的存在，所以在小概率的情况下会出现先执行<code>b=1</code>或者是<code>a=2</code>，然后就会导致x与y的值不为0这种错误的结果。</p><p>这时候就可以使用volatile来禁止指令重排，使其顺序进行，从而使结果正确。</p><p>那么volatile是怎样禁止指令重排的呢？这就涉及到一个新的概念了，内存屏障。</p><h3>内存屏障</h3><p>内存屏障分为四类，如表。</p><table><thead><tr><th align="center">屏障类型</th><th align="center">指令示例</th><th align="center">说明</th></tr></thead><tbody><tr><td align="center">LoadLoad Barriers</td><td align="center">Load1;LoadLoad;Load2</td><td align="center">确保Load1数据装载先于Load2及所有后续装载指令的装载</td></tr><tr><td align="center">StoreStore Barriers</td><td align="center">Store1;StoreStore;Store2</td><td align="center">确保Store1数据对其他处理器可见（刷新到内存）先于Store2及所有后续存储指令的存储</td></tr><tr><td align="center">LoadStore Barriers</td><td align="center">Load1;LoadStore;Store2</td><td align="center">确保Load1数据装载先于Store2及所有后续存储指令的刷新到内存</td></tr><tr><td align="center">SroreLoad Barriers</td><td align="center">Store1;StoreLoad;Load2</td><td align="center">确保Store1数据对其他处理器可见（刷新到内存）先于Load2及所有后续装载指令的装载<br />该指令会使屏障之前所有内存访问指令完成之后才执行该屏障之后的内存访问指令</td></tr></tbody></table><p>接下来的就是JMM针对编译器制定的volatile重排序规则表。</p><table><thead><tr><th align="center">是否能够重排序</th><th align="center">普通读/写</th><th align="center">volatile读</th><th align="center">volatile写</th></tr></thead><tbody><tr><td align="center">普通读/写</td><td align="center"></td><td align="center"></td><td align="center">NO</td></tr><tr><td align="center">volatile读</td><td align="center">NO</td><td align="center">NO</td><td align="center">NO</td></tr><tr><td align="center">volatile写</td><td align="center"></td><td align="center">NO</td><td align="center">NO</td></tr></tbody></table><p>左边为第一个操作，右边为第二个操作。比如第三行第二列的NO就表示，如果第一个操作为volatile读，第二个操作为普通读/写，则编译器不能重排序这两个操作。</p><h3>volatile写</h3><ul><li>在每个volatile写操作前面插入一个<code>StoreStore Barriers</code></li><li>在每个volatile写操作后面插入一个<code>StoreLoad Barriers</code></li></ul><p><img src="https://img.yww52.com/2021/1/2021-1-8/img3.png" alt=""></p><h3>volatile读</h3><ul><li>在每个volatile读操作后面插入一个<code>LoadLoad Barriers</code></li><li>在每个volatile读操作后面插入一个<code>LoadStore Barriers</code></li></ul><p><img src="https://img.yww52.com/2021/1/2021-1-8/img4.png" alt=""></p><h1>参考资料</h1><ul><li><a href="https://book.douban.com/subject/26591326/">Java并发编程的艺术</a></li><li><a href="http://ifeve.com/">并发编程网</a></li><li><a href="https://segmentfault.com/a/1190000008722128">字节码指令</a></li></ul>]]></content:encoded>
            <category domain="https://yww52.com/categories/%25E6%2588%2591%25E7%259A%2584%25E6%258A%2580%25E6%259C%25AF%25E5%25AD%25A6%25E4%25B9%25A0%25E7%2589%25A9%25E8%25AF%25AD%25E6%259E%259C%25E7%2584%25B6%25E6%259C%2589%25E9%2597%25AE%25E9%25A2%2598/">我的技术学习物语果然有问题</category>
        </item>
        <item>
            <title>初识JUC编程</title>
            <link>https://yww52.com/posts/195c9969/</link>
            <guid isPermaLink="true">https://yww52.com/posts/195c9969/</guid>
            <pubDate>Sat, 02 Jan 2021 00:00:00 +0000</pubDate>
            <description>初识JUC编程。</description>
            <content:encoded><![CDATA[<img src="https://img.yww52.com/2021/1/2021-1-2top_img.jpg" alt="初识JUC编程" style="max-width:100%;height:auto;margin-bottom:15px;" /><h1>什么是JUC</h1><p>JUC是<code>java.util.concurrent</code>包的缩写，这个包是Java的并发编程的工具类，所以JUC编程可以理解为Java的并发编程。</p><h1>一些基本的概念</h1><h2>程序，进程与线程</h2><ol><li>程序是一组计算机能识别和执行的指令，只是一个静态的概念。</li><li>进程是操作系统资源分配的基本单位，可以理解为一个程序的运行就为一个进程，它是一个动态的实体。</li><li>线程可以理解为进程之内为了完成某一个任务的线路，一个进程为了完成任务，可能就会将该任务拆分为很多子任务，并开辟多个线程来完成这些子任务，从而加快完成总任务的速度。</li></ol><h2>开启线程的方式</h2><ol><li>继承<code>Thread</code>类，重写<code>run()</code>方法，调用<code>start()</code>方法</li><li>实现<code>Runnable</code>接口，重写<code>run()</code>方法，创建<code>Thread</code>实例，调用其中的<code>start()</code>方法。</li><li>实现<code>Calleble</code>接口</li></ol><h2>串行，并发和并行</h2><ol><li>串行很好理解，每个任务都会排队执行，上一个任务没有结束，下一个任务就无法开始。</li><li>并发就是不用等待上一个任务执行完毕，下一个任务就能开始执行，每个任务交替执行，形成每个任务同时进行的假象。</li><li>并行就是任务同时进行，而不是像并发一样交替执行。</li><li>简单理解为并发就是交替做一件事，并行就是同时做不同的事 。</li><li>即使是单核的CPU也能进行多线程的操作，但是做不到并行。</li></ol><h2>上下文切换</h2><p>即使是单核的CPU也是支持多线程执行的，CPU通过给每个线程分配CPU时间片来实现这个机制，这个时间片非常短，所以我们觉得多个线程是一起执行的，但其实是CPU在不断的切换线程执行。</p><p>在切换线程前会保存这个线程任务的状态，然后加载下个线程任务的状态，这个过程就是一次上下文切换。</p><h2>并发是否比串行执行速度快</h2><p>刚刚提到了上下文切换，可以知道并发其实并不一定会比串行的速度快，因为并发会消耗一部分时间在上下文切换上，所以当并发执行的操作不超过百万次时，速度会比串行执行操作要慢。</p><h2>Java进程的状态</h2><p>从Java的<code>Thread</code>类中就可以看到Java进程的全部状态了。</p><pre><code class="language-Java">    public enum State {        /**         * Thread state for a thread which has not yet started.         */        NEW,        /**         * Thread state for a runnable thread.  A thread in the runnable         * state is executing in the Java virtual machine but it may         * be waiting for other resources from the operating system         * such as processor.         */        RUNNABLE,        /**         * Thread state for a thread blocked waiting for a monitor lock.         * A thread in the blocked state is waiting for a monitor lock         * to enter a synchronized block/method or         * reenter a synchronized block/method after calling         * {@link Object#wait() Object.wait}.         */        BLOCKED,        /**         * Thread state for a waiting thread.         * A thread is in the waiting state due to calling one of the         * following methods:         * &lt;ul&gt;         *   &lt;li&gt;{@link Object#wait() Object.wait} with no timeout&lt;/li&gt;         *   &lt;li&gt;{@link #join() Thread.join} with no timeout&lt;/li&gt;         *   &lt;li&gt;{@link LockSupport#park() LockSupport.park}&lt;/li&gt;         * &lt;/ul&gt;         *         * &lt;p&gt;A thread in the waiting state is waiting for another thread to         * perform a particular action.         *         * For example, a thread that has called &lt;tt&gt;Object.wait()&lt;/tt&gt;         * on an object is waiting for another thread to call         * &lt;tt&gt;Object.notify()&lt;/tt&gt; or &lt;tt&gt;Object.notifyAll()&lt;/tt&gt; on         * that object. A thread that has called &lt;tt&gt;Thread.join()&lt;/tt&gt;         * is waiting for a specified thread to terminate.         */        WAITING,        /**         * Thread state for a waiting thread with a specified waiting time.         * A thread is in the timed waiting state due to calling one of         * the following methods with a specified positive waiting time:         * &lt;ul&gt;         *   &lt;li&gt;{@link #sleep Thread.sleep}&lt;/li&gt;         *   &lt;li&gt;{@link Object#wait(long) Object.wait} with timeout&lt;/li&gt;         *   &lt;li&gt;{@link #join(long) Thread.join} with timeout&lt;/li&gt;         *   &lt;li&gt;{@link LockSupport#parkNanos LockSupport.parkNanos}&lt;/li&gt;         *   &lt;li&gt;{@link LockSupport#parkUntil LockSupport.parkUntil}&lt;/li&gt;         * &lt;/ul&gt;         */        TIMED_WAITING,        /**         * Thread state for a terminated thread.         * The thread has completed execution.         */        TERMINATED;    }</code></pre><h1>synchronized</h1><p>首先来看个简单的例子。</p><pre><code class="language-Java">public class Test implements Runnable{    private int ticket = 30;    private boolean flag = true;    public static void main(String[] args) {        Test test = new Test();        new Thread(test,&quot;第一个人&quot;).start();        new Thread(test,&quot;第二个人&quot;).start();        new Thread(test,&quot;第三个人&quot;).start();    }    /**        买票的方法     */    public void buyTicket() {        if(ticket &lt;= 0) {            flag = false;            return;        }        try {            Thread.sleep(100);        } catch (InterruptedException e) {            e.printStackTrace();        }        System.out.println(Thread.currentThread().getName() + &quot;获得了第&quot; + ticket-- + &quot;张票&quot;);    }    /**         run方法     */    @Override    public void run() {        while(flag) {            buyTicket();        }    }}</code></pre><p>从上面这个例子就会出现两种不正常的情况。</p><ol><li>先买到了第n张票之后才买到第n-1张票</li><li>出现负数的票</li></ol><p>因为多线程难以统一资源所以会出现的这些异常情况，这是我们不想看到的。处理并发的有效的手段就是加锁，所以我们需要用加锁来解决这个问题。</p><p>这里使用了<code>synchronized</code>来对买票的方法进行加锁，可以解决一些简单的并发问题。</p><pre><code class="language-Java">package com.yw;/** * @ClassName Test * @Descriprtion TODO * @Author yww * @Date 2020/12/27 15:21 * @Version 1.0 **/public class Test implements Runnable{    private int ticket = 30;    private boolean flag = true;    public static void main(String[] args) {        Test test = new Test();        new Thread(test,&quot;第一个人&quot;).start();        new Thread(test,&quot;第二个人&quot;).start();        new Thread(test,&quot;第三个人&quot;).start();    }    /**        买票的方法     */    public synchronized void buyTicket() {        if(ticket &lt;= 0) {            flag = false;            return;        }        try {            Thread.sleep(100);        } catch (InterruptedException e) {            e.printStackTrace();        }        System.out.println(Thread.currentThread().getName() + &quot;获得了第&quot; + ticket-- + &quot;张票&quot;);    }    /**         run方法     */    @Override    public  void run() {        while(flag) {            buyTicket();        }    }}</code></pre><p>synchronized分为三种使用情况。</p><ol><li>在普通方法上使用synchronized关键字，锁住的是实例对象，每个实例都各自有自己的锁，互不干扰。</li><li>在静态方法上使用synchronized关键字，锁住的是类，即每个实例对象共用这个锁。</li><li>同步方法块，锁的对象不定，看个人行为。</li></ol><h1>Lock锁</h1><p>也可以不用<code>synchronized</code>来加锁，这里就来讲讲第二种方式。</p><p>这里就只学习Lock类的<code>ReentrantLock</code>这个实现类。</p><p>先探究一下这个实现类的构造方法。</p><pre><code class="language-Java">    /**     * Creates an instance of {@code ReentrantLock}.     * This is equivalent to using {@code ReentrantLock(false)}.     */    public ReentrantLock() {        sync = new NonfairSync();    }    /**     * Creates an instance of {@code ReentrantLock} with the     * given fairness policy.     *     * @param fair {@code true} if this lock should use a fair ordering policy     */    public ReentrantLock(boolean fair) {        sync = fair ? new FairSync() : new NonfairSync();    }</code></pre><p>由此可以看出，不加参数使用该实现类，这个锁就是<code>非公平锁</code>，加了true的参数，就是<code>公平锁</code>。</p><blockquote><p>简单的解释一下公平锁与非公平锁。</p><ol><li>公平锁，表示先来先服务，按照申请锁的顺序去获得锁，每个申请锁的线程都会形成一个队列，一个个来获得锁。</li><li>非公平锁，表示每个线程都可以去申请获得锁，获取不到就等待，获取得到就可以获得锁。</li></ol><p>很多情况默认都是是非公平锁，synchronized也算是非公平锁，谁先抢到，谁就进行。</p></blockquote><p>现在就用该实现类来解决一下上面买票的问题。</p><pre><code class="language-Java">public class Test implements Runnable{    private int ticket = 30;    private boolean flag = true;    Lock lock = new ReentrantLock();    public static void main(String[] args) {        Test test = new Test();        new Thread(test,&quot;第一个人&quot;).start();        new Thread(test,&quot;第二个人&quot;).start();        new Thread(test,&quot;第三个人&quot;).start();    }    /**        买票的方法     */    public  void buyTicket() {        // 加锁        lock.lock();        try {            if(ticket &lt;= 0) {                flag = false;                return;            }            Thread.sleep(100);            System.out.println(Thread.currentThread().getName() + &quot;获得了第&quot; + ticket-- + &quot;张票&quot;);        } catch (InterruptedException e) {            e.printStackTrace();        } finally {            // 解锁            lock.unlock();        }    }        /**         run方法     */    @Override    public  void run() {        while(flag) {            buyTicket();        }    }}</code></pre><p>这样就可以进行简单的加锁来解决一些线程出现的问题。</p><h1>synchronized和ReentrantLock区别</h1><ol><li>synchronized是一个关键字，是JVM层面的，而ReentrantLock是一个来实现类，是API层面的。</li><li>synchronized是自动加锁和释放锁的，而ReentrantLock是手动加锁的，所以也需要手动释放锁。</li><li>synchronized的锁是不可中断的，除非是临界区内的代码出现异常或者是正常结束释放锁，而ReentrantLock的锁是可以中断的，可以使用<code>trylock</code>方法设置超时方法。</li><li>synchronized是非公平锁，而ReentrantLock默认是非公平锁，但是可以通过构造方法设置成公平锁。</li><li>synchronized的线程阻塞后是只能随机唤醒进程，而ReentrantLock通过Condition类可以精准唤醒被绑定的线程。</li></ol><h1>生产者和消费者的问题</h1><h2>synchronzied</h2><p>我们计划，当商品数量为1的时候，消费者进行消费，当商品数量为0的时候，生产者进行生产。</p><p>所以这会涉及到线程之间的通信，商品数量为0的时候消费者就会进入等待，并通知生成者进入就绪状态。商品数量为1的时候生成者就会进入等待，并通知消费者进入就绪状态</p><pre><code class="language-java">public class Test{    public static void main(String[] args) {        Shop shop = new Shop();        new Thread(()-&gt;{            for (int i = 0; i &lt; 50; i++) {                try {                    shop.buyShop();                } catch (InterruptedException e) {                    e.printStackTrace();                }            }        },&quot;消费者&quot;).start();        new Thread(()-&gt;{            for (int i = 0; i &lt; 50; i++) {                try {                    shop.AddShop();                } catch (InterruptedException e) {                    e.printStackTrace();                }            }        },&quot;生成者&quot;).start();    }}class Shop {    private int shopnum = 1;    public synchronized void buyShop() throws InterruptedException {        if(shopnum == 0) {            this.wait();        }        System.out.println(Thread.currentThread().getName() + shopnum--);        this.notifyAll();    }    public synchronized void AddShop() throws InterruptedException {        if(shopnum == 1) {            this.wait();        }        System.out.println(Thread.currentThread().getName() + shopnum++);        this.notifyAll();    }}</code></pre><p>这个代码还会有一个<code>虚假唤醒</code>的问题，当线程数增加到了两个以上，就会出现一些异常情况，比如票数会到达0与1之外的范围，这是因为使用if来进行判断等待状态，只会判断一次，所以可能会出现两个线程都判断成功的情况，所以必须要使用while来进行判断，这样可以防止虚假唤醒的问题。</p><pre><code class="language-Java">public class Test{    public static void main(String[] args) {        Shop shop = new Shop();        new Thread(()-&gt;{            for (int i = 0; i &lt; 50; i++) {                try {                    shop.buyShop();                } catch (InterruptedException e) {                    e.printStackTrace();                }            }        },&quot;消费者&quot;).start();        new Thread(()-&gt;{            for (int i = 0; i &lt; 50; i++) {                try {                    shop.AddShop();                } catch (InterruptedException e) {                    e.printStackTrace();                }            }        },&quot;生产者&quot;).start();    }}class Shop {    private int shopnum = 1;    public synchronized void buyShop() throws InterruptedException {        // 进入等待        while(shopnum == 0) {            this.wait();        }        System.out.println(Thread.currentThread().getName() + shopnum--);        // 通知唤醒        this.notifyAll();    }    public synchronized void AddShop() throws InterruptedException {        // 进入等待        while(shopnum == 1) {            this.wait();        }        // 通知唤醒        System.out.println(Thread.currentThread().getName() + shopnum++);        this.notifyAll();    }}</code></pre><h2>Lock锁</h2><p>这里的Lock接口是没办法实现线程等待和唤醒的，所以就使用了一个<code>Condition</code>类的<code>await</code>方法和<code>signal</code>方法来代替wait和notifyall方法。</p><pre><code class="language-Java">public class Test{    public static void main(String[] args) {        Shop shop = new Shop();        new Thread(()-&gt;{            for (int i = 0; i &lt; 50; i++) {                try {                    shop.buyShop();                } catch (InterruptedException e) {                    e.printStackTrace();                }            }        },&quot;消费者1&quot;).start();        new Thread(()-&gt;{            for (int i = 0; i &lt; 50; i++) {                try {                    shop.AddShop();                } catch (InterruptedException e) {                    e.printStackTrace();                }            }        },&quot;生产者1&quot;).start();        new Thread(()-&gt;{            for (int i = 0; i &lt; 50; i++) {                try {                    shop.buyShop();                } catch (InterruptedException e) {                    e.printStackTrace();                }            }        },&quot;消费者2&quot;).start();        new Thread(()-&gt;{            for (int i = 0; i &lt; 50; i++) {                try {                    shop.AddShop();                } catch (InterruptedException e) {                    e.printStackTrace();                }            }        },&quot;生产者2&quot;).start();    }}class Shop {    private int shopnum = 1;    Lock lock = new ReentrantLock();    Condition condition = lock.newCondition();    public  void buyShop() throws InterruptedException {        lock.lock();        try {            while(shopnum == 0) {                condition.await();            }            System.out.println(Thread.currentThread().getName() + shopnum--);            condition.signal();        } catch (InterruptedException e) {            e.printStackTrace();        } finally {            lock.unlock();        }    }    public  void AddShop() throws InterruptedException {        lock.lock();        try {            while(shopnum == 1) {                condition.await();            }            System.out.println(Thread.currentThread().getName() + shopnum++);            condition.signal();        } catch (InterruptedException e) {            e.printStackTrace();        } finally {            lock.unlock();        }    }}</code></pre><h1>读写锁</h1><p>读写锁，这个类其实和上面两个加锁方法差不多，该锁可以将封锁粒度减少一点。读写锁其实就是之前学到的排他锁和共享锁。</p><p>线程对一个资源加上排它锁，表示只允许该线程进行读写资源，不允许其他线程读写，直到该线程释放了排它锁。</p><p>线程对一个资源加上共享锁，表示该线程可以对该资源进行读操作，但不能修改资源，其他的线程可以对该资源加共享锁，但不能加排它锁进行修改该资源，直到所有线程释放了对该资源的共享锁才能为该资源加排它锁进行修改。</p><pre><code class="language-Java">public class Test{    public static void main(String[] args) {        Mycache mycache = new Mycache();        for(int i = 1; i &lt;= 10; i++) {            final int temp = i;            new Thread(()-&gt;{                mycache.put(temp + &quot;&quot;,temp + &quot;&quot;);            },String.valueOf(i)).start();        }        for(int i = 1; i &lt;= 10; i++) {            final int temp = i;            new Thread(()-&gt;{                mycache.get(temp + &quot;&quot;);            },String.valueOf(i)).start();        }    }}class Mycache {    private volatile Map&lt;String,String&gt; map = new HashMap&lt;&gt;();    private ReadWriteLock readWriteLock = new ReentrantReadWriteLock();    public void put(String key,String value) {        // 加上排它锁        readWriteLock.writeLock().lock();        try {            System.out.println(Thread.currentThread().getName() + &quot;写入&quot; + key);            map.put(key,value);            System.out.println(Thread.currentThread().getName() + &quot;写入完成&quot;);        } catch (Exception e) {            e.printStackTrace();        } finally {            // 释放排它锁            readWriteLock.writeLock().unlock();        }    }    public void get(String key) {        // 加上共享锁        readWriteLock.readLock().lock();        try {            System.out.println(Thread.currentThread().getName() + &quot;读取&quot; + key);            String value = map.get(key);            System.out.println(Thread.currentThread().getName() + &quot;读取完成&quot;);        } catch (Exception e) {            e.printStackTrace();        } finally {            // 释放共享锁            readWriteLock.readLock().unlock();        }    }}</code></pre><h1>集合类</h1><p>经常使用的一些集合类在并发操作下很多都是不安全的，多个线程进行写入操作就会出现并发修改异常<code>ConcurrentModificationException</code>。所以现在来探讨一下解决的方案。</p><h2>List</h2><ol><li><p>使用<code>Vector</code>实现类，这个实现类是线程安全的，为什么安全可以看一下它的add方法，它是使用了<code>synchronized</code>关键字的。</p><pre><code class="language-Java">public synchronized boolean add(E e) {    modCount++;    ensureCapacityHelper(elementCount + 1);    elementData[elementCount++] = e;    return true;}</code></pre></li><li><p>使用Collections工具类将Arraylist转换成安全的实现类。</p><pre><code class="language-Java">public class Test{    public static void main(String[] args) {        List&lt;String&gt; list = Collections.synchronizedList(new ArrayList&lt;&gt;());        for(int i = 1; i &lt;= 20; i++) {            new Thread(()-&gt;{                list.add(&quot;1111&quot;);                System.out.println(list);            },String.valueOf(i)).start();        }    }}</code></pre></li><li><p>使用<code>CopyOnWriteArrayList</code>实现类。</p><pre><code class="language-Java">public class Test{    public static void main(String[] args) {        List&lt;String&gt; list = new CopyOnWriteArrayList&lt;&gt;();        for(int i = 1; i &lt;= 20; i++) {            new Thread(()-&gt;{                list.add(&quot;1111&quot;);                System.out.println(list);            },String.valueOf(i)).start();        }    }}</code></pre></li></ol><h2>Set</h2><p>   HashSet也和ArrayList一样不安全，所以需要进行一些操作。</p><ol><li><p>使用Collections工具类将HashSet转换成安全的实现类。</p><pre><code class="language-java">public class Test{    public static void main(String[] args) {        Set&lt;String&gt; set = Collections.synchronizedSet(new HashSet&lt;&gt;());        for(int i = 1; i &lt;= 20; i++) {            new Thread(()-&gt;{                set.add(UUID.randomUUID().toString());                System.out.println(set);            },String.valueOf(i)).start();        }    }}</code></pre></li><li><p>使用<code>CopyOnWriteArrayList</code>实现类。</p><pre><code class="language-Java">public class Test{    public static void main(String[] args) {        Set&lt;String&gt; set = new CopyOnWriteArraySet&lt;&gt;();        for(int i = 1; i &lt;= 20; i++) {            new Thread(()-&gt;{                set.add(UUID.randomUUID().toString());                System.out.println(set);            },String.valueOf(i)).start();        }    }}</code></pre></li></ol><h2>Map</h2><ol><li><p>使用Collections工具类将HashMap转换成安全的实现类。</p><pre><code class="language-Java">public class Test{    public static void main(String[] args) {        Map&lt;String,String&gt; map = Collections.synchronizedMap(new HashMap&lt;&gt;());        for(int i = 1; i &lt;= 20; i++) {            new Thread(()-&gt;{                map.put(Thread.currentThread().getName(),UUID.randomUUID().toString());                System.out.println(map);            },String.valueOf(i)).start();        }    }}</code></pre></li><li><p>使用<code>ConcurrentHashMap</code>实现类。</p><pre><code class="language-Java">public class Test{    public static void main(String[] args) {        Map&lt;String,String&gt; map = new ConcurrentHashMap&lt;&gt;();        for(int i = 1; i &lt;= 20; i++) {            new Thread(()-&gt;{                map.put(Thread.currentThread().getName(),UUID.randomUUID().toString());                System.out.println(map);            },String.valueOf(i)).start();        }    }}</code></pre></li></ol><h1>Callable</h1><p>Callable是第三种开启线程的方式，但是线程的开启都是通过Thread类的start方法来开启的，而Callable的实现类是无法传入Thread类中的，所以需要借用中间类来实现。</p><pre><code class="language-Java">public class Test{    public static void main(String[] args) throws ExecutionException, InterruptedException {        HelloTest helloTest = new HelloTest();        FutureTask futureTask = new FutureTask(helloTest);        new Thread(futureTask,&quot;线程&quot;).start();        // 获取返回值，有缓存，所以会有可能出现线程阻塞        String msg = (String) futureTask.get();        System.out.println(msg);    }}class HelloTest implements Callable&lt;String&gt;{    @Override    public String call() throws Exception {        System.out.println(&quot;call方法调用&quot;);        return &quot;hello from Callable&quot;;    }}</code></pre><blockquote><p>其中的FutureTask是实现了RunnableFuture，而RunnableFuture接口继承了Runnable，其实借用FutureTask类打开线程本质就是接用Runnable来开启线程的。</p></blockquote><h1>常用辅助类</h1><h2>CountDownLatch</h2><p>这是一个减法计数器，可以使一个线程等很多线程执行完毕后在执行。</p><pre><code class="language-Java">public class Test{    public static void main(String[] args) throws InterruptedException {        // 计数器初始赋值        CountDownLatch countDownLatch = new CountDownLatch(10);        for(int i = 1; i &lt;= 10; i++) {            new Thread(()-&gt;{                System.out.println(Thread.currentThread().getName() + &quot;线程执行&quot;);                // 计数器减一                countDownLatch.countDown();            },String.valueOf(i)).start();        }        // 等待计数器为0        countDownLatch.await();        System.out.println(&quot;程序结束&quot;);    }}</code></pre><h2>CyclicBarrier</h2><p>这是一个加法计数器，可以使很多线程到达一个目的地后在执行线程。</p><pre><code class="language-Java">public class Test{    public static void main(String[] args) throws InterruptedException, BrokenBarrierException {        // 设置线程数到10执行创建的线程，以下就是线程开到10个才会进行打印线程        CyclicBarrier cyclicBarrier = new CyclicBarrier(10,()-&gt;{            System.out.println(&quot;所有线程开启成功&quot;);        });        for(int i = 1; i &lt;= 10; i++) {            new Thread(()-&gt;{                System.out.println(Thread.currentThread().getName() + &quot;线程启动&quot;);                try {                    // 线程到10就会通知之前定义的线程                    cyclicBarrier.await();                } catch (InterruptedException e) {                    e.printStackTrace();                } catch (BrokenBarrierException e) {                    e.printStackTrace();                }            },String.valueOf(i)).start();        }    }}</code></pre><h2>Semaphore</h2><p>一个计数信号量。</p><pre><code class="language-Java">public class Test{    public static void main(String[] args) {        // 指定线程的最高开启数量        Semaphore semaphore = new Semaphore(2);        for(int i = 0; i &lt;= 10; i++) {            new Thread(() -&gt;{                try {                    // 等待Semaphore中线程数少于限制的量，然后开启线程，将该线程计入Semaphore                    semaphore.acquire();                    System.out.println(Thread.currentThread().getName() + &quot;开启了线程&quot;);                    try {                        TimeUnit.SECONDS.sleep(2);                    } catch (InterruptedException interruptedException) {                        interruptedException.printStackTrace();                    }                    System.out.println(Thread.currentThread().getName() + &quot;结束了线程&quot;);                } catch (InterruptedException e) {                    e.printStackTrace();                } finally {                    // 线程结束，将该线程从Semaphore释放                    semaphore.release();                }            },String.valueOf(i)).start();        }    }}</code></pre><h1>阻塞队列</h1><p>阻塞队列顾名思义就是会阻塞的队列，是队列的子类，其类为<code>BlockingQueue</code>,数组的实现方式是<code>ArrayBlockingQueue</code>和链表的实现方式是<code>LinkedBlockingQueue</code>。</p><h2>有返回值会抛出异常</h2><pre><code class="language-Java">public class Test{    public static void main(String[] args) {        ArrayBlockingQueue&lt;String&gt; arrayBlockingQueue = new ArrayBlockingQueue&lt;&gt;(3);        // 队列满了在用add方法入队就会抛出异常        // java.lang.IllegalStateException: Queue full        arrayBlockingQueue.add(&quot;1&quot;);        arrayBlockingQueue.add(&quot;2&quot;);        arrayBlockingQueue.add(&quot;3&quot;);        arrayBlockingQueue.add(&quot;4&quot;);        // 返回队首元素        System.out.println(arrayBlockingQueue.element());        // 队列空了再用remove出队就会抛出异常        // java.util.NoSuchElementException        arrayBlockingQueue.remove();        arrayBlockingQueue.remove();        arrayBlockingQueue.remove();        arrayBlockingQueue.remove();    }}</code></pre><pre><code class="language-Java">    // add的实现，add是用了父类的实现    public boolean add(E e) {        return super.add(e);    } // 父类实现 public boolean add(E e) {        if (offer(e))            return true;        else            throw new IllegalStateException(&quot;Queue full&quot;);    } // remove方法的实现    public E remove() {        E x = poll();        if (x != null)            return x;        else            throw new NoSuchElementException();    } // element方法的实现    public E element() {        E x = peek();        if (x != null)            return x;        else            throw new NoSuchElementException();    }</code></pre><blockquote><p>这里可以看出这三个方法都是调用了下面不抛出异常的三个方法，加以修饰让其抛出异常而已。</p></blockquote><h2>有返回值不抛出异常</h2><pre><code class="language-Java">public class Test{    public static void main(String[] args) {        ArrayBlockingQueue&lt;String&gt; arrayBlockingQueue = new ArrayBlockingQueue&lt;&gt;(3);        // 用offer入队，满了也不会出异常，会返回false        System.out.println(arrayBlockingQueue.offer(&quot;1&quot;));        System.out.println(arrayBlockingQueue.offer(&quot;2&quot;));        System.out.println(arrayBlockingQueue.offer(&quot;3&quot;));        System.out.println(arrayBlockingQueue.offer(&quot;4&quot;));        // 返回队首元素        System.out.println(arrayBlockingQueue.peek());  // 用poll出队，对空会返回null        System.out.println(arrayBlockingQueue.poll());        System.out.println(arrayBlockingQueue.poll());        System.out.println(arrayBlockingQueue.poll());        System.out.println(arrayBlockingQueue.poll());    }}</code></pre><pre><code class="language-Java"> // offer的实现 public boolean offer(E e) {        checkNotNull(e);        final ReentrantLock lock = this.lock;        lock.lock();        try {            if (count == items.length)                return false;            else {                enqueue(e);                return true;            }        } finally {            lock.unlock();        } // poll的实现    public E poll() {        final ReentrantLock lock = this.lock;        lock.lock();        try {            return (count == 0) ? null : dequeue();        } finally {            lock.unlock();        }    }    // peek的实现    public E peek() {        final ReentrantLock lock = this.lock;        lock.lock();        try {            return itemAt(takeIndex); // null when queue is empty        } finally {            lock.unlock();        }    }</code></pre><p>由此可以看出这三个方法都是用到了锁的，使得上面抛出异常的三个方法也是加锁了的。</p><h2>阻塞等待</h2><pre><code class="language-Java">public class Test{    public static void main(String[] args) throws InterruptedException {        ArrayBlockingQueue&lt;String&gt; arrayBlockingQueue = new ArrayBlockingQueue&lt;&gt;(3);  // 因为队满了在用put入队，就会一直等待        arrayBlockingQueue.put(&quot;1&quot;);        arrayBlockingQueue.put(&quot;2&quot;);        arrayBlockingQueue.put(&quot;3&quot;);        arrayBlockingQueue.put(&quot;4&quot;);  // 因为队空了在用take出队，就会一直等待        System.out.println(arrayBlockingQueue.take());        System.out.println(arrayBlockingQueue.take());        System.out.println(arrayBlockingQueue.take());        System.out.println(arrayBlockingQueue.take());    }}</code></pre><pre><code class="language-Java">    // put的实现    public void put(E e) throws InterruptedException {        checkNotNull(e);        final ReentrantLock lock = this.lock;        lock.lockInterruptibly();        try {            while (count == items.length)                notFull.await();            enqueue(e);        } finally {            lock.unlock();        }    } // take的实现    public E take() throws InterruptedException {        final ReentrantLock lock = this.lock;        lock.lockInterruptibly();        try {            while (count == 0)                notEmpty.await();            return dequeue();        } finally {            lock.unlock();        }    }</code></pre><h2>超时等待</h2><pre><code class="language-Java">public class Test{    public static void main(String[] args) throws InterruptedException {        ArrayBlockingQueue&lt;String&gt; arrayBlockingQueue = new ArrayBlockingQueue&lt;&gt;(3);  // 在队满的时候在入队，可以设置一个时间，时间过了就不等待了        arrayBlockingQueue.offer(&quot;1&quot;);        arrayBlockingQueue.offer(&quot;2&quot;);        arrayBlockingQueue.offer(&quot;3&quot;);        arrayBlockingQueue.offer(&quot;4&quot;,2, TimeUnit.SECONDS);  // 在队空的时候在出队，可以设置一个时间，时间过了就不等待了        System.out.println(arrayBlockingQueue.poll());        System.out.println(arrayBlockingQueue.poll());        System.out.println(arrayBlockingQueue.poll());        System.out.println(arrayBlockingQueue.poll(2,TimeUnit.SECONDS));    }}</code></pre><pre><code class="language-Java"> // 这是超时等待的offer方法 public boolean offer(E e, long timeout, TimeUnit unit)        throws InterruptedException {        checkNotNull(e);        long nanos = unit.toNanos(timeout);        final ReentrantLock lock = this.lock;        lock.lockInterruptibly();        try {            while (count == items.length) {                if (nanos &lt;= 0)                    return false;                nanos = notFull.awaitNanos(nanos);            }            enqueue(e);            return true;        } finally {            lock.unlock();        }    } // 这是超时等待的poll方法    public E poll(long timeout, TimeUnit unit) throws InterruptedException {        long nanos = unit.toNanos(timeout);        final ReentrantLock lock = this.lock;        lock.lockInterruptibly();        try {            while (count == 0) {                if (nanos &lt;= 0)                    return null;                nanos = notEmpty.awaitNanos(nanos);            }            return dequeue();        } finally {            lock.unlock();        }    }</code></pre><h2>总结</h2><table><thead><tr><th align="center">方法</th><th align="center">有返回值抛出异常</th><th align="center">有返回值不抛出异常</th><th align="center">阻塞等待</th><th align="center">超时等待</th></tr></thead><tbody><tr><td align="center">入队</td><td align="center">add</td><td align="center">offer</td><td align="center">put</td><td align="center">带参数的offer</td></tr><tr><td align="center">出队</td><td align="center">remove</td><td align="center">poll</td><td align="center">take</td><td align="center">带参数的poll</td></tr><tr><td align="center">返回队首元素</td><td align="center">element</td><td align="center">peek</td><td align="center">-</td><td align="center">-</td></tr></tbody></table><h2>同步队列</h2><p><code>BlockingQueue</code>还有一个实现类为同步队列<code>SynchronousQueue</code>，这个队列只能容纳一个元素，当元素满了再入队就会等待，当元素空了再出队也会等待，类似于上述的阻塞等待，但是只能容纳一个元素。</p><pre><code class="language-Java">public class Test{    public static void main(String[] args)  {        BlockingQueue&lt;String&gt; blockingQueue = new SynchronousQueue&lt;&gt;();        new Thread(()-&gt;{            try {                System.out.println(Thread.currentThread().getName() + &quot;put  1&quot;);                blockingQueue.put(&quot;1&quot;);                System.out.println(Thread.currentThread().getName() + &quot;put  2&quot;);                blockingQueue.put(&quot;2&quot;);                System.out.println(Thread.currentThread().getName() + &quot;put  3&quot;);                blockingQueue.put(&quot;3&quot;);            } catch (InterruptedException e) {                e.printStackTrace();            }        },&quot;线程一&quot;).start();        new Thread(()-&gt;{            try {                TimeUnit.SECONDS.sleep(3);                System.out.println(Thread.currentThread().getName() + &quot;take  1&quot;);                System.out.println(blockingQueue.take());                TimeUnit.SECONDS.sleep(3);                System.out.println(Thread.currentThread().getName() + &quot;take  2&quot;);                System.out.println(blockingQueue.take());                TimeUnit.SECONDS.sleep(3);                System.out.println(Thread.currentThread().getName() + &quot;take  3&quot;);                System.out.println(blockingQueue.take());            } catch (InterruptedException e) {                e.printStackTrace();            }        },&quot;线程二&quot;).start();    }}</code></pre><h1>线程池</h1><h2>四个方法</h2><ol><li><p>创建线程池的方法</p><pre><code class="language-Java"> // 创建一个单例线程池，即只有一个线程的线程池 ExecutorService pool1 = Executors.newSingleThreadExecutor(); // 创建一个指定线程数的线程池，超出的线程就会在队列等待    ExecutorService pool2 = Executors.newFixedThreadPool(5); // 创建一个可缓存的线程池，多个线程就有多个线程（看CPU）    ExecutorService pool3 = Executors.newCachedThreadPool(); // 创建一个定长的线程池，支持定时以及周期性任务执行 ExecutorService pool4 = Executors.newScheduledThreadPool(5);</code></pre><pre><code class="language-Java">// 看一下创建线程的底层实现    public static ExecutorService newSingleThreadExecutor() {        return new FinalizableDelegatedExecutorService            (new ThreadPoolExecutor(1, 1,                                    0L, TimeUnit.MILLISECONDS,                                    new LinkedBlockingQueue&lt;Runnable&gt;()));    }    public static ExecutorService newFixedThreadPool(int nThreads) {        return new ThreadPoolExecutor(nThreads, nThreads,                                      0L, TimeUnit.MILLISECONDS,                                      new LinkedBlockingQueue&lt;Runnable&gt;());    }    public static ExecutorService newCachedThreadPool() {        return new ThreadPoolExecutor(0, Integer.MAX_VALUE,                                      60L, TimeUnit.SECONDS,                                      new SynchronousQueue&lt;Runnable&gt;());    }    public ScheduledThreadPoolExecutor(int corePoolSize) {        super(corePoolSize, Integer.MAX_VALUE, 0, NANOSECONDS,              new DelayedWorkQueue());    }</code></pre><blockquote><p>阿里巴巴编程规范不建议用这种方式开启线程池，因为可能会因为创建线程过多或者请求队列的线程过多而产生OOM，所以建议使用原生的ThreadPoolExecutor来开辟。</p></blockquote></li><li><p>使用线程池里的线程</p><pre><code class="language-Java"> // 丢入Runnable即可 pool1.execute(); // 比如    pool1.execute(()-&gt;{        System.out.println(Thread.currentThread().getName());    });</code></pre></li><li><p>使用完记得关闭线程池</p><pre><code class="language-Java"> pool1.shutdown();</code></pre></li></ol><h2>七大参数</h2><p>七大参数其实就是<code>ThreadPoolExecutor</code>的参数，先看一下构造方法。</p><pre><code class="language-Java">    public ThreadPoolExecutor(int corePoolSize,                              int maximumPoolSize,                              long keepAliveTime,                              TimeUnit unit,                              BlockingQueue&lt;Runnable&gt; workQueue) {        this(corePoolSize, maximumPoolSize, keepAliveTime, unit, workQueue,             Executors.defaultThreadFactory(), defaultHandler);    } // 点进this之后，上面只是简化版    public ThreadPoolExecutor(int corePoolSize,                              int maximumPoolSize,                              long keepAliveTime,                              TimeUnit unit,                              BlockingQueue&lt;Runnable&gt; workQueue,                              ThreadFactory threadFactory,                              RejectedExecutionHandler handler) {        if (corePoolSize &lt; 0 ||            maximumPoolSize &lt;= 0 ||            maximumPoolSize &lt; corePoolSize ||            keepAliveTime &lt; 0)            throw new IllegalArgumentException();        if (workQueue == null || threadFactory == null || handler == null)            throw new NullPointerException();        this.acc = System.getSecurityManager() == null ?                null :                AccessController.getContext();        this.corePoolSize = corePoolSize;        this.maximumPoolSize = maximumPoolSize;        this.workQueue = workQueue;        this.keepAliveTime = unit.toNanos(keepAliveTime);        this.threadFactory = threadFactory;        this.handler = handler;    }</code></pre><table><thead><tr><th align="center">参数</th><th align="center">含义</th></tr></thead><tbody><tr><td align="center">int corePoolSize</td><td align="center">核心线程数</td></tr><tr><td align="center">int maximumPoolSize</td><td align="center">最大并发的线程数，只有阻塞队列满了才会开启更多的线程直到最大并发数</td></tr><tr><td align="center">long keepAliveTime</td><td align="center">超时等候时间，时间到了就会释放线程</td></tr><tr><td align="center">TimeUnit unit</td><td align="center">设置时间长度，与上一个参数搭配</td></tr><tr><td align="center">BlockingQueue<Runnable> workQueue</td><td align="center">阻塞队列</td></tr><tr><td align="center">ThreadFactory threadFactory</td><td align="center">线程工厂，一般固定</td></tr><tr><td align="center">RejectedExecutionHandler handler</td><td align="center">拒绝策略，即线程数多到使阻塞队列满了而且最大线程数不够用了才执行的策略</td></tr></tbody></table><pre><code class="language-Java">public class Test{    public static void main(String[] args)  {        ExecutorService pool = new ThreadPoolExecutor(                2,                5,                3,                TimeUnit.SECONDS,                new ArrayBlockingQueue&lt;&gt;(5),                Executors.defaultThreadFactory(), // 一般都是这个                new ThreadPoolExecutor.AbortPolicy()        );    }}</code></pre><h2>四个拒绝策略</h2><p>拒绝策略共有四个，都是当线程数大于最大并发数加阻塞队列容量的时候，才会采取的策略。</p><pre><code class="language-Java"> * &lt;ol&gt; * * &lt;li&gt; In the default {@link ThreadPoolExecutor.AbortPolicy}, the * handler throws a runtime {@link RejectedExecutionException} upon * rejection. &lt;/li&gt; * * &lt;li&gt; In {@link ThreadPoolExecutor.CallerRunsPolicy}, the thread * that invokes {@code execute} itself runs the task. This provides a * simple feedback control mechanism that will slow down the rate that * new tasks are submitted. &lt;/li&gt; * * &lt;li&gt; In {@link ThreadPoolExecutor.DiscardPolicy}, a task that * cannot be executed is simply dropped.  &lt;/li&gt; * * &lt;li&gt;In {@link ThreadPoolExecutor.DiscardOldestPolicy}, if the * executor is not shut down, the task at the head of the work queue * is dropped, and then execution is retried (which can fail again, * causing this to be repeated.) &lt;/li&gt; * * &lt;/ol&gt;</code></pre><ol><li><p><code>AbortPolicy</code>丢弃任务并抛出<code>RejectedExecutionException</code>异常。</p><pre><code class="language-Java">    public static class AbortPolicy implements RejectedExecutionHandler {        /**         * Creates an {@code AbortPolicy}.         */        public AbortPolicy() { }        /**         * Always throws RejectedExecutionException.         *         * @param r the runnable task requested to be executed         * @param e the executor attempting to execute this task         * @throws RejectedExecutionException always         */        public void rejectedExecution(Runnable r, ThreadPoolExecutor e) {            throw new RejectedExecutionException(&quot;Task &quot; + r.toString() +                                                 &quot; rejected from &quot; +                                                 e.toString());        }    }</code></pre><p>这是线程池默认的拒绝策略，在类中也可以看到。</p><pre><code class="language-Java">    private static final RejectedExecutionHandler defaultHandler =        new AbortPolicy();</code></pre></li><li><p><code>CallerRunsPolicy</code></p><p>调用任务的<code>run()</code>方法绕过线程池直接执行。</p><pre><code class="language-Java">    public static class CallerRunsPolicy implements RejectedExecutionHandler {        /**         * Creates a {@code CallerRunsPolicy}.         */        public CallerRunsPolicy() { }        /**         * Executes task r in the caller&#39;s thread, unless the executor         * has been shut down, in which case the task is discarded.         *         * @param r the runnable task requested to be executed         * @param e the executor attempting to execute this task         */        public void rejectedExecution(Runnable r, ThreadPoolExecutor e) {            if (!e.isShutdown()) {                r.run();            }        }    }</code></pre></li><li><p><code>DiscardPolicy</code>丢弃任务，但是不会抛出异常，这是不推荐使用的拒绝策略。</p><pre><code class="language-Java">    public static class DiscardPolicy implements RejectedExecutionHandler {        /**         * Creates a {@code DiscardPolicy}.         */        public DiscardPolicy() { }        /**         * Does nothing, which has the effect of discarding task r.         *         * @param r the runnable task requested to be executed         * @param e the executor attempting to execute this task         */        public void rejectedExecution(Runnable r, ThreadPoolExecutor e) {        }    }</code></pre></li><li><p><code>DiscardOldestPolicy</code></p><p>抛弃队列中等待最久的任务，然后把当前任务加入队列中。</p><pre><code class="language-Java">    public static class DiscardOldestPolicy implements RejectedExecutionHandler {        /**         * Creates a {@code DiscardOldestPolicy} for the given executor.         */        public DiscardOldestPolicy() { }        /**         * Obtains and ignores the next task that the executor         * would otherwise execute, if one is immediately available,         * and then retries execution of task r, unless the executor         * is shut down, in which case task r is instead discarded.         *         * @param r the runnable task requested to be executed         * @param e the executor attempting to execute this task         */        public void rejectedExecution(Runnable r, ThreadPoolExecutor e) {            if (!e.isShutdown()) {                e.getQueue().poll();                e.execute(r);            }        }    }</code></pre></li></ol><h2>关于最大并发线程数的设置</h2><ol><li><p>CPU密集型CPU有几核就设置为多少个线程。</p><pre><code class="language-Java"> // 该方法可以返回CPU的核心数 Runtime.getRuntime().availableProcessors();</code></pre></li><li><p>IO密集型根据具体情况确定，若有15个任务，那最大并发线程数可以设置为大于15，比如设置为30</p></li></ol>]]></content:encoded>
            <category domain="https://yww52.com/categories/%25E6%2588%2591%25E7%259A%2584%25E6%258A%2580%25E6%259C%25AF%25E5%25AD%25A6%25E4%25B9%25A0%25E7%2589%25A9%25E8%25AF%25AD%25E6%259E%259C%25E7%2584%25B6%25E6%259C%2589%25E9%2597%25AE%25E9%25A2%2598/">我的技术学习物语果然有问题</category>
        </item>
        <item>
            <title>CMS垃圾回收器</title>
            <link>https://yww52.com/posts/b020b173/</link>
            <guid isPermaLink="true">https://yww52.com/posts/b020b173/</guid>
            <pubDate>Sun, 13 Dec 2020 00:00:00 +0000</pubDate>
            <description>CMS的垃圾回收器有很多讲究，所以特地拿出来探讨一下。</description>
            <content:encoded><![CDATA[<img src="https://img.yww52.com/2020/12/2020-12-13/top_img.jpg" alt="CMS垃圾回收器" style="max-width:100%;height:auto;margin-bottom:15px;" /><h1>初步了解</h1><ul><li>CMS GC是HotSpot虚拟机第一款真正意义上的并发收集器，第一次实现了垃圾收集线程与用户线程同时工作</li><li>CMS GC的特点是尽可能减少STW时间，即低延迟，Parallel是吞吐量优先</li><li>CMS采用<code>标记-清除算法</code>和<code>Stop The World</code>机制，在空间碎片化严重的时候也会使用一次<code>标记-压缩算法</code>来整理空间</li></ul><h1>工作流程</h1><p>CMS的工作流程大致分为七个步骤。</p><ol><li><p>初始标记阶段</p></li><li><p>并发标记阶段</p></li><li><p>预清理阶段</p></li><li><p>可被终止的预清理阶段</p></li><li><p>重新标记阶段</p></li><li><p>并发清除阶段</p></li><li><p>并发重置</p></li></ol><p><img src="https://img.yww52.com/2020/12/2020-12-13/img1.png" alt=""></p><h2>初始标记阶段</h2><p>这个阶段主要的工作就是标记出GC Roots能直接关联的对象，这里可分为两部分。</p><ul><li>标记老年代所有的GC Roots对象</li><li>标记年轻代中活着的对象引用到老年代的对象</li></ul><p>这个阶段因为涉及到了GC Roots的确定，所以会<code>发生STW</code>，为了减短STW时间，这个阶段的标记只是初始标记，之后还会继续标记。</p><h2>并发标记阶段</h2><p>由初始标记阶段标记到的对象开始寻找所有存活的对象，这个阶段是并发执行的，用户线程与标记线程同时进行，不会发生STW。</p><p>因为此时是用户线程和标记是并发执行的，所以有可能会出现一些情况，会导致有些对象会漏标。比如</p><ol><li>年轻代的对象变成老年代对象</li><li>直接在老年代分配对象</li><li>老年代的对象进行引用的更新</li></ol><p>除了这些常见的情况，还会有一些特殊情况，所以这个阶段的标记是不完整的，还需要后面标记。</p><p>为了提高后面的重新标记的效率，该阶段还会把上述漏标的对象所在的Card标识为Dirty，后续重新标记的时候就只需要查看这些标记为Dirty的对象。</p><p>:::note{type=&quot;warning&quot;}</p><p>这个阶段会发生Concurrent Mode Failure。之后还会讲到。</p><p>:::</p><h2>预清理阶段</h2><p>这个阶段就是用来处理上个阶段标识为Dirty的对象，也是并发发生的，并找到引用标识为Dirty的对象的存活对象进行标记，最后清除Dirty标识。</p><h2>可终止的预处理阶段</h2><p>这个阶段相当于提前做一些下个阶段的工作，尽可能的减少<code>STW</code>，还会处理一些Dirty的对象，这个阶段是并发的。</p><p>这个阶段默认持续的时间是5秒，即时间超过5秒，就会终止这个阶段，或者当eden区使用内存值小于CMSScheduleRemarkEdenPenetration，默认50%时，也会退出这个阶段。</p><p>可终止的预处理阶段就是为了减少重新标记阶段的时间，当在可终止的预处理阶段时碰上了<code>yound GC</code>的时候，就会大大减少下个阶段的<code>STW</code>，因为重新标记阶段是扫描整个堆的，所以碰上了<code>yound GC</code>就会减少扫描年轻代的压力。</p><h2>重新标记阶段</h2><p>这个阶段是CMS最后的标记阶段，所以这个阶段的主要任务就是标记所有老年代的存活对象。</p><p>这个阶段的主要任务如下：</p><ol><li>扫描GC Roots的引用链，确定老年代存活对象</li><li>扫描整个年轻代，查找是否有年轻代对象引用了老年代</li><li>处理所有之前遗留下来的Dirty的对象</li></ol><p>因为这个阶段需要确定所有老年代的存活对象，所以不能在并发处理了，不然又会有其他引用变化导致标记不全，所以这个阶段是会发生<code>STW</code>的，而且这个停顿时间是整个CMS垃圾回收里时间最长的一个。</p><h2>并发清除阶段</h2><p>清除那些“死了”的对象，因为这个阶段是标记-清除算法中的清除阶段，不涉及移动对象，所以是并发执行的。</p><p>因为这个阶段是并发的，有可能也会产生垃圾，但是这次GC的标记阶段已经过去了，所以就只能下次GC的时候清理了。</p><p>因为CMS是标记-清除算法，所以这个阶段也会产生一些内存碎片。</p><h2>并发重置阶段</h2><p>这个阶段会重置CMS的一些数据结构，为下次GC做准备。</p><h1>注意点</h1><h2>CMS不是Full GC</h2><p>虽然CMS会扫描到年轻代的对象，但是扫描年轻代的对象只是为了确定老年代中的对象是否真正死亡或者存活（是否被引用），CMS只会处理老年代的死亡对象，所以注意和Full GC进行区分。</p><h2>CMS回收过程会发生STW</h2><p>尽管CMS是并发回收，但是也涉及到可达性分析算法，所以一定会发生STW，只是CMS将部分标记的过程与用户线程并行，尽可能地减短了STW的时间。</p><p>CMS发生STW的阶段只有两个，初始标记阶段和重新标记阶段，主要的停顿时间是重新标记阶段，因为这里是要确定老年代所有的存活对象，扫描了整个堆。</p><p>CMS尽量做到低延迟，但因此丢弃了部分吞吐量，适合用在一些服务器上。</p><h2>内存碎片</h2><p>因为CMS采用的是标记-清除算法，所以垃圾收集结束后就会产生大量内存碎片。内存碎片过多会给大对象分配带来很大的麻烦，往往会出现老年代还有很多的剩余空间，但就是无法找到足够大的连续空间来分配大对象的内存，从而不得不提前触发一次<code>Full GC</code>。所以CMS需要设当的进行内存整理，而不是轻易的进行<code>Full GC</code>，可以搭配两个参数进行优化。</p><ul><li>-XX:+UseCMSCompactAtFullCollection（默认是开启的，JDK9后删除）</li><li>-XX:CMSFullGCsBeforeCompaction=n（JDK9后删除）</li></ul><p>详细介绍可以参考下面的参数讲解。</p><h2>Concurrent Mode Failure</h2><p>因为CMS是并发回收的，所以用户线程也是会有可能在GC的过程中消耗内存，年轻代会在一些情况下就会将部分年轻代存活的对象放入老年代，而此时老年代没有足够的空间，此时就会出现<code>Concurrent Mode Failure</code>。</p><p>为了解决整个问题CMS一般都会设置一个阈值，当堆内存使用率到达一定阈值的时候，就会开始初始标记阶段，而不是堆内存快不够用的时候发生GC。将GC的时间提前，尽量减少<code>Concurrent Mode Failure</code>的出现。</p><p>此时的虚拟机就会触发STW启动Serial Old GC来重新进行老年代的垃圾回收，这样停顿时间会更长。这时候Serial Old GC也会因此进行一次碎片整理，解决标记-清除出现的内存碎片化严重的情况。</p><h2>Promotion Failed</h2><p>在进行Minor GC的时候，Survivor空间不足，只能将年轻代的一些对象放入老年代，此时老年代没有足够内存空间存放这些对象，又或者是内存碎片严重，找不到足够的连续的空间存放对象，与<code>Concurrent Mode Failure</code>是有点类似的。</p><h1>CMS的参数</h1><ul><li><p>-XX:UseConcMarkSweepGC指定CMS为老年代的垃圾收集器，开启这个参数设置之后，就会使用ParNew为新生代垃圾回收器，CMS为老年代的垃圾回收器，Serial Old为后备回收器的组合方案。</p></li><li><p>-XX:CMSInitiatingOccupancyFraction=n</p><p>使用这个参数表示，当老年代的内存使用达到n%的时候就开始回收。JDK5以前的版本默认n是68，JDK6之后默认是92。</p><p>在内存使用增长慢的时候，可以设置大一点的阈值，这样可以减少GC的触发频率，如果增长快，可以设置低一点，避免频繁使用老年代串行收集器。</p><blockquote><p>这个参数设置的好，则可以减少Full GC的触发频率，有效解决Concurrent Mode Failure的问题。</p></blockquote></li><li><p>-XX:+UseCMSCompactAtFullCollection（默认是开启的，JDK9后删除）</p><p>用于在CMS收集器不得不进行<code>Full GC</code>时开启内存碎片的合并整理过程，由于内存整理必须移动存活的对象，（在Shenandoah和ZGC出现之前）这个操作是无法并发的，所以又会使停顿时间变长，从而解决内存碎片的问题。为了减少内存整理的停顿时间，可以配合下面这个参数来进行控制内存整理触发频率。</p></li><li><p>-XX:CMSFullGCsBeforeCompaction=n（JDK9后删除）设置在执行了n次Full GC之后,就会对内存进行压缩整理，和XX:+UseCMSCompactAtFullCollection参数搭配使用。</p><blockquote><p>XX:CMSFullGCsBeforeCompaction=n和XX:+UseCMSCompactAtFullCollection参数搭配使用l可以有效解决内存碎片的问题。</p></blockquote></li><li><p>-XX:ParallelGCThreads=n这个参数是设置ParallelGC的线程数的，CMS默认的启动线程数是<code>(ParallelGCThread+3)/4</code></p></li><li><p>-XX:+CMSScavengeBeforeRemark</p><p>在执行remark操作之前先做一次young GC，因为重新标记阶段是会扫描整个堆的，young GC会帮助扫描年轻代，减少重新标记的STW时间。</p><blockquote><p>这个参数可以解决remark阶段停顿时间过长的问题。</p></blockquote></li></ul><h1>参考文章</h1><ul><li><a href="https://blog.csdn.net/houjinxin/article/details/90322817">https://blog.csdn.net/houjinxin/article/details/90322817</a></li><li><a href="https://blog.csdn.net/zqz_zqz/article/details/70568819">https://blog.csdn.net/zqz_zqz/article/details/70568819</a></li><li><a href="https://www.cnblogs.com/webor2006/p/11110263.html">https://www.cnblogs.com/webor2006/p/11110263.html</a></li><li><a href="https://cloud.tencent.com/developer/article/1624694">https://cloud.tencent.com/developer/article/1624694</a></li></ul>]]></content:encoded>
            <category domain="https://yww52.com/categories/%25E6%2588%2591%25E7%259A%2584%25E6%258A%2580%25E6%259C%25AF%25E5%25AD%25A6%25E4%25B9%25A0%25E7%2589%25A9%25E8%25AF%25AD%25E6%259E%259C%25E7%2584%25B6%25E6%259C%2589%25E9%2597%25AE%25E9%25A2%2598/">我的技术学习物语果然有问题</category>
        </item>
        <item>
            <title>腾讯精选练习50题</title>
            <link>https://yww52.com/posts/32849422/</link>
            <guid isPermaLink="true">https://yww52.com/posts/32849422/</guid>
            <pubDate>Sat, 12 Dec 2020 00:00:00 +0000</pubDate>
            <description>LeetCode上的腾讯精选练习50题的专题的刷题笔记。</description>
            <content:encoded><![CDATA[<img src="https://img.yww52.com/2020/12/2020-12-12top_img.jpg" alt="腾讯精选练习50题" style="max-width:100%;height:auto;margin-bottom:15px;" /><h1>7.整数反转</h1><p>利用long来进行中转。</p><pre><code class="language-Java">class Solution {    public int reverse(int x) {        long res = 0;        while (x != 0) {            res = res * 10 + x % 10;            x = x/10;        }        return (int)res == res?(int)res : 0;    }}</code></pre><h1>9. 回文数</h1><h2>解法一</h2><p>回文数，其实跟回文字符串差不多，只是处理不同而已，我这里就直接将数字转换成了字符串然后对比。</p><pre><code class="language-Java">class Solution {    public boolean isPalindrome(int x) {        if(x &lt; 0) return false;        if (x &lt; 10) return true;        String str = String.valueOf(x);        for(int i = 0; i &lt; str.length()/2; i++) {            if(str.charAt(i) != str.charAt(str.length() - 1 - i))                return false;        }        return true;    }}</code></pre><p>也可以直接将字符串反转在和原来的字符串相对比，不过效率第一点。</p><pre><code class="language-Java">class Solution {    public boolean isPalindrome(int x) {        if(x &lt; 0) return false;        if (x &lt; 10) return true;        String str = String.valueOf(x);        String rev = new StringBuffer(str).reverse().toString();        if(rev.equals(str))             return true;        else             return false;    }}</code></pre><h2>解法二</h2><p>直接求出反转后的数字，然后比较是否相等。虽然会有溢出的情况，但是会溢出的就不是回文了，所以可以不用考虑溢出的情况，实在要考虑可以直接来个<code>try-catch</code>来捕获异常。</p><pre><code class="language-Java">class Solution {    public boolean isPalindrome(int x) {        if(x &lt; 0) return false;        int rev = 0;        int p = x;        int temp = 0;        while(p!=0){            temp = p % 10;            rev = rev * 10+temp;            p = p/10;        }        return x == rev;    }}</code></pre><h1>11. 盛最多水的容器</h1><h2>解法一</h2><p>因为这道题可以不用管中间的高度，所以直接嵌套for循环找出最大面积就好了，不过效率很低。明显看出这是O(n^2)的算法，应该算是效率最差的算法了。</p><pre><code class="language-Java">class Solution {    public int maxArea(int[] height) {        int res = Integer.MIN_VALUE;        for(int i = 0; i &lt; height.length; i++) {            for(int j = i; j &lt;height.length; j++) {                if(height[j] &lt;= height[i]) {                    int val = height[j] * (j - i);                    if(val &gt; res) res = val;                } else {                    int val = height[i] * (j - i);                    if(val &gt; res) res = val;                }            }        }        return res;    }}</code></pre><h2>解法二</h2><p>这题明显可以使用双指针的方法。不断选取高度最高的值，并且长方形面积尽量大。最坏的时间复杂度为O(n)。</p><pre><code class="language-Java">class Solution {    public int maxArea(int[] height) {        int i = 0;        int j = height.length - 1;        int res = 0;        while(i &lt; j) {            int val = Math.min(height[i],height[j]) * (j - i);            if (val &gt; res) res = val;            //选择高度更高的逼急            if(height[i] &lt; height[j]) i++;            else j--;         }        return res;    }}</code></pre><h1>14.最长公共前缀</h1><p>最开始是直接两个for循环的，但是看到了别人的解法，只能流出智商低的类水。</p><pre><code class="language-Java">class Solution {    public String longestCommonPrefix(String[] strs) {        if(strs.length == 0) return &quot;&quot;;        String s = strs[0];        for (String str : strs) {            while(!str.startsWith(s)) {                if (s.length() == 0) return &quot;&quot;;                s = s.substring(0,s.length() - 1);            }        }        return s;    }}</code></pre><h1>20.有效的括号</h1><p>这经典题目了，直接用栈就可以判断了。</p><pre><code class="language-Java">class Solution {    public boolean isValid(String s) {        Stack&lt;Character&gt; stack = new Stack&lt;&gt;();        for(char c : s.toCharArray()) {            if (c == &#39;(&#39;) stack.push(&#39;)&#39;);            else if (c == &#39;[&#39;) stack.push(&#39;]&#39;);            else if (c == &#39;{&#39;) stack.push(&#39;}&#39;);            else if (stack.isEmpty() || c != stack.pop()) return false;        }        return stack.isEmpty();    }}</code></pre><h1>21. 合并两个有序的链表</h1><p>这个题因为两个都是有序的连边，所以一个个赋值就可以了。</p><pre><code class="language-Java">class Solution {    public ListNode mergeTwoLists(ListNode l1, ListNode l2) {        ListNode pre = new ListNode();        ListNode res = pre;        while(l1 != null &amp;&amp; l2 != null) {                if(l1.val &gt;= l2.val) {                    pre.next = new ListNode(l2.val);                    l2 = l2.next;                } else {                    pre.next = new ListNode(l1.val);                    l1 = l1.next;                }                pre = pre.next;        }        if(l1 == null) {            pre.next = l2;        }        if(l2 == null) {            pre.next = l1;        }        return res.next;    }}</code></pre><p>还有一种递归写法。</p><pre><code class="language-Java">class Solution {    public ListNode mergeTwoLists(ListNode l1, ListNode l2) {        if(l1 == null) return l2;        if(l2 == null) return l1;        ListNode res;        if(l1.val &gt; l2.val) {            res = l2;            l2 = l2.next;        } else {            res = l1;            l1 = l1.next;        }        res.next = mergeTwoLists(l1,l2);        return res;    }}</code></pre><h1>26. 删除排序数组中的重复项</h1><p>这题考虑的是原地修改数组，那就把这个数组当作一个新的数组就好了，碰到不相同的就放到前面。所以就有了这个算法。</p><pre><code class="language-Java">class Solution {    public int removeDuplicates(int[] nums) {        if (null == nums || nums.length == 1) return nums.length;        int res = 1;        for(int i = 1;i &lt; nums.length; i++) {            if(nums[i] != nums[i - 1]) {                nums[res] = nums[i];                res++;            }        }        return res;    }}</code></pre><h1>46.全排列</h1><p>看题目就知道，一道典型的全排列的题目，我就用了递归的写法。不过这个写法遇到有重复的数字的时候会有重复的数据，这题没有重复数据所以不需要去重，要是有重复数据可以需要考虑去重。</p><pre><code class="language-Java">class Solution {    List&lt;List&lt;Integer&gt;&gt; res = new ArrayList&lt;&gt;();    public List&lt;List&lt;Integer&gt;&gt; permute(int[] nums) {        fun(nums,0);        return res;    }    public void fun(int[] nums,int start) {        if(start == nums.length -1){            List&lt;Integer&gt; list = new ArrayList&lt;&gt;();            for(int i:nums) list.add(i);            res.add(list);        }        for(int i = start;i &lt; nums.length; i++) {            swap(nums,i,start);            fun(nums,start + 1);            swap(nums,i,start);        }    }    public void swap(int[] nums,int i,int j) {        int temp = nums[i];        nums[i] = nums[j];        nums[j] = temp;    }}</code></pre><h1>53. 最大子序和</h1><p>这题一开始我用的是双指针逼近，但是发现无法确定逼近的条件，所以就放弃了这个想法。</p><p>实在没想出。看了下答案，发现不是题难，是我太菜了。</p><p>其实这题用动态规划来解是最好的。</p><p>首先确定一下状态，那就是子数组的和最大。</p><p>然后是状态变化，因为是求子数组的和最大，所以开头是不可能是负的，如果是正的，但是加了后面的负数也是会抵消。</p><pre><code class="language-Java">class Solution {    public int maxSubArray(int[] nums) {        int res = nums[0];        int sum = 0;        for(int num : nums) {            //状态变化            if(sum &gt; 0) sum += num;            else sum = num;            //保存状态            res = Math.max(res,sum);        }        return res;    }}</code></pre><h1>59. 螺旋矩阵 II</h1><p>这道题以前写过类似的，可以直接一个个的赋值，这题的难点就是边界的判断，在while循环中很容易就超出数组的边界了，或者是转过头了没刹住，注意好边界条件就不难了。</p><pre><code class="language-Java">class Solution {    public int[][] generateMatrix(int n) {        int[][] res = new int[n][n];        int count = 1;        int x = 0,y = 0;        res[0][0] = 1;        while(count != n*n) {            //  向右移动            while(y + 1 &lt; n &amp;&amp; res[x][y + 1] == 0) {                res[x][y + 1] = ++count;                y++;             }            //  向下移动            while(x + 1 &lt; n &amp;&amp; res[x + 1][y] == 0) {                res[x + 1][y] = ++count;                x++;             }            //  向左移动            while(y - 1 &gt;= 0 &amp;&amp; res[x][y - 1] == 0) {                res[x][y - 1] = ++count;                y--;             }            //  向上移动            while(x - 1 &gt;= 0 &amp;&amp; res[x - 1][y] == 0) {                res[x - 1][y] = ++count;                x--;             }        }        return res;    }}</code></pre><h1>62. 不同路径</h1><p>一看题目，直接递归搜索。</p><pre><code class="language-Java">class Solution {    public int uniquePaths(int m, int n) {        return dfs(m,n);    }    public int dfs(int x,int y) {        if(x == 1 &amp;&amp; y == 1) return 1;        if(x == 1) return dfs(x,y - 1);        if(y == 1) return dfs(x -1,y);        return dfs(x - 1,y) + dfs(x,y - 1);    }}</code></pre><p>但是当图大的时候，就不行了，直接超时，所以不能直接搜索了，还要优化才行。因为当x或者y等于一的时候，就只有一条路了，所以可以进行优化一下。</p><pre><code class="language-Java">class Solution {    public int uniquePaths(int m, int n) {        return dfs(m,n);    }    public int dfs(int x,int y) {        if(x == 1 || y == 1) return 1;        return dfs(x - 1,y) + dfs(x,y - 1);    }}</code></pre><p>但还是超时了，所以直接递归应该是不可行。所以得考虑其他想法了，比如说动态规划。</p><pre><code class="language-Java">class Solution {    public int uniquePaths(int m, int n) {        int[][] dp = new int[m][n];        for(int i = 0; i &lt; m; i++) {            for(int j = 0; j &lt; n; j++) {                if(i == 0 || j == 0) dp[i][j] = 1;                else dp[i][j] = dp[i - 1][j] + dp[i][j - 1];            }        }        return dp[m - 1][n - 1];    }}</code></pre><p>虽然可以通过了，但是时间和空间的消耗还是挺多的。</p><p>看了一下评论区的解法，发现有一个解法就很有想法。那就是用排列组合的方式。</p><pre><code class="language-Java">class Solution {    public int uniquePaths(int m, int n) {       int N = m + n -2;       //因为行列互换答案是一样的，所以直接找最小值使用       int M = Math.min(m-1,n-1);       // 使用int类型就会溢出了，所以要用long类型       long res = 1;       for(int i = 0; i &lt; M; i++) {           //C = N! / (M! + (N -M)!)简化后得到下面这个式子           res = res * (N - i) / (i + 1);       }       return (int)res;    }}</code></pre><h1>70.爬楼梯</h1><p>这题的本质就是斐波那契数列。</p><p>首先就是dp数组。</p><pre><code class="language-Java">class Solution {    public int climbStairs(int n) {        if (n &lt;= 2) return n;        int[] dp = new int[n + 1];        dp[1] = 1;        dp[2] = 2;        for (int i = 3;i &lt; n + 1; i++) {            dp[i] = dp[i - 1] + dp[i - 2];        }        return dp[n];    }}</code></pre><p>直接递归，不需要存储状态。</p><pre><code class="language-Java">class Solution {    public int climbStairs(int n) {        if (n &lt;= 2) return n;        int a = 1;        int b = 2;        for (int i = 3;i &lt; n + 1; i+=2) {            a = a + b;            b = a + b;        }        if (n % 2 == 0) return b;        else return a;    }}</code></pre><h1>78. 子集</h1><p>这题的思路就是遍历数组，遇到新的元素，就把这个元素加到所有子集里，形成新的子集。</p><pre><code class="language-Java">class Solution {    public List&lt;List&lt;Integer&gt;&gt; subsets(int[] nums) {        List&lt;List&lt;Integer&gt;&gt; res = new ArrayList&lt;&gt;();        res.add(new ArrayList&lt;&gt;());        for(int i = 0;i &lt; nums.length; i++) {            int size = res.size();            for(int j = 0; j &lt; size; j++) {                List&lt;Integer&gt; list = new ArrayList&lt;&gt;(res.get(j));                list.add(nums[i]);                res.add(list);            }        }        return res;    }}</code></pre><h1>88. 合并两个有序数组</h1><h2>解法一</h2><p>直接就把这两个数组合并，然后在排一次序就好了。这应该是最简单，也是最花时间的算法。</p><pre><code class="language-Java">class Solution {    public void merge(int[] nums1, int m, int[] nums2, int n) {        int re = 0;        for(int i = m; i &lt; m + n; i++) {            nums1[i] = nums2[re];            re++;        }        Arrays.sort(nums1);    }}</code></pre><h2>解法二</h2><p>可以使用类似双指针的方法，选取最大的依次放入数组。</p><pre><code class="language-Java">class Solution {    public void merge(int[] nums1, int m, int[] nums2, int n) {        int temp = m + n - 1;        int i = m - 1;        int j = n - 1;        while(i &gt;=0 &amp;&amp; j &gt;= 0) {            if(nums1[i] &lt; nums2[j]) {                nums1[temp] = nums2[j];                temp--;                j--;            } else{                nums1[temp] = nums1[i];                temp--;                i--;            }        }        while(j &gt;= 0) {            nums1[temp] = nums2[j];            temp--;            j--;        }    }}</code></pre><p>简化之后可以得到。</p><pre><code class="language-Java">class Solution {    public void merge(int[] nums1, int m, int[] nums2, int n) {        int temp = m + n - 1;        int i = m - 1;        int j = n - 1;        while(i &gt;=0 &amp;&amp; j &gt;= 0) {            nums1[temp--] = nums1[i] &lt; nums2[j] ? nums2[j--] : nums1[i--];        }        while(j &gt;= 0) {            nums1[temp--] = nums2[j--];        }    }}</code></pre><h1>89. 格雷编码</h1><p>这题真的不会写了，题目描述的不是很清楚，看的都不是很明白，直接查了维基百科，看懂了格雷码转二进制数的规律之后，就很简单了。</p><pre><code>由于G(n) = B(n+1) + B(n)故而B(n) = -B(n+1)+ G(n)自高位至低位运算即可，无需考虑借位。例： 格雷码0111，为4位数，故设二进制数自第5位至第1位分别为：0 b3 b2 b1 b0。b3= 0-0 =0b2=b3-1=0-1=1b1=b2-1=1-1=0b0=b1-1=0-1=1因此所转换为之二进制码为0101</code></pre><pre><code class="language-Java">class Solution {    public List&lt;Integer&gt; grayCode(int n) {        List&lt;Integer&gt; res = new ArrayList&lt;&gt;();        int len = 1&lt;&lt;n;        for(int i = 0; i &lt; len; i++) {            res.add(i ^ i&gt;&gt;1);        }        return res;    }}</code></pre><h1>136. 只出现一次的数字</h1><p>这题直接用异或就好了，因为异或运算符和下面这样的规律。</p><ol><li>异或符和交换律</li><li>n与0异或答案为n</li><li>两个相同的数异或答案为0</li></ol><pre><code class="language-Java">class Solution {    public int singleNumber(int[] nums) {        int res = 0;        for(int i:nums) res ^= i;        return res;    }}</code></pre><h1>141.环形列表</h1><p>这题可以先直接遍历，然后判断是否有重复的就好了。</p><pre><code class="language-Java">public class Solution {    public boolean hasCycle(ListNode head) {        Set&lt;ListNode&gt; set = new HashSet&lt;&gt;();        ListNode node = head;        while(node != null) {            if (!set.add(node)) return true;            node = node.next;        }        return false;    }}</code></pre><p>见到一种很有趣的解法。将每一个的节点指向自己，有环就会出现指向自己的节点。</p><pre><code class="language-Java">public class Solution {    public boolean hasCycle(ListNode head) {        if (head == null || head.next == null) return false;        while(head.next != head) {            if (head == null || head.next == null) return false;            ListNode node = head.next;            head.next = head;            head = node;        }        return true;    }}</code></pre><p>快慢指针。</p><pre><code class="language-Java">public class Solution {    public boolean hasCycle(ListNode head) {        if (head == null || head.next == null) return false;        ListNode slow = head;        ListNode fast = head.next;        while(fast != slow) {            if (fast == null || fast.next == null) return false;            fast = fast.next.next;            slow = slow.next;        }        return true;    }}</code></pre><h1>142. 环形链表 II</h1><p>这题和上面的题其实差不多，只是要返回交接的点。</p><pre><code class="language-Java">public class Solution {    public ListNode detectCycle(ListNode head) {        if(head == null) return null;        if(head.next == null) return null;        ListNode p1 = head;        ListNode p2 = head;        while(p2.next != null &amp;&amp; p2.next.next != null) {                    p1 = p1.next;            p2 = p2.next.next;            if(p1 == p2) {                ListNode res = head;                while(res != p1) {                    p1 = p1.next;                    res = res.next;                }                return res;            }        }        return null;    }}</code></pre><h1>155. 最小栈</h1><p>这题直接就是数据结构了，但是不知道会存多少个数字，所以使用了<code>List</code>来当存储数组。</p><pre><code class="language-Java">class MinStack {    List&lt;Integer&gt; res  = new ArrayList&lt;&gt;();    int min = Integer.MAX_VALUE;    public MinStack() {}        public void push(int x) {        res.add(x);        if(min &gt; x) {            min = x;        }    }        public void pop() {        res.remove(res.size() - 1);        min = Integer.MAX_VALUE;        for(int num:res) {            if(num &lt; min) min = num;        }    }        public int top() {        return res.get(res.size() - 1);    }        public int getMin() {        return min;    }}</code></pre><p>题目要求常数时间内检索到最小元素，这个其实没算实现，因为这里获取最小数的时间复杂度加到了<code>pop</code>方法上了，而且这个的效率不是很好。</p><p>所以有了下面种写法，用链表的方法，每一个链表都存储一个最小值，这样pop的时候也会自动变更最小值。</p><pre><code class="language-Java">class MinStack {    private class ListNode{        int min;        int val;        ListNode next;        public ListNode(int val,int min) {            this.val = val;            this.min = min;        }        public ListNode(int val,int min,ListNode next) {            this.val = val;            this.min = min;            this.next = next;        }    }    private ListNode head;    public void push(int x) {        if(head == null) {            head = new ListNode(x,x);        } else {            //使用头插，因为尾插不好pop            head = new ListNode(x,x &gt; head.min?head.min:x,head);        }    }        public void pop() {        head = head.next;    }        public int top() {        return head.val;    }        public int getMin() {        return head.min;    }    public MinStack() {}}</code></pre><h1>160. 相交链表</h1><p>这题我原本是想用两个指针一个个遍历，可是这两个链表不是等长的，所以找不到那个相交点，用for嵌套for又不符合题目要求，所以没写出来，看到人家的答案才知道，可以把这两个链表连起来，连起来这两个链表就等长了，双指针遍历就行。</p><pre><code class="language-Java">public class Solution {    public ListNode getIntersectionNode(ListNode headA, ListNode headB) {        if(headA == null || headB == null) return null;        ListNode a = headA,b = headB;        while(a != b) {            a = a==null ? headB : a.next;            b = b==null ? headA : b.next;        }        return a;    }}</code></pre><h1>169. 多数元素</h1><p>这题写了几次了，可以用摩尔投票法来找出大于一半的元素。</p><pre><code class="language-Java">class Solution {    public int majorityElement(int[] nums) {        if (nums.length == 1) return nums[0];        int count = 1,res = nums[0];        for(int i = 1;i &lt; nums.length; i++) {            if(nums[i] == res) count++;            if(nums[i] != res) {                count--;                if(count == 0) res = nums[i + 1];            }        }        return res;    }}</code></pre><p>还看到有一个写法，因为是多数元素，所以一定是跨过中位数的，所以直接排序，寻找中位数就可以了。</p><pre><code class="language-Java">class Solution {    public int majorityElement(int[] nums) {        Arrays.sort(nums);        return nums[(nums.length/2)];    }}</code></pre><h1>206. 反转链表</h1><p>反转链表也算简单，注意要存储一下下一个变量，不然引用方向一变链表断开就找不到之后的值了。</p><h2>递归写法</h2><pre><code class="language-Java">class Solution {    public ListNode reverseList(ListNode head) {        return reverse(null,head);    }    private static ListNode reverse(ListNode prev,ListNode curr){        if(curr==null) return prev;        ListNode node = curr.next;        curr.next = prev;        return reverse(curr,node);    }}</code></pre><p>另一种写法。</p><pre><code class="language-Java">class Solution {    public ListNode reverseList(ListNode head) {        if(head == null) return null;        if (head.next == null) return head;        ListNode last = reverseList(head.next);        head.next.next = head;        head.next = null;        return last;    }}</code></pre><h2>迭代写法</h2><pre><code>class Solution {    public ListNode reverseList(ListNode head) {        ListNode prev = null;        ListNode curr = head;        while(curr != null) {            ListNode node = curr.next;            curr.next = prev;            prev = curr;            curr = node;        }        return prev;    }}</code></pre><h1>215 数组中的第K个最大元素</h1><p>这道题很简单，直接排序取第K大就好了。</p><pre><code class="language-java">class Solution {    public int findKthLargest(int[] nums, int k) {        Arrays.sort(nums);        return nums[nums.length - k];    }}</code></pre><p>看了下评论，发现用库函数都能被骂，真的是服了，那顺便手写下快排。</p><pre><code class="language-Java">class Solution {    public int findKthLargest(int[] nums, int k) {        quicksort(nums,0,nums.length - 1);        return nums[nums.length - k];    }    public void quicksort(int[] nums,int left,int right) {        if(left &gt; right) return;        int temp = nums[left];        int i = left;        int j = right;        while(i != j) {            while(nums[j] &gt;= temp &amp;&amp; i &lt; j) j--;            while(nums[i] &lt;= temp &amp;&amp; i &lt; j) i++;            if(i &lt; j) {                nums[i] ^= nums[j];                nums[j] ^= nums[i];                nums[i] ^= nums[j];            }        }        nums[left] = nums[i];        nums[i] = temp;        quicksort(nums,left,i - 1);        quicksort(nums,i + 1,right);    }}</code></pre><h1>217. 存在重复元素</h1><h2>解法一</h2><p>一开始是直接暴力破解，两个for循环，发现超时了，所以就想到了先排序，然后一个for循环查看附近的值是否相同。</p><pre><code class="language-Java">class Solution {    public boolean containsDuplicate(int[] nums) {        Arrays.sort(nums);        for(int i = 1; i &lt; nums.length; i++) {            if(nums[i] == nums[i-1]) return true;        }        return false;    }}</code></pre><h2>解法二</h2><p>可以直接用Set去重，判断set集合中的元素与数组元素的个数是否相同就可以得到答案。</p><pre><code class="language-Java">class Solution {    public boolean containsDuplicate(int[] nums) {        Set&lt;Integer&gt; set = new HashSet&lt;&gt;();        for(int i:nums) {            set.add(i);        }                return set.size()==nums.length?false:true;    }}</code></pre><p>改进一下就是加入失败的时候就直接跳出，可以像这么写。</p><pre><code class="language-Java">class Solution {    public boolean containsDuplicate(int[] nums) {        Set&lt;Integer&gt; set = new HashSet&lt;&gt;();        for(int i:nums) {            if(!set.add(i)) return true;        }        return false;    }}</code></pre><h1>230. 二叉搜索树中第K小的元素</h1><p>这道题和之前写的那道<code>剑指Offer54</code>是有点相似的，指路------&gt;<a href="https://yww52.com/2020/11/17/%E6%A0%91/">这里</a></p><p>这里我就不写这么详细了，就简单写两个方法。</p><pre><code class="language-Java">//中序遍历取第k个值class Solution {    List&lt;Integer&gt; list = new ArrayList&lt;&gt;();    public int kthSmallest(TreeNode root, int k) {        inorder(root);        return list.get(k-1);    }    public void inorder(TreeNode root) {        if(root == null) return;        inorder(root.left);        list.add(root.val);        inorder(root.right);    }}</code></pre><pre><code class="language-Java">class Solution {    int res = 0,sum = 0;    public int kthSmallest(TreeNode root, int k) {        inorder(root,k);        return res;    }    public void inorder(TreeNode root,int k) {        if(root.left != null) {        inorder(root.left,k);           }        sum++;        if(sum == k) {            res = root.val;        }        if(root.right != null) {        inorder(root.right,k);         }    }}</code></pre><h1>231. 2的幂</h1><p>这题真的看起来简单，又没怎么简单，想了好几个方法全是超时的，想到是用二进制了，但是还是不懂写只能借鉴别人的方法了，看了之后只能说太强了。</p><h2>解法一</h2><p>满足<code>n &amp; -n == n</code>就是2的幂。</p><p>在二进制下，如果这个数是2的幂，那么它的二进制位上就会只有一个1。比如1是1，2是10，4是100，8是1000。</p><p>它的负数，在二进制是补码，所以会在各个位数上取反然后加1,这样就只有左边全为1右边全为0 。比如1是.....1111..1，2是...1110，4是...1100,8是....111000。</p><p>所以按位与就会是本身。</p><pre><code class="language-Java">class Solution {    public boolean isPowerOfTwo(int n) {    return (n &gt; 0) &amp;&amp; (n &amp; -n) == n;    }}</code></pre><h2>解法二</h2><p>满足<code>(1&lt;&lt;30) % n == 0</code>就是2的幂。</p><p>1 &lt;&lt; 30位是最大的2的幂，对n取余如果等于0就表示n是2的幂。</p><pre><code class="language-Java">class Solution {    public boolean isPowerOfTwo(int n) {    return (n &gt; 0) &amp;&amp; (1 &lt;&lt; 30) % n == 0;    }}</code></pre><h1>236. 二叉树的最近公共祖先</h1><pre><code class="language-Java">class Solution {    public TreeNode lowestCommonAncestor(TreeNode root, TreeNode p, TreeNode q) {        if(root == null || root == p || root == q) return root;        TreeNode left = lowestCommonAncestor(root.left, p, q);        TreeNode right = lowestCommonAncestor(root.right, p, q);        if(left == null) return right;        if(right == null) return left;        return root;    }}</code></pre><h1>237. 删除链表中的节点</h1><p>这道题，emm，描述的就离谱，其实就是给定一个链表的你要删掉的结点，删掉它。</p><pre><code class="language-Java">class Solution {    public void deleteNode(ListNode node) {        node.val = node.next.val;        node.next = node.next.next;    }}</code></pre><h1>238. 除自身以外数组的乘积</h1><p>这题不难，只是题目要求时间复杂度为O(n)和不能使用除法。这样的话就只能单个的for循环了，所以就可以考虑从左开始乘到当前树（不包含当前数），然后再加个for循环，再乘右边。这种方法速度很快，但是内存消耗太多，暂时还找不到内存消耗小的方法。</p><pre><code class="language-Java">class Solution {    public int[] productExceptSelf(int[] nums) {        int len = nums.length;        int[] output = new int[len];        int left = 1;        int right = 1;        for(int i =0;i &lt; len; i++) {            output[i] = left;            left = left * nums[i];        }        for(int i = len - 1; i &gt;= 0; i--) {            output[i] = output[i] * right;            right = right * nums[i];        }        return output;    }}</code></pre><h1>292. Nim 游戏</h1><p>这题有点搞，我大费周章的想了很久，结果看到评论我傻了，这就是一道脑经急转弯。</p><p>这就是巴什博弈，先手的话，只要是n%(m+1)==0的时候就是一定会输。反之就一定是赢的。在两个人都是最优解的情况。</p><pre><code class="language-Java">class Solution {    public boolean canWinNim(int n) {        if(n%4==0) {            return false;        }        return true;    }}</code></pre><p>简化一下就一行</p><pre><code class="language-java">class Solution {    public boolean canWinNim(int n) {        return n%4!=0;    }}</code></pre><h1>344. 反转字符串</h1><p>这个就直接用遍历双指针遍历就好了，不同的就是交换变量的方式而已。</p><pre><code class="language-Java">class Solution {    public void reverseString(char[] s) {        int front = 0;        int rear = s.length-1;        while(front &lt; rear) {            s[front] ^= s[rear];            s[rear] ^= s[front];            s[front] ^= s[rear];            front++;            rear--;         }    }}</code></pre><pre><code class="language-java">class Solution {    public void reverseString(char[] s) {        int front = 0;        int rear = s.length-1;        while(front &lt; rear) {            char c = s[front];            s[front] = s[rear];            s[rear] = c;            front++;            rear--;         }    }}</code></pre><h1>557. 反转字符串中的单词 III</h1><h2>解法一</h2><p>这题就是先用<code>split方法</code>分割字串，然后可以用StringBuffer的字符串反转方法进行反转。</p><pre><code class="language-Java">class Solution {    public String reverseWords(String s) {        String[] arr = s.split(&quot; &quot;);        StringBuffer buffer = new StringBuffer();        for(int i = 0; i &lt; arr.length; i++) {            if(i == arr.length-1) {            arr[i] = new StringBuffer(arr[i]).reverse().toString();            res = res + arr[i];             continue;            }            arr[i] = new StringBuffer(arr[i]).reverse().toString();            res = res + arr[i] + &quot; &quot;;        }        return res;    }}</code></pre><p>这样效率是很低，字符串这样拼接不好，建议使用StringBuffer。时间和内存消耗效率都很低。</p><h2>解法二</h2><p>先用<code>split方法</code>分割字串，使用StringBuffer进行拼接；</p><pre><code class="language-Java">class Solution {    public String reverseWords(String s) {        String[] arr = s.split(&quot; &quot;);        StringBuffer res = new StringBuffer();        for(int i = 0; i &lt; arr.length; i++) {            res.append(new StringBuffer(arr[i]).reverse().toString());            res.append(&quot; &quot;);        }        //trim去除首尾空格        return res.toString().trim();    }}</code></pre><h2>解法三</h2><p>直接在原数组上进行反转。</p><pre><code class="language-Java">class Solution {    public String reverseWords(String s) {        char[] arr = s.toCharArray();        int front = 0;        int rear = 0;        for(int i = 0;i &lt; s.length(); i++) {            if(arr[i] == &#39; &#39;) {                rear = i - 1;                while(front &lt; rear) {                    arr[front] ^= arr[rear];                    arr[rear] ^= arr[front];                    arr[front] ^= arr[rear];                    front++;                    rear--;                }                front = i + 1;            }        }        //反转最后一个单词        rear = s.length()-1;        while(front &lt; rear) {                arr[front] ^= arr[rear];                arr[rear] ^= arr[front];                arr[front] ^= arr[rear];                front++;                rear--;            }            return new String(arr);    }}</code></pre><p>在优化一下</p><pre><code class="language-Java">class Solution {    public String reverseWords(String s) {        char[] arr = s.toCharArray();        int front = 0;        for(int i = 0;i &lt; s.length(); i++) {            if(arr[i] == &#39; &#39;) {                reverse(arr,front,i - 1);                front = i + 1;            }        }        //反转最后一个单词        reverse(arr,front,s.length()-1);        return new String(arr);    }    public void reverse(char[] arr,int front,int rear){        while(front &lt; rear) {            arr[front] ^= arr[rear];            arr[rear] ^= arr[front];            arr[front] ^= arr[rear];            front++;            rear--;        }    }}</code></pre><p>另外一个搜索的思路</p><pre><code class="language-Java">class Solution {    public String reverseWords(String s) {        char[] arr = s.toCharArray();        for(int i = 0;i &lt; s.length(); i++) {            if(arr[i] == &#39; &#39;) {                continue;            }            int front = i;            while(i &lt; s.length() &amp;&amp; arr[i]!=&#39; &#39;) {                i++;            }            reverse(arr,front,i - 1);        }        return new String(arr);    }    public void reverse(char[] arr,int front,int rear){        while(front &lt; rear) {            arr[front] ^= arr[rear];            arr[rear] ^= arr[front];            arr[front] ^= arr[rear];            front++;            rear--;        }    }}</code></pre>]]></content:encoded>
            <category domain="https://yww52.com/categories/%25E6%2588%2591%25E7%259A%2584%25E6%258A%2580%25E6%259C%25AF%25E5%25AD%25A6%25E4%25B9%25A0%25E7%2589%25A9%25E8%25AF%25AD%25E6%259E%259C%25E7%2584%25B6%25E6%259C%2589%25E9%2597%25AE%25E9%25A2%2598/">我的技术学习物语果然有问题</category>
        </item>
        <item>
            <title>GC垃圾回收机制</title>
            <link>https://yww52.com/posts/202acec1/</link>
            <guid isPermaLink="true">https://yww52.com/posts/202acec1/</guid>
            <pubDate>Mon, 07 Dec 2020 00:00:00 +0000</pubDate>
            <description>Java程序在运行中难免会产生一些垃圾，所以Java就使用了GC垃圾回收机制来自动的进行垃圾回收。</description>
            <content:encoded><![CDATA[<img src="https://img.yww52.com/2020/12/2020-12-7/top_img.jpg" alt="GC垃圾回收机制" style="max-width:100%;height:auto;margin-bottom:15px;" /><h1>什么是垃圾</h1><p>在进行垃圾回收之前，就要先判断出什么是垃圾。</p><p>垃圾其实指的是在这次运行程序中没有被任何指针指向的对象，也就是没有被引用机会的对象，因为没有指向也就无法引用，所以这些对象就是要清理的垃圾。</p><p>如果垃圾不及时的清理，那么在这次运行程序的过程中就会一直占用部分内存，严重的还会导致<code>内存溢出</code>。</p><p>:::note{type=&quot;info&quot;}</p><p>内存溢出就是指程序运行过程中申请的内存大于系统所能够提供的内存，这就会导致无法申请足够的内存，于是就发生了内存溢出。</p><p>:::</p><h1>哪里需要垃圾回收</h1><p>垃圾回收就需要关注哪里产生垃圾，其实垃圾的产生主要在运行时数据区中的<code>方法区</code>和<code>堆</code>。因为栈是用完就会推出的，计数器也不需要回收，所以GC的作用区域就是方法区和堆了。虽然说这两块区域会有GC行为，但正常来说主要的还是堆空间的GC，方法区很少会有GC行为。</p><h1>垃圾回收的算法</h1><h2>垃圾标记阶段</h2><p>垃圾标记阶段就是为一些死的或者活的对象打上标记，用来区分哪些是垃圾，哪些是用的上的对象。判断对象存活的方法一般有两个，引用计数算法和可达性分析算法。</p><h3>引用计数算法</h3><p>计数算法其实就是为每个对象保存以个整型的引用计数器属性，每当该对象被引用了，改对象的引用计数器就会加1，引用失效了计数器就会减1，只要对象的计数器为0，即表示该对象已经不会被引用了，就会为它做上一个标记。</p><p>:::note{type=&quot;info&quot;}</p><p>引用计数算法是很简单高效的算法，但是对象一多，每个对象的计数器也是一个很大的开销。    而且引用计数算法还有一个很严重的问题,无法处理循环引用的情况，这是很致命的，所以Java的垃圾回收器没有使用这个算法。</p><p>:::</p><h4>循环引用问题</h4><p><img src="https://img.yww52.com/2020/12/2020-12-7/img1.png" alt=""></p><p><img src="https://img.yww52.com/2020/12/2020-12-7/img2.png" alt=""></p><p>从上图就可以看出对象的引用关系只要形成环形，很大的可能就会出现循环引用的问题，这就会出现<code>内存泄漏</code>的问题。</p><h4>分析Java是否使用引用计数算法</h4><ol><li><p>配置JVM的虚拟机参数，打印JVM的情况。我是使用IDEA，所以按照IDEA的配置来讲。点击RUN-&gt;Edit Configurations-&gt;Applacion-&gt;VM  options。配置以下参数</p><pre><code class="language-Java">-XX:+PrintGCDetails</code></pre></li><li><p>模拟循环引用的情况。</p><pre><code class="language-java">package com.yw;/** * @ClassName ReferenceCount * @Descriprtion TODO * @Author yww **/public class ReferenceCount {    //    设置引用属性    Object reference = null;    public static void main(String[] args) {        ReferenceCount A = new ReferenceCount();        ReferenceCount B = new ReferenceCount();    //    形成引用环        A.reference = B;        B.reference = A;    //    断开外部引用，A与B形成循环引用        A = null;        B = null;    }}</code></pre><pre><code class="language-Java">// 没有显示调用GC的内存情况Heap PSYoungGen      total 38400K, used 3330K [0x00000000d5f80000, 0x00000000d8a00000, 0x0000000100000000)  eden space 33280K, 10% used [0x00000000d5f80000,0x00000000d62c09a8,0x00000000d8000000)  from space 5120K, 0% used [0x00000000d8500000,0x00000000d8500000,0x00000000d8a00000)  to   space 5120K, 0% used [0x00000000d8000000,0x00000000d8000000,0x00000000d8500000) ParOldGen       total 87552K, used 0K [0x0000000081e00000, 0x0000000087380000, 0x00000000d5f80000)  object space 87552K, 0% used [0x0000000081e00000,0x0000000081e00000,0x0000000087380000) Metaspace       used 3141K, capacity 4496K, committed 4864K, reserved 1056768K  class space    used 342K, capacity 388K, committed 512K, reserved 1048576K</code></pre></li><li><p>显示调用的GC之后</p><pre><code class="language-Java">package com.yw;/** * @ClassName ReferenceCount * @Descriprtion TODO * @Author yww **/public class ReferenceCount {    //    设置引用属性    Object reference = null;    public static void main(String[] args) {        ReferenceCount A = new ReferenceCount();        ReferenceCount B = new ReferenceCount();    //    形成引用环        A.reference = B;        B.reference = A;    //    断开外部引用，A与B形成循环引用        A = null;        B = null;    //    显示调用GC        System.gc();    }}</code></pre><pre><code class="language-Java">//    使用了GC之后的内存情况[GC (System.gc()) [PSYoungGen: 2664K-&gt;776K(38400K)] 2664K-&gt;784K(125952K), 0.0012757 secs] [Times: user=0.00 sys=0.00, real=0.00 secs] [Full GC (System.gc()) [PSYoungGen: 776K-&gt;0K(38400K)] [ParOldGen: 8K-&gt;617K(87552K)] 784K-&gt;617K(125952K), [Metaspace: 3136K-&gt;3136K(1056768K)], 0.0039173 secs] [Times: user=0.00 sys=0.00, real=0.00 secs] Heap PSYoungGen      total 38400K, used 998K [0x00000000d5f80000, 0x00000000d8a00000, 0x0000000100000000)  eden space 33280K, 3% used [0x00000000d5f80000,0x00000000d6079b20,0x00000000d8000000)  from space 5120K, 0% used [0x00000000d8000000,0x00000000d8000000,0x00000000d8500000)  to   space 5120K, 0% used [0x00000000d8500000,0x00000000d8500000,0x00000000d8a00000) ParOldGen       total 87552K, used 617K [0x0000000081e00000, 0x0000000087380000, 0x00000000d5f80000)  object space 87552K, 0% used [0x0000000081e00000,0x0000000081e9a458,0x0000000087380000) Metaspace       used 3148K, capacity 4496K, committed 4864K, reserved 1056768K  class space    used 343K, capacity 388K, committed 512K, reserved 1048576K</code></pre></li><li><p>分析可以清楚的看到调用了GC之后，内存的占用率变低了，如果Java使用的是引用计数算法，那么内存是不应该降低的，所以可以知道的是Java不适用引用计算这种算法。</p></li></ol><h3>可达性分析算法</h3><p>可达性分析算法又称根搜索算法，追踪性垃圾收集。该算法可以有效的解决计数算法中的循环引用的问题，防止内存泄漏的发生。</p><h4>GC Roots</h4><p>GC Roots根集合，是一组必须活跃的引用。可达性分析算法的思路就是以GC Roots为起始点，从上至下的搜索被根对象引用的对象，查看对象是否可达。</p><p>搜索的路径被称为引用链。</p><p>存活的对象都应该是直接或者被间接引用到的，即都是可达的。</p><h4>GC Roots可以是哪些</h4><ol><li>虚拟机栈中引用的对象</li><li>本地方法栈内引用的对象</li><li>方法区中静态属性引用的对象</li><li>方法区常量引用的对象</li><li>Java虚拟机内部的引用</li><li>所有被同步锁synchronized持有的对象</li><li>除了固定的GC Roots集合外，根据用户选用的垃圾收集器以及当前回收的内存区域不同，还可以有其他对象临时加入，比如分代收集和局部回收。</li></ol><h2>对象的finalization机制</h2><p>finalization机制称为对象终止机制，这个机制允许开发人员在对象销毁之前可以自定义逻辑代码。</p><p>即垃圾回收之前，总会先调用这个对象的<code>finalize</code>方法，这个方法可以被子类重写，通常用于在对象被回收时进行资源释放。</p><p>由于<code>finalize</code>方法的存在，虚拟机的对象一般处于三种可能的状态。</p><ol><li>可触及的，即可达的。</li><li>可复活的。不可触及的对象有可能会在finalize方法中复活。</li><li>不可触及的。因为<code>finalize</code>方法只调用一次，当<code>finalize</code>方法被调用对象也没有复活，那就是不可触及的了。</li></ol><p>于是虚拟机判断一个对象是否可回收就会有这样的过程。</p><ul><li>对象不在GC Roots的引用链当中，就会被第一次标记。</li><li>对象没有重写<code>finalize</code>方法，或者<code>finalize</code>方法已被调用，则被判定为不可触及的。</li><li>对象重写了<code>finalize</code>方法，且没被执行，那该对象就会进入Finalizer线程触发其<code>finalize</code>方法，然后进行第二次标记。</li><li>要是该对象的<code>finalize</code>方法进入了引用链，即复活了，就会被移除回收的集合，<code>finalize</code>方法只能执行一次，之后再进入回收的集合就会直接变成不可触及。</li></ul><h2>清除阶段</h2><p>当垃圾被标记之后，就要开始进行清除阶段了。</p><h3>标记-清除算法</h3><p>当堆的有效内存被耗尽的时候，就会停止整个程序，进行标记和清除这两个工作。</p><ul><li>标记：Collector从引用根节点遍历，标记所有被引用的对象，即在对象的Header中标记为可达的对象。</li><li>清除：Collector对堆内存进行全部遍历，发现没有标记的就会将其回收。</li></ul><p>这种方法效率不高，而且进行GC的时候会停止整个程序，清理垃圾后也会产生内存碎片。</p><p>:::note{type=&quot;info&quot;}</p><p>内存碎片表示在内存中散乱的存放着一些对象。当有新的对象需要被加载的时候，可能就会没有足够的连续的内存空间给新的对象，所以即使总的空闲空间是足够的，也不能没有空间存放这个新的对象。</p><p>:::这里的清除不是表示直接删除该对象，其实这些对象还是存放在内存中，只是会将这部分对象所在的内存加入到空闲列表中，加载新的对象时候，就会将其覆盖掉。</p><h3>复制算法</h3><p>将活着的内存空间分为两块（设两块区域为A和B），每次只用一块（设第一次使用的是A），垃圾回收的时候将A中存活的对象复制到B区域当中，然后清除A中所有的对象。当下次B要进行垃圾回收了就复制到A，清除B中所有对象。交换使用。</p><p>这种方法复制到新区域的对象就会是连续的内存空间，就不会产生内存碎片的问题，但缺点也很明显，减少了总体的内存空间，因为分成两半了，而且当需要复制的存活对象很多时候，就会占用很多内存和浪费很多时间。</p><h3>标记-整理（压缩）算法</h3><ul><li>标记：Collector从引用根节点遍历，标记所有被引用的对象，即在对象的Header中标记为可达的对象。</li><li>整理：标记完了之后就会将所有存活的对象整理在内存中的位置，使其连续排放。</li></ul><p>标记-整理算法其实就像是在标记-清除算法后进行一次内存碎片的整理，只是少了清除的遍历，不过因为内存地址变了，有些引用的地址也会变，所以会存在一些风险。</p><h3>清除阶段三种算法的对比</h3><table><thead><tr><th align="center"></th><th align="center">Mark-Sweep</th><th align="center">Copying</th><th align="center">Mark-Compect</th></tr></thead><tbody><tr><td align="center">速度</td><td align="center">中等</td><td align="center">最快</td><td align="center">最慢</td></tr><tr><td align="center">空间开销</td><td align="center">少，但是会有内存碎片</td><td align="center">大，需要两倍的空间</td><td align="center">少，没有内存碎片</td></tr><tr><td align="center">是否要移动对象</td><td align="center">否</td><td align="center">是</td><td align="center">是</td></tr></tbody></table><h2>分代收集算法</h2><ul><li>年轻代年轻代的对象生命周期短，回收频繁，所以使用复制算法是比较好的，复制算法的内存问题可以通过hotspot中的两个survivor的设计得到缓解。</li><li>老年代老年代的对象生命周期长，所以一般是使用标记-清除或者是标记-清除和标记-整理混合使用。</li><li>几乎所有的垃圾回收器都会区分新生代和老年代。</li></ul><h2>增量收集算法</h2><p>如果等到内存空间不够然后一次性的进行垃圾回收，就会出现程序停止的问题，所以增量收集算法就是让垃圾收集的进程和应用程序的进程交替使用，每次只收集一片区域的内存空间。</p><h2>分区算法</h2><p>分区算法和增量收集算法其实差不多，分区算法就是将堆空间分割成多个小块的内存，每次合理回收若干个小的区间，从而减少一次CG产生的停顿时间。</p><h1>垃圾回收的相关知识</h1><h2>System.gc方法</h2><ul><li>使用<code>System.gc()</code>或者是<code>Runtime.getRuntime().gc()</code>就可以显示触发<code>Full GC</code>，同时回收老年代和新生代，尝试释放被丢弃对象占用的内存。</li><li>调用该方法会附带一个免责声明，即会触发<code>Full GC</code>，但是不知道会什么时候触发。</li></ul><h2>内存溢出（OOM）</h2><ul><li>内存溢出其实不太容易出现，如果当应用程序占用的内存增长速度异常，超过了垃圾回收提供的空闲内存，那就会可能出现内存溢出。</li><li>内存溢出之前其实都会进行一次独占式的<code>Full GC</code>,回收大量内存，若果此时内存还是不够用，就会出现内存溢出的情况。</li><li>Javadoc对OOM的理解是，没有空闲内存并且垃圾收集器也无法提供更多内存，就会报OOM。</li></ul><p>出现的原因可能有以下原因：</p><ol><li>Java虚拟机的堆内存设置不够</li><li>代码创建大量对象，而且长时间不能被垃圾收集器收集。</li></ol><h2>内存泄漏（Memory Leak）</h2><ul><li>严格来讲，对象不会被程序用到了，但是GC又回收不掉这些对象的情况，叫内存泄漏。</li><li>宽松一点来讲，一些不当操作导致对象的生命周期变得很长甚至导致OOM，也可以叫做内存泄漏。</li><li>内存泄漏不会立刻引起程序崩溃，但是发生内存泄漏之后，就有可能不断泄漏，然后就可能会出现OOM，即内存溢出的情况，然后导致程序崩溃。</li><li>注意，循环引用是针对引用计数算法的内存泄漏，Java不使用引用计数算法，所以这种不算是内存泄漏。</li></ul><p>一些内存泄漏的例子：</p><ol><li>单例模式单例的生命周期和应用程序一样长，所以当单例引用了外部对象时候，这个外部对象即一直是可达的了，这就会导致内存泄漏的产生。</li><li>一些提供close方法的对象未关闭导致的内存泄漏比如说数据库连接，网络连接和IO的连接操作，需要手动关闭，不然就会出现内存泄漏。</li></ol><h2>Stop The World</h2><p><code>Stop The World</code>简称STW，指GC事件发生的过程中会产生应用程序的停顿，停顿时整个应用程序线程都会被暂停，这个停顿就是STW。</p><p>比如可达性分析算法。可达性分析算法中的<code>GC Roots</code>的是可能会随着程序的而不断更新的，所以当进行枚举<code>GC Roots</code>的时候，会导致Java执行线程停顿。</p><p>所有的GC都会有STW的情况发生，不能避免，只能尽量减少STW的时间</p><h2>安全点</h2><p>事实上，发生GC的时间点并不是随机的，意思就是GC伴随的STW的不是随机某个时间点发生的，只有某一些特殊的位置才会停下来GC，这些位置就称作安全点。但是如何在GC发生的时候，检查到所有的线程都跑到最近的安全点然后开始停顿呢？</p><ul><li>抢先式中断（目前没有虚拟机采用了）中断所有线程，如果还有线程不在安全点就恢复该线程使其运行到最近的安全点。</li><li>主动式中断设置一个中断标志，各个线程运行到安全点的时候主动轮询这个标志，如果中断标志为真，就将该线程中断挂起。</li></ul><h2>安全区域</h2><p>讲述安全点的时候，默认把所有进程认为都在运行着，可是如果有某个进程处于睡眠或者阻塞的时候，还没到安全点就挂着了，这种时候还会一直等到该进程进入安全点吗？这种时候就需要设置安全区域来解决了。</p><p>安全区域是指在一段片段中，对象的引用关系不会发生变化，在这个区域中的任何位置开始GC都是安全的。这其实很好理解，对象的引用关系不发生变化的时候，<code>GC Roots</code>是不会发生变化的，可达性分析算法也就不会受到影响，所以在这段区域中停顿是不会对GC产生影响的。安全区域其实也意味着，这段区域都是安全点。</p><p>所以线程执行的时候要发生GC的就会是这样的：</p><ol><li>当线程进入安全区域的时候，就会标识该线程进入安全区域，如果要发生GC了，就会忽略该线程的情况，该发生就发生，该等就等。</li><li>当线程离开安全区域的时候，会检查JVM的GC是否已经完成，完成了该线程就继续运行，否则线程就会等待可以离开安全区域的信号为止。</li></ol><h2>引用</h2><p>引用分为四种，按照引用类型的强度递减为强引用，软引用，弱引用和虚引用。</p><h3>强引用（StrongReference）</h3><p>强引用是最经常用到的，平时的引用关系的建立很多都是强引用类型的，就比如<code>Object obj = new Object()</code>这种引用关系的建立就是强引用。强引用的关系存在的时候，就会被可达性分析算法标识为可触及的，所以就不会被垃圾收集器回收。</p><ul><li>强引用-不回收，即使要发生OOM了也不会进行回收。</li><li>建立了强引用可以直接访问目标对象。</li><li>强引用是导致内存泄漏的一个主要原因。</li></ul><h3>软引用（SoftReference）</h3><p>软引用就是弱一点的强引用。在系统即将发生OOM的时候，垃圾回收器就会进行第二次回收（第一次是那些不可达的对象），把这些被软引用关系的对象清除掉，要是清除掉之后还是没有足够内存，才会发生OOM。</p><ul><li><p>软引用-内存不足即回收。用来使用在一些虽然有用，但是不是必需的对象上。</p></li><li><p>软引用通常实现缓存方面上</p></li><li><p>建立软引用可以通过使用<code>SoftReference</code>的类来实现。</p><pre><code class="language-java">    Object obj = new Object();    SoftReference&lt;Object&gt; sr = new SoftReference&lt;&gt;(obj);    obj = null;    // 销毁强引用</code></pre></li></ul><h3>弱引用（WeakReference）</h3><p>弱引用的关系在建立了之后就只能活到下一次的垃圾回收，因为GC发生的时候，即使内存足够也会把弱引用的对象全部回收掉。</p><ul><li><p>弱引用-发现即回收。</p></li><li><p>GC回收的时候，会通过算法检查软引用是否要回收，而弱引用对象被检查到会直接被回收。</p></li><li><p>弱引用也是主要用在缓存方面</p></li><li><p>建立弱引用可以通过使用<code>WeakReference</code>的类来实现。</p><pre><code class="language-Java">    Object obj = new Object();    WeakReference&lt;Object&gt; wr = new WeakReference&lt;&gt;(obj);    obj = null;    // 销毁强引用</code></pre></li></ul><h3>虚引用（PhantomReference）</h3><p>虚引用关系是否存在对对象的生存不构成影响，也无法通过虚引用来实例化一个对象，该引用的目的只是为了在被虚引用的对象要被垃圾回收的时候会收到一个系统通知。</p><ul><li><p>虚引用-对象回收跟踪。</p></li><li><p>虚引用相当于没有引用，也获取不到对象。</p></li><li><p>虚引用必须和引用队列搭配使用，因为当对象被发现是虚引用的时候，就会在垃圾回收之后，将该虚引用加入到引用队列当中，用来通知程序。</p></li><li><p>建立虚引用可以通过使用<code>PhantomReference</code>的类来实现。</p><pre><code class="language-Java">    Object obj = new Object();    ReferenceQueue queue = new ReferenceQueue();    PhantomReference&lt;Object&gt; pr = new PhantomReference&lt;&gt;(obj,queue);    obj = null;    // 销毁强引用</code></pre></li></ul><h1>垃圾回收器</h1><h2>分类</h2><ul><li>垃圾回收器可以由不同厂商，不同版本的JVM来实现。</li><li>按照线程数来分类，可以分为<code>串行垃圾回收器</code>和<code>并行垃圾回收器</code>。</li><li>按照工作模式来分类，可以分为<code>并发式垃圾回收器</code>和<code>独占式垃圾回收器</code>。</li><li>按照碎片处理方式来分类，可以分为<code>压缩式垃圾回收器</code>和<code>非压缩式垃圾回收器</code>。</li><li>按照工作的内存区间来分类，可以分为<code>年轻代垃圾回收器</code>和<code>老年代垃圾回收器</code>。</li></ul><h2>性能指标</h2><ul><li>吞吐量：运行用户代码的时间占总运行时间的比例。</li><li>垃圾收集开销：吞吐量的补数，垃圾收集所用的时间占总运行时间的比例。</li><li>暂停时间：执行垃圾回收的时候，工作线程被暂停的时间。</li><li>收集频率。收集操作发生的频率。</li><li>内存占用：Java堆区所占用的内存大小。</li><li>快速：一个对象从诞生到被回收的时间。</li></ul><h2>七款经典的收集器</h2><ul><li>新生代收集器： <code>Serial</code>,<code>ParNew</code>,<code>Parallel</code>,<code>Scavenge</code></li><li>老年代收集器：<code>Serial Old</code>,<code>Parallel Old</code>,<code>CMS</code></li><li>整堆收集器：<code>G1</code></li><li>JDK8中默认使用的是<code>Parallel</code>和<code>Parallel Old</code>的组合，JDK9中默认使用<code>G1</code></li></ul><p><img src="https://img.yww52.com/2020/12/2020-12-7/img3.png" alt=""></p><p>:::note{type=&quot;info&quot;}</p><ol><li>红色的虚线表示的这两个组合在JDK8已起用，JDK9已移除</br><ol start="2"><li>绿色的虚线表示的这个组合在JDK14中已经弃用</br></li><li>CMS GC的虚线表示在JDK14中已经删掉了这个垃圾回收器。</li></ol></li></ol><p>:::</p><h2>Serial GC和Serial Old GC</h2><ul><li><p>Serial收集器是属于串行的垃圾回收器</p></li><li><p>Serial收集器采用<code>复制算法</code>，<code>串行回收</code>，<code>Stop The World</code>机制的方式执行内存回收</p></li><li><p>Serial收集器是收集年轻代的，也提供了收集老年代的收集器，Serial Old</p></li><li><p>Serial Old收集器采用<code>标记-压缩算法</code>，<code>串行回收</code>，<code>Stop The World</code>机制的方式执行内存回收</p></li><li><p>Serial Old是运行在Client模式下默认的老年代垃圾回收器</p></li><li><p>Serial Old在Server模式下有两个用途</p><ol><li>与新生代的Parallel Scavenge配合使用</li><li>作为老年代CMS收集器的后备垃圾收集方案</li></ol></li><li><p>可以设置参数使用Serial收集器，老年代也会默认使用Serial Old</p><pre><code class="language-java">    -XX:+UseSerialGC</code></pre></li></ul><blockquote><p> 现在CPU发展很快，串行的收集器体验不好，特别是对现在高流量的信息时代，所以一般Java Web中不会采用串行垃圾收集器。</p></blockquote><h2>ParNew GC</h2><ul><li><p>ParNew 是一款并行的垃圾收集器，可以说是Serial GC的多线程版本</p></li><li><p>ParNew GC只处理新生代</p></li><li><p>ParNew GC采用了<code>复制算法</code>和<code>Stop The World</code>机制，相对Serial GC来说只是暂停时间更短而已</p></li><li><p>很多的运行在Server模式下的JVM会采用ParNew GC为默认垃圾回收器</p></li><li><p>不考虑版本的话，ParNew GC可以搭配CMS GC或者是Serial GC使用</p></li><li><p>对于新生代回收次数频繁，所以使用ParNew GC并行可以提高效率，老年代回收少，使用串行垃圾回收器可以节省资源</p></li><li><p>设置参数使用ParNew GC</p><pre><code class="language-java">    -XX:+UseParNewGC</code></pre></li></ul><blockquote><p>前面有提到一点就是在JDK9版本中，Serial Old GC不可以与ParNewGC搭配了，在之后CMS也会删除，所以ParNew GC也快到了被移除的阶段了，所以不怎么建议使用了。</p></blockquote><h2>Parallel Scavenge GC 和 Parallel Old GC</h2><ul><li><p>Parallel与ParNew都是并行的，两者性能相差不大</p></li><li><p>Parallel Scavenge GC 采用<code>复制算法</code>和<code>Stop The World</code>机制</p></li><li><p>Parallel Scavenge GC 与ParNew GC不同的是Parallel收集器目标是达到一个可控制的吞吐量，也被称为吞吐量优先的垃圾回收器，自适应调节策略两者也有很大不同。</p></li><li><p>因为Parallel Scavenge GC是以吞吐量为优先的，所以老年代的收集器就不适合用串行的了，所以在JDK1.6的时候提供了Parallel Old GC用来替换Serial Old GC</p></li><li><p>Parallel Old GC采用<code>标记-整理算法</code>，同样基于<code>并行回收</code>和<code>Stop The World</code>。</p></li><li><p>参数设置</p><pre><code class="language-Java">// 默认开启一个，另一个也会开启    -XX:+UseParallelGC    -XX:+UseParallelOldGC// 限制并行的线程,默认与CPU核心数量相等    -XX:ParallelGCThreads// 设置STW最大时间    -XX:MaxGCPauseMillis// 设置Parallel Scavenge GC开启自适应调节策略，默认开启    -XX:+UseAdaptiveSizePolicy</code></pre></li></ul><h2>CMS GC</h2><p>详情可以查看<a href="https://yww52.com/2020/12/12/CMS%E5%9E%83%E5%9C%BE%E5%9B%9E%E6%94%B6%E5%99%A8/">这篇文章</a></p><h2>G1</h2>]]></content:encoded>
            <category domain="https://yww52.com/categories/%25E6%2588%2591%25E7%259A%2584%25E6%258A%2580%25E6%259C%25AF%25E5%25AD%25A6%25E4%25B9%25A0%25E7%2589%25A9%25E8%25AF%25AD%25E6%259E%259C%25E7%2584%25B6%25E6%259C%2589%25E9%2597%25AE%25E9%25A2%2598/">我的技术学习物语果然有问题</category>
        </item>
        <item>
            <title>初识JVM</title>
            <link>https://yww52.com/posts/c1baee3f/</link>
            <guid isPermaLink="true">https://yww52.com/posts/c1baee3f/</guid>
            <pubDate>Fri, 04 Dec 2020 00:00:00 +0000</pubDate>
            <description>初步认识学习JVM。</description>
            <content:encoded><![CDATA[<img src="https://img.yww52.com/2020/12/2020-12-4/top_img.jpg" alt="初识JVM" style="max-width:100%;height:auto;margin-bottom:15px;" /><h1>什么是JVM</h1><p>认识JVM之前就先了解下面三个相近的概念。</p><ul><li><p>JDK英文名称为Java Development Kit。Java开发工具包，开发Java程序经常会使用它。</p></li><li><p>JRE</p><p>英文名称为Java Runtime Environment。Java运行环境，主要包括JVM和一些类库，保证Java程序运行的最小环境。</p></li><li><p>JVM</p><p>英文名称为Java Virtual Machine。Java虚拟机,具体是干什么的，之后在说。</p></li></ul><p>通过概念就能看出来它们之间的关系，如下图。</p><p><img src="https://img.yww52.com/2020/12/2020-12-4/img1.png" alt=""></p><p>更加详细的信息可以参考这个图。</p><p><img src="https://img.yww52.com/2020/12/2020-12-4/img2.png" alt=""></p><h1>JVM的位置</h1><p>JVM在我们的计算机当中是位于什么位置的呢？可以参考一下下图。</p><p><img src="https://img.yww52.com/2020/12/2020-12-4/img3.png" alt=""></p><h1>一次编译，到处运行</h1><p>Java之所以那么火，这么多人使用，其中一个重要的原因就是Java有一个特性，一次编译，到处运行。Java可以实现跨平台运行，这使得开发者在不同平台上都可以运行自己的Java程序，方便了开发者的部署。那么Java是怎么实现跨平台的特性的呢？</p><p>这就关系到Java虚拟机了。我们在IDE编写好自己的Java程序之后，就会有一个扩展名为<code>.java</code>的源代码文件。</p><p>然后我们可以通过一个简单的命令来编译Java这个源代码文件。</p><pre><code class="language-bash">javac hello.java</code></pre><p>Java程序的编译不是直接编译为计算机可以看懂的序列，而是编译成为了一个扩展名为<code>.class</code>的字节码文件。这个字节码文件通过Java虚拟机的翻译，才能变成计算机看得懂的序列，计算机才能成功运行这个Java程序。</p><pre><code class="language-bash">java hello.class</code></pre><p>到这应该就知道为什么Java可以实现跨平台了。Java的字节码文件不管放在哪个系统，只要系统安装有JVM，就能将字节码文件翻译成机器看得懂的语言，机器就能执行出程序。系统不同的只是会导致虚拟机不同，但不会影响字节码文件。</p><h1>JVM的结构体系</h1><p><img src="https://img.yww52.com/2020/12/2020-12-4/img4.png" alt=""></p>]]></content:encoded>
            <category domain="https://yww52.com/categories/%25E6%2588%2591%25E7%259A%2584%25E6%258A%2580%25E6%259C%25AF%25E5%25AD%25A6%25E4%25B9%25A0%25E7%2589%25A9%25E8%25AF%25AD%25E6%259E%259C%25E7%2584%25B6%25E6%259C%2589%25E9%2597%25AE%25E9%25A2%2598/">我的技术学习物语果然有问题</category>
        </item>
        <item>
            <title>SpringBoot整合Druid</title>
            <link>https://yww52.com/posts/f2e39350/</link>
            <guid isPermaLink="true">https://yww52.com/posts/f2e39350/</guid>
            <pubDate>Wed, 25 Nov 2020 00:00:00 +0000</pubDate>
            <description>SpringBoot整合使用Druid.</description>
            <content:encoded><![CDATA[<img src="https://img.yww52.com/2020/11/2020-11-25top_img2.jpg" alt="SpringBoot整合Druid" style="max-width:100%;height:auto;margin-bottom:15px;" /><h1>简介</h1><p>Druid是的一个阿里巴巴的开源项目，它是一个数据库连接池的实现。Druid能够提供强大的监控和扩展功能。这次就用SpringBoot来整合Druid。</p><h1>依赖</h1><pre><code class="language-xml">&lt;dependencies&gt;    &lt;!--WEB的场景启动器--&gt;        &lt;dependency&gt;            &lt;groupId&gt;org.springframework.boot&lt;/groupId&gt;            &lt;artifactId&gt;spring-boot-starter-web&lt;/artifactId&gt;        &lt;/dependency&gt;    &lt;!--Mysql驱动--&gt;        &lt;dependency&gt;            &lt;groupId&gt;mysql&lt;/groupId&gt;            &lt;artifactId&gt;mysql-connector-java&lt;/artifactId&gt;            &lt;scope&gt;runtime&lt;/scope&gt;        &lt;/dependency&gt;    &lt;!--Druid--&gt;    &lt;dependency&gt;             &lt;groupId&gt;com.alibaba&lt;/groupId&gt;             &lt;artifactId&gt;druid&lt;/artifactId&gt;             &lt;version&gt;1.2.1&lt;/version&gt;&lt;/dependency&gt;    &lt;!--log4j日志--&gt;    &lt;dependency&gt;            &lt;groupId&gt;log4j&lt;/groupId&gt;            &lt;artifactId&gt;log4j&lt;/artifactId&gt;            &lt;version&gt;1.2.17&lt;/version&gt;        &lt;/dependency&gt;        &lt;!--单元测试--&gt;        &lt;dependency&gt;            &lt;groupId&gt;org.springframework.boot&lt;/groupId&gt;            &lt;artifactId&gt;spring-boot-starter-test&lt;/artifactId&gt;            &lt;scope&gt;test&lt;/scope&gt;            &lt;exclusions&gt;                &lt;exclusion&gt;                    &lt;groupId&gt;org.junit.vintage&lt;/groupId&gt;                    &lt;artifactId&gt;junit-vintage-engine&lt;/artifactId&gt;                &lt;/exclusion&gt;            &lt;/exclusions&gt;        &lt;/dependency&gt;&lt;/dependencies&gt;</code></pre><h1>配置SpringBoot配置文件</h1><pre><code class="language-yaml"># aplication.yaml使用spring:  datasource:    username: root    password: password    url: jdbc:mysql://localhost:3306/shop?userUnicode=true&amp;characterEncoding=utf-8&amp;serverTimezone=UTC    driver-class-name: com.mysql.cj.jdbc.Driver    type: com.alibaba.druid.pool.DruidDataSource#指定Druid为数据源    # Druid的配置    initialSize: 5    minIdle: 5    maxActive: 20    max-wait: 60000    timeBetweenEvictionRunsMillis: 60000    minEvictableIdleTimeMillis: 300000    validationQuery: SELECT 1 FROM DUAL    testWhileIdle: true    testOnBorrow: false    testOnReturn: false    poolPreparedStatements: true    #配置监控统计拦截的filters，stat:监控统计、log4j：日志记录、wall：防御sql注入    filters: stat,wall,log4j    maxPoolPreparedStatementPerConnectionSize: 20    useGlobalDataSourceStat: true    connectionProperties: druid.stat.mergeSql=true;druid.stat.slowSqlMillis=500</code></pre><h1>编写配置类</h1><pre><code class="language-Java">package com.yww.config;import com.alibaba.druid.pool.DruidDataSource;import org.springframework.boot.context.properties.ConfigurationProperties;import org.springframework.context.annotation.Bean;import org.springframework.context.annotation.Configuration;import javax.sql.DataSource;@Configurationpublic class DruidConfig {    @Bean    @ConfigurationProperties(prefix = &quot;spring.datasource&quot;)    public DataSource dataSource(){        return new DruidDataSource();    }}</code></pre><h1>配置数据源监控</h1><pre><code class="language-Java">package com.yww.config;import com.alibaba.druid.pool.DruidDataSource;import com.alibaba.druid.support.http.StatViewServlet;import org.springframework.boot.context.properties.ConfigurationProperties;import org.springframework.boot.web.servlet.ServletRegistrationBean;import org.springframework.context.annotation.Bean;import org.springframework.context.annotation.Configuration;import javax.sql.DataSource;import java.util.HashMap;import java.util.Map;@Configurationpublic class DruidConfig {    @Bean    @ConfigurationProperties(prefix = &quot;spring.datasource&quot;)    public DataSource dataSource(){        return new DruidDataSource();    }    @Bean    public ServletRegistrationBean statViewServlet() {        ServletRegistrationBean bean = new ServletRegistrationBean(new StatViewServlet(), &quot;/druid/*&quot;);        Map&lt;String, String&gt; initParams = new HashMap&lt;&gt;();        initParams.put(&quot;loginUsername&quot;, &quot;admin&quot;); //后台管理界面的登录账号        initParams.put(&quot;loginPassword&quot;, &quot;123456&quot;); //后台管理界面的登录密码        bean.setInitParameters(initParams);        return bean;    }}</code></pre><blockquote><p>配置好之后访问<a href="http://localhost:8080/druid/login.html">http://localhost:8080/druid/login.html</a>就可以看到Druid为我们提供的用于监控的web界面。</p></blockquote><h1>配置filter过滤器</h1><pre><code class="language-Java">package com.yww.config;import com.alibaba.druid.pool.DruidDataSource;import com.alibaba.druid.support.http.StatViewServlet;import org.springframework.boot.context.properties.ConfigurationProperties;import org.springframework.boot.web.servlet.ServletRegistrationBean;import org.springframework.context.annotation.Bean;import org.springframework.context.annotation.Configuration;import javax.sql.DataSource;import java.util.HashMap;import java.util.Map;@Configurationpublic class DruidConfig {    //注入Druid的配置    @Bean    @ConfigurationProperties(prefix = &quot;spring.datasource&quot;)    public DataSource dataSource(){        return new DruidDataSource();    }    //配置Druid的监控    @Bean    public ServletRegistrationBean statViewServlet() {        ServletRegistrationBean bean = new ServletRegistrationBean(new StatViewServlet(), &quot;/druid/*&quot;);        Map&lt;String, String&gt; initParams = new HashMap&lt;&gt;();        initParams.put(&quot;loginUsername&quot;, &quot;admin&quot;); //后台管理界面的登录账号        initParams.put(&quot;loginPassword&quot;, &quot;123456&quot;); //后台管理界面的登录密码        bean.setInitParameters(initParams);        return bean;    }    //配置filter过滤器    @Beanpublic FilterRegistrationBean webStatFilter() {    FilterRegistrationBean bean = new FilterRegistrationBean();    bean.setFilter(new WebStatFilter());    Map&lt;String, String&gt; initParams = new HashMap&lt;&gt;();    initParams.put(&quot;exclusions&quot;, &quot;*.js,*.css,/druid/*,/jdbc/*&quot;);    bean.setInitParameters(initParams);    //&quot;/*&quot; 表示过滤所有请求    bean.setUrlPatterns(Arrays.asList(&quot;/*&quot;));    return bean;}}</code></pre>]]></content:encoded>
            <category domain="https://yww52.com/categories/%25E6%2588%2591%25E7%259A%2584%25E6%258A%2580%25E6%259C%25AF%25E5%25AD%25A6%25E4%25B9%25A0%25E7%2589%25A9%25E8%25AF%25AD%25E6%259E%259C%25E7%2584%25B6%25E6%259C%2589%25E9%2597%25AE%25E9%25A2%2598/">我的技术学习物语果然有问题</category>
        </item>
        <item>
            <title>SpringBoot使用JDBC</title>
            <link>https://yww52.com/posts/a8057909/</link>
            <guid isPermaLink="true">https://yww52.com/posts/a8057909/</guid>
            <pubDate>Wed, 25 Nov 2020 00:00:00 +0000</pubDate>
            <description>SpringBoot用jdbcTemplate来来进行JDBC操作。</description>
            <content:encoded><![CDATA[<img src="https://img.yww52.com/2020/11/2020-11-25top_img1.jpg" alt="SpringBoot使用JDBC" style="max-width:100%;height:auto;margin-bottom:15px;" /><h1>简介</h1><p>在SpringBoot中简单的使用JDBC,用jdbcTemplate简单的操作数据的增删改查，大型一点的项目建议使用Mybatis。</p><h1>依赖</h1><pre><code class="language-xml">&lt;dependencies&gt;        &lt;!--JDBC的场景启动器--&gt;        &lt;dependency&gt;            &lt;groupId&gt;org.springframework.boot&lt;/groupId&gt;            &lt;artifactId&gt;spring-boot-starter-jdbc&lt;/artifactId&gt;        &lt;/dependency&gt;    &lt;!--WEB的场景启动器--&gt;        &lt;dependency&gt;            &lt;groupId&gt;org.springframework.boot&lt;/groupId&gt;            &lt;artifactId&gt;spring-boot-starter-web&lt;/artifactId&gt;        &lt;/dependency&gt;    &lt;!--Mysql驱动--&gt;        &lt;dependency&gt;            &lt;groupId&gt;mysql&lt;/groupId&gt;            &lt;artifactId&gt;mysql-connector-java&lt;/artifactId&gt;            &lt;scope&gt;runtime&lt;/scope&gt;        &lt;/dependency&gt;        &lt;!--单元测试--&gt;        &lt;dependency&gt;            &lt;groupId&gt;org.springframework.boot&lt;/groupId&gt;            &lt;artifactId&gt;spring-boot-starter-test&lt;/artifactId&gt;            &lt;scope&gt;test&lt;/scope&gt;            &lt;exclusions&gt;                &lt;exclusion&gt;                    &lt;groupId&gt;org.junit.vintage&lt;/groupId&gt;                    &lt;artifactId&gt;junit-vintage-engine&lt;/artifactId&gt;                &lt;/exclusion&gt;            &lt;/exclusions&gt;        &lt;/dependency&gt;&lt;/dependencies&gt;</code></pre><h1>配置springboot配置文件</h1><pre><code class="language-yaml"># 这个是application.yaml版本spring:  datasource:    username: root    password: password    # 可以根据情况来写url    url: jdbc:mysql://localhost:3306/test?userUnicode=true&amp;characterEncoding=utf-8&amp;serverTimezone=UTC    driver-class-name: com.mysql.cj.jdbc.Driver</code></pre><pre><code class="language-properties"># 这个是application.properties版本spring.datasource.username=rootspring.datasource.password=passwordspring.datasource.url=jdbc:mysql://localhost:3306/test?userUnicode=true&amp;characterEncoding=utf-8&amp;serverTimezone=UTCspring.datasource.driver-class-name=com.mysql.cj.jdbc.Driver</code></pre><h1>简单的CRUD</h1><pre><code class="language-java">package com.yww.controller;import org.springframework.beans.factory.annotation.Autowired;import org.springframework.jdbc.core.JdbcTemplate;import org.springframework.web.bind.annotation.GetMapping;import org.springframework.web.bind.annotation.PathVariable;import org.springframework.web.bind.annotation.RestController;import java.util.List;import java.util.Map;@RestControllerpublic class JdbcController {    @Autowired    JdbcTemplate jdbcTemplate;    @GetMapping(&quot;/select&quot;)    public List&lt;Map&lt;String,Object&gt;&gt; userList(){        String sql = &quot;SELECT * FROM user&quot;;        List&lt;Map&lt;String, Object&gt;&gt; maps = jdbcTemplate.queryForList(sql);        return maps;    }    @GetMapping(&quot;/add&quot;)    public String addUser(){        String sql = &quot;INSERT INTO USER VALUES(3,&#39;王五&#39;)&quot;;        jdbcTemplate.update(sql);        return &quot;添加完成&quot;;    }    @GetMapping(&quot;/update/{id}&quot;)    public String updateUser(@PathVariable(&quot;id&quot;) int id){        String sql = &quot;UPDATE user SET id=4,name=&#39;赵六&#39; WHERE id=&quot;+id;        jdbcTemplate.update(sql);        return &quot;修改完成&quot;;    }    @GetMapping(&quot;/delete/{id}&quot;)    public String deleteUser(@PathVariable(&quot;id&quot;) int id){        String sql = &quot;DELETE FROM user WHERE id=?&quot;;        jdbcTemplate.update(sql,id);        return &quot;删除成功&quot;;    }}</code></pre><p>:::note{type=&quot;info&quot;}</p><p>spring帮我们封装好了JDBC的操作，并做好了模板，就是上述的JdbcTemplate。    JdbcTemplate的几个常用方法。    1. execute()，用于执行任何SQL语句。    2. update()，用于执行新增，修改删除等语句。    3. batchUpdate()，用于执行批处理的语句。    4. query()，用于执行查询语句。    5. call()，用于执行存储过程和函数相关的语句。</p><p>:::</p>]]></content:encoded>
            <category domain="https://yww52.com/categories/%25E6%2588%2591%25E7%259A%2584%25E6%258A%2580%25E6%259C%25AF%25E5%25AD%25A6%25E4%25B9%25A0%25E7%2589%25A9%25E8%25AF%25AD%25E6%259E%259C%25E7%2584%25B6%25E6%259C%2589%25E9%2597%25AE%25E9%25A2%2598/">我的技术学习物语果然有问题</category>
        </item>
        <item>
            <title>SpringBoot使用Mybatis</title>
            <link>https://yww52.com/posts/e67586ad/</link>
            <guid isPermaLink="true">https://yww52.com/posts/e67586ad/</guid>
            <pubDate>Wed, 25 Nov 2020 00:00:00 +0000</pubDate>
            <description>用SpringBoot整合Mybatis，并简单的使用</description>
            <content:encoded><![CDATA[<img src="https://img.yww52.com/2020/11/2020-11-25top_img3.jpg" alt="SpringBoot使用Mybatis" style="max-width:100%;height:auto;margin-bottom:15px;" /><h1>简介</h1><p>Mybatis是现在十分流行的数据持久化框架，这次就来用SpringBoot来整合使用Mybatis。</p><h1>依赖</h1><pre><code class="language-xml">  &lt;dependencies&gt;&lt;!-- Mybatis场景启动器 --&gt;        &lt;dependency&gt;            &lt;groupId&gt;org.mybatis.spring.boot&lt;/groupId&gt;            &lt;artifactId&gt;mybatis-spring-boot-starter&lt;/artifactId&gt;            &lt;version&gt;2.1.1&lt;/version&gt;        &lt;/dependency&gt;&lt;!-- web场景启动器 --&gt;        &lt;dependency&gt;            &lt;groupId&gt;org.springframework.boot&lt;/groupId&gt;            &lt;artifactId&gt;spring-boot-starter-web&lt;/artifactId&gt;        &lt;/dependency&gt;&lt;!-- MySQL驱动 --&gt;        &lt;dependency&gt;            &lt;groupId&gt;mysql&lt;/groupId&gt;            &lt;artifactId&gt;mysql-connector-java&lt;/artifactId&gt;            &lt;scope&gt;runtime&lt;/scope&gt;        &lt;/dependency&gt;&lt;!-- Druid --&gt;        &lt;dependency&gt;            &lt;groupId&gt;com.alibaba&lt;/groupId&gt;            &lt;artifactId&gt;druid&lt;/artifactId&gt;            &lt;version&gt;1.2.1&lt;/version&gt;        &lt;/dependency&gt;&lt;!-- log4j日志 --&gt;        &lt;dependency&gt;            &lt;groupId&gt;log4j&lt;/groupId&gt;            &lt;artifactId&gt;log4j&lt;/artifactId&gt;            &lt;version&gt;1.2.17&lt;/version&gt;        &lt;/dependency&gt;&lt;!-- lombok --&gt;         &lt;dependency&gt;             &lt;groupId&gt;org.projectlombok&lt;/groupId&gt;             &lt;artifactId&gt;lombok&lt;/artifactId&gt;         &lt;/dependency&gt;&lt;!-- 单元测试 --&gt;        &lt;dependency&gt;            &lt;groupId&gt;org.springframework.boot&lt;/groupId&gt;            &lt;artifactId&gt;spring-boot-starter-test&lt;/artifactId&gt;            &lt;scope&gt;test&lt;/scope&gt;            &lt;exclusions&gt;                &lt;exclusion&gt;                    &lt;groupId&gt;org.junit.vintage&lt;/groupId&gt;                    &lt;artifactId&gt;junit-vintage-engine&lt;/artifactId&gt;                &lt;/exclusion&gt;            &lt;/exclusions&gt;        &lt;/dependency&gt;    &lt;/dependencies&gt;    &lt;build&gt;        &lt;resources&gt;            &lt;resource&gt;                &lt;directory&gt;src/main/java&lt;/directory&gt;                &lt;includes&gt;                    &lt;include&gt;**/*.xml&lt;/include&gt;                &lt;/includes&gt;                &lt;filtering&gt;true&lt;/filtering&gt;            &lt;/resource&gt;        &lt;/resources&gt;        &lt;plugins&gt;            &lt;plugin&gt;                &lt;groupId&gt;org.springframework.boot&lt;/groupId&gt;                &lt;artifactId&gt;spring-boot-maven-plugin&lt;/artifactId&gt;            &lt;/plugin&gt;        &lt;/plugins&gt;    &lt;/build&gt;</code></pre><h1>定义实体类</h1><pre><code class="language-Java">package com.yww.pojo;import lombok.AllArgsConstructor;import lombok.Data;import lombok.NoArgsConstructor;@Data@NoArgsConstructor@AllArgsConstructorpublic class User {        int ID;        String name;    }</code></pre><h1>定义Mapper接口</h1><pre><code class="language-Java">package com.yww.mapper;import com.yww.pojo.User;import org.apache.ibatis.annotations.Mapper;import org.springframework.stereotype.Repository;import java.util.List;@Mapper@Repositorypublic interface UserMapper {        List&lt;User&gt; selectUserList();        User selectUserByID(int ID);        int addUser(User user);        int updateUser(User user);        int deleteUser(int id);    }</code></pre><h1>创建Mybatis配置文件</h1><pre><code class="language-xml">&lt;?xml version=&quot;1.0&quot; encoding=&quot;UTF-8&quot; ?&gt;&lt;!DOCTYPE mapper        PUBLIC &quot;-//mybatis.org//DTD Mapper 3.0//EN&quot;        &quot;http://mybatis.org/dtd/mybatis-3-mapper.dtd&quot;&gt;&lt;!--绑定mapper接口，位置要写对--&gt;&lt;mapper namespace=&quot;com.yww.mapper.UserMapper&quot;&gt;        &lt;select id=&quot;selectUserList&quot; resultType=&quot;User&quot;&gt;        SELECT * FROM user    &lt;/select&gt;    &lt;select id=&quot;selectUserByID&quot; resultType=&quot;User&quot;&gt;        SELECT * FROM user WHERE ID = #{ID}    &lt;/select&gt;    &lt;insert id=&quot;addUser&quot; parameterType=&quot;User&quot;&gt;        INSERT INTO user (ID,name) VALUES (#{ID},#{name})    &lt;/insert&gt;    &lt;update id=&quot;updateUser&quot; parameterType=&quot;User&quot;&gt;        UPDATE user SET name=#{name} WHERE ID = #{ID}    &lt;/update&gt;    &lt;delete id=&quot;deleteUser&quot; parameterType=&quot;int&quot;&gt;        DELETE FROM user WHERE ID=#{ID}    &lt;/delete&gt;    &lt;/mapper&gt;</code></pre><h1>配置SpringBoot配置文件</h1><pre><code class="language-yaml"># application.yml使用# 连接数据库spring:  datasource:    username: root    password: password    url: jdbc:mysql://localhost:3306/test?userUnicode=true&amp;characterEncoding=utf-8&amp;serverTimezone=UTC    driver-class-name: com.mysql.cj.jdbc.Driver    type: com.alibaba.druid.pool.DruidDataSource    # Druid的配置    initialSize: 5    minIdle: 5    maxActive: 20    max-wait: 60000    timeBetweenEvictionRunsMillis: 60000    minEvictableIdleTimeMillis: 300000    validationQuery: SELECT 1 FROM DUAL    testWhileIdle: true    testOnBorrow: false    testOnReturn: false    poolPreparedStatements: true    filters: stat,wall,log4j    maxPoolPreparedStatementPerConnectionSize: 20    useGlobalDataSourceStat: true    connectionProperties: druid.stat.mergeSql=true;druid.stat.slowSqlMillis=500# mybatis设置mybatis:  type-aliases-package: com.yww.pojo#绑定实体类，不然配置文件识别不出User  mapper-locations: classpath:mybatis/mapper/*.xml#绑定mybatis的配置文件</code></pre><h1>编写controller</h1><pre><code class="language-Java">package com.yww.controller;import com.yww.mapper.UserMapper;import com.yww.pojo.User;import org.springframework.beans.factory.annotation.Autowired;import org.springframework.web.bind.annotation.GetMapping;import org.springframework.web.bind.annotation.RestController;import java.util.List;@RestControllerpublic class UserController {        @Autowired    private UserMapper userMapper;        @GetMapping(&quot;/select&quot;)    public List&lt;User&gt; selectUserList(){        List&lt;User&gt; list = userMapper.selectUserList();        return list;    }        @GetMapping(&quot;/selectbyid&quot;)    public User selectByID(){        User user = userMapper.selectUserByID(1);        return user;    }        @GetMapping(&quot;/insert&quot;)    public int addUser(){        User user = new User(3, &quot;王五&quot;);        return  userMapper.addUser(user);    }        @GetMapping(&quot;/update&quot;)    public int  updateUser(){        User user = new User(3, &quot;赵六&quot;);        return userMapper.updateUser(user);    }        @GetMapping(&quot;/delete&quot;)    public int  deleteUser(){        return userMapper.deleteUser(3);    }    }</code></pre>]]></content:encoded>
            <category domain="https://yww52.com/categories/%25E6%2588%2591%25E7%259A%2584%25E6%258A%2580%25E6%259C%25AF%25E5%25AD%25A6%25E4%25B9%25A0%25E7%2589%25A9%25E8%25AF%25AD%25E6%259E%259C%25E7%2584%25B6%25E6%259C%2589%25E9%2597%25AE%25E9%25A2%2598/">我的技术学习物语果然有问题</category>
        </item>
        <item>
            <title>HashMap</title>
            <link>https://yww52.com/posts/3d614825/</link>
            <guid isPermaLink="true">https://yww52.com/posts/3d614825/</guid>
            <pubDate>Fri, 20 Nov 2020 00:00:00 +0000</pubDate>
            <description>HashMap是经常用到的一个集合，很有必要进行深入了解。</description>
            <content:encoded><![CDATA[<img src="https://img.yww52.com/2020/11/2020-11-20/top_img.jpg" alt="HashMap" style="max-width:100%;height:auto;margin-bottom:15px;" /><h1>简介</h1><p>HashMap是Map经常使用的一个实现类,也是我们经常用到的一个集合，所以很有必要去了解一下。JDK8相对JDK7优化了不少，结构也改变了，所以这里就分开来学习了。</p><p>:::note{type=&quot;info&quot;}</p><p>因为我英语不是特别好，所以有些源码的文档注释是机翻的，所以有些文档注释读起来怪怪的，只能理解一下大概意思。</p><p>:::</p><h1>JDK7</h1><h2>参数和变量</h2><pre><code class="language-Java">    /**     * 默认的初始容量，必须是2的幂，默认16     */    static final int DEFAULT_INITIAL_CAPACITY = 1 &lt;&lt; 4; // aka 16    /**     * 这是HashMap的最大容量，值为1&lt;&lt;30。要是传入更高的值，就会使用这个最大容量     */    static final int MAXIMUM_CAPACITY = 1 &lt;&lt; 30;    /**     * HashMap默认的负载因子     */    static final float DEFAULT_LOAD_FACTOR = 0.75f;</code></pre><pre><code class="language-Java">    /**     * 当表未膨胀时使用的空表实例     */    static final Entry&lt;?,?&gt;[] EMPTY_TABLE = {};    /**     *这个表根据需要调整长度大小，长度必须为2的幂     */    transient Entry&lt;K,V&gt;[] table = (Entry&lt;K,V&gt;[]) EMPTY_TABLE;    /**     * 键值对的数量     */    transient int size;    /**     * 下一个要调整大小的值，值为容量*负载系数     * 如果table == EMPTY_TABLE,则值为初始容量     * 膨胀时将会创建表格     * @serial     */    int threshold;    /**     * 哈希表的负载因子     * @serial     */    final float loadFactor;    /**     * 已对HashMap进行结构修改的次数结构修改是指更改HashMap中的映射数或     * 以其他方式修改其内部结构（例如，重新哈希）的修改。 此字段用于使HashMap的Collection-view上     * 的迭代器快速失败。（请参见ConcurrentModificationException）。     */    transient int modCount;    /**     * 映射容量的默认阈值，高于该阈值时，字符串键将使用替代哈希。     * 备用哈希可减少由于字符串键的哈希码计算能力较弱而导致的冲突发生率。     * &lt;p/&gt;     * 可以通过定义系统属性来覆盖这个值     */    static final int ALTERNATIVE_HASHING_THRESHOLD_DEFAULT = Integer.MAX_VALUE;  /**     * 与此实例相关联的随机化值，该值应用于键的哈希码，以使哈希冲突更难找到。 如果为0，则禁用备用哈希。     */    transient int hashSeed = 0;    private transient Set&lt;Map.Entry&lt;K,V&gt;&gt; entrySet = null;    // 序列化默认版本号    private static final long serialVersionUID = 362498820763181265L;</code></pre><h2>构造器</h2><pre><code class="language-Java">    /**     * 使用默认的容量16和负载因子0.75创建HashMap     */    public HashMap() {        this(DEFAULT_INITIAL_CAPACITY, DEFAULT_LOAD_FACTOR);    }    /**     * 使用指定容量和默认的负载因子创建HashMap     */    public HashMap(int initialCapacity) {        this(initialCapacity, DEFAULT_LOAD_FACTOR);    }    /**     * 使用指定容量和指定的负载因子创建HashMap     */    public HashMap(int initialCapacity, float loadFactor) {        if (initialCapacity &lt; 0)            throw new IllegalArgumentException(&quot;Illegal initial capacity: &quot; +                                               initialCapacity);        if (initialCapacity &gt; MAXIMUM_CAPACITY)            initialCapacity = MAXIMUM_CAPACITY;        if (loadFactor &lt;= 0 || Float.isNaN(loadFactor))            throw new IllegalArgumentException(&quot;Illegal load factor: &quot; +                                               loadFactor);        this.loadFactor = loadFactor;        threshold = initialCapacity;        init();    }    /**     * 创建一个大于Map键值对数量的HashMap，然后将键值对加入到HashMap     * 使用默认的负载因子0.75，和足以包含Map键值对数量的容量     */    public HashMap(Map&lt;? extends K, ? extends V&gt; m) {        this(Math.max((int) (m.size() / DEFAULT_LOAD_FACTOR) + 1,                      DEFAULT_INITIAL_CAPACITY), DEFAULT_LOAD_FACTOR);        inflateTable(threshold);        putAllForCreate(m);    }</code></pre><p>:::note{type=&quot;info&quot;}</p><p>从构造方法来看，HashMap(int initialCapacity, float loadFactor)这个方法才是主体。接下来就来分析一下这个构造方法。</p><p>:::</p><pre><code class="language-Java">    public HashMap(int initialCapacity, float loadFactor) {        //  容量小于0，抛出异常        if (initialCapacity &lt; 0)            throw new IllegalArgumentException(&quot;Illegal initial capacity: &quot; +                                               initialCapacity);        //  容量大于最大值1&lt;&lt;30，使用最大值当做容量        if (initialCapacity &gt; MAXIMUM_CAPACITY)            initialCapacity = MAXIMUM_CAPACITY;        //  负载因子小于等于0或者非数字就抛出异常        if (loadFactor &lt;= 0 || Float.isNaN(loadFactor))            throw new IllegalArgumentException(&quot;Illegal load factor: &quot; +                                               loadFactor);        this.loadFactor = loadFactor;        threshold = initialCapacity;        //  空方法，用来初始化HashMap，有些类里就会有其实现        init();    }</code></pre><h2>数据结构</h2><p>构造方法看完了，现在来看看HashMap的数据结构。JDK7的HashMap是以数组为主，链表为辅的结构。大概结构如下。</p><p><img src="https://img.yww52.com/2020/11/2020-11-20/img1.png" alt=""></p><p>:::note{type=&quot;info&quot;}</p><p>每个链表被称为哈希表的桶，每个节点都存储了一对键值对。</p><p>:::</p><p>这里就看出，每个节点就是哈希表的最小组成单位了，现在就来看一下这个节点类。JDK7时的节点类叫<code>Entry</code>。</p><pre><code class="language-Java">    static class Entry&lt;K,V&gt; implements Map.Entry&lt;K,V&gt; {        final K key;        V value;        Entry&lt;K,V&gt; next;        int hash;        /**         * 构造器         */        Entry(int h, K k, V v, Entry&lt;K,V&gt; n) {            value = v;            next = n;            key = k;            hash = h;        }        //  get和set方法        public final K getKey() {            return key;        }        public final V getValue() {            return value;        }        public final V setValue(V newValue) {            V oldValue = value;            value = newValue;            return oldValue;        }        //  重写equals        public final boolean equals(Object o) {            if (!(o instanceof Map.Entry))                return false;            Map.Entry e = (Map.Entry)o;            Object k1 = getKey();            Object k2 = e.getKey();            if (k1 == k2 || (k1 != null &amp;&amp; k1.equals(k2))) {                Object v1 = getValue();                Object v2 = e.getValue();                if (v1 == v2 || (v1 != null &amp;&amp; v1.equals(v2)))                    return true;            }            return false;        }        //  重写hashCode方法        public final int hashCode() {            return Objects.hashCode(getKey()) ^ Objects.hashCode(getValue());        }        //  重写toString方法        public final String toString() {            return getKey() + &quot;=&quot; + getValue();        }        /**        * 在存在key下put，就会用该方法覆盖value值        */        void recordAccess(HashMap&lt;K,V&gt; m) {        }        /**         * 删除键值对时就会用这个方法         */        void recordRemoval(HashMap&lt;K,V&gt; m) {        }    }</code></pre><p>由此可以看出每个<code>Entry</code>都存储了四个值，一对键值对，一个hash，和一个next值。next指向链表的下一个节点，而hash则用来判断该节点放在数组哪一个位置。</p><h2>put</h2><p>创建了哈希表之后，就可以用put方法来存放键值对，现在就来看看put方法的源码。</p><pre><code class="language-Java">/** * 将指定值与指定的键相连，如果指定的键原来有值，就会覆盖掉旧的值 * 返回与key关联的先前值；如果没有key的映射关系，则返回null 。 * 返回null可能还表明该映射先前将null与key关联。 */public V put(K key, V value) {    if (table == EMPTY_TABLE) {        inflateTable(threshold);    }    if (key == null)        return putForNullKey(value);    int hash = hash(key);    int i = indexFor(hash, table.length);    for (Entry&lt;K,V&gt; e = table[i]; e != null; e = e.next) {        Object k;        if (e.hash == hash &amp;&amp; ((k = e.key) == key || key.equals(k))) {            V oldValue = e.value;            e.value = value;            e.recordAccess(this);            return oldValue;        }    }    modCount++;    addEntry(hash, key, value, i);    return null;}</code></pre><p>put方法有很多门道，现在我们就来一步步解析。</p><h3>第一步</h3><pre><code class="language-Java">    if (table == EMPTY_TABLE) {        inflateTable(threshold);    }</code></pre><p>首先就是判断这个哈希表是不是一个空表。如果是就会调用<code>inflateTable(threshold);</code>这个方法，不是就进行下一步。我们去看看这个方法。</p><pre><code class="language-Java">    private void inflateTable(int toSize) {        // 计算出传入比传入容量大的最小的二次幂        int capacity = roundUpToPowerOf2(toSize);        //  重新计算阈值 threshold = 容量 * 负载因子        threshold = (int) Math.min(capacity * loadFactor, MAXIMUM_CAPACITY + 1);        //  用最小的2次幂来创建一个新的数组        table = new Entry[capacity];        //  初始化哈希掩码值，将初始化推迟到我们真正需要它之前。        initHashSeedAsNeeded(capacity);    }</code></pre><p>这个方法就是用来创建数组的，通过<code>roundUpToPowerOf2</code>方法来确定最小的能大于需要长度<code>toSize</code>的二次幂，是有点绕，那现在去看看这个方法。</p><pre><code class="language-Java">    private static int roundUpToPowerOf2(int number) {        //  1.如果这个需要的长度大于最大值，就用最大值来创建数组        //  2.需要的长度小于1就用1当数组的长度，大于1就用highestOneBit方法来确定长度。        return number &gt;= MAXIMUM_CAPACITY                ? MAXIMUM_CAPACITY                : (number &gt; 1) ? Integer.highestOneBit((number - 1) &lt;&lt; 1) : 1;    }    //  这个方法就是用来确定最小的二次幂    public static int highestOneBit(int i) {        // HD, Figure 3-1        i |= (i &gt;&gt;  1);        i |= (i &gt;&gt;  2);        i |= (i &gt;&gt;  4);        i |= (i &gt;&gt;  8);        i |= (i &gt;&gt; 16);        return i - (i &gt;&gt;&gt; 1);    }</code></pre><p>寻找到最小的二次幂之后，重新计算阈值，然后用最小的2次幂来创建一个新的数组，最后用<code>initHashSeedAsNeeded</code>来初始化哈希掩码值。</p><h3>第二步</h3><pre><code class="language-Java">    if (key == null)        return putForNullKey(value);</code></pre><p>判断传入的key是不是null值，如果是就返回<code>putForNullKey(value)</code>，不然就下一步。现在来看看这个方法。</p><pre><code class="language-Java">    private V putForNullKey(V value) {        /**         *  遍历table[0]的链表，若有key==null，就用新的value覆盖原来的         *  然后返回被覆盖的value，所以table[0]就只会有一个元素         */        for (Entry&lt;K,V&gt; e = table[0]; e != null; e = e.next) {            if (e.key == null) {                V oldValue = e.value;                e.value = value;                e.recordAccess(this);                return oldValue;            }        }        modCount++;       /**         *  要是没有key==null的键，就调用addEntry方法，将空键和值放入table[0]         *  第一次table[0]是没有元素的，所以才能到这里添加空键和值         */        addEntry(0, null, value, 0);        return null;    }</code></pre><h3>第三步*</h3><pre><code class="language-Java">        int hash = hash(key);        int i = indexFor(hash, table.length);        for (Entry&lt;K,V&gt; e = table[i]; e != null; e = e.next) {            Object k;            if (e.hash == hash &amp;&amp; ((k = e.key) == key || key.equals(k))) {                V oldValue = e.value;                e.value = value;                e.recordAccess(this);                return oldValue;            }        }</code></pre><p>这步是最重要的一步，找到插入的位置然后将键值对插入到hashmap中。首先就是调用<code>hash</code>方法来计算传入的<code>key</code>的哈希值。</p><pre><code class="language-Java">    /**     * 检索对象哈希码，并将补充哈希函数应用于结果哈希，以防止质量差的哈希函数。     * 这很关键，只有HashMap使用2的幂的哈希表，不然哈希表在低位无差异时会遇到冲突。     * 注意：空键始终映射到哈希0，因此索引为0。     */    final int hash(Object k) {        int h = hashSeed;        //  若哈希值不为0而且不是String类型，就返回一个哈希值        if (0 != h &amp;&amp; k instanceof String) {            return sun.misc.Hashing.stringHash32((String) k);        }        // 异或        h ^= k.hashCode();        // 此函数可确保在每个位位置仅相差恒定倍数的hashCode具有一定数量的冲突（在默认的加载因子下约为8）。        h ^= (h &gt;&gt;&gt; 20) ^ (h &gt;&gt;&gt; 12);        return h ^ (h &gt;&gt;&gt; 7) ^ (h &gt;&gt;&gt; 4);    }</code></pre><p>得到哈希值之后，就会调用<code>indexFor</code>方法来计算出应该插入的位置。</p><pre><code class="language-Java">    /**     * 返回计算出来的索引     */    static int indexFor(int h, int length) {        // assert Integer.bitCount(length) == 1 : &quot;长度必须为2的非零次幂&quot;;        // h &amp; (length-1)表示hash值与数组长度求余，使用位移运算可以提高效率        // 为什么必须是2的非零次幂，之后还会提到        return h &amp; (length-1);    }</code></pre><p>计算出具体插入的位置之后就开始插入了，这里就是用了一个for循环遍历。</p><pre><code class="language-java">    //  遍历table[i]这个桶里是否存在这个key值，若有就将原来的value覆盖，并返回原来的value    for (Entry&lt;K,V&gt; e = table[i]; e != null; e = e.next) {        Object k;        //  如果哈希值相同，并且key值也相同，就进行替换        if (e.hash == hash &amp;&amp; ((k = e.key) == key || key.equals(k))) {            V oldValue = e.value;            e.value = value;            e.recordAccess(this);            return oldValue;        }    }    modCount++;    //  如果桶里不存在这个key值就将这对键值对加入到桶中    addEntry(hash, key, value, i);    //  第一次加入到桶就返回null值    return null;</code></pre><p>至此<code>put</code>的流程就结束了，到这里其实还是有问题。用<code>indexFor</code>这个方法来求数组的位置，刚刚也分析了这个方法，就是异或求余，那么就会有以下的问题，hash差为一定的倍数的时候，求余出来的下标就会是一样的，这个问题就是经常提到的<code>哈希冲突</code>。为了解决哈希冲突这个问题，就引进了链表，也就是之前说的加入到桶中，这就是为什么哈希表底层是用数组和链表构成的了。下面讲讲大概是怎么用到链表的。</p><p>:::note{type=&quot;info&quot;}</p><p>图中只是为了演示大概解决的过程，实现可以参考下面的createEntry方法和扩容中的transfer方法。由于哈希的计算会使数据尽量平均，而且扩容的时候又会重新计算下标，在数组长度较小的时候哈希冲突的发生的概率是很小的。</p><p>:::</p><p><img src="https://img.yww52.com/2020/11/2020-11-20/img2.png" alt=""></p><p>假设键值对3计算出来的<code>i</code>是3，就会被放到数组的第四个位置，直接放入。</p><p>假设键值对4计算出来的<code>i</code>是2，就会放到数组的第三个位置，但是这个位置已经有键值对1了，这种情况就会用到链表，<code>头插</code>到该链表。</p><p><img src="https://img.yww52.com/2020/11/2020-11-20/img3.png" alt=""></p><p>但是如果是这种情况，就无法通过数组来寻找到键值对4了，所以还会变化一下指针的指向，最后就会变成这样。</p><p><img src="https://img.yww52.com/2020/11/2020-11-20/img4.png" alt=""></p><h3>addEntry</h3><p>在上面put的第三步不难看到，在一个桶里只要有key相同，就覆盖，没有就使用<code>addEntry</code>这个方法加入到桶里。不难看出这个方法发生的情况，数组指定位置没有该元素或者是发生哈希冲突时就会用到这个方法，现在来看看这个方法。</p><pre><code class="language-Java">    /**     * 将具有指定键，值和哈希码的Entry添加到指定存储桶     * 如果有必要，该方法也会负责调整表的大小     */    void addEntry(int hash, K key, V value, int bucketIndex) {        //  如果当前数组长度大于阈值，并且该桶不是空的就执行下面语句        if ((size &gt;= threshold) &amp;&amp; (null != table[bucketIndex])) {            //  扩容到原来长度的两倍            resize(2 * table.length);            //  key等于空，hash赋值为0，不然用hash方法获取哈希            hash = (null != key) ? hash(key) : 0;            //  因为扩容了，所以需要重新计算存储位置            bucketIndex = indexFor(hash, table.length);        }        //  创建Entry加入到桶里        createEntry(hash, key, value, bucketIndex);    }    /**     * 与addEntry类似，不同之处在于，在创建条目作为Map构造或“伪构造”（克隆，反序列化）的一部分时使用此版本。     * 此版本无需担心调整表的大小。     * 子类重写此方法，以更改HashMap（Map），克隆和readObject的行为。     */    void createEntry(int hash, K key, V value, int bucketIndex) {        Entry&lt;K,V&gt; e = table[bucketIndex];        //  这条语句就说明了哈希表链表的插入是头插的        table[bucketIndex] = new Entry&lt;&gt;(hash, key, value, e);        size++;    }</code></pre><h2>扩容</h2><p>刚刚看到<code>addEntry</code>中用<code>resize</code>方法来扩容，现在就来看看这个方法是怎么扩容的。先看看这个方法的源码。</p><pre><code class="language-Java">    /**     * 将此映射的内容重新映射到容量更大的新数组中。     * 当此映射中的键数达到其阈值时，将自动调用此方法。     * 如果当前容量为MAXIMUM_CAPACITY，则此方法不会调整地图的大小，而是将阈值设置为Integer.MAX_VALUE。     * 这具有防止将来通话的效果。     *     * @param 新容量必须是2的幂，而且是大于当前容量。     *        除非当前容量是MAXIMUM_CAPACITY，这种情况就无关紧要。     */    void resize(int newCapacity) {        //  获取旧的数组和size的大小，判断是不是大于MAXIMUM_CAPACITY。        Entry[] oldTable = table;        int oldCapacity = oldTable.length;        if (oldCapacity == MAXIMUM_CAPACITY) {            threshold = Integer.MAX_VALUE;            return;        }        //  创建新的数组        Entry[] newTable = new Entry[newCapacity];        //  数据迁移，将原来数组的键值对放入新数组        transfer(newTable, initHashSeedAsNeeded(newCapacity));        table = newTable;        //重新设置阈值，等于新长度*负载因子或者MAXIMUM_CAPACITY + 1        threshold = (int)Math.min(newCapacity * loadFactor, MAXIMUM_CAPACITY + 1);    }</code></pre><p><code>resize</code>方法是挺好理解的了，只有这个迁移函数有点讲究。</p><pre><code class="language-Java">    /**     * 将所有桶从当前表转移到newTable     */    void transfer(Entry[] newTable, boolean rehash) {        int newCapacity = newTable.length;        //  遍历数组        for (Entry&lt;K,V&gt; e : table) {            //  遍历桶            while(null != e) {                //  保存e的下一个节点，因为之后链表会断开                Entry&lt;K,V&gt; next = e.next;                //  重新计算e的hash和下标                if (rehash) {                    e.hash = null == e.key ? 0 : hash(e.key);                }                int i = indexFor(e.hash, newCapacity);                //  后面这三句可以参考以下图示。                //  这条语句是挺奇怪的，之后也会导致一些问题出现，之后在说                e.next = newTable[i];                //  节点放到桶里                newTable[i] = e;                //  继续处理原来链表中的下一个节点                e = next;            }        }    }</code></pre><p>:::note{type=&quot;info&quot;}</p><p>下面例子简化了长度，和一些细节，因为这样转移是很小几率的，只是为了展示转移方法的大概实现。</p><p>:::</p><p><img src="https://img.yww52.com/2020/11/2020-11-20/img5.png" alt=""></p><p><img src="https://img.yww52.com/2020/11/2020-11-20/img6.png" alt=""></p><p><img src="https://img.yww52.com/2020/11/2020-11-20/img7.png" alt=""></p><p><img src="https://img.yww52.com/2020/11/2020-11-20/img8.png" alt=""></p><p><img src="https://img.yww52.com/2020/11/2020-11-20/img9.png" alt=""></p><p><img src="https://img.yww52.com/2020/11/2020-11-20/img10.png" alt=""></p><p>:::note{type=&quot;info&quot;}</p><p>transfer方法和之前的createEntry方法，表明了1.7的哈希表为什么是头插的。因为头插，哈希表还会出现一些问题，之后还会提到。</p><p>:::</p><h2>get</h2><p>get方法就没有put需要考虑到这么多情况了了，所以简单很多。</p><pre><code class="language-Java">    /**     * 返回指定键所映射到的null如果此映射不包含键的映射关系，则返回null 。     * 更正式地讲，如果此映射包含从键k到值v的映射，     * 使得(key==null ? k==null : key.equals(k)) ，则此方法返回v     * 否则返回null 。（最多可以有一个这样的映射。）     * 返回值null不一定表示该映射不包含该键的映射。 映射也可能将键显式映射为null 。     * containsKey操作可用于区分这两种情况     */    public V get(Object key) {        if (key == null)            return getForNullKey();        Entry&lt;K,V&gt; entry = getEntry(key);        return null == entry ? null : entry.getValue();    }</code></pre><h3>第一步</h3><pre><code class="language-Java">        if (key == null)            return getForNullKey();</code></pre><p>首先就是判断key值是不是为null，如果是null就调用<code>getForNullKey</code>。</p><pre><code class="language-Java">    /**     * get以查找空键。 空键映射到索引0。     * 为了在两个最常用的操作（获取和放置）中提高性能，此空情况被拆分为单独的方法，但在其他条件中并入了条件。     */    private V getForNullKey() {        //  要是数组没有值就返回null        if (size == 0) {            return null;        }        //  遍历table[0]的桶，寻找key==null的value值，并返回        for (Entry&lt;K,V&gt; e = table[0]; e != null; e = e.next) {            if (e.key == null)                return e.value;        }        //  桶里没有就返回null        return null;    }</code></pre><h3>第二步</h3><pre><code class="language-Java">        Entry&lt;K,V&gt; entry = getEntry(key);        //  entry为null，就返回null，不然就返回value        return null == entry ? null : entry.getValue();</code></pre><p>这两句也挺好理解的，用<code>getEntry</code>方法来获取键值对，然后返回value。</p><pre><code class="language-Java">    /**     * 返回与HashMap中的指定键关联的条目。 如果HashMap不包含该键的映射，则返回null     */    final Entry&lt;K,V&gt; getEntry(Object key) {        //  数组为空返回null        if (size == 0) {            return null;        }        //  计算key的哈希值        int hash = (key == null) ? 0 : hash(key);        //  先通过哈希计算下标，然后遍历下标所在的桶，找到就返回entry，没找到就返回null        for (Entry&lt;K,V&gt; e = table[indexFor(hash, table.length)];             e != null;             e = e.next) {            Object k;            if (e.hash == hash &amp;&amp;                ((k = e.key) == key || (key != null &amp;&amp; key.equals(k))))                return e;        }        return null;    }</code></pre><h1>JDK8</h1><p>:::note{type=&quot;success&quot;}</p><p>JDK8优化了不少，但与JDK7在很多部分是一样的，所以我着重去分析一下不一样的地方。</p><p>:::</p><h2>参数和变量</h2><pre><code class="language-Java">    //  相同的参数或者变量    private static final long serialVersionUID = 362498820763181265L;    static final int DEFAULT_INITIAL_CAPACITY = 1 &lt;&lt; 4;    static final int MAXIMUM_CAPACITY = 1 &lt;&lt; 30;    static final float DEFAULT_LOAD_FACTOR = 0.75f;    transient int size;    transient int modCount;    final float loadFactor;    int threshold;    //  JDK7独有的    static final Entry&lt;?,?&gt;[] EMPTY_TABLE = {};    transient Entry&lt;K,V&gt;[] table = (Entry&lt;K,V&gt;[]) EMPTY_TABLE;    static final int ALTERNATIVE_HASHING_THRESHOLD_DEFAULT = Integer.MAX_VALUE;    transient int hashSeed = 0;    private transient Set&lt;Map.Entry&lt;K,V&gt;&gt; entrySet = null;    //  JDK8独有的    transient Node&lt;K,V&gt;[] table;    transient Set&lt;Map.Entry&lt;K,V&gt;&gt; entrySet;    static final int TREEIFY_THRESHOLD = 8;    static final int UNTREEIFY_THRESHOLD = 6;    static final int MIN_TREEIFY_CAPACITY = 64;</code></pre><pre><code class="language-Java">    /**     * 该表在首次使用时初始化，并根据需要调整大小。     * 分配时，长度始终是2的幂。（在某些操作中，我们还允许长度为零，以允许使用当前不需要的引导机制。）     */    transient Node&lt;K,V&gt;[] table;    /**     * 保存缓存的entrySet（）。 注意，AbstractMap字段用于keySet（）和values（）     */    transient Set&lt;Map.Entry&lt;K,V&gt;&gt; entrySet;    //  链表的树化阈值，即链表长度大于8就会有可能转换成红黑树    static final int TREEIFY_THRESHOLD = 8;    //  链表的还原阈值，即红黑树的节点小于6就换转换成链表    static final int UNTREEIFY_THRESHOLD = 6;    //  最小树形化阈值，数组长度大于64才会转换成红黑树    static final int MIN_TREEIFY_CAPACITY = 64;</code></pre><h2>构造器</h2><pre><code class="language-Java">    public HashMap() {        this.loadFactor = DEFAULT_LOAD_FACTOR; // 其他字段默认    }    public HashMap(int initialCapacity) {        this(initialCapacity, DEFAULT_LOAD_FACTOR);    }    public HashMap(int initialCapacity, float loadFactor) {        if (initialCapacity &lt; 0)            throw new IllegalArgumentException(&quot;Illegal initial capacity: &quot; +                                               initialCapacity);        if (initialCapacity &gt; MAXIMUM_CAPACITY)            initialCapacity = MAXIMUM_CAPACITY;        if (loadFactor &lt;= 0 || Float.isNaN(loadFactor))            throw new IllegalArgumentException(&quot;Illegal load factor: &quot; +                                               loadFactor);        this.loadFactor = loadFactor;        //  上面与JDK7的一样，不一样的就是这句和省略了init初始化方法        this.threshold = tableSizeFor(initialCapacity);    }    /**     * 与JDK7的实现有些不一样，但作用是一样的     */    public HashMap(Map&lt;? extends K, ? extends V&gt; m) {        this.loadFactor = DEFAULT_LOAD_FACTOR;        putMapEntries(m, false);    }</code></pre><p>可以看出只有两个比较特殊的地方，那就是<code>tableSizeFor</code>和<code>putMapEntries</code>这两个方法了。</p><pre><code class="language-Java">    /**     * 返回大于指定值的最小二次幂，作用类似highestOneBit方法     * 用来代替JDK7中的roundUpToPowerOf2方法     */    static final int tableSizeFor(int cap) {        int n = cap - 1;        n |= n &gt;&gt;&gt; 1;        n |= n &gt;&gt;&gt; 2;        n |= n &gt;&gt;&gt; 4;        n |= n &gt;&gt;&gt; 8;        n |= n &gt;&gt;&gt; 16;        //  可认为将JDK7中roundUpToPowerOf2和highestOneBit方法合并了        return (n &lt; 0) ? 1 : (n &gt;= MAXIMUM_CAPACITY) ? MAXIMUM_CAPACITY : n + 1;    }    /**     * 实现Map.putAll和Map构造函数。     * 将map中的元素加入到HashMap中去。     */    final void putMapEntries(Map&lt;? extends K, ? extends V&gt; m, boolean evict) {        int s = m.size();        if (s &gt; 0) {            if (table == null) { // pre-size                float ft = ((float)s / loadFactor) + 1.0F;                int t = ((ft &lt; (float)MAXIMUM_CAPACITY) ?                         (int)ft : MAXIMUM_CAPACITY);                if (t &gt; threshold)                    threshold = tableSizeFor(t);            }            else if (s &gt; threshold)                resize();            for (Map.Entry&lt;? extends K, ? extends V&gt; e : m.entrySet()) {                K key = e.getKey();                V value = e.getValue();                putVal(hash(key), key, value, false, evict);            }        }    }</code></pre><h2>数据结构</h2><p>JDK8的底层进行了一些优化，主体依旧是数组，链表为辅，但是链表过长会导致hashmap的查询效率变低因为链表的查询操作的时间复杂度为O(n)，所以在JDK8中引入了红黑树。当链表达到树化阈值的时候，就会转变为红黑树因为红黑树的查询操作的时间复杂度为O(log(n))，所以可以提高查询桶里节点的效率</p>]]></content:encoded>
            <category domain="https://yww52.com/categories/%25E6%2588%2591%25E7%259A%2584%25E6%258A%2580%25E6%259C%25AF%25E5%25AD%25A6%25E4%25B9%25A0%25E7%2589%25A9%25E8%25AF%25AD%25E6%259E%259C%25E7%2584%25B6%25E6%259C%2589%25E9%2597%25AE%25E9%25A2%2598/">我的技术学习物语果然有问题</category>
        </item>
        <item>
            <title>树</title>
            <link>https://yww52.com/posts/7b216a3b/</link>
            <guid isPermaLink="true">https://yww52.com/posts/7b216a3b/</guid>
            <pubDate>Wed, 18 Nov 2020 00:00:00 +0000</pubDate>
            <description>刷Leetcode关于树的题的笔记。</description>
            <content:encoded><![CDATA[<img src="https://img.yww52.com/2020/10/2020-10-23top_img.jpg" alt="树" style="max-width:100%;height:auto;margin-bottom:15px;" /><h1>94. 二叉树的中序遍历</h1><h2>递归</h2><pre><code class="language-Java">class Solution {    List&lt;Integer&gt; res = new ArrayList&lt;&gt;();    public List&lt;Integer&gt; inorderTraversal(TreeNode root) {        if (root == null) return res;        fnc(root);        return res;    }    public void fnc(TreeNode node) {        if (node == null) return;        fnc(node.left);        res.add(node.val);        fnc(node.right);    }}</code></pre><h2>迭代</h2><p>迭代会麻烦一点，主要先记住中序遍历的特点，先遍历左子树，在遍历当前节点，在遍历右子树。</p><p>所以当该节点有左子树的时候就先进入到左边。</p><pre><code class="language-Java">class Solution {    public List&lt;Integer&gt; inorderTraversal(TreeNode root) {        List&lt;Integer&gt; res = new ArrayList&lt;&gt;();        if (root == null) return res;        Stack&lt;TreeNode&gt; stack = new Stack&lt;&gt;();        while(!stack.isEmpty() || root != null) {            // 一直寻找左节点while (root != null) {                stack.push(root);                root = root.left;            }            root = stack.pop();            // 处理左子树的当前节点，进入右子树            res.add(root.val);            root = root.right;}        return res;    }}</code></pre><h1>98. 验证二叉搜索树</h1><h2>解法一</h2><p>二叉搜索树中序遍历是有序的，所以可以先全部遍历，在判断是否有序。</p><pre><code class="language-Java">class Solution {    List&lt;Integer&gt; list = new ArrayList&lt;&gt;();    public boolean isValidBST(TreeNode root) {        isvailbst(root);        for(int i = 1;i &lt; list.size(); i++) {            if(list.get(i) &lt;= list.get(i - 1)) return false;        }        return true;    }    public void isvailbst(TreeNode root) {        if(root == null) return;        isvailbst(root.left);        list.add(root.val);        isvailbst(root.right);    }}</code></pre><h2>解法二</h2><p>上面的解法一其实可以看出是浪费了一些时间了的，其实可以直接在后一个值小于前一个值的时候就停下来的。</p><pre><code>class Solution {    long pre = Long.MIN_VALUE;    public boolean isValidBST(TreeNode root) {        if(root != null) {            if(!isValidBST(root.left)) return false;            if(root.val &lt;= pre) return false;            pre = root.val;            if(!isValidBST(root.right)) return false;        }        return true;    }}</code></pre><h2>解法三</h2><p>这种方法也很好，直接划个边界给给子树递归，判断是否出边界。不过注意的是，设置边界的最大值的时候，不能用<code>Integer.MAX_VALUE</code>和<code>Integer.MIN_VALUE</code>，因为有样例正好是<code>Integer.MAX_VALUE</code>和<code>Integer.MIN_VALUE</code>，所以只能取大一点的long。</p><pre><code class="language-Java">class Solution {    public boolean isValidBST(TreeNode root) {        return vailbst(root,Long.MIN_VALUE,Long.MAX_VALUE);    }    public static boolean vailbst(TreeNode root,long min,long max) {        if(root == null) return true;        if(root.val &lt;= min || root.val &gt;= max) {            return false;        }        return vailbst(root.left,min,root.val) &amp;&amp; vailbst(root.right,root.val,max);    }}</code></pre><h1>100.相同的树</h1><p>相同的树，首先要判断当前节点是否相同。不相同直接返回<code>false</code>。</p><p>然后递归判断两棵树的左子树和右子树，判断一下他们的左子树是否相同和右子树是否相同即可。</p><pre><code class="language-java">class Solution {public boolean isSameTree(TreeNode p, TreeNode q) {        if (p == null &amp;&amp; q == null) {            return true;        }        if (p == null || q == null) {            return false;        }        if (p.val != q.val) {            return false;        }        return isSameTree(p.left,q.left) &amp;&amp; isSameTree(p.right,q.right);    }}</code></pre><h1>101.对称二叉树</h1><h2>递归</h2><ol><li>判断当前节点是否是一样的</li><li>将左子树的左子树和右子树的右子树丢入递归判断，将左子树的右子树和右子树的左子树丢入递归判断，结果取并</li></ol><pre><code class="language-Java">class Solution {    public boolean isSymmetric(TreeNode root) {        if (root == null) {            return true;        }        return issymmetric(root.left, root.right);    }    public boolean issymmetric(TreeNode left,TreeNode right) {        if (left == null &amp;&amp; right == null) {            return true;        }        if (left == null || right == null) {            return false;        }        if (left.val != right.val) {            return false;        }        return issymmetric(left.left,right.right) &amp;&amp; issymmetric(right.left,left.right);    }}</code></pre><h2>迭代</h2><p>使用双栈。</p><pre><code class="language-Java">class Solution {   public boolean isSymmetric(TreeNode root) {        if (root == null) {            return true;        }        Stack&lt;TreeNode&gt; left = new Stack&lt;&gt;();        Stack&lt;TreeNode&gt; right = new Stack&lt;&gt;();        left.push(root.left);        right.push(root.right);        while (!left.isEmpty() &amp;&amp; !right.isEmpty()) {            TreeNode t1 = left.pop();            TreeNode t2 = right.pop();            if (t1 == null &amp;&amp; t2 == null) {                continue;            }            if (t1 == null || t2 == null) {                return false;            }            if (t1.val != t2.val) {                return false;            }            left.push(t1.left);            right.push(t2.right);            left.push(t1.right);            right.push(t2.left);        }        return true;    }}</code></pre><h2>层次遍历</h2><p>使用队列进行层次遍历，然后判断是否回文。</p><h1>102. 二叉树的层序遍历</h1><h2>递归解法</h2><p>主要是判断一下是在哪一层，然后分层处理就可以了。</p><pre><code class="language-Java">class Solution {    List&lt;List&lt;Integer&gt;&gt; res = new ArrayList&lt;&gt;();    public List&lt;List&lt;Integer&gt;&gt; levelOrder(TreeNode root) {        levelorder(root,0);        return res;    }        public void levelorder(TreeNode node,int k) {        if (node == null) return;        // 没有当前层        if (res.size() &lt;= k) {            res.add(new ArrayList&lt;Integer&gt;());        }        // 将节点放入k层的List中        res.get(k).add(node.val);        // 递归处理        levelorder(node.left,k+1);        levelorder(node.right,k+1);    }}</code></pre><h2>迭代解法</h2><p>二叉树的层序遍历的迭代可以借组队列来进行存储节点。</p><pre><code class="language-Java">class Solution {    public List&lt;List&lt;Integer&gt;&gt; levelOrder(TreeNode root) {        List&lt;List&lt;Integer&gt;&gt; res = new ArrayList&lt;&gt;();        if (root == null) return res;        Deque&lt;TreeNode&gt; deque = new LinkedList&lt;&gt;();        deque.offer(root);        while (!deque.isEmpty()) {            // 统计当前层的节点个数            int count = deque.size();            List&lt;Integer&gt; list = new ArrayList&lt;&gt;(count);            while (count-- &gt; 0) {                TreeNode node = deque.poll();                list.add(node.val);                // 先加入左节点在加入右节点，z                if (node.left != null) deque.offer(node.left);                if (node.right != null) deque.offer(node.right);            }            res.add(list);        }        return res;    }}</code></pre><h1>104.二叉树的最大深度</h1><ol><li>首先判断根节点是不是空，不是则值加一</li><li>递归判断左子树与右子树，取最大值</li></ol><pre><code class="language-java">class Solution {    public int maxDepth(TreeNode root) {        int res = 0;        if (root == null) {            return res;        } else {            res ++;        }        return res + Math.max(maxDepth(root.left), maxDepth(root.right));    }}</code></pre><p>优化之后：</p><pre><code class="language-Java">    public int maxDepth(TreeNode root) {        if (root == null) return 0;        return Math.max(maxDepth(root.left), maxDepth(root.right)) + 1;    }</code></pre><h1>105.从前序与中序遍历序列构造二叉树</h1><p>假设根的前序遍历的在中序遍历中的位置为K，那么在中序遍历中，1到K-1都是该根的左子树，K+1到最后是该根的右子树。</p><pre><code class="language-Java">class Solution {    int pre = 0;    int ino = 0;    public TreeNode buildTree(int[] preorder, int[] inorder) {        return build(preorder,inorder,Integer.MAX_VALUE + 1);    }    // stop表示当前子树根的值，中序遍历到了该值，就表示左子树已经搭建好了    public TreeNode build(int[] preorder,int[] inorder, long stop) {        if (pre == preorder.length) return null;        if (inorder[ino] == stop) {            ino++;            return null;        }        int val = preorder[pre++];        TreeNode root = new TreeNode(val);        root.left = build(preorder,inorder,val);        root.right = build(preorder,inorder,stop);        return root;    }}</code></pre><h1>106.从中序与后序遍历序列构造二叉树</h1><p>与上一题差不多，只是有一点点不一样而已。</p><pre><code class="language-Java">class Solution {    int ino = 0;    int pos = 0;    public TreeNode buildTree(int[] inorder, int[] postorder) {        pos = postorder.length - 1;        ino = inorder.length - 1;        return build(inorder,postorder,Integer.MIN_VALUE - 1);    }    public TreeNode build(int[] inorder, int[] postorder,long stop) {        if (pos == -1) return null;        if (inorder[ino] == stop) {            ino--;            return null;        }        int val = postorder[pos--];        TreeNode root = new TreeNode(val);        root.right = build(inorder,postorder,val);        root.left = build(inorder,postorder,stop);        return root;    }}</code></pre><p>.</p><h1>107.二叉树的层次遍历II</h1><p>从低向上层次遍历，其实就是先层次遍历然后反转就好了。</p><pre><code class="language-Java">class Solution {    //定义结果集    List&lt;List&lt;Integer&gt;&gt; res = new ArrayList&lt;&gt;();    public List&lt;List&lt;Integer&gt;&gt; levelOrderBottom(TreeNode root) {        levelorder(root,0);        //反转list        List&lt;List&lt;Integer&gt;&gt; ans = new ArrayList&lt;&gt;();        for(int i = res.size()-1 ; i &gt;= 0; i--) {            ans.add(res.get(i));        }        return ans;    }    public void levelorder(TreeNode node,int k) {        if (node != null) {            if (res.size() &lt;= k) {                res.add(new ArrayList&lt;Integer&gt;());            }            res.get(k).add(node.val);            levelorder(node.left,k+1);            levelorder(node.right,k+1);        }    }}</code></pre><h1>110.平衡二叉树</h1><h2>递归一</h2><p>首先，判断左右两个子树的高度的绝对值不超过1，就要用到计算二叉树的最大深度了，递归获得深度，左右子树深度差的绝对值小于等于1就返回真，然后遍历左子树和右子树就好了。</p><pre><code class="language-Java">class Solution {    public boolean isBalanced(TreeNode root) {        //判断是否为空，因为空了就直接停止递归了        //为空就返回真，因为答案要与运算，返回真不会影响        if (root == null) {            return true;        }        //判断是否符合平衡二叉树，用到否定条件        //如果是用小于等于1返回真，那就没有false返回了，答案全是真        if (Math.abs(maxDepth(root.left) - maxDepth(root.right)) &gt; 1) {            return false;        }        //遍历左子树和右子树取与        return isBalanced(root.left) &amp;&amp; isBalanced(root.right);    }        //最大深度遍历就直接用104题目的代码了。    public int maxDepth(TreeNode root) {        int res = 0;        if (root == null) {            return res;        } else {            res ++;        }        return res + Math.max(maxDepth(root.left), maxDepth(root.right));    }}</code></pre><h2>递归二</h2><p>这个和上面算法差不多，这个算法是修改了最大深度的求法，不符和条件的就返回-1的深度，要是有-1深度返回代表不符合条件。</p><pre><code class="language-Java">class Solution {    public boolean isBalanced(TreeNode root) {        return maxDepth(root) &gt;= 0;    }    public static int maxDepth(TreeNode root) {        if (root == null) {            return 0;        }         int leftheight = maxDepth(root.left);        int rightheight = maxDepth(root.right);        //判断左右子树是否平衡        if(leftheight &gt;= 0 &amp;&amp; rightheight &gt;= 0 &amp;&amp; Math.abs(leftheight - rightheight ) &lt;= 1) {            return Math.max(leftheight,rightheight) + 1;        } else {            return -1;        }    }}</code></pre><h1>111.二叉树的最小深度</h1><p>求最小深度，就与求最大深度相反就好，只是不能让空子树参与计算，不然一直返回0。</p><pre><code class="language-Java">class Solution {    public int minDepth(TreeNode root) {        if (root == null) {            return 0;        }         if (root.left == null &amp;&amp; root.right !=null) {            return 1 + minDepth(root.right);         }        if (root.left != null &amp;&amp; root.right == null) {            return 1 + minDepth(root.left);         }        return 1 + Math.min(minDepth(root.left),minDepth(root.right));            }}</code></pre><h1>112.路径总和</h1><p>这题是判断路径总和是否等于给定值，不能从下到上的计算节点值，因为情况多会无法相加，所以只能从上到下的相减，看是否等于0。</p><pre><code class="language-Java">class Solution {    public boolean hasPathSum(TreeNode root, int sum) {        if (root == null) {            return false;        }        //如果是叶子节点，就直接计算是否符合         if (root.left == null &amp;&amp; root.right == null) {            return sum - root.val == 0;        }        return hasPathSum(root.left,sum - root.val) || hasPathSum(root.right,sum - root.val);    }    }</code></pre><h1>230.二叉搜索树中第K小的元素</h1><p>这题就是根据二叉搜索树的特性，中序遍历会是从小到大，然后找到第K个就好了。</p><pre><code class="language-Java">class Solution {    int res = 0;    int rank = 0;    public int kthSmallest(TreeNode root, int k) {        small(root,k);        return res;    }    public void small(TreeNode root,int k) {        if (root == null) return;        small(root.left,k);        rank++;        if (k == rank) {            res = root.val;            return;        }        small(root.right,k);    }}</code></pre><p>上面的方法是最暴力的解法，还可以进行优化。根据二叉搜索树的左子树会比根节点小，右子树会比根节点大，就可以找到第K小的元素。</p><pre><code class="language-Java">class Solution {        public int kthSmallest(TreeNode root, int k) {        int left = sumTree(root.left);        if (left + 1 == k) return root.val;        else if (k &lt;= left) return kthSmallest(root.left, k);        else return kthSmallest(root.right, k - left - 1);    }    public int sumTree(TreeNode root) {        if (root == null) return 0;        return sumTree(root.left) + sumTree(root.right) + 1;    }}</code></pre><h1>450.删除二叉搜索树中的节点</h1><p>这题通过遍历就可以找到需要删除的节点，这题的难点是怎样删除找到的节点。</p><p>确认了需要删除的节点后，就要先找到该节点右子树最小的值，用该值代替删除的节点，因为这样就不会破环二叉搜索树的结构了。（也可以寻找左子树的最大值）</p><pre><code class="language-Java">class Solution {    public TreeNode deleteNode(TreeNode root, int key) {        if (root == null) return null;        if (root.val == key) {            if (root.left == null) return root.right;            if (root.right == null) return root.left;            // 寻找节点右子树的最小值            TreeNode node = root.right;            while (node.left != null) node = node.left;            // 将节点的左子树赋值给最小值的左子树完成删除            node.left = root.left;            return root.right;        } else if (key &gt; root.val) {            root.right = deleteNode(root.right,key);            } else {                root.left = deleteNode(root.left,key);            }        return root;    }}</code></pre><h1>538. 把二叉搜索树转换为累加树</h1><p>这题主要的问题是没理解题目到底在讲什么，看了答案才知道其实是将树从下到上，从右到左累加，然后将该值赋值给当前节点。</p><pre><code class="language-Java">class Solution {    int sum = 0;    public TreeNode convertBST(TreeNode root) {        if (root == null) return null;        convertBST(root.right);        // 将累加结果赋值给当前节点，并记录累加        root.val = root.val + sum;        sum = root.val;        convertBST(root.left);        return root;    }}</code></pre><h1>589.N叉树的前序遍历</h1><h2>递归</h2><ol><li>判断root是否为空结点</li><li>将数据存入List</li><li>递归前序遍历各个结点</li></ol><p>这道题难的就是结果的存储了，因为经常写的前序遍历都是直接输出结果的，存储的化就要全局的List了。</p><pre><code class="language-Java">public class Solution {    List&lt;Integer&gt; res = new ArrayList&lt;&gt;();    public List&lt;Integer&gt; preorder(Node root) {        if (root == null) {            return res;        }        res.add(root.val);        for(Node node : root.children){            preorder(node);        }        return res;    }}</code></pre><h2>非递归</h2><ol><li>先判断root是否为空结点</li><li>将root结点压入栈</li><li>当栈不为空，将栈中数据存入List，将当前结点的孩子压入栈</li></ol><pre><code class="language-Java">public class Solution {    public List&lt;Integer&gt; preorder(Node root) {        List&lt;Integer&gt; list = new ArrayList&lt;&gt;();        if (root == null) return list;        Stack&lt;Node&gt; stack = new Stack&lt;&gt;();        stack.push(root);        while(!stack.isEmpty()) {            Node node = stack.pop();            list.add(node.val);            for (int i = node.children.size() - 1; i &gt;= 0; i--) {                stack.push(node.children.get(i));            }        }        return list;    }}</code></pre><h1>652.寻找重复的子树</h1><p>这题主要的难点就是怎样判断该节点的子树和其他节点的子树是否一样。</p><p>可以考虑到的是将该节点的子树和根作为一个字符串，如果其他字符串与其一样，就判断为这是重复的子树节点。</p><pre><code class="language-Java">class Solution {// 用来去重    HashMap&lt;String,Integer&gt; map = new HashMap&lt;&gt;(16);    List&lt;TreeNode&gt; res = new ArrayList&lt;&gt;();         public List&lt;TreeNode&gt; findDuplicateSubtrees(TreeNode root) {        traverse(root);        return res;    }    public String traverse(TreeNode root) {        if(root == null) {            return &quot;#&quot;;        }        String left = traverse(root.left);        String right = traverse(root.right);        String str = left + &quot;+&quot; + right + &quot;+&quot; + root.val;        int freq = map.getOrDefault(str,0);        // 不重复将值加入到List中        if (freq == 1) res.add(root);        map.put(str,freq + 1);        return str;    }}</code></pre><h1>897.递增顺序查找树</h1><p>直接中序遍历然后把节点赋给结果子树的右子树就好了。</p><pre><code class="language-Java">class Solution {    //head用来记录根节点，因为res要递归右子树    TreeNode head = new TreeNode();    TreeNode res = right;    public TreeNode increasingBST(TreeNode root) {        if (root == null) {            return null;        }        increasingBST(root.left);        //将当前节点赋值给res的右子树，然后再进入右子树，左子树为空        res.right = root;        root.left=null;        res = res.right;        increasingBST(root.right);        return head.right;    }}</code></pre><h1>938.二叉搜索树的范围和</h1><p>暴力递归破解。</p><pre><code class="language-Java">class Solution {    int sum = 0;    public int rangeSumBST(TreeNode root, int low, int high) {        range(root,low,high);        return sum;    }    public void range(TreeNode root,int low,int high) {        if (root == null) return;        range(root.left,low,high);        if (root.val &gt;= low &amp;&amp; root.val &lt;= high) sum += root.val;        range(root.right,low,high);    }}</code></pre><p>因为中序遍历是从小到大的，所以还可以优化一下。</p><h1>965.单值二叉树</h1><pre><code class="language-java">class Solution {    public boolean isUnivalTree(TreeNode root) {        return isTree(root,root.val);    }    public boolean isTree(TreeNode root,int val) {        if (root == null) return true;        if (root.val != val) return false;        return isTree(root.left,root.val) &amp;&amp; isTree(root.right,root.val);    }}</code></pre><h1>从根到叶的二进制数之和</h1><pre><code class="language-Java">class Solution {    int sum = 0;    public int sumRootToLeaf(TreeNode root) {        return sumroottoleaf(root,sum);    }    public int sumroottoleaf(TreeNode root,int sum) {        // 防止左右子树有一个是NULL的情况        if(root == null) {            return 0;        }        //左移一位再加值        sum = sum*2 + root.val;        if(root.left == null &amp;&amp; root.right == null){            return sum;        }        //递归        return sumroottoleaf(root.left,sum) + sumroottoleaf(root.right,sum);    }}</code></pre><h1>剑指 Offer 54. 二叉搜索树的第k大节点</h1><h2>暴力破解</h2><p>我的第一想法就是直接全部遍历，排序然后找到第k大的节点，虽然效率不这么好，但是也是一种方法。</p><pre><code class="language-Java">class Solution {    List&lt;Integer&gt; arr = new ArrayList&lt;&gt;();    public int kthLargest(TreeNode root, int k) {        largest(root);        Collections.sort(arr);        return arr.get(arr.size()-k);    }    public  void largest(TreeNode root) {        if (root == null) {            return;        }        arr.add(root.val);        largest(root.left);        largest(root.right);    }}</code></pre><h2>中序遍历</h2><p>因为给的树是二叉搜索树，所以应该想到二叉搜索树的中序遍历就是有序的，所以可以省去排序的时间。</p><pre><code>class Solution {    List&lt;Integer&gt; arr = new ArrayList&lt;&gt;();    public int kthLargest(TreeNode root, int k) {        largest(root);        return arr.get(arr.size()-k);    }    public  void largest(TreeNode root) {        if (root == null) {            return;        }        largest(root.left);        arr.add(root.val);        largest(root.right);    }}</code></pre><h2>直接寻找第k大的结点</h2><p>中序遍历毕竟是要遍历整个树，所以效率也只是比暴力破解好了一点点而已，要想节约时间，那就只搜索到第K大的结点就停止就好了，所以就用到了二叉搜索树的性质。（若它的左子树不空，则左子树上所有结点的值均小于它的根结点根结点的值； 若它的右子树不空，则右子树上所有结点的值均大于它的根结点的值。）通过中序遍历，找到第K个结点就可以了。</p><pre><code class="language-Java">class Solution {    int sum = 0,res = 0;    public int kthLargest(TreeNode root, int k) {        largest(root,k);        return res;    }    public void largest(TreeNode root,int k) {        if (root.right != null) {            largest(root.right,k);        }        sum++;        if(sum == k) {            res =  root.val;            return;        }        if (root.left != null) {            largest(root.left,k);        }    }}</code></pre><h1>面试题 04.03. 特定深度节点链表</h1><p>首先就是这个链表的插入，如果使用尾插就需要在声明一个头节点的数组存储头节点，所以我用了头插法，不过要注意顺序，先将右子树的值插入链表再将左子树的值插入链表，这样就能符和题目的顺序了。还有就是链表数组的长度，第一个解法我就直接再加个函数求取树的最大深度了，套用第104题就行。</p><pre><code class="language-Java">class Solution {    public ListNode[] listOfDepth(TreeNode tree) {        int n = maxDepth(tree);        ListNode[] res = new ListNode[n];        listofdepth(tree,0,res);        return res;    }    public int maxDepth(TreeNode root) {        int res = 0;        if (root == null) {            return res;        } else {            res ++;        }        return res + Math.max(maxDepth(root.left), maxDepth(root.right));    }    public void listofdepth(TreeNode tree,int t,ListNode[] res) {        if(tree == null) {            return;        }        ListNode node = new ListNode();        node.val = tree.val;        node.next = null;        listofdepth(tree.right,t+1,res);        node.next = res[t];        res[t] = node;        listofdepth(tree.left,t+1,res);    }}</code></pre><p>这种解法速度还是很快的，只是消耗的内存有点多，可能题目就打算消耗多的内存。还有一种思路就是使用层次遍历，其实也差不多。也看了一下其他解法，不过很多的解法消耗的内存有点多，然后看到一些内存消耗低的解法，发现寻找深度的代码还可以优化，然后又优化了一些细节。</p><pre><code class="language-Java">class Solution {    public ListNode[] listOfDepth(TreeNode tree) {        int depth = maxDepth(tree);        ListNode[] list = new ListNode[depth];        if (tree == null) return list;        listofdepth(tree,0,list);        return list;    }    public int maxDepth(TreeNode node) {        if (node == null) return 0;        return Math.max(maxDepth(node.left),maxDepth(node.right)) + 1;    }    public void listofdepth(TreeNode root,int k,ListNode[] list) {        if (root == null) return;        ListNode node = new ListNode(root.val);        node.next = list[k];        list[k] = node;        listofdepth(root.right,k + 1,list);        listofdepth(root.left,k + 1,list);    }}</code></pre>]]></content:encoded>
            <category domain="https://yww52.com/categories/%25E6%2588%2591%25E7%259A%2584%25E6%258A%2580%25E6%259C%25AF%25E5%25AD%25A6%25E4%25B9%25A0%25E7%2589%25A9%25E8%25AF%25AD%25E6%259E%259C%25E7%2584%25B6%25E6%259C%2589%25E9%2597%25AE%25E9%25A2%2598/">我的技术学习物语果然有问题</category>
        </item>
        <item>
            <title>Mybatis-plus</title>
            <link>https://yww52.com/posts/fd1960c6/</link>
            <guid isPermaLink="true">https://yww52.com/posts/fd1960c6/</guid>
            <pubDate>Sat, 14 Nov 2020 00:00:00 +0000</pubDate>
            <description>顾名思义，Mybatis-plus就是Mybatis的加强版，能帮助你更简便的使用Mybatis。</description>
            <content:encoded><![CDATA[<img src="https://img.yww52.com/2020/11/2020-11-14/top_img.jpg" alt="Mybatis-plus" style="max-width:100%;height:auto;margin-bottom:15px;" /><h1>简介</h1><p>MyBatis-Plus（简称 MP）是一个 MyBatis的增强工具，在 MyBatis 的基础上只做增强不做改变，为简化开发、提高效率而生。</p><ul><li><a href="https://baomidou.com/">官网地址</a></li></ul><h1>快速开始</h1><p>:::note{type=&quot;info&quot;}</p><p>参照官网快速开始的样例，官网用的是h2数据库，我这里用的是Mysql。</p><p>:::</p><ol><li><p>创建数据库<code>schema</code>，并执行以下脚本。</p><pre><code class="language-mysql">DROP TABLE IF EXISTS user;CREATE TABLE user(    id BIGINT(20) NOT NULL COMMENT &#39;主键ID&#39;,    name VARCHAR(30) NULL DEFAULT NULL COMMENT &#39;姓名&#39;,    age INT(11) NULL DEFAULT NULL COMMENT &#39;年龄&#39;,    email VARCHAR(50) NULL DEFAULT NULL COMMENT &#39;邮箱&#39;,    PRIMARY KEY (id));DELETE FROM user;INSERT INTO user (id, name, age, email) VALUES(1, &#39;Jone&#39;, 18, &#39;test1@baomidou.com&#39;),(2, &#39;Jack&#39;, 20, &#39;test2@baomidou.com&#39;),(3, &#39;Tom&#39;, 28, &#39;test3@baomidou.com&#39;),(4, &#39;Sandy&#39;, 21, &#39;test4@baomidou.com&#39;),(5, &#39;Billie&#39;, 24, &#39;test5@baomidou.com&#39;);</code></pre></li><li><p>添加依赖</p><p>主要的依赖。</p><pre><code class="language-xml">&lt;dependency&gt;    &lt;groupId&gt;com.baomidou&lt;/groupId&gt;    &lt;artifactId&gt;mybatis-plus-boot-starter&lt;/artifactId&gt;    &lt;version&gt;3.4.1&lt;/version&gt;&lt;/dependency&gt;</code></pre><p>测试依赖如下。</p><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&lt;/artifactId&gt;        &lt;/dependency&gt;        &lt;!--数据库驱动--&gt;        &lt;dependency&gt;            &lt;groupId&gt;mysql&lt;/groupId&gt;            &lt;artifactId&gt;mysql-connector-java&lt;/artifactId&gt;        &lt;/dependency&gt;        &lt;!--lombok--&gt;        &lt;dependency&gt;            &lt;groupId&gt;org.projectlombok&lt;/groupId&gt;            &lt;artifactId&gt;lombok&lt;/artifactId&gt;            &lt;optional&gt;true&lt;/optional&gt;        &lt;/dependency&gt;        &lt;!--mybatis-plus--&gt;        &lt;dependency&gt;            &lt;groupId&gt;com.baomidou&lt;/groupId&gt;            &lt;artifactId&gt;mybatis-plus-boot-starter&lt;/artifactId&gt;            &lt;version&gt;3.4.1&lt;/version&gt;        &lt;/dependency&gt;        &lt;!--单元测试--&gt;        &lt;dependency&gt;            &lt;groupId&gt;org.springframework.boot&lt;/groupId&gt;            &lt;artifactId&gt;spring-boot-starter-test&lt;/artifactId&gt;            &lt;scope&gt;test&lt;/scope&gt;            &lt;exclusions&gt;                &lt;exclusion&gt;                    &lt;groupId&gt;org.junit.vintage&lt;/groupId&gt;                    &lt;artifactId&gt;junit-vintage-engine&lt;/artifactId&gt;                &lt;/exclusion&gt;            &lt;/exclusions&gt;        &lt;/dependency&gt;&lt;/dependencies&gt;</code></pre></li><li><p>创建一个实体类</p><pre><code class="language-Java">package com.yw.pojo;import lombok.Data;@Datapublic class User {    private Long id;    private String name;    private Integer age;    private String email;}</code></pre></li><li><p>创建Mapper接口</p><pre><code class="language-Java">package com.yw.Mapper;import com.baomidou.mybatisplus.core.mapper.BaseMapper;import com.yw.pojo.User;import org.springframework.stereotype.Repository;@Repositorypublic interface UserMapper extends BaseMapper&lt;User&gt; {}</code></pre></li><li><p>配置数据源和日志</p><pre><code class="language-yaml"># 配置MySQLspring: datasource:  username: root  password: password  url: jdbc:mysql://localhost:3306/schema?userUnicode=true&amp;characterEncoding=utf-8&amp;serverTimezone=UTC  driver-class-name: com.mysql.cj.jdbc.Driver# 配置日志，以便更好查看结果 mybatis-plus:  configuration:   log-impl: org.apache.ibatis.logging.stdout.StdOutImpl</code></pre></li><li><p>添加扫描Mapper文件的注解</p><pre><code class="language-Java">package com.yw;import org.mybatis.spring.annotation.MapperScan;import org.springframework.boot.SpringApplication;import org.springframework.boot.autoconfigure.SpringBootApplication;@SpringBootApplication@MapperScan(&quot;com.yw.Mapper&quot;)public class DemoApplication {    public static void main(String[] args) {        SpringApplication.run(DemoApplication.class, args);    }}</code></pre></li><li><p>测试类</p><pre><code class="language-Java">package com.yw;import com.yw.Mapper.UserMapper;import com.yw.pojo.User;import org.junit.jupiter.api.Test;import org.springframework.beans.factory.annotation.Autowired;import org.springframework.boot.test.context.SpringBootTest;import java.util.List;@SpringBootTestclass DemoApplicationTests {    @Autowired    private UserMapper userMapper;    @Test    public void testSelect() {        List&lt;User&gt; userList = userMapper.selectList(null);        System.out.println(userList);    }    }</code></pre></li></ol><blockquote><p>然后就完成了。这的确比mybatis简单了不少。</p><p>mybatis大概流程是导入依赖，定义实体类，定义mapper接口，创建绑定mapper接口的xml配置文件,配置数据源并绑定mybatis的配置文件和实体类，然后才能使用。现在简单了很多。</p></blockquote><h1>主键生成策略</h1><h2>雪花算法</h2><p>主键生成策略有很多，不过主要学习一下这个雪花算法。</p><p>首先来做个插入的测试。</p><pre><code class="language-Java">package com.yw;import com.yw.Mapper.UserMapper;import com.yw.pojo.User;import org.junit.jupiter.api.Test;import org.springframework.beans.factory.annotation.Autowired;import org.springframework.boot.test.context.SpringBootTest;@SpringBootTestclass DemoApplicationTests {        @Autowired    private UserMapper userMapper;    //用的还是官网给的测试数据库    @Test    public void testInsert() {        User user = new User();        user.setAge(20);        user.setEmail(&quot;1141950370@qq.com&quot;);        user.setName(&quot;yww&quot;);        userMapper.insert(user);    }}</code></pre><p>这个测试是没有设置主键<code>id</code>的值的，来看一下主键的生成。</p><p>插入成功之后看到数据是这样的。<img src="https://img.yww52.com/2020/11/2020-11-14/img1.png" alt=""></p><p>这么一长串的<code>id</code>是什么东西呢，我们在官网看到主键有个注释<code>@TableId</code>，点进去看看源码。</p><pre><code class="language-Java">package com.baomidou.mybatisplus.annotation;import java.lang.annotation.*;/** * 表主键标识 * * @author hubin * @since 2016-01-23 */@Documented@Retention(RetentionPolicy.RUNTIME)@Target({ElementType.FIELD, ElementType.ANNOTATION_TYPE})public @interface TableId {    /**     * 字段名（该值可无）     */    String value() default &quot;&quot;;    /**     * 主键类型     * {@link IdType}     */    IdType type() default IdType.NONE;}</code></pre><p>这里就可以看到主键是可以可无的，下面是默认的主键生成策略，我们在点<code>IdType</code>进去看看有什么策略。</p><pre><code class="language-Java">package com.baomidou.mybatisplus.annotation;import lombok.Getter;/** * 生成ID类型枚举类 * * @author hubin * @since 2015-11-10 */@Getterpublic enum IdType {    /**     * 数据库ID自增     * &lt;p&gt;该类型请确保数据库设置了 ID自增 否则无效&lt;/p&gt;     */    AUTO(0),    /**     * 该类型为未设置主键类型(注解里等于跟随全局,全局里约等于 INPUT)     */    NONE(1),    /**     * 用户输入ID     * &lt;p&gt;该类型可以通过自己注册自动填充插件进行填充&lt;/p&gt;     */    INPUT(2),    /* 以下3种类型、只有当插入对象ID 为空，才自动填充。 */    /**     * 分配ID (主键类型为number或string）,     * 默认实现类 {@link com.baomidou.mybatisplus.core.incrementer.DefaultIdentifierGenerator}(雪花算法)     *     * @since 3.3.0     */    ASSIGN_ID(3),    /**     * 分配UUID (主键类型为 string)     * 默认实现类 {@link com.baomidou.mybatisplus.core.incrementer.DefaultIdentifierGenerator}(UUID.replace(&quot;-&quot;,&quot;&quot;))     */    ASSIGN_UUID(4),    /**     * @deprecated 3.3.0 please use {@link #ASSIGN_ID}     */    @Deprecated    ID_WORKER(3),    /**     * @deprecated 3.3.0 please use {@link #ASSIGN_ID}     */    @Deprecated    ID_WORKER_STR(3),    /**     * @deprecated 3.3.0 please use {@link #ASSIGN_UUID}     */    @Deprecated    UUID(4);    private final int key;    IdType(int key) {        this.key = key;    }}</code></pre><p>在这里就看到当主键为空的时候，会有一个默认的生成策略<code>ASSIGN_ID</code>，这个策略的默认实现算法是<code>雪花算法</code>。</p><p><img src="https://img.yww52.com/2020/11/2020-11-14/img2.png" alt="雪花算法"></p><h2>总结</h2><p>官网已经将上述方法已经总结的很好了,我就直接放官方的表了。</p><table><thead><tr><th align="center">值</th><th align="center">描述</th></tr></thead><tbody><tr><td align="center">AUTO</td><td align="center">数据库ID自增(需要数据库也要设置为自增)</td></tr><tr><td align="center">NONE</td><td align="center">无状态,该类型为未设置主键类型(注解里等于跟随全局,全局里约等于 INPUT)</td></tr><tr><td align="center">INPUT</td><td align="center">insert前自行set主键值</td></tr><tr><td align="center">ASSIGN_ID</td><td align="center">分配ID(主键类型为Number(Long和Integer)或String)(since 3.3.0),使用接口<code>IdentifierGenerator</code>的方法<code>nextId</code>(默认实现类为<code>DefaultIdentifierGenerator</code>雪花算法)</td></tr><tr><td align="center">ASSIGN_UUID</td><td align="center">分配UUID,主键类型为String(since 3.3.0),使用接口<code>IdentifierGenerator</code>的方法<code>nextUUID</code>(默认default方法)</td></tr><tr><td align="center"><del>ID_WORKER</del></td><td align="center">分布式全局唯一ID 长整型类型(please use <code>ASSIGN_ID</code>)</td></tr><tr><td align="center"><del>UUID</del></td><td align="center">32位UUID字符串(please use <code>ASSIGN_UUID</code>)</td></tr><tr><td align="center"><del>ID_WORKER_STR</del></td><td align="center">分布式全局唯一ID 字符串类型(please use <code>ASSIGN_ID</code>)</td></tr></tbody></table><blockquote><p>从官网的删除线和源码来看，后面三种是已经不支持使用了的。</p><p>默认的生成策略是NONE，相当于INPUT，就是自己设置主键的值，当主键没有设值的时候，默认是使用ASSIGN_ID雪花算法。</p></blockquote><h1>代码生成器</h1><p>首先需要引入依赖。</p><pre><code class="language-xml">        &lt;!--   代码生成器    --&gt;        &lt;dependency&gt;            &lt;groupId&gt;com.baomidou&lt;/groupId&gt;            &lt;artifactId&gt;mybatis-plus-generator&lt;/artifactId&gt;            &lt;version&gt;${mybatis-plus-generator.version}&lt;/version&gt;        &lt;/dependency&gt;        &lt;!--   代码生成器的引擎 velocity  --&gt;        &lt;dependency&gt;            &lt;groupId&gt;org.apache.velocity&lt;/groupId&gt;            &lt;artifactId&gt;velocity-engine-core&lt;/artifactId&gt;            &lt;version&gt;${velocity.version}&lt;/version&gt;        &lt;/dependency&gt;</code></pre><p>引擎可以选择其他的，这里就使用<code>velocity</code>。</p><p>这里给出一个比较通用的配置。</p><p>想要更加自定义的配置可以参考代码生成器的配置文档</p><p><a href="https://baomidou.com/pages/981406/">https://baomidou.com/pages/981406/</a></p><pre><code class="language-Java">import com.baomidou.mybatisplus.annotation.FieldFill;import com.baomidou.mybatisplus.annotation.IdType;import com.baomidou.mybatisplus.generator.FastAutoGenerator;import com.baomidou.mybatisplus.generator.config.DataSourceConfig;import com.baomidou.mybatisplus.generator.config.rules.DateType;import com.baomidou.mybatisplus.generator.fill.Column;import org.jetbrains.annotations.Contract;import org.jetbrains.annotations.NotNull;import org.springframework.beans.factory.config.YamlPropertiesFactoryBean;import org.springframework.core.io.ClassPathResource;import java.util.Arrays;import java.util.Collections;import java.util.List;import java.util.Properties;/** * &lt;p&gt; *      MyBatisPlus的代码生成器 * &lt;/p&gt; * * @ClassName CodeGenerator * @Author yww * @Date 2022/10/8 16:26 */public class CodeGenerator {    /**     * 获取配置文件application.yml中关于mysql的配置     * @return  数据库配置     */    @NotNull    @Contract(&quot; -&gt; new&quot;)    private static DataSourceConfig.Builder getDataSourceConfig() {        // 配置文件为application.yml使用        YamlPropertiesFactoryBean factoryBean = new YamlPropertiesFactoryBean();        factoryBean.setResources(new ClassPathResource(&quot;application.yml&quot;));        Properties properties = factoryBean.getObject();        if (properties == null) {            throw new RuntimeException(&quot;请确认配置文件是否名为application.yml，是否在resources资源目录下！&quot;);        }        String url = (String) properties.get(&quot;spring.datasource.url&quot;);        String username = (String) properties.get(&quot;spring.datasource.username&quot;);        String password = (String) properties.get(&quot;spring.datasource.password&quot;);        if (url == null || url.isEmpty()) {            throw new RuntimeException(&quot;mysql的url出错！&quot;);        }        if (username == null || username.isEmpty()) {            throw new RuntimeException(&quot;mysql的username出错！&quot;);        }        if (password == null || password.isEmpty()) {            throw new RuntimeException(&quot;mysql的password出错！&quot;);        }        return new DataSourceConfig.Builder(url, username, password);    }    /**     * 执行 run     */    public static void main(String[] args)  {        // 项目路径 + /src/main        String path = System.getProperty(&quot;user.dir&quot;).replaceAll(&quot;\\\\&quot;,&quot;/&quot;) + &quot;/src/main&quot;;        FastAutoGenerator.create(getDataSourceConfig())            // 全局配置            .globalConfig(builder -&gt; builder                    // 作者名称                    .author(&quot;yww&quot;)                    // 指定输出目录                    .outputDir(path + &quot;/java&quot;)                    // 禁止打开输出目录                    .disableOpenDir()                    // 时间策略                    .dateType(DateType.TIME_PACK)                    // 注释日期                    .commentDate(&quot;yyyy-MM-dd&quot;)                    // 开启springdoc的注解                    .enableSpringdoc()                    .build()            )            // 包配置            .packageConfig(builder -&gt; builder                    // 父包名                    .parent(&quot;com.yww.management&quot;)                    .build()            )            // 模板配置            .templateConfig(builder -&gt; builder                    // 禁用所有模板                    //.disable()                    .controller(path + &quot;/resources/templates/controller.java.vm&quot;)                    .build())            // 策略配置            .strategyConfig((scanner, builder) -&gt; builder                    // 增加表匹配                    .addInclude(getTables(scanner.apply(&quot;请输入表名，如需输入多个表名，可用英文逗号分隔，选择所有表输入all&quot;)))                    // controller策略                    .controllerBuilder().enableRestStyle().enableHyphenStyle()                    // entity策略配置                    .entityBuilder().enableLombok()                    .idType(IdType.ASSIGN_ID)                    .enableTableFieldAnnotation()                    .addTableFills(new Column(&quot;create_time&quot;, FieldFill.INSERT))                    .addTableFills(new Column(&quot;update_time&quot;, FieldFill.INSERT_UPDATE))                    .build()            )            .execute();    }    /**     * 处理输入all的情况     */    protected static List&lt;String&gt; getTables(String tables) {        return &quot;all&quot;.equals(tables) ? Collections.emptyList() : Arrays.asList(tables.split(&quot;,&quot;));    }}</code></pre>]]></content:encoded>
            <category domain="https://yww52.com/categories/%25E6%2588%2591%25E7%259A%2584%25E6%258A%2580%25E6%259C%25AF%25E5%25AD%25A6%25E4%25B9%25A0%25E7%2589%25A9%25E8%25AF%25AD%25E6%259E%259C%25E7%2584%25B6%25E6%259C%2589%25E9%2597%25AE%25E9%25A2%2598/">我的技术学习物语果然有问题</category>
        </item>
        <item>
            <title>Linux</title>
            <link>https://yww52.com/posts/53d0684b/</link>
            <guid isPermaLink="true">https://yww52.com/posts/53d0684b/</guid>
            <pubDate>Wed, 11 Nov 2020 00:00:00 +0000</pubDate>
            <description>学习尚硅谷的教程时候的学习笔记。</description>
            <content:encoded><![CDATA[<img src="https://img.yww52.com/2020/11/2020-11-11/top_img.jpg" alt="Linux" style="max-width:100%;height:auto;margin-bottom:15px;" /><h1>虚拟机网络</h1><table><thead><tr><th align="center">连接方式</th><th align="center">连接的网卡</th><th align="center">能否连接局域网</th><th align="center">能否连接公网</th></tr></thead><tbody><tr><td align="center">桥接</td><td align="center">VMnet0(本地真实网卡)</td><td align="center">可以</td><td align="center">可以</td></tr><tr><td align="center">NAT</td><td align="center">VMnet8</td><td align="center">不可以</td><td align="center">可以</td></tr><tr><td align="center">仅主机</td><td align="center">VMnet1</td><td align="center">不可以</td><td align="center">不可以</td></tr></tbody></table><ol><li>桥接连接的网卡是随机连接的，要是连接到了没网络的网卡还需要自己设置。</li><li>桥接模式下虚拟机相当一台独立的计算机，拥有一个独立的IP地址，与本机在一个局域网内。</li><li>NAT模式可以帮助虚拟机通过本机所在的网络来访问公网。</li><li>仅主机模式会建立一个全封闭的虚拟网络，只有主机能访问</li></ol><h1>目录结构</h1><p><img src="https://img.yww52.com/2020/11/2020-11-11/img1.jpg" alt=""></p><h1>常用命令</h1><h2>目录命令</h2><h3>ls</h3><pre><code class="language-shell"># 查看文件夹的信息ls # 查看详细信息ls -l# 查看隐藏目录ls -a # 显示文件大小ls -lh</code></pre><h3>cd</h3><pre><code class="language-shell"># 进入目录cd [路径]# 进入家目录cd ~# 进入上一次所在的目录cd -# 进入上一级目录cd ..</code></pre><h3>mkdir和rmdir</h3><pre><code class="language-shell"># 创建目录mkdir [目录名]# 删除目录，只能删除空目录rmdir [目录名]# 递归建立目录mkdir -p [目录路径]# 递归删除目录,只能删除空目录rmdir -p [目录路径]</code></pre><h2>文件操作</h2><h3>touch</h3><pre><code class="language-shell"># touch创建文件touch [文件名]# 若已有该文件则会修改文件时间</code></pre><h3>stat</h3><pre><code class="language-shell"># 查看文件详细信息stat</code></pre><h3>cat</h3><pre><code class="language-shell"># 查看文件内容cat [文件名]# 显示行号cat -n [文件名]# 显示所有隐藏符号cat -A [文件名]</code></pre><h3>more和less</h3><pre><code class="language-shell"># 分屏显示文件内容more [文件名]# 空格键翻页，回车向下一行，b向上翻页，/字符串就算搜索字符串，q退出# 分行显示文件内容less [文件名]</code></pre><h3>head和tail</h3><pre><code class="language-shell"># 显示文件开头n行head -[n] [文件名]# 显示文件末尾n行tail -[n] [文件名]# 监听文件新增内容tail -f [文件名]</code></pre><h2>文件和目录的命令</h2><h3>rm</h3><pre><code class="language-shell"># 删除文件或者目录rm [文件名]# 强制删除rm -f# 递归删除rm - r</code></pre><h3>cp</h3><pre><code class="language-shell"># 复制文件,不自定义名字就会默认原名cp [文件名] [复制后]# 递归复制，多用于目录cp -r </code></pre><h3>mv</h3><pre><code class="language-shell"># 移动或重命名mv [文件名] [移动后]</code></pre><h2>权限</h2><h3>基本权限</h3><p>通过命<code>ls -l</code>就能看到一些文件的基本权限，比如下图。</p><p><img src="https://img.yww52.com/2020/11/2020-11-11/img2.png" alt=""></p><ol><li>第一位表示的是文件的类型。<ul><li><code>-</code>代表的是普通文件</li><li><code>d</code>代表是目录</li><li><code>b</code>代表块设备文件</li><li><code>c</code>代表字符设备，比如键盘鼠标等等</li><li><code>s</code>代表套接字文件</li><li><code>l</code>位链接文件</li><li><code>p</code>位管道文件</li></ul></li><li>第二到第四位代表文件所有者的权限(user)</li><li>第五到第七位代表组群所有者的权限(group)</li><li>第八到第十位代表其他人的权限(other)</li><li><ul><li><code>-</code>代表没用该权限</li><li><code>r</code>代表可读</li><li><code>w</code>代表可写</li><li><code>x</code>代表可执行</li></ul></li><li>后面两个root代表的是文件所有者和群组</li></ol><h3>修改权限</h3><h4>chmod</h4><pre><code class="language-shell"># 增加文件所有者的一个文件的可写权限chmod u+w [文件名]# 删除权限chmod u-w [文件名]# 修改所有者，组和其他用户权限chmod u+w,g+w,o+w [文件名]# 设置权限chmod u=rwx,g=rwx,o=rwx [文件名]</code></pre><h4>数字权限</h4><pre><code class="language-shell"># 4代表可读，2代表可写，1代表可执行# 修改文件权限,4+2+1=7,4+1=5chmod 755 [文件名]# 经常使用到的数字权限chmod 644chmod 755chmod 777</code></pre><h4>chown</h4><pre><code class="language-shell">## 修改文件的所有者chown [用户] [文件名]## 修改文件的所有者和所有组chown [用户]:[组] [文件名]</code></pre><h4>chgrp</h4><pre><code class="language-shell">## 修改文件的所属组chown [组] [文件名]</code></pre><h3>默认权限</h3><p>每次新建的文件都会有权限，这就是默认权限，umask。对文件来讲默认权限最大为666，对目录来讲是777。</p><pre><code class="language-shell">## 查看当前umask的值umask## 查看当前默认权限umask -S</code></pre><h2>帮助命令</h2><h3>man</h3><pre><code class="language-shell">man ls## 查看命令有几个级别man -f what is </code></pre><h3>info</h3><pre><code class="language-shell">info ls </code></pre><h3>help</h3><pre><code class="language-shell">## help只能获取shell内置命令的帮助,不常用cd --help## 可以用type来判断是不是内置命令type cd</code></pre><h2>搜索命令</h2><h3>whereis</h3><pre><code class="language-shell">## 用来搜索系统命令，源文件和帮助文档位置whereis ls</code></pre><h3>which</h3><pre><code class="language-shell">## 查找命令，若该命令有别名也会查找别名which ls</code></pre><h3>locate</h3><pre><code class="language-shell">## 通过文件名搜索,因为是通过数据库查找，所以新增的需要更新数据库locate [文件名]</code></pre><h3>find</h3><h4>按照文件名搜索</h4><pre><code class="language-shell">## 通过文件名搜索find [搜索的路径] -name [文件名]## 不区分大小写的文件名find [搜索的路径] -iname [文件名]## 通过inode查找find [搜索的路径] -inum [inode]</code></pre><h4>按照文件大小搜索</h4><pre><code class="language-shell">## 按照文件大小搜索find [搜索的路径] -size [大小]</code></pre><h4>按照权限搜索</h4><pre><code class="language-shell">find [搜索的路径] -perm [数字权限]## 三个权限只要有一个比数字权限的大或者等于find [搜索的路径] -perm +[数字权限]## 三个权限每一个都要比数字权限的大或者等于find [搜索的路径] -perm -[数字权限]</code></pre><h4>grep</h4><pre><code class="language-shell">## 查找文件中符合条件的字符串grep &quot;ww&quot; [文件名]## 输出行号grep -n## 不区分大小写grep -i## 查找不符合条件的grep -v## 符合的部分高亮grep --color=auto</code></pre><h2>压缩与解压缩</h2><h3>zip格式</h3><pre><code class="language-shell">## 压缩zip [压缩后文件名] [原文件]zip test.zip test.txt## 解压缩unzip [压缩文件]</code></pre><h3>gz格式</h3><pre><code class="language-shell">## 压缩文件，能压缩目录但不打包gzip [文件名]# 不保留原文件gzip -r [目录]# 会将目录内文件一个个压缩，不压缩目录## 解压缩gzip -d [压缩文件]gunzip [压缩文件]gzip -dr [目录]</code></pre><h3>bz2格式</h3><pre><code class="language-shell">## 压缩，不能压缩目录bzip2 [文件名]# 不保留原文件bzip2 -k [文件名]# 保留原文件## 解压缩bzip2 -d [压缩文件]bunzip2 [压缩文件]</code></pre><h3>tar格式</h3><pre><code class="language-shell">## 打包,c表示打包，f表示指定文件名，v显示打包过程tar -cvf [文件名.tar] [原文件或目录]## 解压,x表示解打包tar -xvf [tar包]## 压缩包,生成.tar.gz的压缩包gzip [tar包]## 解压缩包gzip -d [tar.gz压缩文件]</code></pre><h3>tar.gz和tar.bz2</h3><pre><code class="language-shell">## 压缩,z表示tar.gz格式，j表示tar.bz2tar -zcvf [文件名.tar.gz] [原文件或目录]tar -jcvf [文件名.tar.bz2] [原文件或目录]## 解压缩tar -zxvf [文件名.tar.gz] tar -jxvf [文件名.tar.bz2] ## 解压缩到指定位置tar -zxvf [文件名.tar.gz] -C [路径]tar -jxvf [文件名.tar.bz2] -C [路径]## 只查看不解压，用t参数tar -ztvf [文件名.tar.gz] [原文件或目录]tar -jtvf [文件名.tar.gz] [原文件或目录]</code></pre><h2>关机和重启</h2><h3>sync</h3><pre><code class="language-shell"># 数据同步，刷新文件系统的缓存区sync</code></pre><h3>shutdown</h3><pre><code class="language-shell"># 现在关机shutdown -h now# 在10点关机shutdown -h 10:00# 现在重启shutdown -r now# 取消关机或重启shutdown -c</code></pre><h3>reboot</h3><pre><code class="language-shell"># 现在重启reboot</code></pre><h2>网络命令</h2><h3>ifconfig</h3><pre><code class="language-shell"># 查询网卡的信息ifconfig</code></pre><h3>ping</h3><pre><code class="language-shell"># ping向一个主机发送ICMP请求ping [IP]# 指定ping的次数ping -c 10 [IP]# 广播ping -b [IP]</code></pre><h3>netstat</h3><pre><code class="language-shell"># 查询本机开启的所有端口netstat -tuln# 查看本机开启的所有端口是哪个服务netstat -tulnp# 查看网关netstat -rn</code></pre><h1>VIM</h1><h2>进入输入模式</h2><table><thead><tr><th align="center">插入命令</th><th align="center">描述</th></tr></thead><tbody><tr><td align="center">i</td><td align="center">在光标所在字符后插入</td></tr><tr><td align="center">I</td><td align="center">在光标所在字符行尾插入</td></tr><tr><td align="center">a</td><td align="center">在光标所在字符前插入</td></tr><tr><td align="center">A</td><td align="center">在光标所在行行首插入</td></tr><tr><td align="center">o</td><td align="center">在光标下插入新行</td></tr><tr><td align="center">O</td><td align="center">在光标上插入新行</td></tr></tbody></table><h2>编辑模式</h2><pre><code class="language-shell"># 在命令模式用:进入编辑模式# 保存:w# 退出:q# 强制不保存退出:q!# 强制保存退出:wq!</code></pre><h2>命令模式</h2><h3>移动光标</h3><ol><li>上下左右键</li><li>HJKL键，h向左，j向下，k向上，l向右</li><li><code>gg</code>移动光标到文件头，<code>G</code>移动到文件尾</li><li><code>^</code>移动到行首，<code>&amp;</code>到行尾</li><li>进入编辑模式加数字到第几行，比如<code>:3</code>是到第三行</li></ol><h3>删除</h3><ol><li><code>x</code>删除一个字母，<code>nx</code>删除n个字母</li><li><code>dd</code>删除一行，<code>ndd</code>删除n行</li><li>进入编辑模式，<code>:n1,n2d</code>删除n1到n2行</li><li>其实删除是放到了剪切板，按<code>p</code>数粘贴到下一行，<code>P</code>是粘贴到上一行</li></ol><h3>复制</h3><ol><li><code>yy</code>复制单行，<code>nyy</code>复制n行</li><li>按<code>p</code>数粘贴到下一行，<code>P</code>是粘贴到上一行</li></ol><h3>撤销</h3><ol><li><code>u</code>撤销上一次操作</li><li><code>ctrl+r</code>是反撤销</li></ol><h3>替换</h3><ol><li><code>r</code>替换光标的字符</li><li><code>R</code>进入替换模式，esc退出</li></ol><h3>vim配置文件</h3><table><thead><tr><th align="center">参数</th><th align="center">描述</th></tr></thead><tbody><tr><td align="center">:set nu</td><td align="center">显示行号</td></tr><tr><td align="center">:set nonu</td><td align="center">取消行号</td></tr><tr><td align="center">:syntax on</td><td align="center">依据语法显示相关颜色帮助</td></tr><tr><td align="center">:syntax off</td><td align="center">取消颜色帮助</td></tr><tr><td align="center">:set hlsearch</td><td align="center">设置将查询的字符串高亮，默认</td></tr><tr><td align="center">:set nohlsearch</td><td align="center">取消设置将查询的字符串高亮</td></tr><tr><td align="center">:set ruler</td><td align="center">显示右下角状态栏，默认</td></tr><tr><td align="center">:set noruler</td><td align="center">取消显示右下角状态栏</td></tr><tr><td align="center">:set showmode</td><td align="center">显示状态显示，默认</td></tr><tr><td align="center">:set noshowmode</td><td align="center">不显示状态显示</td></tr><tr><td align="center">:set list</td><td align="center">显示隐藏字符</td></tr><tr><td align="center">:set nolist</td><td align="center">不显示隐藏字符，默认</td></tr></tbody></table><blockquote><p>在编辑器中输入只是临时的，需要改就需要到家目录下的<code>.vimrc</code>中添加配置，比如添加<code>set nu</code>该用户的编辑器就会显示行号</p></blockquote>]]></content:encoded>
            <category domain="https://yww52.com/categories/%25E6%2588%2591%25E7%259A%2584%25E6%258A%2580%25E6%259C%25AF%25E5%25AD%25A6%25E4%25B9%25A0%25E7%2589%25A9%25E8%25AF%25AD%25E6%259E%259C%25E7%2584%25B6%25E6%259C%2589%25E9%2597%25AE%25E9%25A2%2598/">我的技术学习物语果然有问题</category>
        </item>
        <item>
            <title>记一次容器编排</title>
            <link>https://yww52.com/posts/683134ab/</link>
            <guid isPermaLink="true">https://yww52.com/posts/683134ab/</guid>
            <pubDate>Tue, 10 Nov 2020 00:00:00 +0000</pubDate>
            <description>记一次容器编排。</description>
            <content:encoded><![CDATA[<img src="https://img.yww52.com/2020/11/2020-11-10top_img.jpg" alt="记一次容器编排" style="max-width:100%;height:auto;margin-bottom:15px;" /><h1>前言</h1><p>老师丢了一个spring写的项目给我们部署到服务器上。这次来记录一下部署的过程。</p><h1>分析项目</h1><p>这是一个spring的项目，没用到springboot，只能用tomcat部署了。因为还用到了MySQL连接，所以还需要一个MySQL的容器。知道了需要的容器那就开始行动了。</p><h1>构建好大概框架</h1><ol><li><p>创建一个项目的文件夹</p><pre><code class="language-shell">    mkdir math    cd math</code></pre></li><li><p>创建一个放Tomcat配置的文件夹</p><pre><code class="language-shell">    mkdir webapp</code></pre></li><li><p>创建一个放MySQL配置的文件夹</p><pre><code class="language-shell">    mkdir database</code></pre></li><li><p>创建一个<code>docker-compose.yml</code>的配置文件</p><pre><code class="language-shell">    touch docker-compose.yml</code></pre></li></ol><h1>Tomcat容器</h1><p>因为这个项目打包出来是war包的，所以我想的就是直接将war包放到Tomcat的webapps里然后运行Tomcat就好了。（以下操作是在webapp文件夹进行）</p><p>:::note{type=&quot;warning&quot;}</p><p>注意设置项目中连接mysql的url，ip填写为mysql（其他也行，后面自己改服务名）   localhost是指Tomcat容器的IP，因为容器与容器是隔离的，所以会连接不到MySQL容器的IP。</p><p>:::</p><ol><li><p>将改好的项目war包移入服务器的这个文件夹中。</p></li><li><p>定制Tomcat的镜像，创建一个<code>Dockerfile</code>文件。</p><pre><code class="language-dockerfile">FROM tomcat:9.0LABEL maintainer = &quot;yww  &lt;1141950370@qq.com&gt;&quot;# 暴露端口EXPOSE 9010# 将war包放入Tomcat容器的webapps文件夹中ADD math.war /usr/local/tomcat/webapps/</code></pre></li></ol><h1>MySQL容器</h1><p>这个容器才是这次的重点，因为要导入自己的sql语句，所以需要额外的shell脚本来实现。（以下操作是在database文件夹进行）</p><ol><li><p>将sql文件移入这个文件夹，我的文件名是<code>mathStock.sql</code>。</p></li><li><p>定制MySQL的镜像，创建一个<code>Dockerfile</code>文件。</p><pre><code class="language-dockerfile">FROM mysql:8.0LABEL maintainer = &quot;yww  &lt;1141950370@qq.com&gt;&quot;# 设置mysql的工作位置ENV WORK_PATH /usr/local/#定义会被容器自动执行的目录和所需文件的环境变量ENV AUTO_RUN_DIR /docker-entrypoint-initdb.dENV SQLFILE mathStock.sqlENV INSTALL_DATA_SHELL setup.sh# 将需要的文件放入容器内COPY ./$SQLFILE $WORK_PATH/COPY ./$INSTALL_DATA_SHELL $AUTO_RUN_DIR/# 给shell脚本和自动执行目录增加可执行的权限RUN chmod a+x $AUTO_RUN_DIR/$INSTALL_DATA_SHELL#设置容器启动时执行的命令CMD [&quot;sh&quot;, &quot;/docker-entrypoint-initdb.d/import.sh&quot;]</code></pre></li><li><p>创建一个shell脚本用于导入sql文件，我的文件名是<code>setup.sh</code></p><pre><code class="language-shell">#!/bin/bashmysql -uroot -ppassword &lt;&lt;EOFsource /usr/local/mathStock.sql;</code></pre></li></ol><h1>容器编排docker-compose.yml</h1><pre><code class="language-yml"># 版本需要选对，不然报错version: &#39;3&#39;services:# 编排Tomcat容器      webapp:           container_name: webapp           image: webapp           build:               context: ./webapp               dockerfile: Dockerfile           ports:                   - &quot;9010:8080&quot;           depends_on:                   - mysql# 编排MySQL容器      mysql:           container_name: mysql           image: database           build:                context: ./database                 dockerfile: Dockerfile           ports:                   - &quot;3306:3306&quot;            # 挂载数据卷           volumes:                   - &quot;./mysql:/var/lib/mysql&quot;                   - &quot;./my.cnf:/etc/mysql/conf.d/my.cnf&quot;           #设置编码格式           command: [                  &#39;--character-set-server=utf8mb4&#39;,                   &#39;--collation-server=utf8mb4_unicode_ci&#39;              ]           #设置初始化密码           environment:                MYSQL_ROOT_PASSWORD: &quot;password&quot;</code></pre><h1>运行</h1><p>编排完之后就可以运行了。</p><pre><code class="language-shell">docker-compose up -d </code></pre><h1>参考文章</h1><ul><li><a href="https://blog.csdn.net/u011913691/article/details/90665639">https://blog.csdn.net/u011913691/article/details/90665639</a></li></ul>]]></content:encoded>
            <category domain="https://yww52.com/categories/%25E6%2588%2591%25E7%259A%2584%25E6%258A%2580%25E6%259C%25AF%25E5%25AD%25A6%25E4%25B9%25A0%25E7%2589%25A9%25E8%25AF%25AD%25E6%259E%259C%25E7%2584%25B6%25E6%259C%2589%25E9%2597%25AE%25E9%25A2%2598/">我的技术学习物语果然有问题</category>
        </item>
        <item>
            <title>Swagger3</title>
            <link>https://yww52.com/posts/35ca0e58/</link>
            <guid isPermaLink="true">https://yww52.com/posts/35ca0e58/</guid>
            <pubDate>Tue, 03 Nov 2020 00:00:00 +0000</pubDate>
            <description>Swagger3</description>
            <content:encoded><![CDATA[<img src="https://img.yww52.com/2020/11/2020-11-3top_img.jpg" alt="Swagger3" style="max-width:100%;height:auto;margin-bottom:15px;" /><h1>前言</h1><p>学完之前的<a href="https://yww52.com/2020/10/25/Swagger/">swagger2</a>之后,发现还有一个新的版本，swagger3，用起来更加的简单，而且是官方的starter。</p><h1>使用</h1><ol><li><p>首先导入依赖</p><pre><code class="language-xml">&lt;dependency&gt;    &lt;groupId&gt;io.springfox&lt;/groupId&gt;    &lt;artifactId&gt;springfox-boot-starter&lt;/artifactId&gt;    &lt;version&gt;3.0.0&lt;/version&gt;&lt;/dependency&gt;</code></pre></li><li><p>创建swagger配置类。</p><pre><code class="language-Java">package com.yww.config;import org.springframework.context.annotation.Configuration;@Configurationpublic class swaggerConfig {}</code></pre></li><li><p>然后就能访问url<a href="http://localhost:8088/swagger-ui/">http://localhost:8088/swagger-ui/</a>找到文档了。</p></li></ol><p>有很多的配置与swagger2是一样的，就不具体阐述了。</p><h1>增强</h1><p>Swagger3还可以使用<code>knife4j</code>来增加Swagger的体验，这个前身是<code>swagger-bootstrap-ui</code>。</p><p>官方的文档。<a href="https://doc.xiaominfo.com/knife4j/documentation/">https://doc.xiaominfo.com/knife4j/documentation/</a></p><p>可以根据底层的不同来选择依赖，比如使用<code>OpenAPI3</code>结构的可使用以下版本。</p><pre><code class="language-xml">&lt;dependency&gt;    &lt;groupId&gt;com.github.xiaoymin&lt;/groupId&gt;    &lt;artifactId&gt;knife4j-spring-boot-starter&lt;/artifactId&gt;    &lt;!--在引用时请在maven中央仓库搜索3.X最新版本号--&gt;    &lt;version&gt;3.0.2&lt;/version&gt;&lt;/dependency&gt;</code></pre><p>使用后也是访问<code>doc.html</code>就能看到。</p><p><code>knife4j</code>还有增强的特性，可以参考<a href="https://doc.xiaominfo.com/knife4j/documentation/enhance.html">https://doc.xiaominfo.com/knife4j/documentation/enhance.html</a>来开启增强模式。</p>]]></content:encoded>
            <category domain="https://yww52.com/categories/%25E6%2588%2591%25E7%259A%2584%25E6%258A%2580%25E6%259C%25AF%25E5%25AD%25A6%25E4%25B9%25A0%25E7%2589%25A9%25E8%25AF%25AD%25E6%259E%259C%25E7%2584%25B6%25E6%259C%2589%25E9%2597%25AE%25E9%25A2%2598/">我的技术学习物语果然有问题</category>
        </item>
        <item>
            <title>MySQL</title>
            <link>https://yww52.com/posts/c24675b4/</link>
            <guid isPermaLink="true">https://yww52.com/posts/c24675b4/</guid>
            <pubDate>Sun, 01 Nov 2020 00:00:00 +0000</pubDate>
            <description>MySQL</description>
            <content:encoded><![CDATA[<img src="https://img.yww52.com/2020/11/2020-11-1top_img.jpg" alt="MySQL" style="max-width:100%;height:auto;margin-bottom:15px;" /><h1>基本概念</h1><h1>连接数据库</h1><pre><code class="language-mysql">-- username 则为自己的用户名，一般为root，password为密码mysql -u[username] -p[password]mysql -u[username] -p-- 断开连接，即退出数据库操作页面exit</code></pre><p>:::note{type=&quot;info&quot;}</p><p>MySQL的关键字是不区分大小写的，这里我全部用大写来记录。</p><p>:::</p><h1>DDL数据定义语言</h1><h2>数据库操作</h2><h3>创建数据库</h3><pre><code class="language-mysql">CREATE DATABASE [数据库名];CREATE DATABASE IF NOT EXISTS [数据库名];</code></pre><h3>删除数据库</h3><pre><code class="language-mysql">DROP DATABASE [数据库名];DROP DATABASE IF EXISTS [数据库名];</code></pre><h2>查看数据库</h2><pre><code class="language-mysql">-- 查看所有的数据库SHOW DATABASES;-- 查看创建库使用的语句SHOW CREATE DATABASES [库名];</code></pre><h3>切换数据库</h3><pre><code class="language-mysql">-- 切换到指定的数据库USE [数据库名];--  数据库名和表名如果是特殊的词，就需要用``来区分</code></pre><h2>表中的数据类型</h2><h3>数值类型</h3><table><thead><tr><th align="center">类型</th><th align="center">描述</th><th align="center">大小</th></tr></thead><tbody><tr><td align="center">tinyint</td><td align="center">很小的一个数据</td><td align="center">1个字节</td></tr><tr><td align="center">smallint</td><td align="center">较小的一个数据</td><td align="center">2个字节</td></tr><tr><td align="center">mediumint</td><td align="center">中等大小的一个数据</td><td align="center">3个字节</td></tr><tr><td align="center">int</td><td align="center">标准大小的数据(整数常用)</td><td align="center">4个字节</td></tr><tr><td align="center">bigint</td><td align="center">较大的数据</td><td align="center">8个字节</td></tr><tr><td align="center">float</td><td align="center">单精度浮点数</td><td align="center">4个字节</td></tr><tr><td align="center">double</td><td align="center">双精度浮点数</td><td align="center">8个字节</td></tr><tr><td align="center">decimal</td><td align="center">字符串形式的浮点数(用来解决浮点数精度问题)</td><td align="center"></td></tr></tbody></table><h3>字符串类型</h3><table><thead><tr><th align="center">类型</th><th align="center">描述</th><th align="center">大小</th></tr></thead><tbody><tr><td align="center">char</td><td align="center">固定大小的字符串</td><td align="center">0~255</td></tr><tr><td align="center">varchar</td><td align="center">可变的字符串，对应Java的String</td><td align="center">0~65535</td></tr><tr><td align="center">tinytext</td><td align="center">微型文本</td><td align="center">2^8-1</td></tr><tr><td align="center">text</td><td align="center">文本串</td><td align="center">2^16-1</td></tr></tbody></table><h3>时间与日期</h3><table><thead><tr><th align="center">类型</th><th align="center">描述</th><th align="center">格式</th></tr></thead><tbody><tr><td align="center">date</td><td align="center">日期</td><td align="center">YYYY-MM-DD</td></tr><tr><td align="center">time</td><td align="center">时间</td><td align="center">HH:mm:ss</td></tr><tr><td align="center">datetime</td><td align="center">常用的时间格式，即date+time</td><td align="center">YYYY-MM-DD HH:mm:ss</td></tr><tr><td align="center">timestamp</td><td align="center">1970.1.1到现在时间的毫秒数</td><td align="center"></td></tr><tr><td align="center">year</td><td align="center">年份表示</td><td align="center"></td></tr></tbody></table><p>:::note{type=&quot;info&quot;}</p><p>MM与mm分大小写是为了区分月份和分钟。  HH大写是为了表示是24小时制，hh为12小时制。  YYYY表示week-based-year,即只要本周跨年，那这周就算入下一年。  yyyy就表示真实年份。</p><p>:::</p><h3>null</h3><p>未知的数据类型，不建议参与运算。</p><h2>字段属性</h2><table><thead><tr><th align="center">关键字</th><th align="center">描述</th></tr></thead><tbody><tr><td align="center">NULL</td><td align="center">该数据列可以包含NULL值</td></tr><tr><td align="center">NOT NULL</td><td align="center">该数据列不允许为NULL值</td></tr><tr><td align="center">DEFAULT</td><td align="center">为数据列指定默认值</td></tr><tr><td align="center">PRIMARY KEY</td><td align="center">设数据列为主键</td></tr><tr><td align="center">AUTO_INCREMENT</td><td align="center">设数据列为自动递增，设用于整数</td></tr><tr><td align="center">UNSIGNED</td><td align="center">设数据列为无符号</td></tr><tr><td align="center">CHARACTER SET [name]</td><td align="center">为数据列指定一个字符集</td></tr><tr><td align="center">COMMENT</td><td align="center">注释</td></tr></tbody></table><h2>表的操作</h2><h3>查看表</h3><pre><code class="language-mySQL">-- 进入数据库后查看该数据库所有的表SHOW TABLES [表名];-- 查看创建表使用的语句SHOW CREATE TABLE [表名];</code></pre><h3>查看表中的信息</h3><pre><code class="language-mysql">-- 查看表中的具体信息，即结构DESCRIBE [表名];DESC [表名];</code></pre><h3>创建表</h3><pre><code class="language-mysql">CREATE TABLE IF NOT EXISTS `[表名]`(属性)[表类型][设置字符集]-- 例子CREATE TABLE IF NOT EXISTS `user`(`id` INT(5) NOT NULL AUTO_INCREMENT COMMENT &#39;id号&#39;,        `username` VARCHAR(20) NOT NULL COMMENT &#39;用户名&#39;,        `password` VARCHAR(20) NOT NULL COMMENT &#39;密码&#39;,        `sex` VARCHAR(2) COMMENT &#39;性别&#39;,        `birthday` DATETIME DEFAULT NULL COMMENT &#39;生日&#39;,        `address` VARCHAR(100) DEFAULT NULL COMMENT &#39;地址&#39;,        `email` VARCHAR(50) DEFAULT NULL COMMENT &#39;邮箱&#39;,        PRIMARY KEY(`id`)    )ENGINE=INNODB DEFAULT CHARSET=utf8</code></pre><h3>修改表</h3><pre><code class="language-mysql">-- 修改表名ALTER TABLE [旧表名] RENAME AS [新表名];-- 增加表的字段ALTER TABLE [表名] ADD [属性];ALTER TABLE user1 ADD age INT(10) DEFAULT NULL COMMENT &#39;年龄&#39;;-- 修改表的约束-- CHANGE可以更改列名和列类型，而MODIFY只能修改列类型-- CHANGE只修改列类型的时候，也要写两个相同的列名-- 所以只修改列类型建议用MODIFYALTER TABLE [表名] CHANGE [旧列名] [新列名] [类型];ALTER TABLE [表名] MODIFY [列名] [类型];-- 删除表的字段ALTER TABLE [表名] DROP [列名];</code></pre><h3>删除表</h3><pre><code class="language-mysql">DROP TABLE [表名];DROP TABLE IF EXISTS [表名];</code></pre><h3>添加外键</h3><pre><code class="language-mysql">ALTER TABLE [当前表名] ADD CONSTRAINT `FK_[列名]` FOREIGN KEY([列名]) REFERENCES [对象表名]([列名]);</code></pre><h1>DML数据操纵语言</h1><h2>INSERT</h2><pre><code class="language-mysql">-- 插入一条数据INSERT INTO [表名]([列名1]，[列名2]，[列名3]) VALUES([值1]，[值2]，[值3]);INSERT INTO [表名] VALUES([值1]，[值2]，[值3]);-- 插入多条语句INSERT INTO [表名]([列名1]，[列名2]，[列名3]) VALUES([值1]，[值2]，[值3]),([值1]，[值2]，[值3]);INSERT INTO [表名] VALUES([值1]，[值2]，[值3]),([值1]，[值2]，[值3]);</code></pre><h2>UPDATE</h2><pre><code class="language-mysql">-- 条件一般使用WHEREUPDATE [表名] SET [列名]=[值] [条件];UPDATE [表名] SET [列名1]=[值1],[列名2]=[值2] [条件];</code></pre><h2>DELETE</h2><pre><code class="language-mysql">-- 删除数据DELETE FORM [表名] [条件];-- 清空表中数据TRUNCATE [表名];-- TRUNCATE会让自增计数器清零</code></pre><h1>DQL数据查询语言</h1><pre><code class="language-mysql">CREATE TABLE IF NOT EXISTS `user`(`id` INT(5) NOT NULL AUTO_INCREMENT COMMENT &#39;id号&#39;,    `username` VARCHAR(20) NOT NULL COMMENT &#39;名字&#39;,    `password` VARCHAR(20) NOT NULL COMMENT &#39;密码&#39;,    `sex` VARCHAR(2) COMMENT &#39;性别&#39;,    `birthday` DATETIME DEFAULT NULL COMMENT &#39;生日&#39;,    `address` VARCHAR(100) DEFAULT NULL COMMENT &#39;地址&#39;,    `email` VARCHAR(50) DEFAULT NULL COMMENT &#39;邮箱&#39;,    PRIMARY KEY(`id`))ENGINE=INNODB DEFAULT CHARSET=utf8</code></pre><blockquote><p>建个表好进行之后的例子</p></blockquote><h2>简单的查询</h2><pre><code class="language-mysql">-- 查询表中所有信息SELECT * FROM [表名];SELECT * FROM `user`;-- 查询表中指定信息SELECT [列名1],[列名2] FROM [表名];SELECT `id`,`name` FROM `user`;-- 给结果的字段及表器别名,AS可以省略SELECT [列名1] AS [别名1],[列名2] AS [别名2] FROM [表名] AS [别名3];SELECT `username` AS &#39;用户名&#39;,`password` AS &#39;密码&#39; FROM `user` AS &#39;用户表&#39;;-- 处理结果的信息，简单的字符串拼接SELECT CONCAT([message],[列名]) FROM [表名];SELECT CONCAT(&#39;用户名为&#39;,`username`) FROM `user`;</code></pre><h2>去重</h2><pre><code class="language-mysql">-- 去除同样的数据SELECT DISTINCT `username` FROM `user`;</code></pre><h2>一些常用连接符</h2><table><thead><tr><th align="center">AND</th><th align="center">&amp;&amp;</th><th align="center">A AND B</th></tr></thead><tbody><tr><td align="center">OR</td><td align="center">||</td><td align="center">A OR B</td></tr><tr><td align="center">NOT</td><td align="center">!</td><td align="center">NOT A</td></tr></tbody></table><h2>LIKE</h2><pre><code class="language-mysql">-- %代表任意字符,_代表一个字符SELECT [列名] FROM [表名] LIKE &#39;[通配符]&#39;;-- 查询用户名第一个字陈的人SELECT `username` FROM `user` LIKE &#39;陈%&#39; ;-- 查询用户名第一个字陈且名字中第三个字为明SELECT `username` FROM `user` LIKE &#39;陈_明%&#39; ;</code></pre><h2>WHERE</h2><pre><code class="language-mysql">-- 查询id为300的用户信息，大于小于同理SELECT * FROM `user` WHERE id=300;-- 查询id在300到400之间的用户信息SELECT * FROM `user` WHERE id&gt;=300 AND id&lt;=400;SELECT * FROM `user` WHERE id BETWEEN 300 AND 400;-- 查询id为300，301，302的用户信息SELECT * FROM `user` WHERE id=300 AND id=301 AND id=302;SELECT * FROM `user` WHERE id IN (300,301,302);-- 查询邮箱为空的用户信息SELECT * FROM `user` WHERE email=&#39;&#39;;SELECT * FROM `user` WHERE email IS NULL;-- 查询邮箱不为空的用户信息SELECT * FROM `user` WHERE email IS NOT NULL;</code></pre><h2>JOIN</h2><pre><code class="language-mysql">-- 首先假设有张部门表depart，有id和department两个属性-- 查询用户的id，userrname和department这三个属性,相同的属性需要通过别名来划分-- INNER JOINSELECT s.id,username,departmen FROM student AS s INNER JOIN depart AS d ON s.id=d.id-- RIGHT JOINSELECT s.id,username,departmen FROM student AS s RIGHT JOIN depart AS d ON s.id=d.id-- LEFT JOINSELECT s.id,username,departmen FROM student AS s LEFT JOIN depart AS d ON s.id=d.id-- FULL JOINSELECT s.id,username,departmen FROM student AS s FULL JOIN depart AS d ON s.id=d.id</code></pre><ul><li>INNER JOIN如果表中有至少一个匹配，则返回行。</li><li>RIGHT JOIN即使左表中没有匹配，也从右表返回所有的行。假如depart有id=1000而user表中没有id=1000的数据也会查到id=1000数据，只是在user表的列数据为null。不会查到user表中有而depart表中没有的数据。</li><li>LEFT JOIN即使右表中没有匹配，也从左表返回所有的行。假如user有id=1000而depart表中没有id=1000的数据也会查到id=1000数据，只是在depart表的列数据为null。不会查到depart表中有而user表中没有的数据。</li><li>FULL JOIN只要其中一个表中存在匹配，则返回行。</li></ul><h2>排序</h2><pre><code class="language-mysql"># ASC升序,DESC降序SELECT * FROM user ORDER BY id ASCSELECT * FROM user ORDER BY id DESC</code></pre><h2>分页</h2><pre><code class="language-mysql">-- limit n,m-- n代表第一个值的坐标(从0开始)，m代表一页的数据多少-- 查询user表中第一到第五条数据SELECT * FROM user LIMIT 0,5</code></pre><h2>GROUP BY</h2><pre><code class="language-mysql"># 按一个或多个列对结果集进行分组SELECT birthday,COUNT(*) FROM user GROUP BY birthday# 查出每个生日的用户人数多少</code></pre><h2>HAVING</h2><pre><code class="language-mysql"># HAVING可以让筛选分组后的各组数据SELECT birthday,COUNT(*) FROM user GROUP BY birthday HAVING SUM(COUNT(*)) &gt; 100# 查出大于100个人的生日是同一天的生日</code></pre>]]></content:encoded>
            <category domain="https://yww52.com/categories/%25E6%2588%2591%25E7%259A%2584%25E6%258A%2580%25E6%259C%25AF%25E5%25AD%25A6%25E4%25B9%25A0%25E7%2589%25A9%25E8%25AF%25AD%25E6%259E%259C%25E7%2584%25B6%25E6%259C%2589%25E9%2597%25AE%25E9%25A2%2598/">我的技术学习物语果然有问题</category>
        </item>
        <item>
            <title>Docker</title>
            <link>https://yww52.com/posts/f5f9fa9b/</link>
            <guid isPermaLink="true">https://yww52.com/posts/f5f9fa9b/</guid>
            <pubDate>Tue, 27 Oct 2020 00:00:00 +0000</pubDate>
            <description>Docker 是一个开源的应用容器引擎。</description>
            <content:encoded><![CDATA[<img src="https://img.yww52.com/2020/10/2020-10-27/top_img.jpg" alt="Docker" style="max-width:100%;height:auto;margin-bottom:15px;" /><h1>官网</h1><p><a href="https://www.docker.com/">https://www.docker.com/</a></p><h1>Docker的安装</h1><p>去到官方文档<a href="https://docs.docker.com/get-docker/">https://docs.docker.com/get-docker/</a>，选择对应的操作系统安装就可以了。</p><p><strong>我的环境是CentOS 7.8</strong></p><pre><code class="language-bash"># 下载需要的安装包yum install -y yum-utils# 更新yum软件包的索引yum makecache fast# 安装dockeryum install docker-ce docker-ce-cli containerd.io# 启动dockersystemctl start docker# 建立docker组sudo groupadd docker# 将当前用户加入docker组sudo usermod -aG docker $USER# 查看版本docker version# 查看docker的信息docker info# 查看帮助命令docker --help</code></pre><p>出现了docker版本的信息就代表安装成功了。</p><h1>HelloWrold</h1><pre><code class="language-bash">#创建hello-world容器docker run hello-world</code></pre><p><img src="https://img.yww52.com/2020/10/2020-10-27/img1.png" alt=""></p><h1>阿里云镜像加速</h1><ol><li>打开阿里云的官网。</li><li>打开容器镜像服务。</li><li>点击镜像中心下的镜像加速器。</li><li>选择版本就输入命令就可以配置好阿里云的镜像加速了。</li></ol><p><img src="https://img.yww52.com/2020/10/2020-10-27/img2.png" alt=""></p><p>:::note{type=&quot;info&quot;}</p><p>这是阿里云的镜像加速，也可以找其他国内的源，不换源的话拉取镜像会很慢。</p><p>:::</p><h1>镜像</h1><h2>拉取镜像</h2><pre><code class="language-shell"># 拉取镜像docker pull [name]# 选取版本下载，不选默认选择最新版本的镜像docker pull [name][:tag]# 示例docker pull mysqldocker pull mysql:5.7</code></pre><h2>查看镜像</h2><pre><code class="language-shell"># 查看顶层镜像信息    docker imagesdocker image ls# 可选项    docker images -a, --all# 显示中间层的镜像    docker images -q, --quiet# 只显示镜像的IDdocker images -f since=mongo:3.2, --filter# 显示在mongo:3.2之后建立的镜像docker images -f before=mongo:3.2, --filter# 显示在mongo:3.2之前建立的镜像</code></pre><h2>查看镜像的历史</h2><pre><code class="language-shell">docker history [容器名]:[TAG]</code></pre><h2>搜索镜像</h2><pre><code class="language-shell"># 命令寻找镜像docker search [name]</code></pre><h2>删除镜像</h2><pre><code class="language-shell"># 通过镜像ID删除镜像docker rm [选项] [ID/名字]# 删除多个镜像docker rm [选项] [ID1] [ID2] ...# 删除所有容器docker rm -f $(docker images -aq)# 删除所有镜像名为nginxdocker image rm $(docker image ls -q nginx)# 删除所有在mongo:3.2之前的镜像docker image rm $(docker image ls -q -f before=mongo:3.2)</code></pre><h2>提交生成镜像</h2><pre><code class="language-shell"># 将自定义的容器提交成镜像docker commit -m=&quot;message&quot; -a=&quot;Author&quot; [容器ID] [要生成镜像的名字]:[TAG]docker commit --author &quot;Author&quot; --message &quot;message&quot; [容器ID] [要生成镜像的名字]:[TAG]# 查看镜像就能看到自定义的镜像docker images </code></pre><p>:::note{type=&quot;warning&quot;}</p><p>慎用docker commit。因为这样生成的黑箱镜像会对其他人的维护工作造成很大压力。生成镜像建议使用Dockerfile。</p><p>:::</p><h1>容器</h1><h2>新建并启动容器</h2><pre><code class="language-shell">docker run [可选参数] image# 参数docker run --name image# 给容器取名字docker run -d image# 以守护态运行启动，需要一个前台进程，不然会自动停止docker run -p 容器端口 image# 指定端口docker run -p 主机端口:容器端口 imagedocker run -p ip:主机端口:容器端口docker run -P image# 随机指定端口docker run -it image # t表示分配一个伪终端，i表示让容器标准输入打开</code></pre><h2>退出容器</h2><pre><code class="language-shell"># 通过命令来退出容器，在一些情况退出容器会使容器停止exit# 容器不停止退出CTRL + P + Q</code></pre><h2>查看运行的容器</h2><pre><code class="language-shell"># 查看运行的容器docker ps docker container ls# 查看容器运行的历史docker ps -a# 查看正在运行的容器的容器IDdocker ps -q </code></pre><h2>进入容器</h2><pre><code class="language-shell"># 进入容器开启新终端，从中使用exit不会使容器停止docker exec -it [容器ID] # 进入容器打开正在执行的终端，从中exit退出会使容器停止docker attach [容器ID]</code></pre><h2>启动容器</h2><pre><code class="language-shell"># 启动容器docker start [容器ID]docker container start [容器ID]# 重启容器docker container restart [容器ID]</code></pre><h2>停止容器</h2><pre><code class="language-shell"># 停止容器docker container stop [容器ID]# 强制停止容器，即杀死容器docker kill [容器ID]</code></pre><h2>删除容器</h2><pre><code class="language-shell"># 删除指定ID的容器，不能删除正在运行的容器docker rm  [容器ID]docker container rm [容器ID]# 强制删除治党ID的容器docker rm -f [容器ID]# 删除所有容器docker rm -f $(docker ps -aq)</code></pre><h2>查看日志</h2><pre><code class="language-shell"># 显示日志docker logs [容器ID]# 显示所有的日志并带上时间docker logs -tf [容器ID]# 显示指定条数的日志docker logs -tf --tail [numbers] [容器ID]</code></pre><h2>查看容器信息</h2><pre><code class="language-shell"># 查看容器信息docker container ls [容器ID]# 查看容器内部的进程信息docker top [容器ID]# 查看容器全部的信息docker inspect [容器ID]</code></pre><h2>拷贝容器内的文件到主机</h2><pre><code class="language-shell"># 将容器内的文件拷贝到主机的目录docker cp [容器ID]:[文件路径] [主机路径]</code></pre><h2>清理所有处于终止状态的容器</h2><pre><code class="language-shell">docker container prune</code></pre><h1>部署Tomcat</h1><ol><li><p>拉取Tomcat的镜像</p><pre><code class="language-shell">    docker pull tomcat</code></pre></li><li><p>启动容器</p><pre><code class="language-shell"># 启动容器        docker run -d --name tomcattest -p 5000:8080 tomcat# 一次性的测试容器    docker run -it --rm tomcat</code></pre></li><li><p>打开公网ip的5000端口，或者服务器内部的localhost的5000端口就能看到页面了。因为下载的镜像是被处理过的，所以看不到index页面。</p></li><li><p>进入容器</p><pre><code class="language-shell">    docker exec -it tomcattest /bin/bash# 进入到webapps发现没有任何的网站信息，发现是被存储到了webapps.dist里了，所以要想看到index，就需要拷贝过来    cp -r ./webapps.dist/ROOT/ webapps# 然后就能看见index页面了</code></pre></li></ol><h1>数据卷</h1><h2>自定义路径挂载</h2><pre><code class="language-shell"># 将容器内目录挂载到本地目录上docker run  -v [主机目录]:[容器目录]# 例子docker run -it -v /home/test:/home centos</code></pre><h2>匿名挂载</h2><pre><code class="language-shell"># 不指定本地目录的挂载docker run -v [容器目录]# 例子docker run -d -P --name nginxtest1 -v /etc/nginx nginx</code></pre><h2>具名挂载</h2><pre><code class="language-shell"># 指定卷名不指定本地目录挂载docker run -v [卷名]:[容器目录]# 例子docker run -d -P --name nginxtest2 -v nginx-test:/etc/nginx nginx</code></pre><h2>查看数据卷信息</h2><pre><code class="language-shell"># 查看当前数据卷的使用信息docker volume ls # 指定卷名查看数据卷的具体信息,可以看到本地的挂载目录docker volume inspect [卷名]</code></pre><h2>删除数据卷</h2><pre><code class="language-shell">docker volume rm [卷名]</code></pre><h2>删除无主的数据卷</h2><pre><code class="language-shell">docker volume prune</code></pre><h1>DockerFile</h1><h2>一个简单的Dockerfile</h2><ol><li><p>建一个新的文件夹，然后在里面建一个Dockerfile文件，内容为</p><pre><code class="language-dockerfile">FROM nginxRUN echo &#39;&lt;h1&gt;Hello, Docker!&lt;/h1&gt;&#39; &gt; /usr/share/nginx/html/index.html</code></pre></li><li><p>然后生成镜像</p><pre><code class="language-shell">    docker build -t nginx:v2 .</code></pre></li><li><p>然后创建容器打开url，就能看到<code>Hello Docker!</code>了。</p></li></ol><h2>构建镜像</h2><pre><code class="language-shell">docker build [选项] [镜像名]:[TAG] &lt;上下文路径/URL/-&gt;# 一般就为docker build  [镜像名]:[TAG] .</code></pre><p>:::note{type=&quot;info&quot;}</p><p>注意这个最后这个点，这个是表示指定当前目录为上下文路径，后面有些指令就是从上下文路径开始指引，不一定是Dockerfile的路径，为了方便才让Dockerfile文件与上下文路径一致。</p><p>:::</p><h2>指令</h2><h3>FROM</h3><p>FROM用来指定基础的镜像，即用这个镜像来进行二次开发。</p><p>这个指令是必须的，而且必须是第一条。</p><blockquote><p>Docker中有一个特殊的镜像，那就是<code>scratch</code>，表示一个空白的镜像。</p></blockquote><h3>MAINTAINER</h3><p>这个指令用来表示作者的信息。</p><pre><code class="language-dockerfile">MAINTAINER &lt;username email&gt;</code></pre><h3>RUN</h3><p>用来执行命令。</p><pre><code class="language-dockerfile"># shell格式RUN echo &#39;&lt;h1&gt;Hello, Docker!&lt;/h1&gt;&#39; &gt; /usr/share/nginx/html/index.html# exec格式RUN [&quot;可执行文件&quot;, &quot;参数1&quot;, &quot;参数2&quot;]</code></pre><p>:::note{type=&quot;warning&quot;}</p><p>当有多个命令的时候，尽量在一个RUN指令中完成，而不是使用多个RUN。因为一个指令就是建立一层，多层的结构就会显得十分的臃肿。</p><p>:::</p><h3>COPY</h3><p>从构建上下文的目录中将一个文件复制到镜像内的目标路径当中。</p><pre><code class="language-dockerfile">COPY  [文件路径] [镜像内目标路径]</code></pre><p>如果源路径为文件夹，复制的时候不是直接复制该文件夹，而是将文件夹中的内容复制到目标路径。</p><h3>ADD</h3><p>ADD也是复制，与COPY差不多，但是能干更多的事情，比如复制的文件是tar压缩文件，会帮你自动解压到目标路径。</p><pre><code class="language-dockerfile">ADD  [文件路径] [镜像内目标路径]</code></pre><blockquote><p>ADD有很多复杂的功能，所以只拷贝文件的时候尽量使用COPY，需要自动解压就用ADD。</p></blockquote><h3>WORKDIR</h3><p>用来设置工作目录。</p><pre><code class="language-dockerfile">WORKDIR &lt;工作目录&gt;</code></pre><p>工作目录是用来指定整个镜像的，因为每一层都是隔离的，所以不指定工作目录就会出现目录的混乱，即上一层使用了cd进入了新的目录，而下一层一不小心就当成是上一层的目录继续操作。</p><h3>EXPOSE</h3><p>暴露端口，类似之前的<code>-p</code>参数，声明容器运行时容器提供服务端口。注意仅仅是声明，不会自动进行端口映射。</p><h3>VOLUME</h3><pre><code class="language-dockerfile">VOLUME [&quot;&lt;路径1&gt;&quot;, &quot;&lt;路径2&gt;&quot;...]VOLUME &lt;路径&gt;</code></pre><p>这里的目录挂载之后就会在运行的时候自动挂载为匿名卷。</p><h3>ENV</h3><p>设置环境变量。</p><h3>CMD</h3><pre><code class="language-dockerfile"># shell格式CMD &lt;命令&gt;# exec格式CMD [&quot;可执行文件&quot;, &quot;参数1&quot;, &quot;参数2&quot;...]# 例子CMD echo $HOMECMD [&quot;sh&quot;,&quot;-c&quot;,&quot;echo $HOME&quot;]</code></pre><h3>ENTRYPOINT</h3><p>ENTRYPOINT与CMD差不多，只不过在容器run后面加命令就会自动添加到ENTRYPOINT的参数去。</p><blockquote><p>更多详细内容可以参考官网的<a href="https://docs.docker.com/engine/reference/builder/">Dockerfile reference</a>。</p></blockquote><h1>发布镜像</h1><h2>Dockerhub</h2><p><a href="https://www.docker.com/">https://www.docker.com/</a></p><pre><code class="language-shell"># 登陆dockerhubdocker login -u [username] -p [password]# 发布镜像docker push</code></pre><h2>阿里云镜像</h2><p>打开阿里云的容器镜像服务，创建命名空间和容器镜像，然后参考官方文档就能跟着发布了。</p><h1>Docker网络</h1><h2>docker0</h2><p>当Docker启动的时候就能看到docker会自主创建一个<code>docker0</code>的网桥。</p><pre><code class="language-shell">ip a# 查看网络信息 </code></pre><p><img src="https://img.yww52.com/2020/10/2020-10-27/img3.png" alt=""></p><p>docker0类似一个路由器，连接着各个容器。当创建一个容器的时候，也会创建一对的<code>veth pair</code> 接口，一个连接容器，一个连接docker0，这样就可以实现容器与docker0的通信和容器与容器的通信了（只限于ip）。这样Docker 就创建了在主机和所有容器之间一个虚拟共享网络。<img src="https://img.yww52.com/2020/10/2020-10-27/img4.png" alt=""></p><p>:::note{type=&quot;warning&quot;}</p><p>因为docker0的存在，所以默认创建的容器之间是可以通过ip来通信的，即每个容器之间通过docker分配的ip是可以互相ping通，但是不能通过容器名来ping通，因为是没有解析的。</p><p>:::</p><h2>link</h2><pre><code class="language-shell"># 通过link可以简单的链接网络docker run -d -P --link [需要连接的容器名] [镜像]# --link 其实就是为容器的host增加一条解析记录，所以在该容器就能ping通需要连接的容器名。# 这个只是增加解析，所以是不能反向ping通的</code></pre><h2>自定义网络</h2><pre><code class="language-shell"># 之前创建容器的时候，默认是与docker0进行桥接模式,下面两个命令是一样的docker run -d -P --net bridge [镜像]docker run -d -P [镜像]# 在docker0的网络中，每个容器只能通过IP互相ping通，但是不能访问域名即容器名，可以通过--link单向打通</code></pre><pre><code class="language-shell"># 自定义网络就修复了docker0的缺陷，容器能通过容器名来ping通# 创建自定义网络,具体ip看情况定docker network create --driver bridge --subnet 192.168.0.0/16 --gateway 192.168.0.1 [自定义网络名]docker network create -d bridge [自定义网络名]# 创建容器时指定所在网络docker run -d -P --net [自定义网络名] [镜像]</code></pre><h2>连通网络</h2><pre><code class="language-shell"># docker0网络下的容器通过容器名是不能Ping通另一个网络的容器的# 需要将容器直接连通另一个网络网卡docker network connect [另一个网络] [容器]# 其实就是将容器加入到另一个网络# 给了一个容器两个ip地址</code></pre><h1>DockerCompose</h1><h2>下载安装</h2><pre><code class="language-shell"># 官方文档下载sudo curl -L &quot;https://github.com/docker/compose/releases/download/1.27.4/docker-compose-$(uname -s)-$(uname -m)&quot; -o /usr/local/bin/docker-compose# 从github上下载会十分慢，建议换成国内源sudo curl -L https://get.daocloud.io/docker/compose/releases/download/1.25.1/docker-compose-`uname -s`-`uname -m` &gt; /usr/local/bin/docker-compose# 下载完成后会在/usr/local/bin下面会有二进制文件docker-compose# 将可执行权限应用于二进制文件sudo chmod +x /usr/local/bin/docker-compose</code></pre><h2>卸载</h2><pre><code class="language-shell"># 使用curl安装的sudo rm /usr/local/bin/docker-compose# 使用pip安装的pip uninstall docker-compose</code></pre><h2>Docker Compose入门</h2><ol><li><p>创建一个新的目录用于DockerCompose</p></li><li><p>在目录中创建一个<code>app.py</code></p><pre><code class="language-python">import timeimport redisfrom flask import Flaskapp = Flask(__name__)cache = redis.Redis(host=&#39;redis&#39;, port=6379)def get_hit_count():    retries = 5    while True:        try:            return cache.incr(&#39;hits&#39;)        except redis.exceptions.ConnectionError as exc:            if retries == 0:                raise exc            retries -= 1            time.sleep(0.5)@app.route(&#39;/&#39;)def hello():    count = get_hit_count()    return &#39;Hello World! I have been seen {} times.\n&#39;.format(count)</code></pre></li><li><p>创建一个<code>requirements.txt</code></p><pre><code class="language-txt">flaskredis</code></pre></li><li><p>建立<code>Dockerfile</code></p><pre><code class="language-dockerfile">FROM python:3.7-alpineWORKDIR /codeENV FLASK_APP=app.pyENV FLASK_RUN_HOST=0.0.0.0RUN apk add --no-cache gcc musl-dev linux-headersCOPY requirements.txt requirements.txtRUN pip install -r requirements.txtEXPOSE 5000COPY . .CMD [&quot;flask&quot;, &quot;run&quot;]</code></pre></li><li><p>创建<code>docker-compose.yml</code>定义服务</p><pre><code class="language-yml">version: &quot;3.8&quot;services:  web:    build: .    ports:      - &quot;5000:5000&quot;  redis:    image: &quot;redis:alpine&quot;</code></pre></li><li><p>然后启动应用程序</p><pre><code>docker-compose up</code></pre></li><li><p>打开url<a href="http://localhost:5000">http://localhost:5000</a>。就能看到计数的开始了。</p></li></ol><blockquote><p>这是官网的入门程序。地址为<a href="https://docs.docker.com/compose/gettingstarted/">https://docs.docker.com/compose/gettingstarted/</a>。</p></blockquote><h2>常用命令</h2><h3>查看项目中的所有容器</h3><pre><code class="language-shell">docker-compose ps</code></pre><h3>启动存在的服务容器</h3><pre><code class="language-shell">docker-compose start [SERVISE]</code></pre><h3>重启项目中的服务</h3><pre><code class="language-shell">docker-compose restart [SERVISE]# 指定重启前停止容器的超时，默认为10秒docker-compose restart -t, --timeout TIMEOUT</code></pre><h3>停止运行的容器</h3><pre><code class="language-shell">docker-compose stop [SERVISE]# 停止容器的超时，默认为10秒docker-compose stop -t, --timeout TIMEOUT</code></pre><h3>删除所有停止的服务容器</h3><pre><code class="language-shell">docker-compose rm [SERVISE]# 强制删除docker-compose rm -f [SERVISE] docker-compose rm --force [SERVISE] # 删除容器所挂载的数据卷docker-compose rm -v [SERVISE] </code></pre><h3>down</h3><pre><code class="language-shell"># 此命令将会停止 up 命令所启动的容器，并移除网络docker-compose down # 通过--volumes还删除Redis容器使用的数据量：docker-compose down --volumes</code></pre><h3>up</h3><pre><code class="language-shell">docker-compose up# -d 在后台运行服务容器。#--no-color 不使用颜色来区分不同的服务的控制台输出。#--no-deps 不启动服务所链接的容器。#--force-recreate 强制重新创建容器，不能与 --no-recreate 同时使用。#--no-recreate 如果容器已经存在了，则不重新创建，不能与 --force-recreate 同时使用。#--no-build 不自动构建缺失的服务镜像。#-t, --timeout TIMEOUT 停止容器时候的超时（默认为 10 秒）。</code></pre><h2>docker-compose.yml</h2><pre><code class="language-yaml"># 1. 版本，注意版本要匹配不然会报错version: &quot;3.8&quot;# 2. 服务services:  service1:# 服务1的配置  service2:# 服务2的配置# 3. 其他的配置</code></pre><h2>简单的微服务</h2><blockquote><p>用SpringBoot写上面类似的计数</p></blockquote><ol><li><p>创建springboot，导入依赖</p><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;org.springframework.boot&lt;/groupId&gt;            &lt;artifactId&gt;spring-boot-starter-web&lt;/artifactId&gt;        &lt;/dependency&gt;        &lt;dependency&gt;            &lt;groupId&gt;org.springframework.boot&lt;/groupId&gt;            &lt;artifactId&gt;spring-boot-starter-test&lt;/artifactId&gt;            &lt;scope&gt;test&lt;/scope&gt;            &lt;exclusions&gt;                &lt;exclusion&gt;                    &lt;groupId&gt;org.junit.vintage&lt;/groupId&gt;                    &lt;artifactId&gt;junit-vintage-engine&lt;/artifactId&gt;                &lt;/exclusion&gt;            &lt;/exclusions&gt;        &lt;/dependency&gt;&lt;/dependencies&gt;</code></pre></li><li><p>创建一个请求</p><pre><code class="language-Java">package com.yww.controller;import org.springframework.beans.factory.annotation.Autowired;import org.springframework.data.redis.core.StringRedisTemplate;import org.springframework.web.bind.annotation.GetMapping;import org.springframework.web.bind.annotation.RestController;@RestControllerpublic class HelloDocker {    @Autowired    StringRedisTemplate stringRedisTemplate;    @GetMapping(&quot;/hello&quot;)    public String hello(){        Long views = stringRedisTemplate.opsForValue().increment(&quot;views&quot;);        return &quot;Hello Docker views:&quot;+views;    }}</code></pre></li><li><p>配置Springboot的配置文件<code>application.properties</code></p><pre><code class="language-properties">server.port=8080spring.redis.host=redis        # 写服务名相当于域名</code></pre></li><li><p>编写<code>Dockerfile</code></p><pre><code class="language-dockerfile">FROM java:8COPY *.jar /app.jarCMD [&quot;--server.port=8080&quot;]ENTRYPOINT [&quot;java&quot;,&quot;-jar&quot;,&quot;/app.jar&quot;]</code></pre></li><li><p>编写<code>docker-compose.yml</code></p><pre><code class="language-yml">version: &#39;3.8&#39;services:  demoapp:    build: .    image: demoapp    depends_on:      - redis    ports:    - &quot;8080:8080&quot;  redis:    image: &quot;library/redis:alpine&quot;</code></pre></li><li><p>然后打包，将生成的Jar包上传到服务器上，然后运行,访问url<a href="ip:8080/hello">ip:8080/hello</a>就可以看到计数了。</p><pre><code class="language-shell">    docker-compose up</code></pre></li></ol><h1>Docker swarm</h1><p><img src="https://img.yww52.com/2020/10/2020-10-27/img6.png" alt=""></p><h2>swarm集群</h2><h3>初始化一个swarm集群</h3><pre><code class="language-shell"># 初始化一个swarm节点docker swarm init # 要让其他服务器加入，需要配置一个ip,你的Docker主机有多个网卡，就需要指定ipdocker swarm init --advertise--addr [ip]</code></pre><h3>加入节点</h3><pre><code class="language-shell"># 获取加入tokendocker swarm join-token managerdocker swarm join-token worker# 获得一串带有token的命令，可以以manager或worker的节点加入swarm</code></pre><h3>查看节点信息</h3><pre><code class="language-shell">docker node ls </code></pre><h3>离开集群</h3><pre><code class="language-shell">docker swarm leave</code></pre><h2>Raft协议</h2><p>Raft协议，也是一种一致性的算法，具体的算法内容就自行查阅。</p><p>要先知道的是managert节点分为三种状态，<code>Leader</code>,<code>Reachable</code>,<code>Unreachable</code>。</p><p>现在以Docker集群来进行大概的情况讲解，假设现在swarm的集群有四个节点。</p><h3>一个manager节点</h3><p>因为只有manager节点能进行管理，所以在只有一个manager节点，三个worker节点的情况下，manager节点出现故障即状态为<code>Unreachable</code>，该集群就无法进行管理，只能等待manager节点的修复。</p><h3>两个manager节点</h3><p>在这种情况下，其中一个manager节点状态为<code>Unreachable</code>，另一个manager节点也会无法进行管理操作，所以两个manager节点是无法进行很好的管理的。</p><p>当故障的manager节点修复好在加入集群，状态就会变成<code>Reachable</code>，而另外的那个就会变成<code>Leader</code>。</p><h3>三个manager节点</h3><p>这种就是最起码的集群管理。在这种情况下，第一个manager节点出故障了，其他两个是能进行管理操作的，而不是像之前一样无法管理，当挂了两个就不能管理了。所以一个标准的集群式至少需要三个manager节点的，至少有两个manager节点可用，这样才能保证集群的正常运行。</p><h2>服务</h2><pre><code class="language-shell"># 创建一个服务,命令类似run,创建的服务会随机分配给集群的节点docker service create [iamge]-p--name# 指定服务的运行方式,replicated模式为只在worker节点运行，global为全部节点docker service create --mode replicated [iamge]docker service create --mode global [iamge]# 查看服务docker service ls docker service ps [服务名]docker service inspect [服务名]# 创建到num个服务的副本，然后随机分布给集群中的节点,也可以回滚docker service update --replicas [num] [服务名]docker service scale [服务名]=[num]# 删除服务docker service rm [服务名]</code></pre><p>:::note{type=&quot;info&quot;}</p><p>可以把集群看成是一个整体，因为服务的副本也是接的相同的端口，所以访问哪个节点的ip端口，都可以访问到这个服务。</p><p>:::</p><h1>附录</h1><p>docker命令一图流。</p><p><img src="https://img.yww52.com/2020/10/2020-10-27/img5.png" alt=""></p><h1>参考资料</h1><ul><li><a href="https://yeasy.gitbook.io/docker_practice/contributing">https://yeasy.gitbook.io/docker_practice/contributing</a></li></ul>]]></content:encoded>
            <category domain="https://yww52.com/categories/%25E6%2588%2591%25E7%259A%2584%25E6%258A%2580%25E6%259C%25AF%25E5%25AD%25A6%25E4%25B9%25A0%25E7%2589%25A9%25E8%25AF%25AD%25E6%259E%259C%25E7%2584%25B6%25E6%259C%2589%25E9%2597%25AE%25E9%25A2%2598/">我的技术学习物语果然有问题</category>
        </item>
        <item>
            <title>swagger</title>
            <link>https://yww52.com/posts/bf71eba1/</link>
            <guid isPermaLink="true">https://yww52.com/posts/bf71eba1/</guid>
            <pubDate>Mon, 26 Oct 2020 00:00:00 +0000</pubDate>
            <description>swagger是一款让你能更好书写API文档的框架。</description>
            <content:encoded><![CDATA[<img src="https://img.yww52.com/2020/10/2020-10-26/top_img.jpg" alt="swagger" style="max-width:100%;height:auto;margin-bottom:15px;" /><h1>简单的集成swagger</h1><ol><li><p>为你的项目导入两个依赖</p><pre><code class="language-xml">    &lt;dependency&gt;       &lt;groupId&gt;io.springfox&lt;/groupId&gt;       &lt;artifactId&gt;springfox-swagger2&lt;/artifactId&gt;       &lt;version&gt;2.9.2&lt;/version&gt;    &lt;/dependency&gt;    &lt;dependency&gt;       &lt;groupId&gt;io.springfox&lt;/groupId&gt;       &lt;artifactId&gt;springfox-swagger-ui&lt;/artifactId&gt;    &lt;/dependency&gt;</code></pre></li><li><p>创建一个配置类</p><pre><code class="language-Java">package com.yww.config;import org.springframework.context.annotation.Configuration;import springfox.documentation.swagger2.annotations.EnableSwagger2;@Configuration@EnableSwagger2        //开启swagger2public class SwaggConfig {}</code></pre></li><li><p>开启项目，打开url<a href="http://localhost:8080/swagger-ui.html">http://localhost:8080/swagger-ui.html</a>，就能看到ui界面<img src="https://img.yww52.com/2020/10/2020-10-26/img1.png" alt=""></p></li></ol><h1>配置文档信息</h1><ol><li><p>因为swagger的实例Bean是Docker，所以用Docker来配置swagger。</p><pre><code class="language-Java">package com.yww.config;import org.springframework.context.annotation.Bean;import org.springframework.context.annotation.Configuration;import springfox.documentation.service.ApiInfo;import springfox.documentation.service.Contact;import springfox.documentation.spi.DocumentationType;import springfox.documentation.spring.web.plugins.Docket;import springfox.documentation.swagger2.annotations.EnableSwagger2;import java.util.ArrayList;@Configuration@EnableSwagger2public class SwaggConfig {    @Bean    public Docket docket(){        //调用apiInfo方法来传入信息        return new Docket(DocumentationType.SWAGGER_2).apiInfo(apiInfo());    }    //自定义apiInfo方法实现自定义信息配置    private ApiInfo apiInfo(){        //创建一个contact对象以便输入        Contact contact = new Contact(&quot;yw&quot;, &quot;localhost:8080/&quot;, &quot;1141950370@qq.com&quot;);        return new ApiInfo(                &quot;Yw的swagger文档&quot;,                   //标题                &quot;永远相信美好的事情即将发生&quot;,        //描述                &quot;v1.0&quot;,                            //版本号                &quot;urn:tos&quot;,                        //termsOfServiceUrl                contact,                        //contact对象                &quot;Apacher 2.0&quot;,                    //许可信息                &quot;http://www.apache.org/licenses/LINCENSE-2.0 &quot;,        //许可信息的链接                new ArrayList());    }}</code></pre></li><li><p>配置完成以后就可以打开<a href="http://localhost:8080/swagger-ui.html">http://localhost:8080/swagger-ui.html</a>看到信息了。 <img src="https://img.yww52.com/2020/10/2020-10-26/img2.png" alt=""></p></li></ol><h1>扫描接口的配置</h1><p>使用Docket的select方法扫描，通过build方法来构建扫描的配置，<strong>用apis方法来配置扫描的位置</strong>，<strong>paths方法来配置接口扫描过滤</strong>。</p><h2>扫描指定的包</h2><pre><code class="language-Java">@Bean    public Docket docket(){        return new Docket(DocumentationType.SWAGGER_2)                .apiInfo(apiInfo())                .select()            //basePackage方法指定扫描的包                .apis(RequestHandlerSelectors.basePackage(&quot;com.yww.control&quot;))                .build();    }</code></pre><h2>扫描全部的包</h2><pre><code class="language-Java">@Bean    public Docket docket(){        return new Docket(DocumentationType.SWAGGER_2)                .apiInfo(apiInfo())                .select()            //any方法扫描全部的包                .apis(RequestHandlerSelectors.any())                .build();    }</code></pre><h2>不扫描包</h2><pre><code class="language-Java">@Bean    public Docket docket(){        return new Docket(DocumentationType.SWAGGER_2)                .apiInfo(apiInfo())                .select()            //none方法不扫描包                .apis(RequestHandlerSelectors.none())                .build();    }</code></pre><h2>扫描类上的注解</h2><pre><code class="language-Java">@Bean    public Docket docket(){        return new Docket(DocumentationType.SWAGGER_2)                .apiInfo(apiInfo())                .select()            //withClassAnnotation方法扫描类上带有Controller注解的包                .apis(RequestHandlerSelectors.withClassAnnotation(Controller.class))                .build();    }</code></pre><h2>扫描方法上的注解</h2><pre><code class="language-Java">@Bean    public Docket docket(){        return new Docket(DocumentationType.SWAGGER_2)                .apiInfo(apiInfo())                .select()            //withMethodAnnotation方法扫描方法上带有GetMapping注解的方法                .apis(RequestHandlerSelectors.withMethodAnnotation(GetMapping.class))                .build();    }</code></pre><h2>扫描任何请求</h2><pre><code class="language-Java">@Bean    public Docket docket(){        return new Docket(DocumentationType.SWAGGER_2)                .apiInfo(apiInfo())                .select()                .apis(RequestHandlerSelectors.basePackage(&quot;com.yww.control&quot;))            //any方法扫描全部请求            .paths(PathSelectors.any())                .build();    }</code></pre><h2>不扫描请求</h2><pre><code class="language-Java">@Beanpublic Docket docket(){    return new Docket(DocumentationType.SWAGGER_2)            .apiInfo(apiInfo())            .select()            .apis(RequestHandlerSelectors.basePackage(&quot;com.yww.control&quot;))        //none方法不扫描请求        .paths(PathSelectors.none())            .build();}</code></pre><h2>通过正则来扫描请求</h2><pre><code class="language-Java">@Bean    public Docket docket(){        return new Docket(DocumentationType.SWAGGER_2)                .apiInfo(apiInfo())                .select()                .apis(RequestHandlerSelectors.basePackage(&quot;com.yww.control&quot;))            //regex方法通过正则匹配来扫描请求            .paths(PathSelectors.regex(final String pathRegex))                .build();    }</code></pre><h2>控制路径来扫描请求</h2><pre><code class="language-Java">@Bean    public Docket docket(){        return new Docket(DocumentationType.SWAGGER_2)                .apiInfo(apiInfo())                .select()                .apis(RequestHandlerSelectors.basePackage(&quot;com.yww.control&quot;))            //ant方法通过路径来扫描请求            .paths(PathSelectors.ant(&quot;/yw/**&quot;))                .build();    }</code></pre><p>:::note{type=&quot;info&quot;}</p><p>使用select配置扫描，用其中的apis方法配置扫描对象，用paths方法来指定扫描的请求</p><p>:::</p><h1>配置swagger的启动</h1><pre><code class="language-Java">@Bean    public Docket docket(){        return new Docket(DocumentationType.SWAGGER_2)                .apiInfo(apiInfo())            //使用enable方法来配置swagger是否启动，默认为true                .enable(false)                .select()                .apis(RequestHandlerSelectors.basePackage(&quot;com.yww.control&quot;))                .build();    }</code></pre><h1>配置swagger分组</h1><pre><code class="language-Java">package com.yww.config;import org.springframework.context.annotation.Bean;import org.springframework.context.annotation.Configuration;import springfox.documentation.spi.DocumentationType;import springfox.documentation.spring.web.plugins.Docket;import springfox.documentation.swagger2.annotations.EnableSwagger2;@Configuration@EnableSwagger2public class SwaggConfig {//通过groupName方法分组，创建不同的api接口页面    @Bean    public Docket docket1(){        return new Docket(DocumentationType.SWAGGER_2)                .groupName(&quot;Yw1&quot;);    }    @Bean    public Docket docket2(){        return new Docket(DocumentationType.SWAGGER_2)                .groupName(&quot;Yw2&quot;);    }    }</code></pre><p><img src="https://img.yww52.com/2020/10/2020-10-26/img3.png" alt=""></p><h1>实体类的配置</h1><ol><li><p>首先创建一个实体类</p><pre><code class="language-Java">package com.yww.pojo;public class User {    private String name;    private String password;    private String password;    public String getName() {        return name;    }    public void setName(String name) {        this.name = name;    }    public String getPassword() {        return password;    }    public void setPassword(String password) {        this.password = password;    }}</code></pre></li><li><p>然后再control层加入返回实体类的请求就可以看到实体类了。</p><pre><code class="language-Java">package com.yww.control;import com.yww.pojo.User;import org.springframework.web.bind.annotation.PostMapping;import org.springframework.web.bind.annotation.RestController;/** * @ClassName UserControl * @Descriprtion TODO * @Author Yw * @Date 2020/10/26 19:46 * @Version 1.0 **/@RestControllerpublic class UserControl {    //通过返回的实体类就能看到User的信息了    @PostMapping(&quot;/user&quot;)    public User user(){        return new User();    }}</code></pre><p><img src="https://img.yww52.com/2020/10/2020-10-26/img4.png" alt=""></p></li><li><p>配置注释</p><pre><code class="language-Java">//@ApiModel和@ApiModelProperty注释配置注释信息@ApiModel(&quot;用户类&quot;)public class User {    @ApiModelProperty(&quot;用户名&quot;)    private String name;    @ApiModelProperty(&quot;密码&quot;)    private String password;    public String getName() {        return name;    }    public void setName(String name) {        this.name = name;    }    public String getPassword() {        return password;    }    public void setPassword(String password) {        this.password = password;    }}</code></pre><p><img src="https://img.yww52.com/2020/10/2020-10-26/img5.png" alt=""></p><pre><code class="language-java">@RestControllerpublic class UserControl {    @PostMapping(&quot;/user&quot;)    public User user(){        return new User();    }    //@ApiOperation为一个接口加注释，@ApiParam为参数加注释    @ApiOperation(&quot;Hellor控制类&quot;)    @GetMapping(&quot;/hello&quot;)    public String hello(@ApiParam(&quot;用户名&quot;)String name){        return &quot;Hello&quot;+name;    }}</code></pre><p><img src="https://img.yww52.com/2020/10/2020-10-26/img6.png" alt=""></p></li></ol><h1>配置swagger的皮肤</h1><h2>默认的皮肤</h2><pre><code class="language-xml">&lt;dependency&gt;      &lt;groupId&gt;io.springfox&lt;/groupId&gt;      &lt;artifactId&gt;springfox-swagger-ui&lt;/artifactId&gt;      &lt;version&gt;2.9.2&lt;/version&gt;&lt;/dependency&gt;</code></pre><p>这就是之前导入的依赖，页面ui地址<a href="http://localhost:8080/swagger-ui.html">http://localhost:8080/swagger-ui.html</a><img src="https://img.yww52.com/2020/10/2020-10-26/img7.png" alt=""></p><h2>bootstrap的UI</h2><pre><code class="language-xml">&lt;dependency&gt;   &lt;groupId&gt;com.github.xiaoymin&lt;/groupId&gt;   &lt;artifactId&gt;swagger-bootstrap-ui&lt;/artifactId&gt;   &lt;version&gt;1.9.1&lt;/version&gt;&lt;/dependency&gt;</code></pre><p>页面UI地址<a href="http://localhost:8080/doc.html">http://localhost:8080/doc.html</a></p><p><img src="https://img.yww52.com/2020/10/2020-10-26/img8.png" alt=""></p><h2>Layui-ui</h2><pre><code class="language-xml">&lt;dependency&gt;   &lt;groupId&gt;com.github.caspar-chen&lt;/groupId&gt;   &lt;artifactId&gt;swagger-ui-layer&lt;/artifactId&gt;   &lt;version&gt;1.1.3&lt;/version&gt;&lt;/dependency&gt;</code></pre><p>页面UI地址<a href="http://localhost:8080/docs.html">http://localhost:8080/docs.html</a></p><p><img src="https://img.yww52.com/2020/10/2020-10-26/img9.png" alt=""></p><h2>mg-ui</h2><pre><code class="language-xml">&lt;dependency&gt;   &lt;groupId&gt;com.zyplayer&lt;/groupId&gt;   &lt;artifactId&gt;swagger-mg-ui&lt;/artifactId&gt;   &lt;version&gt;1.0.6&lt;/version&gt;&lt;/dependency&gt;</code></pre><p>页面UI地址<a href="http://localhost:8080/document.html">http://localhost:8080/document.html</a></p><p><img src="https://img.yww52.com/2020/10/2020-10-26/img10.png" alt=""></p><p>:::note{type=&quot;info&quot;}</p><p>更多好看的ui可以去寻找哦。</p><p>:::</p>]]></content:encoded>
            <category domain="https://yww52.com/categories/%25E6%2588%2591%25E7%259A%2584%25E6%258A%2580%25E6%259C%25AF%25E5%25AD%25A6%25E4%25B9%25A0%25E7%2589%25A9%25E8%25AF%25AD%25E6%259E%259C%25E7%2584%25B6%25E6%259C%2589%25E9%2597%25AE%25E9%25A2%2598/">我的技术学习物语果然有问题</category>
        </item>
        <item>
            <title>数据结构</title>
            <link>https://yww52.com/posts/79666db/</link>
            <guid isPermaLink="true">https://yww52.com/posts/79666db/</guid>
            <pubDate>Fri, 23 Oct 2020 00:00:00 +0000</pubDate>
            <description>有空就复习一下数据结构，看的书是大话数据结构，还是挺有趣的这本书，因为是复习，所以本篇的代码用的是C写的。</description>
            <content:encoded><![CDATA[<img src="https://img.yww52.com/2020/10/2020-10-23top_img.jpg" alt="数据结构" style="max-width:100%;height:auto;margin-bottom:15px;" /><h1>绪论</h1><h2>数据</h2><p>数据是描述客观事物的符号，是计算机中可以操作的对象，是能被计算机识别，并输入给计算机处理的符号集合。</p><h2>数据元素</h2><p>数据元素是组成数据的，有一定意义的单位，在计算机中通常作为整体处理，也被称为记录。</p><h2>数据项</h2><p>数据项是数据不可分割的最小单位，若干个数据项可以组成一个数据元素。</p><h2>数据对象</h2><p>数据对象是性质相同的数据元素的组合，是数据的子集。</p><h2>数据结构</h2><p>不同的数据元素之间不是独立的，而是存在特定的关系，这种关系就是结构。</p><p>数据结构就是相互之间存在一种或多种特定关系的数据元素的集合。</p><h2>逻辑结构</h2><p>逻辑结构是指对象中数据元素之间的相互关系。</p><ol><li>集合结构集合结构就是数据元素除了同属于一个集合外没有其他的关系。</li><li>线性结构线性结构中的数据元素之间是一对一的关系。</li><li>树形结构树形结构中的数据元素之间是一对多的层次关系。</li><li>图形结构图形结构中的数据元素之间是多对多的关系。</li></ol><h2>物理结构</h2><p>物理结构是指数据的逻辑结构在计算机中的存储形式。</p><ol><li>顺序存储结构顺序存储结构意思是把数据元素存放在地址连续的存储单元里，其数据间的逻辑关系和物理关系是一致的。</li><li>链式存储结构链式存储结构是把数据元素存放在任意的存储单元里，这组存储单元可以是连续的，也可以是不连续的。</li></ol><h1>算法</h1><p>算法是解决特定问题求解步骤的描述，在计算机表现为指令的有限序列，并且每条指令表示一个或多个操作。</p><h2>算法的特性</h2><ol><li>输入</li><li>输出</li><li>有穷性</li><li>确定性</li><li>可行性</li></ol><h2>算法设计的要求</h2><ol><li>正确性</li><li>可读性</li><li>健壮性</li><li>时间效率高和存储量低</li></ol><h2>算法效率的度量方法</h2><h3>事后统计方法</h3><p>这种方法主要是通过设计好的测试程序和数据，利用计算机计时器对不同算法编制的程序的运行时间进行比较，从而确定算法效率的高低。</p><h3>事前分析估算方法</h3><p>这种方法是在计算机程序编制前依据统计方法对算法进行估算。</p><h2>推导大O阶方法</h2><ol><li>用常数1取代运行时间中的所有加法常数</li><li>在修改后的运行次数函数中，只保留最高阶项。</li><li>如果最高阶项存在且不是1，则去除与这个项目相乘的常数。</li></ol><p>以下是一些简单的例子。</p><pre><code class="language-c">//因为运行次数都只是1次，所以为O(1)int sum = 0;sum = 100*100;</code></pre><pre><code class="language-c">//简单的for循环，循环n次，所以为O(n)int i;for(i = 0; i &lt; n; i++){      // O(1)的语句    }</code></pre><pre><code class="language-c">//count每次与2相乘，故会执行log2n次，O(log2n)int count = 1;while(count &lt; n){        count *= 2;    }</code></pre><pre><code class="language-c">// 两个for循环，每个循环n次，故O(n^2)int i,j;for(i = 0; i &lt; n; i++){      for(j = 0; j &lt; n; j++){          // O(1)的语句      }    }// 两个for循环，一个循环m次一个循环n次，故O(n*m)int i,j;for(i = 0; i &lt; m; i++){      for(j = 0; j &lt; n; j++){          // O(1)的语句      }    }// 两个for循环，一个循环m次，一个每次递减，通过级数之和可得循环了n^2/2+n/2次,故O(n^2)int i,j;for(i = 0; i &lt; n; i++){      for(j = i; j &lt; n; j++){          // O(1)的语句      }    }</code></pre><h1>线性表</h1><h2>线性表的顺序存储结构</h2><p>线性表的顺序存储结构指的是用一段地址连续的存储单元依次存储线性表的数据元素。</p><h3>定义存储的结构</h3><pre><code class="language-c"># define MAXSIZE 100// 设置顺序表的最大存储个数typedef int ElemType;//存储数据的类型typedef struct SqList{    ElemType data[MAXSIZE];    int length;//表示顺序表当前长度}SqList;</code></pre><h3>获取数据</h3><ol><li>判断是否为空表。</li><li>判断获取数据的下标是否在数组的范围。</li><li>获取数组指定位置的数据。</li></ol><pre><code class="language-c">// 时间复杂度为O(1)int GetElem(SqList L,int i,ElemType *e){        if(L.length==0)            return -1;        if(i-1 &lt; 0 || i &gt; L.length)            return -1;        *e = L.data[i-1];        return 1;     }// -1表示失败，1表示成功</code></pre><h3>插入数据</h3><ol><li>判断表是否已经满了。</li><li>判断插入的位置是否正确。</li><li>判断插入的位置是否在表尾，不在表尾就指定位置之后的数据全部后移一位。</li><li>将数据插入指定位置，表长加1。</li></ol><pre><code class="language-c">// 插入的位置在最后，时间复杂度为O(1)// 插入的位置在最前，时间复杂度为O(n)int ListInsert(SqList *L,int i,ElemType e){        int j;        if(L-&gt;length == 0)            return -1;        if(i-1 &lt; 0 || i &gt; L-&gt;length + 1)            return -1;        if(i &lt;= L-&gt;length){            for(j = L-&gt;length-1;j &gt;= i-1;j++){                L-&gt;data[j+1] = L-&gt;data[j];            }        }        L-&gt;data[i-1] = e;        L-&gt;length++;        return 1;   }</code></pre><h3>删除数据</h3><ol><li>判断表是否为空表</li><li>判断删除的位置是否正确。</li><li>判断删除的位置是否是最后一位，不是，删除的位置后面的数据全部前移一位。</li><li>表长减一。</li></ol><pre><code class="language-c">// 删除的位置在最后，时间复杂度为O(1)// 删除的位置在最前，时间复杂度为O(n)int ListDelete(SqList *L,int i){     if(L-&gt;length == 0)            return -1;    if(i - 1 &lt; 0 || i &gt; L-&gt;length)            return -1;    if(i &lt; L-&gt;length)            for(j = i-1;j &lt;= L-&gt;length;j++){                L-&gt;data[j] =  L-&gt;data[j+1];            }    L-&gt;length = -1;    return 1;   }</code></pre><h2>线性表的链式存储结构</h2><h3>定义存储结构</h3><pre><code class="language-c">typedef struct Node {int data;struct Node *next;} Node;</code></pre><h3>获取数据</h3><ol><li>从头查找，一直找到第i个结点</li><li>计数到i就停止，返回数据</li><li>没到i链表就结束了，就返回-1</li></ol><pre><code class="language-c">int getElem(Node *L,int i) {    int j = 1;    Node *pre = L-&gt;next;    while (pre!=null &amp;&amp; j&lt;i) {pre = pre-&gt;next;        j++;    }    if (pre==null || j&gt;i) {return -1;    }    return pre-&gt;data;}</code></pre><h3>插入数据</h3><ol><li><p>首先遍历到第i-1个结点</p></li><li><p>然后创建一个结点，next指向第i个结点</p></li><li><p>第i-1个结点的next指向新创建的结点</p></li></ol><pre><code class="language-c">int insert(Node *L,int i,int e) {    int j = 1;    Node *pre = L;    while (pre!=null &amp;&amp; j&lt;i) {pre = pre-&gt;next;        j++;    }    if(pre==null || j&gt;i) {return -1;    }    Node *p = (Node*)malloc(sizeof(Node));    p-&gt;data = e;    p-&gt;next = pre-&gt;next;    pre-&gt;next = p;    return 1;}</code></pre><h3>删除数据</h3><ol><li>首先遍历到第i-1个结点</li><li>第i-1个结点的next指向第i个结点的next</li><li>释放第i个结点</li></ol><pre><code class="language-c">int delete(Node *L,int i) {    int j = 1;    Node *pre = L;    while (pre!=null &amp;&amp; j&lt;i) {pre = pre-&gt;next;        j++;    }    if(pre==null || j&gt;i) {return -1;    }    Node *p = pre-&gt;next;    pre-&gt;next = p-&gt;next;    free(p);    return 1;}</code></pre><h1>树</h1><ul><li>结点拥有的子树数称为结点的度，度为0称为叶结点，树的度是整个树度的最大值</li><li>树中结点的最大层次称为树的深度或高度</li><li>如果树中的结点的各个子树看成从左到右是有次序的，就称为有序树，反之为无序树</li></ul><h2>定义二叉树存储结构</h2><pre><code class="language-c">typedef struct BiTree{    TElemType data;    struct BiTree *lchild,*rchild;}BiTree;</code></pre><h2>二叉树的遍历</h2><ul><li>前序遍历先访问根节点，然后前序遍历左子树，再前序遍历右子树。</li><li>中序遍历先中序遍历左子树，然后访问根节点，再中序遍历右子树。</li><li>后序遍历先后序遍历左子树，再后序遍历右子树，然后再访问根结点。</li><li>层序遍历从上到下，从左到右的顺序访问结点。</li></ul><h2>创建二叉树</h2><ol><li>输入根节点数据</li><li>递归创建左子树，递归创建右子树</li></ol><pre><code class="language-c">void CreaterBiTree (BiTree *T){    char ch;    scanf(&quot;%c&quot;,&amp;ch);    if (&#39;#&#39; == ch) {        T = NULL;        return;    } else {        T = (BiTree*) malloc(sizeof(BiTree));        T-&gt;data = ch;        CreaterBiTree(T-&gt;lchild);        CreaterBiTree(T-&gt;rchild);    }}BiTree* CreaterBiTree (){    BiTree * T = (BiTree*) malloc(sizeof(BiTree));    char ch;    scanf(&quot;%c&quot;,&amp;ch);    if (&#39;#&#39; == ch) {        T = NULL;        return;    } else {        T-&gt;data = ch;        T-&gt;lchild = CreaterBiTree();        T-&gt;rchild = CreaterBiTree();    }}</code></pre><h3>前序遍历</h3><ol><li>判断是否为空结点</li><li>打印根节点数据，递归左子树，递归右子树</li></ol><pre><code class="language-c">void PreOrderTraverse (BiTree* T){    if(NULL == T){        return;    }    printf(&quot;%d&quot;,T-&gt;data);    PreOrderTraverse(T-&gt;lchild);    PreOrderTraverse(T-&gt;rchild);}</code></pre><h3>中序遍历</h3><ol><li>判断是否为空结点</li><li>递归左子树，打印根节点数据，递归右子树</li></ol><pre><code class="language-c">void InOrderTraverse (BiTree* T){    if(NULL == T){        return;    }    InOrderTraverse(T-&gt;lchild);    printf(&quot;%d&quot;,T-&gt;data);    InOrderTraverse(T-&gt;rchild);}</code></pre><h3>后序遍历</h3><ol><li>判断是否为空结点</li><li>递归左子树，递归右子树，打印根节点数据</li></ol><pre><code class="language-c">void PostOrderTraverse (BiTree* T){    if(NULL == T){        return;    }    PostOrderTraverse(T-&gt;lchild);    PostOrderTraverse(T-&gt;rchild);    printf(&quot;%d&quot;,T-&gt;data);}</code></pre><h3>层次遍历</h3><pre><code class="language-c">typedef struct queue {int front,rear;BiTree* data[20];}queue;//初始化队列queue* initqueue() {queue* q = (queue*)malloc(sizeof(queue));q-&gt;front = 0;q-&gt;rear = 0;}//判断队列是否为空int emptyqueue(queue* q) {if(q-&gt;rear==q-&gt;front)return 1;elsereturn 0;}//出队bitree* dequeue(queue* q) {return q-&gt;data[(q-&gt;front)++];}//入队 void inqueue(queue* q,BiTree* root) {if(q-&gt;rear==19)return;elseq-&gt;data[(q-&gt;rear)++]=root;}void levorder(BiTree* root,queue* q) {BiTree *p;p = root;if(p!=NULL)inqueue(q,p);while(!emptyqueue(q)){p = dequeue(q);printf(&quot;%d&quot;,p-&gt;data);if(p-&gt;lchild!=NULL)inqueue(q,p-&gt;lchild);if(p-&gt;rchild!=NULL)inqueue(q,p-&gt;rchild); } }</code></pre><h3>性质</h3><ul><li>知道前序遍历的顺序和中序遍历的顺序可以确定一颗二叉树</li><li>知道中序遍历的顺序和后序遍历的顺序可以确定一颗二叉树</li><li>知道前序序遍历的顺序和后序遍历的顺序是不可以确定一颗二叉树的</li></ul>]]></content:encoded>
            <category domain="https://yww52.com/categories/%25E6%2588%2591%25E7%259A%2584%25E6%258A%2580%25E6%259C%25AF%25E5%25AD%25A6%25E4%25B9%25A0%25E7%2589%25A9%25E8%25AF%25AD%25E6%259E%259C%25E7%2584%25B6%25E6%259C%2589%25E9%2597%25AE%25E9%25A2%2598/">我的技术学习物语果然有问题</category>
        </item>
        <item>
            <title>YAML基本语法</title>
            <link>https://yww52.com/posts/bc6b6e0d/</link>
            <guid isPermaLink="true">https://yww52.com/posts/bc6b6e0d/</guid>
            <pubDate>Tue, 20 Oct 2020 00:00:00 +0000</pubDate>
            <description>记录一下Yaml的语法</description>
            <content:encoded><![CDATA[<img src="https://img.yww52.com/2020/10/2020-10-20top_img.jpg" alt="YAML基本语法" style="max-width:100%;height:auto;margin-bottom:15px;" /><h1>基本语法</h1><pre><code class="language-yaml">Key: value</code></pre><p>注意冒号后面的空格是一定要有的，YAML语法对空格是十分敏感的，而且是用空格来控制层级关系，就比如下面这个例子。</p><pre><code class="language-yml">server: port: 8081 path: </code></pre><p>这就表示server这个配置有两个子配置，这两个子配置是port和path，它们都在同一个层级，主要是看首字母的列数，子配置的首字母列数是不能在父配置的左边的比如下面的这个列子就是错误。</p><pre><code class="language-yml"> server:port: 8081</code></pre><p>:::note{type=&quot;warning&quot;}</p><p>YAML对大小写敏感，所以不要弄错大小写。</p><p>:::</p><h1>值</h1><ol><li><p>值为数字，字符串或者是布尔类型</p><pre><code class="language-yml">Key: value</code></pre><p>字符串默认是不用加引号的，也可以加上，单引号包括是换转义特殊字符的，而双引号不会，就比如经常写的\n换行等字符。</p></li><li><p>对象和Map</p><pre><code class="language-yml">User: Name: student ID: 02</code></pre><pre><code class="language-yaml">User: {Name: student,ID: 02}</code></pre></li><li><p>数组</p><pre><code class="language-yaml">User: - user01 - user02 - user03</code></pre><pre><code class="language-yaml">User: [user01,user02,user03]</code></pre></li></ol>]]></content:encoded>
            <category domain="https://yww52.com/categories/%25E6%2588%2591%25E7%259A%2584%25E6%258A%2580%25E6%259C%25AF%25E5%25AD%25A6%25E4%25B9%25A0%25E7%2589%25A9%25E8%25AF%25AD%25E6%259E%259C%25E7%2584%25B6%25E6%259C%2589%25E9%2597%25AE%25E9%25A2%2598/">我的技术学习物语果然有问题</category>
        </item>
        <item>
            <title>SpringBoot</title>
            <link>https://yww52.com/posts/6f2612a2/</link>
            <guid isPermaLink="true">https://yww52.com/posts/6f2612a2/</guid>
            <pubDate>Thu, 15 Oct 2020 00:00:00 +0000</pubDate>
            <description>学习SpringBoot的笔记</description>
            <content:encoded><![CDATA[<img src="https://img.yww52.com/2020/10/2020-10-15top_img.jpg" alt="SpringBoot" style="max-width:100%;height:auto;margin-bottom:15px;" /><h1>SpringBoot</h1><p>SpringBoot是一个全新开源的轻量级框架。它基于Spring4.0设计，进一步简化了Spring的配置，更加简单的完成整个搭建和开发。</p><h1>微服务架构</h1><p><a href="https://martinfowler.com/articles/microservices.html">https://martinfowler.com/articles/microservices.html</a></p><h1>第一个SpringBoot程序</h1><p>可以去<a href="https://start.spring.io/">官网</a>去创建一个springboot的项目（用idea自带创建也是通过官网创建的），配置什么的自己选择，注意要添加一个Spring Web的依赖，不然不能用作Web开发，然后GENERATE下载项目到本地，用idea导入项目就可以了。</p><p>在项目自带的Application类的同级目录下（一定要同级目录，不然SpringBoot无法扫描到你写的类）创建包和类，现创建一个Hello的类。</p><pre><code class="language-Java">@Controller@ResponseBody//将返回的数据写给浏览器，对象就会返回JSON数据//这两个由@RestController替换，因为这两个注解已经被RestController注解包括了。public class Hello {    @GetMapping(&quot;/hello&quot;)    public String hello(){        return &quot;Hello World!&quot;;    }}</code></pre><p>然后通过Application类来运行，然后打开你的<a href="http://localhost:8080/hello">http://localhost:8080/hello</a>就能看到HelloWorld了。</p><h1>SpringBoot程序详解</h1><p>分析一下我们的第一个SpringBoot程序。</p><h2>Pom.xml</h2><pre><code class="language-xml">&lt;?xml version=&quot;1.0&quot; encoding=&quot;UTF-8&quot;?&gt;&lt;project xmlns=&quot;http://maven.apache.org/POM/4.0.0&quot; xmlns:xsi=&quot;http://www.w3.org/2001/XMLSchema-instance&quot;xsi:schemaLocation=&quot;http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd&quot;&gt;&lt;modelVersion&gt;4.0.0&lt;/modelVersion&gt;&lt;parent&gt;&lt;groupId&gt;org.springframework.boot&lt;/groupId&gt;&lt;artifactId&gt;spring-boot-starter-parent&lt;/artifactId&gt;&lt;version&gt;2.3.4.RELEASE&lt;/version&gt;&lt;relativePath/&gt; &lt;!-- lookup parent from repository --&gt;&lt;/parent&gt;&lt;groupId&gt;com.yw&lt;/groupId&gt;&lt;artifactId&gt;demo&lt;/artifactId&gt;&lt;version&gt;0.0.1-SNAPSHOT&lt;/version&gt;&lt;name&gt;demo&lt;/name&gt;&lt;description&gt;My first springboot demo&lt;/description&gt;&lt;properties&gt;&lt;java.version&gt;1.8&lt;/java.version&gt;&lt;/properties&gt;&lt;dependencies&gt;        &lt;!--Web场景启动器--&gt;&lt;dependency&gt;&lt;groupId&gt;org.springframework.boot&lt;/groupId&gt;&lt;artifactId&gt;spring-boot-starter-web&lt;/artifactId&gt;&lt;/dependency&gt;&lt;/dependencies&gt; &lt;!--打包工具--&gt;&lt;build&gt;&lt;plugins&gt;&lt;plugin&gt;&lt;groupId&gt;org.springframework.boot&lt;/groupId&gt;&lt;artifactId&gt;spring-boot-maven-plugin&lt;/artifactId&gt;&lt;/plugin&gt;&lt;/plugins&gt;&lt;/build&gt;&lt;/project&gt;</code></pre><ol><li><p>父项目</p><pre><code class="language-xml">&lt;!--Springboot的父项目--&gt;&lt;parent&gt;        &lt;groupId&gt;org.springframework.boot&lt;/groupId&gt;        &lt;artifactId&gt;spring-boot-starter-parent&lt;/artifactId&gt;        &lt;version&gt;2.3.4.RELEASE&lt;/version&gt;        &lt;relativePath/&gt; &lt;!-- lookup parent from repository --&gt;&lt;/parent&gt;&lt;!--点进去发现还有一个父项目--&gt;&lt;parent&gt;    &lt;groupId&gt;org.springframework.boot&lt;/groupId&gt;    &lt;artifactId&gt;spring-boot-dependencies&lt;/artifactId&gt;    &lt;version&gt;2.3.4.RELEASE&lt;/version&gt;&lt;/parent&gt;&lt;!--点进去看到有很多properties，这些都是依赖的版本，即这个父项目管理了所有用到的依赖的版本，这就是为什么导入依赖时候不用声明版本号的原因。--&gt;</code></pre></li><li><p>导入的Spring Web的依赖</p><pre><code class="language-Xml">&lt;dependency&gt;    &lt;groupId&gt;org.springframework.boot&lt;/groupId&gt;    &lt;artifactId&gt;spring-boot-starter-web&lt;/artifactId&gt;&lt;/dependency&gt;</code></pre><pre><code class="language-xml">&lt;!--spring-boot-starter-web里的内容--&gt;&lt;?xml version=&quot;1.0&quot; encoding=&quot;UTF-8&quot;?&gt;&lt;project xsi:schemaLocation=&quot;http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd&quot; xmlns=&quot;http://maven.apache.org/POM/4.0.0&quot;    xmlns:xsi=&quot;http://www.w3.org/2001/XMLSchema-instance&quot;&gt;  &lt;!-- This module was also published with a richer model, Gradle metadata,  --&gt;  &lt;!-- which should be used instead. Do not delete the following line which  --&gt;  &lt;!-- is to indicate to Gradle or any Gradle module metadata file consumer  --&gt;  &lt;!-- that they should prefer consuming it instead. --&gt;  &lt;!-- do_not_remove: published-with-gradle-metadata --&gt;  &lt;modelVersion&gt;4.0.0&lt;/modelVersion&gt;  &lt;groupId&gt;org.springframework.boot&lt;/groupId&gt;  &lt;artifactId&gt;spring-boot-starter-web&lt;/artifactId&gt;  &lt;version&gt;2.3.4.RELEASE&lt;/version&gt;  &lt;name&gt;spring-boot-starter-web&lt;/name&gt;  &lt;description&gt;Starter for building web, including RESTful, applications using Spring MVC. Uses Tomcat as the default embedded container&lt;/description&gt;  &lt;url&gt;https://spring.io/projects/spring-boot&lt;/url&gt;  &lt;organization&gt;    &lt;name&gt;Pivotal Software, Inc.&lt;/name&gt;    &lt;url&gt;https://spring.io&lt;/url&gt;  &lt;/organization&gt;  &lt;licenses&gt;    &lt;license&gt;      &lt;name&gt;Apache License, Version 2.0&lt;/name&gt;      &lt;url&gt;https://www.apache.org/licenses/LICENSE-2.0&lt;/url&gt;    &lt;/license&gt;  &lt;/licenses&gt;  &lt;developers&gt;    &lt;developer&gt;      &lt;name&gt;Pivotal&lt;/name&gt;      &lt;email&gt;info@pivotal.io&lt;/email&gt;      &lt;organization&gt;Pivotal Software, Inc.&lt;/organization&gt;      &lt;organizationUrl&gt;https://www.spring.io&lt;/organizationUrl&gt;    &lt;/developer&gt;  &lt;/developers&gt;  &lt;scm&gt;    &lt;connection&gt;scm:git:git://github.com/spring-projects/spring-boot.git&lt;/connection&gt;    &lt;developerConnection&gt;scm:git:ssh://git@github.com/spring-projects/spring-boot.git&lt;/developerConnection&gt;    &lt;url&gt;https://github.com/spring-projects/spring-boot&lt;/url&gt;  &lt;/scm&gt;  &lt;issueManagement&gt;    &lt;system&gt;GitHub&lt;/system&gt;    &lt;url&gt;https://github.com/spring-projects/spring-boot/issues&lt;/url&gt;  &lt;/issueManagement&gt;  &lt;dependencies&gt;    &lt;dependency&gt;      &lt;groupId&gt;org.springframework.boot&lt;/groupId&gt;      &lt;artifactId&gt;spring-boot-starter&lt;/artifactId&gt;      &lt;version&gt;2.3.4.RELEASE&lt;/version&gt;      &lt;scope&gt;compile&lt;/scope&gt;    &lt;/dependency&gt;    &lt;dependency&gt;      &lt;groupId&gt;org.springframework.boot&lt;/groupId&gt;      &lt;artifactId&gt;spring-boot-starter-json&lt;/artifactId&gt;      &lt;version&gt;2.3.4.RELEASE&lt;/version&gt;      &lt;scope&gt;compile&lt;/scope&gt;    &lt;/dependency&gt;    &lt;dependency&gt;      &lt;groupId&gt;org.springframework.boot&lt;/groupId&gt;      &lt;artifactId&gt;spring-boot-starter-tomcat&lt;/artifactId&gt;      &lt;version&gt;2.3.4.RELEASE&lt;/version&gt;      &lt;scope&gt;compile&lt;/scope&gt;    &lt;/dependency&gt;    &lt;dependency&gt;      &lt;groupId&gt;org.springframework&lt;/groupId&gt;      &lt;artifactId&gt;spring-web&lt;/artifactId&gt;      &lt;version&gt;5.2.9.RELEASE&lt;/version&gt;      &lt;scope&gt;compile&lt;/scope&gt;    &lt;/dependency&gt;    &lt;dependency&gt;      &lt;groupId&gt;org.springframework&lt;/groupId&gt;      &lt;artifactId&gt;spring-webmvc&lt;/artifactId&gt;      &lt;version&gt;5.2.9.RELEASE&lt;/version&gt;      &lt;scope&gt;compile&lt;/scope&gt;    &lt;/dependency&gt;  &lt;/dependencies&gt;&lt;/project&gt;</code></pre></li></ol><p>:::note{type=&quot;info&quot;}</p><p>spring-boot-starter,SpringBoot的场景启动器。那什么是场景启动器呢？在SpringBoot框架中，开发者已经帮我们模拟了很多个使用SpringBoot的开发场景，并将这些场景需要的依赖和配置全部写入到这些场景启动器当中。比如我们现在是开发web的场景，我们就导入了这个Spring Web的依赖，主要就是这个spring-boot-starter-web的web场景启动器，里面都是我们开发web需要的依赖以及一些配置。</p><p>:::</p><h2>主程序</h2><pre><code class="language-Java">@SpringBootApplicationpublic class SpringbootApplication {public static void main(String[] args) {SpringApplication.run(SpringbootApplication.class, args);}}</code></pre><blockquote><p>@SpringBootApplication这个注解标注了这是SpringBoot的主配置类，SpringBoot通过这个主配置类的main方法来启动这个SpringBoot的应用。查看里面的源码就会发现这个注解是由很多注解组成的，下面详细分析一下。</p></blockquote><pre><code class="language-Java">@SpringBootConfiguration@EnableAutoConfiguration@ComponentScan(excludeFilters = { @Filter(type = FilterType.CUSTOM, classes = TypeExcludeFilter.class),@Filter(type = FilterType.CUSTOM, classes = AutoConfigurationExcludeFilter.class) })public @interface SpringBootApplication {</code></pre><h3>@SpringBootConfiguration</h3><p>这个注解表示这个类是一个SpringBoot的配置类，点开里面发现还有个**@Configuration**注解，这个注解表示这是个Spring的配置类。</p><pre><code class="language-Java">@Configurationpublic @interface SpringBootConfiguration {</code></pre><p>在点开就发现了这个**@Component**，所以发现这个配置类是容器的一个组件。</p><pre><code class="language-Java">@Componentpublic @interface Configuration {</code></pre><h3>@EnableAutoConfiguration</h3><p>这个注解告诉了SpringBoot要开启自动配置功能。</p><pre><code class="language-Java">@AutoConfigurationPackage@Import(AutoConfigurationImportSelector.class)public @interface EnableAutoConfiguration {</code></pre><p>**@Import(AutoConfigurationImportSelector.class)**这是导入组件的选择器，会给容器导入很多这个场景所需要的所有组件并配置号这些组件。</p><p><strong>@AutoConfigurationPackage</strong>这个注解表示自动配置包，点进去发现有一个这样的注解</p><pre><code class="language-Java">@Import(AutoConfigurationPackages.Registrar.class)</code></pre><p>这个注解是Spring的底层注解，@Import指定了AutoConfigurationPackages.Registrar.class这个类给容器导入一些组件。</p><p><strong>所以这个@EnableAutoConfiguration就是将主配置类同目录下的包与类的所有组件扫描到Spring容器里面，这就是为什么一定要在同目录下创建包与类，因为不同目录就不会扫描得到。</strong></p><h2>Resource文件夹</h2><ol><li><p>static文件夹保存静态资源的文件夹，比如js,css文件等。</p></li><li><p>templates文件夹保存模板页面的文件夹，因为SpringBoot默认jar包嵌入式的Tomcat，所以不支持JSP页面，所以需要用到模板引擎来配置页面。</p></li><li><p>application.properties（application.yml）SpringBoot的配置文件（名字是固定的不能修改）,因为SpringBoot已经帮我们配置好了很多配置，想要修改这些默认配置就需要在这个配置文件自己修改，这个配置文件由两种形式，properties和yml形式。</p><blockquote><ol><li>application.properties的格式是    key=value<br>子配置就用点，比如service下的port配置就表示为service.port=value</li><li>application.yml的格式是    key: value    注意有空格，因为yml文件对空格很敏感， 具体语法可参照<a href="https://yww52.com/2020/10/20/yml%E8%AF%AD%E6%B3%95/">YAML语法</a>,之后就用yaml的这种格式来写配置文件</li></ol></blockquote></li></ol><h1>配置文件值的注入</h1><p>首先先写个需要注入值的类。</p><pre><code class="language-Java">@Component@ConfigurationProperties(prefix = &quot;user&quot;)public class User {    private String name;    private int age;    private Date birth;    private Boolean happy;    private Map&lt;String,Object&gt; family;    private List&lt;Object&gt; hobby;    private Dog dog;    public String getName() {        return name;    }    public void setName(String name) {        this.name = name;    }    public int getAge() {        return age;    }    public void setAge(int age) {        this.age = age;    }    public Date getBirth() {        return birth;    }    public void setBirth(Date birth) {        this.birth = birth;    }    public Boolean getHappy() {        return happy;    }    public void setHappy(Boolean happy) {        this.happy = happy;    }    public Map&lt;String, Object&gt; getFamily() {        return family;    }    public void setFamily(Map&lt;String, Object&gt; family) {        this.family = family;    }    public List&lt;Object&gt; getHobby() {        return hobby;    }    public void setHobby(List&lt;Object&gt; hobby) {        this.hobby = hobby;    }    public Dog getDog() {        return dog;    }    public void setDog(Dog dog) {        this.dog = dog;    }    @Override    public String toString() {        return &quot;User{&quot; +                &quot;name=&#39;&quot; + name + &#39;\&#39;&#39; +                &quot;, age=&quot; + age +                &quot;, birth=&quot; + birth +                &quot;, happy=&quot; + happy +                &quot;, hobby=&quot; + hobby +                &quot;, family=&quot; + family +                &quot;, dog=&quot; + dog +                &#39;}&#39;;    }}</code></pre><pre><code class="language-Java">@Componentpublic class Dog {    private String name;    private int age;    public String getName() {        return name;    }    public void setName(String name) {        this.name = name;    }    public int getAge() {        return age;    }    public void setAge(int age) {        this.age = age;    }    @Override    public String toString() {        return &quot;Dog{&quot; +                &quot;name=&#39;&quot; + name + &#39;\&#39;&#39; +                &quot;, age=&quot; + age +                &#39;}&#39;;    }}</code></pre><p>:::note{type=&quot;info&quot;}</p><ol><li>@Component表示这是spring的组件，会被注入到spring的容器中进行管理<ol start="2"><li>@ConfigurationProperties(prefix = &quot;user&quot;)表示绑定配置文件中的user的配置，这样就可以进行值的注入了</li></ol></li></ol><p>:::</p><p>然后导入一个配置文件处理器的依赖（不导入会爆红虽然没影响），可以去配置文件注入值了。</p><pre><code class="language-xml">&lt;dependency&gt;&lt;groupId&gt;org.springframework.boot&lt;/groupId&gt;&lt;artifactId&gt;spring-boot-configuration-processor&lt;/artifactId&gt;&lt;/dependency&gt;</code></pre><p>这样就可以成功的将值注入了，可以去测试类进行一下测试。</p><pre><code class="language-Java">@SpringBootTestclass DemoApplicationTests {@AutowiredUser user;//其实不一定要与配置类中的名称一致，user01也能读取出值@Testvoid contextLoads() {System.out.println(user);}}</code></pre><p> 得到答案</p><pre><code>User{name=&#39;Yw&#39;, age=20, birth=Wed Oct 21 00:00:00 CST 2020, happy=true, hobby=[play, game], family={father=yww1, mother=yww2}, dog=Dog{name=&#39;wwangwang&#39;, age=3}}</code></pre><h1>导入文件资源</h1><h2>@PropertySource</h2><p>在上述的示例当中，要是总是把注入的值都加进全局的配置文件中就会显得很乱，所以可以新建一个文件在通过@PropertySource标签导入进去也可以实现值的注入，这样application.properties中就可以不用写user的值了，值都写在user.properties即可。</p><pre><code class="language-Java">@Component@ConfigurationProperties(prefix = &quot;user&quot;)@PropertySource(value = {&quot;classpath:user.properties&quot;})public class User {</code></pre><p>:::note{type=&quot;info&quot;}</p><p>注意@PropertySource这个标签导入的是properties的文件，yml文件会出问题。</p><p>:::</p><h2>@ImportResource</h2><p>这个标签是用来导入spring的配置文件到springBoot中的（即xml格式）</p><pre><code class="language-Java">@ImportResource(locations = {&quot;classpath:beans.xml&quot;})</code></pre><h2>@Bean</h2><p>这个标签的功能与@ImportResource类似，也是为了导入配置，给容器添加一个组件的。比如写一个配置类。</p><pre><code class="language-Java">@Configuration//表示这个类是一个配置类，就像是一个spring的配置文件public class Config {    @Bean//将方法的返回值注入到容器之中，类似spring配置中    public Hello hello(){        return new Hello();    }}</code></pre><p>:::note{type=&quot;info&quot;}</p><ol><li>@Configuration 表示这个类是一个配置类，就像是一个spring的配置文件<ol start="2"><li>@Bean 将方法的返回值注入到容器之中，类似spring配置中<beans></beans>注入bean的操作，方法名就是这个bean的id。</li></ol></li></ol><p>:::</p><h1>Profile</h1><h2>properties的多文档</h2><p>首先就是主要的配置文件<code>application.properties</code>，创建多文档的方式就是按照<code>application-[name].properties</code>的形式命名创建在resources目录下。就比如我创建了一个测试环境的配置，命名为<code>application-dev.properties</code></p><h2>yaml的多文档</h2><p>这个就很方便了，直接在<code>application.yml</code>中就可以创建多个文档配置了。只要通过<code>---</code>就能将配置文件分成不同的文档配置，通过<code>spring.profiles: [name]</code>这个配置就能为这个文档配置取个名字了。</p><pre><code class="language-yaml">server:  port: 8085---server:  port: 8086spring:  profiles: dev---</code></pre><h2>激活指定的Profile</h2><h3>properties</h3><p>在主配置文件，即<code>application.properties</code>下增加这个配置，就可以激活相应名字的文档配置文件。</p><pre><code class="language-properties">spring.profiles.active=dev</code></pre><h3>yaml</h3><p>yaml形式也是通过这个配置spring.profiles.active=dev，就能激活相应名字的文档配置文件。</p><pre><code class="language-yaml">server:  port: 8085spring:  profiles:    active: dev---server:  port: 8086spring:  profiles: dev</code></pre><h3>共同的激活方式</h3><ol><li><p>命令行比如在打了jar包之后，在命令行运行jar包的时候增加配置就能激活指定配置文件生效。</p><pre><code class="language-cmd">java -jar ---.jar --spring.profiles.active=dev</code></pre></li><li><p>虚拟机参数修改虚拟机参数也可以修改。</p><pre><code>-Dspring.profiles.active=dev</code></pre></li></ol><h1>配置文件的优先级</h1><p>配置文件的优先级由高到低分别为：</p><ol><li>项目路径下的config文件夹中的配置文件</li><li>项目路径下的配置文件</li><li>资源路径下的config文件夹中的配置文件</li><li>资源路径下的配置文件</li></ol><p>springboot会通过优先级来加载配置文件，要是有不止一个配置文件，springboot也是会全部加载这些配置文件。对于设置了相同的配置，优先级高的配置文件设置的就会覆盖掉优先级低的配置文件的设置。就比如每个配置文件都设置了不同的打开端口，就会以优先级高的配置文件的为准。其他不冲突的配置也是生效的。</p>]]></content:encoded>
            <category domain="https://yww52.com/categories/%25E6%2588%2591%25E7%259A%2584%25E6%258A%2580%25E6%259C%25AF%25E5%25AD%25A6%25E4%25B9%25A0%25E7%2589%25A9%25E8%25AF%25AD%25E6%259E%259C%25E7%2584%25B6%25E6%259C%2589%25E9%2597%25AE%25E9%25A2%2598/">我的技术学习物语果然有问题</category>
        </item>
        <item>
            <title>第十一届蓝桥杯第二次模拟赛</title>
            <link>https://yww52.com/posts/16bca3f0/</link>
            <guid isPermaLink="true">https://yww52.com/posts/16bca3f0/</guid>
            <pubDate>Thu, 15 Oct 2020 00:00:00 +0000</pubDate>
            <description>第十一届蓝桥杯第二次模拟赛</description>
            <content:encoded><![CDATA[<img src="https://yww52.com/default_cover.jpg" alt="第十一届蓝桥杯第二次模拟赛" style="max-width:100%;height:auto;margin-bottom:15px;" /><h1>12.5MB</h1><pre><code>【问题描述】在计算机存储中，12.5MB是多少字节？【答案提交】这是一道结果填空的题，你只需要算出结果后提交即可。本题的结果为一个整数，在提交答案时只填写这个整数，填写多余的内容将无法得分。</code></pre><p>第一题没什么难度就是计算，1MB=1024KB，1KB=1024Byte，所以直接计算器，答案为<code>13,107,200</code></p><h1>最多边数</h1><pre><code>【问题描述】一个包含有2019个结点的有向图，最多包含多少条边？（不允许有重边）【答案提交】这是一道结果填空的题，你只需要算出结果后提交即可。本题的结果为一个整数，在提交答案时只填写这个整数，填写多余的内容将无法得分。</code></pre><p>边数最多，那就是有向完全图了，所以根据数据结构的知识，含有n个节点的有向完全图，边数为n*(n-1)。所以答案为<code>4074342</code>。</p><h1>单词重排</h1><pre><code>【问题描述】将LANQIAO中的字母重新排列，可以得到不同的单词，如LANQIAO、AAILNOQ等，注意这7个字母都要被用上，单词不一定有具体的英文意义。请问，总共能排列如多少个不同的单词。【答案提交】这是一道结果填空的题，你只需要算出结果后提交即可。本题的结果为一个整数，在提交答案时只填写这个整数，填写多余的内容将无法得分。</code></pre><p>这明显就是全排序的题目，所以用全排序的算法就可以得出结果。</p><h2>解法一</h2><p>我一开始看到这填空题才是七个数的全排列，就直接笔算了，全排列后的种数为<code>7！</code>。因为题目里的这七个字母有两个是一样的，所以全排列后除2就可以得到答案<code>2520</code>。</p><h2>解法二</h2><p>第二种是在网上看到的灵感，就是先实现全排列，然后加入到set集合里去重，最后也可以得到正确答案。</p><pre><code class="language-Java">import java.util.HashSet;import java.util.Set;public class Solution {    static char[] arr = &quot;LANQIAO&quot;.toCharArray();    static Set&lt;String&gt; res = new HashSet&lt;&gt;();//start表示开始的位置，end表示最后的位置    public static void Permutation(char[] arr,int start,int end){        if (end &lt;= 1) {            return;        }        if (start == end) {            res.add(new String(arr));        }        else{            for (int i = 0; i &lt;= end; i++) {                swapchar(arr,i,start);                Permutation(arr,start+1,end);                swapchar(arr,i,start);            }        }    }    public static void swapchar(char[] arr,int a,int b){        char t = arr[a];        arr[a] = arr[b];        arr[b] = t;    }    public static void main(String[] args) {        Permutation(arr, 0, 6);        System.out.println(res.size());    }}</code></pre><h1>括号序列</h1><pre><code>【问题描述】由1对括号，可以组成一种合法括号序列：()。由2对括号，可以组成两种合法括号序列：()()、(())。由4对括号组成的合法括号序列一共有多少种？【答案提交】这是一道结果填空的题，你只需要算出结果后提交即可。本题的结果为一个整数，在提交答案时只填写这个整数，填写多余的内容将无法得分。</code></pre><h2>解法一</h2><p>这道题我一开始看的想法就是左括号和右括号看成0和1，然后直接枚举暴力破解，因为第一个肯定是左括号，最后一个肯定是右括号，所以输出了64种，一种种排除几分钟也能得到正确答案<code>14</code>种。</p><blockquote><p>这种方法比较蠢，上了几个for循环，我就不放代码了。</p></blockquote><h2>解法二</h2><p>在网上找到的一种方法。这其实是卡特兰数的一种应用（可以去百度了解一下） 。</p><p>S(n) = S(0)S(n-1) + S(1)S(n-2) + ..... + S(n-1)S(0)</p><p>所以就可以直接套公式了。</p><ul><li>当有0对括号的时候，有1种序列</li><li>当有1对括号的时候，有1种序列</li><li>当有2对括号的时候，有2种序列</li><li>当有3对括号的时候，有5种序列</li></ul><p>所以代进去就能得到答案为<code>14</code>，代码如下。</p><pre><code class="language-Java">public class Solution {    public static void main(String[] args) {        System.out.println(Catalan(4));    }    public static int Catalan(int k){        if(k&lt;=1){            return 1;        }        int[] res = new int[k+1];        res[0] = 1;        res[1] = 1;        for (int i = 2; i &lt;= k; i++) {            for (int j = 0; j &lt; i; j++) {                res[i] = res[i] + (res[j] * res[i - j - 1]);            }        }        return res[k];    }}</code></pre><h1>反倍数</h1><pre><code>【问题描述】给定三个整数 a, b, c，如果一个整数既不是 a 的整数倍也不是 b 的整数倍还不是 c 的整数倍，则这个数称为反倍数。请问在 1 至 n 中有多少个反倍数。【输入格式】输入的第一行包含一个整数 n。第二行包含三个整数 a, b, c，相邻两个数之间用一个空格分隔。【输出格式】输出一行包含一个整数，表示答案。【样例输入】302 3 6【样例输出】10【样例说明】以下这些数满足要求：1, 5, 7, 11, 13, 17, 19, 23, 25, 29。【评测用例规模与约定】对于 40% 的评测用例，1 &lt;= n &lt;= 10000。对于 80% 的评测用例，1 &lt;= n &lt;= 100000。对于所有评测用例，1 &lt;= n &lt;= 1000000，1 &lt;= a &lt;= n，1 &lt;= b &lt;= n，1 &lt;= c &lt;= n。</code></pre><p>这题也没什么技巧，最大的数据也只有1000000，所以一个for循环加个if判断语句就可以暴力破解了。</p><pre><code class="language-Java">public class Solution {    public static void main(String[] args) {        int res = 0;        Scanner scanner = new Scanner(System.in);        int n = scanner.nextInt();        int a = scanner.nextInt();        int b = scanner.nextInt();        int c = scanner.nextInt();        for (int i = 1; i &lt;= n; i++) {            if(i%a!=0&amp;&amp;i%b!=0&amp;&amp;i%c!=0) {                res++;            }        }        System.out.println(res);    }}</code></pre><h1>凯撒加密</h1><pre><code>【问题描述】给定一个单词，请使用凯撒密码将这个单词加密。凯撒密码是一种替换加密的技术，单词中的所有字母都在字母表上向后偏移3位后被替换成密文。即a变为d，b变为e，...，w变为z，x变为a，y变为b，z变为c。例如，lanqiao会变成odqtldr。【输入格式】输入一行，包含一个单词，单词中只包含小写英文字母。【输出格式】输出一行，表示加密后的密文。【样例输入】lanqiao【样例输出】odqtldr【评测用例规模与约定】对于所有评测用例，单词中的字母个数不超过100。</code></pre><p>凯撒密码从c敲到Java，常见的题，我不是太清楚这个单词的结构，我默认它是一长串连续的字母组成的字符串，没有空格。</p><pre><code class="language-Java">public class Solution {    public static void main(String[] args) {        Scanner scanner = new Scanner(System.in);        String str = scanner.nextLine();        char[] arr = str.toCharArray();        for(char c:arr) {            if (c == &#39;x&#39;) {                System.out.print(&#39;a&#39;);            }            if (c == &#39;y&#39;) {                System.out.println(&#39;b&#39;);            }            if (c == &#39;z&#39;) {                System.out.println(&#39;c&#39;);            }            System.out.print((char) (c + 3));        }    }}</code></pre><h1>螺旋</h1><pre><code>【问题描述】对于一个 n 行 m 列的表格，我们可以使用螺旋的方式给表格依次填上正整数，我们称填好的表格为一个螺旋矩阵。例如，一个 4 行 5 列的螺旋矩阵如下：1 2 3 4 514 15 16 17 613 20 19 18 712 11 10 9 8【输入格式】输入的第一行包含两个整数 n, m，分别表示螺旋矩阵的行数和列数。第二行包含两个整数 r, c，表示要求的行号和列号。【输出格式】输出一个整数，表示螺旋矩阵中第 r 行第 c 列的元素的值。【样例输入】4 52 2【样例输出】15【评测用例规模与约定】对于 30% 的评测用例，2 &lt;= n, m &lt;= 20。对于 70% 的评测用例，2 &lt;= n, m &lt;= 100。对于所有评测用例，2 &lt;= n, m &lt;= 1000，1 &lt;= r &lt;= n，1 &lt;= c &lt;= m。</code></pre><p>这个题目难点就是求出这个数组，我们可以螺旋递增来求出这个数组，然后再输出r行c列的值就好了。</p><pre><code class="language-Java">public class Solution {    public static void main(String[] args) {        Scanner scanner = new Scanner(System.in);        int k = 1,x=0,y = 0;        int n = scanner.nextInt();        int m = scanner.nextInt();        int r = scanner.nextInt();        int c = scanner.nextInt();        int[][] arr = new int[n][m];        arr[0][0] = k;        while(n*m&gt;k){            //向右循环递增            while (y + 1 &lt; m &amp;&amp; arr[x][y + 1]==0) {                arr[x][y+1] = ++k;                y++;            }            //向下循环递增            while (x + 1 &lt; n &amp;&amp; arr[x+1][y]==0) {                arr[x+1][y] = ++k;                x++;            }            //向左循环递增            while (y-1 &gt;= 0 &amp;&amp; arr[x][y-1]==0) {                arr[x][y-1] = ++k;                y--;            }            //向上循环递增            while (x-1 &gt;= 0 &amp;&amp; arr[x-1][y]==0) {                arr[x-1][y] = ++k;                x--;            }        }            System.out.println(arr[r-1][c-1]);    }}</code></pre><h1>摆动序列</h1><h1>通电</h1><h1>植树</h1>]]></content:encoded>
            <category domain="https://yww52.com/categories/%25E6%2588%2591%25E7%259A%2584%25E6%258A%2580%25E6%259C%25AF%25E5%25AD%25A6%25E4%25B9%25A0%25E7%2589%25A9%25E8%25AF%25AD%25E6%259E%259C%25E7%2584%25B6%25E6%259C%2589%25E9%2597%25AE%25E9%25A2%2598/">我的技术学习物语果然有问题</category>
        </item>
        <item>
            <title>SpringMVC执行原理</title>
            <link>https://yww52.com/posts/7a197da5/</link>
            <guid isPermaLink="true">https://yww52.com/posts/7a197da5/</guid>
            <pubDate>Tue, 22 Sep 2020 00:00:00 +0000</pubDate>
            <description>SpringMVC的执行原理</description>
            <content:encoded><![CDATA[<img src="https://img.yww52.com/2020/9/2020-9-22/top_img.jpg" alt="SpringMVC执行原理" style="max-width:100%;height:auto;margin-bottom:15px;" /><h1>springmvc的组件</h1><ol><li><p>前端控制器（DispatcherServlet）</p><p>前端控制器就是用来处理请求的中心，接收请求和响应结果都是由它控制的，也算是springmvc里的中心枢纽。</p></li><li><p>处理器映射器（HandlerMapping）</p><p>处理器映射器根据请求里的url来查找相应的处理器，即用来寻找处理器的。</p></li><li><p>处理器（Handler）</p><p>处理器是进行业务操作的。</p></li><li><p>处理器适配器（HandlerAdapter）</p><p>会把处理器包装成适配器，这样就可以支持多种类型的处理器，类比笔记本的适配器（适配器模式的应用）</p></li><li><p>视图解析器（ViewResovler）</p><p>即对返回来的视图进行解析，将代码解析成为对应的视图</p></li></ol><h1>工作流程</h1><p><img src="https://img.yww52.com/2020/9/2020-9-22/img1.png" alt="工作流程"></p><h1>具体分析</h1><ol><li>用户发送请求，即在浏览器上输入url，DispatcherServlet会接收请求。</li><li>DispatcherServlet收到请求后就会调用HandlerMapping来寻找需要的Handler。</li><li>HandlerMapping找到Handler后就会返回给DispatcherServlet。</li><li>前端控制器DispatcherServlet收到Handler后就会调用HandlerAdapter对Handler进行处理。</li><li>HandlerAdapter经过适配调用具体的Handler(Controller，也叫后端控制器)。</li><li>执行完毕后就会将一个ModelAndView返回给HandlerAdapter。</li><li>HandlerAdapter将ModelAndView返回给DispatcherServlet。</li><li>DispatcherServlet将调用ViewReslover来解析这个ModelAndView。</li><li>ViewReslover解析ModelAndView后返回具体View给DispatcherServlet。</li><li>DispatcherServlet根据这个view就会渲染出所需要的页面。</li><li>DispatcherServlet将这个页面返回给用户，用户获取结果。</li></ol><h1>对于hellospringmvc的详解</h1><ol><li><p>首先在web.xml里注册的就是DispatcherServlet前端控制器，用户发出的请求（即输入url）先到的是这里。</p><p>::: info这里DispatcherServlet收到请求就会获取有用的信息，就是将url拆分。就比如我输入的url，<a href="https://localhost:8080/helloSpringmvc/hello">https://localhost:8080/helloSpringmvc/hello</a>    就会拆分为三部分</p><ol><li>localhost:8080，这是tomcat开放的端口。</li><li>helloSpringmvc，这个就是访问的你的项目名（可以自己更改），即tomcat的webapps下的helloSpringmvc文件夹。</li><li>hello，即为控制器的id ,发出请求输入ur就是为了找到这个控制器。</li></ol><p>:::</p></li><li><p>然后经过关联的配置文件，即resource下的springmvc-servlet.xml。</p><pre><code class="language-xml">&lt;servlet&gt;    &lt;servlet-name&gt;springmvc&lt;/servlet-name&gt;    &lt;servlet-class&gt;org.springframework.web.servlet.DispatcherServlet&lt;/servlet-class&gt;    &lt;init-param&gt;        &lt;param-name&gt;contextConfigLocation&lt;/param-name&gt;        &lt;param-value&gt;classpath:springmvc-servlet.xml&lt;/param-value&gt;    &lt;/init-param&gt;    &lt;load-on-startup&gt;1&lt;/load-on-startup&gt;&lt;/servlet&gt;&lt;servlet-mapping&gt;    &lt;!--匹配到输入的url--&gt;        &lt;servlet-name&gt;springmvc&lt;/servlet-name&gt;        &lt;url-pattern&gt;/&lt;/url-pattern&gt;&lt;/servlet-mapping&gt;</code></pre></li><li><p>springmvc-servlet.xml先是注册了HandlerMapping处理器映射器和HandlerAdapter处理器适配器，springmvc自动帮你调用这两个组件。</p><pre><code class="language-xml">  &lt;bean class=&quot;org.springframework.web.servlet.handler.BeanNameUrlHandlerMapping&quot;/&gt;  &lt;bean class=&quot;org.springframework.web.servlet.handler.BeanNameUrlHandlerMapping&quot;/&gt;</code></pre></li><li><p>在springmvc-servlet.xml注册的Handler控制器,这样DispatcherServlet就可以通过这里找到这个hello控制器，然后DispatcherServlet将该控制器交给HandlerAdapter进行处理该Handler所包含的业务逻辑，这里就是HelloController类里的业务操作。</p><pre><code class="language-xml">                 &lt;!--Handler--&gt;    &lt;bean id=&quot;hello&quot; class=&quot;com.chen.controller.HelloController&quot;/&gt;</code></pre></li><li><p>HelloController中调用了Controller接口，即可以理解这就是mvc架构中Controller层，在这个类进行我们的业务逻辑操作，因为在springmvc中Control层的数据要交给DispatcherServlet处理的，所以返回了一个包装好的ModelAndView，Model存储数据，View存储视图。</p><pre><code class="language-Java">public class HelloController implements Controller {    public ModelAndView handleRequest(HttpServletRequest request, HttpServletResponse response)        throws Exception {                    //创建一个ModelAndView对象                    ModelAndView mv = new ModelAndView();                    //封装所需要的数据到ModelAndView的对象中                    mv.addObject(&quot;msg&quot;,&quot;HelloSpringMVC!&quot;);                    //封装要跳转的视图，放在ModelAndView中                    mv.setViewName(&quot;hello&quot;);     //   /WEB-INF/jsp/hello.jsp                    return mv;            }}</code></pre></li><li><p>DispatcherServlet收到包装好的ModelAndView后，通过注册的视图解析器进行解析渲染视图。</p><pre><code class="language-xml">&lt;bean class=&quot;org.springframework.web.servlet.view.InternalResourceViewResolver&quot;      id=&quot;InternalResourceViewResolver&quot;&gt;        &lt;!--前缀--&gt;        &lt;property name=&quot;prefix&quot; value=&quot;/WEB-INF/jsp/&quot;/&gt;        &lt;!--后缀--&gt;        &lt;property name=&quot;suffix&quot; value=&quot;.jsp&quot;/&gt;&lt;/bean&gt;</code></pre><p>::: info这里就是对控制器进行拼接，比如hello控制器，加上前缀与后缀就成为了/WEB-INF/jsp/hello.jsp，然后就会在web目录下找到该视图进行跳转。</p><p>:::</p></li></ol><p>:::note{type=&quot;success&quot;}</p><p>结合springmvc中第一个springmvc程序来看。由此可以看出springmvc帮我们做了很多事情，我们只需要配置好那几个重要的组件，然后编写业务逻辑和视图就能完成对于一个控制器的工作。</p><p>:::</p><h1>参考文章</h1><ol><li><a href="https://www.cnblogs.com/xiaoxi/p/6164383.html">https://www.cnblogs.com/xiaoxi/p/6164383.html</a></li></ol>]]></content:encoded>
            <category domain="https://yww52.com/categories/%25E6%2588%2591%25E7%259A%2584%25E6%258A%2580%25E6%259C%25AF%25E5%25AD%25A6%25E4%25B9%25A0%25E7%2589%25A9%25E8%25AF%25AD%25E6%259E%259C%25E7%2584%25B6%25E6%259C%2589%25E9%2597%25AE%25E9%25A2%2598/">我的技术学习物语果然有问题</category>
        </item>
        <item>
            <title>IntelliJ IDEA的优化配置</title>
            <link>https://yww52.com/posts/dcaabd92/</link>
            <guid isPermaLink="true">https://yww52.com/posts/dcaabd92/</guid>
            <pubDate>Sat, 11 Jul 2020 00:00:00 +0000</pubDate>
            <description>将IDEA配置好可以很大程度的提升你的学习体验。</description>
            <content:encoded><![CDATA[<img src="https://img.yww52.com/2020/7/2020-7-11/top_img.jpg" alt="IntelliJ IDEA的优化配置" style="max-width:100%;height:auto;margin-bottom:15px;" /><h1>前言</h1><p>闲着没事升级了一下我的idea，现在版本为2020.1.3，顺便记录一下idea的配置与优化，方便以后再改。</p><h1>主题方面</h1><p>一个好的主题会让你喜欢上idea，所以可以去换一个喜欢的主题，体验超好。网上有很多去下载就好了，我就不放地址了。点击下载一般都会下一个jar包，随便找个地方放就好。</p><p>然后在idea里的File -&gt; Manage IDE settings -&gt; import settings,把jar包导进去重启就好了。</p><h1>设置优化</h1><ol><li><p>idea主色配置File -&gt; Settings -&gt; Appearance &amp; Behavior -&gt; Appearance<img src="https://img.yww52.com/2020/7/2020-7-11/img1.png" alt=""></p><blockquote><p>这个看个人喜欢了，我用多了还是深色的好。</p></blockquote></li><li><p>自动导包 File -&gt; Settings -&gt; Editor -&gt; General -&gt; Auto Import <img src="https://img.yww52.com/2020/7/2020-7-11/img2.png" alt=""></p><blockquote><p>这个就不用多说了吧，自动导包还是很方便的。</p></blockquote></li><li><p>字体缩放File -&gt; Settings -&gt; Editor -&gt; General，勾选第一个选项。<img src="https://img.yww52.com/2020/7/2020-7-11/img3.png" alt=""></p><blockquote><p>然后按着CTRL+滑动滚轮就可以控制字体大小了，很好用的。</p></blockquote></li><li><p>大小写不敏感File -&gt; Settings -&gt; Editor -&gt; General -&gt; Code Completion，取消第一个的勾选。 <img src="https://img.yww52.com/2020/7/2020-7-11/img4.png" alt=""></p><blockquote><p>这个就会让你在打错大小写的情况下还会提醒你。</p></blockquote></li><li><p>设置背景图片File -&gt; Settings -&gt; Appearance &amp; Behavior -&gt; Appearance<img src="https://img.yww52.com/2020/7/2020-7-11/img5.png" alt=""></p></li><li><p>取消自动打开项目File -&gt; Settings -&gt; Appearance &amp; Behavior -&gt; System Settings，取消第一个勾选<img src="https://img.yww52.com/2020/7/2020-7-11/img6.png" alt=""></p><blockquote><p>取消了之后就不会每次打开idea就会进入到上次关闭的项目了，当然这个看个人喜好。</p></blockquote></li><li><p>显示方法分隔线和行号File -&gt; Settings -&gt; Editor -&gt; General -&gt; Appearance,勾选上选项。<img src="https://img.yww52.com/2020/7/2020-7-11/img7.png" alt=""></p><blockquote><p>这个会让方法与方法之间出现分隔线，左边出现行号。</p></blockquote></li><li><p>快捷键更换File -&gt; Settings -&gt; Keymap<img src="https://img.yww52.com/2020/7/2020-7-11/img8.png" alt=""></p><blockquote><p>有些人习惯了Eclipse的快捷键，从这里就可以改成Eclipse的，下面的快捷键也可以自行修改。</p></blockquote></li><li><p>设置方法模板File -&gt; Settings -&gt; Editor -&gt; Live Templates<img src="https://img.yww52.com/2020/7/2020-7-11/img9.png" alt=""></p><blockquote><p>方法模板可以在下面设置，不过比较常用的就是psvm和sout生成主方法和输出语句，第一个选项就是输入了sout之后点空格就会生成模板，我比较喜欢空格，自行选择。</p></blockquote></li><li><p>类模板的设置File -&gt; Settings -&gt; Editor -&gt; File and Code Templates -&gt; Class</p><p><img src="https://img.yww52.com/2020/7/2020-7-11/img10.png" alt=""></p><blockquote><p>这段注释就是生成类自动帮你生成的模板，可以自行修改。</p></blockquote></li></ol><h1>Maven环境配置</h1><p>File -&gt; Settings -&gt; Build, Execution, Deployment -&gt; Build Tools -&gt; Maven</p><p><img src="https://img.yww52.com/2020/7/2020-7-11/img11.png" alt=""></p><blockquote><p>第一个是maven所在位置，第二个是maven里的配置文件setting.xml的位置，第三个是maven-repomaven里的仓库文件夹的位置。</p></blockquote><h1>插件</h1><p>用idea就不能少了插件，插件可以让你的体验在上升一个档次哦。</p><h2>插件安装方法</h2><ol><li><p>在idea上直接安装File -&gt; Settings -&gt; Plugins，直接搜索名字就好了。有时候有些插件会下载很慢，可以直接去官网下载。</p></li><li><p>官网下载离线插件官网地址----&gt;<a href="https://plugins.jetbrains.com/">https://plugins.jetbrains.com/</a><img src="https://img.yww52.com/2020/7/2020-7-11/img12.png" alt=""></p><p>下载之后点这里导入插件就好了。</p></li></ol><h2>插件推荐</h2><ol><li><p>CodeGlance安装了这个插件就会在idea右侧生成一个代码地图，用过Vscode都应该知道这个的好处。</p></li><li><p>Translation顾名思义，一个翻译插件，可以帮你在IDE里翻译源码和控制台的一些不会的英文语句，挺方便的。</p></li><li><p>Rainbow Brackets彩虹括号，让你的每个括号都带上颜色，一对括号一个颜色，好看又能让你快速分辨。还有更多用法可参照<a href="https://github.com/izhangzhihao/intellij-rainbow-brackets">官方文档</a></p></li><li><p>Chinese</p><p>其实这个我不用，idea中文插件，用中文没得灵魂。</p></li><li><p>Grep Console</p><p>日志配色。</p></li><li><p>Alibaba Java Coding Guidelines阿里巴巴代码规范，提醒你不规范的地方</p></li></ol>]]></content:encoded>
            <category domain="https://yww52.com/categories/%25E6%2588%2591%25E7%259A%2584%25E6%258A%2580%25E6%259C%25AF%25E5%25AD%25A6%25E4%25B9%25A0%25E7%2589%25A9%25E8%25AF%25AD%25E6%259E%259C%25E7%2584%25B6%25E6%259C%2589%25E9%2597%25AE%25E9%25A2%2598/">我的技术学习物语果然有问题</category>
        </item>
        <item>
            <title>SpringMVC</title>
            <link>https://yww52.com/posts/ae0f95e0/</link>
            <guid isPermaLink="true">https://yww52.com/posts/ae0f95e0/</guid>
            <pubDate>Tue, 07 Jul 2020 00:00:00 +0000</pubDate>
            <description>SpringMVC是一个基于MVC架构的用来简化web应用程序开发的应用开发框架。</description>
            <content:encoded><![CDATA[<img src="https://img.yww52.com/2020/7/2020-7-7/top_img.jpg" alt="SpringMVC" style="max-width:100%;height:auto;margin-bottom:15px;" /><h1>什么是SpringMVC</h1><p>Spring MVC是一个基于MVC架构的用来简化web应用程序开发的应用开发框架，它是Spring的一部分，它和Struts2一样都属于表现层的框架。</p><blockquote><p>MVC可参考之前的文章<a href="https://yww52.com/2020/06/06/JSP/#MVC">mvc架构</a></p></blockquote><h1>第一个SpringMVC程序</h1><ol><li><p>新建一个maven项目，取个名字一路next即可。</p></li><li><p>添加web的支持<img src="https://img.yww52.com/2020/7/2020-7-7/img1.png" alt="1"><img src="https://img.yww52.com/2020/7/2020-7-7/img2.png" alt="2"></p></li><li><p>在pom.xml导入依赖</p><pre><code class="language-xml">&lt;dependencies&gt;       &lt;!--spring-webmvc--&gt;       &lt;dependency&gt;            &lt;groupId&gt;org.springframework&lt;/groupId&gt;            &lt;artifactId&gt;spring-webmvc&lt;/artifactId&gt;            &lt;version&gt;5.2.6.RELEASE&lt;/version&gt;       &lt;/dependency&gt;&lt;/dependencies&gt;</code></pre></li><li><p>在resources下创建配置springmvc的配置文件，我这里是springmvc-servlet.xml</p><pre><code class="language-xml">&lt;?xml version=&quot;1.0&quot; encoding=&quot;UTF-8&quot;?&gt;&lt;beans xmlns=&quot;http://www.springframework.org/schema/beans&quot;       xmlns:xsi=&quot;http://www.w3.org/2001/XMLSchema-instance&quot;       xsi:schemaLocation=&quot;http://www.springframework.org/schema/beans       http://www.springframework.org/schema/beans/spring-beans.xsd        http://www.springframework.org/schema/beans &quot;&gt;    &lt;bean class=&quot;org.springframework.web.servlet.handler.BeanNameUrlHandlerMapping&quot;/&gt;    &lt;bean class=&quot;org.springframework.web.servlet.mvc.SimpleControllerHandlerAdapter&quot;/&gt;    &lt;!--注册并配置视图解析器--&gt;    &lt;bean class=&quot;org.springframework.web.servlet.view.InternalResourceViewResolver&quot;          id=&quot;InternalResourceViewResolver&quot;&gt;        &lt;!--前缀--&gt;        &lt;property name=&quot;prefix&quot; value=&quot;/WEB-INF/jsp/&quot;/&gt;        &lt;!--后缀--&gt;        &lt;property name=&quot;suffix&quot; value=&quot;.jsp&quot;/&gt;    &lt;/bean&gt;    &lt;!--hello的Handler，匹配到创建的HelloController--&gt;    &lt;bean id=&quot;/hello&quot; class=&quot;com.chen.HelloController&quot;/&gt;&lt;/beans&gt;</code></pre></li><li><p>配置web.xml</p><pre><code class="language-xml">&lt;?xml version=&quot;1.0&quot; encoding=&quot;UTF-8&quot;?&gt;&lt;web-app xmlns=&quot;http://xmlns.jcp.org/xml/ns/javaee&quot;         xmlns:xsi=&quot;http://www.w3.org/2001/XMLSchema-instance&quot;         xsi:schemaLocation=&quot;http://xmlns.jcp.org/xml/ns/javaee                              http://xmlns.jcp.org/xml/ns/javaee/web-app_4_0.xsd&quot;         version=&quot;4.0&quot;&gt;&lt;!--注册并配置DispatcherServlet--&gt;    &lt;servlet&gt;        &lt;servlet-name&gt;springmvc&lt;/servlet-name&gt;        &lt;servlet-class&gt;org.springframework.web.servlet.DispatcherServlet&lt;/servlet-class&gt;        &lt;!--            关联springmvc的配置文件,这里是springmvc-servlet.xml        --&gt;        &lt;init-param&gt;            &lt;param-name&gt;contextConfigLocation&lt;/param-name&gt;            &lt;param-value&gt;classpath:springmvc-servlet.xml&lt;/param-value&gt;        &lt;/init-param&gt;        &lt;!--启动级别-1--&gt;        &lt;load-on-startup&gt;1&lt;/load-on-startup&gt;    &lt;/servlet&gt;    &lt;!--匹配请求--&gt;    &lt;!--/ 匹配了除了jsp页面的所有请求--&gt;    &lt;!--/* 匹配所有的请求--&gt;    &lt;servlet-mapping&gt;        &lt;servlet-name&gt;springmvc&lt;/servlet-name&gt;        &lt;url-pattern&gt;/&lt;/url-pattern&gt;    &lt;/servlet-mapping&gt;    &lt;/web-app&gt;</code></pre></li><li><p>在Java文件夹下创建HelloController</p><pre><code class="language-Java">public class HelloController implements Controller {//在idea有两个Controller接口，导入的是org.springframework.web.servlet.mvc下    public ModelAndView handleRequest(HttpServletRequest request, HttpServletResponse response)         throws Exception {            //ModelAndView            ModelAndView mv = new ModelAndView();            //封装对象            mv.addObject(&quot;msg&quot;,&quot;HelloSpringMVC!&quot;);            //封装要跳转的视图            mv.setViewName(&quot;hello&quot;); //: /WEB-INF/jsp/hello.jsp            return mv;    }}</code></pre></li><li><p>然后在WEB-INF的jsp的文件夹下创建hello.jsp文件，显示msg的信息</p><pre><code class="language-jsp">&lt;%@ page contentType=&quot;text/html;charset=UTF-8&quot; language=&quot;java&quot; %&gt;&lt;html&gt;&lt;head&gt;    &lt;title&gt;Title&lt;/title&gt;&lt;/head&gt;&lt;body&gt;    ${msg}&lt;/body&gt;&lt;/html&gt;</code></pre></li><li><p>配置tomcat启动就好</p></li></ol><p>结构的目录为</p><p><img src="https://img.yww52.com/2020/7/2020-7-7/img3.png" alt="3"></p><h1>执行原理</h1><p>跳转至该文章。<a href="https://yww52.com/2020/09/21/Springmvc%E6%89%A7%E8%A1%8C%E5%8E%9F%E7%90%86/">springmvc执行原理</a></p><h1>注解的使用</h1><blockquote><p>与上面的第一个Springmvc程序来对比</p></blockquote><ol><li><p>首先是sprngmvc的配置程序</p><pre><code class="language-xml">&lt;?xml version=&quot;1.0&quot; encoding=&quot;UTF-8&quot;?&gt;&lt;beans xmlns=&quot;http://www.springframework.org/schema/beans&quot;       xmlns:xsi=&quot;http://www.w3.org/2001/XMLSchema-instance&quot;       xmlns:context=&quot;http://www.springframework.org/schema/context&quot;       xmlns:mvc=&quot;http://www.springframework.org/schema/mvc&quot;    xsi:schemaLocation=&quot;http://www.springframework.org/schema/beans        http://www.springframework.org/schema/beans/spring-beans.xsd        http://www.springframework.org/schema/context        https://www.springframework.org/schema/context/spring-context.xsd        http://www.springframework.org/schema/mvc        https://www.springframework.org/schema/mvc/spring-mvc.xsd&quot;&gt;        &lt;!--使用注解要添加扫描的包--&gt;    &lt;context:component-scan base-package=&quot;com.yw.controller&quot;/&gt;    &lt;mvc:default-servlet-handler/&gt;    &lt;mvc:annotation-driven/&gt;    &lt;bean class=&quot;org.springframework.web.servlet.view.InternalResourceViewResolver&quot;&gt;        &lt;property name=&quot;prefix&quot; value=&quot;/WEB-INF/jsp/&quot;/&gt;        &lt;property name=&quot;suffix&quot; value=&quot;.jsp&quot;/&gt; &lt;/bean&gt;&lt;/beans&gt;</code></pre><blockquote><ol><li><p>&lt;mvc:default-servlet-handler/&gt;</p><p>因为之前配置DispatcherServlet的时候配置了拦截方式为“/”,故包括了静态资源，所有springmvc会将静态资源的请求当成是普通的请求，所以会找不到对应的处理器而导致错误。这条语句将把静态资源和非静态资源的请求分开，从而避免错误。</p></li><li><p>&lt;mvc:annotation-driven/&gt;</p><p>这个帮我们注册了很多的类 ，包括之前的BeanNameUrlHandlerMapping和SimpleControllerHandlerAdapter，并告知spring，启用了注解。</p></li></ol></blockquote></li><li><p>然后是Controller类</p><pre><code class="language-java">import org.springframework.stereotype.Controller;import org.springframework.ui.Model;import org.springframework.web.bind.annotation.RequestMapping;//自动视为一个Controller的类//@RequestMapping(&quot;/1&quot;)，这里也可以添加路径，这里加上了那下面的路径就是/1/hello了@Controllerpublic class HelloController  {    // 访问的路径    @RequestMapping(&quot;/hello&quot;)    public String hello(Model model){        model.addAttribute(&quot;msg&quot;,&quot;hello springmvc&quot;);        return &quot;hello&quot;;        //返回给视图解析器拼接，然后寻找视图    }}</code></pre></li><li><p>web.xml用之前的就好，再写jsp页面就可以运行了。</p><p>::: success</p><p>springmvc-servlet.xml和web.xml基本都是不用怎么改变的，除了改一些路径。</p><p>:::</p></li></ol><h1>简单的标签详解</h1><ol><li><p><strong>@Controller</strong>告诉spring该类为一个控制器，并交给spring来托管</p></li><li><p><strong>@RequestMapping</strong></p><pre><code class="language-Java">@Controller@RequestMapping(&quot;/hello1&quot;)public class HelloController  {    //表示获取的路径，放在类上为父级，方法上为子级。全标签是这样@RequestMapping(value=&quot;/hello2&quot;)    @RequestMapping(&quot;/hello2&quot;)    //访问路径为http://localhost:8080/项目名/hello1/hello2    public String hello(Model model){        model.addAttribute(&quot;msg&quot;,&quot;hello springmvc&quot;);        return &quot;hello&quot;;    }            //@PathVariable表示绑定参数，method默认为GET，所以通过get方法就能访问，换成其他方法比如delete就会报错。    @RequestMapping(value=&quot;/hello2/{a}/{b}&quot;,method = RequestMethod.GET)    //通过访问路径http://localhost:8080/项目名/hello1/hello2/1/2就能获取a和b的值了    public String hello1(@PathVariable int a,@PathVariable int b, Model model){        int c = a + b;        model.addAttribute(&quot;msg&quot;,c);        return &quot;hello&quot;;    }            //通过method参数就会有以下的衍生注解代表不同方法，不同的是衍生注解只能放在方法上    /*    @GetMapping(&quot;/hello2/{a}/{b}&quot;)    @PostMapping(&quot;/hello2/{a}/{b}&quot;)    @PutMapping(&quot;/hello2/{a}/{b}&quot;)    @DeleteMapping(&quot;/hello2/{a}/{b}&quot;)    @PatchMapping(&quot;/hello2/{a}/{b}&quot;)    */    @GetMapping(&quot;/hello2/{a}/{b}&quot;)    public String hello1(@PathVariable int a,@PathVariable int b, Model model){        int c = a + b;        model.addAttribute(&quot;msg&quot;,c);        return &quot;hello&quot;;    }}</code></pre></li></ol><h1>关于重定向和转发</h1><h1>使用视图解析器</h1><p>在SpringMvc的配置文件下注册了视图解析器，即下面的这段代码，就可以通过视图解析器来进行转发。</p><pre><code class="language-xml">&lt;!--在springmvc的配置文件中注册视图解析器--&gt;&lt;bean class=&quot;org.springframework.web.servlet.view.InternalResourceViewResolver&quot;    id = &quot;internalResourceViewResolver&quot;&gt;    &lt;!--前缀和后缀，前缀需手动索引--&gt;        &lt;property name=&quot;prefix&quot; value=&quot;/WEB-INF/jsp/&quot;/&gt;        &lt;property name=&quot;suffix&quot; value=&quot;.jsp&quot;/&gt;&lt;/bean&gt;</code></pre><pre><code class="language-Java">@Controllerpublic class HelloController  {    @GetMapping(&quot;/hello&quot;)    public String hello(Model model){        model.addAttribute(&quot;msg&quot;,&quot;Hello World!&quot;);        return &quot;hello&quot;;        //返回给视图解析器就会拼接成/WEB-INF/jsp/hello.jsp    }}</code></pre><h2>不使用视图解析器</h2><h3>直接使用servlet的方法</h3><pre><code class="language-java">@Controllerpublic class HelloController  {    //转发    @GetMapping(&quot;/hello1&quot;)    public void hello1(HttpServletRequest request, HttpServletResponse response) throws IOException {        response.getWriter().println(&quot;Hello World!&quot;);    }    //转发    @GetMapping(&quot;/hello2&quot;)    public void hello2(HttpServletRequest request, HttpServletResponse response) throws IOException {        response.sendRedirect(&quot;index.jsp&quot;);    }    //重定向    @GetMapping(&quot;/hello3&quot;)    public void hello3(HttpServletRequest request, HttpServletResponse response) throws Exception {        request.setAttribute(&quot;msg&quot;,&quot;Hello World!&quot;);        request.getRequestDispatcher(&quot;/WEB-INF/jsp/hello.jsp&quot;).forward(request,response);    }}</code></pre><p>:::note{type=&quot;warning&quot;}</p><p>因为实质是servlet，所以是可以直接使用servlet的方法的，但是不推荐这么使用，过于繁琐。</p><p>:::</p><h3>使用前缀</h3><pre><code class="language-java">@Controllerpublic class HelloController  {    //forward前缀 转发    @GetMapping(&quot;/hello1&quot;)    public String hello1(Model model){        model.addAttribute(&quot;msg&quot;,&quot;Hello World!&quot;);        return &quot;forward:/WEB-INF/jsp/hello.jsp&quot;;    }    //forward前缀 跳转到目标方法    @GetMapping(&quot;/hello2&quot;)    public String hello2(Model model){        model.addAttribute(&quot;msg&quot;,&quot;Hello World!&quot;);        return &quot;forward:/hello1&quot;;    }    //redirect前缀 重定向    @GetMapping(&quot;/hello3&quot;)    public String hello3(Model model){        model.addAttribute(&quot;msg&quot;,&quot;Hello World!&quot;);        return &quot;redirect:/index.jsp&quot;;    }}</code></pre><p>:::note{type=&quot;info&quot;}</p><p>前缀的优先级是比视图解析器大的，有前缀的前提下是不经过视图解析器的，建议还是注册视图解析器，转发不用前缀，重定向使用前缀进行。</p><p>:::</p><h1>数据处理</h1><h2>获取数据</h2><ol><li><p>通过url相同的参数名获取数据</p><pre><code class="language-Java">@Controllerpublic class HelloController  {    @GetMapping(&quot;/hello1&quot;)    //输入http://localhost:8080/项目名/hello?msg01=hello world!    public String hello1(String msg01, Model model){        model.addAttribute(&quot;msg&quot;,msg01);        return &quot;hello&quot;;    }    @GetMapping(&quot;/hello2&quot;)    //输入http://localhost:8080/项目名/hello?msg01=hello&amp;msg02= world!    public String hello2(String msg01,String msg02,Model model){        model.addAttribute(&quot;msg&quot;,msg01+msg02);        return &quot;hello&quot;;    }}</code></pre></li><li><p>通过url不相同的参数名获取数据</p><pre><code class="language-java">@Controllerpublic class HelloController  {    @GetMapping(&quot;/hello&quot;)    //输入http://localhost:8080/项目名/hello?msg0=hello world!    //通过RequestParam标签绑定参数名字可以修改名字，但不能用回原来的参数名来获取了，即只能用msg0来获取    public String hello(@RequestParam(&quot;msg0&quot;) String msg01, Model model){        model.addAttribute(&quot;msg&quot;,msg01);        return &quot;hello&quot;;    }}</code></pre></li><li><p>通过url来获取一个对象</p><ol><li><p>首先创建一个实体类</p><pre><code class="language-Java">public class user {    String name;    int id;    public user(String name, int id) {        this.name = name;        this.id = id;    }    public String getName() {        return name;    }    public int getId() {        return id;    }    @Override    public String toString() {        return &quot;user{&quot; +                &quot;name=&#39;&quot; + name + &#39;\&#39;&#39; +                &quot;, id=&quot; + id +                &#39;}&#39;;    }}</code></pre></li><li><p>获取对象</p><pre><code class="language-Java">@Controllerpublic class HelloController  {    @GetMapping(&quot;/hello&quot;)    //输入http://localhost:8080/项目名/hello?name=he&amp;id=2    //注意的是，输入的参数要与构造器的顺序一样    public String hello(User user, Model model){        model.addAttribute(&quot;msg&quot;,user);        return &quot;hello&quot;;    }}</code></pre></li></ol></li></ol><h2>解析到前端页面</h2><ol><li><p>使用ModelAndView</p><pre><code class="language-java">public class HelloController implements Controller {    public ModelAndView handleRequest(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse) throws Exception {        ModelAndView mv = new ModelAndView();        //封装数据        mv.addObject(&quot;msg&quot;, &quot;Hello World!&quot;);        //封装跳转页面        mv.setViewName(&quot;hello&quot;);        return mv;    }}</code></pre></li><li><p>使用Model</p><pre><code class="language-java">@Controllerpublic class HelloController  {    @GetMapping(&quot;/hello&quot;)    public String hello(Model model){        //封装数据        model.addAttribute(&quot;msg&quot;,&quot;Hello World!&quot;);        //返回跳转视图        return &quot;hello&quot;;    }}</code></pre></li><li><p>使用ModelMap</p><pre><code class="language-Java">@Controllerpublic class HelloController  {    @GetMapping(&quot;/hello&quot;)    public String hello(ModelMap modelMap){        modelMap.addAttribute(&quot;msg&quot;,&quot;Hello World!&quot;);        return &quot;hello&quot;;    }}</code></pre></li></ol><p>:::note{type=&quot;info&quot;}</p><p>Model其实是一个接口实现类，一定程度上是继承了ModelMap，因为Model的实现也是要用到ModelMap的方法的，而ModelMap继承了LinkedHashMap，所以方法比Model多一些。</p><p>:::</p><h1>Json的处理</h1><ol><li><p>添加jackson-databind的依赖</p><pre><code class="language-xml">&lt;dependency&gt;            &lt;groupId&gt;com.fasterxml.jackson.core&lt;/groupId&gt;            &lt;artifactId&gt;jackson-databind&lt;/artifactId&gt;            &lt;version&gt;2.11.0&lt;/version&gt;&lt;/dependency&gt;</code></pre></li><li><p>实体测试类</p><pre><code class="language-java">public class User {    String name;    int id;    public User(String name, int id) {        this.name = name;        this.id = id;    }    public String getName() {        return name;    }    public int getId() {        return id;    }    @Override    public String toString() {        return &quot;user{&quot; +                &quot;name=&#39;&quot; + name + &#39;\&#39;&#39; +                &quot;, id=&quot; + id +                &#39;}&#39;;    }}</code></pre></li><li><p>Controller类</p><pre><code class="language-java">@RestController//作用为跳过视图解析器，返回json字符串public class HelloController  {    @RequestMapping(&quot;/hello&quot;)    public String json() throws JsonProcessingException {        ObjectMapper mapper = new ObjectMapper();        User user = new User(&quot;yww&quot;, 2);        String res = mapper.writeValueAsString(user);        return res;    }}</code></pre></li></ol><p>:::note{type=&quot;warning&quot;}</p><p>注意的是新加依赖要刷新project setting里的lib目录，不然没添加会直接500报错</p><p>:::</p>]]></content:encoded>
            <category domain="https://yww52.com/categories/%25E6%2588%2591%25E7%259A%2584%25E6%258A%2580%25E6%259C%25AF%25E5%25AD%25A6%25E4%25B9%25A0%25E7%2589%25A9%25E8%25AF%25AD%25E6%259E%259C%25E7%2584%25B6%25E6%259C%2589%25E9%2597%25AE%25E9%25A2%2598/">我的技术学习物语果然有问题</category>
        </item>
        <item>
            <title>Ngrok内网穿透</title>
            <link>https://yww52.com/posts/4a19524b/</link>
            <guid isPermaLink="true">https://yww52.com/posts/4a19524b/</guid>
            <pubDate>Fri, 03 Jul 2020 00:00:00 +0000</pubDate>
            <description>Ngrok内网穿透就是让外网也能访问内网IP。</description>
            <content:encoded><![CDATA[<img src="https://img.yww52.com/2020/7/2020-7-3/top_img.jpg" alt="Ngrok内网穿透" style="max-width:100%;height:auto;margin-bottom:15px;" /><h2>什么是Ngrok</h2><p>ngrok 是一个反向代理，通过在公共的端点和本地运行的 Web 服务器之间建立一个安全的通道。ngrok 可捕获和分析所有通道上的流量，便于后期分析和重放。</p><h2>有什么用</h2><p>内网穿透意思是能让外网访问到你本地，就比如你在本地运行tomacat等，外网是无法访问到你本地的项目的，当然你部署到服务器上就可以，这篇文章只是为了简化你的操作，而且你应该不会什么测试都部署到服务器，所以就能使用ngrok实现内网穿透。</p><h2>注册</h2><p>先去官网注册一个用户，然后登陆。国内Ngrok的地址：<a href="https://www.ngrok.cc/">https://www.ngrok.cc/</a></p><h2>购买隧道</h2><p><img src="https://img.yww52.com/2020/7/2020-7-3/img1.png" alt=""></p><p>这个是免费的，当然你想体验更流畅那就付费呗。</p><p><img src="https://img.yww52.com/2020/7/2020-7-3/img2.png" alt=""></p><h2>使用</h2><p><img src="https://img.yww52.com/2020/7/2020-7-3/img3.png" alt=""></p><p><img src="https://img.yww52.com/2020/7/2020-7-3/img4.png" alt=""></p><p>把你的隧道ID输入就好了<img src="https://img.yww52.com/2020/7/2020-7-3/img5.png" alt=""></p><p>然后你就能用赠送的域名访问到你的内网了。</p><p><img src="https://img.yww52.com/2020/7/2020-7-3/img6.png" alt=""></p><h2>后话</h2><p>这只是个简单的穿透，不过也挺有用的，能让外网访问到你的本地项目，在一些时候能很方便。你不想用他的域名，应该是可以解析的，解析成你的就好。</p>]]></content:encoded>
            <category domain="https://yww52.com/categories/%25E6%2588%2591%25E7%259A%2584%25E6%258A%2580%25E6%259C%25AF%25E5%25AD%25A6%25E4%25B9%25A0%25E7%2589%25A9%25E8%25AF%25AD%25E6%259E%259C%25E7%2584%25B6%25E6%259C%2589%25E9%2597%25AE%25E9%25A2%2598/">我的技术学习物语果然有问题</category>
        </item>
        <item>
            <title>AOP</title>
            <link>https://yww52.com/posts/92933cdb/</link>
            <guid isPermaLink="true">https://yww52.com/posts/92933cdb/</guid>
            <pubDate>Tue, 30 Jun 2020 00:00:00 +0000</pubDate>
            <description>AOP是spring的另一个核心，是oop的延续。</description>
            <content:encoded><![CDATA[<img src="https://img.yww52.com/2020/6/2020-6-30/top_img.jpg" alt="AOP" style="max-width:100%;height:auto;margin-bottom:15px;" /><h2>什么是AOP</h2><p>Spring 是一个轻量级的IOC和AOP的框架，spring的两大核心就是IOC和AOP，<a href="https://yww52.com/2020/06/23/IOC/">IOC我之前写有</a>，那什么是AOP呢？首先先看一下百度的定义。</p><blockquote><p>在软件业，AOP为Aspect Oriented Programming的缩写，意为：面向切面编程，通过预编译方式和运行期间动态代理实现程序功能的统一维护的一种技术。AOP是OOP的延续，是软件开发中的一个热点，也是Spring框架中的一个重要内容，是函数式编程的一种衍生范型。利用AOP可以对业务逻辑的各个部分进行隔离，从而使得业务逻辑各部分之间的耦合度降低，提高程序的可重用性，同时提高了开发的效率。</p></blockquote><h2>面向切面编程</h2><p>AOP，面向切面编程，这又是一种新的思想。一开始我们接触的是面向过程，比如c语言。然后是面向对象编程oop，一系列的面向对象语言，比如Java。学到后面就发现，面向对象也不能满足人们了，就出现了面向接口编程，但人总是懒得，这又出现了面向切面编程。面向切面顾名思义，把一个程序一刀切就出现了切面。怎么理解呢。就比如我们得业务，实现增删改查，一开始的我们是这样的。</p><p><img src="https://img.yww52.com/2020/6/2020-6-30/img1.png" alt=""></p><p>这时候人们想增加一个功能，比如日志，就会变成这样</p><p><img src="https://img.yww52.com/2020/6/2020-6-30/img2.png" alt=""></p><p>人们会发现这样会重复写了一段代码，当很多业务方法都需要加上日志这个功能的时候，就会多写很多的代码，为了简便，人们就想到了</p><p>增加一个类来实现日志就好了，就比如这样</p><p><img src="https://img.yww52.com/2020/6/2020-6-30/img3.png" alt=""></p><p>但是每一个方法都要加多一段代码来调用日志的方法，这样也是很麻烦的，如果要有很多很多的方法，一个一个的加不仅会破环原方法的代码，还会很繁琐。</p><p>所以出现了我们面向切面开发的AOP。</p><p><img src="https://img.yww52.com/2020/6/2020-6-30/img4.png" alt=""></p><blockquote><p>这样大概就能理解什么是面向切面编程了吧。我们为了实现功能的操作，都可以理解为一条直线，从中切一刀，插入一个功能，该功能还能切入其他的方法，就这样便减少了代码的重复性，也减少了耦合。</p></blockquote><h2>AOP的术语</h2><h3>通知（Advice）</h3><p>通知可以理解为你需要实现个功能，就比如上述例子的日志功能。</p><h3>连接点（JoinPoint）</h3><p>连接点就是你可以你可以使用通知功能的地方，就比如增加操作的前后都是可以增加日志功能的，方法前后都是连接点。</p><h3>切入点（Pointcut）</h3><p>切入点就很好理解了吧，上述的日志功能横穿两个方法，交点就是这个通知的切入点，这个交点也是连接点。其实切入点就是连接点，连接点是你能够使用通知的地方，这些地方会有很多，但你不会所有都用，有些地方你就不会去使用通知的功能，你用通知的连接点才是切入点。</p><h3>切面（Aspect）</h3><p>切面其实就是通知和切入点的结合，这两者结合就把一个方法切出一个面，所以叫切面。</p><h3>引入（introduction）</h3><p>引入的意思就是把切面用到目标里。比如查询需要引入日志这个通知，就是把查询这个功能也切一刀。</p><h3>目标（target）</h3><p>目标其实就是指需要添加通知的对象，就是刚刚所说的那条线。</p><h3>代理(proxy)</h3><p>看到这其实就发现AOP与之前所说的动态代理很像，整个的AOP实现其实都是通过代理来实现的，没了解过动态代理的可以去参考一下我<a href="https://yww52.com/2020/06/28/%E4%BB%A3%E7%90%86%E6%A8%A1%E5%BC%8F/">之前的文章</a>。</p><h3>织入(weaving)</h3><p>把切面应用到目标对象来创建新的代理对象的过程。有3种方式，spring采用的是运行时。</p><h2>AOP的实现原理</h2><p>刚刚也说到了，AOP其实就是通过动态代理来实现的。spring用代理类包裹切面，把它们织入到Bean中，从而代理类代替原有的类拦截了方法的调用，拦截了方法的调用然后改代理类又实现了一些附带的功能，然后再把方法调用转发给Bean，从而实现了AOP。</p><p>怎么拦截就是代理的问题了。动态代理分为两种，一个是基于接口的动态代理，就是JDK的动态代理，另一个便是基于类的代理，就是使用cglib的动态代理。</p><ol><li>基于接口的动态代理目标类和代理类都实现了同一接口，但不同的是代理类能会实现一些附带的功能，目标类只是代理类的一部分，虽然最后都是目标类完成接口的功能，但代理类不同于接口的方法就可以插入到目标类实现接口方法的前后，这就实现了AOP。就像是代理类里有两兄弟，实现了接口的那些方法就是哥哥，而实现了不同于接口方法的其他功能就是弟弟，当人们想要完成接口的方法的时候，弟弟虽然办不到，但是他招待了你（就是意义上的拦截），你以为他是能实现接口的哥哥，便把任务给了他，弟弟答应了又顺带收了你的钱（即附加的功能，比如日志），然后弟弟知道自己不能做这个任务，便让哥哥实现，这就是JDK的动态代理实现AOP的方式。</li><li>基于类的动态代理基于类那就更加好理解了。目标类就像是父亲，而继承了目标类的代理类就是儿子。人们都找儿子去实现接口的功能，但是儿子在完成接口的任务之前或者之后都会做一些其他的事（即附加的功能，比如日志），所以这就是cglib的动态代理实现AOP的方式。</li></ol><h2>参考文章</h2><p>这两篇还是很好理解的，此文就是借鉴两文然后加上我自己的理解写成的。</p><ul><li><a href="https://blog.csdn.net/qukaiwei/article/details/50367761">https://blog.csdn.net/qukaiwei/article/details/50367761</a></li><li><a href="https://blog.csdn.net/q982151756/article/details/80513340">https://blog.csdn.net/q982151756/article/details/80513340</a></li></ul>]]></content:encoded>
            <category domain="https://yww52.com/categories/%25E6%2588%2591%25E7%259A%2584%25E6%258A%2580%25E6%259C%25AF%25E5%25AD%25A6%25E4%25B9%25A0%25E7%2589%25A9%25E8%25AF%25AD%25E6%259E%259C%25E7%2584%25B6%25E6%259C%2589%25E9%2597%25AE%25E9%25A2%2598/">我的技术学习物语果然有问题</category>
        </item>
        <item>
            <title>代理模式</title>
            <link>https://yww52.com/posts/7b510e10/</link>
            <guid isPermaLink="true">https://yww52.com/posts/7b510e10/</guid>
            <pubDate>Sun, 28 Jun 2020 00:00:00 +0000</pubDate>
            <description>代理模式顾名思义就是为一个对象提供一个代理，并让其他对象通过代理对象来控制这个对象。代理模式又分为两种，一种叫静态代理，一种叫动态代理。</description>
            <content:encoded><![CDATA[<img src="https://img.yww52.com/2020/6/2020-6-28top_img.jpg" alt="代理模式" style="max-width:100%;height:auto;margin-bottom:15px;" /><h2>什么是代理模式</h2><p>代理模式顾名思义就是为一个对象提供一个代理，并让其他对象通过代理对象来控制这个对象。代理模式又分为两种，一种叫静态代理，一种叫动态代理。</p><h2>静态代理</h2><h3>角色分析</h3><ol><li>Isubject抽象角色，一般使用接口和抽象类，不过抽象类不怎么好用，大多都是接口。</li><li>RealSubject真实角色，被代理的角色。</li><li>Proxy代理角色，代理真实角色</li></ol><h3>代码实现1</h3><p>光用文字描述，会觉得十分的绕，还是用代码来讲解一下吧。首先先模拟一下环境，比如买火车票这件事情。卖火车票这件事情就是一个抽象角色，我们为它创建一个接口。</p><pre><code class="language-Java">public interface buyTickets {    void buytickets();}</code></pre><p>有买票的人也就会有卖的人，这里是卖票的设为火车站。这样的话火车站就是一个真实角色，为它创建一个类。</p><pre><code class="language-Java">public class Station implements buyTickets {    public void buytickets() {        System.out.println(&quot;火车站在卖票&quot;);    }}</code></pre><p>那么问题就来了，我们学生需要买票回家，是该怎么买呢？应该不会有多少人去到火车站买票吧。大多都是网上订票吧，就比如使用软件买票，方便也快捷，这个软件就是一个代理角色，代理了火车站卖票。我们也为他创建一个类。一般代理都会有很多特定的作用，即附属一些其他的方法，就比如使用软件卖票会十分方便。</p><pre><code class="language-Java">public class Software implements buyTickets {    private Station station;    public  Software(){}    public Software(Station station) {        this.station = station;    }    public void buytickets() {        station.buytickets();    }    public void Convenient(){        System.out.println(&quot;用软件卖票十分方便&quot;);    }}</code></pre><p>好了，那就模拟一下我们买票的过程吧。比如我们通过软件就能看到火车站在卖票，然后在软件上买票，这样就不用去到火车站在买票了，也就是我们不需要使用真实角色的卖票方法，使用的是代理对象的卖票方法，代理对象就会帮我们去真实对象中调用想要的方法（代理对象有点类似中介）。</p><pre><code class="language-Java">public class test01 {    public static void main(String[] args) {     //首先是一个火车站要卖票        Station station = new Station();     //然后就是软件代理火车站卖票        Software software = new Software(station);        software.buytickets();    }}</code></pre><h3>代码实现2</h3><p>上述是我们日常生活中一个小小的例子，可能没能够体会到代理模式的好处，我们来看看项目中的一些简单实现吧，比如经常见到的增删改查操作。首先是业务接口和接口实现类。</p><pre><code class="language-Java">public interface userService {    void add();    void delete();    void update();    void select();}</code></pre><pre><code class="language-Java">public class userServiceImpl implements userService{    public void add() {        System.out.println(&quot;增加了一个用户&quot;);    }    public void delete() {        System.out.println(&quot;删除了一个用户&quot;);    }    public void update() {        System.out.println(&quot;修改了一个用户&quot;);    }    public void select() {        System.out.println(&quot;查询了一个用户&quot;);    }}</code></pre><p>这是没有代理角色的情况，我们也可以直接创建userServiceImpl对象调用里面的方法，但是要是之后需要了增加一些业务需求，比如增加一个日志功能，我们常规就是直接修改userServiceImpl类，比如这样</p><pre><code class="language-Java">public class userServiceImpl implements userService{    public void add() {        System.out.println(&quot;使用add方法增加了一个用户&quot;);        System.out.println(&quot;增加了一个用户&quot;);    }    public void delete() {        System.out.println(&quot;使用delete方法删除了一个用户&quot;);        System.out.println(&quot;删除了一个用户&quot;);    }    public void update() {        System.out.println(&quot;使用update方法修改了一个用户&quot;);        System.out.println(&quot;修改了一个用户&quot;);    }    public void select() {        System.out.println(&quot;使用select方法查询了一个用户&quot;);        System.out.println(&quot;查询了一个用户&quot;);    }}</code></pre><p>这只是四个增删改查的操作，你都会发现这样很麻烦，要是再加几个功能，或者再加几个操作，难道还要一个个改吗？这时候用代理就能很好的解决这种问题。那我们先创建这个代理类。</p><pre><code class="language-Java">public class Proxy  implements userService{    private userServiceImpl userserviceimpl;    public Proxy() {}    public Proxy(userServiceImpl userserviceimpl) {        this.userserviceimpl = userserviceimpl;    }    public void add() {        log(&quot;add&quot;);        userserviceimpl.add();    }    public void delete() {        log(&quot;delete&quot;);        userserviceimpl.delete();    }    public void update() {        log(&quot;update&quot;);        userserviceimpl.update();    }    public void select() {        log(&quot;select&quot;);        userserviceimpl.select();    }    public void log(String logs){        System.out.println(&quot;使用了&quot;+logs+&quot;方法&quot;);    }}</code></pre><pre><code class="language-Java">public class test01 {    public static void main(String[] args) {        userServiceImpl userserviceimpl = new userServiceImpl();        Proxy proxy = new Proxy(userserviceimpl);        proxy.add();    }}</code></pre><blockquote><p>当然有人会觉得代理也没差多少，也是挺麻烦的，都是需要手动添加很多代码，但代理模式可以更加方便地拓展，比如需要加功能就再添加一个方法，不用总是改接口实现类里的方法，代码一多就能感受到代理模式的方便了，随便改源码不是一个好的习惯。</p></blockquote><h2>动态代理</h2><p>静态代理大概了解了，就能发现静态代理会有一个很大的问题。当需要代理的类多了，就会发现代理的类也会增多，因为静态代理是一个类绑定一个代理类的，这就很麻烦取管理了，因为代理类的创建就是为了方便，所以动态代理就出现了。动态代理的代理类是动态生成的，我们写好一个工具类就能动态生成很多类的代理类了。</p><blockquote><p>动态代理分为两大类。</p><ol><li>JDK动态代理</li><li>CGLib动态代理</li></ol></blockquote><h3>JDK动态代理</h3><p>JDK动态代理是Java自带的一种动态代理的方式，核心原理就是通过反射来实现的。首先要实现JDK的动态代理就要先了解一个类和一个接口,直接看文档的说法。</p><ol><li><p>ProxyProxy提供了创建动态代理类和实例的静态方法，它也是由这些方法创建的所有的动态代理类的超类。这里也不深入了解了，只要了解它主要的一个方法，生成动态代理类的方法。</p><blockquote><p> Foo f = (Foo) Proxy.newProxyInstance(Foo.class.getClassLoader(),                                          new Class&lt;?&gt;[] { Foo.class },                                          handler);</p></blockquote></li><li><p>InvocationHandler每个代理实例都有一个关联的调用处理程序。当在代理实例上调用方法时，方法调用将会被编码分派到其调用处理程序的invoke方法。官方说法有点绕，简单来说是这个接口就只有一个invoke方法,这个方法是处理被代理实例即真实角色的，并会返回一个结果。</p><blockquote><p>public Object invoke(Object proxy, Method method, Object[] args)  throws Throwable;</p></blockquote></li></ol><p>大概了解了这两个类，就可以写一个生成动态代理的工具类吧。（基本固定，可以当作万能的工具类）</p><pre><code class="language-Java">public class ProxyInvocationHandler implements InvocationHandler {    //设置一个被代理的对象    private Object target;    public void setTarget(Object target) {        this.target = target;    }    //生成代理类，文档的写法    public Object getProxy(){        return Proxy.newProxyInstance(this.getClass().getClassLoader(),                                      target.getClass().getInterfaces(),this);    }    //处理要被代理实例，并返回结果    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {        Object result = method.invoke(target, args);        return result;    }}</code></pre><p>写完了代理类就来写测试一下吧，例子就选之前的那个增删改查的接口吧。</p><pre><code class="language-Java">public class test01 {    public static void main(String[] args) {        //先创建一个真实角色        userServiceImpl userservice = new userServiceImpl();        ProxyInvocationHandler pih = new ProxyInvocationHandler();        //闯入需要代理的角色        pih.setTarget(userservice);        //生成一个代理类        userService proxy = (userService) pih.getProxy();        proxy.add();        proxy.delete();        proxy.select();        proxy.update();    }}</code></pre><p>当我们需要添加功能的时候，比如添加一个日志的功能，就改写一下工具类就好了。</p><pre><code class="language-Java">public class ProxyInvocationHandler implements InvocationHandler {    //设置一个被代理的对象    private Object target;    public void setTarget(Object target) {        this.target = target;    }    //生成代理类    public Object getProxy(){        return Proxy.newProxyInstance(this.getClass().getClassLoader(),                                      target.getClass().getInterfaces(),this);    }    //处理要被代理实例，并返回结果    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {        log(method.getName());        Object result = method.invoke(target, args);        return result;    }    public void log(String msg){        System.out.println(&quot;使用了&quot;+msg+&quot;方法&quot;);    }}</code></pre><h3>CGLib动态代理</h3>]]></content:encoded>
            <category domain="https://yww52.com/categories/%25E6%2588%2591%25E7%259A%2584%25E6%258A%2580%25E6%259C%25AF%25E5%25AD%25A6%25E4%25B9%25A0%25E7%2589%25A9%25E8%25AF%25AD%25E6%259E%259C%25E7%2584%25B6%25E6%259C%2589%25E9%2597%25AE%25E9%25A2%2598/">我的技术学习物语果然有问题</category>
        </item>
        <item>
            <title>环境搭建手册</title>
            <link>https://yww52.com/posts/419e09e5/</link>
            <guid isPermaLink="true">https://yww52.com/posts/419e09e5/</guid>
            <pubDate>Sat, 27 Jun 2020 00:00:00 +0000</pubDate>
            <description>总结一下环境的配置，比如导入依赖</description>
            <content:encoded><![CDATA[<img src="https://img.yww52.com/2020/6/2020-6-27top_img.jpg" alt="环境搭建手册" style="max-width:100%;height:auto;margin-bottom:15px;" /><h1>前言</h1><p>为了以后更好的做个cv工程师，所以现在总结一下配置。</p><h1>我的环境</h1><ul><li><strong>IntelliJ IDEA 2020.2.2</strong></li><li><strong>Mysql  8.0.18</strong></li><li><strong>Tomcat  9.03.5</strong></li><li><strong>maven  3.6.1</strong></li><li><strong>jdk  1.8</strong></li></ul><h1>Servlet更新web.xml</h1><pre><code class="language-xml">&lt;web-app xmlns=&quot;http://xmlns.jcp.org/xml/ns/javaee&quot;         xmlns:xsi=&quot;http://www.w3.org/2001/XMLSchema-instance&quot;         xsi:schemaLocation=&quot;http://xmlns.jcp.org/xml/ns/javaee                             http://xmlns.jcp.org/xml/ns/javaee/web-app_3_1.xsd&quot;         version=&quot;3.1&quot;&gt;&lt;/web-app&gt;</code></pre><h1>Mybatis</h1><h2>pom.xml</h2><pre><code class="language-xml">&lt;?xml version=&quot;1.0&quot; encoding=&quot;UTF8&quot;?&gt;&lt;project xmlns=&quot;http://maven.apache.org/POM/4.0.0&quot;         xmlns:xsi=&quot;http://www.w3.org/2001/XMLSchema-instance&quot;         xsi:schemaLocation=&quot;http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd&quot;&gt;    &lt;!--.......--&gt;&lt;!--导入依赖--&gt;    &lt;dependencies&gt;        &lt;!--mysql驱动--&gt;        &lt;dependency&gt;            &lt;groupId&gt;mysql&lt;/groupId&gt;            &lt;artifactId&gt;mysql-connector-java&lt;/artifactId&gt;            &lt;version&gt;5.1.47&lt;/version&gt;        &lt;/dependency&gt;        &lt;!--mybatis--&gt;        &lt;dependency&gt;            &lt;groupId&gt;org.mybatis&lt;/groupId&gt;            &lt;artifactId&gt;mybatis&lt;/artifactId&gt;            &lt;version&gt;3.5.4&lt;/version&gt;        &lt;/dependency&gt;        &lt;!--junit--&gt;        &lt;dependency&gt;            &lt;groupId&gt;junit&lt;/groupId&gt;            &lt;artifactId&gt;junit&lt;/artifactId&gt;            &lt;version&gt;4.12&lt;/version&gt;        &lt;/dependency&gt;    &lt;/dependencies&gt;    &lt;build&gt;        &lt;resources&gt;            &lt;resource&gt;                &lt;directory&gt;src/main/resources&lt;/directory&gt;                &lt;includes&gt;                    &lt;include&gt;**/*.properties&lt;/include&gt;                    &lt;include&gt;**/*.xml&lt;/include&gt;                &lt;/includes&gt;                &lt;filtering&gt;false&lt;/filtering&gt;            &lt;/resource&gt;            &lt;resource&gt;                &lt;directory&gt;src/main/java&lt;/directory&gt;                &lt;includes&gt;                    &lt;include&gt;**/*.properties&lt;/include&gt;                    &lt;include&gt;**/*.xml&lt;/include&gt;                &lt;/includes&gt;                &lt;filtering&gt;false&lt;/filtering&gt;            &lt;/resource&gt;        &lt;/resources&gt;    &lt;/build&gt;&lt;/project&gt;</code></pre><h2>resource下的Mybatis配置文件</h2><p>官方的：</p><pre><code class="language-xml">&lt;?xml version=&quot;1.0&quot; encoding=&quot;UTF-8&quot; ?&gt;&lt;!DOCTYPE configuration  PUBLIC &quot;-//mybatis.org//DTD Config 3.0//EN&quot;  &quot;http://mybatis.org/dtd/mybatis-3-config.dtd&quot;&gt;&lt;configuration&gt;  &lt;environments default=&quot;development&quot;&gt;    &lt;environment id=&quot;development&quot;&gt;      &lt;transactionManager type=&quot;JDBC&quot;/&gt;      &lt;dataSource type=&quot;POOLED&quot;&gt;        &lt;property name=&quot;driver&quot; value=&quot;${driver}&quot;/&gt;        &lt;property name=&quot;url&quot; value=&quot;${url}&quot;/&gt;        &lt;property name=&quot;username&quot; value=&quot;${username}&quot;/&gt;        &lt;property name=&quot;password&quot; value=&quot;${password}&quot;/&gt;      &lt;/dataSource&gt;    &lt;/environment&gt;  &lt;/environments&gt;  &lt;mappers&gt;    &lt;mapper resource=&quot;org/mybatis/example/BlogMapper.xml&quot;/&gt;  &lt;/mappers&gt;&lt;/configuration&gt;</code></pre><p>自我优化的，数据库属性自己修改：</p><pre><code class="language-xml">&lt;?xml version=&quot;1.0&quot; encoding=&quot;UTF-8&quot; ?&gt;&lt;!DOCTYPE configuration        PUBLIC &quot;-//mybatis.org//DTD Config 3.0//EN&quot;        &quot;http://mybatis.org/dtd/mybatis-3-config.dtd&quot;&gt;&lt;!--configuration核心配置文件--&gt;&lt;configuration&gt;    &lt;!--扫描实体类包，创建别名--&gt;    &lt;typeAliases&gt;        &lt;package name=&quot;com.chen.pojo&quot;/&gt;    &lt;/typeAliases&gt;    &lt;environments default=&quot;development&quot;&gt;        &lt;environment id=&quot;development&quot;&gt;            &lt;transactionManager type=&quot;JDBC&quot;/&gt;            &lt;dataSource type=&quot;POOLED&quot;&gt;                &lt;property name=&quot;driver&quot; value=&quot;com.mysql.cj.jdbc.Driver&quot;/&gt;&lt;!--这里用&amp;amp;代替&amp;--&gt;                &lt;property name=&quot;url&quot; value=&quot;jdbc:mysql://localhost:3306/test?useSSL=false&amp;amp;serverTimezone=UTC&quot;/&gt;                &lt;property name=&quot;username&quot; value=&quot;root&quot;/&gt;                &lt;property name=&quot;password&quot; value=&quot;password&quot;/&gt;            &lt;/dataSource&gt;        &lt;/environment&gt;    &lt;/environments&gt;    &lt;!--每一个xml都需要在核心配置里加载映射--&gt;    &lt;mappers&gt;        &lt;mapper resource=&quot;...&quot;/&gt;    &lt;/mappers&gt;&lt;/configuration&gt;</code></pre><h2>Mybaitis工具类</h2><pre><code class="language-Java">public class Mybatisutils {        private static SqlSessionFactory sqlSessionFactory;        static{            try {                //使用Mybatis第一步：获取sqlSessionFactory对象                String resource = &quot;mybatis-config.xml&quot;;//resource下的mybatis-config.xml                InputStream inputStream = Resources.getResourceAsStream(resource);                 sqlSessionFactory = new SqlSessionFactoryBuilder().build(inputStream);            } catch (IOException e) {                e.printStackTrace();            }        }        //通过SqlSessionFactory中获得SqlSession的实例，通过SqlSession使用sql语句        public static SqlSession getSqlSession(){            return sqlSessionFactory.openSession();        }}</code></pre><h2>业务类的配置文件</h2><pre><code class="language-xml">&lt;?xml version=&quot;1.0&quot; encoding=&quot;UTF8&quot; ?&gt;&lt;!DOCTYPE mapper        PUBLIC &quot;-//mybatis.org//DTD Mapper 3.0//EN&quot;        &quot;http://mybatis.org/dtd/mybatis-3-mapper.dtd&quot;&gt;&lt;!--一个namespace绑定一个接口--&gt;&lt;mapper namespace=&quot;com.chen.dao.UserMapper&quot;&gt;&lt;/mapper&gt;</code></pre><h1>Spring</h1><h2>pom.xml</h2><pre><code class="language-xml">&lt;!--导这个依赖大概就够用了，为了方便test使用@Test，可以再导入junit的依赖--&gt;&lt;dependencies&gt;    &lt;!--spring-webmvc--&gt;        &lt;dependency&gt;            &lt;groupId&gt;org.springframework&lt;/groupId&gt;            &lt;artifactId&gt;spring-webmvc&lt;/artifactId&gt;            &lt;version&gt;5.2.6.RELEASE&lt;/version&gt;        &lt;/dependency&gt;    &lt;!--junit--&gt;    &lt;dependency&gt;            &lt;groupId&gt;junit&lt;/groupId&gt;            &lt;artifactId&gt;junit&lt;/artifactId&gt;            &lt;version&gt;4.12&lt;/version&gt;        &lt;/dependency&gt;&lt;/dependencies&gt;</code></pre><h2>配置文件</h2><pre><code class="language-xml">&lt;?xml version=&quot;1.0&quot; encoding=&quot;UTF-8&quot;?&gt;&lt;beans xmlns=&quot;http://www.springframework.org/schema/beans&quot;       xmlns:xsi=&quot;http://www.w3.org/2001/XMLSchema-instance&quot;       xsi:schemaLocation=&quot;http://www.springframework.org/schema/beans        http://www.springframework.org/schema/beans/spring-beans.xsd&quot;&gt;   &lt;!--使用Spring来创建对象，这些对象都称为bean--&gt;    &lt;bean&gt;     &lt;/bean&gt;&lt;/beans&gt;</code></pre><h2>p命名空间的导入</h2><pre><code class="language-xml">&lt;?xml version=&quot;1.0&quot; encoding=&quot;UTF-8&quot;?&gt;&lt;beans xmlns=&quot;http://www.springframework.org/schema/beans&quot;       xmlns:xsi=&quot;http://www.w3.org/2001/XMLSchema-instance&quot;&lt;!--即加入这句就好--&gt;       xmlns:p=&quot;http://www.springframework.org/schema/p&quot;              xsi:schemaLocation=&quot;http://www.springframework.org/schema/beans        http://www.springframework.org/schema/beans/spring-beans.xsd&quot;&gt;&lt;/beans&gt;</code></pre><h2>C命名空间的导入</h2><pre><code class="language-xml">&lt;?xml version=&quot;1.0&quot; encoding=&quot;UTF-8&quot;?&gt;&lt;beans xmlns=&quot;http://www.springframework.org/schema/beans&quot;       xmlns:xsi=&quot;http://www.w3.org/2001/XMLSchema-instance&quot;&lt;!--即加入这句就好--&gt;       xmlns:c=&quot;http://www.springframework.org/schema/c&quot;       xsi:schemaLocation=&quot;http://www.springframework.org/schema/beans        http://www.springframework.org/schema/beans/spring-beans.xsd&quot;&gt;&lt;/beans&gt;</code></pre><h2>用注解装配，导入context约束和注解的支持</h2><pre><code class="language-xml">&lt;?xml version=&quot;1.0&quot; encoding=&quot;UTF-8&quot;?&gt;&lt;beans xmlns=&quot;http://www.springframework.org/schema/beans&quot;       xmlns:xsi=&quot;http://www.w3.org/2001/XMLSchema-instance&quot;       xmlns:context=&quot;http://www.springframework.org/schema/context&quot;       xsi:schemaLocation=&quot;        http://www.springframework.org/schema/beans        http://www.springframework.org/schema/beans/spring-beans.xsd                        http://www.springframework.org/schema/context        http://www.springframework.org/schema/context/spring-context.xsd&quot;&gt;    &lt;!--开启注解--&gt;   &lt;context:annotation-config/&gt;    &lt;!--指定扫描的包，注解才能生效--&gt;   &lt;context:component-scan base-package=&quot;com.chen.pojo&quot;/&gt;    &lt;/beans&gt;</code></pre><h2>AOP的实现</h2><ol><li><p>在pom.xml里导入依赖</p><pre><code class="language-xml">&lt;dependency&gt;    &lt;groupId&gt;org.aspectj&lt;/groupId&gt;    &lt;artifactId&gt;aspectjweaver&lt;/artifactId&gt;     &lt;version&gt;1.9.5&lt;/version&gt;&lt;/dependency&gt;</code></pre></li><li><p>在application.xml里导入aop约束</p><pre><code class="language-xml">&lt;?xml version=&quot;1.0&quot; encoding=&quot;UTF-8&quot;?&gt;&lt;beans xmlns=&quot;http://www.springframework.org/schema/beans&quot;       xmlns:aop=&quot;http://www.springframework.org/schema/aop&quot;       xmlns:xsi=&quot;http://www.w3.org/2001/XMLSchema-instance&quot;       xsi:schemaLocation=&quot;        http://www.springframework.org/schema/beans        http://www.springframework.org/schema/beans/spring-beans.xsd                 http://www.springframework.org/schema/aop        http://www.springframework.org/schema/aop/spring-aop.xsd&quot;&gt;&lt;/beans&gt;</code></pre></li></ol><h1>SpringMVC</h1><h2>pom.xml</h2><pre><code class="language-xml">&lt;?xml version=&quot;1.0&quot; encoding=&quot;UTF-8&quot;?&gt;&lt;project xmlns=&quot;http://maven.apache.org/POM/4.0.0&quot;         xmlns:xsi=&quot;http://www.w3.org/2001/XMLSchema-instance&quot;         xsi:schemaLocation=&quot;http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd&quot;&gt;    &lt;modelVersion&gt;4.0.0&lt;/modelVersion&gt;    &lt;groupId&gt;org.example&lt;/groupId&gt;    &lt;artifactId&gt;springmvc&lt;/artifactId&gt;    &lt;version&gt;1.0-SNAPSHOT&lt;/version&gt;    &lt;dependencies&gt;       &lt;!--spring-webmvc--&gt;       &lt;dependency&gt;            &lt;groupId&gt;org.springframework&lt;/groupId&gt;            &lt;artifactId&gt;spring-webmvc&lt;/artifactId&gt;            &lt;version&gt;5.2.6.RELEASE&lt;/version&gt;       &lt;/dependency&gt;&lt;/dependencies&gt;    &lt;!--处理静态资源的路径问题--&gt;    &lt;build&gt;        &lt;resources&gt;            &lt;resource&gt;                &lt;directory&gt;src/main/resources&lt;/directory&gt;                &lt;includes&gt;                    &lt;include&gt;**/*.properties&lt;/include&gt;                    &lt;include&gt;**/*.xml&lt;/include&gt;                &lt;/includes&gt;                &lt;filtering&gt;false&lt;/filtering&gt;            &lt;/resource&gt;            &lt;resource&gt;                &lt;directory&gt;src/main/java&lt;/directory&gt;                &lt;includes&gt;                    &lt;include&gt;**/*.properties&lt;/include&gt;                    &lt;include&gt;**/*.xml&lt;/include&gt;                &lt;/includes&gt;                &lt;filtering&gt;false&lt;/filtering&gt;            &lt;/resource&gt;        &lt;/resources&gt;    &lt;/build&gt;&lt;/project&gt;</code></pre><h2>web.xml</h2><pre><code class="language-xml">&lt;?xml version=&quot;1.0&quot; encoding=&quot;UTF-8&quot;?&gt;&lt;web-app xmlns=&quot;http://xmlns.jcp.org/xml/ns/javaee&quot;         xmlns:xsi=&quot;http://www.w3.org/2001/XMLSchema-instance&quot;         xsi:schemaLocation=&quot;http://xmlns.jcp.org/xml/ns/javaee http://xmlns.jcp.org/xml/ns/javaee/web-app_4_0.xsd&quot;         version=&quot;4.0&quot;&gt;    &lt;!--注册DispatcherServlet--&gt;    &lt;servlet&gt;        &lt;servlet-name&gt;springmvc&lt;/servlet-name&gt;        &lt;servlet-class&gt;org.springframework.web.servlet.DispatcherServlet&lt;/servlet-class&gt;        &lt;!--为DispatcherServlet绑定配置文件--&gt;        &lt;init-param&gt;            &lt;param-name&gt;contextConfigLocation&lt;/param-name&gt;            &lt;param-value&gt;classpath:springmvc-servlet.xml&lt;/param-value&gt;        &lt;/init-param&gt;        &lt;!--设置启动优先级为1--&gt;        &lt;load-on-startup&gt;1&lt;/load-on-startup&gt;    &lt;/servlet&gt;    &lt;!--控制拦截/处理除了jsp的所有请求，/*处理所有请求--&gt;    &lt;servlet-mapping&gt;        &lt;servlet-name&gt;springmvc&lt;/servlet-name&gt;        &lt;url-pattern&gt;/&lt;/url-pattern&gt;    &lt;/servlet-mapping&gt; &lt;!--解决乱码问题--&gt;    &lt;filter&gt;        &lt;filter-name&gt;encoding&lt;/filter-name&gt;        &lt;filter-class&gt;org.springframework.web.filter.CharacterEncodingFilter&lt;/filter-class&gt;        &lt;init-param&gt;            &lt;param-name&gt;encoding&lt;/param-name&gt;            &lt;param-value&gt;utf-8&lt;/param-value&gt;        &lt;/init-param&gt;    &lt;/filter&gt;    &lt;filter-mapping&gt;        &lt;filter-name&gt;encoding&lt;/filter-name&gt;        &lt;url-pattern&gt;/*&lt;/url-pattern&gt;    &lt;/filter-mapping&gt;&lt;/web-app&gt;</code></pre><h2>springmvc-servlet.xml</h2><pre><code class="language-xml">&lt;?xml version=&quot;1.0&quot; encoding=&quot;UTF-8&quot;?&gt;&lt;beans xmlns=&quot;http://www.springframework.org/schema/beans&quot;       xmlns:xsi=&quot;http://www.w3.org/2001/XMLSchema-instance&quot;       xmlns:context=&quot;http://www.springframework.org/schema/context&quot;       xmlns:mvc=&quot;http://www.springframework.org/schema/mvc&quot;       xsi:schemaLocation=&quot;http://www.springframework.org/schema/beans        http://www.springframework.org/schema/beans/spring-beans.xsd        http://www.springframework.org/schema/context        https://www.springframework.org/schema/context/spring-context.xsd        http://www.springframework.org/schema/mvc        https://www.springframework.org/schema/mvc/spring-mvc.xsd&quot;&gt;&lt;!--开启注解扫描--&gt;    &lt;context:component-scan base-package=&quot;com.yw.controller&quot;/&gt;    &lt;!--处理静态资源--&gt;    &lt;mvc:default-servlet-handler/&gt;    &lt;!--注册所需要的类--&gt;    &lt;mvc:annotation-driven/&gt;&lt;!--注册配置视图解析器--&gt;    &lt;bean class=&quot;org.springframework.web.servlet.view.InternalResourceViewResolver&quot;    id = &quot;internalResourceViewResolver&quot;&gt;        &lt;property name=&quot;prefix&quot; value=&quot;/WEB-INF/jsp/&quot;/&gt;        &lt;property name=&quot;suffix&quot; value=&quot;.jsp&quot;/&gt;    &lt;/bean&gt;&lt;!--处理Json的乱码问题--&gt;    &lt;mvc:annotation-driven&gt;        &lt;mvc:message-converters register-defaults=&quot;true&quot;&gt;            &lt;bean class=&quot;org.springframework.http.converter.StringHttpMessageConverter&quot;&gt;                &lt;constructor-arg value=&quot;UTF-8&quot;/&gt;            &lt;/bean&gt;            &lt;bean class=&quot;org.springframework.http.converter.json.MappingJackson2HttpMessageConverter&quot;&gt;                &lt;property name=&quot;objectMapper&quot;&gt;                    &lt;bean class=&quot;org.springframework.http.converter.json.Jackson2ObjectMapperFactoryBean&quot;&gt;                        &lt;property name=&quot;failOnEmptyBeans&quot; value=&quot;false&quot;/&gt;                    &lt;/bean&gt;                &lt;/property&gt;            &lt;/bean&gt;        &lt;/mvc:message-converters&gt;    &lt;/mvc:annotation-driven&gt;&lt;/beans&gt;</code></pre><h1>SpringBoot</h1><p>由于SpringBoot比较特殊，他的配置用的很多，而且大多都是关于整合其他的依赖来使用，所以这里就只是简单的做一个目录，具体的整合细节都移步到文章中好了。</p><h2>SpringBoot配置Pom.xml</h2><p>因人而异，此处我使用了阿里脚手架上比较标准的一套配置文件。</p><pre><code class="language-xml">&lt;?xml version=&quot;1.0&quot; encoding=&quot;UTF-8&quot;?&gt;&lt;project xmlns=&quot;http://maven.apache.org/POM/4.0.0&quot; xmlns:xsi=&quot;http://www.w3.org/2001/XMLSchema-instance&quot;xsi:schemaLocation=&quot;http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd&quot;&gt;&lt;modelVersion&gt;4.0.0&lt;/modelVersion&gt;&lt;groupId&gt;com.yww&lt;/groupId&gt;&lt;artifactId&gt;server&lt;/artifactId&gt;&lt;version&gt;0.0.1&lt;/version&gt;&lt;name&gt;server&lt;/name&gt;&lt;description&gt;阿里的配置配置文件&lt;/description&gt;&lt;properties&gt;&lt;java.version&gt;8&lt;/java.version&gt;&lt;project.build.sourceEncoding&gt;UTF-8&lt;/project.build.sourceEncoding&gt;&lt;project.reporting.outputEncoding&gt;UTF-8&lt;/project.reporting.outputEncoding&gt;&lt;spring-boot.version&gt;2.7.3&lt;/spring-boot.version&gt;&lt;/properties&gt;   &lt;dependencies&gt;        &lt;!--    SpringBoot Starter    --&gt;&lt;dependency&gt;&lt;groupId&gt;org.springframework.boot&lt;/groupId&gt;&lt;artifactId&gt;spring-boot-starter&lt;/artifactId&gt;&lt;/dependency&gt;&lt;!--    Web启动器    --&gt;&lt;dependency&gt;&lt;groupId&gt;org.springframework.boot&lt;/groupId&gt;&lt;artifactId&gt;spring-boot-starter-web&lt;/artifactId&gt;&lt;/dependency&gt;&lt;!--    单元测试   --&gt;&lt;dependency&gt;&lt;groupId&gt;org.springframework.boot&lt;/groupId&gt;&lt;artifactId&gt;spring-boot-starter-test&lt;/artifactId&gt;&lt;scope&gt;test&lt;/scope&gt;&lt;/dependency&gt;&lt;/dependencies&gt;&lt;!-- SpringBoot版本 --&gt;&lt;dependencyManagement&gt;&lt;dependencies&gt;&lt;dependency&gt;&lt;groupId&gt;org.springframework.boot&lt;/groupId&gt;&lt;artifactId&gt;spring-boot-dependencies&lt;/artifactId&gt;&lt;version&gt;${spring-boot.version}&lt;/version&gt;&lt;type&gt;pom&lt;/type&gt;&lt;scope&gt;import&lt;/scope&gt;&lt;/dependency&gt;&lt;/dependencies&gt;&lt;/dependencyManagement&gt;&lt;build&gt;&lt;!--    资源导出配置    --&gt;&lt;resources&gt;&lt;resource&gt;&lt;directory&gt;src/main/java&lt;/directory&gt;&lt;includes&gt;&lt;include&gt;**/*.xml&lt;/include&gt;&lt;/includes&gt;&lt;filtering&gt;false&lt;/filtering&gt;&lt;/resource&gt;&lt;/resources&gt;&lt;plugins&gt;&lt;plugin&gt;&lt;groupId&gt;org.apache.maven.plugins&lt;/groupId&gt;&lt;artifactId&gt;maven-compiler-plugin&lt;/artifactId&gt;&lt;version&gt;3.8.1&lt;/version&gt;&lt;configuration&gt;&lt;source&gt;1.8&lt;/source&gt;&lt;target&gt;1.8&lt;/target&gt;&lt;encoding&gt;UTF-8&lt;/encoding&gt;&lt;/configuration&gt;&lt;/plugin&gt;&lt;plugin&gt;&lt;groupId&gt;org.springframework.boot&lt;/groupId&gt;&lt;artifactId&gt;spring-boot-maven-plugin&lt;/artifactId&gt;&lt;version&gt;${spring-boot.version}&lt;/version&gt;&lt;configuration&gt;&lt;mainClass&gt;com.yww.blog.BlogApplication&lt;/mainClass&gt;&lt;/configuration&gt;&lt;executions&gt;&lt;execution&gt;&lt;id&gt;repackage&lt;/id&gt;&lt;goals&gt;&lt;goal&gt;repackage&lt;/goal&gt;&lt;/goals&gt;&lt;/execution&gt;&lt;/executions&gt;&lt;/plugin&gt;&lt;/plugins&gt;&lt;/build&gt;    &lt;/project&gt;</code></pre><h2>JDBC</h2><p>指路<a href="https://yww52.com/2020/11/24/SpringBoot%E6%95%B4%E5%90%88JDBC/">整合JDBC</a></p><h2>Druid</h2><p>指路<a href="https://yww52.com/2020/11/24/SpringBoot%E6%95%B4%E5%90%88Druid/">整合Druid</a></p><h2>Mybatis</h2><p>指路<a href="https://yww52.com/2020/11/24/SpringBoot%E6%95%B4%E5%90%88Mybatis/">整合Mybatis</a></p><h2>Swagger</h2><h3>Swagger2</h3><p>指路<a href="https://yww52.com/2020/10/25/Swagger/">集成Swagger2</a></p><h3>Swagger3</h3><p>指路<a href="https://yww52.com/2020/11/02/Swagger3/">集成Swagger3</a></p><h2>Mybatis-Plus</h2><p>指路<a href="https://yww52.com/2020/11/13/Mybatis-plus/">Mybatis-Plus</a></p><h2>Shiro</h2><p>指路<a href="https://yww52.com/2021/01/24/Shiro/">Shiro</a></p><h2>Redis</h2><p>指路<a href="https://yww52.com/2021/04/01/%E6%95%B4%E5%90%88Redis/">整合Redis</a></p><p>指路<a href="https://yww52.com/2021/02/14/Redis/">Redis的学习</a></p><h2>结果类封装</h2><p>指路<a href="https://yww52.com/2021/05/21/%E7%BB%93%E6%9E%9C%E7%B1%BB%E5%B0%81%E8%A3%85/">结果类封装</a></p><h2>异常处理</h2><p>指路<a href="https://yww52.com/2021/10/09/%E5%BC%82%E5%B8%B8%E5%A4%84%E7%90%86/">异常处理</a></p>]]></content:encoded>
            <category domain="https://yww52.com/categories/%25E6%2588%2591%25E7%259A%2584%25E6%258A%2580%25E6%259C%25AF%25E5%25AD%25A6%25E4%25B9%25A0%25E7%2589%25A9%25E8%25AF%25AD%25E6%259E%259C%25E7%2584%25B6%25E6%259C%2589%25E9%2597%25AE%25E9%25A2%2598/">我的技术学习物语果然有问题</category>
        </item>
        <item>
            <title>Spring5</title>
            <link>https://yww52.com/posts/7f09f5da/</link>
            <guid isPermaLink="true">https://yww52.com/posts/7f09f5da/</guid>
            <pubDate>Fri, 26 Jun 2020 00:00:00 +0000</pubDate>
            <description>Spring 是一个轻量级的控制反转（IOC）和面向切面（AOP）的框架</description>
            <content:encoded><![CDATA[<img src="https://img.yww52.com/2020/6/2020-6-26top_img.jpg" alt="Spring5" style="max-width:100%;height:auto;margin-bottom:15px;" /><h2>简介</h2><p>​Spring是Java EE编程领域的一个轻量级开源框架，该框架由一个叫Rod Johnson的程序员在 2002 年最早提出并随后创建，是为了解决企业级编程开发中的复杂性，实现敏捷开发的应用型框架 。Spring是一个开源容器框架，它集成各类型的工具，通过核心的Bean factory实现了底层的类的实例化和生命周期的管理。在整个框架中，各类型的功能被抽象成一个个的 Bean，这样就可以实现各种功能的管理，包括动态加载和切面编程。</p><blockquote><p>Spring 就是一个轻量级的控制反转<a href="https://yww52.com/2020/06/22/IOC/">IOC</a>和面向切面<a href="https://yww52.com/2020/06/29/AOP/">AOP</a>的框架。</p></blockquote><p>参考网站：</p><ul><li><a href="https://spring.io/projects/spring-framework#overview">官网</a></li><li><a href="https://repo.spring.io/release/org/springframework/spring/">官方下载地址</a></li><li><a href="https://github.com/spring-projects/spring-framework">GitHub地址</a></li></ul><h2>第一个Spring程序</h2><ol><li><p>创建一个maven项目</p></li><li><p>在pom.xml里导入依赖</p><pre><code class="language-xml">&lt;dependencies&gt;        &lt;dependency&gt;            &lt;groupId&gt;org.springframework&lt;/groupId&gt;            &lt;artifactId&gt;spring-webmvc&lt;/artifactId&gt;            &lt;version&gt;5.2.6.RELEASE&lt;/version&gt;        &lt;/dependency&gt;    &lt;/dependencies&gt;</code></pre></li><li><p>创建一个类，比如我的HelloSpring类</p><pre><code class="language-Java">public class HelloSpring {    private String str;    public String getStr(){        return  str;    }    public void setStr(String str){        this.str = str;    }    @Override    public String toString(){        return &quot;{&quot;+&quot;str=&quot;+str+&quot;}&quot;;    }}</code></pre></li><li><p>在resources资源目录下配置applicationContext.xml</p><pre><code class="language-xml">&lt;?xml version=&quot;1.0&quot; encoding=&quot;UTF-8&quot;?&gt;&lt;beans xmlns=&quot;http://www.springframework.org/schema/beans&quot;       xmlns:xsi=&quot;http://www.w3.org/2001/XMLSchema-instance&quot;       xsi:schemaLocation=&quot;http://www.springframework.org/schema/beans        http://www.springframework.org/schema/beans/spring-beans.xsd&quot;&gt;&lt;!--使用Spring来创建对象，这些对象都称为bean--&gt;    &lt;bean id =&quot;hello&quot; class=&quot;com.chen.pojo.HelloSpring&quot;&gt;        &lt;property name=&quot;str&quot; value=&quot; Hello Spring&quot;/&gt;    &lt;/bean&gt;        &lt;/beans&gt;</code></pre></li><li><p>测试类</p><pre><code class="language-Java">public class test01 {    public static void main(String[] args) {        ApplicationContext context = new ClassPathXmlApplicationContext(&quot;applicationContext.xml&quot;);        HelloSpring hello = context.getBean(&quot;hello&quot;，HelloSpring.class);        System.out.println(hello.toString());    }}//得到结果，输出    {str= Hello Spring}</code></pre></li></ol><h2>Spring配置</h2><h3>别名</h3><p>为创建的对象起别名，即为bean的id起别名</p><pre><code class="language-xml">&lt;alias name=&quot;userserviceimpl&quot; alias=&quot;user&quot;&gt;&lt;/alias&gt;</code></pre><h3>Bean</h3><p>以上面的例子为例</p><pre><code class="language-xml">&lt;bean id =&quot;hello&quot; class=&quot;com.chen.pojo.HelloSpring&quot; name =&quot;user,u1,u2&quot;&gt;        &lt;property name=&quot;str&quot; value=&quot; Hello Spring&quot;/&gt;    &lt;/bean&gt;</code></pre><p>:::note{type=&quot;success&quot;}</p><p>id为bean的标识符，即创建的对象名</p><p>class为对象对所对应的类名，注意是全限定名name为别名，与alias差不多，可以起多个别名</p><p>property属性，为str这个参数赋值</p><p>:::</p><h3>import</h3><p>合并xml配置文件。</p><h2>依赖注入</h2><p>依赖注入DI，具体说法可参考我之前写的<a href="https://yww52.com/2020/06/23/IOC">有关IOC的文章</a></p><h3>构造器注入</h3><ol><li><p>使用无参构造器创建对象这种是Spring默认的创建对象的方式，在上述的Spring例子当中，HelloSpring的bean创建对象是用的无参构造器，若没有了无参构造器就会报错。</p></li><li><p>使用有参构造器创建对象</p><ul><li><p>下标</p><pre><code class="language-xml">    &lt;bean id =&quot;hello&quot; class=&quot;com.chen.pojo.HelloSpring&quot;&gt;        &lt;constructor-arg index=&quot;0&quot; value=&quot;helloSpring&quot;&gt;;            &lt;!--从0开始，依次为HelloSpring的有参构造器赋值--&gt;    &lt;/bean&gt;</code></pre></li><li><p>类型</p><pre><code class="language-xml">    &lt;bean id =&quot;hello&quot; class=&quot;com.chen.pojo.HelloSpring&quot;&gt;        &lt;constructor-arg type=&quot;String&quot; value=&quot;helloSpring&quot;&gt;;            &lt;!--按照类型来赋值，但相同类型多了就不行了，还是不推荐这种--&gt;    &lt;/bean&gt;</code></pre></li><li><p>参数名</p><pre><code class="language-xml">    &lt;bean id =&quot;hello&quot; class=&quot;com.chen.pojo.HelloSpring&quot;&gt;        &lt;constructor-arg name=&quot;str&quot; value=&quot;helloSpring&quot;&gt;;            &lt;!--按构造器的参数名来赋值，即为HelloSpring的构造器参数str赋值--&gt;    &lt;/bean&gt;</code></pre></li></ul></li></ol><blockquote><p> 当bean被注册，加载到该配置文件，即使没有用到这个对象，也会生成。</p></blockquote><h3>Set方式注入</h3><p>注入到对象中的属性，例如我的这个例子。</p><pre><code class="language-Java">public class Student {    private String name;    private Address address;    private String[] books;    private List&lt;String&gt; hobbies;    private Map&lt;String,String&gt; card;    private Set&lt;String&gt; games;    private String ll;    private Properties info;    @Override    public String toString() {        return &quot;Student{&quot; +                &quot;name=&#39;&quot; + name + &#39;\&#39;&#39; +                &quot;, address=&quot; + address +                &quot;, books=&quot; + Arrays.toString(books) +                &quot;, hobbies=&quot; + hobbies +                &quot;, card=&quot; + card +                &quot;, games=&quot; + games +                &quot;, ll=&#39;&quot; + ll + &#39;\&#39;&#39; +                &quot;, info=&quot; + info +                &#39;}&#39;;    }    public String getName() {        return name;    }    public void setName(String name) {        this.name = name;    }    public Address getAddress() {        return address;    }    public void setAddress(Address address) {        this.address = address;    }    public String[] getBooks() {        return books;    }    public void setBooks(String[] books) {        this.books = books;    }    public List&lt;String&gt; getHobbies() {        return hobbies;    }    public void setHobbies(List&lt;String&gt; hobbies) {        this.hobbies = hobbies;    }    public Map&lt;String, String&gt; getCard() {        return card;    }    public void setCard(Map&lt;String, String&gt; card) {        this.card = card;    }    public Set&lt;String&gt; getGames() {        return games;    }    public void setGames(Set&lt;String&gt; games) {        this.games = games;    }    public String getLl() {        return ll;    }    public void setLl(String ll) {        this.ll = ll;    }    public Properties getInfo() {        return info;    }    public void setInfo(Properties info) {        this.info = info;    }}</code></pre><ol><li><p>基本数据类型和String类型的注入</p><pre><code class="language-xml">&lt;bean id=&quot;student&quot; class=&quot;com.chen.pojo.Student&quot;&gt;        &lt;property name=&quot;name&quot; value=&quot;student1&quot;/&gt;&lt;/bean&gt;</code></pre></li><li><p>引用类型的注入</p><pre><code class="language-xml">&lt;!--先注册好这个引用类型--&gt;     &lt;bean id=&quot;adress&quot; class=&quot;com.chen.pojo.Address&quot;/&gt;&lt;bean id=&quot;student&quot; class=&quot;com.chen.pojo.Student&quot;&gt;        &lt;property name=&quot;address&quot; ref=&quot;adress&quot;/&gt;&lt;/bean&gt;</code></pre></li><li><p>数组的注入</p><pre><code class="language-xml">&lt;bean id=&quot;student&quot; class=&quot;com.chen.pojo.Student&quot;&gt;        &lt;property name=&quot;books&quot;&gt;            &lt;array&gt;                &lt;value&gt;a&lt;/value&gt;                &lt;value&gt;b&lt;/value&gt;                &lt;value&gt;c&lt;/value&gt;                &lt;value&gt;d&lt;/value&gt;            &lt;/array&gt;        &lt;/property&gt;&lt;/bean&gt;</code></pre></li><li><p>List的注入</p><pre><code class="language-xml">&lt;bean id=&quot;student&quot; class=&quot;com.chen.pojo.Student&quot;&gt;        &lt;property name=&quot;hobbies&quot;&gt;            &lt;list&gt;                &lt;value&gt;music&lt;/value&gt;                &lt;value&gt;game&lt;/value&gt;            &lt;/list&gt;        &lt;/property&gt;&lt;/bean&gt;</code></pre></li><li><p>Map的注入</p><pre><code class="language-xml">&lt;bean id=&quot;student&quot; class=&quot;com.chen.pojo.Student&quot;&gt;         &lt;property name=&quot;card&quot;&gt;            &lt;map&gt;                &lt;entry key=&quot;1&quot; value=&quot;a&quot;/&gt;                &lt;entry key=&quot;2&quot; value=&quot;b&quot;/&gt;                &lt;entry key=&quot;3&quot; value=&quot;c&quot;/&gt;                &lt;entry key=&quot;4&quot; value=&quot;d&quot;/&gt;            &lt;/map&gt;        &lt;/property&gt;&lt;/bean&gt;</code></pre></li><li><p>Set的注入</p><pre><code class="language-xml">&lt;bean id=&quot;student&quot; class=&quot;com.chen.pojo.Student&quot;&gt;         &lt;property name=&quot;games&quot;&gt;            &lt;set&gt;                &lt;value&gt;kkk&lt;/value&gt;                &lt;value&gt;sss&lt;/value&gt;                &lt;value&gt;www&lt;/value&gt;            &lt;/set&gt;        &lt;/property&gt;&lt;/bean&gt;</code></pre></li><li><p>null注入即空值注入</p><pre><code class="language-xml">&lt;bean id=&quot;student&quot; class=&quot;com.chen.pojo.Student&quot;&gt;    &lt;property name=&quot;ll&quot;&gt;            &lt;null/&gt;        &lt;/property&gt;&lt;/bean&gt;</code></pre></li><li><p>Properties的注入</p><pre><code class="language-xml">&lt;bean id=&quot;student&quot; class=&quot;com.chen.pojo.Student&quot;&gt;    &lt;property name=&quot;info&quot;&gt;            &lt;props&gt;                &lt;prop key=&quot;s&quot;&gt;111&lt;/prop&gt;                &lt;prop key=&quot;ww&quot;&gt;333&lt;/prop&gt;            &lt;/props&gt;        &lt;/property&gt;&lt;/bean&gt;</code></pre></li></ol><p>:::note{type=&quot;info&quot;}</p><p>注入的时候是通过你创建的set方法注入的，所以创建类的时候要写上set方法，养成习惯get方法也写上,为了更好的测试，就重写toString方法拓展方式注入</p><p>:::</p><h4>P命名空间的注入</h4><p>想要使用这种方法，先得在bean元素上加入p的命名空间，如下</p><pre><code class="language-xml">&lt;?xml version=&quot;1.0&quot; encoding=&quot;UTF-8&quot;?&gt;&lt;beans xmlns=&quot;http://www.springframework.org/schema/beans&quot;       xmlns:xsi=&quot;http://www.w3.org/2001/XMLSchema-instance&quot;              xmlns:p=&quot;http://www.springframework.org/schema/p&quot;              xsi:schemaLocation=&quot;http://www.springframework.org/schema/beans        http://www.springframework.org/schema/beans/spring-beans.xsd&quot;&gt;&lt;/beans&gt;</code></pre><pre><code class="language-xml">&lt;bean id=&quot;adress&quot; class=&quot;com.chen.pojo.Address&quot; p:address=&quot;this&quot; p:a=&quot;18&quot;/&gt;</code></pre><p>:::note{type=&quot;info&quot;}</p><p>这种方式的注入是简单类型的注入，引用类型也可以,类似set注入，但能支持注入的类型少。</p><p>:::</p><h4>C命名空间注入</h4><p>类似P命名空间，用前需要在bean元素上加入c的命名空间，如下</p><pre><code class="language-xml">&lt;?xml version=&quot;1.0&quot; encoding=&quot;UTF-8&quot;?&gt;&lt;beans xmlns=&quot;http://www.springframework.org/schema/beans&quot;       xmlns:xsi=&quot;http://www.w3.org/2001/XMLSchema-instance&quot;       xmlns:c=&quot;http://www.springframework.org/schema/c&quot;       xsi:schemaLocation=&quot;http://www.springframework.org/schema/beans        http://www.springframework.org/schema/beans/spring-beans.xsd&quot;&gt;&lt;/beans&gt;</code></pre><pre><code class="language-xml">&lt;bean id=&quot;adress&quot; class=&quot;com.chen.pojo.Address&quot; c:address=&quot;this&quot; c:a=&quot;18&quot;/&gt;</code></pre><p>:::note{type=&quot;info&quot;}</p><p>这种方式的是通过构造器注入的，想要注入的属性就要存在这样的有参构造器。注意因为创建对象要用到无参构造器，所以不要漏写。</p><p>:::</p><h2>Bean的作用域（Scope）</h2><h3>单例模式</h3><p>单例模式是指每次创建的bean都是同一个，这也是Spring默认的模式，比如</p><pre><code class="language-xml">&lt;!--singleton表示单例模式，不写scope也默认为单例--&gt;&lt;bean id = &quot;address&quot; class=&quot;com.chen.pojo.Address&quot; scope=&quot;singleton&quot;&gt;        &lt;property name=&quot;a&quot; value=&quot;1&quot;/&gt;        &lt;property name=&quot;address&quot; value=&quot;s&quot;/&gt;    &lt;/bean&gt;</code></pre><pre><code class="language-Java">//测试类，两个get的对象都是一样的public class test01 {    public static void main(String[] args) {        ApplicationContext context = new ClassPathXmlApplicationContext(&quot;applicationContext.xml&quot;);        Address address1 =  context.getBean(&quot;address&quot;,Address.class);        Address address2 =  context.getBean(&quot;address&quot;,Address.class);        System.out.println(address1.hashCode());        System.out.println(address2.hashCode());    }}</code></pre><blockquote><p>单例模式表示bean只会创建一个对象，每次get的都是同一个</p></blockquote><h3>原型模式</h3><p>原型模式就和单例模式相反，每次get的都不是同一个。</p><pre><code class="language-xml">&lt;!--每次创建都不是同一个对象--&gt;&lt;bean id = &quot;address&quot; class=&quot;com.chen.pojo.Address&quot; scope=&quot;prototype&quot;&gt;        &lt;property name=&quot;a&quot; value=&quot;1&quot;/&gt;        &lt;property name=&quot;address&quot; value=&quot;s&quot;/&gt;    &lt;/bean&gt;</code></pre><pre><code class="language-Java">//测试类，两个get的对象都是一样的public class test01 {    public static void main(String[] args) {        ApplicationContext context = new ClassPathXmlApplicationContext(&quot;applicationContext.xml&quot;);        Address address1 =  context.getBean(&quot;address&quot;,Address.class);        Address address2 =  context.getBean(&quot;address&quot;,Address.class);        System.out.println(address1.hashCode());        System.out.println(address2.hashCode());    }}</code></pre><blockquote><p>原型模式表示每次创建的bean都不是同一个对象</p></blockquote><h2>Spring的自动装配</h2><p>首先为了更好的测试，先创建两个类，之后的例子就从这两个类中测试。</p><pre><code class="language-Java">public class User {    private String name;     private Score score;        public Score getScore() {        return score1;    }    public void setScore(Score score) {        this.score1 = score;    }    public String getName() {        return name;    }    public void setName(String name) {        this.name = name;    }}</code></pre><pre><code class="language-Java">public class Score {     private String name;        public String getName() {        return name;    }    public void setName(String name) {        this.name = name;    }    @Override    public String toString() {        return &quot;Score{&quot; +                &quot;name=&#39;&quot; + name + &#39;\&#39;&#39; +                &#39;}&#39;;    }}</code></pre><blockquote><p>自动装配是从bean的autowire属性配置的。</p></blockquote><h3>byName的自动装配</h3><p>byName的自动装配是从类中寻找具有相同属性的类，然后装配到bean里，比如在User中有Score类创建的对象，而且有set方法，所以可以自动将Score的对象装配到User类里。</p><pre><code class="language-xml">&lt;?xml version=&quot;1.0&quot; encoding=&quot;UTF-8&quot;?&gt;&lt;beans xmlns=&quot;http://www.springframework.org/schema/beans&quot;       xmlns:xsi=&quot;http://www.w3.org/2001/XMLSchema-instance&quot;       xsi:schemaLocation=&quot;http://www.springframework.org/schema/beans        http://www.springframework.org/schema/beans/spring-beans.xsd&quot;&gt;    &lt;bean id=&quot;score&quot; class=&quot;com.chen.pojo.Score&quot;&gt;        &lt;property name=&quot;name&quot; value=&quot;课程&quot;/&gt;    &lt;/bean&gt;    &lt;bean id=&quot;user&quot; class=&quot;com.chen.pojo.User&quot; autowire=&quot;byName&quot;&gt;        &lt;property name=&quot;name&quot; value=&quot;张三&quot;/&gt;    &lt;/bean&gt;    &lt;!--  自动装配等同于这样装配        &lt;bean id=&quot;user&quot; class=&quot;com.chen.pojo.User&quot; &gt;            &lt;property name=&quot;name&quot; value=&quot;张三&quot;/&gt;            &lt;property name=&quot;score&quot; ref=&quot;score&quot;/&gt;        &lt;/bean&gt;    --&gt;&lt;/beans&gt;</code></pre><pre><code class="language-java">public class test01 {    public static void main(String[] args) {        ApplicationContext context = new ClassPathXmlApplicationContext(&quot;applicationContext.xml&quot;);        User user = context.getBean(&quot;user&quot;, User.class);        System.out.println(user.getScore());    }}</code></pre><p>:::note{type=&quot;warning&quot;}</p><p>是按照User类里的Set方法寻找的，比如Score类在bean里注册的id为score，那User类里就要有setScore这个set方法，将set去掉，首字母小写，然后与id对应一致才能找的到，这是byName的自动装配。</p><p>:::</p><h3>byTypee的自动装配</h3><pre><code class="language-xml">&lt;?xml version=&quot;1.0&quot; encoding=&quot;UTF-8&quot;?&gt;&lt;beans xmlns=&quot;http://www.springframework.org/schema/beans&quot;       xmlns:xsi=&quot;http://www.w3.org/2001/XMLSchema-instance&quot;       xsi:schemaLocation=&quot;http://www.springframework.org/schema/beans        http://www.springframework.org/schema/beans/spring-beans.xsd&quot;&gt;        &lt;bean id=&quot;score11&quot; class=&quot;com.chen.pojo.Score&quot;&gt;        &lt;property name=&quot;name&quot; value=&quot;课程&quot;/&gt;    &lt;/bean&gt;    &lt;!--这里修改了id,用byName是找不到的--&gt;        &lt;bean id=&quot;user&quot; class=&quot;com.chen.pojo.User&quot; autowire=&quot;byType&quot;&gt;        &lt;property name=&quot;name&quot; value=&quot;张三&quot;/&gt;    &lt;/bean&gt;    &lt;!--  自动装配等同于这样装配        &lt;bean id=&quot;user&quot; class=&quot;com.chen.pojo.User&quot; &gt;            &lt;property name=&quot;name&quot; value=&quot;张三&quot;/&gt;            &lt;property name=&quot;score&quot; ref=&quot;score&quot;/&gt;        &lt;/bean&gt;    --&gt;&lt;/beans&gt;</code></pre><pre><code class="language-java">public class test01 {    public static void main(String[] args) {        ApplicationContext context = new ClassPathXmlApplicationContext(&quot;applicationContext.xml&quot;);        User user = context.getBean(&quot;user&quot;, User.class);        System.out.println(user.getScore());    }}</code></pre><p>:::note{type=&quot;warning&quot;}</p><p>byType是寻找类里属性类型相同的bean，所以不关id的事情了，id都可以省略。弊端就是当一个类注册了两个bean的时候，byType不知道选择哪一个就会报错。</p><p>:::</p><h3>用注解实现自动装配</h3><p>首先想用注解先要导入context约束和注解的支持，如下</p><pre><code class="language-xml">&lt;?xml version=&quot;1.0&quot; encoding=&quot;UTF-8&quot;?&gt;&lt;beans xmlns=&quot;http://www.springframework.org/schema/beans&quot;       xmlns:xsi=&quot;http://www.w3.org/2001/XMLSchema-instance&quot;              xmlns:context=&quot;http://www.springframework.org/schema/context&quot;              xsi:schemaLocation=&quot;        http://www.springframework.org/schema/beans        http://www.springframework.org/schema/beans/spring-beans.xsd                                   http://www.springframework.org/schema/context        http://www.springframework.org/schema/context/spring-context.xsd&quot;&gt;    &lt;!--开启注解--&gt;   &lt;context:annotation-config/&gt;    &lt;/beans&gt;</code></pre><p>@Autowired注解这个注解可以放在属性名上，也可以放在set方法上，放在属性名上，连set方法都可以不用写，当然是引用的类里要有set方法。</p><pre><code class="language-xml">&lt;bean  class=&quot;com.chen.pojo.Score&quot;&gt;        &lt;property name=&quot;name&quot; value=&quot;课程&quot;/&gt;    &lt;/bean&gt;&lt;bean id=&quot;user&quot; class=&quot;com.chen.pojo.User&quot; &gt;    &lt;/bean&gt;</code></pre><pre><code class="language-java">public class User {     private String name;     @Autowired     private Score score;        public Score getScore() {        return score1;    }    public void setScore1(Score score) {        this.score1 = score;    }    public String getName() {        return name;    }    public void setName(String name) {        this.name = name;    }}</code></pre><blockquote><p>@Autowired里面有个属性是required，默认为true，表示不能为空值，可以手动关闭@Autowired(required = falser)。</p></blockquote><p>:::note{type=&quot;info&quot;}</p><p>@Autowired是通过byType模式来寻找的，若是一个类有多个bean才会选择用byName来自动装配。   要是多个bean的id都不符合byName的时候，就需要使用@Qualifier(value=&quot;id&quot;)来配合使用，value的值为哪个bean的id，就使用哪个bean自动装配。</p><p>:::</p><pre><code class="language-xml">&lt;bean id=&quot;score1&quot; class=&quot;com.chen.pojo.Score&quot;&gt;        &lt;property name=&quot;name&quot; value=&quot;课程1&quot;/&gt;    &lt;/bean&gt;&lt;bean id=&quot;score2&quot; class=&quot;com.chen.pojo.Score&quot;&gt;        &lt;property name=&quot;name&quot; value=&quot;课程2&quot;/&gt;    &lt;/bean&gt;&lt;bean id=&quot;score3&quot; class=&quot;com.chen.pojo.Score&quot;&gt;        &lt;property name=&quot;name&quot; value=&quot;课程3&quot;/&gt;    &lt;/bean&gt;    &lt;bean id=&quot;user&quot; class=&quot;com.chen.pojo.User&quot; &gt;    &lt;/bean&gt;&lt;!-- user类里的score属性上注解，value值为哪个id就配置哪个id@Autowired    @Qualifier(value = &quot;score2&quot;)    private Score score;--&gt;</code></pre><h2>使用注解开发</h2><p>想使用注解来进行开发，先要确定好自己的配置。</p><ol><li><p>导入aop的包（之前导入的依赖包含了aop的包），注解使用不了就确定一下有没有aop的包导入</p></li><li><p>导入context的约束，开启支持注解</p></li><li><p>还要指定扫描的包，是包里的注解生效，比如例子的包</p><pre><code class="language-xml">&lt;context:component-scan base-package=&quot;com.chen.pojo&quot;/&gt;</code></pre></li></ol><h3>注册bean</h3><p>在xml中注册bean</p><pre><code class="language-xml">&lt;bean id=&quot;score&quot; class=&quot;com.chen.pojo.Score&quot;&gt;    &lt;/bean&gt;</code></pre><p>使用注解来注册bean</p><pre><code class="language-Java">@Componentpublic class Score {private String name;    public String getName() {        return name;    }    public void setName(String name) {        this.name = name;    }    @Override    public String toString() {        return &quot;Score{&quot; +                &quot;name=&#39;&quot; + name + &#39;\&#39;&#39; +                &#39;}&#39;;    }}</code></pre><p>:::note{type=&quot;success&quot;}</p><p>使用了@Component注解，会将这个类装配。可以指定id,在没有指定id的时候，这个类的bean的id就是当前类名首字母小写的字符串。</p><p>指定id可以这样写到@Component(&quot;xxx&quot;)。</p><p>:::</p><p>:::note{type=&quot;info&quot;}</p><p>@Component 的三个衍生注解，在 web 开发中，会按照 mvc 三层架构分层。</p><ul><li>dao 层@Repository</li><li>service 层@Service</li><li>web 层@Controller</li></ul><p>这四个注解功能都是一样的。</p><p>:::</p><h3>属性注入</h3><pre><code class="language-Java">@Componentpublic class Score {    @Value(&quot;lll&quot;)private String name;    public String getName() {        return name;    }    public void setName(String name) {        this.name = name;    }    @Override    public String toString() {        return &quot;Score{&quot; +                &quot;name=&#39;&quot; + name + &#39;\&#39;&#39; +                &#39;}&#39;;    }}</code></pre><p>:::note{type=&quot;success&quot;}</p><p>属性注入可以使用@Value(&quot;&quot;)的注解，当然简单的才会使用注解，复杂一点的注入还是xml配置注解好一点。</p><p>:::</p><h3>自动装配</h3><p>就是之前得用注解实现自动装配</p><h3>作用域</h3><pre><code class="language-Java">@Component@Scope(&quot;prototype&quot;)public class Score {@Value(&quot;chen&quot;)    private String name;    public String getName() {        return name;    }    public void setName(String name) {        this.name = name;    }    @Override    public String toString() {        return &quot;Score{&quot; +                &quot;name=&#39;&quot; + name + &#39;\&#39;&#39; +                &#39;}&#39;;    }}</code></pre><p>:::note{type=&quot;success&quot;}</p><p>作用域可以使用@Scope(&quot;“)，里面是模式，常用的就是singleton和prototype，默认也是singleton。</p><p>:::</p><h2>使用Java的方式配置Spring</h2><p>先看demo吧</p><ol><li><p>用户类</p><pre><code class="language-Java">public class User {    @Value(&quot;chen&quot;)    private String name;    public String getName() {        return name;    }    public void setName(String name) {        this.name = name;    }}</code></pre></li><li><p>Java配置类</p><pre><code class="language-Java">@Configurationpublic class pojoconfig {    @Bean    public User getUser(){        return new User();    }}</code></pre></li><li><p>测试类</p><pre><code class="language-Java">public class test01 {    public static void main(String[] args) {        ApplicationContext context = new AnnotationConfigApplicationContext(pojoconfig.class);        User user = context.getBean(&quot;getUser&quot;,User.class);        System.out.println(user.getName());    }}</code></pre></li></ol><blockquote><p>一个个分析吧。</p><p>用户类没什么区别，主要是Java配置类。</p><p>@Configuration表示这个类是个配置类，类似于xml。</p><p>@Bean表示bean的配置。方法名就相当于id。</p></blockquote><h2>AOP</h2><p>关于AOP的介绍就放到另一篇文章里。<a href="https://yww52.com/2020/06/30/AOP/">AOP</a></p><p>这里就讲讲spring中AOP的具体实现。首先要导入依赖，和aop的约束</p><pre><code class="language-xml">&lt;dependency&gt;     &lt;groupId&gt;org.aspectj&lt;/groupId&gt;     &lt;artifactId&gt;aspectjweaver&lt;/artifactId&gt;     &lt;version&gt;1.9.5&lt;/version&gt;   &lt;/dependency&gt;</code></pre><pre><code class="language-xml">&lt;?xml version=&quot;1.0&quot; encoding=&quot;UTF-8&quot;?&gt;&lt;beans xmlns=&quot;http://www.springframework.org/schema/beans&quot;       xmlns:aop=&quot;http://www.springframework.org/schema/aop&quot;       xmlns:xsi=&quot;http://www.w3.org/2001/XMLSchema-instance&quot;       xsi:schemaLocation=&quot;http://www.springframework.org/schema/beans        http://www.springframework.org/schema/beans/spring-beans.xsd        http://www.springframework.org/schema/aop        http://www.springframework.org/schema/aop/spring-aop.xsd&quot;&gt;&lt;/beans&gt;</code></pre><h3>使用Spring的接口来实现</h3><ol><li><p>首先定义好 接口，并创建好真实对象，即接口实现类</p><pre><code class="language-Java">public interface userService {    void add();    void delete();    void update();    void select();}</code></pre><pre><code class="language-Java">public class userServiceImpl implements userService{    public void add() {        System.out.println(&quot;增加了一个用户&quot;);    }    public void delete() {        System.out.println(&quot;删除了一个用户&quot;);    }    public void update() {        System.out.println(&quot;修改了一个用户&quot;);    }    public void select() {        System.out.println(&quot;查询了一个用户&quot;);    }}</code></pre></li><li><p>定义一个功能类</p><pre><code class="language-Java">public class log implements MethodBeforeAdvice {    public void before(Method method, Object[] objects, Object target) throws Throwable {        System.out.println(target.getClass().getName()+&quot;的&quot;+method.getName()+&quot;被执行了&quot;);    }}</code></pre><blockquote><p>method表示目标对象的方法，objects表示参数,target目标对象。实验的例子是使用MethodBeforeAdvice这个接口，即在目标方法调用之前调用的Advice通知。</p><p>MethodBeforeAdvice             前置通知</p><p>AfterReturningAdvice             后置通知，有返回值</p><p>MethodInterceptor                 环绕通知ThrowsAdvice                         异常通知</p></blockquote></li><li><p>然后就要在applicationContext.xml里配置了</p><pre><code class="language-xml">&lt;?xml version=&quot;1.0&quot; encoding=&quot;UTF-8&quot;?&gt;&lt;beans xmlns=&quot;http://www.springframework.org/schema/beans&quot;       xmlns:aop=&quot;http://www.springframework.org/schema/aop&quot;       xmlns:xsi=&quot;http://www.w3.org/2001/XMLSchema-instance&quot;       xsi:schemaLocation=&quot;http://www.springframework.org/schema/beans        http://www.springframework.org/schema/beans/spring-beans.xsd        http://www.springframework.org/schema/aop        http://www.springframework.org/schema/aop/spring-aop.xsd&quot;&gt;    &lt;!--先注册真实对象和功能的类进入--&gt;    &lt;bean id=&quot;userServiceImpl&quot; class=&quot;com.chen.pojo.userServiceImpl&quot;&gt;&lt;/bean&gt;    &lt;bean id =&quot;log&quot; class=&quot;com.chen.pojo.log&quot;&gt;&lt;/bean&gt;    &lt;aop:config&gt;        &lt;!--定义切入点,expression为执行的位置--&gt;        &lt;aop:pointcut id=&quot;p&quot; expression=&quot;execution(* com.chen.pojo.userServiceImpl.*(..))&quot;/&gt;        &lt;!--执行环绕，advice-ref表示哪一个通知，pointcut-ref表示切入位置--&gt;        &lt;aop:advisor advice-ref=&quot;log&quot; pointcut-ref=&quot;p&quot;&gt;&lt;/aop:advisor&gt;    &lt;/aop:config&gt;&lt;/beans&gt;</code></pre><blockquote><p>这里来解析一下execution的表达式。* com.chen.pojo.userServiceImpl..*  .*(..)</p><ol><li>execution()为表达式主体，里面为表达式</li><li>第一个 * 表示返回的类型，*表示任意类型的返回值</li><li>表示需要拦截的包，即真实对象所在的位置</li><li>包名后面的,,*表示当前包以及子包</li><li>第二个 * 表示方法，*表示所有方法</li><li>(..)表示方法可以有任意的参数</li></ol></blockquote></li><li><p>测试类</p><pre><code class="language-java">public class test01 {    public static void main(String[] args) {        ApplicationContext context = new ClassPathXmlApplicationContext(&quot;applicationContext.xml&quot;);        userService userService = context.getBean(&quot;userServiceImpl&quot;, userService.class);                userService.add();        userService.delete();        userService.update();        userService.select();    }}</code></pre><blockquote><p>注意这里的用的是接口而不是实现类。</p></blockquote></li></ol><h3>使用自定义类来实现</h3><ol><li><p>定义接口和实现类，和上述一样</p></li><li><p>定义一个通知类</p><pre><code class="language-java">public class log {    public  void before(){        System.out.println(&quot;方法执行&quot;);    }}</code></pre></li><li><p>配置xml文件</p><pre><code class="language-xml">&lt;?xml version=&quot;1.0&quot; encoding=&quot;UTF-8&quot;?&gt;&lt;beans xmlns=&quot;http://www.springframework.org/schema/beans&quot;       xmlns:aop=&quot;http://www.springframework.org/schema/aop&quot;       xmlns:xsi=&quot;http://www.w3.org/2001/XMLSchema-instance&quot;       xsi:schemaLocation=&quot;http://www.springframework.org/schema/beans        http://www.springframework.org/schema/beans/spring-beans.xsd        http://www.springframework.org/schema/aop        http://www.springframework.org/schema/aop/spring-aop.xsd&quot;&gt;    &lt;bean id=&quot;userServiceImpl&quot; class=&quot;com.chen.pojo.userServiceImpl&quot;&gt;&lt;/bean&gt;    &lt;bean id = &quot;log&quot; class=&quot;com.chen.pojo.log&quot;/&gt;    &lt;aop:config&gt;        &lt;!--自己定义一个切面，引用刚刚的类--&gt;       &lt;aop:aspect ref=&quot;log&quot;&gt;           &lt;!--定义切入点--&gt;           &lt;aop:pointcut id=&quot;point&quot; expression=&quot;execution(* com.chen.pojo.userServiceImpl.*(..))&quot;/&gt;           &lt;!--环绕方式，method表示环绕方法，pointcut-ref在哪环绕--&gt;           &lt;aop:before method=&quot;before&quot; pointcut-ref=&quot;point&quot;/&gt;       &lt;/aop:aspect&gt;    &lt;/aop:config&gt;&lt;/beans&gt;</code></pre></li></ol><blockquote><p>这种方法还是不够好，简单的插入可以使用这种。</p></blockquote><h3>使用注解来实现</h3><p>使用注解来实现跟自定义类实现其实差不多。</p><ol><li><p>定义接口和实现类，和上述一样</p></li><li><p>定义通知类</p><pre><code class="language-java">//定义切面@Aspectpublic class log {    //定义环绕方式，里面是切入点    @Before(&quot;execution(* com.chen.pojo.userServiceImpl.*(..))&quot;)    public void before(){        System.out.println(&quot;方法执行&quot;);    }}</code></pre></li><li><p>然后把这个类注册到spring里，并开启注解支持</p><pre><code class="language-xml">&lt;?xml version=&quot;1.0&quot; encoding=&quot;UTF-8&quot;?&gt;&lt;beans xmlns=&quot;http://www.springframework.org/schema/beans&quot;       xmlns:aop=&quot;http://www.springframework.org/schema/aop&quot;       xmlns:xsi=&quot;http://www.w3.org/2001/XMLSchema-instance&quot;       xsi:schemaLocation=&quot;http://www.springframework.org/schema/beans        http://www.springframework.org/schema/beans/spring-beans.xsd        http://www.springframework.org/schema/aop        http://www.springframework.org/schema/aop/spring-aop.xsd&quot;&gt;    &lt;bean id=&quot;userServiceImpl&quot; class=&quot;com.chen.pojo.userServiceImpl&quot;&gt;&lt;/bean&gt;    &lt;bean id =&quot;log&quot; class=&quot;com.chen.pojo.log&quot;/&gt;    &lt;!--开启注解支持--&gt;    &lt;aop:aspectj-autoproxy/&gt;&lt;/beans&gt;</code></pre><blockquote><p>开启注解支持还能设置动态代理模式&lt;aop:aspectj-autoproxy  proxy-target-class=&quot;false&quot;/&gt;</p><p>值为false表示使用JDK动态代理（默认），为true表示使用cglib动态代理</p></blockquote></li></ol><h2>Spring与Mybatis整合</h2><h3>导入依赖</h3><pre><code class="language-xml">&lt;?xml version=&quot;1.0&quot; encoding=&quot;UTF-8&quot;?&gt;&lt;project xmlns=&quot;http://maven.apache.org/POM/4.0.0&quot;         xmlns:xsi=&quot;http://www.w3.org/2001/XMLSchema-instance&quot;         xsi:schemaLocation=&quot;http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd&quot;&gt;    &lt;modelVersion&gt;4.0.0&lt;/modelVersion&gt;    &lt;groupId&gt;com.chen&lt;/groupId&gt;    &lt;artifactId&gt;spring-study&lt;/artifactId&gt;    &lt;packaging&gt;pom&lt;/packaging&gt;    &lt;version&gt;1.0-SNAPSHOT&lt;/version&gt;    &lt;modules&gt;        &lt;module&gt;spring-01&lt;/module&gt;    &lt;/modules&gt;    &lt;dependencies&gt;        &lt;dependency&gt;            &lt;groupId&gt;junit&lt;/groupId&gt;            &lt;artifactId&gt;junit&lt;/artifactId&gt;            &lt;version&gt;4.12&lt;/version&gt;        &lt;/dependency&gt;        &lt;dependency&gt;            &lt;groupId&gt;org.mybatis&lt;/groupId&gt;            &lt;artifactId&gt;mybatis&lt;/artifactId&gt;            &lt;version&gt;3.5.4&lt;/version&gt;        &lt;/dependency&gt;        &lt;dependency&gt;            &lt;groupId&gt;mysql&lt;/groupId&gt;            &lt;artifactId&gt;mysql-connector-java&lt;/artifactId&gt;            &lt;version&gt;5.1.47&lt;/version&gt;        &lt;/dependency&gt;        &lt;dependency&gt;            &lt;groupId&gt;org.springframework&lt;/groupId&gt;            &lt;artifactId&gt;spring-webmvc&lt;/artifactId&gt;            &lt;version&gt;5.2.6.RELEASE&lt;/version&gt;        &lt;/dependency&gt;        &lt;dependency&gt;            &lt;groupId&gt;org.springframework&lt;/groupId&gt;            &lt;artifactId&gt;spring-jdbc&lt;/artifactId&gt;            &lt;version&gt;5.1.9.RELEASE&lt;/version&gt;        &lt;/dependency&gt;        &lt;dependency&gt;            &lt;groupId&gt;org.assertj&lt;/groupId&gt;            &lt;artifactId&gt;aspectjweaver&lt;/artifactId&gt;            &lt;version&gt;1.9.4&lt;/version&gt;        &lt;/dependency&gt;        &lt;dependency&gt;            &lt;groupId&gt;org.mybatis&lt;/groupId&gt;            &lt;artifactId&gt;mybatis-spring&lt;/artifactId&gt;            &lt;version&gt;2.0.2&lt;/version&gt;        &lt;/dependency&gt;    &lt;/dependencies&gt;            &lt;build&gt;        &lt;resources&gt;            &lt;resource&gt;                &lt;directory&gt;src/main/resources&lt;/directory&gt;                &lt;includes&gt;                    &lt;include&gt;**/*.properties&lt;/include&gt;                    &lt;include&gt;**/*.xml&lt;/include&gt;                &lt;/includes&gt;                &lt;filtering&gt;true&lt;/filtering&gt;            &lt;/resource&gt;            &lt;resource&gt;                &lt;directory&gt;src/main/java&lt;/directory&gt;                &lt;includes&gt;                    &lt;include&gt;**/*.properties&lt;/include&gt;                    &lt;include&gt;**/*.xml&lt;/include&gt;                &lt;/includes&gt;                &lt;filtering&gt;true&lt;/filtering&gt;            &lt;/resource&gt;        &lt;/resources&gt;    &lt;/build&gt;&lt;/project&gt;</code></pre><h3>配置spring的xml</h3><pre><code class="language-xml">&lt;?xml version=&quot;1.0&quot; encoding=&quot;UTF-8&quot;?&gt;&lt;beans xmlns=&quot;http://www.springframework.org/schema/beans&quot;       xmlns:aop=&quot;http://www.springframework.org/schema/aop&quot;       xmlns:xsi=&quot;http://www.w3.org/2001/XMLSchema-instance&quot;       xsi:schemaLocation=&quot;http://www.springframework.org/schema/beans        http://www.springframework.org/schema/beans/spring-beans.xsd        http://www.springframework.org/schema/aop        http://www.springframework.org/schema/aop/spring-aop.xsd&quot;&gt;    &lt;!--dateSource,用spring来代替原来的mybatis的数据源的配置文件--&gt;    &lt;bean id=&quot;dateSource&quot; class=&quot;org.springframework.jdbc.datasource.DriverManagerDataSource&quot;&gt;        &lt;property name=&quot;driverClassName&quot; value=&quot;com.mysql.jdbc.Driver&quot;/&gt;        &lt;property name=&quot;url&quot; value=&quot;jdbc:mysql://localhost:3306/test?useSSL=false&amp;amp;serverTimezone=UTC&quot;/&gt;        &lt;property name=&quot;username&quot; value=&quot;root&quot;/&gt;        &lt;property name=&quot;password&quot; value=&quot;password&quot;/&gt;    &lt;/bean&gt;    &lt;!--sqlSessionFactory--&gt;    &lt;bean id=&quot;sqlSessionFactory&quot; class=&quot;org.mybatis.spring.SqlSessionFactoryBean&quot;&gt;        &lt;!--绑定mybatis的数据源--&gt;        &lt;property name=&quot;dataSource&quot; ref=&quot;dateSource&quot;/&gt;        &lt;!--绑定mybatis的配置文件,其实不用mybatis-config.xml也可以--&gt;        &lt;property name=&quot;configLocation&quot; value=&quot;classpath:mybatis-config.xml&quot;/&gt;        &lt;!--绑定映射--&gt;        &lt;property name=&quot;mapperLocations&quot; value=&quot;classpath:....&quot;/&gt;    &lt;/bean&gt;    &lt;!--创建sqlSessionTemplate,效果于创建sqlSession一样，因为没有set方法，只能构造器注入--&gt;    &lt;bean id=&quot;sqlSession&quot; class=&quot;org.mybatis.spring.SqlSessionTemplate&quot;&gt;        &lt;constructor-arg index=&quot;0&quot; ref=&quot;sqlSessionFactory&quot;/&gt;    &lt;/bean&gt;&lt;/beans&gt;</code></pre><blockquote><p>这个xml是可以代替原来Mybatis的配置文件，以后使用可以直接通过import导入就好了。</p></blockquote><h2>事务</h2><h3>导入约束</h3><p>用Spring事务时先导入约束，如下</p><pre><code class="language-xml">&lt;?xml version=&quot;1.0&quot; encoding=&quot;UTF-8&quot;?&gt;&lt;beans xmlns=&quot;http://www.springframework.org/schema/beans&quot;       xmlns:tx=&quot;http://www.springframework.org/schema/tx&quot;       xmlns:xsi=&quot;http://www.w3.org/2001/XMLSchema-instance&quot;       xsi:schemaLocation=&quot;        http://www.springframework.org/schema/beans        http://www.springframework.org/schema/beans/spring-beans.xsd        http://www.springframework.org/schema/tx        http://www.springframework.org/schema/tx/spring-tx.xsd&quot;&gt;&lt;/beans&gt;</code></pre><h3>配置事务通知</h3><pre><code class="language-xml">&lt;!--声明式事务--&gt;&lt;bean id=&quot;transactionManager&quot; class=&quot;org.springframework.jdbc.datasource.DataSourceTransactionManager&quot;&gt;        &lt;constructor-arg ref=&quot;dataSource&quot; /&gt;    &lt;/bean&gt;&lt;!--配置事务--&gt;    &lt;tx:advice id=&quot;txAdvice&quot; transaction-manager=&quot;transactionManager&quot;&gt;        &lt;tx:attributes&gt;            &lt;tx:method name=&quot;select&quot; propagation=&quot;REQUIRED&quot;/&gt; &lt;!--方法名，里面有属性可以配置事务--&gt;        &lt;/tx:attributes&gt;    &lt;/tx:advice&gt;</code></pre><h3>配置事务切入</h3><pre><code class="language-xml">&lt;!--声明式事务--&gt;&lt;bean id=&quot;transactionManager&quot; class=&quot;org.springframework.jdbc.datasource.DataSourceTransactionManager&quot;&gt;        &lt;constructor-arg ref=&quot;dataSource&quot; /&gt;    &lt;/bean&gt;&lt;!--配置事务--&gt;&lt;tx:advice id=&quot;txAdvice&quot; transaction-manager=&quot;transactionManager&quot;&gt;        &lt;tx:attributes&gt;            &lt;tx:method name=&quot;select&quot; propagation=&quot;REQUIRED&quot;/&gt; &lt;!--方法名，里面有属性可以配置事务--&gt;        &lt;/tx:attributes&gt;    &lt;/tx:advice&gt;&lt;aop:config&gt;        &lt;aop:pointcut id=&quot;txp&quot; expression=&quot;execution(* com.chen.mapper..*.*(..))&quot;/&gt;        &lt;aop:advisor advice-ref=&quot;txAdvice&quot; pointcut-ref=&quot;txp&quot;/&gt;    &lt;/aop:config&gt;</code></pre><h2>后话</h2><p>spring终于结束了，说实话spring相对Mybatis还是简单很多的，但细节还是挺多的，学了几天，前面又忘了，还是得多看看细节才行。emm，我的笔记也比较水，还有一些细节还没写出来，看得云里雾里得也挺正常，大概就看看复习一下就好了。end。下个框架见。</p><blockquote><p>还没写完---</p></blockquote>]]></content:encoded>
            <category domain="https://yww52.com/categories/%25E6%2588%2591%25E7%259A%2584%25E6%258A%2580%25E6%259C%25AF%25E5%25AD%25A6%25E4%25B9%25A0%25E7%2589%25A9%25E8%25AF%25AD%25E6%259E%259C%25E7%2584%25B6%25E6%259C%2589%25E9%2597%25AE%25E9%25A2%2598/">我的技术学习物语果然有问题</category>
        </item>
        <item>
            <title>IOC</title>
            <link>https://yww52.com/posts/183e2cbd/</link>
            <guid isPermaLink="true">https://yww52.com/posts/183e2cbd/</guid>
            <pubDate>Tue, 23 Jun 2020 00:00:00 +0000</pubDate>
            <description>IOC是Spring的核心，想学好Spring就要了解IOC</description>
            <content:encoded><![CDATA[<img src="https://img.yww52.com/2020/6/2020-6-23top_img.jpg" alt="IOC" style="max-width:100%;height:auto;margin-bottom:15px;" /><h2>前言</h2><p>今天看了Iteye的开涛对IOC的理解，<a href="http://jinnianshilongnian.iteye.com/blog/1413846">原文地址</a>，对IOC的有了一些理解，所以来记录一下。</p><h2>什么是IOC</h2><p>IOC全称为Inversion of Control，即为控制反转。IOC并不是一种技术，而是一种设计思想。怎样理解这种思想呢？就先从IOC的名称上看吧。IOC称控制反转，包含了两个重要的词语，控制和反转。理解IOC就可以从这两个词语去理解，是怎样的控制，怎样的反转。</p><h3>控制</h3><p>在我们常规的JavaSE的设计思想来看，我们需要用到其他对象来是实现功能的时候，就会在当前类里new出一个对象来进行调用，进而实现自己想要的功能，这就是我们主动地创造了对象来使用。而IOC不一样，IOC思想是别人已经帮你创造好了一个IOC容器，这个IOC容器会帮你创建好你所需要的对象，你不需要自己去new对象，你只需要获取IOC容器中的对象即可。所以这个控制是指，IOC容器控制了对象，控制了你所需要的所有外部资源。</p><h3>反转</h3><p>什么是反转，这个问题在对控制的理解里差不多能体会到。我们去创造对象来进行调用，这就是正转，而IOC这种思想就不是这样，它是反转过来的，是IOC容器来控制对象，我们只是接收容器传出来的对象，所以说是反转。</p><h3>理解</h3><p>理解了控制和反转两个词语之后，就应该IOC是怎样的思想了。我们也可以举个简单的例子。假如你想要一个书桌来学习，按常规的设计思想来看，你就需要木材与工具，然后在做好这个书卓，然后在学习，这样来讲你是不是就会觉得这种思想在一定程度来说是挺蠢的。按IOC的思想来看，你需要一个书卓，你就只需要去家具店买就好了，家具店已经有做好的书桌给你。这么说应该就能理解了，家具店就像是一个IOC容器，你需要书桌，我就可以提供书桌给你，这就是IOC。</p><h2>IOC的优点</h2><p>在我们常规的设计思想中，对象中创建对象，就会导致类与类之间的耦合度增高，从而让它们难以控制，而IOC的出现，人们不用自己创建对象，就能降低与类之间的耦合度，让程结构序更加的灵活，更好的测试。</p><blockquote><p>耦合就类似钟表，钟表内每个齿轮都有着联系，当一个齿轮坏了，也会影响另一个，进而影响全部。所以类与类之间的耦合度高在一方面来看不算是什么好事，当代码多起来的时候，错一小处就会影响全局。</p></blockquote><h2>DI</h2><p>说到IOC,就不得不说到DI—Dependency Injection,即依赖注入。在了解了IOC之后，DI也就不难理解。有人认为DI是实现IOC的一种方法，其实差不多。依赖注入表示，程序需要依赖IOC容器提供创建的对象，在程序中需要一个对象，IOC容器就会创建好一个对象来注入到程序中，你只要使用这个对象就可以了，至于对象是什么时候创建的，怎样创建的，在哪创建的你都可以不用去管，你只需要使用这个对象去完成你所需要的功能就好了，这就是我理解的DI。</p><h2>一个demo</h2><p>到此，应该对IOC和DI有自己的理解了，下面就通过一个小demo来加深一下理解。在常规的写法，要实现一个功能，会有dao接口，业务层接口和它们的实现类，最后使用业务层的实现类来实现这个功能。</p><pre><code class="language-Java">// Userdaopublic interface UserDao {    void getUser();}//UserDaoImplpublic class UserDaoImpl implements UserDao {    public void getUser() {        System.out.println(&quot;实现第一个功能&quot;);    }}//UserServicepublic interface UserService {    void getuser();}//UserServiceImppublic class UserServiceImpl implements UserService {    private UserDao userDao = new UserDaoImpl();    public void getuser() {        userDao.getUser();    }}//最后就会获得用户的数据public class test01 {    public static void main(String[] args) {        UserService userService = new UserServiceImpl();            userService.getuser();//实现第一个功能    }}</code></pre><p>一个功能是不能满足的时候，就会想着增加第二个功能，就需要这样做</p><pre><code class="language-Java">// Userdao public interface UserDao {    void getUser();}//UserDaoImplpublic class UserDaoImpl implements UserDao {    public void getUser() {        System.out.println(&quot;实现第一个功能&quot;);    }}//UserServicepublic interface UserService {    void getuser();}public class UserDaoImpl2 implements UserDao {    public void getUser() {        System.out.println(&quot;实现第二个功能&quot;);    }}//UserServiceImp//要想实现第二个功能就要修改业务层，从而改变实现的功能public class UserServiceImpl implements UserService {    private UserDao userDao = new UserDaoImpl2();    public void getuser() {        userDao.getUser();    }}//最后就会获得用户的数据public class test01 {    public static void main(String[] args) {        UserService userService = new UserServiceImpl();            userService.getuser();//实现第二个功能    }}</code></pre><p>当需要的功能多的时候，我们就会很难受，总是修改业务实现类，所以就出现了IOC这种思想来帮组我们解决这种问题。</p><pre><code class="language-Java">// UserDao public interface UserDao {    void getUser();}//UserDaoImplpublic class UserDaoImpl implements UserDao {    public void getUser() {        System.out.println(&quot;获取用户数据&quot;);    }}//UserDaoImpl2public class UserDaoImpl2 implements UserDao {    public void getUser() {        System.out.println(&quot;第二次获得用户数据&quot;);    }}//Service//增加了Set方法来设置获取的对象public interface UserService {    void getuser();    void setUserDao(UserDao userDao);}//ServiceImplpublic class UserServiceImpl implements UserService {    private UserDao userDao;//先设置引用，对象由set方法后期获得    public void setUserDao(UserDao userDao){        this.userDao = userDao;    }    public void getuser() {        userDao.getUser();    }}public class test01 {    public static void main(String[] args) {        UserService userService = new UserServiceImpl();        userService.setUserDao(new UserDaoImpl());        userService.getuser();//实现第一个功能        userService.setUserDao(new UserDaoImpl2());        userService.getuser();//实现第二个功能    }}</code></pre>]]></content:encoded>
            <category domain="https://yww52.com/categories/%25E6%2588%2591%25E7%259A%2584%25E6%258A%2580%25E6%259C%25AF%25E5%25AD%25A6%25E4%25B9%25A0%25E7%2589%25A9%25E8%25AF%25AD%25E6%259E%259C%25E7%2584%25B6%25E6%259C%2589%25E9%2597%25AE%25E9%25A2%2598/">我的技术学习物语果然有问题</category>
        </item>
        <item>
            <title>Javase</title>
            <link>https://yww52.com/posts/85d837e1/</link>
            <guid isPermaLink="true">https://yww52.com/posts/85d837e1/</guid>
            <pubDate>Thu, 18 Jun 2020 00:00:00 +0000</pubDate>
            <description>这是我学习Java时候做的笔记，比较水。</description>
            <content:encoded><![CDATA[<img src="https://img.yww52.com/2020/6/2020-6-18top_img.jpg" alt="Javase" style="max-width:100%;height:auto;margin-bottom:15px;" /><h2>前言</h2><p>以前都是记笔记，每篇文章都很短而且很乱，现在学完很久了，有些东西都快忘了，最近又刚刚整完主题，所以索性就整理一下前面的文章，顺便整合起来复习一下。</p><p>:::note{type=&quot;success&quot;}</p><p>这是我学习JavaSE的时候记的笔记，文章较长，建议目录直接跳转</p><p>:::</p><h2>什么是JavaSE</h2><p>复习前先来看看什么是JavaSE，来看看百度百科是怎么描述的。（大概了解一下就好了）</p><blockquote><p>Java SE（Java Standard Edition，Java 标准版）是Java技术的核心和基础，是Java ME和Java EE编程的基础 [1] 。Java SE是由Sun Microsystems公司于1995年5月推出的Java程序设计语言和Java平台的总称。</p></blockquote><h2>Java环境配置</h2><p>学习Java的第一件事情就是要先配置好Java所需要的环境，不然运行不了还怎么学习。</p><h3>下载JDK</h3><p>JDK是Java语言的软件开发工具包，是整个Java开发的核心，它包含了Java的运行环境和Java工具，所以这个是必须要下载的。</p><p>:::note{type=&quot;info&quot;}</p><p>下载的版本就选Java8好了，不一定最新的就是最好的，当然最新的也可以。至于IDE嘛，喜欢哪个用哪个，我用的就是idea</p><p>:::</p><h3>设置环境变量</h3><ol><li><p>此电脑，右键，点击“属性”，点击“高级系统设置”，点击“高级”下的环境变量，配置下面三个变量，有就添加变量，没有就新建变量</p></li><li><p>分别配置好以下三个环境变量</p><ul><li><p>JAVA_HOME路径就填JDK的安装路径</p></li><li><p>PATH路径填JDK命令文件的位置，即bin目录的位置</p></li><li><p>CLASSPATH</p><p>开头输入**“.;”**,然后填lib目录的位置</p></li></ul></li></ol><h3>检查是否配置成功</h3><p>配置完了之后就去检查一下有没有配置成功吧，以免之后出错。去cmd输入命令java-version，若有版本号出现就证明配置成功了。</p><h2>省略一部分</h2><p>时间不允许我写这么多了，所以我就跳过了一些基本知识，之后我在补（如果有机会的话）。</p><ul><li>操作符</li><li>变量和常量</li><li>控制流程语句（for,if,while）</li></ul><blockquote><p>这些是跟很多编程语言都是一样的，特别是和C,C++，所以我省了</p></blockquote><h2>方法</h2><p>方法也就是C语言里面的函数，也就是一段实现特定效果的语段。main方法就是主方法，程序的入口。</p><h3>方法的简单使用</h3><pre><code class="language-Java">public class Hello {    public static void main(String[] args) {        //方法调用，若方法没加static修饰，就new一个Hello的对象，然后调用这个方法        helloworld();    }//定义，修饰符，返回值加方法名，方法体     static void helloworld(){        System.out.println(&quot;Hello World!&quot;);    }}</code></pre><h3>方法重载</h3><p>方法重载就在于方法名是一样的，参数列表不一样。</p><p>:::note{type=&quot;warning&quot;}</p><p>重载不是重写，这是两个概念，不要弄混了</p><p>:::</p><pre><code class="language-Java">public class Hello {    public static void main(String[] args) {        helloworld();        helloworld(1);    }    static void helloworld(){        System.out.println(&quot;Hello World!&quot;);    }    static void helloworld(int i){        System.out.println(&quot;Hello World!&quot;);    }}</code></pre><h3>方法递归</h3><p>递归的思想就是自己调用自己（套娃），比如下面这个求阶乘的方法。</p><pre><code class="language-Java">public class Hello {    public static void main(String[] args) {        System.out.println(fnc(5));    }     static int fnc(int i){        int res = i;        if(i&gt;1)         res = res * fnc(i - 1);//套娃        return res;     }}</code></pre><h2>面向对象</h2><p>成天都说Java是面向对象的语言。那什么是面向对象编程呢？</p><blockquote><p>面向对象是相对于面向过程来讲的，面向对象方法，把相关的数据和方法组织为一个整体来看待，从更高的层次来进行系统建模，更贴近事物的自然运行模式</p></blockquote><p>大概了解一下就好了，Java是离不开类和对象的，所以之后就会感受到什么是面向对象编程。</p><h2>类和对象</h2><p>类就是一个抽象的概念，对象就是从类里实例化出来的个体。之后的Java学习都要用到。现在就来定义一个简单的类。</p><pre><code class="language-java">//注意一个类文件里面只能有一个public类public class demo{    public int i;    public void print(){        System.out.println(i);    }}</code></pre><h3>类属性和类方法</h3><p>上述的i和print()就本别是类属性和类方法。把类当作是一个整体，类属性就相当于是一个部件，类方法相当于部件实现的功能。</p><blockquote><p>类属性和方法调用必须通过对象，比如demo类有个test对象，那就是test.i和test.print()</p></blockquote><h3>访问修饰符</h3><p>上述类，属性，对象都用public来修饰，public就是一个访问修饰符，访问修饰符是来设定权限的，设定谁才能使用这个类，以下是四种Java的访问权限。</p><table><thead><tr><th align="center">访问级别</th><th align="center">访问修饰符</th><th align="center">同类</th><th align="center">同包不同类(不含子类)</th><th align="center">同包子类</th><th align="center">不同的包(不含子类)</th><th align="center">不同包子类</th></tr></thead><tbody><tr><td align="center">公开的</td><td align="center">public</td><td align="center"><strong>√</strong></td><td align="center"><strong>√</strong></td><td align="center"><strong>√</strong></td><td align="center"><strong>√</strong></td><td align="center"><strong>√</strong></td></tr><tr><td align="center">受保护的</td><td align="center">protected</td><td align="center"><strong>√</strong></td><td align="center"><strong>√</strong></td><td align="center"><strong>√</strong></td><td align="center"></td><td align="center"><strong>√</strong></td></tr><tr><td align="center">默认的</td><td align="center">什么都不加</td><td align="center"><strong>√</strong></td><td align="center"><strong>√</strong></td><td align="center"></td><td align="center"></td><td align="center"></td></tr><tr><td align="center">私有</td><td align="center">private</td><td align="center"><strong>√</strong></td><td align="center"></td><td align="center"></td><td align="center"></td><td align="center"></td></tr></tbody></table><h3>引用</h3><p>如果一个变量的类型是 类类型，而非基本类型，那么该变量又叫做引用。</p><p>听起来很绕，写起来就知道什么意思了。</p><p>使用类就要创建这个类的对象。就比如new demo()。但也只是创建对象，无法访问到它，所以就用引用来代表这个对象。</p><pre><code class="language-Java">//引用了d这个变量来代表新创建的demo对象demo d = new demo();</code></pre><h3>构造方法</h3><p>类还有一个很重要的概念就是构造方法，也叫构造器。构造方法使用来创建类的，所以方法名与类名一模一样，没有返回值，如果要实例化对象，就一定会用到构造方法。</p><pre><code class="language-java">public class Hello {    public static void main(String[] args) {    demo d1 = new demo();        demo d2 = new demo(1);    }}class demo{    public int i;    public void print(int i){        System.out.println(i);    }    //无参构造函数    demo(){    i = 2;    }     //有参构造函数（重载）    demo(int t ){       i = t;    }}</code></pre><blockquote><p>需要注意的是，系统会默认提供无参的构造方法，所以你能使用new demo()来创建对象，但如果定义了有参的构造方法，无参的就会没了，所以就不能使用new demo()来创建对象，所以养成个好习惯，创建类的时候，顺便写上无参的构造方法。</p></blockquote><h3>Java的三大特性</h3><h4>继承</h4><p>:::note{type=&quot;info&quot;}</p><p>关键字extends</p><p>:::</p><p>子类继承父类的所有方法和属性，Java的继承包括两种：</p><ol><li>类的单继承，即只能继承一个父类</li><li>接口的多继承，一个类可以实现（继承）多个接口</li></ol><h4>封装</h4><p>:::note{type=&quot;info&quot;}</p><p>关键字就是private,protected,public</p><p>:::</p><p>封装就是将一个类里的属性和方法包装起来，严格控制访问权限。就比如JavaBean就是一个标准的封装</p><pre><code class="language-Java">class demo{        private String name;    private  int id;        public demo(int id, String name) {        this.id = id;        this.name = name;    }    public int getId() {        return id;    }    public void setId(int id) {        this.id = id;    }    public String getName() {        return name;    }    public void setName(String name) {        this.name = name;    }}</code></pre><h4>多态</h4><p>多态就是同一个行为具有多个不同表现形式或形态的能力。实现多态的三个必要条件有</p><ol><li>继承</li><li>重写</li><li>父类引用指向子类对象</li></ol><p>可以先看个简单的例子</p><pre><code class="language-Java">public class Test {    public static void main(String[] args) {      show(new Cat());  // 以 Cat 对象调用 show 方法      show(new Dog());  // 以 Dog 对象调用 show 方法                      Animal a = new Cat();  // 向上转型        a.eat();               // 调用的是 Cat 的 eat      Cat c = (Cat)a;        // 向下转型        c.work();        // 调用的是 Cat 的 work  }      public static void show(Animal a)  {      a.eat();          // 类型判断        if (a instanceof Cat)  {  // 猫做的事情             Cat c = (Cat)a;              c.work();          } else if (a instanceof Dog) { // 狗做的事情             Dog c = (Dog)a;              c.work();          }      }  }abstract class Animal {      abstract void eat();  }  class Cat extends Animal {      public void eat() {          System.out.println(&quot;吃鱼&quot;);      }      public void work() {          System.out.println(&quot;抓老鼠&quot;);      }  }  class Dog extends Animal {      public void eat() {          System.out.println(&quot;吃骨头&quot;);      }      public void work() {          System.out.println(&quot;看家&quot;);      }  }</code></pre><h3>this与super关键字</h3><h4>this</h4><p>this的本质是”创建好的对象的地址“，经常用为当前对象。下面举个例子来说明this的使用方法。</p><pre><code class="language-Java">// 1class A（int a,int b） {this.a = a;           //this.a指的是这个对象中定义的a，与构造器定义的区分this.b = b;}   // 2class A(int a,int b,int c){    this(a,b);           // 引用同个类的构造器，必须放在第一行，不能引用两个    this.c = c;}// 3this.a();//通过this来调用本类的方法</code></pre><p>:::note{type=&quot;warning&quot;}</p><p>使用this关键字来调用重载的构造方法时，可以避免相同的初始化代码，但只能在构造方法中使用，而且须位于构造方法的第一句。</p><p>:::</p><blockquote><p>this不能用于static方法中。因为static方法是属于类的，this是代表对象，具体详情可以参考下面关于static的描述</p></blockquote><h4>super</h4><p>super可以理解为指向父类对象（离得最近的）的一个指针，使用方法也有三种。</p><pre><code class="language-Java">public class demo {    public  int id;    demo(){        id = 10;    }    public void print(){        System.out.println(id);    }    public static void main(String[] args) {    }}class demo_01 extends demo{    demo_01(){        super();//1.引父类的无参构造方法    }    public void print(){        id = super.id;//2. 调用父类的成员        super.print();//3. 调用父类的方法，跟第二个使用很像    }}</code></pre><h4>两者对比</h4><ul><li>子类的构造函数中要调用super()，一定要放在首行，即使不用系统也会自行调用super()，即子类调用自己的构造器时会先调用父类的，先创建一个父类的对象</li><li>super是从子类中调用父类的方法或变量，而this为在同一类中调用其它方法或变量</li><li>从本质上来讲this是一个指向本对象的指针，而super是一个Java的关键字</li></ul><h2>数组</h2><p>数组是一个固定长度的，包含了相同类型数据的容器。</p><h3>一维数组</h3><h4>数组的声明</h4><pre><code class="language-Java">// 优先使用这种方法声明datatype[] arr_name;// 这种是C/C++风格的声明，不推荐datatype  arr_name[];</code></pre><h4>数组的实例</h4><pre><code class="language-Java">// 优先使用这种方法实例datatype[] arr;//不推荐datatype arr[];</code></pre><h4>创建数组</h4><pre><code class="language-java">//dataType[] arr_name = new dataType[arraysize];int arr = new int[10];</code></pre><h4>三种初始化方式</h4><pre><code class="language-Java">// 1.静态初始化   type[]  arr_name={,,,}//2. 默认初始化   type[]  arr_name = new type[int]    //全初始化为默认值，Boolean为false，引用类型为为null//3.动态初始化   //先分配空间，然后一个个初始化</code></pre><h3>二维数组</h3><p>二维数组可以理解为一个数组，这个数组存着几个一维数组,所以Java的数组可以是不规则的。</p><h4>二维数组创建：</h4><pre><code class="language-java">type[][] arr_name= new type[int][int];</code></pre><h4>二维数组的初始化</h4><pre><code class="language-Java">//1.静态初始化type[][] arr_name ={{},{},{}}//2.动态初始化，可以使数组不规则 arr_name[0]=new type[]{,,,}</code></pre><h2>接口</h2><p>接口时一个抽象的类型，跟抽象类有着差不多作用，但又不是类，相当于一个模板需要我们去完善，实现功能，所以接口是不能被实例化的，使用它就需要继承它来实现。</p><h3>抽象类</h3><p>抽象类是一种特殊的类，它的作用就是给子类提供了模板，<strong>它不能被实例化</strong>，所以抽象类是被当作父类去继承的。说到抽象类，就先聊聊什么是抽象类。类里有抽象方法的类被叫为抽象类，但要注意抽象类不是一定有抽象方法。</p><blockquote><p>定义抽象类就用关键字abstract，将类声明为抽象类</p></blockquote><h3>抽象方法</h3><p>有抽象方法的类被叫为抽象类，那什么是抽象方法呢，就是专门给我们去继承实现的方法。所以抽象方法是必须要被重写的，而且继承了该抽象类就必须重写抽象方法，不然系统会编译出错。</p><blockquote><p>抽象方法的定义也是用关键字abstract，抽象方法是必须被重写的，所以它是不需要方法体的。</p></blockquote><p>:::note{type=&quot;warning&quot;}</p><p>子类继承了抽象类，没重写其抽象方法子类就必须在声明为抽象类，还有的就是构造方法和用static修饰的类方法是不能被声明为抽象方法的。</p><p>:::</p><h3>接口</h3><p>大致了解了抽象类和抽象方法就能理解接口是什么了吧。接口时抽象方法的集合，虽然也可以定义变量，但最好不要破坏了接口的特性，全是抽象方法就好了。</p><blockquote><p>接口是用关键字interface来定义的，类通过关键字implements去实现接口，而且里面的方法是不能有抽象方法的，继承的类必须重写里面所有的抽象方法。</p></blockquote><p>这是一个简单的例子</p><pre><code class="language-Java">public class demo implements demotest {    public static void main(String[] args)  {        demo d = new demo();        d.a();        d.b();    }    public void a() {        System.out.println(&quot;aaa&quot;);    }    public void b() {        System.out.println(&quot;bbb&quot;);    }}interface demotest {    void a();    void b();}</code></pre><h3>接口的多继承</h3><p>在知道了接口之后，就会有个疑问，既然有了抽象类的概念，为什么还要特意设计接口呢？这就要涉及到接口的一个特点了。</p><blockquote><p>类是只支持单继承，而接口支持多继承。</p></blockquote><p>人们在写程序的时候发现，在继承了一个类之后，人们就不能继续继承了，有些功能又要重新定义和实现，因为Java不想我们弄得很乱，不能像C++那样可以支持继承多个类，所以设计了接口来解决这种问题。可以通过下面的列子来大概了解接口的魅力。</p><pre><code class="language-Java">//继承了demoA的方法，又继承了demoB的方法，如果还想添加功能可以再写接口在实现public class demo implements demoA,demoB {    public static void main(String[] args) {        demo d = new demo();        d.a();        d.b();    }    public void a() {        System.out.println(&quot;aaa&quot;);    }    public void b() {        System.out.println(&quot;bbb&quot;);    }}interface demoA {    void a();}interface demoB {    void b();}</code></pre><p>:::note{type=&quot;warning&quot;}</p><p>接口的每个方法都会被隐式的指定public abstract来修饰，变量会被隐式的指定 public static final修饰。</p><p>JDK8之后，接口也可以有方法体和静态方法了，但怎么说呢，在我看来还是不加的好</p><p>:::</p><h2>Scanner类</h2><pre><code class="language-java">import java.util.Scannerpublic class Main {       public static void main(String[] args) {        Scanner sc= new Scanner(System.in);        String str1 = sc.next();//        next方法只读去空格之前的数据，并且指向本行        String str2 = sc.nextLine();//        nextLine方法可以扫描到一行内容并作为一个字符串而被获取到        int i  = sc.nextInt();//        nextInt方法只读取数值    }}</code></pre><h3>System.in和System.out</h3><ul><li>该方法只能从键盘获取一个字符，而且获取的是char类型的，其他类型的难处理</li></ul><pre><code class="language-java">import java.io.IOException;public class Main {    public static void main(String[] args)  {        System.out.println(&quot;请输入一个字符：&quot;);        char c = 0;        try {            c = (char) System.in.read();        } catch (IOException e) {            e.printStackTrace();        }        System.out.println(c);    }}</code></pre><h3>InputStreamReader和BufferedReader</h3><ul><li>可以获取字符串，但是获得int,float等类型需要转换</li></ul><pre><code class="language-java">import java.io.BufferedReader;import java.io.IOException;import java.io.InputStreamReader;public class Main {    public static void main(String[] args)  {        System.out.println(&quot;请输入：&quot;);        InputStreamReader is = new InputStreamReader(System.in);        BufferedReader br = new BufferedReader(is);        try {            String msg =br.readLine();            System.out.println(msg);        } catch (IOException e) {            e.printStackTrace();        }    }</code></pre><h2>String类</h2><p>在Java中，有个很重要的类-String类，注意S是大写，就是我们所说的字符串类，这使得我们使用字符串十分方便。</p><h3>String的简单应用</h3><pre><code class="language-Java">public class demo {    public static void main(String[] args) {        String str1 = &quot;hello&quot;;        String str2 = &quot;world!&quot;;        System.out.println(str1+str2);        String str3 = str1+str2;        System.out.println(str3);    }}</code></pre><p>:::note{type=&quot;warning&quot;}</p><p>注意String创建了就是不可改变的。</p><p>:::</p><p>既然字符串是不可变的，那为什么会出现下面的操作呢？</p><pre><code>public class demo {    public static void main(String[] args) {        String str1 = &quot;hello&quot;;        String str2 = &quot;world!&quot;;        String str3 = str1+str2;        System.out.println(str3);        str1 = &quot;hello world!&quot;;        System.out.println(str1);    }}</code></pre><blockquote><p> 有人会好奇str1为什么还能继续赋值呢。其实说字符串不可变，是说字符串本身不可变，即“hello”等字符串不能改变，str1只是一个引用，继续赋值只是换了一个地址索引，原来的“hello”还是会存在内存中。</p></blockquote><h3>类里经常使用的方法</h3><p>都是经常使用的一些方法</p><pre><code class="language-Java">//返回 char指定索引处的值char charAt(int index) //将此字符串与指定对象进行比较boolean equals(Object anObject) //使用平台的默认字符集将此 String编码为字节序列，将结果存储到新的字节数组中 byte[] getBytes() //返回指定字符第一次出现的字符串内的索引 int indexOf(int ch)//返回 true如果，且仅当 length()为0 boolean isEmpty()  //返回此字符串的长度int length()  //将此字符串转换为新的字符数组。 char[] toCharArray()  </code></pre><h2>异常</h2><p>异常是导致程序中断的事件，它不是算是错误，错误error是值jvm运行你的Java程序出的问题。</p><blockquote><p>Exception类是所有异常类的父类</p></blockquote><h3>RuntimeException(运行时异常)</h3><ul><li>ArithmeticException                                                                 试图除以0异常</li><li>NullpointerException                                                               空指针异常</li><li>ClassCastException                                                                   强制转换异常（引用数据类型转换）</li><li>ArraylndexOutOfBoundsException                                        数组索引越界异常    </li><li>NumberFormatException                                                         数字格式化异常</li></ul><h3>CheckedException(已检查异常)</h3><p>这类异常在编译时必须处理，否则无法通过编译   </p><h3>处理异常的方法</h3><ol><li><p>用try-catch-finally来捕获异常</p><pre><code class="language-Java">public class demo {    public static void main(String[] args) {        try                        //try来捕获异常        {                System.out.println(&quot;try&quot;);            throw new Exception( );        }        catch(Exception e)        //catch来处理异常        {            System.err.println(&quot;catch&quot;);            e.printStackTrace( );        }        finally                    //finally是必须执行的语句        {            System.out.println(&quot;finally&quot;);        }    }}</code></pre></li><li><p>throws抛出异常让系统解决</p><pre><code class="language-Java">public class demo {    public static void main(String[] args) throws Exception {            }}</code></pre></li></ol><h2>泛型</h2><p>泛型即参数化类型，本质为数据类型的参数化 ,也就是说所操作的数据类型被指定为一个参数的方式传递，类似于方法中的变量参数。可以用在类、接口、方法的创建中，分别简称为泛型类、泛型接口、泛型方法。</p><h3>为什么要使用泛型呢？</h3><p>首先举一个简单的例子</p><pre><code class="language-Java">public class Test {    public static void main(String[] args) {        List arrayList = new ArrayList();        arrayList.add(&quot;aaaa&quot;);        arrayList.add(100);        for(int i = 0; i&lt; arrayList.size();i++){            String item = (String)arrayList.get(i);            System.out.println(&quot;元素是&quot; + item);        }    }}</code></pre><p>nbsp;运行后就会发现程序崩溃了，这是因为在ArrayList中加入了Integer和String类型的数据，转换就出现了异常，所以为了解决一些集合内的类型转换的问题，就出现了泛型，泛型的使用可以减少类型转换和转换异常的情况。</p><h3>泛型的特性</h3><ol><li>泛型只在编译阶段有效，编译后程序会采取去泛型化的措施，泛型是不会进入到运行阶段的</li><li>泛型类型在逻辑上可以看成是不同的类型，实际上都是相同的基本类型</li></ol><h3>使用方法</h3><p>泛型有三种使用方式，泛型类，泛型接口，泛型方法。</p><h2>集合框架</h2><h3>Collection</h3><p>&nbsp;首先学容器之前还是学习一下Collection接口，毕竟List与Set接口都是继承的Collection的，所以List与Set都有它的方法，别与Collections类弄混了，前者是集合类的上层接口，后者是一个集合框架的帮助类</p><pre><code class="language-java">//先新建两个CollectionCollection&lt;String&gt;  c1 = new ArrayList&lt;&gt;();Collection&lt;String&gt;  c2 = new ArrayList&lt;&gt;();// size()，返回值是int，返回当前集合元素的个数System.out.println(c1.size());//isEmpty()，返回值是Boolean类型，如果集合是空的就返回true，反之返回falseSystem.out.println(c1.isEmpty());//add()，向集合里加入元素c1.add(&quot;student&quot;);//remove()，移除集合中的元素，并不是删除c1.remove(&quot;student&quot;);//clear()，移除所有的元素c1.clear();//toArray()，转换一个Object数组Object[] objs = c1.toArray();//contains()，返回值为Boolean类型，判断元素是否在集合内System.out.println(c1.contains(&quot;student&quot;));//containsAll()，返回值为Boolean类型,判断集c1是否包含有c2的所有元素c1.containsAll(c2);//addAll()，将c2的所有元素加入c1中c1.addAll(c2);//removeAll(),移除c1与c2共同的元素(移除c1与c2的交集)c1.removeAll(c2);//retainAll()，只保留c1与c2共同的元素(移除c1与c2非交集的元素)c1.retainAll(c2);</code></pre><h3>List</h3><h4>概念</h4><p>List是有序，可重复的容器，List接口继承了Collection,因为是有序的，所以List是有索引位置的，因此相对与Collection接口，会有一些 特殊的关于索引位置的方法</p><h4>常用实现类</h4><ul><li><p><strong>LinkedList</strong></p><p>&nbsp;&nbsp;LinkedList底层用双向链表实现的储存。查询效率低，删减效率高，线程不安全</p></li><li><p><strong>ArrayList</strong></p><p> &nbsp;&nbsp;ArrayList底层是用数组实现的储存。查询效率高，增删效率低，线程不安全</p></li><li><p><strong>Vector</strong></p><p> &nbsp;&nbsp;Vector底层是用数组实现的List,相关的方法都加了同步检查，故线程安全,效率低</p></li><li><p>当需要线程安全时就用Vector实现类。查找多就用ArrayList实现类，增删多就用LinkList。</p></li></ul><h4>特殊方法</h4><pre><code class="language-java">List&lt;String&gt; list = new ArrayList&lt;&gt;();// List重载了一个add()方法,在指定索引位置插入一个元素list.add(3，&quot;sss&quot;);//List重载了一个remove()方法,移除指定索引位置的元素list.remove(3);//set()，将指定索引位置元素改为指定元素list.set(2,&quot;student&quot;);//get(),返回指定索引位置的元素System.out.println(list.get(2));//indexOf()，返回指定元素的第一次的索引位置，若没有，返回-1System.out.println(list.indexOf(&quot;student&quot;));//lastIndexOf(),返回指定元素的最后一次的索引位置，若没有，返回-1System.out.println(list.lastIndexOf(&quot;student&quot;));</code></pre><h4>一些要点</h4><ul><li>List允许加重复的元素</li><li>List允许存在null元素</li><li>List是有索引位置的</li><li>List是支持泛型的，所以尽量使用List时要加泛型</li></ul><h3>Map</h3><h4>概念</h4><p>Map是存储Key与Value的容器，每个key都会对应一个Value值，所以键对象不能重复,如果存放相同的键对象，对应值对象会被新的值对象代替。</p><h4>常用实现类</h4><ul><li><strong>HashMap</strong></li></ul><p>底层是通过哈希表来实现的。线程不安全，但效率高。允许Key与Value为null。</p><ul><li><p><strong>HashTable</strong></p><p> &nbsp;&nbsp;底层实现与HashMap几乎一样，只是方法添加了synchronized来确保线程同步检查。所以线程安全，但效率低，且不允许Key与Value为null。</p></li><li><p><strong>TreeMap</strong></p><p> &nbsp;&nbsp;底层是通过红黑二叉树来实现的。HashMap效率高于TreeMap,需要排序时才会选择使用TreeMap，即按照Key递增的顺序排序，如果是类排序可以考虑使用comparable接口来实现</p></li></ul><h4>常用的方法</h4><pre><code class="language-Java"> Map&lt;Integer,String&gt; map1 = new HashMap&lt;&gt;(); Map&lt;Integer,String&gt; map2 = new HashMap&lt;&gt;();//put()，存放键值对map1.put(1,&quot;student1&quot;);//get()，通过键对象得到对应的值对象map1.get(1);//remove()，删除键对象对应的键值对map1.remove(1);//containsKey()与containsValue()，判断是否包含键对象或值对象是否存在改容器map1.containsKey(1);map1.containsValue(&quot;student1&quot;);//size()，返回容器中键值对的数量System.out.println(map1.size());//isEmpty(),判断容器是否为空，返回值为Boolean类型System.out.println(map1.isEmpty());//putAll(),将map2所有的键值对存放到map1里map1.putAll(map2);//clear(),清空所有键值对map1.clear();</code></pre><h3>Set</h3><h4>概念</h4><p>Set是<strong>无序而且是不可重复的</strong>，与List一样继承Collection接口。即没有索引只能遍历查找，通过equals方法为true的不能放入，即使是NULL也只能有一个。往Set里面加入元素，本质就是把这个元素作为Key加入到内部的Map里，由于Map里的key是不重复的所以Set也不重复</p><h4>常用实现类</h4><ul><li><p><strong>HashSet</strong></p><pre><code>底层是采用HashMap实现的，本质是简化版的HashMap，因此查询与增删效率较高。</code></pre></li><li><p><strong>TreeSet</strong></p></li></ul><h4>常用方法</h4><blockquote><p>因为Set继承与Collection接口，而且没有新增方法，所以方法与Collection是一样的。</p></blockquote><h3>迭代器</h3><h4>概念</h4><p>迭代器是一种对象，它能够用来遍历标准模板库容器中的部分或全部元素，每个迭代器对象代表容器中的确定的地址。</p><h4>使用</h4><ul><li><p>遍历List容器</p><pre><code class="language-Java">    List&lt;String&gt; list = new ArrayList&lt;&gt;();        list.add(&quot;1&quot;);        list.add(&quot;2&quot;);        list.add(&quot;3&quot;);        list.add(&quot;4&quot;);        list.add(&quot;5&quot;);//使用list里的iterator方法返回一个迭代器，使用该迭代器的hasNext方法得到下一个对象，进而遍历容器        for(Iterator&lt;String&gt; iter = list.iterator();iter.hasNext();){            String t = iter.next();            System.out.println(t);        }</code></pre></li><li><p>遍历Set容器</p><pre><code class="language-Java">//遍历Set的方法与遍历List是一样的    Set&lt;String&gt; set = new HashSet&lt;&gt;();        set.add(&quot;1&quot;);        set.add(&quot;2&quot;);        set.add(&quot;3&quot;);        set.add(&quot;4&quot;);        set.add(&quot;5&quot;);        for(Iterator&lt;String&gt; iter = set.iterator();iter.hasNext();){            String t = iter.next();            System.out.println(t);        }</code></pre></li><li><p>遍历Map容器</p><pre><code class="language-Java">    Map&lt;Integer,String&gt; map = new HashMap&lt;&gt;();        map.put(1, &quot;student1&quot;);        map.put(2, &quot;student2&quot;);        map.put(3, &quot;student3&quot;);//通过Map.entrySet使用iterator遍历key和value        Iterator&lt;Map.Entry&lt;Integer, String&gt;&gt; it = map.entrySet().iterator();        while (it.hasNext()) {            Map.Entry&lt;Integer, String&gt; entry = it.next();            System.out.println(&quot;key= &quot; + entry.getKey() + &quot; and value= &quot; + entry.getValue());        }</code></pre></li></ul><h3>容器遍历汇总</h3><h4>List遍历</h4><pre><code class="language-Java">List&lt;String&gt; list = new ArrayList&lt;&gt;();        list.add(&quot;1&quot;);        list.add(&quot;2&quot;);        list.add(&quot;3&quot;);        list.add(&quot;4&quot;);        list.add(&quot;5&quot;);//第一种，用for循环遍历for(int i = 0;i &lt; list.size() ; i++){            System.out.println(list.get(i));        }// 第二种，用迭代器遍历for(Iterator&lt;String&gt; iter = list.iterator();iter.hasNext();){            String t = iter.next();            System.out.println(t);        }//第三种，用增强for循环来遍历for(String s :list){            System.out.println(s);        }//第四种，用foreach遍历list.forEach(item -&gt; {            System.out.println(item);        });//第五种，用list.stream().forEachlist.stream().forEach(item -&gt; {            System.out.println(item);        });//经过测试，第一种是效率最高的，也是最推荐的，接下来是第二种，第三种，第四种，第五种</code></pre><h4>Set遍历</h4><pre><code class="language-Java">//Set的遍历与List大同小异,只是没有用索引遍历Set&lt;String&gt; set = new HashSet&lt;&gt;();        set.add(&quot;1&quot;);        set.add(&quot;2&quot;);        set.add(&quot;3&quot;);        set.add(&quot;4&quot;);        set.add(&quot;5&quot;);//第一种，用迭代器遍历        for(Iterator&lt;String&gt; iter = set.iterator();iter.hasNext();){            String t = iter.next();            System.out.println(t);        }//第二种，用增强for循环来遍历for(String s :set){            System.out.println(s);        }//第三种，用foreach遍历set.forEach(item -&gt; {             System.out.println(item);        });//第四种，用set.stream().forEach         set.stream().forEach(item -&gt; {                System.out.println(item);            });//效率比较与List的遍历时一样的</code></pre><h4>Map遍历</h4><pre><code class="language-java">//第一种，用增强for循环遍历 for (Integer key : map.keySet()) {            System.out.println(&quot;key= &quot;+ key + &quot; and value= &quot; + map.get(key));        }//第二种，通过Map.entrySet使用iterator遍历key和valueIterator&lt;Map.Entry&lt;Integer, String&gt;&gt; it = map.entrySet().iterator();        while (it.hasNext()) {            Map.Entry&lt;Integer, String&gt; entry = it.next();            System.out.println(&quot;key= &quot; + entry.getKey() + &quot; and value= &quot; + entry.getValue());        }//第三种，这种方法仅遍历，无法在for循环时实现remove等操作        for(Map.Entry&lt;Integer,String&gt;entry:map.entrySet()){                System.out.println(&quot;key= &quot; + entry.getKey() + &quot; and value= &quot; + entry.getValue());            }//第四种，只能获取values，不能获取key        for(String t:map.values()){                    System.out.println(&quot;value= &quot;+t);                }//一般使用第一种，但因为第一种方法是二次取值，所以效率会比第二第三种都要低</code></pre><h3>Collections工具类</h3><h4>概念</h4><p>类java.util.Collections提供了对Set,List,Map的辅助方法</p><h4>常用方法</h4><pre><code class="language-Java">List&lt;String&gt; list = new ArrayList&lt;&gt;();//sort(),对list容器内的元素从小到大排序        Collections.sort(list);//shuffle(),对list容器内的元素随机排序        Collections.shuffle(list);//reverse(),对list容器内的元素逆序排序        Collections.reverse(list);//fill(),用一个指定对象重写容器        Collections.fill(list,&quot;student&quot;);//binarySearch(),对于顺序的list容器，采用二分法查找特定对象 Collections.sort(list);        Collections.binarySearch(list, &quot;student&quot;);</code></pre><h2>IO流</h2><h3>IO流的分类</h3><ul><li>根据处理数据的类型不同分为：字符流和字节流</li><li>根据数据流向的不同分为：输入流和输出流</li></ul><h3>节点流</h3><ol><li><p><strong>文件字节流</strong>( 数据源为文件)</p><ul><li><p>FileInputStream                   通过字节的方式从文件到程序，读取文件               </p></li><li><p>FileOutputStream                 通过字节的方式从程序到文件，写入文件</p></li></ul></li><li><p><strong>文件字符流</strong>( 数据源为文件)</p><ul><li><p>FileReader                            通过字符的方式从文件到程序 </p></li><li><p>FileWriter                             通过字符的方式从程序到文件</p><blockquote><p> 处理的是字符数组 char</p></blockquote></li></ul></li><li><p><strong>字节数组流</strong>(数据源为字节数组对象)</p><ul><li><p>ByteArrayInputStream                通过字节的方式从字节数组到程序    </p></li><li><p>ByteArrayOutputStream            拖过字节的方式从程序到字节数组</p></li></ul></li></ol><p>:::note{type=&quot;warning&quot;}</p><p>使用流之后需要释放流，字节数组流不需要释放，系统会自动释放的</p><p>在cry - with -resources 工具类里，在抛出异常的try{}后加入流的声明，则会帮你自动释放资源，比如try(is;os;is1;os2){}</p><p>:::</p><h3>处理流</h3><ol><li><p><strong>字节缓冲流：</strong></p><ul><li><p><strong>BufferedInputStream</strong></p></li><li><p><strong>BufferedOutputStream</strong></p></li><li><p>默认是缓存8K，可以自己修改，原理类似于修饰模式。</p></li><li><p>可以提升性能，运输效率，只用关闭外层的流即可，内层会自动关。手动关闭的话要从里往外关闭</p></li></ul></li><li><p><strong>字符缓冲流：</strong></p><ul><li><p><strong>BufferedReader</strong>readLine()        读取一行文字</p></li><li><p><strong>BufferedWriter</strong>newLine()            换行符</p></li><li><p>不要用父类引用指向子类对象，不然用不了这些新增方法</p></li></ul></li><li><p><strong>转换流：</strong></p><ul><li><p><strong>InputStreamReader</strong>           </p></li><li><p><strong>OutputStreamWriter</strong>            </p></li><li><p>将字节流转为字符流，方便处理（字节流需全文本）</p></li><li><p>能为字节流指定字符集</p></li></ul></li><li><p><strong>数据流：</strong></p><ul><li><p><strong>DataInputStream</strong></p></li><li><p><strong>DataOutputStream</strong></p></li><li><p>保留数据类型，读取的顺序与写出保持一致</p></li><li><p>数据流里放的是InputStream与OutputStream的字节流</p></li></ul></li><li><p><strong>对象流：</strong></p><ul><li><p><strong>ObjectInputStream</strong>               反序列化</p></li><li><p><strong>ObjectOutputStream</strong>            序列化</p></li><li><p>先写出后读取，读取的顺序与写出的顺序保持一致</p></li><li><p>不是所有的对象都能序列化，必须是实现了Serializable接口</p></li><li><p>方法和数据流差不多，多了writeobject和readObject</p></li><li><p>如果不需要序列化某个对象的值，就用transient来修饰类的对象</p></li></ul></li><li><p><strong>打印流：</strong></p><ul><li><p><strong>PrintfStream</strong></p></li><li><p><strong>PrintWriter</strong></p></li></ul></li><li><p><strong>随机访问：</strong></p><ul><li><p><strong>RandomAcessFile</strong></p></li><li><p>两个模式(mode)<br>&quot;r&quot;读     &quot;rw&quot;读写</p></li></ul></li><li><p><strong>合并：</strong></p><ul><li><p><strong>SequenceInputStream</strong></p></li><li><p>先用Vector容器来储存输入流，然后用该容器的elements()方法返回SequenceInputStream的参数，从而合成一个流</p></li></ul></li></ol><h2>TCP与UDP的简单实现</h2><h3>TCP</h3><ul><li><p>使用基于TCP协议的Socket网络编程</p></li><li><p>TCP协议基于请求- 响应模式</p></li><li><p>利用IO流实现数据的传输</p></li><li><p>TCP使用的基本流程</p></li></ul><pre><code class="language-java">  import java.io.DataInputStream;  import java.net.ServerSocket;  import java.net.Socket;    /**   * 创建服务器   * 1. 使用ServerSocket创建服务器   需要指定端口   * 2. 阻塞式的等待连接  accept返回一个socket对象   * 3. 输入输出流操作   * 4. 释放资源   *   */  public class TCP_Setver {      public static void main(String[] args) throws Exception{  //        1.          ServerSocket server = new ServerSocket(9999);  //        2.          Socket client = server.accept();          System.out.println(&quot;一个客户端建立了连接&quot;);  //        3.          DataInputStream dis = new DataInputStream(client.getInputStream());          String data = dis.readUTF();          System.out.println(data);  //        4.          dis.close();        }  }  </code></pre><pre><code class="language-java">  import java.io.DataOutputStream;  import java.net.Socket;    /**   * 创建客户端   * 1. 使用Socket创建客户端   指定服务器的地址和端口   * 2. 输入输出流操作   * 3. 释放资源   */  public class TCP_Client {      public static void main(String[] args) throws Exception {  //        1.          Socket client = new Socket(&quot;localhost&quot;,9999);  //        2.          DataOutputStream dos =new DataOutputStream(client.getOutputStream());          String data = &quot;hello&quot;;          dos.writeUTF(data);          dos.flush();  //        3.          dos.close();          client.close();      }  }</code></pre><h3>UDP</h3><ul><li><p>基于UDP协议的Socket网络编程实现</p></li><li><p>不需要IO流实现数据传输，数据被打包成包裹发送到目的地（注意传输的包裹不要太大）</p></li><li><p>用到的两个类： </p><ol><li>DatagramPacket</li><li>DatagramSocket</li></ol></li><li><p>代码实现</p></li></ul><pre><code class="language-java">import java.net.DatagramPacket;import java.net.DatagramSocket;import java.net.InetSocketAddress;      /**    *                      发送端    *    * 1. 使用DatagramSocket(int port)指定端口,创建发送端    * 2. 准备数据(一定要转成字节数组)    * 3. 将字节数组封装成DatagramPacket包裹，需要指定目的地    * 4. 用send(DatagramPacket p)    * 5. 释放资源    */   public class UDP_Client {       public static void main(String[] args) throws Exception{   //        1.           DatagramSocket client = new DatagramSocket(8888);      //        2.  比如发送一段文字           String msg = &quot;你好!&quot;;           byte[] datas = msg.getBytes();      //        3.  指定的端口要是接收端创建时指定的端口           DatagramPacket packet = new DatagramPacket(datas,0,datas.length,                   new InetSocketAddress(&quot;localhost&quot;,6666));      //        4.           client.send(packet);      //        5.           client.close();          }   }   </code></pre><pre><code class="language-java">   import java.net.DatagramPacket;   import java.net.DatagramSocket;      /**    *                 接收端    *   1. 使用DatagramSocket(int port)指定端口,创建接收端    *   2. 准备容器，封装成DatagramPacket包裹，用来接收发送端的包裹    *   3. 用seceive(DatagramPacket p)阻塞式的接收包裹    *   4. 分析数据，比如用getData()和getLengeth()方法    *   5. 释放资源    */   public class UDP_Server {       public static void main(String[] args) throws Exception{   //        1.           DatagramSocket server = new DatagramSocket(6666);      //        2.           byte[] datas = new byte[1024*10];           DatagramPacket packet = new DatagramPacket(datas,0,datas.length);      //        3.           server.receive(packet);      //        4.           byte[] flush = packet.getData();           int len = packet.getLength();           System.out.println(new String(flush,0,flush.length));      //        5.           server.close();       }   }</code></pre><h2>多线程</h2><blockquote><p>笔记丢了，下次在补</p></blockquote><p>:::note{type=&quot;success&quot;}</p><p>终于整理完了，暂时不写了，如果想到有些漏的，在加上，还有一些其他的知识，我会用单独的文章来写</p><p>:::</p><blockquote><p>笔记有一些是网上的，所以要是有侵犯到你的权益可以通知我删除。</p></blockquote>]]></content:encoded>
            <category domain="https://yww52.com/categories/%25E6%2588%2591%25E7%259A%2584%25E6%258A%2580%25E6%259C%25AF%25E5%25AD%25A6%25E4%25B9%25A0%25E7%2589%25A9%25E8%25AF%25AD%25E6%259E%259C%25E7%2584%25B6%25E6%259C%2589%25E9%2597%25AE%25E9%25A2%2598/">我的技术学习物语果然有问题</category>
        </item>
        <item>
            <title>博客主题更换</title>
            <link>https://yww52.com/posts/70b61401/</link>
            <guid isPermaLink="true">https://yww52.com/posts/70b61401/</guid>
            <pubDate>Wed, 17 Jun 2020 00:00:00 +0000</pubDate>
            <description>这次将next换成了butterfly，果然我还是喜欢花里胡哨的主题</description>
            <content:encoded><![CDATA[<img src="https://yww52.com/default_cover.jpg" alt="博客主题更换" style="max-width:100%;height:auto;margin-bottom:15px;" /><h2>前言</h2><p>​前几天，在b站逛逛，突然看到一个花里胡哨的主题---Butterfly，的确还挺好看的，至少比next的好看（个人意见），所以便决定换掉我的next主题。</p><h2>Butterfly</h2><p>​就因为换个主题，我玩了一天的yml，这些前端的东西，对于我只学了一点点的人来说确看的云里雾里，不过Butterfly作者也很照顾我们这些不懂的人，主题配置文档写的清清楚楚，很多功能就是改个值就能启动，也很方便。（好了吹到这里）</p><h2>关于魔改</h2><p>​我是菜鸟，玩了两天还是没玩出来，结果还是是照抄小康大佬和 XueXi大佬的（感谢大佬的教程），虽然花里胡哨，但是好看就完事了。</p><h2>指路</h2><ul><li><a href="https://github.com/jerryc127/hexo-theme-butterfly">Butterfly主题</a></li><li><a href="https://xiaokang.me/">小康</a></li><li><a href="https://xiabor.com/about/">XueXi</a></li><li><a href="https://blog.lete114.top/about/">Lete</a></li></ul>]]></content:encoded>
            <category domain="https://yww52.com/categories/%25E7%259E%258E%25E6%258A%2598%25E8%2585%25BE/">瞎折腾</category>
        </item>
        <item>
            <title>Mybatis</title>
            <link>https://yww52.com/posts/da3e1361/</link>
            <guid isPermaLink="true">https://yww52.com/posts/da3e1361/</guid>
            <pubDate>Mon, 08 Jun 2020 00:00:00 +0000</pubDate>
            <description>Mybatis 是一款优秀的持久层框架，经常用它来整合项目，能很好的管理数据库的操作。</description>
            <content:encoded><![CDATA[<img src="https://img.yww52.com/2020/6/2020-6-8/top_img.jpg" alt="Mybatis" style="max-width:100%;height:auto;margin-bottom:15px;" /><h4>Mybatis简介</h4><p>​MyBatis 是一款优秀的持久层框架，它支持自定义 SQL、存储过程以及高级映射。MyBatis 免除了几乎所有的 JDBC 代码以及设置参数和获取结果集的工作。MyBatis 可以通过简单的 XML 或注解来配置和映射原始类型、接口和 Java POJO（Plain Old Java Objects，普通老式 Java 对象）为数据库中的记录。可以参考官网学习<a href="https://mybatis.org/mybatis-3/zh/index.html">Mybatis</a></p><h4>Mybatis简单使用</h4><ol><li><p>创建maven项目，与之前的第一个servlet程序的流程一样</p></li><li><p>配置环境，在父项目的pom.xml导入依赖,pom.xml如下</p><pre><code class="language-xml">&lt;?xml version=&quot;1.0&quot; encoding=&quot;UTF8&quot;?&gt;&lt;project xmlns=&quot;http://maven.apache.org/POM/4.0.0&quot;         xmlns:xsi=&quot;http://www.w3.org/2001/XMLSchema-instance&quot;         xsi:schemaLocation=&quot;http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd&quot;&gt;    &lt;modelVersion&gt;4.0.0&lt;/modelVersion&gt;    &lt;groupId&gt;com.chen&lt;/groupId&gt;    &lt;artifactId&gt;Mybatis-test&lt;/artifactId&gt;    &lt;packaging&gt;pom&lt;/packaging&gt;    &lt;version&gt;1.0-SNAPSHOT&lt;/version&gt;&lt;!--子项目--&gt;    &lt;modules&gt;        &lt;module&gt;Mybatis-01&lt;/module&gt;    &lt;/modules&gt;&lt;!--导入依赖--&gt;    &lt;dependencies&gt;                &lt;!--mysql--&gt;        &lt;dependency&gt;            &lt;groupId&gt;mysql&lt;/groupId&gt;            &lt;artifactId&gt;mysql-connector-java&lt;/artifactId&gt;            &lt;version&gt;5.1.47&lt;/version&gt;        &lt;/dependency&gt;                &lt;!--mybatis--&gt;        &lt;dependency&gt;            &lt;groupId&gt;org.mybatis&lt;/groupId&gt;            &lt;artifactId&gt;mybatis&lt;/artifactId&gt;            &lt;version&gt;3.5.4&lt;/version&gt;        &lt;/dependency&gt;        &lt;!--junit单--&gt;        &lt;dependency&gt;            &lt;groupId&gt;junit&lt;/groupId&gt;            &lt;artifactId&gt;junit&lt;/artifactId&gt;            &lt;version&gt;4.12&lt;/version&gt;        &lt;/dependency&gt;    &lt;/dependencies&gt;    &lt;build&gt;        &lt;resources&gt;            &lt;resource&gt;                &lt;directory&gt;src/main/resources&lt;/directory&gt;                &lt;includes&gt;                    &lt;include&gt;**/*.properties&lt;/include&gt;                    &lt;include&gt;**/*.xml&lt;/include&gt;                &lt;/includes&gt;                &lt;filtering&gt;true&lt;/filtering&gt;            &lt;/resource&gt;            &lt;resource&gt;                &lt;directory&gt;src/main/java&lt;/directory&gt;                &lt;includes&gt;                    &lt;include&gt;**/*.properties&lt;/include&gt;                    &lt;include&gt;**/*.xml&lt;/include&gt;                &lt;/includes&gt;                &lt;filtering&gt;true&lt;/filtering&gt;            &lt;/resource&gt;        &lt;/resources&gt;    &lt;/build&gt;&lt;/project&gt;</code></pre></li><li><p>在resource目录下编写Mybatis的核心配置文件</p><pre><code class="language-xml">&lt;?xml version=&quot;1.0&quot; encoding=&quot;UTF-8&quot; ?&gt;&lt;!DOCTYPE configuration        PUBLIC &quot;-//mybatis.org//DTD Config 3.0//EN&quot;        &quot;http://mybatis.org/dtd/mybatis-3-config.dtd&quot;&gt;&lt;!--configuration核心配置文件--&gt;&lt;configuration&gt;    &lt;environments default=&quot;development&quot;&gt;        &lt;environment id=&quot;development&quot;&gt;            &lt;transactionManager type=&quot;JDBC&quot;/&gt;            &lt;dataSource type=&quot;POOLED&quot;&gt;                &lt;property name=&quot;driver&quot; value=&quot;com.mysql.jdbc.Driver&quot;/&gt;                    &lt;!--这里用&amp;amp;代替&amp;--&gt;                &lt;property name=&quot;url&quot; value=&quot;jdbc:mysql://localhost:3306/test?useSSL=false&amp;amp;serverTimezone=UTC&quot;/&gt;                &lt;property name=&quot;username&quot; value=&quot;root&quot;/&gt;                &lt;property name=&quot;password&quot; value=&quot;password&quot;/&gt;            &lt;/dataSource&gt;        &lt;/environment&gt;    &lt;/environments&gt;&lt;!--每一个xml都需要在核心配置里加载映射--&gt;    &lt;mappers&gt;        &lt;mapper resource=&quot;com/chen/dao/getUserList.xml&quot;/&gt;    &lt;/mappers&gt;&lt;/configuration&gt;</code></pre></li><li><p>编写Mybatis工具类</p><blockquote><p>​        每个基于 MyBatis 的应用都是以一个 SqlSessionFactory 的实例为核心的。SqlSessionFactory 的实例可以通过 SqlSessionFactoryBuilder 获得。而 SqlSessionFactoryBuilder 则可以从 XML 配置文件或一个预先配置的 Configuration 实例来构建出 SqlSessionFactory 实例。从 XML 文件中构建 SqlSessionFactory 的实例非常简单，建议使用类路径下的资源文件进行配置。 但也可以使用任意的输入流（InputStream）实例，比如用文件路径字符串或 file:// URL 构造的输入流。MyBatis 包含一个名叫 Resources 的工具类，它包含一些实用方法，使得从类路径或其它位置加载资源文件更加容易。</p></blockquote><pre><code class="language-java">//sqlSessionFactory --&gt;sqlSessionpublic class Mybatisutils {        private static SqlSessionFactory sqlSessionFactory;        static{            try {                //使用Mybatis第一步：获取sqlSessionFactory对象                String resource = &quot;org/mybatis/example/mybatis-config.xml&quot;;                InputStream inputStream = Resources.getResourceAsStream(resource);                 sqlSessionFactory = new SqlSessionFactoryBuilder().build(inputStream);            } catch (IOException e) {                e.printStackTrace();            }        }        //既然有了 SqlSessionFactory，顾名思义，我们可以从中获得 SqlSession 的实例。        // SqlSession 提供了在数据库执行 SQL 命令所需的所有方法        //你可以通过 SqlSession 实例来直接执行已映射的 SQL 语句。        public static SqlSession getSqlSession(){            return sqlSessionFactory.openSession();        }        }</code></pre></li><li><p>定义接口类</p><pre><code class="language-Java">public interface UserMapper {    List&lt;user&gt; getUserList();    user getUserById(int id);}</code></pre></li><li><p>编写xml文件(同目录下好管理)，<strong>写好xml之后需要到核心配置处加载映射</strong></p><pre><code class="language-xml">&lt;?xml version=&quot;1.0&quot; encoding=&quot;UTF8&quot; ?&gt;&lt;!DOCTYPE mapper        PUBLIC &quot;-//mybatis.org//DTD Mapper 3.0//EN&quot;        &quot;http://mybatis.org/dtd/mybatis-3-mapper.dtd&quot;&gt;&lt;!--一个namespace绑定一个接口--&gt;&lt;mapper namespace=&quot;com.chen.dao.UserMapper&quot;&gt;    &lt;!--id表示接口中的方法名，parameterType为接收的参数类型，resultType为返回的类型--&gt;    &lt;!--对应接口中的getUserList方法--&gt;    &lt;select id=&quot;getUserList&quot; resultType=&quot;com.chen.pojo.user&quot;&gt;            select * from mybatis    &lt;/select&gt;    &lt;!--对应接口中的getUserById方法--&gt;    &lt;select id=&quot;getUserById&quot; parameterType=&quot;int&quot; resultType=&quot;com.chen.pojo.user&quot;&gt;               select *from mybatis where id= #{id}        &lt;!--#{id}是接收getUserById定义的参数--&gt;    &lt;/select&gt;    &lt;/mapper&gt;</code></pre></li><li><p>调用</p><ol><li><p>使用工具类中的getSqlSession方法获取sqlSession对象</p></li><li><p>通过sqlSession对象的getMapper方法获得接口的实现</p></li><li><p>然后通过实现的接口对象来调用方法</p></li><li><p>关闭sqlSession对象</p></li></ol><pre><code class="language-java">public class UserDaoTest {    @Test    public void test(){//1        SqlSession sqlSession = Mybatisutils.getSqlSession();//2        UserMapper mapper = sqlSession.getMapper(UserMapper.class);    //class对象获得接口信息//3        List&lt;user&gt; userlist = mapper.getUserList();        for(user u:userlist){             System.out.println(u.getId()+&quot;---&quot;+u.getName()+&quot;---&quot;+u.getPwd());        }//4        sqlSession.close();    }    @Test    public void getUserById(){//1        SqlSession sqlSession = Mybatisutils.getSqlSession();//2        UserMapper mapper = sqlSession.getMapper(UserMapper.class);//3        user u = mapper.getUserById(1);         System.out.println(u.getId()+&quot;---&quot;+u.getName()+&quot;---&quot;+u.getPwd());//4        sqlSession.close();    }}</code></pre></li></ol><h4>增删改查</h4><p>​<strong>增删改都是需要提交事务的</strong></p><ol><li><p>接口</p><pre><code class="language-Java">package com.chen.dao;import com.chen.pojo.user;import java.util.List;public interface UserMapper {    List&lt;user&gt; getUserList();    user getUserById(int id);    int addUser(user u);    int updateUser(user u);    int deleteUser(int id);}</code></pre></li><li><p>绑定的xml</p><pre><code class="language-Java">&lt;?xml version=&quot;1.0&quot; encoding=&quot;UTF8&quot; ?&gt;&lt;!DOCTYPE mapper        PUBLIC &quot;-//mybatis.org//DTD Mapper 3.0//EN&quot;        &quot;http://mybatis.org/dtd/mybatis-3-mapper.dtd&quot;&gt;&lt;mapper namespace=&quot;com.chen.dao.UserMapper&quot;&gt;    &lt;select id=&quot;getUserList&quot; resultType=&quot;com.chen.pojo.user&quot;&gt;        select * from mybatis    &lt;/select&gt;    &lt;select id=&quot;getUserById&quot; parameterType=&quot;int&quot; resultType=&quot;com.chen.pojo.user&quot;&gt;        select *from mybatis where id= #{id}    &lt;/select&gt;    &lt;insert id=&quot;addUser&quot; parameterType=&quot;com.chen.pojo.user&quot;&gt;        insert into mybatis(id,name,pwd) values(#{id},#{name},#{pwd})    &lt;/insert&gt;    &lt;update id=&quot;updateUser&quot; parameterType=&quot;com.chen.pojo.user&quot;&gt;        update mybatis set name=#{name},pwd=#{pwd} where id=#{id}    &lt;/update&gt;    &lt;delete id=&quot;deleteUser&quot; parameterType=&quot;int&quot;&gt;        delete from mybatis where id = #{id}    &lt;/delete&gt;&lt;/mapper&gt;</code></pre></li><li><p>测试类</p><pre><code class="language-Java">package com.chen.dao;import com.chen.pojo.user;import com.chen.utils.Mybatisutils;import org.apache.ibatis.session.SqlSession;import org.junit.Test;import java.util.List;public class UserDaoTest {    @Test    public void test(){        //获得sqlSession对象        SqlSession sqlSession = Mybatisutils.getSqlSession();        //执行sql语句        UserMapper mapper = sqlSession.getMapper(UserMapper.class);        List&lt;user&gt; userlist = mapper.getUserList();        for(user u:userlist){            System.out.println(u.getId()+&quot;---&quot;+u.getName()+&quot;---&quot;+u.getPwd());        }        //关闭sqlSession        sqlSession.close();    }        @Test    public void getUserById(){        SqlSession sqlSession = Mybatisutils.getSqlSession();        UserMapper mapper = sqlSession.getMapper(UserMapper.class);        user u = mapper.getUserById(1);        System.out.println(u);        sqlSession.close();    }    //            增删改的的操作都是需要commit提交事务的，不然实现不了        @Test    public void Adduser(){        SqlSession sqlSession = Mybatisutils.getSqlSession();        UserMapper mapper = sqlSession.getMapper(UserMapper.class);        mapper.addUser(new user(4, &quot;liuliu&quot;, &quot;4&quot;));        sqlSession.commit();        sqlSession.close();    }    @Test    public void UpdateUser(){        SqlSession sqlSession = Mybatisutils.getSqlSession();        UserMapper mapper = sqlSession.getMapper(UserMapper.class);        mapper.updateUser(new user(4,&quot;六六&quot;,&quot;5&quot;));        sqlSession.commit();        sqlSession.close();    }        @Test    public void DelteUser(){        SqlSession sqlSession = Mybatisutils.getSqlSession();        UserMapper mapper = sqlSession.getMapper(UserMapper.class);        mapper.deleteUser(4);        sqlSession.commit();        sqlSession.close();    }}</code></pre></li></ol><h4>Mybatis的配置详情</h4><p>​Mybatis配置文件就是resources文件下的mybatis-config.xml。虽然可以改名，官方推荐使用这个名字</p><blockquote><p>需要注意的是，这些配置是按顺序的，比如环境配置是不能放在属性配置前面的，不然会报错</p></blockquote><p><img src="https://img.yww52.com/2020/6/2020-6-8/img1.png" alt=""></p><h5>环境配置（envirnments）</h5><blockquote><p>MyBatis 可以配置成适应多种环境，这种机制有助于将 SQL 映射应用于多种数据库之中， 现实情况下有多种理由需要这么做。例如，开发、测试和生产环境需要有不同的配置；或者想在具有相同 Schema 的多个生产数据库中使用相同的 SQL 映射。还有许多类似的使用场景。</p><p><strong>不过要记住：尽管可以配置多个环境，但每个 SqlSessionFactory 实例只能选择一种环境。</strong></p><p>所以，如果你想连接两个数据库，就需要创建两个 SqlSessionFactory 实例，每个数据库对应一个。而如果是三个数据库，就需要三个实例，依此类推</p></blockquote><pre><code class="language-xml">&lt;environments default=&quot;development&quot;&gt;            &lt;environment id=&quot;development&quot;&gt;            &lt;transactionManager type=&quot;JDBC&quot;/&gt;            &lt;dataSource type=&quot;POOLED&quot;&gt;                &lt;property name=&quot;driver&quot; value=&quot;com.mysql.jdbc.Driver&quot;/&gt;                &lt;property name=&quot;url&quot; value=&quot;jdbc:mysql://localhost:3306/test?useSSL=false&amp;amp;serverTimezone=UTC&quot;/&gt;                &lt;property name=&quot;username&quot; value=&quot;root&quot;/&gt;                &lt;property name=&quot;password&quot; value=&quot;password&quot;/&gt;            &lt;/dataSource&gt;        &lt;/environment&gt;            &lt;environment id=&quot;test&quot;&gt;            &lt;transactionManager type=&quot;JDBC&quot;/&gt;             &lt;dataSource type=&quot;POOLED&quot;&gt;                &lt;property name=&quot;driver&quot; value=&quot;com.mysql.jdbc.Driver&quot;/&gt;                &lt;property name=&quot;url&quot; value=&quot;jdbc:mysql://localhost:3306/test?useSSL=false&amp;amp;serverTimezone=UTC&quot;/&gt;                &lt;property name=&quot;username&quot; value=&quot;root&quot;/&gt;                &lt;property name=&quot;password&quot; value=&quot;password&quot;/&gt;            &lt;/dataSource&gt;        &lt;/environment&gt;        &lt;/environments&gt;</code></pre><ul><li><p>环境的选择，如果没有环境参数就会选择默认环境，即上述default的环境id</p><pre><code>SqlSessionFactory factory = new SqlSessionFactoryBuilder().build(reader, environment);SqlSessionFactory factory = new SqlSessionFactoryBuilder().build(reader, environment, properties);</code></pre></li><li><p>事务管理器的配置</p><p>如果你正在使用 Spring + MyBatis，则没有必要配置事务管理器，因为 Spring 模块会使用自带的管理器来覆盖前面的配置。</p><pre><code class="language-xml">&lt;transactionManager type=&quot;JDBC&quot;/&gt;</code></pre></li><li><p>数据源的配置</p><pre><code class="language-xml">&lt;dataSource type=&quot;POOLED&quot;&gt;</code></pre></li></ul><h5>属性（properties）</h5><p>属性可以在外部进行配置，并可以进行动态替换。你既可以在典型的 Java 属性文件中配置这些属性，也可以在 properties 元素的子元素中设置。</p><ol><li><p>固定的属性可以直接放在外部的配置文件，比如在resources资源目录下创建一个config-properties文件,&amp;可以不用转译</p><pre><code class="language-properties">driver = com.mysql.jdbc.Driverurl = jdbc:mysql://localhost:3306/test?useSSL=false&amp;serverTimezone=UTCusername = rootpassword = password</code></pre><p>然后在配置文件中调用</p><pre><code class="language-xml">&lt;configuration&gt;    &lt;properties resource=&quot;config.properties&quot;/&gt;        &lt;environments default=&quot;development&quot;&gt;        &lt;environment id=&quot;development&quot;&gt;            &lt;transactionManager type=&quot;JDBC&quot;/&gt;            &lt;dataSource type=&quot;POOLED&quot;&gt;                &lt;property name=&quot;driver&quot; value=&quot;${driver}&quot;/&gt;                &lt;property name=&quot;url&quot; value=&quot;${url}&quot;/&gt;                &lt;property name=&quot;username&quot; value=&quot;${username}&quot;/&gt;                &lt;property name=&quot;password&quot; value=&quot;${password}&quot;/&gt;            &lt;/dataSource&gt;        &lt;/environment&gt;    &lt;/environments&gt;    &lt;mappers&gt;        &lt;mapper resource=&quot;com/chen/dao/UserMapper.xml&quot;/&gt;    &lt;/mappers&gt;&lt;/configuration&gt;</code></pre></li><li><p>也可以动态的调用，比如</p><pre><code class="language-properties">driver = com.mysql.jdbc.Driverurl = jdbc:mysql://localhost:3306/test?useSSL=false&amp;serverTimezone=UTC</code></pre><p>在配置文件中</p><pre><code class="language-xml">&lt;configuration&gt;    &lt;properties resource=&quot;config.properties&quot;&gt;        &lt;property name=&quot;username&quot; value=&quot;root&quot;/&gt;        &lt;property name=&quot;password&quot; value=&quot;password&quot;/&gt;    &lt;/properties&gt;    &lt;environments default=&quot;development&quot;&gt;        &lt;environment id=&quot;development&quot;&gt;            &lt;transactionManager type=&quot;JDBC&quot;/&gt;            &lt;dataSource type=&quot;POOLED&quot;&gt;                &lt;property name=&quot;driver&quot; value=&quot;${driver}&quot;/&gt;                &lt;property name=&quot;url&quot; value=&quot;${url}&quot;/&gt;                &lt;property name=&quot;username&quot; value=&quot;${username}&quot;/&gt;                &lt;property name=&quot;password&quot; value=&quot;${password}&quot;/&gt;            &lt;/dataSource&gt;        &lt;/environment&gt;    &lt;/environments&gt;    &lt;mappers&gt;        &lt;mapper resource=&quot;com/chen/dao/UserMapper.xml&quot;/&gt;    &lt;/mappers&gt;&lt;/configuration&gt;</code></pre></li><li><p>注意的是，外部的优先级较高，在配置文件中properties属性与外部文件的属性冲突时是使用外部文件的属性</p></li></ol><h5>类型别名（typeAliases）</h5><p>类型别名可为 Java 类型设置一个缩写名字。 它仅用于 XML 配置，意在降低冗余的全限定类名书写。</p><ol><li><p>类</p><pre><code class="language-xml">&lt;typeAliases&gt;  &lt;typeAlias alias=&quot;user&quot; type=&quot;com.chen.pojo.user&quot;/&gt;&lt;/typeAliases&gt;</code></pre></li><li><p>包</p><pre><code class="language-xml">&lt;typeAliases&gt;  &lt;package name=&quot;com.chen.pojo&quot;/&gt;&lt;/typeAliases&gt;</code></pre><blockquote><p>指定一个包名，MyBatis 会在包名下面搜索需要的 Java Bean。在没有注解的情况下，会使用 Bean 的首字母小写的非限定类名来作为它的别名。比如在Mybatis会自动扫描路径上的包，在没有注解的情况下，里面的user类的别名就会定义为user</p></blockquote></li><li><p>通过注解来起别名</p><pre><code class="language-Java">@Alias(&quot;user&quot;)public class user {    }</code></pre></li></ol><h5>设置（Settings）</h5><p>这是 MyBatis 中极为重要的调整设置，它们会改变 MyBatis 的运行时行为。了解几个就行</p><table><thead><tr><th align="left"><strong>设置名</strong></th><th><strong>描述</strong></th><th><strong>有效值</strong></th><th><strong>默认值</strong></th></tr></thead><tbody><tr><td align="left">cacheEnabled</td><td>全局性地开启或关闭所有映射器配置文件中已配置的任何缓存。</td><td>true|false</td><td>true</td></tr><tr><td align="left">lazyLoadingEnabled</td><td>延迟加载的全局开关。当开启时，所有关联对象都会延迟加载。 特定关联关系中可通过设置 <code>fetchType</code> 属性来覆盖该项的开关状态。</td><td>true|false</td><td>false</td></tr><tr><td align="left">logImpl</td><td>指定 MyBatis 所用日志的具体实现，未指定时将自动查找。</td><td>SLF4J|LOG4J|LOG4J2JDK_LOGGING|<br />COMMONS_LOGGING|<br />STDOUT_LOGGING<br />|NO_LOGGING</td><td>未设置</td></tr></tbody></table><h5>映射器（mappers）</h5><p>既然 MyBatis 的行为已经由上述元素配置完了，我们现在就要来定义 SQL 映射语句了。 但首先，我们需要告诉 MyBatis 到哪里去找到这些语句。 在自动查找资源方面，Java 并没有提供一个很好的解决方案，所以最好的办法是直接告诉 MyBatis 到哪里去找映射文件。 你可以使用相对于类路径的资源引用，或完全限定资源定位符（包括 <code>file:///</code> 形式的 URL），或类名和包名等。</p><ol><li><p>使用相对于类路径的资源引用，推荐使用这种</p><pre><code class="language-xml">&lt;mappers&gt;  &lt;mapper resource=&quot;org/mybatis/builder/AuthorMapper.xml&quot;/&gt;  &lt;mapper resource=&quot;org/mybatis/builder/BlogMapper.xml&quot;/&gt;  &lt;mapper resource=&quot;org/mybatis/builder/PostMapper.xml&quot;/&gt;&lt;/mappers&gt;</code></pre></li><li><p>使用完全限定资源定位符（URL）,不推荐</p><pre><code class="language-xml">&lt;mappers&gt;  &lt;mapper url=&quot;file:///var/mappers/AuthorMapper.xml&quot;/&gt;  &lt;mapper url=&quot;file:///var/mappers/BlogMapper.xml&quot;/&gt;  &lt;mapper url=&quot;file:///var/mappers/PostMapper.xml&quot;/&gt;&lt;/mappers&gt;</code></pre></li><li><p>使用映射器接口实现类的完全限定类名</p><blockquote><ol><li>接口与Mapper配置文件同名</li><li>接口和它的Mapper配置文件同包</li></ol></blockquote><pre><code class="language-xml">&lt;mappers&gt;  &lt;mapper class=&quot;org.mybatis.builder.AuthorMapper&quot;/&gt;  &lt;mapper class=&quot;org.mybatis.builder.BlogMapper&quot;/&gt;  &lt;mapper class=&quot;org.mybatis.builder.PostMapper&quot;/&gt;&lt;/mappers&gt;</code></pre></li><li><p>将包内的映射器接口实现全部注册为映射器</p><blockquote><ol><li>接口与Mapper配置文件同名</li><li>接口和它的Mapper配置文件同包，也可以将它的Mapper配置文件放在resources下创建相同路径的文件夹</li></ol></blockquote><pre><code class="language-xml">&lt;mappers&gt;  &lt;package name=&quot;org.mybatis.builder&quot;/&gt;&lt;/mappers&gt;</code></pre></li></ol><h4>作用域（Scope）和生命周期</h4><p>理解我们之前讨论过的不同作用域和生命周期类别是至关重要的，因为错误的使用会导致非常严重的并发问题。</p><h5>SqlSessionFactoryBuilder</h5><ul><li>这个类可以被实例化、使用和丢弃，一旦创建了 SqlSessionFactory，就不再需要它了。</li><li>因此 SqlSessionFactoryBuilder 实例的最佳作用域是方法作用域（也就是局部方法变量）。</li></ul><h5>SqlSessionFactory</h5><ul><li>SqlSessionFactory 一旦被创建就应该在应用的运行期间一直存在，没有任何理由丢弃它或重新创建另一个实例。</li><li>因此 SqlSessionFactory 的最佳作用域是应用作用域。</li></ul><h5>SqlSession</h5><ul><li>每个线程都应该有它自己的 SqlSession 实例。</li><li>SqlSession 的实例不是线程安全的，因此是不能被共享的，所以它的最佳的作用域是请求或方法作用域。</li><li>绝对不能将 SqlSession 实例的引用放在一个类的静态域，甚至一个类的实例变量也不行，也绝不能将 SqlSession 实例的引用放在任何类型的托管作用域中。</li><li>如果你现在正在使用一种 Web 框架，考虑将 SqlSession 放在一个和 HTTP 请求相似的作用域中。 换句话说，每次收到 HTTP 请求，就可以打开一个 SqlSession，返回一个响应后，就关闭它。 这个关闭操作很重要，为了确保每次都能执行关闭操作，你应该把这个关闭操作放到 finally 块中。</li></ul><h4>属性名与字段不一致</h4><p>在有些操作里，会因为pojo包里的属性名与数据库的字段名不一样而会达不到应有效果，比如有时候会查不到不同属性名的值，所以就要注意属性名要与字段一致，要是不一样还有一些其他的解决方法。</p><h5>起别名</h5><p>为你不同的属性名取一个与字段一样的别名，这样就能实现属性名与字段一致，比如属性名为password,字段名为pwd，则为password起一个pwd的别名</p><pre><code class="language-xml">&lt;typeAliases&gt;        &lt;typeAlias alias=&quot;pwd&quot; type=&quot;password&quot;/&gt;&lt;/typeAliases&gt;</code></pre><h5>ResultMap</h5><p>resultMap为结果集映射，将你的字段名映射为属性名，便能使属性名与字段一致</p><pre><code class="language-xml">    &lt;resultMap id=&quot;result&quot; type=&quot;user&quot;&gt;   &lt;!--column为数据库字段，映射类型为user，property为映射的属性名--&gt;&lt;result column=&quot;id&quot; property=&quot;id&quot;/&gt;        &lt;result column=&quot;name&quot; property=&quot;name&quot;/&gt;        &lt;!--一样的属性可以不用写，只写不同的就可以--&gt;        &lt;result column=&quot;pwd&quot; property=&quot;password&quot;/&gt;    &lt;/resultMap&gt;&lt;!--resulMap值与上述id一致--&gt;    &lt;select id=&quot;getUserById&quot; parameterType=&quot;int&quot; resultMap=&quot;result&quot;&gt;        select *from mybatis where id= #{id}    &lt;/select&gt;</code></pre><h4>日志</h4><p>Mybatis 通过使用内置的日志工厂提供日志功能。因为Mybatis的设置日志是为设置的，所以需要设置才能看得到<img src="https://img.yww52.com/2020/6/2020-6-8/img2.png" alt=""></p><h5>STDOUT_LOGGING</h5><pre><code class="language-xml">    &lt;settings&gt;        &lt;setting name=&quot;logImpl&quot; value=&quot;STDOUT_LOGGING&quot;/&gt;    &lt;/settings&gt;&lt;!--name与value大小写不要错，空格不要多--&gt;</code></pre><p><img src="https://img.yww52.com/2020/6/2020-6-8/img3.png" alt=""></p><h5>Log4j</h5><p>Log4j是Apache的一个开源项目，通过使用Log4j，我们可以控制日志信息输送的目的地是控制台、文件、GUI组件，甚至是套接口服务器、NT的事件记录器、UNIXSyslog守护进程等；我们也可以控制每一条日志的输出格式；通过定义每一条日志信息的级别，我们能够更加细致地控制日志的生成过程。最令人感兴趣的就是，这些可以通过一个配置文件来灵活地进行配置，而不需要修改应用的代码。</p><ol><li><p>导入依赖</p><pre><code class="language-xml">&lt;!-- https://mvnrepository.com/artifact/log4j/log4j --&gt;        &lt;dependency&gt;            &lt;groupId&gt;log4j&lt;/groupId&gt;            &lt;artifactId&gt;log4j&lt;/artifactId&gt;            &lt;version&gt;1.2.17&lt;/version&gt;        &lt;/dependency&gt;</code></pre></li><li><p>在resouces目录下创建log4j.properties配置文件，并设置配置，网上很多可以去复制</p><pre><code class="language-properties">#将等级为DEBUG的日志信息输出到console和file两个目的地log4j.rootLogger=DEBUG,console,file#控制台输出的相关设置log4j.appender.console=org.apache.log4j.ConsoleAppenderlog4j.appender.console.Target=System.outlog4j.appender.console.Threshold=DEBUGlog4j.appender.console.layout=org.apache.log4j.PatternLayoutlog4j.appender.console.layout.ConversionPattern=[%c]-%m%n#文件输出的相关配置log4j.appender.file=org.apache.log4j.RollingFileAppenderlog4j.appender.file.File=./log/Mybatis.loglog4j.appender.file.MaxFileSize=10mblog4j.appender.file.Threshold=DEBUGlog4j.appender.file.layout=org.apache.log4j.PatternLayoutlog4j.appender.file.layout.ConversionPattern=[%p][%d{yy-MM-dd}[%c]%m%n#日志输出级别log4j.logger.org.mybatis=DEBUGlog4j.logger.java.sql=DEBUGlog4j.logger.java.sql.Statement=DEBUGlog4j.logger.java.sql.ResultSet=DEBUGlog4j.logger.java.sql.PreparedStatement=DEBUG</code></pre></li><li><p>设置日志实现</p><pre><code class="language-xml"> &lt;settings&gt;        &lt;setting name=&quot;logImpl&quot; value=&quot;LOG4J&quot;/&gt;    &lt;/settings&gt;</code></pre></li></ol><h4>注解</h4><p>对于像 BlogMapper 这样的映射器类来说，还有另一种方法来完成语句映射。 它们映射的语句可以不用 XML 来配置，而可以使用 Java 注解来配置。</p><ol><li><p>注解</p><pre><code class="language-java">public interface UserMapper {    @Select(&quot;select * from user&quot;)    List&lt;user&gt; getUserList();}</code></pre></li><li><p>去核心配置文件处绑定接口</p><pre><code class="language-xml">&lt;mappers&gt;    &lt;mapper class=&quot;com.chen.dao.UserMapper&quot;/&gt;&lt;/mappers&gt;</code></pre></li></ol><blockquote><p>使用注解来映射简单语句会使代码显得更加简洁，但对于稍微复杂一点的语句，Java 注解不仅力不从心，还会让你本就复杂的 SQL 语句更加混乱不堪。 因此，如果你需要做一些很复杂的操作，最好用 XML 来映射语句。</p></blockquote>]]></content:encoded>
            <category domain="https://yww52.com/categories/%25E6%2588%2591%25E7%259A%2584%25E6%258A%2580%25E6%259C%25AF%25E5%25AD%25A6%25E4%25B9%25A0%25E7%2589%25A9%25E8%25AF%25AD%25E6%259E%259C%25E7%2584%25B6%25E6%259C%2589%25E9%2597%25AE%25E9%25A2%2598/">我的技术学习物语果然有问题</category>
        </item>
        <item>
            <title>JSP</title>
            <link>https://yww52.com/posts/78b18e67/</link>
            <guid isPermaLink="true">https://yww52.com/posts/78b18e67/</guid>
            <pubDate>Sat, 06 Jun 2020 00:00:00 +0000</pubDate>
            <description>JSP也算Javaweb的基础，还是有必要学的。</description>
            <content:encoded><![CDATA[<img src="https://img.yww52.com/2020/6/2020-6-6/top_img.jpg" alt="JSP" style="max-width:100%;height:auto;margin-bottom:15px;" /><h4>前言</h4><p>​JSP是一门比较老的技术了，在这个前后端分离大火的时代，JSP基本已经被淘汰了，但是JSP还是有学习的必要的，学习了JSP有助于了解JavaWeb，能更好的理解后面所学的框架。</p><h4>什么是JSP</h4><p>​JSP全称为JavaServer Pages,Java服务端页面，是一种动态网页技术，用于实现动态web。JSP部署于网络服务器上，可以响应客户端发送的请求，并根据请求内容动态地生成HTML,XML或其他格式文档的web网页，然后返回给请求者。JSP技术以Java语言作为脚本语言，为用户的HTTP请求提供服务，并能与服务器上的其它Java程序共同处理复杂的业务需求。</p><h4>JSP原理</h4><h5>JSP是怎么实现动态web</h5><p>​JSP页面，比如index.jsp的页面会在tomcat服务器运行的时候会转变成一个Java程序，你可以在idea目录下找到对应的Java程序与字节码文件，点进去看源码会发现，这个Java程序继承了HttpJspBase，而HttpJspBase继承了Sevlet，所以可以看出<strong>JSP本质上就是Servlet</strong>,我们在JSP中用HTML来写，而系统自动用一个Servlet来帮你写出这个JSP，所以Servlet实现了动态Web,从而JSP也实现了动态Web。</p><blockquote><p>我的目录地址为C:\Users\Yw.IntelliJIdea2019.1\system\tomcat\Unnamed_javaweb01\work\Catalina\localhost\web01_war\org\apache\jsp</p></blockquote><h5>JSP的一些源码分析</h5><ol><li><p>首先处理JSP文件</p><pre><code class="language-Java">// 初始化  public void _jspInit() {  }//销毁  public void _jspDestroy() {  }//JSPService，处理请求响应  public void _jspService(final javax.servlet.http.HttpServletRequest request, final javax.servlet.http.HttpServletResponse response)      throws java.io.IOException, javax.servlet.ServletException {    if (!javax.servlet.DispatcherType.ERROR.equals(request.getDispatcherType())) {      final java.lang.String _jspx_method = request.getMethod();      if (&quot;OPTIONS&quot;.equals(_jspx_method)) {        response.setHeader(&quot;Allow&quot;,&quot;GET, HEAD, POST, OPTIONS&quot;);        return;      }      if (!&quot;GET&quot;.equals(_jspx_method) &amp;&amp; !&quot;POST&quot;.equals(_jspx_method) &amp;&amp; !&quot;HEAD&quot;.equals(_jspx_method)) {        response.setHeader(&quot;Allow&quot;,&quot;GET, HEAD, POST, OPTIONS&quot;);        response.sendError(HttpServletResponse.SC_METHOD_NOT_ALLOWED, &quot;JSP 只允许 GET、POST 或 HEAD。Jasper 还允许 OPTIONS&quot;);        return;      }    }</code></pre></li><li><p>判断请求</p></li><li><p>其中还内置了一些对象</p><pre><code class="language-Java">    final javax.servlet.jsp.PageContext pageContext;            //页面上下文    javax.servlet.http.HttpSession session = null;                //Session    final javax.servlet.ServletContext application;                //application    final javax.servlet.ServletConfig config;                    //config    javax.servlet.jsp.JspWriter out = null;                        //out    final java.lang.Object page = this;                            //当前页    </code></pre></li><li><p>输出页面前，还多了一些源码</p><pre><code class="language-Java"> response.setContentType(&quot;text/html&quot;);        //设置响应的页面类型 pageContext = _jspxFactory.getPageContext(this, request, response,                  null, true, 8192, true); _jspx_page_context = pageContext; application = pageContext.getServletContext(); config = pageContext.getServletConfig(); session = pageContext.getSession(); out = pageContext.getOut(); _jspx_out = out;//这些对象可以在JSP页面中直接使用</code></pre></li><li><p>输出页面，比如Index.jsp</p><pre><code class="language-Java">       out.write(&quot;&lt;html&gt;\n&quot;);      out.write(&quot;&lt;body&gt;\n&quot;);      out.write(&quot;&lt;h2&gt;Hello World!&lt;/h2&gt;\n&quot;);      out.write(&quot;&lt;/body&gt;\n&quot;);      out.write(&quot;&lt;/html&gt;\n&quot;);        </code></pre></li><li><p>所以基本上除了输出页面的代码，也就是我们在JSP上写的HTML的代码是不一样的以外，其他的基本上都是一样的，系统帮我写好了实现HTML同样效果的Servlet</p></li><li><p>所以可以得出这样JSP实现的流程。首先客户端向服务器发起了一个请求，服务器中的Web容器会接收请求，找到对应的JSP页面，然后系统会自动将JSP页面转化为Java程序，编译得到class文件，然后web容器帮我们处理这样的class文件，也就是Servlet，Web容器就发起响应，所以我们所看到的就是这个Java程序，也就是Servlet</p></li></ol><blockquote><p>在JSP中</p><p>写HTML的代码，会被转换成out.write(&quot;&quot;)的形式，例如上面的输出页面</p><p>写Java代码，就会是Java的代码</p></blockquote><h4>MVC三层架构</h4><h5>MVC</h5><p>​Model,View,Controller。即模型，视图，控制器。 </p><h5>Controller 控制器</h5><blockquote><p>实质就是Servlet，大概有三个作用</p></blockquote><ol><li>接收用户的请求</li><li>响应交给业务层处理对应代码</li><li>视图跳转</li></ol><h5>View 视图</h5><blockquote><p>实质是JSP，也是Servlet，大概有两个作用</p></blockquote><ol><li>展示数据</li><li>提供链接发起Servlet请求</li></ol><blockquote><p>因为Controller与View都算是Servlet，但为了更好的管理，便规定为Controller写Servlet，View专注于显示界面</p></blockquote><h5>早期架构</h5><p>早期的架构只有控制器和视图两层，即以下的结构</p><p><img src="https://img.yww52.com/2020/6/2020-6-6/img1.png" alt=""></p><blockquote><p>这种架构是在控制器中直接写对用户的操作，例如增删改查，但这样的话，Servlet中就会有很多代码，处理起来十分麻烦，所以便加多了一层Model层</p></blockquote><h5>Model 模型</h5><p>Model其实包括了业务层Service，用户类JavaBean和Dao层</p><h5>在优化下，才出现了如今的MVC三层架构</h5><p><img src="https://img.yww52.com/2020/6/2020-6-6/img2.png" alt=""></p><h4>总结</h4><p>​由此可以知道<strong>JSP其实就是Servlet</strong>，所以能实现动态Web，不过因为这种技术是在HTML中嵌入Java的代码，这是一件令人头疼的事情，毕竟这样的话做一个项目就要同时学习前端和后端，所以前后端分离的思想出来之后，JSP就逐渐被淘汰了。</p>]]></content:encoded>
            <category domain="https://yww52.com/categories/%25E6%2588%2591%25E7%259A%2584%25E6%258A%2580%25E6%259C%25AF%25E5%25AD%25A6%25E4%25B9%25A0%25E7%2589%25A9%25E8%25AF%25AD%25E6%259E%259C%25E7%2584%25B6%25E6%259C%2589%25E9%2597%25AE%25E9%25A2%2598/">我的技术学习物语果然有问题</category>
        </item>
        <item>
            <title>第一个Servlet程序</title>
            <link>https://yww52.com/posts/26bc41e8/</link>
            <guid isPermaLink="true">https://yww52.com/posts/26bc41e8/</guid>
            <pubDate>Fri, 05 Jun 2020 00:00:00 +0000</pubDate>
            <description>简单使用servlet的小demo</description>
            <content:encoded><![CDATA[<img src="https://img.yww52.com/2020/6/2020-6-5/top_img.jpg" alt="第一个Servlet程序" style="max-width:100%;height:auto;margin-bottom:15px;" /><ol><li><p>创建一个Maven项目，不用选择模板先</p><p><img src="https://img.yww52.com/2020/6/2020-6-5/img1.png" alt=""></p></li><li><p>导入依赖</p><pre><code class="language-xml">&lt;!--在pom.xml的project标签里导入依赖--&gt;&lt;dependencies&gt;        &lt;dependency&gt;            &lt;groupId&gt;javax.servlet&lt;/groupId&gt;            &lt;artifactId&gt;javax.servlet-api&lt;/artifactId&gt;            &lt;version&gt;4.0.1&lt;/version&gt;            &lt;scope&gt;provided&lt;/scope&gt;        &lt;/dependency&gt;        &lt;dependency&gt;            &lt;groupId&gt;javax.servlet.jsp&lt;/groupId&gt;            &lt;artifactId&gt;jsp-api&lt;/artifactId&gt;            &lt;version&gt;2.1&lt;/version&gt;            &lt;scope&gt;provided&lt;/scope&gt;        &lt;/dependency&gt;    &lt;/dependencies&gt;</code></pre></li><li><p>创建一个子模块，选择图中的模板</p><p><img src="https://img.yww52.com/2020/6/2020-6-5/img2.png" alt=""></p><blockquote><p>接下来配置好maven和setting.xml和maven-repo的路径，大概如图所示</p></blockquote><p><img src="https://img.yww52.com/2020/6/2020-6-5/img8.png" alt=""></p></li><li><p>创建好子模块后，更新web.xml</p><pre><code class="language-xml">&lt;!--更新src/main/webapp/WEB-INF下的web.xml如下--&gt;&lt;web-app xmlns=&quot;http://xmlns.jcp.org/xml/ns/javaee&quot;         xmlns:xsi=&quot;http://www.w3.org/2001/XMLSchema-instance&quot;         xsi:schemaLocation=&quot;http://xmlns.jcp.org/xml/ns/javaee     http://xmlns.jcp.org/xml/ns/javaee/web-app_3_1.xsd&quot;         version=&quot;3.1&quot;&gt;&lt;/web-app&gt;</code></pre></li><li><p>还原配置</p><p>在main下创建好java包和resources包，并在右键Mark Director as标记两个包，Java包为Sources Root,resources包为Resources Root类型，成功如图下</p><p><img src="https://img.yww52.com/2020/6/2020-6-5/img3.png" alt=""></p></li><li><p>在Java包里编写servle程序，若没有添加servlet程序选项可以自行创建类，然后继承HttpServlet接口</p><p><img src="https://img.yww52.com/2020/6/2020-6-5/img4.png" alt=""></p></li><li><p>编写好后点击右上角的设置，配置好Tomcat服务器的设置</p><p><img src="https://img.yww52.com/2020/6/2020-6-5/img5.png" alt=""></p><p><img src="https://img.yww52.com/2020/6/2020-6-5/img6.png" alt=""></p><blockquote><p>添加之后配置好部署，添加子模块的war包，就可以启动Tomcat了</p></blockquote><p><img src="https://img.yww52.com/2020/6/2020-6-5/img7.png" alt=""></p></li></ol>]]></content:encoded>
            <category domain="https://yww52.com/categories/%25E6%2588%2591%25E7%259A%2584%25E6%258A%2580%25E6%259C%25AF%25E5%25AD%25A6%25E4%25B9%25A0%25E7%2589%25A9%25E8%25AF%25AD%25E6%259E%259C%25E7%2584%25B6%25E6%259C%2589%25E9%2597%25AE%25E9%25A2%2598/">我的技术学习物语果然有问题</category>
        </item>
        <item>
            <title>简单的Web基础</title>
            <link>https://yww52.com/posts/afc5caff/</link>
            <guid isPermaLink="true">https://yww52.com/posts/afc5caff/</guid>
            <pubDate>Thu, 04 Jun 2020 00:00:00 +0000</pubDate>
            <description>刚学web就要先了解一些基础。</description>
            <content:encoded><![CDATA[<img src="https://img.yww52.com/2020/6/2020-6-4/top_img.jpg" alt="简单的Web基础" style="max-width:100%;height:auto;margin-bottom:15px;" /><h2>web应用</h2><p>web应用程序是一个可以提供浏览器访问的程序，一个web服务器应该是由多部份组成的，前端页面，webService功能，和Java程序，等等</p><h2>静态web</h2><p>​静态网站是最初的建站方式，浏览者所看到的每个页面是建站者上传到服务器上的一个 html （ htm ）文件，这种网站每增加、删除、修改一个页面，都必须重新对服务器的文件进行一次下载上传。网页内容一经发布到网站服务器上，无论是否有用户访问，每个静态网页的内容都是保存在网站服务器上的，也就是说，静态网页是实实在在保存在服务器上的文件，每个网页都是一个独立的文件</p><p><img src="https://img.yww52.com/2020/6/2020-6-4/img1.png" alt="静态web"></p><h2>动态web</h2><p>​动态web有点类似静态web，但不是那种有图片动，轮播图的那些伪动态，它的资源数据不是一成不变的，就比如是淘宝，不同的人会有不同的推荐​<img src="https://img.yww52.com/2020/6/2020-6-4/img2.png" alt="动态web"></p><h2>web服务器</h2><p>​web服务器用来处理用户的请求，然后返回响应。有LLS,Tomcat服务器，我们主要学的就是Tomcat服务器。现在来了解一下Tomcat服务器。</p><h2>Tomcat</h2><p>​Tomcat是Apache 软件基金会（Apache Software Foundation）的Jakarta 项目中的一个核心项目，由于有了Sun 的参与和支持，最新的Servlet 和JSP 规范总是能在Tomcat 中得到体现。因为Tomcat 技术先进、性能稳定，而且免费，因而深受Java 爱好者的喜爱并得到了部分软件开发商的认可，成为目前比较流行的Web 应用服务器。</p><p>​Tomcat 服务器是一个免费的开放源代码的Web 应用服务器，属于轻量级应用服务器，在中小型系统和并发访问用户不是很多的场合下被普遍使用，是开发和调试JSP 程序的首选。对于一个初学者来说，可以这样认为，当在一台机器上配置好Apache 服务器，可利用它响应HTML（标准通用标记语言下的一个应用）页面的访问请求。实际上Tomcat是Apache 服务器的扩展，但运行时它是独立运行的，所以当你运行tomcat 时，它实际上作为一个与Apache 独立的进程单独运行的。</p><p>​Apache 为HTML页面服务，而Tomcat 实际上运行JSP 页面和Servlet。另外，Tomcat和IIS等Web服务器一样，具有处理HTML页面的功能，另外它还是一个Servlet和JSP容器，独立的Servlet容器是Tomcat的默认模式。不过，Tomcat处理静态HTML的能力不如Apache服务器。</p><p>​这是百度对Tomcat的描述，其实在现在看来，我们淘汰了JSP这项技术，但Tomcat依旧是十分主流的web服务器。</p><p><img src="https://cdn.jsdelivr.net/gh/whyneh/HexoFile1@v1.5/2020/6/2020-6-4/img3.png" alt=""></p><p>​这就Tomcat所有的文件，大致都可以了解一下，<strong>bin为一些脚本文件，比如启动和关闭的脚本文件，conf为配置文件，lib是一些所需的依赖jar包，logs为日志，webapps用来存放网站</strong>。</p><ul><li><p>可以配置启动的端口号</p><pre><code class="language-xml">&lt;!--conf/server.xml--&gt; &lt;Connector port=&quot;8080&quot; protocol=&quot;HTTP/1.1&quot;               connectionTimeout=&quot;20000&quot;               redirectPort=&quot;8443&quot;                URIEncoding=&quot;UTF-8&quot;/&gt;&lt;!--默认是为8080端口，可以修改--&gt;</code></pre></li><li><p>可以配置主机名</p><pre><code class="language-xml">&lt;!--conf/server.xml--&gt; &lt;Host name=&quot;localhost&quot;  appBase=&quot;webapps&quot;            unpackWARs=&quot;true&quot; autoDeploy=&quot;true&quot;&gt;&lt;!--这里可以改主机名，但要去hosts文件里加个映射才能生效，也可以改存放网站的位置--&gt;</code></pre><p>为什么要改hosts文件，这就涉及到网站是如何进行访问。</p><ol><li><p>首先客户端输入域名发起请求</p></li><li><p>客户端先会对本机的hosts进行检查，检查是否有这个域名的映射，要是有就会返回这个域名对应的ip地址，然后就可以直接访问相关的web程序，要是没有检查到就会去DNS服务器上寻早该域名对应的ip(这也就是所理解的dns解析域名)，找到就返回ip，找不到就访问不了</p></li></ol></li></ul>]]></content:encoded>
            <category domain="https://yww52.com/categories/%25E6%2588%2591%25E7%259A%2584%25E6%258A%2580%25E6%259C%25AF%25E5%25AD%25A6%25E4%25B9%25A0%25E7%2589%25A9%25E8%25AF%25AD%25E6%259E%259C%25E7%2584%25B6%25E6%259C%2589%25E9%2597%25AE%25E9%25A2%2598/">我的技术学习物语果然有问题</category>
        </item>
        <item>
            <title>关于equels与==</title>
            <link>https://yww52.com/posts/4447e73e/</link>
            <guid isPermaLink="true">https://yww52.com/posts/4447e73e/</guid>
            <pubDate>Mon, 01 Jun 2020 00:00:00 +0000</pubDate>
            <description>在初学Java的时候，equels与==总是会感觉很混乱，这次特意来总结一下。</description>
            <content:encoded><![CDATA[<img src="https://img.yww52.com/2020/6/2020-6-1top_img.jpg" alt="关于equels与==" style="max-width:100%;height:auto;margin-bottom:15px;" /><h4>前言</h4><p>​在初学Java的时候，equels与==总是会感觉很混乱，这次特意来总结一下。首先来大致了解一下这两个的意思。</p><ul><li><p>==作用是来判断两个对象是不是同一个对象，因为Java只有值传递，所以对于==来说不管是基本的数据类型还是引用的数据类型，其本质比较的都是值，只是引用的数据类型变量的值是对象的地址。所以就会容易搞混</p></li><li><p>equals方法也是比较两个对象是否相等。equals方法是Object类中的方法。</p><pre><code class="language-Java">//这是Object类中的equals方法public boolean equals(Object obj){    return(this == obj);}</code></pre></li></ul><h4>在基本数据类型中的比较</h4><p>​基本数据类型不能算做为对象，所以并没有继承有equals的方法，所以只有==的比较</p><pre><code class="language-Java"> int num1 = 15;     int num2 = 15;    System.out.println(num1==num2);// 运行结果为true，==判断值是否相等</code></pre><h4>在引用数据类型中的比较</h4><h5>String类型中的比较</h5><p>​String类型是重写了equals方法的，所以这里会经常弄乱，重写后的equals方法为</p><pre><code class="language-Java"> public boolean equals(Object anObject) {        if (this == anObject) {            return true;        }        if (anObject instanceof String) {            String anotherString = (String) anObject;            int n = value.length;            if (n == anotherString.value.length) {                char v1[] = value;                char v2[] = anotherString.value;                int i = 0;                while (n-- != 0) {                    if (v1[i] != v2[i])                            return false;                    i++;                }                return true;            }        }        return false;    }//String类重写的equals方法</code></pre><h6>不new String的情况</h6><pre><code class="language-java">        String a = &quot;ab&quot;;        String b = &quot;ab&quot;;      System.out.println(a==b);      System.out.println(a.equals(b));//两个结果都是true</code></pre><p>​为什么两个都是True，这就要关系到字符串常量池了。当进行第一条语句的时候，编译器就会先在字符串常量池寻找有没有&quot;ab&quot;这个字符串，若没有就会将这个字符串放入字符串常量池。第二条语句进行的时候，就可以找到&quot;ab&quot;这个字符串，就会把字符串常量池里的引用地址给b，所以a与b的字符串是同一个地址的，所以==是true的。在上面的String重写的equals方法可以看出，equals方法是比较内容是否相同，所以第二个也是true。</p><h6>new String的情况</h6><pre><code class="language-Java">       String a = new String(&quot;ab&quot;);       String b = new String(&quot;ab&quot;);     System.out.println(a==b);     System.out.println(a.equals(b));</code></pre><p>new String的语句是在堆内存中开辟一块空间用来存放&quot;ab&quot;这个字符串，然后在栈中开辟一块a或b的空间放着字符串的对象的引用，所以a,b两个虽然内容相同，但其实是两个对象，所以地址是不一样的，==在引用中比较的就是对象的地址，结果就是false。String的equals方法是比较内容的，内容相同就是true</p><h5>不在String中比较</h5><pre><code class="language-Java">Scanner scanner = new Scanner(System.in);        Scanner scanner2 = new Scanner(System.in);       System.out.println(scanner.equals(scanner2));       //false       System.out.println(scanner==scanner2);   //false        Scanner sc = scanner;       System.out.println(scanner.equals(sc));            //true       System.out.println(scanner==sc);     //true</code></pre><p>​这个就比较容易理解了，因为equals方法没被重写，所以是比较两个对象是否是同一个对象，==比较两个地址是否是一样，也是比较是否为同一个对象，所以这两者的作用在equals方法没被重写的时候是一样的。</p><h4>总结</h4><ul><li><p>在基本数据类型中</p><p>==比较两个值是否相同</p></li><li><p>在引用类型中</p><ul><li><p>equals方法没被重写==与equals方法比较的是两个的地址是否相同</p></li><li><p>equals方法被重写，以String类举例</p><p>==比较地址，equals比较方法</p></li></ul></li></ul><h4>一个特殊的例子</h4><p>首先先看代码</p><pre><code class="language-Java">Integer num11 = 127;        Integer num22 = 127;      System.out.println(num11 == num22);//true  System.out.println(num11 == 127);//true        Integer num111 = 128;        Integer num222 = 128;      System.out.println(num111 == num222);//false  System.out.println(num111 == 128);//true</code></pre><p>​按照所想，在Integer类当中，==是比较两个是否是同一个地址，但上述的运行结果中却出现了一个false，接下来在进行以下测试</p><pre><code class="language-Java">Integer num11 = new Integer(127);        Integer num22 = new Integer(127);      System.out.println(num11 == num22);//false      System.out.println(num11 == 127);//true        Integer num111 = new Integer(128);        Integer num222 = new Integer(128);      System.out.println(num111 == num222);//false      System.out.println(num111 == 128);//true</code></pre><blockquote><p>由此可以看出</p><ol><li>第一段代码中，在-128~127的范围中，==的结果是为true，而不在这个范围中，==则为false</li><li>第二段代码中，两个对象不一样，地址不一样，所以==的结果接就是就是false，没有任何异常的地方。</li><li>直接用Integer类型与Int类型作比较，值相等就为true</li></ol></blockquote><p>最后在寻找过后得到结果</p><p>​Java对于Integer是有一种缓存机制的，缓存了-128到127之间的数字，有点类似String，所以在第一个例子中，num11与num22其实是共享一个地址的，所以==的结果是true，不在缓存范围外的数字，并不是共享一个地址，所以==的结果就为false</p>]]></content:encoded>
            <category domain="https://yww52.com/categories/%25E6%2588%2591%25E7%259A%2584%25E6%258A%2580%25E6%259C%25AF%25E5%25AD%25A6%25E4%25B9%25A0%25E7%2589%25A9%25E8%25AF%25AD%25E6%259E%259C%25E7%2584%25B6%25E6%259C%2589%25E9%2597%25AE%25E9%25A2%2598/">我的技术学习物语果然有问题</category>
        </item>
        <item>
            <title>Markdown</title>
            <link>https://yww52.com/posts/6fc8eab5/</link>
            <guid isPermaLink="true">https://yww52.com/posts/6fc8eab5/</guid>
            <pubDate>Fri, 15 May 2020 00:00:00 +0000</pubDate>
            <description>Markdown是一种轻量级的标记语言，能很大程度提高你写作的体验，也能更好的渲染。</description>
            <content:encoded><![CDATA[<img src="https://img.yww52.com/2020/5/2020-5-15top_img.jpg" alt="Markdown" style="max-width:100%;height:auto;margin-bottom:15px;" /><h2>标题</h2><ul><li><p>使用=和-来标记一级和二级标题</p><pre><code class="language-Java">一级标题=============二级标题-----------------</code></pre></li><li><p>使用#来标记标题</p><pre><code class="language-Java"># 一级标题## 二级标题### 三级标题#### 四级标题##### 五级标题###### 六级标题// 推荐这种写法</code></pre></li></ul><h2>字体</h2><pre><code class="language-Java">*斜体文本*_斜体文本_**粗体文本**__粗体文本__***粗斜体文本***___粗斜体文本___//效果图如下</code></pre><blockquote><p><em>斜体文本</em><em>斜体文本</em><strong>粗体文本</strong><strong>粗体文本</strong><em><strong>粗斜体文本</strong></em><em><strong>粗斜体文本</strong></em></p></blockquote><h2>特定格式</h2><ul><li><p>分割线</p><pre><code class="language-Java">**** * ******- - -----------//  这些方式都可以代表分隔线，符号只要大于三个，有无空格都可以，效果就像下方这条线</code></pre><blockquote><hr></blockquote></li><li><p>删除线</p><pre><code class="language-Java">~~删除线~~</code></pre><blockquote><p><del>删除线</del></p></blockquote></li><li><p>下划线</p><pre><code class="language-java">下划线就用&lt;u&gt;标签</code></pre></li><li><p>脚注</p><pre><code class="language-Java">//[^脚注]  //[^脚注]:脚注的内容 </code></pre></li></ul><h2>列表</h2><ul><li><p>有序列表</p><pre><code class="language-java">* 第一项* 第二项* 第三项+ 第一项+ 第二项+ 第三项- 第一项- 第二项- 第三项//三种方式都可以</code></pre><blockquote><ul><li>第一项</li><li>第二项</li><li>第三项</li></ul></blockquote></li><li><p>无序列表</p><pre><code class="language-java">1. 第一项2. 第二项3. 第三项</code></pre><blockquote><ol><li>第一项</li><li>第二项</li><li>第三项</li></ol></blockquote></li></ul><h2>区块</h2><pre><code class="language-Java">/*&gt; 一级区块&gt;&gt; 二级区块&gt;&gt;&gt; 三级区块*/</code></pre><blockquote><p>一级区块</p><blockquote><p>二级区块</p><blockquote><p>三级区块</p></blockquote></blockquote></blockquote><h2>代码块</h2><pre><code class="language-Java">/*```java(语言，可以不写)```*/// 就是这个框</code></pre><h2>链接</h2><pre><code>1. [链接名称](链接地址)2. &lt;链接地址&gt;</code></pre><blockquote><ol><li><a href="http://www.baidu.com">百度</a></li><li><a href="http://www.baidu.com">http://www.baidu.com</a></li></ol></blockquote><h2>图片</h2><pre><code class="language-Java">![alt 属性文本](图片地址)![alt 属性文本](图片地址 &quot;可选标题&quot;)// 就不插入了,里面的属性文本可以不写，要是会HTML就自己加</code></pre><h2>表格</h2><pre><code class="language-Java">| 左对齐 | 右对齐 | 居中对齐 || :-----| ----: | :----: || 单元格 | 单元格 | 单元格 || 单元格 | 单元格 | 单元格 |// 表头与单元格之间用---来分隔  :-----左对齐  ----:右对齐  :----:居中对齐</code></pre><table><thead><tr><th align="left">左对齐</th><th align="right">右对齐</th><th align="center">居中对齐</th></tr></thead><tbody><tr><td align="left">单元格</td><td align="right">单元格</td><td align="center">单元格</td></tr><tr><td align="left">单元格</td><td align="right">单元格</td><td align="center">单元格</td></tr></tbody></table><h2>图</h2><ul><li><strong>流程图</strong></li><li><strong>UML时序图</strong></li><li><strong>甘特图</strong></li></ul><h2>其他的技巧</h2><ul><li><p>转义</p><pre><code>Markdown里有很多特定符号不能直接显示，所以要想显示这些符号就要在这些符号前面加上反斜线\`   反引号*   星号_   下划线{}  花括号[]  方括号()  小括号#   井字号+   加号-   减号.   英文句点!   感叹号</code></pre><blockquote><p><strong>没有反斜线的星号</strong></p><p>** 正常显示星号 **</p></blockquote></li><li><p>公式</p><pre><code class="language-Java">使用$$包裹Tex或LaTex格式的数学公式来实现    $$        ------    $$// 不会公式就去学习学习</code></pre></li></ul>]]></content:encoded>
            <category domain="https://yww52.com/categories/%25E6%2588%2591%25E7%259A%2584%25E6%258A%2580%25E6%259C%25AF%25E5%25AD%25A6%25E4%25B9%25A0%25E7%2589%25A9%25E8%25AF%25AD%25E6%259E%259C%25E7%2584%25B6%25E6%259C%2589%25E9%2597%25AE%25E9%25A2%2598/">我的技术学习物语果然有问题</category>
        </item>
        <item>
            <title>Matlab</title>
            <link>https://yww52.com/posts/dc9cb9f1/</link>
            <guid isPermaLink="true">https://yww52.com/posts/dc9cb9f1/</guid>
            <pubDate>Mon, 09 Dec 2019 00:00:00 +0000</pubDate>
            <description>数学软件，一些简单的操作。</description>
            <content:encoded><![CDATA[<img src="https://yww52.com/default_cover.jpg" alt="Matlab" style="max-width:100%;height:auto;margin-bottom:15px;" /><h4>基本操作</h4><pre><code class="language-matlab">%定义变量syms a%清屏clc%清空所有clear</code></pre><h4>矩阵的操作</h4><pre><code class="language-matlab">%定义矩阵,空格或逗号分隔一行里的元素，分号是分行a = [ ; ; ]%定义元素全为1的矩阵A1 = ones(2,4)%定义元素全为0的矩阵A2 = zeros(3,3)%定义单位矩阵A3 = eye(4)%定义随机数的矩阵A4 = rand(2,3)%定义魔方矩阵，每行每列没对角线的和相等A5 = magic(3)</code></pre><pre><code class="language-matlab">%求A行列式的值det(A)%求A矩阵的秩(r(A))rank(A)%求A矩阵的迹(tr(A))trace(A)%求A行列式的逆矩阵inv(A)%求A的特征值与特征向量eig(A)%化简矩阵Arref(A)</code></pre><h5>克莱姆法则求解</h5><pre><code class="language-matlab">% 克莱姆法则求解非齐次线性方程组的解% 判断行列式是否为0，不为0即有唯一解D = det(A)%然后用结果那一列换到行列式里，得D1,D2等x1 = D1/D x2 = D2/D ....% 克莱姆法则求解齐次线性方程组的解% 判断行列式是否为0，不为0即仅有零解，为0则有非零解</code></pre><h5>求线性方程组得通解</h5><pre><code class="language-matlab">%A为系数矩阵，b为结果juz%先用pinv求x0x0 = pinv(A)*b(先format rat,然后x0 = A\b)%然后求解z = null(A,&#39;r&#39;)</code></pre><h4>数学分析的基本运算</h4><pre><code class="language-matlab">%复合函数z = compose(f,g)%即Z(x)=f(g(x)) %求解反函数gx = finverse(fx)%求数列与函数的极限limit(fx ,x,0)%表示fx这个算式在x-&gt;0的情况下的极限值%求导数与偏导数diff(fx,x,1)%表示求f的一阶导数%求不定积分int(y,x)%表示y式的不定积分%求定积分int(fx,x,0,inf)%表示fx在dx的情况下0到无穷大的定积分</code></pre><h5>雅可比矩阵</h5><pre><code class="language-matlab">% 定义函数矩阵f = [x*y*z; y; x+z];v = [x,y,z];%表示按dx,dy,dz的顺序求解jacobi矩阵R = jacobian(f,v)</code></pre><h5>求解多项式的根</h5><pre><code class="language-matlab">%比如多项式 x^3-6*x^2-72*x-27 = 0p = [1 -6 72 -27]r = roots(p)</code></pre><h4>微分方程</h4><pre><code class="language-matlab">%求解微分方程,fx为微分方程或方程组,cond为初始条件或边界条件，name表示变量，默认是tY = dsolve(&#39;fx1,fx2...&#39;,&#39;cond1,cond2...&#39;,&#39;name&#39;)</code></pre><h4>Matlab作图</h4><h5>作图的基本的操作</h5><pre><code class="language-matlab">% 为图像起标题title(&#39;&#39;)%为x轴，y轴起标题xlabel(&#39;&#39;)ylabel(&#39;&#39;)%创建一个窗口figure%继续在这个窗口画图hold%起标识legend()%加网格线grid on</code></pre><h5>作图</h5><pre><code class="language-matlab">% 画散点图 scatter(X,Y,S,C)%S是标记面积的大小，S为空则为默认大小，C为RGB值%如scantter(X,Y,S，&#39;r&#39;,&#39;filled&#39;)，用红色来画散点，filled为填充标记% 二维曲线plot(x,y)plot(x,y1,x,y2)plot(x,y,linespec)%linespec为线型，标记，和颜色% rectangle函数作图rectangle(&#39;Position&#39;,[x,y,w,h])%x，y为矩形左下角坐标，w为宽,h为高rectangle(&#39;Position&#39;,[x,y,w,h],&#39;Curvature&#39;,cur)%加入曲率，cur为标量值，曲率从0-1，故[1 1]创建一个椭圆或者园% 产生网格meshgrid(x,y)%从数组x,y产生网格% 画mesh图和surf图mesh(x,y,z)surf(x,y,z)</code></pre><h5>动态</h5><pre><code class="language-matlab">%二维彗星图comet(x,y,n)%n表示轨迹尾线的长度，从0到1</code></pre>]]></content:encoded>
            <category domain="https://yww52.com/categories/%25E6%2588%2591%25E7%259A%2584%25E6%258A%2580%25E6%259C%25AF%25E5%25AD%25A6%25E4%25B9%25A0%25E7%2589%25A9%25E8%25AF%25AD%25E6%259E%259C%25E7%2584%25B6%25E6%259C%2589%25E9%2597%25AE%25E9%25A2%2598/">我的技术学习物语果然有问题</category>
        </item>
        <item>
            <title>git常用命令</title>
            <link>https://yww52.com/posts/423abe9e/</link>
            <guid isPermaLink="true">https://yww52.com/posts/423abe9e/</guid>
            <pubDate>Thu, 28 Nov 2019 00:00:00 +0000</pubDate>
            <description>Git是目前世界上最先进的分布式版本控制系统，能够很好的帮你管理项目。</description>
            <content:encoded><![CDATA[<img src="https://img.yww52.com/2019/11/2019-11-28/top_img.jpg" alt="git常用命令" style="max-width:100%;height:auto;margin-bottom:15px;" /><h2>本地库操作</h2><h3>git的签名</h3><pre><code class="language-bash">#用户名和邮件地址又称签名#系统用户级别的签名设置（这个是一定要设置的）git config --global user.name namegit config --global user.email email#仓库级别的签名设置git config user.name name git config user.email email#这两个签名就是范围不同，一个是全局的一个是部分的，当两个签名都存在时，就会使用仓库级别的签名#注意的是，用户名不能是中文的</code></pre><h3>git的添加和删除</h3><pre><code class="language-bash">#将操作提交的暂存区git add file#将所有操作都提交到暂存区git add .#将操作从暂存区删除掉git rm --cached file</code></pre><h3>git提交</h3><pre><code class="language-bash">#将暂存区中没提交的操作提交到本地版本库git commit file#因为git每次提交都需要写日志来清楚知道修改了什么，所以可以这么写git commit -m&quot;message&quot; (file)#还能这么写,-a表示执行修改或删除的操作都提交到本地库，可以不用add操作git commit -a -m&quot;message&quot;</code></pre><h3>git状态查看</h3><pre><code class="language-bash">#查看库中有无修改操作，即有无操作可以add或commitgit status file</code></pre><h3>查看历史记录</h3><pre><code class="language-bash">#这些操作都是查看历史版本的，只是显示的方式不同    git log     git log --pretty=oneline    git log --oneline    git reflog #这个方式比较好看出索引和易于前进后退</code></pre><h3>前进后退</h3><pre><code class="language-bash"># 基于索引操作git reset --hard &quot;索引值&quot;# 用^来操作git reset --hard Head^^#该操作只能后退，一个^后退一个版本#用~来操作       git reset --hard Head~n#这样表示后退n个版本    </code></pre><h3>reset三个参数的对比</h3><h4>soft</h4><ul><li>仅仅时在本地库移动HEAD指针</li></ul><h4>mixed</h4><ul><li><p>在本地库移动HEAD指针</p></li><li><p>重置暂存区</p></li></ul><h4>hard</h4><ul><li>在本地库移动HEAD指针</li><li>重置暂存区</li><li>重置工作区</li></ul><h3>从本地库中删除文件</h3><pre><code class="language-bash">#用rm命令把文件从工作区删除rm file#此时用status命令可以发现该操作可以添加到暂存区git add file#然后把该操作提交到本地版本库就可以从本地库中删除该文件git commit -m&quot;delete a file&quot;</code></pre><h3>比较文件差异</h3><pre><code class="language-bash">#将工作区中的文件和暂存区进行比较git diff file#将工作区中的文件和本地库历史记录比较，不带文件名时多个文件比较git diff [历史版本] file      </code></pre><h2>分支操作</h2><h3>查看分支情况</h3><pre><code class="language-bash">git branchgit branch -v </code></pre><h3>新建分支</h3><pre><code class="language-bash">git branch branch_name</code></pre><h3>切换分支</h3><pre><code class="language-bash">git checkout branch_name</code></pre><h3>合并分支</h3><pre><code class="language-bash">#切换到被合并的分支上，然后执行merge命令git merge branch_name</code></pre><h2>关于远程仓库的操作</h2><h3>关于远程仓库的地址</h3><pre><code class="language-bash">#查看现在库里保存的远程仓库的地址git remote -v#设置远程仓库的地址，也可以理解为为origin创建一个别名git remote add origin (加上远程仓库的地址，就比如https)#删除现在的远程仓库的地址git remote rm origin</code></pre><h3>本地库的推送</h3><pre><code class="language-bash">git push origin master#master可以改为其他分支，必须指定</code></pre><h3>克隆</h3><pre><code class="language-bash">git clone (加上远程库的地址)#克隆会帮你初始好本地库，并帮你把远程库下载到本地，还可以为你的origin创建好别名</code></pre><h3>拉取</h3><pre><code class="language-bash">#pull=fetch+merge#抓取,该操作只是把远程仓库的东西下载下来，并没有修改你本地的文件git fetch origin master #这时你可以切换到origin/master这个分支上查看变化git checkout origin/master #然后通过两个分支的合并就可以变成远程仓库的样子git merge origin/master#当然直接执行pull也可以git pull origin master</code></pre>]]></content:encoded>
            <category domain="https://yww52.com/categories/%25E6%2588%2591%25E7%259A%2584%25E6%258A%2580%25E6%259C%25AF%25E5%25AD%25A6%25E4%25B9%25A0%25E7%2589%25A9%25E8%25AF%25AD%25E6%259E%259C%25E7%2584%25B6%25E6%259C%2589%25E9%2597%25AE%25E9%25A2%2598/">我的技术学习物语果然有问题</category>
        </item>
        <item>
            <title>如何给你的U盘或移动硬盘自定义图标</title>
            <link>https://yww52.com/posts/54d98528/</link>
            <guid isPermaLink="true">https://yww52.com/posts/54d98528/</guid>
            <pubDate>Wed, 27 Nov 2019 00:00:00 +0000</pubDate>
            <description>上网冲浪的时候看到的一个骚操作，记录一下</description>
            <content:encoded><![CDATA[<img src="https://yww52.com/default_cover.jpg" alt="如何给你的U盘或移动硬盘自定义图标" style="max-width:100%;height:auto;margin-bottom:15px;" /><h1>操作步骤</h1><p>首先，选好自己想要自定义的图片，将该图片转为32*32的ico文件。关于怎么转换，直接上网搜就好了。</p><p>然后新建一个 <code>autorun.inf</code> 文件，并编辑里面的内容为如下</p><pre><code>[autorun]icon=icon.ico</code></pre><p>emm,那个icon.ico就是你那个图片转化之后的ico文件，名字是可以修改的，只要<code>autorun.inf</code>的文件内容里也跟着修改就行了，然后将ico的图片和那个 <code>autorun.inf</code>文件放在移动硬或U盘的根目录下面就完工了。如果你是U盘，就拔掉在插入，如果是移动硬盘可能就要重启了。</p><h1>关于可能出现的问题</h1><p>当你新建好 <code>autorn.inf</code>这个文件后，将它移入U盘，或者直接在U盘下新建 <code>autorn.inf</code>这个文件，可能系统会提示你没有权限执行。这个问题也卡了我好久，对于这个问题你就要赋予U盘所有的权限，即你的用户需要是管理员权限，当你弄好权限后就可以将 <code>autorn.inf</code>这个文件放进U盘了。</p><p>不过可能系统还会告诉你你没有权限执行，这里我也是查了好久，最后我查了一下 <code>autorn.inf</code>这个文件，发现该文件的作用是允许在双击磁盘时自动运行指定的某个文件，而且近几年出现了有利用该文件来传播木马和病毒，即木马或病毒可能会因为你的U盘来侵入你的电脑。折腾了一下，也查了一下，就知道了方法。只要你将你电脑的所有安全杀毒关掉，就可以将 <code>autorn.inf</code>这个文件放进去了。</p><blockquote><p>这是从某位博主上发现的操作，时代久远，已经找不到原文了</p></blockquote>]]></content:encoded>
            <category domain="https://yww52.com/categories/%25E7%259E%258E%25E6%258A%2598%25E8%2585%25BE/">瞎折腾</category>
        </item>
        <item>
            <title>HCS</title>
            <link>https://yww52.com/posts/a8fe19e2/</link>
            <guid isPermaLink="true">https://yww52.com/posts/a8fe19e2/</guid>
            <pubDate>Mon, 25 Nov 2019 00:00:00 +0000</pubDate>
            <description>HTML+CSS+JavaScript，前端三件套。以弃。</description>
            <content:encoded><![CDATA[<img src="https://img.yww52.com/2019/11/2019-11-25top_img.jpg" alt="HCS" style="max-width:100%;height:auto;margin-bottom:15px;" /><h2>HTML</h2><blockquote><p>Hyper Text Markup Language 超文本标记语言由标签构成的语言，标记语言不是编程语言</p></blockquote><ul><li><p>HTML文档后缀名为html或htm</p></li><li><p>标签可分为围堵标签和自闭和标签。围堵标签就是有开始标签和结束标签，比如<code>&lt;html&gt;&lt;/html&gt;</code>。自闭和标签就是开始标签和结束标签是在一起的，比如 <code>&lt;br/&gt;</code></p></li><li><p>标签可以嵌套</p></li><li><p>在开始标签中可以定义属性。属性是由键值对构成的，值需要用引号引出，单引号或双引号都可以</p></li><li><p>标签不区分大小写</p><pre><code class="language-html">&lt;html&gt;    &lt;head&gt;        &lt;title&gt; title &lt;/title&gt;    &lt;/head&gt;    &lt;body&gt;        &lt;font color=&quot;red&quot;&gt;Hello world&lt;/font&gt;    &lt;/body&gt;&lt;/html&gt; </code></pre></li></ul><h3>标签</h3><h3>文件标签</h3><ul><li>html :  html文档的根标签</li><li>head：头标签。用于指定html文档的一些属性。引入外部资源</li><li>title：标题标签</li><li>body：体标签</li><li>&lt;！DOCTYPE html&gt; html5中定义该文档是html文档</li></ul><h3>文本标签</h3><pre><code>1.&lt;!-- 注释内容 --&gt;    2. &lt;h1&gt; to &lt;h6&gt;  &lt;/h1&gt;to&lt;/h6&gt;&lt;!--标题标签--&gt;   3.&lt;br/&gt;或&lt;br&gt;   &lt;!--换行--&gt;   4.&lt;hr&gt;&lt;!--一条水平线，属性color,width,size,align--&gt;   5.&lt;p&gt;&lt;/p&gt;&lt;!--段落标签--&gt;   6.&lt;b&gt;&lt;/b&gt;&lt;!--字体加粗--&gt;   7.&lt;i&gt;&lt;/i&gt;&lt;!--字体倾斜--&gt; 8.&lt;font&gt;&lt;/font&gt;&lt;!--字体标签,属性color,size,face--&gt;</code></pre><h3>图片标签</h3><pre><code class="language-html">&lt;img/&gt;&lt;!--src属性表示图片路径--&gt;&lt;!--       相对路径1. 以./开头，表示当前目录2. 以../开头，表示表示后退一级到父目录--&gt;</code></pre><h3>列表标签</h3><pre><code class="language-html">&lt;ol type=&#39;A&#39;&gt;&lt;!--有序列表,type可以定义排列的样式--&gt;&lt;li&gt;1&lt;/li&gt;&lt;li&gt;2&lt;/li&gt;&lt;/ol&gt;&lt;ul type=&#39;disc&#39;&gt;&lt;!--无序列表,type可以定义排列的样式--&gt;&lt;li&gt;1&lt;/li&gt;&lt;li&gt;2&lt;/li&gt;&lt;/ul&gt;</code></pre><h3>链接标签</h3><pre><code class="language-html">&lt;a href=&quot;http://www.baidu.com&quot; &gt;name&lt;/a&gt;&lt;!--超链接 a--&gt;&lt;!--href属性，指定访问资源的URL--&gt;&lt;!--target属性，打开资源的方式，默认值为_self，原网站打开._blank，从新页面打开链接.--&gt;</code></pre><h3>div和span</h3><pre><code class="language-html">&lt;div&gt;&lt;/div&gt;  &lt;!--每一个div占满一整行。块级标签--&gt;&lt;span&gt;&lt;/span&gt;&lt;!--文本信息在一行展示，行内标签，内联标签--&gt;</code></pre><h3>语义化标签</h3><pre><code class="language-html">&lt;header&gt;&lt;/header&gt;  &lt;footer&gt;&lt;/footer&gt;&lt;!-- h5中为了提高程序的可读性--&gt;</code></pre><h3>表格标签</h3><pre><code class="language-html">&lt;table&gt;&lt;/table&gt;&lt;!--定义表格，可以通过调整其属性来调整表格属性--&gt;&lt;tr&gt;&lt;/tr&gt;&lt;!--定义行--&gt;&lt;td&gt;&lt;/td&gt;&lt;!--定义单元格，colspan,rowspan属性关于单元格合并--&gt;&lt;th&gt;&lt;/th&gt;&lt;!--定义表头单元格--&gt;&lt;caption&gt;&lt;/caption&gt;&lt;!--表格标题--&gt;&lt;thead&gt;&lt;/thead&gt;&lt;!--表示表格头部份--&gt;&lt;tbody&gt;&lt;/tbody&gt;&lt;!--表示表格体部份--&gt;&lt;tfoot&gt;&lt;/tfoot&gt;&lt;!--表示表格尾的部份--&gt;</code></pre><h3>表单标签</h3><ul><li><p><strong>表单是用来采集用户输入的数据的，用于和服务器进行交互，使用到的标签有：form</strong></p><pre><code class="language-html">&lt;!-- form:可以定义一个范围，代表采集用户数据的范围属性：1.action        指定提交数据的URL    2.method      指定提交的方式，一共7种，get和post比较常用            get的请求参数会在地址栏中，而post不会，会封装在请求体中            get请求参数大小有限制，post没有限制            get不太安全，post较为安全    要想表单的数据被提交，一定要指定其name属性--&gt;&lt;form action=&quot;#&quot; method=&quot;post&quot;&gt;用户名：&lt;input name=&quot;username&quot;&gt;&lt;br&gt;密码：&lt;input name=&quot;password&quot;&gt;&lt;br&gt;    &lt;input type=&quot;submit&quot; value=&quot;login&quot;&gt;&lt;/form&gt;</code></pre></li></ul><h3><strong>表单项标签</strong></h3><ul><li><p>input：可以通过type属性值，改变元素展示的样式</p><pre><code class="language-html">&lt;form action=&quot;#&quot; method=&quot;post&quot;&gt;    &lt;!-- input的type属性的值text，password，radio，checkbox，file,hidden.--&gt;     &lt;!-- text为文本输入框，默认。password为密码输入框。radio单选框，checkbox复选框,file文件选择框，hidden隐藏域。--&gt;    用户名：&lt;input type=&quot;text&quot; name=&quot;username&quot;&gt;&lt;br&gt;    密码：&lt;input type=&quot;password&quot; name=&quot;password&quot;&gt;&lt;br&gt;&lt;!-- radio:要想让多个单选框的值实现单选，多个单选框的name值要一样，只要你选了，提交的属性值都是on，所以一般要个每一个单选框提供value属性，指定选中后提交的值。checked属性可以指定默认值--&gt;    性别：&lt;input type=&quot;radio&quot; name=&quot;gender&quot; value=&quot;male&quot;&gt;男         &lt;input type=&quot;radio&quot; name=&quot;gender&quot; value=&quot;female&quot;&gt;女    &lt;br&gt;    &lt;!--checkbox，类似radio的使用方法--&gt;    爱好：&lt;input type=&quot;checkbox&quot; name=&quot;hobby&quot; value=&quot;game&quot;&gt;游戏         &lt;input type=&quot;checkbox&quot; name=&quot;hobby&quot; value=&quot;java&quot;&gt;&lt;br&gt;java    &lt;br&gt;    &lt;!--     --&gt;    图片：&lt;input type=&quot;file&quot; name=&quot;file&quot;&gt;&lt;br&gt;    隐藏域：&lt;input type=&quot;hidden&quot; name=&quot;hidden&quot;&gt;&lt;br&gt;       &lt;label&gt;用户名：&lt;/label&gt; &lt;input type=&quot;text&quot; name=&quot;username&quot;&gt;&lt;br&gt;        &lt;!--input type属性还可以定义按钮，比如sumit提按钮，button普通按钮，              image图片提交按钮--&gt;    &lt;input type=&quot;sumit&quot; value=&quot;login&quot;&gt;&lt;br&gt;    &lt;input type=&quot;button&quot; value=&quot;login&quot;&gt;&lt;br&gt;    &lt;input type=&quot;image&quot; src=&quot; &quot;&gt;&lt;br&gt;         &lt;!--input type一些有用的属性--&gt;    取色器：&lt;input type=&quot;color&quot; name=&quot;color&quot;&gt;&lt;br&gt;    生日：&lt;input type=&quot;date&quot; name=&quot;birthday&quot;&gt;&lt;br&gt;    生日：&lt;input type=&quot;datetime-local&quot; name=&quot;birthday&quot;&gt;&lt;br&gt;    邮箱：&lt;input type=&quot;email&quot; name=&quot;email&quot;&gt;&lt;br&gt;    年龄：&lt;input type=&quot;number&quot; name=&quot;number&quot;&gt;&lt;br&gt;&lt;/form&gt;</code></pre></li><li><p>select：下拉列表</p><pre><code class="language-html">省份：&lt;select name=&quot;province&quot;&gt;     &lt;!--option是用来定义列表项的--&gt;     &lt;option value=&quot;&quot;&gt;--请选择--&lt;/option&gt;     &lt;option value=&quot;1&quot;&gt;广西&lt;/option&gt;     &lt;option value=&quot;2&quot;&gt;上海&lt;/option&gt;     &lt;option value=&quot;3&quot;&gt;北京&lt;/option&gt;</code></pre></li><li><p>testarea：文本域</p><pre><code class="language-html">文本域：&lt;testarea cols=&quot;20&quot; rows=&quot;5&quot; name=&quot;me&quot;&gt;&lt;/testarea&gt;&lt;!--    cols指定每一行有多少个字符    rows默认行数，多了会自动扩容--&gt;</code></pre></li><li><p>label标签：指定输入项的文字描述信息</p></li></ul><h2>CSS</h2><blockquote><p>Cascading    Style    Sheets层叠样式表</p><p>层叠表示多个样式可以作用在同一个html元素上，使其同时生效</p></blockquote><h3>CSS的基本使用</h3><h4>内联样式</h4><pre><code class="language-html">&lt;!--   内联样式 在标签内使用style属性指定css代码--&gt;&lt;div style=&quot;color: brown&quot;&gt;    hello world&lt;/div&gt;</code></pre><h4>内部样式</h4><pre><code class="language-html">&lt;!DOCTYPE html&gt;&lt;html lang=&quot;en&quot;&gt;&lt;head&gt;    &lt;meta charset=&quot;UTF-8&quot;&gt;    &lt;title&gt;内部样式&lt;/title&gt;&lt;!--内部样式在head标签里面定义style标签，style标签内写css的代码--&gt;    &lt;style&gt;        div{            color: aqua;        }    &lt;/style&gt;&lt;/head&gt;&lt;body&gt;&lt;div&gt;hello world&lt;/div&gt;&lt;/body&gt;&lt;/html&gt;</code></pre><h4>外部样式</h4><pre><code class="language-html">&lt;!DOCTYPE html&gt;&lt;html lang=&quot;en&quot;&gt;&lt;head&gt;    &lt;meta charset=&quot;UTF-8&quot;&gt;    &lt;title&gt;外部样式&lt;/title&gt;&lt;!--外部样式先定义一个cs资源文件(后缀名为css)，里面写css代码然后在head标签内定义link标签，引入这个资源文件比如这个l.css内容为： div{            color: aqua;        }不用link标签的话还可以写为            &lt;style&gt;            @import&quot;l.css&quot;            &lt;/style&gt;--&gt;    &lt;link rel=&quot;stylesheet&quot; href=&quot;l.css&quot;&gt;&lt;/head&gt;&lt;body&gt;    &lt;div&gt;hello world&lt;/div&gt;    &lt;/body&gt;&lt;/html&gt;</code></pre><h3>CSS语法</h3><h4>选择器</h4><ul><li><p>基础选择器</p><pre><code class="language-html">&lt;!DOCTYPE html&gt;&lt;html lang=&quot;en&quot;&gt;&lt;head&gt;    &lt;meta charset=&quot;UTF-8&quot;&gt;    &lt;title&gt;基础选择器&lt;/title&gt;    &lt;!--       基础选择器1.id选择器，#id属性值{}2.元素选择器，就是之前用的div{}3.类选择器：.class{}        id选择器优先级高于元素选择器，类选择器优先级比元素选择器高--&gt;    &lt;style&gt;         #div1{                color: aqua;        }         div{                color:red;        }         .cls1{                color: darkblue;        }    &lt;/style&gt;&lt;/head&gt;&lt;body&gt;    &lt;div id=&quot;div1&quot;&gt;hello world  &lt;/div&gt;    &lt;div &gt; hello world &lt;/div&gt;    &lt;div class=&quot;cls1&quot;&gt;hello world&lt;/div&gt;&lt;/body&gt;&lt;/html&gt;</code></pre></li><li><p>扩展选择器</p><pre><code class="language-html">&lt;!--1. 选择所有的元素                           *{}2. 并集选择器                            选择器1,选择器2{ }3.子选择器，筛选1的元素下的选择器2              选择器1 选择器2{ }4.父选择器，筛选了2的父元素的选择器1         选择器1&gt;选择器2{ }5.属性选择器，选择元素名称，属性名=属性值元素    元素名称[属性名=&quot;属性值&quot;]{ }6.伪类选择器，选择一些元素具有的状态        元素:状态{ }    比如标签&lt;a&gt;,就有四种状态：初始化link,被访问过visited,                    正在访问active，鼠标悬浮状态hover--&gt;</code></pre></li></ul><h4>属性</h4><ul><li><p>文本，文字</p><pre><code class="language-html">&lt;!DOCTYPE html&gt;&lt;html lang=&quot;en&quot;&gt;&lt;head&gt;    &lt;meta charset=&quot;UTF-8&quot;&gt;    &lt;title&gt;属性&lt;/title&gt;&lt;!--    1.颜色color，2.字体大小font-size，        3.text-align对齐方式，4.line-height行高--&gt;    &lt;style&gt;        p{            color: brown;                            font-size: 30px;            text-align: center;            line-height: 100px;        }    &lt;/style&gt;&lt;/head&gt;&lt;body&gt;&lt;p&gt;    hello world     &lt;/p&gt;    &lt;div&gt;&lt;/div&gt;&lt;/body&gt;&lt;/html&gt;</code></pre></li><li><p>背景background</p></li><li><p>边框</p><pre><code class="language-html">  &lt;style&gt;        p{            border:1px solid red;    /*    border:像素 实现 颜色，                                    这个是复合标签控制了边框的四条边*/        }    &lt;/style&gt;</code></pre></li><li><p>尺寸</p><pre><code class="language-html"> &lt;style&gt;        div{            border: 1px solid red;            height:200px;        /*height高度，width宽度*/            width:200px;        }    &lt;/style&gt;</code></pre></li><li><p>控制布局</p><pre><code class="language-html">&lt;!--            1.margin    外边距        2.padding    内边距                内边距一般会影响整个边框的大小，可以设置一个属性                    box-sizing:border-box来决定盒子的大小不会改变        3.flot        浮动--&gt;</code></pre></li></ul><h2>JavaScript</h2><blockquote><p>JavaScript是一门客户端脚本语言</p><p>JavaScript是在客户端浏览器中运行的，不需要编译，直接就可以被浏览器解析</p><p>JavaScript = ECMAScript + JavaScript特有特性(BOM+DOM)</p></blockquote><h3>ECMAScript</h3><h4>与html结合方式</h4><pre><code class="language-html">&lt;!DOCTYPE html&gt;&lt;html lang=&quot;en&quot;&gt;    &lt;!--script标签可以放在各种位置，只是执行的顺序不同--&gt;&lt;head&gt;    &lt;meta charset=&quot;UTF-8&quot;&gt;    &lt;title&gt;JavaScript&lt;/title&gt;&lt;!--第一种结合方法：内部--&gt;    &lt;script&gt;        alert(&quot;helloworold&quot;);    &lt;/script&gt;&lt;!--第二种结合方法外部--&gt;    &lt;script src=&quot;1.js&quot;&gt;&lt;/script&gt;&lt;/head&gt;&lt;body&gt;&lt;/body&gt;</code></pre><h4>注释</h4><ul><li>单行    //注释内容</li><li>多行    /*注释内容*/</li></ul><h4>数据类型</h4><ul><li>原始数据类型<ul><li>number            数字。        整数/小数/NaN</li><li>string                字符串        &quot;abc&quot;  &quot;a&quot;  &#39;a&#39;</li><li>boolean            true和false</li><li>null                   一个对象为空的占位符</li><li>undefined         未定义。即没有给一个变量给初始化值，默认为undefined</li></ul></li><li>引用数据类型</li></ul><h4>变量</h4><ul><li>Java是强类型的，在定义变量的时候会定义了该变量的类型，开辟的空间只能放该类型的数据</li><li>JavaScript是弱类型的，不定义变量的类型，开辟好的空间可以放任意类型的值</li><li>定义变量                 var   变量名 = 初始值</li><li>可以使用typeof运算符来得出该变量的类型</li></ul><h4>运算符</h4><ul><li>+正号</li></ul><p>在JS中，如果运算数不是所要求的类型，js就会进行自动转换，如</p><p>string转number，会按字面值转，如果是英文则会转成NaN（一个不是数字的数字）。</p><p>boolean转number，true转为1，false转为0</p><ul><li><p>比较运算符(&gt;       &lt;   ==)</p><ul><li>类型相同，直接比较，比如字符串比较，按字典顺序逐一比较</li><li>类型不同，先转换在比较</li></ul></li><li><p>===全等于</p></li></ul><p>在比较前线判断类型，类型不一样直接返回false，比如&quot;123&quot;==123返回true，而&quot;123&quot;===123返回false</p><ul><li>逻辑运算符(&amp;&amp;  ||  !   )<ul><li>number转boolean        0为假，非0为真</li><li>string转boolean            除了空字符串，其他都是true</li><li>null和undefined            都是false</li><li>对象                                 所有对象都是true</li></ul></li></ul><h4>对象</h4><h5>Function对象</h5><ul><li><p><strong>这是一个函数对象</strong>，就像是Java中方法和对象的结合体，有点特殊</p></li><li><p>创建方式</p><pre><code class="language-javascript">&lt;!DOCTYPE html&gt;&lt;html lang=&quot;en&quot;&gt;&lt;head&gt;    &lt;meta charset=&quot;UTF-8&quot;&gt;    &lt;title&gt;Function对象&lt;/title&gt;&lt;/head&gt;&lt;body&gt;&lt;script&gt;    // 1.        var fun = new Function(形参，方法体);        var fun1 = new Function(&quot;a&quot;,&quot;b&quot;,&quot;alert(a+b)&quot;);    fun1(1, 2);    //2.        function fun(形参){    方法体      }        function fun2(a,b){            alert(a + b);        }    fun2(1, 3);    //3.        var fun = function(形参){  方法体  }        var fun3 = function (a,b) {            alert(a + b);        }    fun3(1, 4);&lt;/script&gt;&lt;/body&gt;&lt;/html&gt;</code></pre></li><li><p>length属性表示形参的个数，有点像使用对象的属性，直接函数名.length就可以返回该函数的形参个数</p></li><li><p><strong>注意的特殊地方</strong></p><ol><li><p>形式参数不用定义，即不用var来定义形参</p></li><li><p>这里的函数其实就是对象，可以重新为该函数来赋值，即可以再次定义,这里就感觉像该对象被重新赋值，跟Java中的方法还是有很大区别</p><pre><code class="language-JavaScript">// 上面定义了fun3，现在也可以重新定义    fun3 = function(形参){    方法体        }</code></pre></li><li><p>关于方法的调用。在Java中方法调用传入的数一定要和形参相同，而在JS中，方法的调用只与方法名称有关，传入的参数不和形参个数一样也能调用，即使不传参数也能调用。</p></li><li><p>在方法声明中会有一个隐藏的内置对象，即一个数组(arguments)，封装所有的实际参数，所以实际参数比形参个数要多时，它也会接收到多出来的实参</p><pre><code class="language-JavaScript">&lt;!DOCTYPE html&gt;&lt;html lang=&quot;en&quot;&gt;&lt;head&gt;    &lt;meta charset=&quot;UTF-8&quot;&gt;    &lt;title&gt;Function对象&lt;/title&gt;&lt;/head&gt;&lt;body&gt;&lt;script&gt;     function fun() {        var sum = 0 ;        for(var i = 1;i&lt;arguments.length;i++){            sum = sum + arguments[i];        }        return sum;    }    var sum = fun(1, 2, 3);    alert(sum);    sum = fun(1, 2, 3,4,5);    alert(sum);&lt;/script&gt;&lt;/body&gt;&lt;/html&gt;</code></pre></li></ol></li></ul><h5>Array</h5><ul><li><p>创建方式</p><pre><code class="language-JavaScript">&lt;!DOCTYPE html&gt;&lt;html lang=&quot;en&quot;&gt;&lt;head&gt;    &lt;meta charset=&quot;UTF-8&quot;&gt;    &lt;title&gt;Array对象&lt;/title&gt;&lt;/head&gt;&lt;body&gt;&lt;script&gt;    //1.      var array = new Array(元素)        var array1 = new Array(1,2,3,4,5);    document.write(array1,&quot;&lt;br&gt;&quot;);    //2.      var array = new Array(size)        var array2 = new Array(5);    document.write(array2,&quot;&lt;br&gt;&quot;);    //3.      var array = [元素]        var array3 = [1,2,3,4,5];    document.write(array3,&quot;&lt;br&gt;&quot;);&lt;/script&gt;&lt;/body&gt;&lt;/html&gt;</code></pre></li><li><p>length属性返回数组的长度</p></li><li><p>常用方法</p><ol><li><p>join(分隔符)将数组中的元素按照指定的分隔符拼接为字符串</p></li><li><p>push(元素)</p><p>为数组添加元素</p></li></ol></li><li><p><strong>注意的特殊地方</strong></p><ol><li>数组里的数据可以是不同类型。在Java中数组定义时就规定了类型，而在JS中，数组时可以放进去不同类型的参数的</li><li>JS中，数组的长度时可以变化的，即你访问了越界，即返回undefined值</li></ol></li></ul><h5>Date</h5><ul><li><p>创建方式</p><pre><code class="language-javascript">    var date = new Date()</code></pre></li><li><p>一些方法</p><ol><li>toLcaleString()            返回当前date对象对应的时间本地字符串格式</li><li>getTime()                     返回当前对象的时间到1970年1月1日零点的毫秒差</li></ol></li></ul><h5>Math</h5><ul><li>Math不用创建，直接使用方法就可以了，与Java类似</li><li>方法<ol><li>random()                    返回0~1的随机数，含0不含1</li><li>round(x)                      返回四舍五入为最接近的整数</li><li>ceil(x)                           返回对数进行向上取整</li><li>floor(x)                        返回对数进行向下取整</li></ol></li></ul><blockquote><p>虽然大致学过一遍，久了又忘记了</p></blockquote>]]></content:encoded>
            <category domain="https://yww52.com/categories/%25E6%2588%2591%25E7%259A%2584%25E6%258A%2580%25E6%259C%25AF%25E5%25AD%25A6%25E4%25B9%25A0%25E7%2589%25A9%25E8%25AF%25AD%25E6%259E%259C%25E7%2584%25B6%25E6%259C%2589%25E9%2597%25AE%25E9%25A2%2598/">我的技术学习物语果然有问题</category>
        </item>
        <item>
            <title>反射</title>
            <link>https://yww52.com/posts/7af799fd/</link>
            <guid isPermaLink="true">https://yww52.com/posts/7af799fd/</guid>
            <pubDate>Wed, 13 Nov 2019 00:00:00 +0000</pubDate>
            <description>反射是十分重要的，是Java框架的灵魂</description>
            <content:encoded><![CDATA[<img src="https://img.yww52.com/2019/11/2019-11-13top_img.jpg" alt="反射" style="max-width:100%;height:auto;margin-bottom:15px;" /><h2>反射</h2><ul><li><p>反射指的是可以在运行时加载，使用编译期间完全未知的类</p></li><li><p>程序在运行中。可以动态加载一个只有名称的类。对于任意一个已加载的类，都能够知道这个类的所有属性和方法；对于任意一个对象，都能够调用它的任意一个方法和属性</p></li><li><p>一个类只有一个Class对象，在加载一次出来的Class对象也是之前的那一个</p></li><li><p>加载完类之后，在堆内存里就会产生一个Class对象，类的整个结构信息会放到对应的Class对象中</p></li></ul><h2>获取Class对象的三种方式：</h2><pre><code class="language-java">public class ReflectionTest {    public static void main(String[] args)  {//      1.用Class.forName()获取Class对象，推荐使用这个        try {            Class clazz1 = Class.forName(&quot;gui.beantest&quot;);//Class.forName(包名+类名)            System.out.println(clazz1);        }         catch (ClassNotFoundException e) {            e.printStackTrace();        }//      2.用 .class 来获取Class对象        Class clazz2 = beantest.class;//类名.class        System.out.println(clazz2);//      3.用 getClass()来获取Class对象        beantest cc = new beantest(1,2,&quot;name&quot;);        Class clazz3 =  cc.getClass();//对象.getClass()        System.out.println(clazz3);    }}</code></pre><h2>获取类的信息</h2><pre><code class="language-java">@SuppressWarnings(&quot;all&quot;)public class ReflectionTest {    public static void main(String[] args)  throws Exception {        String path = &quot;gui.beantest&quot;;            Class clazz = Class.forName(path);            System.out.println(clazz);//            获取类的名字        System.out.println(clazz.getName());            //包名+类名        System.out.println(clazz.getSimpleName());      //类名//            获取属性信息        Field[] files1 = clazz.getFields();         //只能获得public类型的属性        Field[] files2 = clazz.getDeclaredFields();//能获得所有类型的属性        Field f =clazz.getDeclaredField(&quot;uname&quot;);//获得这个属性的            System.out.println(files1.length);            System.out.println(files2.length);            System.out.println(f);//            获取方法信息        Method[] method1 = clazz.getMethods();   //只能获得public类型的方法        Method[] method2 = clazz.getDeclaredMethods();   //获得所有类型的方法        Method m = clazz.getMethod(&quot;setId&quot;,int.class);    //获取这个方法，没参数就写null，有参数就传参数类型的class            System.out.println(method1.length);            System.out.println(method2.length);            System.out.println(m);//          获取构造器信息        Constructor[] constructors = clazz.getDeclaredConstructors();        Constructor c =clazz.getDeclaredConstructor(int.class,int.class,String.class);    //传递不同的参数类型获取不同的构造器        for(Constructor temp:constructors){            System.out.println(temp);        }        System.out.println(c);    }}</code></pre><h2>动态操作</h2><pre><code class="language-java">public class Demo01 {    public static void main(String[] args) throws Exception{        Class clazz = Class.forName(&quot;test.Bean&quot;);        //        通过反射调用构造方法，构造对象        Bean u1 = (Bean) clazz.getDeclaredConstructor().newInstance();        System.out.println(u1);        Bean u2 = (Bean) clazz.getDeclaredConstructor(int.class,int.class,String.class).newInstance(1,2,&quot;ee&quot;);        System.out.println(u2.getUname());        //        通过反射调用普通方法        Bean u3 =(Bean) clazz.getDeclaredConstructor().newInstance();        Method me = clazz.getMethod(&quot;setId&quot;, int.class);        me.invoke(u3,5);//通过Method的invoke方法来使用方法        System.out.println(u3.getId());        //        通过反射操作属性        Bean u4 =(Bean) clazz.getDeclaredConstructor().newInstance();        Field f = clazz.getDeclaredField(&quot;uname&quot;);        f.setAccessible(true);//这个属性不需要安全检查，可以直接访问，不然私有的属性不能修改        f.set(u4,&quot;lll&quot;);        System.out.println(u4.getUname());        System.out.println(f.get(u4));            }}</code></pre><blockquote><p>getDeclaredConstructor()这个方法会查找到所有的构造方法，然后通过后面的newInstance()方法来完成实例化，在强转就可以得到类了。如果有参数就输入参数。（注意这个Javabean中最好要有一个无参构造器，不然无参的这个实例化会报错）</p></blockquote><h2>反射机制的性能问题</h2><ul><li><p>setAccessible(),启用和禁用安全检查的开关，true时取消Java访问检查，但并不是true就能访问，false就不能访问</p></li><li><p>禁止安全检查可以提高反射的运行速度</p><pre><code class="language-java">public class Demo02 {    public static void main(String[] args) throws Exception{        test01();        test02();        test03();    }    public static void test01(){        Bean u = new Bean();        long start = System.currentTimeMillis();        for(int i = 0;i&lt;1000000000L;i++){            u.getUname();        }        long end = System.currentTimeMillis();        System.out.println(&quot;调用普通方法耗时为：&quot;+(end-start)+&quot;ms&quot;);    }    public static void test02() throws Exception{        Bean u = new Bean();        Class clazz = u.getClass();        Method me = clazz.getDeclaredMethod(&quot;getUname&quot;,null);        long start = System.currentTimeMillis();        for(int i = 0;i&lt;1000000000L;i++){            me.invoke(u, null);        }        long end = System.currentTimeMillis();        System.out.println(&quot;运用反射调用方法耗时为：&quot;+(end-start)+&quot;ms&quot;);    }    public static void test03() throws Exception{        Bean u = new Bean();        Class clazz = u.getClass();        Method me = clazz.getDeclaredMethod(&quot;getUname&quot;,null);        me.setAccessible(true);        long start = System.currentTimeMillis();        for(int i = 0;i&lt;1000000000L;i++){            me.invoke(u, null);        }        long end = System.currentTimeMillis();        System.out.println(&quot;关闭访问检查后运用反射调用方法耗时为：&quot;+(end-start)+&quot;ms&quot;);    }}</code></pre><blockquote><p> 这里就可以知道，运用反射调用方法的时间差不多是直接调用方法的时间的10倍，而关闭了访问检查之后就下降到接近5倍的程度</p></blockquote></li></ul>]]></content:encoded>
            <category domain="https://yww52.com/categories/%25E6%2588%2591%25E7%259A%2584%25E6%258A%2580%25E6%259C%25AF%25E5%25AD%25A6%25E4%25B9%25A0%25E7%2589%25A9%25E8%25AF%25AD%25E6%259E%259C%25E7%2584%25B6%25E6%259C%2589%25E9%2597%25AE%25E9%25A2%2598/">我的技术学习物语果然有问题</category>
        </item>
        <item>
            <title>注解</title>
            <link>https://yww52.com/posts/a70f4376/</link>
            <guid isPermaLink="true">https://yww52.com/posts/a70f4376/</guid>
            <pubDate>Tue, 12 Nov 2019 00:00:00 +0000</pubDate>
            <description>注解对Java开发还是很重要的，特别是框架里注解可以简化操作</description>
            <content:encoded><![CDATA[<img src="https://img.yww52.com/2019/11/2019-11-12top_img.jpg" alt="注解" style="max-width:100%;height:auto;margin-bottom:15px;" /><h3>作用</h3><ul><li>可以对程序作出解释，但又不是程序的部分，这一点跟注释差不多</li><li>可以被程序读取，注解信息处理流程是注解和注释的区别</li></ul><h3>一些常见的注解</h3><ul><li>@Override定义在java.lang.Override里，该注解只修饰方法，表示这个方法是重写了超类的方法</li><li>@Deprecated定义在java.lang.Deprecated里，可修饰方法，属性，类，表示不建议使用这个元素，使用时会出现划线</li><li>@SuppressWarnings()定义在java.lang.SuppressWarnings里，用来抑制编译时的警告信息，不过要往里加一个参数才能使用，比如&quot;all&quot;等</li></ul><h3>自定义的注解</h3><ul><li><p>元注解是为注解做的注解，如@Target,@Retention,@Documented,@Inherited，位于java,lang,annotation</p><ul><li><p>@Target，用于描述注解的使用范围 </p><pre><code class="language-java">  @Target(value = Element.TYPE)</code></pre></li><li><p>@Retention,表示需要什么级别保存该注释，描述注解的生命周期</p><pre><code class="language-java">  @Retention(RetentionPolicy.RUNTIME)                // 在运行时有效，可以被反射机制读取  @Retention(RetentionPolicy.SOURCE)                //在class文件中有效  @Retention(RetentionPolicy.RUNTIME)                // 在源文件中有效</code></pre></li></ul></li><li><p>使用@interface自定义注解，自动继承了java.lang.annotation接口，其中每一个方法就是声明了一个配置参数，如果只有一个参数一般命名为”value“</p><pre><code class="language-java">public @interface Testannotation{    String name()     default &quot;&quot;;        //这里表示参数默认值为”“，如果使用该注解不传参数也没默认值就会报错}</code></pre></li></ul>]]></content:encoded>
            <category domain="https://yww52.com/categories/%25E6%2588%2591%25E7%259A%2584%25E6%258A%2580%25E6%259C%25AF%25E5%25AD%25A6%25E4%25B9%25A0%25E7%2589%25A9%25E8%25AF%25AD%25E6%259E%259C%25E7%2584%25B6%25E6%259C%2589%25E9%2597%25AE%25E9%25A2%2598/">我的技术学习物语果然有问题</category>
        </item>
        <item>
            <title>JDBC</title>
            <link>https://yww52.com/posts/8fe47ff4/</link>
            <guid isPermaLink="true">https://yww52.com/posts/8fe47ff4/</guid>
            <pubDate>Wed, 06 Nov 2019 00:00:00 +0000</pubDate>
            <description>JDBC是用来规范客户端程序如何来访问数据库的应用程序接口。</description>
            <content:encoded><![CDATA[<img src="https://img.yww52.com/2019/11/2019-11-06top_img.jpg" alt="JDBC" style="max-width:100%;height:auto;margin-bottom:15px;" /><h2>MySQL的安装</h2><p>   去Mysql官网下载即可</p><h2>数据库管理工具</h2><p>   因为MySQL命令行操作在用起来十分繁琐，所以需要选择一个数据库的管理工具，方便使用MySQL，推荐使用  navicat</p><h2>装载驱动</h2><ol><li><p>为什么要装驱动        在编程中连接数据库时，必须先装载特定厂商的数据库驱动程序，不同的数据库有不同的装载方法。驱动就是各个数据库厂商实现的Sun公司提出的jdbc接口。即对connection等接口的实现类的jar文件。</p></li><li><p>下载驱动</p><p>首先要先去官网下载好对应版本的jar包，我的就是<strong>mysql-connector-java-8.0.18</strong>，版本要对应，不知道版本的可以使用命令行进去MySQL然后输入命令\s来查看版本</p></li></ol><h2>连接数据库</h2><ol><li><p>旧版本的连接(8.0版本之前)  </p><pre><code class="language-java"> //装载MySQL驱动   Class.forName(&quot;com.mysql.jdbc.Driver&quot;); //连接MySQL数据库   connection con =            DriverMannager(&quot;jdbc:mysql://host:localhost/database&quot;,&quot;user&quot;,&quot;password&quot;);</code></pre></li><li><p><strong>MySQL8.0之后的版本连接数据库时不需要建立SSL连接，需要显示关闭，最后还要设置CST才能连接</strong></p><pre><code class="language-java">//                    装载驱动    Class.forName(&quot;com.mysql.cj.jdbc.Driver&quot;);//                    连接MySQL数据库    connection con =DriverMannager(&quot;jdbc:mysql://localhost:port/database?useSSL=false&amp;serverTimezone=UTC&quot;,&quot;user&quot;,&quot;password&quot;);</code></pre></li></ol><p>:::note{type=&quot;info&quot;}</p><p>localhost是你数据库的主机名,port数据库的端口,database时库名user和password时用户名和密码，因人而异连接对象内部其实包含了Socket对象，是一个远程连接，比较耗时</p><p>:::</p><h2>数据库的接口使用</h2><h3>Statement接口</h3><ol><li><p>由createStatement创建，用于发送简单的不带参数的sql语句</p><pre><code class="language-java">Statement stm = con.createStatement();String name = &quot;学生1&quot;;String sql = &quot;&quot;;                //这里就加入你想使用的sql语句stm.execute(sql);</code></pre><p>  一般也不会使用Statement接口，因为会出现sql注入的危险，比如：</p><pre><code class="language-java">Stament stm = con.createStatement();String id = &quot;5 or 1=1&quot;;                    //如果外界恶意添加or 1=1或其他一些语句，就会使数据库不安全，这里就会删除很多数据String sql = &quot;delete frome test where id =&quot;+id;                stm.execute(sql);</code></pre></li></ol><h3>Statement常用的三个方法</h3><ol><li><p><strong>execute()</strong>            运行语句，返回是否有结果集，Boolean类型</p></li><li><p><strong>executeQuery()</strong>       运行select语句，返回ResultSet结果集</p><pre><code class="language-java">String sql = &quot;select * from jdbctest  where id&gt;?&quot;; PreparedStatement ps = conn.prepareStatement(sql); ps.setObject(1,2);        //把id大于2的记录取出来 ResultSet rs = ps.executeQuery(); while(rs.next()){        //next()返回是Boolean值，游标会不断移动来判断数据是否存在      System.out.println(rs.getInt(1)+&quot;-----&quot;+rs.getString(2)+&quot;-----&quot;+rs.getString(3));</code></pre></li><li><p><strong>executeUpdate()</strong>      运行insert/update/delete操作，返回更新的行数</p></li></ol><h3>PreparedStatement类</h3><p><strong>继承Statement接口</strong>，由prepareStatement创建，用于发送含有一个或多个输入参数的sql语句，PrepareStatement对象比Statement对象效率更高，而且可以防止SQL注入，推荐使用</p><pre><code class="language-java">  String sql = &quot;insert into jdbctest (username,pwd) values(?,?)&quot;;//?为占位符         PreparedStatement ps = con.prepareStatement(sql);//传入占位符里的参数，参数索引要从1开始，参数处理都可以用setObject()来代替         ps.setString(1,&quot;lili&quot;);         ps.setString(2,&quot;1141&quot;);         ps.execute();//直接用setObject()也可以ps.setObject(1,&quot;li&quot;);</code></pre><h2>关闭</h2><pre><code class="language-java">  if(rs!=null){                rs.close();            }  if(ps!=null){                ps.close();            }  if(conn!=null) {                con.close();            }</code></pre><p> <strong>后打开的先关闭，比如resultset--&gt;statment---&gt;connection这样的顺序关闭，不要合在一起关闭哦</strong></p><h2>批量处理</h2><ul><li><p>Batch</p></li><li><p>对于大量数据的处理，建议使用Statement，因为PreparedStatement的预编译空间有限，数据多时会出现异常</p></li><li><p>还有就是设置为手动提交事务</p><pre><code class="language-java">package jabc_Test;import java.sql.*;public class demo01 {    public static void main(String[] args) throws Exception {//          加载驱动类         Class.forName(&quot;com.mysql.cj.jdbc.Driver&quot;);//         连接MySQL数据库         Connection con=DriverManager.getConnection(&quot;jdbc:mysql://localhost:3306/test?useSSL=false&amp;serverTimezone=UTC&quot;,                 &quot;root&quot;,&quot;password&quot;);         Statement smt = con.createStatement();    //使用statement                 con.setAutoCommit(false);        //原本是自动提交事务的，现在设为手动         for(int i =0; i&lt;2000 ; i++ ){             smt.addBatch(&quot;insert into jdbctest (username,pwd) values (&#39;li&quot;+i+&quot;&#39;,&#39;11111&#39;)&quot;);         }        smt.executeBatch();         con.commit();      //提交事务        if(smt!=null){            smt.close();        }        if(con!=null) {            con.close();        }    }}</code></pre></li></ul><h2>事务</h2><h3>基本概念</h3><ul><li>一组要么同时执行成功，要么同时执行失败的SQL语句，事务是数据库操作的一个执行单元</li><li>事务开始于：<ul><li>连接到数据库上，并执行一条DML语句(insert,update或delete)</li><li>前一个事务结束后，又输入另外一条DML语句</li></ul></li><li>事务结束于：<ul><li>执行了commit或rollback语句</li><li>执行一条DDL语句，比如create table语句；这种情况下为自动执行了commit语句</li><li>执行了一条DCL语句，例如grant语句；这种情况下为自动执行commit语句</li><li>断开于数据库的连接</li><li>执行了一条DML语句，但语句失败了，这种情况中会为这个无效的DML语句执行rollback语句</li></ul></li></ul><h3>事务的四大特点(ACID)</h3><ul><li>atomicity(原子性)  表示一个事务内的所有操作时一个整体，要么全部成功，要么全部失败</li><li>consistency(一致性)  表示一个事务内一个操作失败时，所有更改的数据都会回到事务执行之前</li><li>isolation(隔离性)  事务查看数据时数据所处的状态，要么是另一事务修改它之前的状态，要么是另一事务修改它之后的状态，不会查看中间状态的数据，具体看下面的事务隔离级别</li><li>durability(持久性)  持久性事务完成之后，修改时永久的</li></ul><h3>事务隔离级别(从低到高)</h3><ul><li>读取未提交</li><li>读取已提交</li><li>可重复读</li><li>序列化</li></ul><h2>CLOB与BLOB</h2><h3>CLOB</h3><ul><li>用于存储大量的文本数据</li><li>大字段的操作常常是以流的方式来处理的</li><li>MySQL相关的类型<ul><li>tinytext                            最大长度为255字符的TEXT列(2^8-1)</li><li>text                                    最大长度为65535字符的TEXT序列(2^16-1)</li><li>mediumtext                    最大长度为16777215字符的TEXT序列(2^24-1)</li><li>longtext                            最大长度为4294967295字符的TEXT序列(2^32-1)</li></ul></li></ul><pre><code class="language-java">package jabc_Test;import java.io.*;import java.sql.*;public class demo01 {    public static void main(String[] args) throws Exception {        Class.forName(&quot;com.mysql.cj.jdbc.Driver&quot;);        Connection con = DriverManager.getConnection                (&quot;jdbc:mysql://localhost:3306/test?useSSL=false&amp;serverTimezone=UTC&quot;, &quot;root&quot;, &quot;password&quot;);        PreparedStatement ps = con.prepareStatement(&quot;insert into jdbctest(username,info1,info2) values (?,?,?)&quot;);    //info1,info2类型都是longtext        ps.setString(1,&quot;li&quot;);//        1.将文本文件内容输入到数据库中        ps.setClob(2,new FileReader(new File(&quot;li.txt&quot;)));        //        2.将字符串输入到数据库中        ps.setClob(3, new BufferedReader(new InputStreamReader(new ByteArrayInputStream(&quot;hello&quot;.getBytes()))));                ps.executeUpdate();// 后面记得close    }}</code></pre><h3>BLOB</h3><ul><li>用于存储大量的二进制数据</li><li>与CLOB类似，常常也是以流的方式处理</li><li>MySQL中相关类型<ul><li>tinyblob                            最大长度为255字符的BLOB列(2^8-1)</li><li>blob                                    最大长度为65535字符的BLOB序列(2^16-1)</li><li>mediumblob                    最大长度为16777215字符的BLOB序列(2^24-1)</li><li>longblob                            最大长度为4294967295字符的BLOB序列(2^32-1)</li></ul></li></ul><pre><code class="language-java">package jabc_Test;import java.io.*;import java.sql.*;public class demo01 {    public static void main(String[] args) throws Exception {        Class.forName(&quot;com.mysql.cj.jdbc.Driver&quot;);        Connection con = DriverManager.getConnection                (&quot;jdbc:mysql://localhost:3306/test?useSSL=false&amp;serverTimezone=UTC&quot;, &quot;root&quot;, &quot;password&quot;);        PreparedStatement ps = con.prepareStatement(&quot;insert into jdbctest(username,heading) values (?,?)&quot;);        ps.setString(1,&quot;li&quot;);       ps.setBlob(2,new FileInputStream(&quot;background.png&quot;));        ps.execute();    }}</code></pre><h2>简单的dao层设置</h2><pre><code class="language-Java">public class Basedao {    static{        //加载驱动        try {            Class.forName(&quot;com.mysql.cj.jdbc.Driver&quot;);        } catch (ClassNotFoundException e) {            e.printStackTrace();        }    }    //连接数据库    public static Connection getconn(){        //建立连接对线        Connection conn = null;        try {            conn = DriverManager.getConnection(&quot;jdbc:mysql://localhost:3306/shop?useSSL=false&amp;serverTimezone=UTC&quot;,&quot;root&quot;,&quot;password&quot;);        } catch (SQLException e) {            e.printStackTrace();        }        return conn;    }    //执行sql语句操作    public static int exectuIUD(String sql,Object[] params){            int count = 0;            Connection conn = Basedao.getconn();        //准备SQL        PreparedStatement ps = null;        try {            ps = conn.prepareStatement(sql);            for(int i =0;i&lt;params.length;i++){                ps.setObject(i+1,params[i]);            }           count =  ps.executeUpdate();        } catch (SQLException e) {            e.printStackTrace();        }finally {            Basedao.closeall(null,ps,conn);        }        return count;    }        //关闭所有连接    public static void closeall(ResultSet rs,PreparedStatement ps,Connection conn){            try {                if(rs!=null)                    rs.close();                if(ps!=null)                    ps.close();                if(conn!=null)                    conn.close();            } catch (SQLException e) {                e.printStackTrace();            }        }}</code></pre>]]></content:encoded>
            <category domain="https://yww52.com/categories/%25E6%2588%2591%25E7%259A%2584%25E6%258A%2580%25E6%259C%25AF%25E5%25AD%25A6%25E4%25B9%25A0%25E7%2589%25A9%25E8%25AF%25AD%25E6%259E%259C%25E7%2584%25B6%25E6%259C%2589%25E9%2597%25AE%25E9%25A2%2598/">我的技术学习物语果然有问题</category>
        </item>
        <item>
            <title>正则表达式</title>
            <link>https://yww52.com/posts/2f57a694/</link>
            <guid isPermaLink="true">https://yww52.com/posts/2f57a694/</guid>
            <pubDate>Sun, 03 Nov 2019 00:00:00 +0000</pubDate>
            <description>正则表达式时一种强大而灵活的文本处理工具，用于文本的复杂处理</description>
            <content:encoded><![CDATA[<img src="https://img.yww52.com/2019/11/2019-11-03top_img.jpg" alt="正则表达式" style="max-width:100%;height:auto;margin-bottom:15px;" /><h2>简介</h2><ul><li><p>正则表达式时一种强大而灵活的文本处理工具，用于文本的复杂处理</p></li><li><p>正则表达式就是描述了一个规则，通过这个规则可以匹配一类字符串</p></li></ul><h2>语法</h2><ol><li><p><strong>普通字符</strong>字母，数字，汉字，下划线以及没有特殊定义的标点符号都是普通字符。表达式中的普通字符在匹配一个字符串时，匹配与之相同的一个字符</p></li><li><p><strong>简单的转义字符</strong></p><pre><code>\n        代表换行符\t        代表制表符\\        代表\本身还有一些特殊的字符表达</code></pre></li><li><p>标准字符集合</p><pre><code>\d        任意一个数字\w          任意一个字母，数字或下划线\s        包括空格，制表符，换行符等空白字符的其中任意一个.        小数点可以匹配任意一个字符(除了换行符)                  注意大小写，若是大写则代表相反的意思，比如 \D 表示除了数字之外的字符</code></pre></li><li><p>自定义字符集合</p><ul><li><p>方括号的匹配方式，能够匹配方括号中任意一个字符，比如</p><pre><code>[123]        匹配1，2，3字符[2-8]        匹配2~8的数字[ ^123 ]        匹配除了1，2，3之外的字符</code></pre></li><li><p>正则表达式的特殊符号被包含到中括号中则失去特殊意义，除了^,-之外</p></li><li><p>标准字符集合，除小数点外，如果被包含于中括号，自定义字符集合将包括该集合，比如</p><pre><code>[\d.\-+]        匹配数字，小数点，+，-</code></pre></li></ul></li><li><p>量词</p><pre><code>{n}                    重复n次{m,n}                至少重复m次，最多重复n次匹配的时候默认的是贪婪模式，意思为能匹配多的就匹配多的非贪婪模式，匹配字符越少越好，修饰匹配次数的特殊符号后加一个“？”</code></pre></li><li><p>字符边界</p><pre><code>        把一整个文本当成一个字符串^                    与字符串开始的地方匹配$                    与字符串结束的地方匹配\b                    匹配一个单词边界，即前面和后面的字符不全是\w</code></pre></li><li><p>选择符和分组</p><pre><code>|                或，表示匹配左边或右边( )                捕获组，可以保存捕获的内容，并从1开始编号(?: )            非捕获组，表示不需要保存捕获的内容反向引用          每一对（）会分配一个编号，使用（）根据左括号开始从1编号，通过反向引用可以对分组已捕获的字符串进行引用，比如\1会表示第一个捕获组的内容</code></pre></li><li><p>预搜索（零宽断言）</p><pre><code>零宽断言表示一种零宽度的匹配，匹配的是一个位置(?=exp)                断言自身出现的位置后面有表达式exp(?!exp)                断言此位置的后面不能有表达式exp(?&lt;=exp)                断言自身出现的位置前面有表达式exp(?&lt;!exp)                断言此位置的前面不能有表达式exp</code></pre></li></ol><h2>匹配模式</h2><ol><li><p><strong>IGNORECASE</strong></p><p>忽略大小写模式，即匹配时忽略大小写</p></li><li><p><strong>SINGLELINE</strong></p><p>单行模式，整个文本看作一个字符串</p></li><li><p><strong>MULTILINE</strong></p><p>每行都是一个字符串，如果需要匹配最开始和结束的位置，可以用\A和\Z</p></li></ol><h2>Java中使用正则表达式</h2><p>在Java中使用正则主要是java.util.regex包地Pattern和Matcher类</p><h3>类Pattern</h3><ol><li>正则表达式的编译表示形式</li></ol><h3>类Matcher</h3><ol><li><p>通过解释Pattern对character  sequence执行匹配操作的引擎</p></li><li><pre><code class="language-java">Matcher m = p.matcher(str) ;         //匹配str字符串</code></pre></li></ol><h3>一个简单地使用</h3><pre><code class="language-java">// 一个简单的使用Pattern p = Pattern.compile(&quot;\\w+&quot;);Matcher m = p.matcher(&quot;asjdhjksa234qw&quot;) ;boolean flag = m.matches();//尝试将整个字符序列与该模式匹配// 符合则flag为true，反之为falseflag = m.find();//扫描输入的序列，查找该模式匹配的下一个子序列String res = m.group();//返回找到的子序列</code></pre>]]></content:encoded>
            <category domain="https://yww52.com/categories/%25E6%2588%2591%25E7%259A%2584%25E6%258A%2580%25E6%259C%25AF%25E5%25AD%25A6%25E4%25B9%25A0%25E7%2589%25A9%25E8%25AF%25AD%25E6%259E%259C%25E7%2584%25B6%25E6%259C%2589%25E9%2597%25AE%25E9%25A2%2598/">我的技术学习物语果然有问题</category>
        </item>
        <item>
            <title>static与final</title>
            <link>https://yww52.com/posts/92a59ea9/</link>
            <guid isPermaLink="true">https://yww52.com/posts/92a59ea9/</guid>
            <pubDate>Thu, 31 Oct 2019 00:00:00 +0000</pubDate>
            <description>static和final还是会弄乱的，特地整理一下</description>
            <content:encoded><![CDATA[<img src="https://img.yww52.com/2019/10/2019-10-31top_img.jpg" alt="static与final" style="max-width:100%;height:auto;margin-bottom:15px;" /><h2>static</h2><ul><li><p>static修饰的变量为类的变量，属于类，即所有的该类的实例化对象共同使用该static变量</p></li><li><p>static方法中不能访问非static成员</p><blockquote><p>这个很容易理解，每个类都只有一份static的资源，还没类还没实例化就有了该static方法，但没有实例化的成员即非静态成员，所以静态方法是不能访问非static成员的</p></blockquote></li><li><p>static的变量和方法或者块都是属于类的，普通变量和方法是属于对象的。</p></li></ul><h2>final</h2><ul><li><p>用final修饰类，表明中这个类是不能不继承的，类中的变量可以是非final的变量，但所有的方法都会被隐式地指定为final方法</p></li><li><p>使用final修饰方法原因有两个。一，把方法锁定，防止任何继承类重写该方法。二，提高效率。在早期的Java实现版本会将final方法转为内嵌调用，但如果方法过于庞大，可能看不到内嵌的优化效果，新的Java版本已经   不需要final方法来优化</p></li><li><p>final不能修饰构造方法</p></li><li><p>final修饰变量，即表示该变量被定义为一个常量，该常量被初始化赋值之后就不能修改</p></li></ul>]]></content:encoded>
            <category domain="https://yww52.com/categories/%25E6%2588%2591%25E7%259A%2584%25E6%258A%2580%25E6%259C%25AF%25E5%25AD%25A6%25E4%25B9%25A0%25E7%2589%25A9%25E8%25AF%25AD%25E6%259E%259C%25E7%2584%25B6%25E6%259C%2589%25E9%2597%25AE%25E9%25A2%2598/">我的技术学习物语果然有问题</category>
        </item>
    </channel>
</rss>