<?xml version="1.0" encoding="UTF-8"?><rss version="2.0" xmlns:content="http://purl.org/rss/1.0/modules/content/"><channel><title>糖糖IT</title><description>开源世界的淘金者。我负责挖，你负责用。每个项目都亲测跑通，效果看得见。 | TangTangIt - Gold miner in the open source world. I do the digging, you do the using. Every project is tested and running — results guaranteed.</description><link>https://tangtangit.com/</link><language>zh_CN</language><item><title>🎙️ 视频配音还在花钱？这款免费多语言语音合成工具直接白嫖！</title><link>https://tangtangit.com/posts/5_free-tts/</link><guid isPermaLink="true">https://tangtangit.com/posts/5_free-tts/</guid><description>免费、界面好看、操作简单的在线语音合成工具，支持文本转语音和语音转文字，多语言多音色，一键下载MP3，视频配音、字幕提取全搞定！</description><pubDate>Fri, 07 Aug 2026 00:00:00 GMT</pubDate><content:encoded>&lt;h1&gt;🎙️ 视频配音还在花钱？这款免费多语言语音合成工具直接白嫖！&lt;/h1&gt;
&lt;p&gt;😤 &lt;strong&gt;你是不是也遇到过这些事？&lt;/strong&gt;&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;视频要配音 → 打开某软件：请先充值 💢&lt;br /&gt;
文本要转语音 → 网页版限时免费，转完水印糊一脸 💢&lt;br /&gt;
语音要转文字 → 要么收费，要么担心录音被上传 💢💢&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;咱就是说，一个语音工具而已，凭什么动不动就要钱啊！！&lt;/p&gt;
&lt;hr /&gt;
&lt;h2&gt;✨ 今天糖糖IT挖到宝了——&lt;strong&gt;Free TTS Studio&lt;/strong&gt;&lt;/h2&gt;
&lt;p&gt;一句话总结：&lt;strong&gt;界面好看、操作简单、完全免费&lt;/strong&gt;，文本转语音、语音转文字两大功能全都不要钱！&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://tangtangit.com/_astro/image-20260806153819602.B6gGxN7I_Z1m5kMC.webp&quot; alt=&quot;image-20260806153819602&quot; /&gt;&lt;/p&gt;
&lt;hr /&gt;
&lt;h2&gt;📝 功能一：文本转语音&lt;/h2&gt;
&lt;h3&gt;① 输入文本&lt;/h3&gt;
&lt;p&gt;直接在输入框里粘贴要转语音的文字 👇&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://tangtangit.com/_astro/image-20260806153853664.kEgzqbPc_29CHid.webp&quot; alt=&quot;image-20260806153853664&quot; /&gt;&lt;/p&gt;
&lt;h3&gt;② 多语言支持&lt;/h3&gt;
&lt;p&gt;右侧选择语言，中文、英语、日语、韩语、法语、西班牙语等主流语言全覆盖 👇&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://tangtangit.com/_astro/image-20260806154010263.D_OnLNiM_1ARjCI.webp&quot; alt=&quot;image-20260806154010263&quot; /&gt;&lt;/p&gt;
&lt;h3&gt;③ 多音色选择&lt;/h3&gt;
&lt;p&gt;男声、女声随便挑，总有一款适合你的视频 👇&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://tangtangit.com/_astro/image-20260806154029354.D4dyor0Z_Z2efi5O.webp&quot; alt=&quot;image-20260806154029354&quot; /&gt;&lt;/p&gt;
&lt;h3&gt;④ 语音风格切换&lt;/h3&gt;
&lt;p&gt;通用、新闻播报、叙事等多种风格随心切换，配音不再一个调 👇&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://tangtangit.com/_astro/image-20260806154057694.CKW4Zs_m_1yFzkn.webp&quot; alt=&quot;image-20260806154057694&quot; /&gt;&lt;/p&gt;
&lt;p&gt;对语速、音调有要求？也能精细调节 👇&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://tangtangit.com/_astro/image-20260806154121597.C-4Ti3Jy_ZyrDg9.webp&quot; alt=&quot;image-20260806154121597&quot; /&gt;&lt;/p&gt;
&lt;h3&gt;⑤ 一键下载 MP3&lt;/h3&gt;
&lt;p&gt;生成结果一键下载成 MP3，不满意随时重新生成，真正做到&lt;strong&gt;免费管饱&lt;/strong&gt; 👇&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://tangtangit.com/_astro/image-20260806154204234.D9e_c7Mj_1wGdnd.webp&quot; alt=&quot;image-20260806154204234&quot; /&gt;&lt;/p&gt;
&lt;hr /&gt;
&lt;h2&gt;🎧 功能二：语音转文字&lt;/h2&gt;
&lt;p&gt;支持&lt;strong&gt;录音&lt;/strong&gt;和&lt;strong&gt;上传文件&lt;/strong&gt;两种方式，基于浏览器端 Whisper 引擎，&lt;strong&gt;全部免费、无需密钥&lt;/strong&gt;。&lt;/p&gt;
&lt;p&gt;各种文件格式都能上传，录音或上传文件即可轻松实现语音转文字 👇&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://tangtangit.com/_astro/image-20260806154529850.CZCzxCX8_Z2oDufd.webp&quot; alt=&quot;image-20260806154529850&quot; /&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://tangtangit.com/_astro/image-20260806154646361.BhVQisZL_ZYMwvV.webp&quot; alt=&quot;image-20260806154646361&quot; /&gt;&lt;/p&gt;
&lt;hr /&gt;
&lt;h2&gt;📊 信息速览表&lt;/h2&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;维度&lt;/th&gt;
&lt;th&gt;详情&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;💰 价格&lt;/td&gt;
&lt;td&gt;完全免费&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;🖥️ 使用方式&lt;/td&gt;
&lt;td&gt;在线网页，无需安装&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;🌍 语言支持&lt;/td&gt;
&lt;td&gt;中/英/日/韩/法/西等多语言&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;🎭 音色&lt;/td&gt;
&lt;td&gt;男声、女声可选&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;🎚️ 高级调节&lt;/td&gt;
&lt;td&gt;语速、音调、语音风格&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;💾 导出格式&lt;/td&gt;
&lt;td&gt;MP3 一键下载&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;🎙️ 语音转文字&lt;/td&gt;
&lt;td&gt;Whisper 引擎，录音/上传文件&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;🔑 密钥要求&lt;/td&gt;
&lt;td&gt;无，全部免费&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;hr /&gt;
&lt;h2&gt;💬 最后说两句&lt;/h2&gt;
&lt;p&gt;做视频配音、念稿、字幕提取，这个工具一个顶俩，还完全不收费。感兴趣的宝子请一键三连，评论区扣1，关注我直接领走～&lt;/p&gt;
&lt;hr /&gt;
&lt;p&gt;#TTS #语音合成 #免费工具 #视频配音 #效率神器 #AI工具 #宝藏网站 #语音转文字&lt;/p&gt;
</content:encoded></item><item><title>🎨 3分钟搞定GitHub主页美化，新手也能拥有超酷个人门面！</title><link>https://tangtangit.com/posts/4_github%E7%BE%8E%E5%8C%96/</link><guid isPermaLink="true">https://tangtangit.com/posts/4_github%E7%BE%8E%E5%8C%96/</guid><description>三步打造专属GitHub个人主页：profile设置、同名仓库README、整体美化，文末附赠6款美化神器！</description><pubDate>Wed, 29 Jul 2026 00:00:00 GMT</pubDate><content:encoded>&lt;h1&gt;🎨 3分钟搞定GitHub主页美化，新手也能拥有超酷个人门面！&lt;/h1&gt;
&lt;p&gt;对于每一个刚注册 GitHub 的人来说，装饰自己的主页都是必做的一件事——&lt;strong&gt;GitHub 主页是对外展示的&quot;门面&quot;&lt;/strong&gt;，也是分享开源项目的最佳途径。&lt;/p&gt;
&lt;p&gt;没废话，直接上手！一共三步：&lt;strong&gt;个人 profile 设置 → README.md 设置 → 整体美化&lt;/strong&gt; 🚀&lt;/p&gt;
&lt;hr /&gt;
&lt;h2&gt;1️⃣ 个人 profile 设置&lt;/h2&gt;
&lt;p&gt;进入 GitHub 后，&lt;strong&gt;点击右上角头像 → Profile&lt;/strong&gt;，再点击&lt;strong&gt;左侧的头像&lt;/strong&gt;。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://tangtangit.com/_astro/image-20260730145121327.DWS_PMT9_Zum9aL.webp&quot; alt=&quot;image-20260730145121327&quot; /&gt;&lt;/p&gt;
&lt;p&gt;这样就进入了个人界面的编辑页面：&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://tangtangit.com/_astro/image-20260730145317986.BgDoXS2L_28IuNb.webp&quot; alt=&quot;image-20260730145317986&quot; /&gt;&lt;/p&gt;
&lt;h3&gt;Public profile 修改项一览&lt;/h3&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;配置项&lt;/th&gt;
&lt;th&gt;说明&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Name&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;填真实姓名或昵称&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Public email&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;填合适的邮箱，需在 email settings 中设为 Public&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Bio&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;个人简介/技能标签&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Pronouns&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;人称代词，可忽略&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Url&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;个人主页（如博客链接）&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Social accounts&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;社交账号，可选填&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;ORCID iD&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;学术相关信息，可忽略&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;确认无误后点击下方 &lt;strong&gt;更新&lt;/strong&gt; 按钮：&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://tangtangit.com/_astro/image-20260730145728728.B93fNZDD_Z8WMFI.webp&quot; alt=&quot;image-20260730145728728&quot; /&gt;&lt;/p&gt;
&lt;p&gt;设置好后回到主页，效果如下：&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://tangtangit.com/_astro/image-20260730145940914.DuNyH1bl_Z13THGl.webp&quot; alt=&quot;image-20260730145940914&quot; /&gt;&lt;/p&gt;
&lt;hr /&gt;
&lt;h2&gt;2️⃣ README.md 设置&lt;/h2&gt;
&lt;p&gt;接下来设置主页上的 README.md：需要&lt;strong&gt;创建一个与 GitHub 账号同名且为 Public 的仓库&lt;/strong&gt;（注意是注册账号名，不是第一步里的 Name，比如我这里用的是 &lt;code&gt;tangtang-it&lt;/code&gt;）。&lt;/p&gt;
&lt;p&gt;创建时打开&quot;新增 README 文件&quot;开关，最后点击创建：&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://tangtangit.com/_astro/image-20260730150518142.Cou9F8i4_23QCFd.webp&quot; alt=&quot;image-20260730150518142&quot; /&gt;&lt;/p&gt;
&lt;p&gt;回到首页，就能看到主页上多了一个 README.md 文件：&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://tangtangit.com/_astro/image-20260730154637000.BghN01nG_ZHsHd3.webp&quot; alt=&quot;image-20260730154637000&quot; /&gt;&lt;/p&gt;
&lt;hr /&gt;
&lt;h2&gt;3️⃣ 整体美化&lt;/h2&gt;
&lt;p&gt;接下来就是重头戏——界面美化。简单写下想展示的内容，交给任意 AI 生成一份可以直接复制使用的 md 文件即可。&lt;/p&gt;
&lt;p&gt;如果 AI 生成的效果还不满意，可以用 &lt;strong&gt;&lt;a href=&quot;https://github.com/tangtang-it/tangtang-readme&quot;&gt;tangtang-readme&lt;/a&gt;&lt;/strong&gt; 做进一步优化。我的界面之前是这样的：&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://tangtangit.com/_astro/image-20260730154212725.CMc0dHFW_2eGLFY.webp&quot; alt=&quot;image-20260730154212725&quot; /&gt;&lt;/p&gt;
&lt;p&gt;美化后变成这样：&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://tangtangit.com/_astro/image-20260730155656105.CXfWGxjZ_Z1XrhD9.webp&quot; alt=&quot;image-20260730155656105&quot; /&gt;&lt;/p&gt;
&lt;p&gt;用薄荷绿配色美化后的效果：&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://tangtangit.com/_astro/image-20260730160125838.DwAK5mNW_p6mxB.webp&quot; alt=&quot;image-20260730160125838&quot; /&gt;&lt;/p&gt;
&lt;hr /&gt;
&lt;h2&gt;🎁 附赠：6 款 GitHub 主页美化神器&lt;/h2&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;工具&lt;/th&gt;
&lt;th&gt;亮点&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;&lt;a href=&quot;https://github.com/tangtang-it/tangtang-readme&quot;&gt;tangtang-readme&lt;/a&gt;&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;对 README 文件进行优化并添加合适的 banner&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;&lt;a href=&quot;https://gprm.itsvg.in/&quot;&gt;GRPM&lt;/a&gt;&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;填写信息、勾选组件，直接复制生成的 Markdown&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;&lt;a href=&quot;https://rahuldkjain.github.io/github-profile-readme-generator/&quot;&gt;GitHub Profile README Generator&lt;/a&gt;&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;老牌生成器，选项很全：技术栈、社交链接、Stats 都能加&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;&lt;a href=&quot;https://github.com/kyechan99/capsule-render&quot;&gt;Capsule Render&lt;/a&gt;&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;生成各种样式的彩色头图/尾图&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;&lt;a href=&quot;https://leviarista.github.io/github-profile-header-generator&quot;&gt;github-profile-header-generator&lt;/a&gt;&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;专门做顶部 Banner 图&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;&lt;a href=&quot;https://www.profileme.dev/&quot;&gt;ProfileMe.dev&lt;/a&gt;&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;界面好看，实时预览效果&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;hr /&gt;
&lt;p&gt;三步走完，一个专属于你的 GitHub 门面就打造完成啦～快去试试吧！&lt;/p&gt;
&lt;p&gt;#GitHub美化 #GitHub主页 #个人主页 #开源 #README #程序员必备 #效率神器 #宝藏工具&lt;/p&gt;
</content:encoded></item><item><title>✅ 实测好用！免费m3u8在线播放器推荐</title><link>https://tangtangit.com/posts/3_m3u8online/</link><guid isPermaLink="true">https://tangtangit.com/posts/3_m3u8online/</guid><description>完全免费，不用下载不用安装，浏览器输入地址就能用，一站式解决播放、回放、批量检测三个老大难。界面极简，新手秒上手。</description><pubDate>Thu, 23 Jul 2026 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;🌟 发现一个宝藏线上播放器，m3u8源再也不用折腾了！&lt;/p&gt;
&lt;p&gt;📺 &lt;strong&gt;痛点一击即中&lt;/strong&gt;&lt;br /&gt;
你是不是也遇到过？手上有条m3u8链接，非要先下个播放器，安装五分钟，捆绑软件倒是来了一堆。想回看刚才的直播片段？传统播放器根本不支持。源有没有失效？还得一个个点开试，浪费时间到崩溃。&lt;/p&gt;
&lt;p&gt;✨ &lt;strong&gt;就是这个在线神器&lt;/strong&gt;&lt;br /&gt;
完全免费，不用下载不用安装，浏览器输入地址就能用，一站式解决播放、回放、批量检测三个老大难。界面极简，新手秒上手。&lt;/p&gt;
&lt;p&gt;🧪 &lt;strong&gt;核心实测体验&lt;/strong&gt;&lt;br /&gt;
· 粘贴即播：复制m3u8链接，点一下播放，画面秒出，流畅不卡，实测加载速度比本机播放器还快一截。&lt;br /&gt;
· 历史留痕：开启历史记录模式，之前看过的链接都在，点一下就能切回去，省去反复复制粘贴的麻烦。&lt;br /&gt;
· 批量检测：扔进去几十条源，3秒出结果，有效无效一目了然，以前手动测试半小时打底。
&lt;img src=&quot;./index.assets/%E8%BD%AF%E4%BB%B6%E6%88%AA%E5%9B%BE.png&quot; alt=&quot;image-20260723102424242&quot; /&gt;&lt;/p&gt;
&lt;p&gt;📊 &lt;strong&gt;信息速览表&lt;/strong&gt;&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;维度&lt;/th&gt;
&lt;th&gt;详情&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;💰 价格&lt;/td&gt;
&lt;td&gt;完全免费&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;📦 安装&lt;/td&gt;
&lt;td&gt;无需，纯网页&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;⏱️ 打开速度&lt;/td&gt;
&lt;td&gt;比本地播放器快30%+&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;🔁 历史回放&lt;/td&gt;
&lt;td&gt;✅ 支持&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;📋 批量检测&lt;/td&gt;
&lt;td&gt;✅ 支持，3秒出结果&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;🎯 适用场景&lt;/td&gt;
&lt;td&gt;临时测试、追播、源筛选&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;⚠️ 小遗憾&lt;/td&gt;
&lt;td&gt;暂不支持下载视频&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;💬 &lt;strong&gt;用过的都在夸&lt;/strong&gt;&lt;br /&gt;
“临时测个源太方便了！”&lt;br /&gt;
“找回了不用装软件的安全感。”&lt;br /&gt;
“批量检测救我狗命，效率翻倍。”&lt;/p&gt;
&lt;p&gt;🔗 浏览器直接打开：https://m3u8player.clickio.site&lt;/p&gt;
&lt;p&gt;#m3u8播放器 #免费在线工具 #直播源检测 #效率神器 #极客好物 #视频播放 #在线工具推荐&lt;/p&gt;
</content:encoded></item><item><title>🔥 别再为PDF花钱了！这款免费PDF神器打工人都给我冲！！</title><link>https://tangtangit.com/posts/2_stirling-pdf/</link><guid isPermaLink="true">https://tangtangit.com/posts/2_stirling-pdf/</guid><description>PDF能想溃瞬间到的功能，它全有，而且全免费！！</description><pubDate>Mon, 20 Jul 2026 00:00:00 GMT</pubDate><content:encoded>&lt;h1&gt;🔥 别再为PDF花钱了！这款免费PDF神器打工人都给我冲！！&lt;/h1&gt;
&lt;hr /&gt;
&lt;p&gt;😤 &lt;strong&gt;你是不是也遇到过这些崩？&lt;/strong&gt;&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;PDF要合并 → 弹窗：开通会员 💢&lt;br /&gt;
PDF要压缩 → 弹窗：开通会员 💢&lt;br /&gt;
PDF要转格式 → 还是弹窗：开通会员 💢💢💢&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;咱就是说，一个PDF工具而已，凭什么动不动就要钱啊！！&lt;/p&gt;
&lt;hr /&gt;
&lt;h2&gt;✨ 今天糖糖IT挖到宝了——&lt;strong&gt;Stirling PDF&lt;/strong&gt;&lt;/h2&gt;
&lt;p&gt;一句话总结：&lt;strong&gt;PDF能想溃瞬间到的功能，它全有，而且全免费！！&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://tangtangit.com/_astro/image-20260720204440639.8Vko6FXq_Z1JyKU7.webp&quot; alt=&quot;image-20260720204440639&quot; /&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://tangtangit.com/_astro/image-20260720193211386.D5y9QVba_ZjqQFz.webp&quot; alt=&quot;image-20260720193211386&quot; /&gt;&lt;/p&gt;
&lt;p&gt;🎯 最新V2版本更香：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;📦 &lt;strong&gt;有状态处理&lt;/strong&gt;：上传一次文件，多个工具来回切换用，不用反复上传！&lt;/li&gt;
&lt;li&gt;🖥️ &lt;strong&gt;原生桌面应用&lt;/strong&gt;：秒启动，不用开浏览器也能用！&lt;/li&gt;
&lt;li&gt;🆓 &lt;strong&gt;完全开源免费&lt;/strong&gt;，GitHub上狂揽几十K星标！&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;h2&gt;📥 怎么下载？&lt;/h2&gt;
&lt;p&gt;超简单！三平台全支持（Windows / Mac / Linux）&lt;/p&gt;
&lt;p&gt;👉 直接去 GitHub Releases 页面挑对应版本就行：&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://tangtangit.com/_astro/image-20260720194602275.Hj5vivWq_7BNl.webp&quot; alt=&quot;image-20260720194602275&quot; /&gt;&lt;/p&gt;
&lt;hr /&gt;
&lt;h2&gt;🧪 实测体验&lt;/h2&gt;
&lt;h3&gt;🔽 压缩功能&lt;/h3&gt;
&lt;p&gt;一键压缩，还支持撤销！6M 的 PDF 直接压到 3M，效果肉眼可见 👇&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://tangtangit.com/_astro/image-20260720194149583.C5mNt3k2_ZgqjoC.webp&quot; alt=&quot;image-20260720194149583&quot; /&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://tangtangit.com/_astro/image-20260720194415644.StYkpclc_Z1GLHvp.webp&quot; alt=&quot;image-20260720194415644&quot; /&gt;&lt;/p&gt;
&lt;h3&gt;🖼️ 图片水印&lt;/h3&gt;
&lt;p&gt;轻松给PDF加水印，操作丝滑不卡顿～&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://tangtangit.com/_astro/image-20260720195254406.BV4foQlM_5zXld.webp&quot; alt=&quot;image-20260720195254406&quot; /&gt;&lt;/p&gt;
&lt;hr /&gt;
&lt;h2&gt;💬 最后说两句&lt;/h2&gt;
&lt;p&gt;除了上面这些，还有合并、对比、格式转换、移除密码等等几十种功能，主打一个 &lt;strong&gt;&quot;功能拉满还不收钱&quot;&lt;/strong&gt;。&lt;/p&gt;
&lt;p&gt;就不一个个展示了，感兴趣的宝子请一键三连，评论区扣1，关注我直接领走～&lt;/p&gt;
&lt;hr /&gt;
&lt;p&gt;#PDF工具 #免费软件 #效率神器 #打工人必备 #StirlingPDF #开源软件 #办公技巧 #宝藏工具&lt;/p&gt;
</content:encoded></item><item><title>发现宝藏！这款开源简历编辑器太绝了✨</title><link>https://tangtangit.com/encrypted-example/</link><guid isPermaLink="true">https://tangtangit.com/encrypted-example/</guid><description>一款现代化的在线简历编辑器，界面超级好看，操作简单到哭！</description><pubDate>Sat, 21 Mar 2026 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;💫 这是什么神仙项目？
Magic Resume- 一款现代化的在线简历编辑器，界面超级好看，操作简单到哭！
&lt;img src=&quot;https://tangtangit.com/_astro/image.DW7X5UpI_aJoFn.webp&quot; alt=&quot;魔方简历&quot; /&gt;
🔗 项目地址：&lt;/p&gt;
&lt;p&gt;https://github.com/JOYCEQL/magic-resume&lt;/p&gt;
&lt;p&gt;up主已成功运行并体验了，用下来感觉很不错！！！值得点赞！！！&lt;/p&gt;
&lt;p&gt;🌟 核心亮点（每一个都戳中我）：&lt;/p&gt;
&lt;p&gt;✅ AI智能辅助写作 - 不知道怎么写？AI帮你润色，让你的简历闪闪发光&lt;/p&gt;
&lt;p&gt;✅ 实时预览 - 边写边看效果，所见即所得，再也不用来回调整&lt;/p&gt;
&lt;p&gt;✅ 自动一页排版 - 强迫症福音！自动优化排版，简历永远一页搞定&lt;/p&gt;
&lt;p&gt;✅ 暗黑模式 - 深夜肝简历也不伤眼，太贴心了&lt;/p&gt;
&lt;p&gt;✅ 一键导出PDF - 格式完全不乱，专业度满分&lt;/p&gt;
&lt;p&gt;✅ 自动保存 - 再也不怕忘记保存崩溃了&lt;/p&gt;
&lt;p&gt;✅ 多语言支持 - 中英文简历随意切换&lt;/p&gt;
&lt;p&gt;✅ 自定义主题 - 你的简历你做主，想怎么配色就怎么配色&lt;/p&gt;
&lt;p&gt;🎯 适合人群：&lt;/p&gt;
&lt;p&gt;👉 正在找工作的大学生/职场新人&lt;/p&gt;
&lt;p&gt;👉 想要美化简历但不会设计的你&lt;/p&gt;
&lt;p&gt;👉 需要频繁更新简历的求职者&lt;/p&gt;
&lt;p&gt;👉 简历排版困难户&lt;/p&gt;
</content:encoded></item><item><title>SpringCloud Seata：分布式事务解决方案</title><link>https://tangtangit.com/posts/springcloud-07-seata/</link><guid isPermaLink="true">https://tangtangit.com/posts/springcloud-07-seata/</guid><description>深入理解Seata分布式事务框架的AT模式、TCC模式、SAGA模式，以及全局事务ID、分支事务、TC/TM/RM三大角色。</description><pubDate>Fri, 20 Mar 2026 00:00:00 GMT</pubDate><content:encoded>&lt;h1&gt;seata概述&lt;/h1&gt;
&lt;p&gt;分布式事务处理过程的ID+三组件模型&lt;/p&gt;
&lt;p&gt;全局事务ID&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;TransactionID XID&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;三个角色&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Transaction Coordinator(TC):&lt;/strong&gt; Maintain status of global and branch transactions, drive the global commit or rollback.&lt;/p&gt;
&lt;p&gt;事务协调器，维护全局事务的运行状态，负责协调并驱动全局事务的提交或回滚;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Transaction Manager(TM):&lt;/strong&gt; Define the scope of global transaction: begin a global transaction, commit or rollback a global transaction.&lt;/p&gt;
&lt;p&gt;控制全局事务的边界，负责开启一个全局事务，并最终发起全局提交或全局回滚的决议;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Resource Manager(RM):&lt;/strong&gt; Manage resources that branch transactions working on, talk to TC for registering branch transactions and reporting status of branch transactions, and drive the branch transaction commit or rollback.&lt;/p&gt;
&lt;p&gt;控制分支事务，负责分支注册、状态汇报，并接收事务协调器的指令，驱动分支（本地)事务的提交和回滚&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;blockquote&gt;
&lt;p&gt;TM 向 TC 申请开启一个全局事务，全局事务创建成功并生成一个全局唯一的 XID；
XID 在微服务调用链路的上下文中传播；
RM 向 TC 注册分支事务，将其纳入 XID 对应全局事务的管辖；
TM 向 TC 发起针对 XID 的全局提交或回滚决议；
TC 调度 XID 下管辖的全部分支事务完成提交或回滚请求。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;&lt;img src=&quot;https://tangtangit.com/_astro/image-20220220152818271.DX7F35eT_Z1Ie1ui.webp&quot; alt=&quot;image-20220220152818271&quot; /&gt;&lt;/p&gt;
&lt;h1&gt;安装seata&lt;/h1&gt;
&lt;p&gt;我们首先需要点击&lt;a href=&quot;https://github.com/seata/seata/releases/&quot;&gt;下载地址&lt;/a&gt;进行下载 &lt;strong&gt;Seata&lt;/strong&gt;。&lt;/p&gt;
&lt;p&gt;文件为：  seata-server-1.3.0.zip&lt;/p&gt;
&lt;p&gt;解压：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;unzip seata-server-1.3.0.zip
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;由于我们是使用 &lt;strong&gt;nacos&lt;/strong&gt; 作为服务中心和配置中心，因此我们下载解压后需要做一些修改操作&lt;/p&gt;
&lt;h2&gt;1、编辑文件file.conf、registry.conf&lt;/h2&gt;
&lt;p&gt;进入 &lt;code&gt;conf&lt;/code&gt; 目录编辑 &lt;code&gt;registry.conf&lt;/code&gt; 和 &lt;code&gt;file.conf&lt;/code&gt; 两个文件，编辑后内容如下&lt;/p&gt;
&lt;h3&gt;1.1 file.conf&lt;/h3&gt;
&lt;blockquote&gt;
&lt;p&gt;file.conf   改指定参数&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;store {
  ## store mode: file、db、redis
  mode = &quot;db&quot;
  ## database store property
  db {
    ## the implement of javax.sql.DataSource, such as DruidDataSource(druid)/BasicDataSource(dbcp)/HikariDataSource(hikari) etc.
    datasource = &quot;druid&quot;
    ## mysql/oracle/postgresql/h2/oceanbase etc.
    dbType = &quot;mysql&quot;
    driverClassName = &quot;com.mysql.jdbc.Driver&quot;
    url = &quot;jdbc:mysql://127.0.0.1:3306/seata&quot;
    user = &quot;root&quot;
    password = &quot;easyParkMysql2021&quot;
    minConn = 5
    maxConn = 30
    globalTable = &quot;global_table&quot;
    branchTable = &quot;branch_table&quot;
    lockTable = &quot;lock_table&quot;
    queryLimit = 100
    maxWait = 5000
  }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;/blockquote&gt;
&lt;h3&gt;1.2 registry.conf&lt;/h3&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;code&gt;registry.conf&lt;/code&gt;   改指定参数&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;registry {
  # file 、nacos 、eureka、redis、zk、consul、etcd3、sofa
  type = &quot;nacos&quot;

  nacos {
    application = &quot;seata-server&quot;
    serverAddr = &quot;127.0.0.1:8848&quot;
    group = &quot;SEATA_GROUP&quot;
    namespace = &quot;&quot;
    cluster = &quot;default&quot;
    username = &quot;nacos&quot;
    password = &quot;essyParkNacos2021&quot;
  }
}

config {
  # file、nacos 、apollo、zk、consul、etcd3
  type = &quot;nacos&quot;

  nacos {
    serverAddr = &quot;127.0.0.1:8848&quot;
    namespace = &quot;&quot;
    group = &quot;SEATA_GROUP&quot;
    username = &quot;nacos&quot;
    password = &quot;essyParkNacos2021&quot;
  }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;/blockquote&gt;
&lt;h2&gt;2、新增&lt;code&gt;nacos-conf.sh&lt;/code&gt; 和 &lt;code&gt;config.txt&lt;/code&gt;&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;由于新版 &lt;strong&gt;Seata&lt;/strong&gt; 中没有 &lt;code&gt;nacos-conf.sh&lt;/code&gt; 和 &lt;code&gt;config.txt&lt;/code&gt; 两个文件，因此我们需要独立下载：&lt;/li&gt;
&lt;/ul&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;a href=&quot;https://github.com/seata/seata/blob/develop/script/config-center/nacos/nacos-config.sh&quot;&gt;nacos-config.sh 下载地址&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;https://github.com/seata/seata/blob/develop/script/config-center/config.txt&quot;&gt;config.txt 下载地址&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;&lt;code&gt;config.txt&lt;/code&gt;就是seata各种详细的配置，执行 &lt;code&gt;nacos-config.sh&lt;/code&gt; 即可将这些配置导入到&lt;strong&gt;nacos&lt;/strong&gt;，这样就不需要将&lt;code&gt;file.conf&lt;/code&gt;和&lt;code&gt;registry.conf&lt;/code&gt;放到我们的项目中了，需要什么配置就直接从&lt;strong&gt;nacos&lt;/strong&gt;中读取。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;我们需要将 &lt;code&gt;config.txt&lt;/code&gt; 文件放到 &lt;strong&gt;seata&lt;/strong&gt; 目录下，而非 &lt;strong&gt;conf&lt;/strong&gt; 目录下，并且需要修改 &lt;code&gt;config.txt&lt;/code&gt; 内容&lt;/p&gt;
&lt;h3&gt;2.1、config.txt&lt;/h3&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;code&gt;config.txt&lt;/code&gt;   改指定参数&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://tangtangit.com/_astro/image-20210425155521861.Bf8ZxYI7_E7bcM.webp&quot; alt=&quot;image-20210425155521861&quot; /&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;transport.type=TCP
transport.server=NIO
transport.heartbeat=true
transport.enableClientBatchSendRequest=false
transport.threadFactory.bossThreadPrefix=NettyBoss
transport.threadFactory.workerThreadPrefix=NettyServerNIOWorker
transport.threadFactory.serverExecutorThreadPrefix=NettyServerBizHandler
transport.threadFactory.shareBossWorker=false
transport.threadFactory.clientSelectorThreadPrefix=NettyClientSelector
transport.threadFactory.clientSelectorThreadSize=1
transport.threadFactory.clientWorkerThreadPrefix=NettyClientWorkerThread
transport.threadFactory.bossThreadSize=1
transport.threadFactory.workerThreadSize=default
transport.shutdown.wait=3
 
# 原来是service.vgroupMapping.my_test_tx_group，我改成service.vgroupMapping.hello_group (这句话修改完删掉)
service.vgroupMapping.hello_group=default
service.default.grouplist=127.0.0.1:8091
service.enableDegrade=false
service.disableGlobalTransaction=false
client.rm.asyncCommitBufferLimit=10000
client.rm.lock.retryInterval=10
client.rm.lock.retryTimes=30
client.rm.lock.retryPolicyBranchRollbackOnConflict=true
client.rm.reportRetryCount=5
client.rm.tableMetaCheckEnable=false
client.rm.sqlParserType=druid
client.rm.reportSuccessEnable=false
client.rm.sagaBranchRegisterEnable=false
client.tm.commitRetryCount=5
client.tm.rollbackRetryCount=5
client.tm.defaultGlobalTransactionTimeout=60000
client.tm.degradeCheck=false
client.tm.degradeCheckAllowTimes=10
client.tm.degradeCheckPeriod=2000
 
# store.mode=file改成 store.mode=db (这句话修改完删掉)
store.mode=db
store.file.dir=file_store/data
store.file.maxBranchSessionSize=16384
store.file.maxGlobalSessionSize=512
store.file.fileWriteBufferCacheSize=16384
store.file.flushDiskMode=async
store.file.sessionReloadReadSize=100
 
# db配置指向seata数据库 (这句话修改完删掉)
store.db.datasource=druid
store.db.dbType=mysql
store.db.driverClassName=com.mysql.jdbc.Driver
store.db.url=jdbc:mysql://127.0.0.1:3306/seata?useUnicode=true
store.db.user=root
store.db.password=12345678
store.db.minConn=5
store.db.maxConn=30
store.db.globalTable=global_table
store.db.branchTable=branch_table
store.db.queryLimit=100
store.db.lockTable=lock_table
store.db.maxWait=5000
store.redis.host=127.0.0.1
store.redis.port=6379
store.redis.maxConn=10
store.redis.minConn=1
store.redis.database=0
store.redis.password=null
store.redis.queryLimit=100
server.recovery.committingRetryPeriod=1000
server.recovery.asynCommittingRetryPeriod=1000
server.recovery.rollbackingRetryPeriod=1000
server.recovery.timeoutRetryPeriod=1000
server.maxCommitRetryTimeout=-1
server.maxRollbackRetryTimeout=-1
server.rollbackRetryTimeoutUnlockEnable=false
client.undo.dataValidation=true
client.undo.logSerialization=jackson
client.undo.onlyCareUpdateColumns=true
server.undo.logSaveDays=7
server.undo.logDeletePeriod=86400000
client.undo.logTable=undo_log
client.log.exceptionRate=100
transport.serialization=seata
transport.compressor=none
metrics.enabled=false
metrics.registryType=compact
metrics.exporterList=prometheus
metrics.exporterPrometheusPort=9898
&lt;/code&gt;&lt;/pre&gt;
&lt;/blockquote&gt;
&lt;h3&gt;2.2、nacos-conf.sh&lt;/h3&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;code&gt;nacos-conf.sh&lt;/code&gt;  不需要改&lt;/p&gt;
&lt;p&gt;放在&lt;strong&gt;conf&lt;/strong&gt; 目录下&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://tangtangit.com/_astro/image-20210425160358879.C6kurbS0_kMwgW.webp&quot; alt=&quot;image-20210425160358879&quot; /&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;#!/usr/bin/env bash
# Copyright 1999-2019 Seata.io Group.
#
# Licensed under the Apache License, Version 2.0 (the &quot;License&quot;);
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at、
#
#      http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an &quot;AS IS&quot; BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.

while getopts &quot;:h:p:g:t:u:w:&quot; opt
do
  case $opt in
  h)
    host=$OPTARG
    ;;
  p)
    port=$OPTARG
    ;;
  g)
    group=$OPTARG
    ;;
  t)
    tenant=$OPTARG
    ;;
  u)
    username=$OPTARG
    ;;
  w)
    password=$OPTARG
    ;;
  ?)
    echo &quot; USAGE OPTION: $0 [-h host] [-p port] [-g group] [-t tenant] [-u username] [-w password] &quot;
    exit 1
    ;;
  esac
done

urlencode() {
  for ((i=0; i &amp;lt; ${#1}; i++))
  do
    char=&quot;${1:$i:1}&quot;
    case $char in
    [a-zA-Z0-9.~_-]) printf $char ;;
    *) printf &apos;%%%02X&apos; &quot;&apos;$char&quot; ;;
    esac
  done
}

if [[ -z ${host} ]]; then
    host=localhost
fi
if [[ -z ${port} ]]; then
    port=8848
fi
if [[ -z ${group} ]]; then
    group=&quot;SEATA_GROUP&quot;
fi
if [[ -z ${tenant} ]]; then
    tenant=&quot;&quot;
fi
if [[ -z ${username} ]]; then
    username=&quot;&quot;
fi
if [[ -z ${password} ]]; then
    password=&quot;&quot;
fi

nacosAddr=$host:$port
contentType=&quot;content-type:application/json;charset=UTF-8&quot;

echo &quot;set nacosAddr=$nacosAddr&quot;
echo &quot;set group=$group&quot;

failCount=0
tempLog=$(mktemp -u)
function addConfig() {
  curl -X POST -H &quot;${contentType}&quot; &quot;http://$nacosAddr/nacos/v1/cs/configs?dataId=$(urlencode $1)&amp;amp;group=$group&amp;amp;content=$(urlencode $2)&amp;amp;tenant=$tenant&amp;amp;username=$username&amp;amp;password=$password&quot; &amp;gt;&quot;${tempLog}&quot; 2&amp;gt;/dev/null
  if [[ -z $(cat &quot;${tempLog}&quot;) ]]; then
    echo &quot; Please check the cluster status. &quot;
    exit 1
  fi
  if [[ $(cat &quot;${tempLog}&quot;) =~ &quot;true&quot; ]]; then
    echo &quot;Set $1=$2 successfully &quot;
  else
    echo &quot;Set $1=$2 failure &quot;
    (( failCount++ ))
  fi
}

count=0
for line in $(cat $(dirname &quot;$PWD&quot;)/config.txt | sed s/[[:space:]]//g); do
  (( count++ ))
	key=${line%%=*}
    value=${line#*=}
	addConfig &quot;${key}&quot; &quot;${value}&quot;
done

echo &quot;**=****=****=****=****=****=****=****=****=****=****=****=****=****=**===&quot;
echo &quot; Complete initialization parameters,  total-count:$count ,  failure-count:$failCount &quot;
echo &quot;**=****=****=****=****=****=****=****=****=****=****=****=****=****=**===&quot;

if [[ ${failCount} -eq 0 ]]; then
	echo &quot; Init nacos config finished, please start seata-server. &quot;
else
	echo &quot; init nacos config fail. &quot;
fi
&lt;/code&gt;&lt;/pre&gt;
&lt;/blockquote&gt;
&lt;h2&gt;3、执行导入&lt;/h2&gt;
&lt;p&gt;在 &lt;strong&gt;conf&lt;/strong&gt; 目录下执行&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;sh nacos-config.sh -h 127.0.0.1 (其中127.0.0.1是nacos地址)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;操作结束后，我们便可以在 &lt;strong&gt;nacos&lt;/strong&gt; 控制台中看到配置列表，日后配置有需要修改便可以直接从这边修改，而不用修改目录文件：&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://tangtangit.com/_astro/image-20210425161637145.BFsVr9py_ZIVaot.webp&quot; alt=&quot;image-20210425161637145&quot; /&gt;&lt;/p&gt;
&lt;h2&gt;4、数据库配置&lt;/h2&gt;
&lt;p&gt;在 &lt;strong&gt;1.3.0&lt;/strong&gt; 最新版中依然没有 sql 文件，所以我们还是需要另外下载：&lt;a href=&quot;https://github.com/seata/seata/blob/1.4.1/script/server/db/mysql.sql&quot;&gt;sql 下载地址&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;新建seata数据库在 &lt;strong&gt;seata&lt;/strong&gt; 数据中执行这个文件，生成三张表：&lt;/p&gt;
&lt;h3&gt;4.1、&lt;strong&gt;seata&lt;/strong&gt; 库&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;-- -------------------------------- The script used when storeMode is &apos;db&apos; --------------------------------
-- the table to store GlobalSession data
CREATE TABLE IF NOT EXISTS `global_table`
(
    `xid`                       VARCHAR(128) NOT NULL,
    `transaction_id`            BIGINT,
    `status`                    TINYINT      NOT NULL,
    `application_id`            VARCHAR(32),
    `transaction_service_group` VARCHAR(32),
    `transaction_name`          VARCHAR(128),
    `timeout`                   INT,
    `begin_time`                BIGINT,
    `application_data`          VARCHAR(2000),
    `gmt_create`                DATETIME,
    `gmt_modified`              DATETIME,
    PRIMARY KEY (`xid`),
    KEY `idx_gmt_modified_status` (`gmt_modified`, `status`),
    KEY `idx_transaction_id` (`transaction_id`)
) ENGINE = InnoDB
  DEFAULT CHARSET = utf8;

-- the table to store BranchSession data
CREATE TABLE IF NOT EXISTS `branch_table`
(
    `branch_id`         BIGINT       NOT NULL,
    `xid`               VARCHAR(128) NOT NULL,
    `transaction_id`    BIGINT,
    `resource_group_id` VARCHAR(32),
    `resource_id`       VARCHAR(256),
    `branch_type`       VARCHAR(8),
    `status`            TINYINT,
    `client_id`         VARCHAR(64),
    `application_data`  VARCHAR(2000),
    `gmt_create`        DATETIME(6),
    `gmt_modified`      DATETIME(6),
    PRIMARY KEY (`branch_id`),
    KEY `idx_xid` (`xid`)
) ENGINE = InnoDB
  DEFAULT CHARSET = utf8;

-- the table to store lock data
CREATE TABLE IF NOT EXISTS `lock_table`
(
    `row_key`        VARCHAR(128) NOT NULL,
    `xid`            VARCHAR(96),
    `transaction_id` BIGINT,
    `branch_id`      BIGINT       NOT NULL,
    `resource_id`    VARCHAR(256),
    `table_name`     VARCHAR(32),
    `pk`             VARCHAR(36),
    `gmt_create`     DATETIME,
    `gmt_modified`   DATETIME,
    PRIMARY KEY (`row_key`),
    KEY `idx_branch_id` (`branch_id`)
) ENGINE = InnoDB
  DEFAULT CHARSET = utf8;
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;4.2 业务数据库&lt;/h3&gt;
&lt;p&gt;在我们的业务数据库中执行 &lt;strong&gt;undo_log&lt;/strong&gt; 这张表：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;CREATE TABLE `undo_log`
(
    `id` BIGINT(20) NOT NULL AUTO_INCREMENT,
    `branch_id` BIGINT(20) NOT NULL,
    `xid` VARCHAR(100) NOT NULL,
    `context` VARCHAR(128) NOT NULL,
    `rollback_info` LONGBLOB NOT NULL,
    `log_status` INT(11) NOT NULL,
    `log_created` DATETIME NOT NULL,
    `log_modified` DATETIME NOT NULL,
    `ext` VARCHAR(100) DEFAULT NULL,
    PRIMARY KEY (`id`),
    UNIQUE KEY `ux_undo_log` (`xid`, `branch_id`)
) ENGINE = INNODB
AUTO_INCREMENT = 1
DEFAULT CHARSET = utf8;
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;5、启动seata&lt;/h2&gt;
&lt;h3&gt;5.1编写启动脚本&lt;/h3&gt;
&lt;p&gt;做好了以上准备，我们便可以启动 &lt;strong&gt;seata&lt;/strong&gt; 了，直接在 &lt;strong&gt;bin&lt;/strong&gt; 目录下 cmd 执行 &lt;strong&gt;bat&lt;/strong&gt; 脚本即可，启动结束便可在 &lt;strong&gt;nacos&lt;/strong&gt; 中看到 &lt;strong&gt;seata&lt;/strong&gt; 服务：&lt;/p&gt;
&lt;p&gt;新建start.sh，并存放在seata根目录下&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://tangtangit.com/_astro/image-20210425162137590.BioXI_p5_Z22X2da.webp&quot; alt=&quot;image-20210425162137590&quot; /&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;nohup  ./bin/seata-server.sh -p 8091 -h 8.135.57.242 -m file  &amp;gt;nohup.out 2&amp;gt;1 &amp;amp;
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;5.2启动&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;sh start.sh
&lt;/code&gt;&lt;/pre&gt;
&lt;h1&gt;项目中集成seata&lt;/h1&gt;
&lt;blockquote&gt;
&lt;p&gt;在 Seata 安装的步骤中我们便完成了 &lt;strong&gt;Seata 服务端&lt;/strong&gt; 的启动安装，接下来就是在项目中集成 &lt;strong&gt;Seata 客户端&lt;/strong&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2&gt;1、添加依赖&lt;/h2&gt;
&lt;p&gt;我们需要在 &lt;strong&gt;pom.xml&lt;/strong&gt; 文件中添加两个依赖：&lt;code&gt;seata 依赖&lt;/code&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;        &amp;lt;dependency&amp;gt;
            &amp;lt;groupId&amp;gt;com.alibaba.cloud&amp;lt;/groupId&amp;gt;
            &amp;lt;artifactId&amp;gt;spring-cloud-starter-alibaba-seata&amp;lt;/artifactId&amp;gt;
            &amp;lt;exclusions&amp;gt;
                &amp;lt;!-- 排除依赖 指定版本和服务器端一致 --&amp;gt;
                &amp;lt;exclusion&amp;gt;
                    &amp;lt;groupId&amp;gt;io.seata&amp;lt;/groupId&amp;gt;
                    &amp;lt;artifactId&amp;gt;seata-all&amp;lt;/artifactId&amp;gt;
                &amp;lt;/exclusion&amp;gt;
                &amp;lt;exclusion&amp;gt;
                    &amp;lt;groupId&amp;gt;io.seata&amp;lt;/groupId&amp;gt;
                    &amp;lt;artifactId&amp;gt;seata-spring-boot-starter&amp;lt;/artifactId&amp;gt;
                &amp;lt;/exclusion&amp;gt;
            &amp;lt;/exclusions&amp;gt;
        &amp;lt;/dependency&amp;gt;
        &amp;lt;dependency&amp;gt;
            &amp;lt;groupId&amp;gt;io.seata&amp;lt;/groupId&amp;gt;
            &amp;lt;artifactId&amp;gt;seata-all&amp;lt;/artifactId&amp;gt;
            &amp;lt;version&amp;gt;1.3.0&amp;lt;/version&amp;gt;
        &amp;lt;/dependency&amp;gt;
        &amp;lt;dependency&amp;gt;
            &amp;lt;groupId&amp;gt;io.seata&amp;lt;/groupId&amp;gt;
            &amp;lt;artifactId&amp;gt;seata-spring-boot-starter&amp;lt;/artifactId&amp;gt;
            &amp;lt;version&amp;gt;1.3.0&amp;lt;/version&amp;gt;
        &amp;lt;/dependency&amp;gt;
        &amp;lt;!--druid,解决mybatisPLus和seata3.0连接池不对应导致localdatetime转换失败--&amp;gt;
        &amp;lt;dependency&amp;gt;
            &amp;lt;groupId&amp;gt;com.alibaba&amp;lt;/groupId&amp;gt;
            &amp;lt;artifactId&amp;gt;druid&amp;lt;/artifactId&amp;gt;
            &amp;lt;version&amp;gt;1.1.21&amp;lt;/version&amp;gt;
        &amp;lt;/dependency&amp;gt;
        &amp;lt;!--druid连接池--&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;注意：&lt;/strong&gt; 这里需要排除 &lt;code&gt;spring-cloud-starter-alibaba-seata&lt;/code&gt; 自带的 &lt;strong&gt;seata&lt;/strong&gt; 依赖，然后引入我们自己需要的 &lt;strong&gt;seata&lt;/strong&gt; 版本，如果版本不一致启动时可能会造成 &lt;code&gt;no available server to connect&lt;/code&gt; 错误！&lt;/p&gt;
&lt;h2&gt;2、需要自己配置&lt;strong&gt;seata&lt;/strong&gt;代理数据源&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;@Configuration
public class DataSourceProxyConfig {

    @Bean
    @ConfigurationProperties(prefix = &quot;spring.datasource&quot;)
    public DruidDataSource druidDataSource() {
        return new DruidDataSource();
    }

    @Primary
    @Bean
    public DataSourceProxy dataSource(DruidDataSource druidDataSource) {
        return new DataSourceProxy(druidDataSource);
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;配置完数据源我们得在启动类的 &lt;code&gt;SpringBootApplication&lt;/code&gt; 上排除Druid数据源依赖，否则可能会出现循环依赖的错误：&lt;/p&gt;
&lt;blockquote&gt;
&lt;ul&gt;
&lt;li&gt;@EnableAutoDataSourceProxy&lt;/li&gt;
&lt;li&gt;(exclude = {DataSourceAutoConfiguration.class})&lt;/li&gt;
&lt;/ul&gt;
&lt;/blockquote&gt;
&lt;pre&gt;&lt;code&gt;//此注解是开启数据源自动代理
@EnableAutoDataSourceProxy
@SpringBootApplication(exclude = {DataSourceAutoConfiguration.class})
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;3、配置事务组项&lt;/h2&gt;
&lt;p&gt;在 &lt;strong&gt;nacos&lt;/strong&gt; 的配置文件控制台中加入我们服务的事务组项：&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;如不想添加，也可以在文件（&lt;code&gt;config.txt&lt;/code&gt;）中指定，添加内容如下&lt;/p&gt;
&lt;p&gt;service.vgroupMapping.hello_group=default&lt;/p&gt;
&lt;/blockquote&gt;
&lt;pre&gt;&lt;code&gt;service.vgroupMapping + 服务名称 = default
group为： SEATA_GROUP
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;img src=&quot;https://tangtangit.com/_astro/image-20210425163007209.CdceBqbE_ZNeiKv.webp&quot; alt=&quot;image-20210425163007209&quot; /&gt;&lt;/p&gt;
&lt;h2&gt;4、项目中配置修改&lt;/h2&gt;
&lt;p&gt;application.yml配置&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;spring:
  profiles:
    active: dev
  cloud:
    alibaba:
      seata:
        #这个要全局唯一，这个是前面在config.txt中的service.vgroupMapping.my_test_tx_group=default  
        #修改为service.vgroupMapping.hello_group=default。 
        tx-service-group: hello_group
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;#seata配置
seata:
  enabled: true
  #可以随意，现在设置成应用名
  application-id: ${spring.application.name}
  #事务组  这里的名字与config.txt中vgroup_mapping.my_test_tx_group = &quot;default&quot;相同
  tx-service-group: hello_group
  enable-auto-data-source-proxy: true
  #use-jdk-proxy: false
  service:
    #事务组  这里的名字与config.txt中vgroup_mapping.my_test_tx_group = &quot;default&quot;相同
    vgroup-mapping:
      my_test_tx_group: hello_group
    #这里的名字与config.txt中default.grouplist = &quot;127.0.0.1:8091&quot;相同
    grouplist:
      default: 8.135.57.242:8091
  #disable-global-transaction: false
  config:
    type: nacos
    nacos:
      #这里的地址就是你的nacos的地址,可以更换为线上
      serverAddr: 8.135.57.242:8848
      #这里的名字就是registry.conf中 nacos的group名字
      group: SEATA_GROUP
      userName: &quot;nacos&quot;
      password: &quot;essyParkNacos2021&quot;
  registry:
    type: nacos
    nacos:
      application: seata-server
      #这里的地址就是你的nacos的地址,可以更换为线上
      server-addr: 8.135.57.242:8848
      #这里的名字就是registry.conf中 nacos的group名字
      group: SEATA_GROUP
      userName: &quot;nacos&quot;
      password: &quot;essyParkNacos2021&quot;
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;5、开启全局事务&lt;/h2&gt;
&lt;p&gt;这步就是最终一步了，在我们需要开启事务的方法上添加 &lt;code&gt;@GlobalTransactional&lt;/code&gt; 注解，类似于我们单体事务添加的&lt;code&gt;@Transactional&lt;/code&gt;&lt;/p&gt;
&lt;h3&gt;user服务&lt;/h3&gt;
&lt;blockquote&gt;
&lt;p&gt;调用并开始全局事务&lt;/p&gt;
&lt;/blockquote&gt;
&lt;pre&gt;&lt;code&gt;    @Autowired
    private ThirdFeignClient thirdFeignClient;

	@GlobalTransactional(name = &quot;testSeata&quot;,rollbackFor = Exception.class)
    @GetMapping(&quot;/testSeata&quot;)
    public void test(){

        log.info(&quot;开始保存&quot;);
        thirdFeignClient.test();

        log.info(&quot;手工抛出异常&quot;);
        throw new EpException(ServiceCodeEnum.BAD_REQUEST);
    }
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;third服务&lt;/h3&gt;
&lt;p&gt;提供服务&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;    @GetMapping(value = &quot;/test&quot;)
    public void test(){
        savePictureValidationCode(&quot;我就是ip&quot;,&quot;我就是uuid&quot;,&quot;我就是code&quot;,&quot;base64&quot;);
    }

/**
     * 保存数据
     *
     * @param addressIp 请求的ip
     * @param uuid      uuid
     * @param rightCode 验证码正确的内容
     */
    private void savePictureValidationCode(String addressIp, String uuid, String rightCode, String imgBase64String) {
        PictureValidationCodeDO codeDO = new PictureValidationCodeDO();
        codeDO.setRightCode(rightCode);
        codeDO.setAddressIp(addressIp);
        codeDO.setUuid(uuid);
        codeDO.setImgBase64(imgBase64String);
        validateCodeService.save(codeDO);
    }
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;openfiegn&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;@FeignClient(value = &quot;third&quot;)
public interface ThirdFeignClient {

    /**
     * 校验验证码
     */
    @PostMapping(value = &quot;/kaptcha/validate&quot;)
    JsonResultVO&amp;lt;String&amp;gt; validate(@RequestBody ValidateCheckRequest checkRequest);

    @GetMapping(value = &quot;/kaptcha/test&quot;)
    void test();

}
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;6、注意事项&lt;/h2&gt;
&lt;p&gt;seata常见问题及解决&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;seata
1.1 Caused by: io.seata.common.exception.NotSupportYetException: aa_bb_record needs to contain the primary key.&lt;/li&gt;
&lt;/ol&gt;
&lt;blockquote&gt;
&lt;p&gt;seata 全局事务报业务表(postgresql 序列主键) 需要包含primary key&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;解决：
&lt;em&gt;&lt;strong&gt;多数据源配置　seata.enable-auto-data-source-proxy=false
单个数据源配置　seata.enable-auto-data-source-proxy=true&lt;/strong&gt;&lt;/em&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;seata.enable-auto-data-source-proxy=false
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;1.2 io.seata.common.exception.NotSupportYetException: undo_log needs to contain the primary key.&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;seata1.2.0 全局事务报undo_log 需要包含primary key&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;解决：
官方undo_log ddl &lt;a href=&quot;https://github.com/seata/seata/blob/develop/script/client/at/db/mysql.sql&quot;&gt;参考&lt;/a&gt;
加了自增id后的ddl如下：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;CREATE TABLE `undo_log`
(
    `id` BIGINT(20) NOT NULL AUTO_INCREMENT,
    `branch_id` BIGINT(20) NOT NULL,
    `xid` VARCHAR(100) NOT NULL,
    `context` VARCHAR(128) NOT NULL,
    `rollback_info` LONGBLOB NOT NULL,
    `log_status` INT(11) NOT NULL,
    `log_created` DATETIME NOT NULL,
    `log_modified` DATETIME NOT NULL,
    `ext` VARCHAR(100) DEFAULT NULL,
    PRIMARY KEY (`id`),
    UNIQUE KEY `ux_undo_log` (`xid`, `branch_id`)
) ENGINE = INNODB
AUTO_INCREMENT = 1
DEFAULT CHARSET = utf8;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;1.Caused by: io.seata.common.exception.NotSupportYetException: MZSF.MZYS_TBS_DPRESP needs to contain the primary key.&lt;/p&gt;
&lt;p&gt;或者 TCC模式 下报io.seata.common.exception.NotSupportYetException: multi pk only support mysql !&lt;/p&gt;
&lt;p&gt;原因：@GlobalTransactional和@Transactional不一起使用，解决方案：去除@Transactional 或者 使用 saveAndFlush&lt;/p&gt;
&lt;p&gt;2.ArrayIndexOutOfBoundsException: 22/4001&lt;/p&gt;
&lt;p&gt;原因：AT模式Jpa saveAll报数组越界， 改成循环saveAndFlush&lt;/p&gt;
&lt;p&gt;3.@BusinessActionContextParameter会将参数存在branch表，不必担心数据丢失&lt;/p&gt;
&lt;p&gt;4.复合主键进行删除操作时先将删除的对象查询出来，可存在@BusinessActionContextParameter,用于二阶段回滚时使用&lt;/p&gt;
&lt;p&gt;5.seata无法调用存储过程,原因：seata无法判断存储过程进行了什么操作&lt;/p&gt;
</content:encoded></item><item><title>SpringCloud Sentinel：流量控制与熔断降级</title><link>https://tangtangit.com/posts/springcloud-06-sentinel/</link><guid isPermaLink="true">https://tangtangit.com/posts/springcloud-06-sentinel/</guid><description>介绍Sentinel的流控规则、熔断降级、系统自适应限流等核心功能，以及与Hystrix的对比。</description><pubDate>Tue, 17 Mar 2026 00:00:00 GMT</pubDate><content:encoded>&lt;h1&gt;Sentinel 分布式系统的流量防卫兵&lt;/h1&gt;
&lt;p&gt;&lt;img src=&quot;https://tangtangit.com/_astro/image-20220220154529229.MeKmVVBo_Z1eplF7.webp&quot; alt=&quot;image-20220220154529229&quot; /&gt;&lt;/p&gt;
&lt;h2&gt;什么是服务雪崩&lt;/h2&gt;
&lt;p&gt;在微服务架构中，根据业务来拆分成一个个的服务，服务与服务之间可以通过 &lt;code&gt;HTTP/RPC&lt;/code&gt; 相互调用，在 Spring Cloud 中可以用 &lt;code&gt;RestTemplate + LoadBalanceClient&lt;/code&gt; 和 &lt;code&gt;Feign&lt;/code&gt; 来调用。为了保证其高可用，单个服务通常会集群部署。由于网络原因或者自身的原因，服务并不能保证 100% 可用，如果单个服务出现问题，调用这个服务就会出现线程阻塞，此时若有大量的请求涌入，&lt;code&gt;Servlet&lt;/code&gt; 容器的线程资源会被消耗完毕，导致服务瘫痪。服务与服务之间的依赖性，故障会传播，会对整个微服务系统造成灾难性的严重后果，这就是服务故障的 &lt;strong&gt;“雪崩”&lt;/strong&gt; 效应。为了解决这个问题，业界提出了 &lt;strong&gt;熔断器模型&lt;/strong&gt;。&lt;/p&gt;
&lt;p&gt;阿里巴巴开源了 Sentinel 组件，实现了熔断器模式，Spring Cloud 对这一组件进行了整合。在微服务架构中，一个请求需要调用多个服务是非常常见的&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;%E5%88%86%E5%B8%83%E5%BC%8F%E6%9E%B6%E6%9E%84.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;较底层的服务如果出现故障，会导致连锁故障。当对特定的服务的调用的不可用达到一个阀值熔断器将会被打开&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;%E6%95%85%E9%9A%9C%E7%86%94%E6%96%AD.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;熔断器打开后，为了避免连锁故障，通过 &lt;code&gt;fallback&lt;/code&gt; 方法可以直接返回一个固定值&lt;/p&gt;
&lt;h2&gt;什么是 Sentinel&lt;/h2&gt;
&lt;p&gt;随着微服务的流行，服务和服务之间的稳定性变得越来越重要。Sentinel 以流量为切入点，从流量控制、熔断降级、系统负载保护等多个维度保护服务的稳定性。&lt;/p&gt;
&lt;h3&gt;Sentinel 的特征&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;丰富的应用场景：&lt;/strong&gt; Sentinel 承接了阿里巴巴近 10 年的双十一大促流量的核心场景，例如秒杀（即突发流量控制在系统容量可以承受的范围）、消息削峰填谷（对于突然到来的大量请求，您可以配置流控规则，以稳定的速度逐步处理这些请求，从而避免流量突刺造成系统负载过高）、集群流量控制、实时熔断下游不可用应用等&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;完备的实时监控：&lt;/strong&gt; Sentinel 同时提供实时的监控功能。您可以在控制台中看到接入应用的单台机器秒级数据，甚至 500 台以下规模的集群的汇总运行情况&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;广泛的开源生态：&lt;/strong&gt; Sentinel 提供开箱即用的与其它开源框架 / 库的整合模块，例如与 Spring Cloud、Dubbo、gRPC 的整合。您只需要引入相应的依赖并进行简单的配置即可快速地接入 Sentinel&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;完善的 SPI 扩展点：&lt;/strong&gt; Sentinel 提供简单易用、完善的 SPI 扩展接口。您可以通过实现扩展接口来快速地定制逻辑。例如定制规则管理、适配动态数据源等&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;Sentinel 的主要特性&lt;/h3&gt;
&lt;p&gt;![](Sentinel 的主要特性.png)&lt;/p&gt;
&lt;h3&gt;Sentinel 的开源生态&lt;/h3&gt;
&lt;p&gt;![](Sentinel 的开源生态.png)&lt;/p&gt;
&lt;h3&gt;Sentinel 的组成&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;核心库（Java 客户端）：&lt;/strong&gt; 不依赖任何框架 / 库，能够运行于所有 Java 运行时环境，同时对 Dubbo / Spring Cloud 等框架也有较好的支持&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;控制台（Dashboard）：&lt;/strong&gt; 基于 Spring Boot 开发，打包后可以直接运行，不需要额外的 Tomcat 等应用容器&lt;/li&gt;
&lt;/ul&gt;
&lt;h1&gt;Sentinel 控制台&lt;/h1&gt;
&lt;h2&gt;概述&lt;/h2&gt;
&lt;p&gt;Sentinel 提供一个轻量级的开源控制台，它提供机器发现以及健康情况管理、监控（单机和集群），规则管理和推送的功能。另外，鉴权在生产环境中也必不可少。这里，我们将会详细讲述如何通过简单的步骤就可以使用这些功能。Sentinel 控制台最少应该包含如下功能&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;查看机器列表以及健康情况：&lt;/strong&gt; 收集 Sentinel 客户端发送的心跳包，用于判断机器是否在线。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;监控 (单机和集群聚合)：&lt;/strong&gt; 通过 Sentinel 客户端暴露的监控 API，定期拉取并且聚合应用监控信息，最终可以实现秒级的实时监控。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;规则管理和推送：&lt;/strong&gt; 统一管理推送规则。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;鉴权：&lt;/strong&gt; 生产环境中鉴权非常重要。这里每个开发者需要根据自己的实际情况进行定制。&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;启动控制台&lt;/h2&gt;
&lt;h3&gt;获取&lt;/h3&gt;
&lt;p&gt;您可以从 &lt;a href=&quot;http://www.qfdmy.com/wp-content/themes/quanbaike/go.php?url=aHR0cHM6Ly9naXRodWIuY29tL2FsaWJhYmEvU2VudGluZWwvcmVsZWFzZXM=&quot;&gt;官方 GitHub Release 页面&lt;/a&gt; 页面下载最新版本的控制台 JAR 包。&lt;/p&gt;
&lt;h3&gt;启动&lt;/h3&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;注意：&lt;/strong&gt; 启动 Sentinel 控制台需要 JDK 版本为 1.8 及以上版本&lt;/p&gt;
&lt;/blockquote&gt;
&lt;pre&gt;&lt;code&gt;java -Dserver.port=8080 -Dcsp.sentinel.dashboard.server=localhost:8080 -Dproject.name=sentinel-dashboard -jar sentinel-dashboard.jar
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;其中 &lt;code&gt;-Dserver.port=8080&lt;/code&gt; 用于指定 Sentinel 控制台端口为 &lt;code&gt;8080&lt;/code&gt;&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;注意：&lt;/strong&gt; 从 Sentinel 1.6.0 起，Sentinel 控制台引入基本的 &lt;strong&gt;登录&lt;/strong&gt; 功能，&lt;strong&gt;默认用户名和密码都是 sentinel&lt;/strong&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h3&gt;鉴权&lt;/h3&gt;
&lt;p&gt;用户可以通过如下参数进行配置&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;-Dsentinel.dashboard.auth.username=sentinel&lt;/code&gt; 用于指定控制台的登录用户名为 sentinel&lt;/li&gt;
&lt;li&gt;&lt;code&gt;-Dsentinel.dashboard.auth.password=123456&lt;/code&gt; 用于指定控制台的登录密码为 123456；如果省略这两个参数，默认用户和密码均为 sentinel&lt;/li&gt;
&lt;li&gt;&lt;code&gt;-Dserver.servlet.session.timeout=7200&lt;/code&gt; 用于指定 Spring Boot 服务端 session 的过期时间，如 7200 表示 7200 秒；60m 表示 60 分钟，默认为 30 分钟&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;验证安装是否成功&lt;/h2&gt;
&lt;p&gt;通过浏览器访问 &lt;a href=&quot;http://www.qfdmy.com/wp-content/themes/quanbaike/go.php?url=aHR0cDovL2xvY2FsaG9zdDo4MDgwLyMvbG9naW4=&quot;&gt;http://localhost:8080/#/login&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;详细说明请参考 &lt;a href=&quot;http://www.qfdmy.com/wp-content/themes/quanbaike/go.php?url=aHR0cHM6Ly9naXRodWIuY29tL2FsaWJhYmEvU2VudGluZWwvd2lraQ==&quot;&gt;官方 Sentinel Wiki&lt;/a&gt;&lt;/p&gt;
&lt;h1&gt;Sentinel 客户端接入&lt;/h1&gt;
&lt;h2&gt;POM&lt;/h2&gt;
&lt;p&gt;如果要在您的项目中引入 Sentinel，需要增加 &lt;code&gt;org.springframework.cloud:spring-cloud-starter-alibaba-sentinel&lt;/code&gt; Starter POM&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&amp;lt;dependency&amp;gt;
    &amp;lt;groupId&amp;gt;org.springframework.cloud&amp;lt;/groupId&amp;gt;
    &amp;lt;artifactId&amp;gt;spring-cloud-starter-alibaba-sentinel&amp;lt;/artifactId&amp;gt;
&amp;lt;/dependency&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;修改配置文件&lt;/h2&gt;
&lt;p&gt;在 Nacos 控制台修改配置文件，增加对 Sentinel 的支持&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;spring:
  application:
    # 服务名
    name: service-consumer
  cloud:
    nacos:
      discovery:
        # 服务注册中心
        server-addr: 192.168.141.132:8848
      config:
        # 服务配置中心
        server-addr: 192.168.141.132:8848
    # 熔断限流
    sentinel:
      transport:
        dashboard: localhost:8888
# 开启 Feign 对 Sentinel 的支持
feign:
  sentinel:
    enabled: true
server:
  # 服务端口
  port: 8080
management:
  # 端点检查（健康检查）
  endpoints:
    web:
      exposure:
        include: &quot;*&quot;
user:
  name: &quot;灶门祢豆子&quot;
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;配置熔断类&lt;/h2&gt;
&lt;p&gt;编写一个 Feign 接口的实现类并增加 &lt;code&gt;@Component&lt;/code&gt; 注解&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;package com.funtl.spring.cloud.alibaba.consumer.service.fallback;
import com.funtl.spring.cloud.alibaba.consumer.service.EchoService;
import org.springframework.stereotype.Component;
@Component
public class EchoServiceFallback implements EchoService {
    @Override
    public String echo(String string) {
        return &quot;echo fallback&quot;;
    }
    @Override
    public String lb() {
        return &quot;lb fallback&quot;;
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;修改 Feign 接口&lt;/h2&gt;
&lt;p&gt;在 &lt;code&gt;@FeignClient&lt;/code&gt; 接口上增加 &lt;code&gt;fallback&lt;/code&gt; 属性指定熔断类即可&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;package com.funtl.spring.cloud.alibaba.consumer.service;
import com.funtl.spring.cloud.alibaba.consumer.service.fallback.EchoServiceFallback;
import org.springframework.cloud.openfeign.FeignClient;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
@FeignClient(value = &quot;service-provider&quot;, fallback = EchoServiceFallback.class)
public interface EchoService {
    @GetMapping(value = &quot;/echo/{string}&quot;)
    String echo(@PathVariable(&quot;string&quot;) String string);
    @GetMapping(value = &quot;/lb&quot;)
    String lb();
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;测试熔断&lt;/h2&gt;
&lt;p&gt;启动 &lt;code&gt;service-consumer&lt;/code&gt; 服务并停止 &lt;code&gt;service-provider&lt;/code&gt; 服务，通过浏览器访问 &lt;a href=&quot;http://www.qfdmy.com/wp-content/themes/quanbaike/go.php?url=aHR0cDovL2xvY2FsaG9zdDo4MDgwL2ZlaWduL2VjaG8vaGk=&quot;&gt;http://localhost:8080/feign/echo/hi&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;%E6%B5%8B%E8%AF%95%E7%86%94%E6%96%AD.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;注意：&lt;/strong&gt; 必须至少请求过一次才能在 Sentinel 控制台看到对应的服务&lt;/p&gt;
&lt;/blockquote&gt;
</content:encoded></item><item><title>SpringCloud Nacos：注册中心与配置中心</title><link>https://tangtangit.com/posts/springcloud-05-nacos/</link><guid isPermaLink="true">https://tangtangit.com/posts/springcloud-05-nacos/</guid><description>全面讲解Nacos作为注册中心和配置中心的使用方法、原理架构、命名空间管理、多环境配置，以及与Eureka的对比。</description><pubDate>Sat, 14 Mar 2026 00:00:00 GMT</pubDate><content:encoded>&lt;h1&gt;nacos&lt;/h1&gt;
&lt;h2&gt;概述&lt;/h2&gt;
&lt;p&gt;Nacos 致力于帮助您发现、配置和管理微服务。Nacos 提供了一组简单易用的特性集，帮助您快速实现动态服务发现、服务配置、服务元数据及流量管理。&lt;/p&gt;
&lt;p&gt;Nacos 帮助您更敏捷和容易地构建、交付和管理微服务平台。 Nacos 是构建以 “服务” 为中心的现代应用架构 (例如微服务范式、云原生范式) 的服务基础设施。&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;http://www.qfdmy.com/wp-content/themes/quanbaike/go.php?url=aHR0cHM6Ly9uYWNvcy5pby96aC1jbi9pbmRleC5odG1s&quot;&gt;Nacos 官网&lt;/a&gt;&lt;/p&gt;
&lt;h2&gt;关键特性&lt;/h2&gt;
&lt;h3&gt;服务发现和服务健康监测&lt;/h3&gt;
&lt;p&gt;Nacos 支持基于 DNS 和基于 RPC 的服务发现。服务提供者使用 原生 SDK、OpenAPI、或一个 独立的 Agent TODO 注册 Service 后，服务消费者可以使用 DNS TODO 或 HTTP&amp;amp;API 查找和发现服务。&lt;/p&gt;
&lt;p&gt;Nacos 提供对服务的实时的健康检查，阻止向不健康的主机或服务实例发送请求。Nacos 支持传输层 (PING 或 TCP) 和应用层 (如 HTTP、MySQL、用户自定义）的健康检查。 对于复杂的云环境和网络拓扑环境中（如 VPC、边缘网络等）服务的健康检查，Nacos 提供了 agent 上报模式和服务端主动检测 2 种健康检查模式。Nacos 还提供了统一的健康检查仪表盘，帮助您根据健康状态管理服务的可用性及流量。&lt;/p&gt;
&lt;h3&gt;动态配置服务&lt;/h3&gt;
&lt;p&gt;动态配置服务可以让您以中心化、外部化和动态化的方式管理所有环境的应用配置和服务配置。动态配置消除了配置变更时重新部署应用和服务的需要，让配置管理变得更加高效和敏捷。配置中心化管理让实现无状态服务变得更简单，让服务按需弹性扩展变得更容易。&lt;/p&gt;
&lt;p&gt;Nacos 提供了一个简洁易用的 UI 帮助您管理所有的服务和应用的配置。Nacos 还提供包括配置版本跟踪、金丝雀发布、一键回滚配置以及客户端配置更新状态跟踪在内的一系列开箱即用的配置管理特性，帮助您更安全地在生产环境中管理配置变更和降低配置变更带来的风险。&lt;/p&gt;
&lt;h3&gt;动态 DNS 服务&lt;/h3&gt;
&lt;p&gt;动态 DNS 服务支持权重路由，让您更容易地实现中间层负载均衡、更灵活的路由策略、流量控制以及数据中心内网的简单 DNS 解析服务。动态 DNS 服务还能让您更容易地实现以 DNS 协议为基础的服务发现，以帮助您消除耦合到厂商私有服务发现 API 上的风险。&lt;/p&gt;
&lt;p&gt;Nacos 提供了一些简单的 DNS APIs TODO 帮助您管理服务的关联域名和可用的 IP:PORT 列表.&lt;/p&gt;
&lt;h3&gt;服务及其元数据管理&lt;/h3&gt;
&lt;p&gt;Nacos 能让您从微服务平台建设的视角管理数据中心的所有服务及元数据，包括管理服务的描述、生命周期、服务的静态依赖分析、服务的健康状态、服务的流量管理、路由及安全策略、服务的 SLA 以及最首要的 metrics 统计数据。&lt;/p&gt;
&lt;h3&gt;服务 (Service)&lt;/h3&gt;
&lt;p&gt;服务是指一个或一组软件功能（例如特定信息的检索或一组操作的执行），其目的是不同的客户端可以为不同的目的重用（例如通过跨进程的网络调用）。Nacos 支持主流的服务生态，如 Kubernetes Service、gRPC|Dubbo RPC Service 或者 Spring Cloud RESTful Service.&lt;/p&gt;
&lt;h3&gt;服务注册中心 (Service Registry)&lt;/h3&gt;
&lt;p&gt;服务注册中心，它是服务，其实例及元数据的数据库。服务实例在启动时注册到服务注册表，并在关闭时注销。服务和路由器的客户端查询服务注册表以查找服务的可用实例。服务注册中心可能会调用服务实例的健康检查 API 来验证它是否能够处理请求。&lt;/p&gt;
&lt;h3&gt;服务元数据 (Service Metadata)&lt;/h3&gt;
&lt;p&gt;服务元数据是指包括服务端点 (endpoints)、服务标签、服务版本号、服务实例权重、路由规则、安全策略等描述服务的数据&lt;/p&gt;
&lt;h3&gt;服务提供方 (Service Provider)&lt;/h3&gt;
&lt;p&gt;是指提供可复用和可调用服务的应用方&lt;/p&gt;
&lt;h3&gt;服务消费方 (Service Consumer)&lt;/h3&gt;
&lt;p&gt;是指会发起对某个服务调用的应用方&lt;/p&gt;
&lt;h3&gt;配置 (Configuration)&lt;/h3&gt;
&lt;p&gt;在系统开发过程中通常会将一些需要变更的参数、变量等从代码中分离出来独立管理，以独立的配置文件的形式存在。目的是让静态的系统工件或者交付物（如 WAR，JAR 包等）更好地和实际的物理运行环境进行适配。配置管理一般包含在系统部署的过程中，由系统管理员或者运维人员完成这个步骤。配置变更是调整系统运行时的行为的有效手段之一。&lt;/p&gt;
&lt;h3&gt;配置管理 (Configuration Management)&lt;/h3&gt;
&lt;p&gt;在数据中心中，系统中所有配置的编辑、存储、分发、变更管理、历史版本管理、变更审计等所有与配置相关的活动统称为配置管理。&lt;/p&gt;
&lt;h3&gt;名字服务 (Naming Service)&lt;/h3&gt;
&lt;p&gt;提供分布式系统中所有对象 (Object)、实体 (Entity) 的 “名字” 到关联的元数据之间的映射管理服务，例如 ServiceName -&amp;gt; Endpoints Info, Distributed Lock Name -&amp;gt; Lock Owner/Status Info, DNS Domain Name -&amp;gt; IP List, 服务发现和 DNS 就是名字服务的 2 大场景。&lt;/p&gt;
&lt;h3&gt;配置服务 (Configuration Service)&lt;/h3&gt;
&lt;p&gt;&lt;img src=&quot;nacos%E9%80%BB%E8%BE%91%E6%9E%B6%E6%9E%84.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;在服务或者应用运行过程中，提供动态配置或者元数据以及配置管理的服务提供者。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;服务管理：&lt;/strong&gt; 实现服务 CRUD，域名 CRUD，服务健康状态检查，服务权重管理等功能&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;配置管理：&lt;/strong&gt; 实现配置管 CRUD，版本管理，灰度管理，监听管理，推送轨迹，聚合数据等功能&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;元数据管理：&lt;/strong&gt; 提供元数据 CURD 和打标能力&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;插件机制：&lt;/strong&gt; 实现三个模块可分可合能力，实现扩展点 SPI 机制&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;事件机制：&lt;/strong&gt; 实现异步化事件通知，sdk 数据变化异步通知等逻辑&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;日志模块：&lt;/strong&gt; 管理日志分类，日志级别，日志可移植性（尤其避免冲突），日志格式，异常码 + 帮助文档&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;回调机制：&lt;/strong&gt; sdk 通知数据，通过统一的模式回调用户处理。接口和数据结构需要具备可扩展性&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;寻址模式：&lt;/strong&gt; 解决 ip，域名，nameserver、广播等多种寻址模式，需要可扩展&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;推送通道：&lt;/strong&gt; 解决 server 与存储、server 间、server 与 sdk 间推送性能问题&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;容量管理：&lt;/strong&gt; 管理每个租户，分组下的容量，防止存储被写爆，影响服务可用性&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;流量管理：&lt;/strong&gt; 按照租户，分组等多个维度对请求频率，长链接个数，报文大小，请求流控进行控制&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;缓存机制：&lt;/strong&gt; 容灾目录，本地缓存，server 缓存机制。容灾目录使用需要工具&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;启动模式：&lt;/strong&gt; 按照单机模式，配置模式，服务模式，dns 模式，或者 all 模式，启动不同的程序 + UI&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;一致性协议：&lt;/strong&gt; 解决不同数据，不同一致性要求情况下，不同一致性机制&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;存储模块：&lt;/strong&gt; 解决数据持久化、非持久化存储，解决数据分片问题&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Nameserver：&lt;/strong&gt; 解决 namespace 到 clusterid 的路由问题，解决用户环境与 nacos 物理环境映射问题&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;CMDB：&lt;/strong&gt; 解决元数据存储，与三方 cmdb 系统对接问题，解决应用，人，资源关系&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Metrics：&lt;/strong&gt; 暴露标准 metrics 数据，方便与三方监控系统打通&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Trace：&lt;/strong&gt; 暴露标准 trace，方便与 SLA 系统打通，日志白平化，推送轨迹等能力，并且可以和计量计费系统打通&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;接入管理：&lt;/strong&gt; 相当于阿里云开通服务，分配身份、容量、权限过程&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;用户管理：&lt;/strong&gt; 解决用户管理，登录，sso 等问题&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;权限管理：&lt;/strong&gt; 解决身份识别，访问控制，角色管理等问题&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;审计系统：&lt;/strong&gt; 扩展接口方便与不同公司审计系统打通&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;通知系统：&lt;/strong&gt; 核心数据变更，或者操作，方便通过 SMS 系统打通，通知到对应人数据变更&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;OpenAPI：&lt;/strong&gt; 暴露标准 Rest 风格 HTTP 接口，简单易用，方便多语言集成&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Console：&lt;/strong&gt; 易用控制台，做服务管理、配置管理等操作&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;SDK：&lt;/strong&gt; 多语言 sdk&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Agent：&lt;/strong&gt; dns-f 类似模式，或者与 mesh 等方案集成&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;CLI：&lt;/strong&gt; 命令行对产品进行轻量化管理，像 git 一样好用&lt;/li&gt;
&lt;/ul&gt;
&lt;h1&gt;创建项目工程&lt;/h1&gt;
&lt;h2&gt;工程：spring_cloud_alibaba&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;&amp;lt;?xml version=&quot;1.0&quot; encoding=&quot;UTF-8&quot;?&amp;gt;
&amp;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;&amp;gt;
    &amp;lt;modelVersion&amp;gt;4.0.0&amp;lt;/modelVersion&amp;gt;
    &amp;lt;parent&amp;gt;
        &amp;lt;groupId&amp;gt;org.springframework.boot&amp;lt;/groupId&amp;gt;
        &amp;lt;artifactId&amp;gt;spring-boot-starter-parent&amp;lt;/artifactId&amp;gt;
        &amp;lt;version&amp;gt;2.1.6.RELEASE&amp;lt;/version&amp;gt;
        &amp;lt;relativePath/&amp;gt; &amp;lt;!-- lookup parent from repository --&amp;gt;
    &amp;lt;/parent&amp;gt;
    &amp;lt;groupId&amp;gt;com.eim&amp;lt;/groupId&amp;gt;
    &amp;lt;artifactId&amp;gt;spring_cloud_alibaba&amp;lt;/artifactId&amp;gt;
    &amp;lt;version&amp;gt;0.0.1-SNAPSHOT&amp;lt;/version&amp;gt;
    &amp;lt;packaging&amp;gt;pom&amp;lt;/packaging&amp;gt;
    &amp;lt;url&amp;gt;http://www.funtl.com&amp;lt;/url&amp;gt;
    &amp;lt;modules&amp;gt;
        &amp;lt;module&amp;gt;hello-spring-cloud-alibaba-dependencies&amp;lt;/module&amp;gt;
    &amp;lt;/modules&amp;gt;
    &amp;lt;properties&amp;gt;
        &amp;lt;java.version&amp;gt;1.8&amp;lt;/java.version&amp;gt;
        &amp;lt;maven.compiler.source&amp;gt;${java.version}&amp;lt;/maven.compiler.source&amp;gt;
        &amp;lt;maven.compiler.target&amp;gt;${java.version}&amp;lt;/maven.compiler.target&amp;gt;
        &amp;lt;project.build.sourceEncoding&amp;gt;UTF-8&amp;lt;/project.build.sourceEncoding&amp;gt;
        &amp;lt;project.reporting.outputEncoding&amp;gt;UTF-8&amp;lt;/project.reporting.outputEncoding&amp;gt;
    &amp;lt;/properties&amp;gt;
    &amp;lt;licenses&amp;gt;
        &amp;lt;license&amp;gt;
            &amp;lt;name&amp;gt;Apache 2.0&amp;lt;/name&amp;gt;
            &amp;lt;url&amp;gt;https://www.apache.org/licenses/LICENSE-2.0.txt&amp;lt;/url&amp;gt;
        &amp;lt;/license&amp;gt;
    &amp;lt;/licenses&amp;gt;
    &amp;lt;developers&amp;gt;
        &amp;lt;developer&amp;gt;
            &amp;lt;id&amp;gt;liwemin&amp;lt;/id&amp;gt;
            &amp;lt;name&amp;gt;Lusifer Lee&amp;lt;/name&amp;gt;
            &amp;lt;email&amp;gt;lee.lusifer@gmail.com&amp;lt;/email&amp;gt;
        &amp;lt;/developer&amp;gt;
    &amp;lt;/developers&amp;gt;
    &amp;lt;!-- 项目统一管理的 --&amp;gt;
    &amp;lt;dependencyManagement&amp;gt;
        &amp;lt;dependencies&amp;gt;
            &amp;lt;dependency&amp;gt;
                &amp;lt;groupId&amp;gt;com.eim&amp;lt;/groupId&amp;gt;
                &amp;lt;artifactId&amp;gt;hello-spring-cloud-alibaba-dependencies&amp;lt;/artifactId&amp;gt;
                &amp;lt;version&amp;gt;${project.version}&amp;lt;/version&amp;gt;
                &amp;lt;type&amp;gt;pom&amp;lt;/type&amp;gt;
                &amp;lt;scope&amp;gt;import&amp;lt;/scope&amp;gt;
            &amp;lt;/dependency&amp;gt;
        &amp;lt;/dependencies&amp;gt;
    &amp;lt;/dependencyManagement&amp;gt;
    &amp;lt;profiles&amp;gt;
        &amp;lt;profile&amp;gt;
            &amp;lt;id&amp;gt;default&amp;lt;/id&amp;gt;
            &amp;lt;activation&amp;gt;
                &amp;lt;activeByDefault&amp;gt;true&amp;lt;/activeByDefault&amp;gt;
            &amp;lt;/activation&amp;gt;
            &amp;lt;properties&amp;gt;
                &amp;lt;spring-javaformat.version&amp;gt;0.0.12&amp;lt;/spring-javaformat.version&amp;gt;
            &amp;lt;/properties&amp;gt;
            &amp;lt;build&amp;gt;
                &amp;lt;plugins&amp;gt;
                    &amp;lt;plugin&amp;gt;
                        &amp;lt;groupId&amp;gt;io.spring.javaformat&amp;lt;/groupId&amp;gt;
                        &amp;lt;artifactId&amp;gt;spring-javaformat-maven-plugin&amp;lt;/artifactId&amp;gt;
                        &amp;lt;version&amp;gt;${spring-javaformat.version}&amp;lt;/version&amp;gt;
                    &amp;lt;/plugin&amp;gt;
                    &amp;lt;plugin&amp;gt;
                        &amp;lt;groupId&amp;gt;org.apache.maven.plugins&amp;lt;/groupId&amp;gt;
                        &amp;lt;artifactId&amp;gt;maven-surefire-plugin&amp;lt;/artifactId&amp;gt;
                        &amp;lt;configuration&amp;gt;
                            &amp;lt;includes&amp;gt;
                                &amp;lt;include&amp;gt;**/*Tests.java&amp;lt;/include&amp;gt;
                            &amp;lt;/includes&amp;gt;
                            &amp;lt;excludes&amp;gt;
                                &amp;lt;exclude&amp;gt;**/Abstract*.java&amp;lt;/exclude&amp;gt;
                            &amp;lt;/excludes&amp;gt;
                            &amp;lt;systemPropertyVariables&amp;gt;
                                &amp;lt;java.security.egd&amp;gt;file:/dev/./urandom&amp;lt;/java.security.egd&amp;gt;
                                &amp;lt;java.awt.headless&amp;gt;true&amp;lt;/java.awt.headless&amp;gt;
                            &amp;lt;/systemPropertyVariables&amp;gt;
                        &amp;lt;/configuration&amp;gt;
                    &amp;lt;/plugin&amp;gt;
                    &amp;lt;plugin&amp;gt;
                        &amp;lt;groupId&amp;gt;org.apache.maven.plugins&amp;lt;/groupId&amp;gt;
                        &amp;lt;artifactId&amp;gt;maven-enforcer-plugin&amp;lt;/artifactId&amp;gt;
                        &amp;lt;executions&amp;gt;
                            &amp;lt;execution&amp;gt;
                                &amp;lt;id&amp;gt;enforce-rules&amp;lt;/id&amp;gt;
                                &amp;lt;goals&amp;gt;
                                    &amp;lt;goal&amp;gt;enforce&amp;lt;/goal&amp;gt;
                                &amp;lt;/goals&amp;gt;
                                &amp;lt;configuration&amp;gt;
                                    &amp;lt;rules&amp;gt;
                                        &amp;lt;bannedDependencies&amp;gt;
                                            &amp;lt;excludes&amp;gt;
                                                &amp;lt;exclude&amp;gt;commons-logging:*:*&amp;lt;/exclude&amp;gt;
                                            &amp;lt;/excludes&amp;gt;
                                            &amp;lt;searchTransitive&amp;gt;true&amp;lt;/searchTransitive&amp;gt;
                                        &amp;lt;/bannedDependencies&amp;gt;
                                    &amp;lt;/rules&amp;gt;
                                    &amp;lt;fail&amp;gt;true&amp;lt;/fail&amp;gt;
                                &amp;lt;/configuration&amp;gt;
                            &amp;lt;/execution&amp;gt;
                        &amp;lt;/executions&amp;gt;
                    &amp;lt;/plugin&amp;gt;
                    &amp;lt;plugin&amp;gt;
                        &amp;lt;groupId&amp;gt;org.apache.maven.plugins&amp;lt;/groupId&amp;gt;
                        &amp;lt;artifactId&amp;gt;maven-install-plugin&amp;lt;/artifactId&amp;gt;
                        &amp;lt;configuration&amp;gt;
                            &amp;lt;skip&amp;gt;true&amp;lt;/skip&amp;gt;
                        &amp;lt;/configuration&amp;gt;
                    &amp;lt;/plugin&amp;gt;
                    &amp;lt;plugin&amp;gt;
                        &amp;lt;groupId&amp;gt;org.apache.maven.plugins&amp;lt;/groupId&amp;gt;
                        &amp;lt;artifactId&amp;gt;maven-javadoc-plugin&amp;lt;/artifactId&amp;gt;
                        &amp;lt;configuration&amp;gt;
                            &amp;lt;skip&amp;gt;true&amp;lt;/skip&amp;gt;
                        &amp;lt;/configuration&amp;gt;
                        &amp;lt;inherited&amp;gt;true&amp;lt;/inherited&amp;gt;
                    &amp;lt;/plugin&amp;gt;
                &amp;lt;/plugins&amp;gt;
            &amp;lt;/build&amp;gt;
        &amp;lt;/profile&amp;gt;
    &amp;lt;/profiles&amp;gt;
    &amp;lt;repositories&amp;gt;
        &amp;lt;repository&amp;gt;
            &amp;lt;id&amp;gt;spring-milestone&amp;lt;/id&amp;gt;
            &amp;lt;name&amp;gt;Spring Milestone&amp;lt;/name&amp;gt;
            &amp;lt;url&amp;gt;https://repo.spring.io/milestone&amp;lt;/url&amp;gt;
            &amp;lt;snapshots&amp;gt;
                &amp;lt;enabled&amp;gt;false&amp;lt;/enabled&amp;gt;
            &amp;lt;/snapshots&amp;gt;
        &amp;lt;/repository&amp;gt;
        &amp;lt;repository&amp;gt;
            &amp;lt;id&amp;gt;spring-snapshot&amp;lt;/id&amp;gt;
            &amp;lt;name&amp;gt;Spring Snapshot&amp;lt;/name&amp;gt;
            &amp;lt;url&amp;gt;https://repo.spring.io/snapshot&amp;lt;/url&amp;gt;
            &amp;lt;snapshots&amp;gt;
                &amp;lt;enabled&amp;gt;true&amp;lt;/enabled&amp;gt;
            &amp;lt;/snapshots&amp;gt;
        &amp;lt;/repository&amp;gt;
    &amp;lt;/repositories&amp;gt;
    &amp;lt;pluginRepositories&amp;gt;
        &amp;lt;pluginRepository&amp;gt;
            &amp;lt;id&amp;gt;spring-milestone&amp;lt;/id&amp;gt;
            &amp;lt;name&amp;gt;Spring Milestone&amp;lt;/name&amp;gt;
            &amp;lt;url&amp;gt;https://repo.spring.io/milestone&amp;lt;/url&amp;gt;
            &amp;lt;snapshots&amp;gt;
                &amp;lt;enabled&amp;gt;false&amp;lt;/enabled&amp;gt;
            &amp;lt;/snapshots&amp;gt;
        &amp;lt;/pluginRepository&amp;gt;
        &amp;lt;pluginRepository&amp;gt;
            &amp;lt;id&amp;gt;spring-snapshot&amp;lt;/id&amp;gt;
            &amp;lt;name&amp;gt;Spring Snapshot&amp;lt;/name&amp;gt;
            &amp;lt;url&amp;gt;https://repo.spring.io/snapshot&amp;lt;/url&amp;gt;
            &amp;lt;snapshots&amp;gt;
                &amp;lt;enabled&amp;gt;true&amp;lt;/enabled&amp;gt;
            &amp;lt;/snapshots&amp;gt;
        &amp;lt;/pluginRepository&amp;gt;
    &amp;lt;/pluginRepositories&amp;gt;
&amp;lt;/project&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;创建统一的依赖管理&lt;/h2&gt;
&lt;blockquote&gt;
&lt;p&gt;spring-cloud-alibaba-dependencies 是工程的一个子模块。并没有继承工程&lt;/p&gt;
&lt;/blockquote&gt;
&lt;pre&gt;&lt;code&gt;&amp;lt;?xml version=&quot;1.0&quot; encoding=&quot;UTF-8&quot;?&amp;gt;
&amp;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;&amp;gt;

    &amp;lt;modelVersion&amp;gt;4.0.0&amp;lt;/modelVersion&amp;gt;

    &amp;lt;groupId&amp;gt;com.eim&amp;lt;/groupId&amp;gt;
    &amp;lt;artifactId&amp;gt;hello-spring-cloud-alibaba-dependencies&amp;lt;/artifactId&amp;gt;
    &amp;lt;version&amp;gt;0.0.1-SNAPSHOT&amp;lt;/version&amp;gt;
    &amp;lt;name&amp;gt;hello-spring-cloud-alibaba-dependencies&amp;lt;/name&amp;gt;
    &amp;lt;packaging&amp;gt;pom&amp;lt;/packaging&amp;gt;

    &amp;lt;properties&amp;gt;
        &amp;lt;spring.cloud.version&amp;gt;Greenwich.RELEASE&amp;lt;/spring.cloud.version&amp;gt;
        &amp;lt;spring.cloud.alibaba.version&amp;gt;2.1.0.RELEASE&amp;lt;/spring.cloud.alibaba.version&amp;gt;
    &amp;lt;/properties&amp;gt;

    &amp;lt;dependencyManagement&amp;gt;
        &amp;lt;dependencies&amp;gt;
            &amp;lt;dependency&amp;gt;
                &amp;lt;groupId&amp;gt;com.alibaba.cloud&amp;lt;/groupId&amp;gt;
                &amp;lt;artifactId&amp;gt;spring-cloud-alibaba-dependencies&amp;lt;/artifactId&amp;gt;
                &amp;lt;version&amp;gt;${spring.cloud.alibaba.version}&amp;lt;/version&amp;gt;
                &amp;lt;type&amp;gt;pom&amp;lt;/type&amp;gt;
                &amp;lt;scope&amp;gt;import&amp;lt;/scope&amp;gt;
            &amp;lt;/dependency&amp;gt;
            &amp;lt;dependency&amp;gt;
                &amp;lt;groupId&amp;gt;org.springframework.cloud&amp;lt;/groupId&amp;gt;
                &amp;lt;artifactId&amp;gt;spring-cloud-dependencies&amp;lt;/artifactId&amp;gt;
                &amp;lt;version&amp;gt;${spring.cloud.version}&amp;lt;/version&amp;gt;
                &amp;lt;type&amp;gt;pom&amp;lt;/type&amp;gt;
                &amp;lt;scope&amp;gt;import&amp;lt;/scope&amp;gt;
            &amp;lt;/dependency&amp;gt;
        &amp;lt;/dependencies&amp;gt;
    &amp;lt;/dependencyManagement&amp;gt;

    &amp;lt;repositories&amp;gt;
        &amp;lt;repository&amp;gt;
            &amp;lt;id&amp;gt;spring-milestone&amp;lt;/id&amp;gt;
            &amp;lt;name&amp;gt;Spring Milestone&amp;lt;/name&amp;gt;
            &amp;lt;url&amp;gt;https://repo.spring.io/milestone&amp;lt;/url&amp;gt;
            &amp;lt;snapshots&amp;gt;
                &amp;lt;enabled&amp;gt;false&amp;lt;/enabled&amp;gt;
            &amp;lt;/snapshots&amp;gt;
        &amp;lt;/repository&amp;gt;
        &amp;lt;repository&amp;gt;
            &amp;lt;id&amp;gt;spring-snapshot&amp;lt;/id&amp;gt;
            &amp;lt;name&amp;gt;Spring Snapshot&amp;lt;/name&amp;gt;
            &amp;lt;url&amp;gt;https://repo.spring.io/snapshot&amp;lt;/url&amp;gt;
            &amp;lt;snapshots&amp;gt;
                &amp;lt;enabled&amp;gt;true&amp;lt;/enabled&amp;gt;
            &amp;lt;/snapshots&amp;gt;
        &amp;lt;/repository&amp;gt;
    &amp;lt;/repositories&amp;gt;
    &amp;lt;pluginRepositories&amp;gt;
        &amp;lt;pluginRepository&amp;gt;
            &amp;lt;id&amp;gt;spring-milestone&amp;lt;/id&amp;gt;
            &amp;lt;name&amp;gt;Spring Milestone&amp;lt;/name&amp;gt;
            &amp;lt;url&amp;gt;https://repo.spring.io/milestone&amp;lt;/url&amp;gt;
            &amp;lt;snapshots&amp;gt;
                &amp;lt;enabled&amp;gt;false&amp;lt;/enabled&amp;gt;
            &amp;lt;/snapshots&amp;gt;
        &amp;lt;/pluginRepository&amp;gt;
        &amp;lt;pluginRepository&amp;gt;
            &amp;lt;id&amp;gt;spring-snapshot&amp;lt;/id&amp;gt;
            &amp;lt;name&amp;gt;Spring Snapshot&amp;lt;/name&amp;gt;
            &amp;lt;url&amp;gt;https://repo.spring.io/snapshot&amp;lt;/url&amp;gt;
            &amp;lt;snapshots&amp;gt;
                &amp;lt;enabled&amp;gt;true&amp;lt;/enabled&amp;gt;
            &amp;lt;/snapshots&amp;gt;
        &amp;lt;/pluginRepository&amp;gt;
    &amp;lt;/pluginRepositories&amp;gt;

&amp;lt;/project&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;h1&gt;安装nacos&lt;/h1&gt;
&lt;h2&gt;概述&lt;/h2&gt;
&lt;p&gt;Nacos 官方提供了 Docker 版本，方便我们快速部署，操作步骤如下&lt;/p&gt;
&lt;h2&gt;操作步骤&lt;/h2&gt;
&lt;h3&gt;Clone 项目&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;git clone https://github.com/nacos-group/nacos-docker.git
cd nacos-docker
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;单机模式&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;docker-compose -f example/standalone-mysql.yaml up -d
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;查看日志&lt;/h3&gt;
&lt;blockquote&gt;
&lt;p&gt;第一个 -f  是表示：指定yaml文件&lt;/p&gt;
&lt;/blockquote&gt;
&lt;pre&gt;&lt;code&gt;docker-compose -f example/standalone-mysql.yaml logs -f
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;nacos-docker启动失败&lt;/strong&gt;**&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;错误信息：bin/docker-startup.sh: line 95: /home/nacos/logs/start.out: Permission denied&lt;/p&gt;
&lt;p&gt;Centos7安全Selinux禁止了一些安全权限&lt;/p&gt;
&lt;p&gt;解决方法有这么三个：&lt;/p&gt;
&lt;blockquote&gt;
&lt;ol&gt;
&lt;li&gt;在docker run中加入 --privileged=true  给容器加上特定权限&lt;/li&gt;
&lt;li&gt;关闭selinux&lt;/li&gt;
&lt;li&gt;在selinux添加规则，修改挂载目录de&lt;/li&gt;
&lt;/ol&gt;
&lt;/blockquote&gt;
&lt;/blockquote&gt;
&lt;h2&gt;Nacos 控制台&lt;/h2&gt;
&lt;p&gt;http://ip:8848/nacos&lt;/p&gt;
&lt;p&gt;初始密码账号&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;账号：&lt;/strong&gt; nacos&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;密码：&lt;/strong&gt; nacos&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;概述&lt;/h2&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;服务注册中心：&lt;/strong&gt; 存储服务实例和服务负载均衡策略的数据库&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;&lt;img src=&quot;%E6%B3%A8%E5%86%8C%E4%B8%AD%E5%BF%83%E6%BC%94%E7%A4%BA.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;h2&gt;服务提供者&lt;/h2&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;服务提供方：&lt;/strong&gt; 是指提供可复用和可调用服务的应用方&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h3&gt;POM&lt;/h3&gt;
&lt;p&gt;创建一个名为 &lt;code&gt;hello-spring-cloud-alibaba-provider&lt;/code&gt; 的服务提供者项目，&lt;code&gt;pom.xml&lt;/code&gt; 配置如下：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&amp;lt;?xml version=&quot;1.0&quot; encoding=&quot;UTF-8&quot;?&amp;gt;
&amp;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;&amp;gt;
    &amp;lt;modelVersion&amp;gt;4.0.0&amp;lt;/modelVersion&amp;gt;
    &amp;lt;parent&amp;gt;
        &amp;lt;groupId&amp;gt;com.funtl&amp;lt;/groupId&amp;gt;
        &amp;lt;artifactId&amp;gt;hello-spring-cloud-alibaba&amp;lt;/artifactId&amp;gt;
        &amp;lt;version&amp;gt;0.0.1-SNAPSHOT&amp;lt;/version&amp;gt;
        &amp;lt;relativePath&amp;gt;../pom.xml&amp;lt;/relativePath&amp;gt;
    &amp;lt;/parent&amp;gt;
    &amp;lt;artifactId&amp;gt;hello-spring-cloud-alibaba-provider&amp;lt;/artifactId&amp;gt;
    &amp;lt;packaging&amp;gt;jar&amp;lt;/packaging&amp;gt;
    &amp;lt;url&amp;gt;http://www.funtl.com&amp;lt;/url&amp;gt;
    &amp;lt;inceptionYear&amp;gt;2018-Now&amp;lt;/inceptionYear&amp;gt;
    &amp;lt;licenses&amp;gt;
        &amp;lt;license&amp;gt;
            &amp;lt;name&amp;gt;Apache 2.0&amp;lt;/name&amp;gt;
            &amp;lt;url&amp;gt;https://www.apache.org/licenses/LICENSE-2.0.txt&amp;lt;/url&amp;gt;
        &amp;lt;/license&amp;gt;
    &amp;lt;/licenses&amp;gt;
    &amp;lt;developers&amp;gt;
        &amp;lt;developer&amp;gt;
            &amp;lt;id&amp;gt;liwemin&amp;lt;/id&amp;gt;
            &amp;lt;name&amp;gt;Lusifer Lee&amp;lt;/name&amp;gt;
            &amp;lt;email&amp;gt;lee.lusifer@gmail.com&amp;lt;/email&amp;gt;
        &amp;lt;/developer&amp;gt;
    &amp;lt;/developers&amp;gt;
    &amp;lt;dependencies&amp;gt;
        &amp;lt;!-- Spring Boot Begin --&amp;gt;
        &amp;lt;dependency&amp;gt;
            &amp;lt;groupId&amp;gt;org.springframework.boot&amp;lt;/groupId&amp;gt;
            &amp;lt;artifactId&amp;gt;spring-boot-starter-web&amp;lt;/artifactId&amp;gt;
        &amp;lt;/dependency&amp;gt;
        &amp;lt;dependency&amp;gt;
            &amp;lt;groupId&amp;gt;org.springframework.boot&amp;lt;/groupId&amp;gt;
            &amp;lt;artifactId&amp;gt;spring-boot-starter-actuator&amp;lt;/artifactId&amp;gt;
        &amp;lt;/dependency&amp;gt;
        &amp;lt;dependency&amp;gt;
            &amp;lt;groupId&amp;gt;org.springframework.boot&amp;lt;/groupId&amp;gt;
            &amp;lt;artifactId&amp;gt;spring-boot-starter-test&amp;lt;/artifactId&amp;gt;
            &amp;lt;scope&amp;gt;test&amp;lt;/scope&amp;gt;
        &amp;lt;/dependency&amp;gt;
        &amp;lt;!-- Spring Boot End --&amp;gt;
        &amp;lt;!-- Spring Cloud Begin --&amp;gt;
        &amp;lt;dependency&amp;gt;
            &amp;lt;groupId&amp;gt;org.springframework.cloud&amp;lt;/groupId&amp;gt;
            &amp;lt;artifactId&amp;gt;spring-cloud-starter-alibaba-nacos-discovery&amp;lt;/artifactId&amp;gt;
        &amp;lt;/dependency&amp;gt;
        &amp;lt;!-- Spring Cloud End --&amp;gt;
    &amp;lt;/dependencies&amp;gt;
    &amp;lt;build&amp;gt;
        &amp;lt;plugins&amp;gt;
            &amp;lt;plugin&amp;gt;
                &amp;lt;groupId&amp;gt;org.springframework.boot&amp;lt;/groupId&amp;gt;
                &amp;lt;artifactId&amp;gt;spring-boot-maven-plugin&amp;lt;/artifactId&amp;gt;
                &amp;lt;configuration&amp;gt;
                    &amp;lt;mainClass&amp;gt;com.funtl.spring.cloud.alibaba.provider.ProviderApplication&amp;lt;/mainClass&amp;gt;
                &amp;lt;/configuration&amp;gt;
            &amp;lt;/plugin&amp;gt;
        &amp;lt;/plugins&amp;gt;
    &amp;lt;/build&amp;gt;
&amp;lt;/project&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;application.yml&lt;/h3&gt;
&lt;p&gt;在 &lt;code&gt;application.yml&lt;/code&gt; 中配置 Nacos Server 的地址&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;spring:
  application:
    # 服务名
    name: service-provider
  cloud:
    nacos:
      discovery:
        # 服务注册中心
        server-addr: 192.168.141.132:8848
server:
  # 服务端口
  port: 8070
management:
  # 端点检查（健康检查）
  endpoints:
    web:
      exposure:
        include: &quot;*&quot;
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Application&lt;/h3&gt;
&lt;p&gt;通过 Spring Cloud 原生注解 &lt;code&gt;@EnableDiscoveryClient&lt;/code&gt; 开启服务注册发现功能&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;package com.funtl.spring.cloud.alibaba.provider;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.client.discovery.EnableDiscoveryClient;
@SpringBootApplication
@EnableDiscoveryClient
public class ProviderApplication {
    public static void main(String[] args) {
        SpringApplication.run(ProviderApplication.class, args);
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Controller&lt;/h3&gt;
&lt;p&gt;编写一个 &lt;code&gt;RestController&lt;/code&gt; 提供 RESTFul API 用于测试提供者&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;package com.funtl.spring.cloud.alibaba.provider.controller;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RestController;
@RestController
public class EchoController {
    @GetMapping(value = &quot;/echo/{string}&quot;)
    public String echo(@PathVariable String string) {
        return &quot;Hello Nacos Provider &quot; + string;
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;img src=&quot;%E6%9C%8D%E5%8A%A1%E5%88%97%E8%A1%A8.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;h1&gt;安装tar.gz包nacos&lt;/h1&gt;
&lt;h2&gt;1、 下载&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;地址：
https://github.com/alibaba/nacos/releases

nacos-server-2.0.0.tar.gz

解压
tar -zxvf nacos-server-2.0.0.tar.gz
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;2、启动，停止&lt;/h2&gt;
&lt;blockquote&gt;
&lt;p&gt;bin目录下 startup.sh 、shutdown.sh&lt;/p&gt;
&lt;/blockquote&gt;
&lt;pre&gt;&lt;code&gt;Linux/Unix/Mac
启动命令(standalone代表着单机模式运行，非集群模式):

启动
sh startup.sh -m standalone

关闭
sh shutdown.sh -m standalone
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;3、Nacos支持三种部署模式&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;单机模式 - 用于测试和单机试用。&lt;/li&gt;
&lt;li&gt;集群模式 - 用于生产环境，确保高可用。&lt;/li&gt;
&lt;li&gt;多集群模式 - 用于多数据中心场景。&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;4、配置mysql&lt;/h2&gt;
&lt;p&gt;单机模式支持mysql&lt;/p&gt;
&lt;p&gt;在0.7版本之前，在单机模式时nacos使用嵌入式数据库实现数据的存储，不方便观察数据存储的基本情况。0.7版本增加了支持mysql数据源能力，具体的操作步骤：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;1.安装数据库，版本要求：5.6.5+&lt;/li&gt;
&lt;li&gt;2.初始化mysql数据库，数据库初始化文件：confi文件夹下nacos-mysql.sql&lt;/li&gt;
&lt;li&gt;3.修改conf/application.properties文件，增加支持mysql数据源配置（目前只支持mysql），添加mysql数据源的url、用户名和密码。&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;spring.datasource.platform=mysql

db.num=1
db.url.0=jdbc:mysql://11.162.196.16:3306/nacos_devtest?characterEncoding=utf8&amp;amp;connectTimeout=1000&amp;amp;socketTimeout=3000&amp;amp;autoReconnect=true
db.user=nacos_devtest
db.password=youdontknow
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;再以单机模式启动nacos，nacos所有写嵌入式数据库的数据都写到了mysql&lt;/p&gt;
&lt;h1&gt;Nacos Feign 客户端&lt;/h1&gt;
&lt;h2&gt;什么是 Feign&lt;/h2&gt;
&lt;p&gt;Feign 是一个声明式的伪 HTTP 客户端，它使得写 HTTP 客户端变得更简单。使用 Feign，只需要创建一个接口并注解。它具有可插拔的注解特性，可使用 Feign 注解和 JAX-RS 注解。Feign 支持可插拔的编码器和解码器。Feign 默认集成了 Ribbon，Nacos 也很好的兼容了 Feign，默认实现了负载均衡的效果&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Feign 采用的是基于接口的注解&lt;/li&gt;
&lt;li&gt;Feign 整合了 Ribbon&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;POM&lt;/h2&gt;
&lt;p&gt;在 &lt;code&gt;hello-spring-cloud-alibaba-consumer&lt;/code&gt; 项目中增加 &lt;code&gt;org.springframework.cloud:spring-cloud-starter-openfeign&lt;/code&gt; 依赖&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&amp;lt;dependency&amp;gt;
    &amp;lt;groupId&amp;gt;org.springframework.cloud&amp;lt;/groupId&amp;gt;
    &amp;lt;artifactId&amp;gt;spring-cloud-starter-openfeign&amp;lt;/artifactId&amp;gt;
&amp;lt;/dependency&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Application&lt;/h2&gt;
&lt;p&gt;通过 &lt;code&gt;@EnableFeignClients&lt;/code&gt; 注解开启 Feign 功能&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;package com.funtl.spring.cloud.alibaba.consumer;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.client.discovery.EnableDiscoveryClient;
import org.springframework.cloud.openfeign.EnableFeignClients;
@SpringBootApplication
@EnableDiscoveryClient
@EnableFeignClients
public class ConsumerApplication {	
    public static void main(String[] args) {
        SpringApplication.run(ConsumerApplication.class, args);
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Service&lt;/h2&gt;
&lt;p&gt;创建业务结构，通过 &lt;code&gt;@FeignClient(&quot;服务名&quot;)&lt;/code&gt; 注解来指定调用哪个服务&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;package com.funtl.spring.cloud.alibaba.consumer.service;
import org.springframework.cloud.openfeign.FeignClient;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
@FeignClient(value = &quot;service-provider&quot;)
public interface EchoService {
    @GetMapping(value = &quot;/echo/{string}&quot;)
    String echo(@PathVariable(&quot;string&quot;) String string);
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Controller&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;package com.funtl.spring.cloud.alibaba.consumer.controller;
import com.funtl.spring.cloud.alibaba.consumer.service.EchoService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RestController;
@RestController
public class TestEchoController {
    @Autowired
    private EchoService echoService;
    @GetMapping(value = &quot;/feign/echo/{str}&quot;)
    public String echo(@PathVariable String str) {
        return echoService.echo(str);
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;验证是否成功&lt;/h2&gt;
&lt;p&gt;通过浏览器访问 &lt;a href=&quot;http://www.qfdmy.com/wp-content/themes/quanbaike/go.php?url=aHR0cDovL2xvY2FsaG9zdDo4MDgwL2ZlaWduL2VjaG8vaGk=&quot;&gt;http://localhost:8080/feign/echo/hi&lt;/a&gt;&lt;/p&gt;
&lt;h2&gt;配置负载均衡&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;修改 &lt;code&gt;service-provider&lt;/code&gt; 服务的端口号如 8071，并启动多个实例，IDEA 中依次点击 &lt;strong&gt;Run&lt;/strong&gt; -&amp;gt; &lt;strong&gt;Edit Configurations&lt;/strong&gt; 并勾选 &lt;strong&gt;Allow parallel run&lt;/strong&gt; 以允许 IDEA 多实例运行项目&lt;/li&gt;
&lt;li&gt;再次启动 &lt;code&gt;service-provider&lt;/code&gt; 实例，IDEA Spring Boot 面板可以看到&lt;/li&gt;
&lt;li&gt;Nacos Server 控制台可以看到 &lt;code&gt;service-provider&lt;/code&gt; 有 2 个实例&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;服务：&lt;/strong&gt; 通过预定义接口网络访问的提供给客户端的软件功能
&lt;strong&gt;服务名：&lt;/strong&gt; 服务提供的标识，通过该标识可以唯一确定其指代的服务
&lt;strong&gt;服务分组：&lt;/strong&gt; 不同的服务可以归类到同一分组
&lt;strong&gt;虚拟集群：&lt;/strong&gt; 同一个服务下的所有服务实例组成一个默认集群，集群可以被进一步按需求划分，划分的单位可以是虚拟集群
&lt;strong&gt;实例：&lt;/strong&gt; 提供一个或多个服务的具有可访问网络地址（IP:Port）的进程
&lt;strong&gt;权重：&lt;/strong&gt; 实例级别的配置。权重为浮点数。权重越大，分配给该实例的流量越大
&lt;strong&gt;健康保护阈值：&lt;/strong&gt; 为了防止因过多实例 (Instance) 不健康导致流量全部流向健康实例 (Instance) ，继而造成流量压力把健康 健康实例 (Instance) 压垮并形成雪崩效应，应将健康保护阈值定义为一个 0 到 1 之间的浮点数。当域名健康实例 (Instance) 占总服务实例 (Instance) 的比例小于该值时，无论实例 (Instance) 是否健康，都会将这个实例 (Instance) 返回给客户端。这样做虽然损失了一部分流量，但是保证了集群的剩余健康实例 (Instance) 能正常工作&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;在 &lt;code&gt;service-provider&lt;/code&gt; 项目的 &lt;code&gt;EchoController&lt;/code&gt; 中增加测试方法&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;@Value(&quot;${server.port}&quot;)
private String port;
@GetMapping(value = &quot;/lb&quot;)
public String lb() {
    return &quot;Hello Nacos Provider i am from port: &quot; + port;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;在 &lt;code&gt;service-consumer&lt;/code&gt; 项目的 &lt;code&gt;EchoService&lt;/code&gt; 中增加测试方法&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;@GetMapping(value = &quot;/lb&quot;)String lb();
&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;在 &lt;code&gt;service-consumer&lt;/code&gt; 项目的 &lt;code&gt;TestEchoController&lt;/code&gt; 中增加测试方法&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;@GetMapping(value = &quot;/lb&quot;)public String lb() {    return echoService.lb();}
&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;重启服务，通过浏览器访问 &lt;a href=&quot;http://www.qfdmy.com/wp-content/themes/quanbaike/go.php?url=aHR0cDovL2xvY2FsaG9zdDo4MDgwL2xi&quot;&gt;http://localhost:8080/lb&lt;/a&gt; ，反复刷新浏览器，浏览器交替显示&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;Hello Nacos Provider i am from port: 8070Hello Nacos Provider i am from port: 8071
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;附：扩展阅读&lt;/h2&gt;
&lt;h3&gt;常见负载均衡策略&lt;/h3&gt;
&lt;p&gt;负载主机可以提供很多种负载均衡方法，也就是我们常说的调度方法或算法&lt;/p&gt;
&lt;h4&gt;轮循&lt;/h4&gt;
&lt;p&gt;&lt;strong&gt;Round Robin：&lt;/strong&gt; 这种方法会将收到的请求循环分配到服务器集群中的每台机器，即有效服务器。如果使用这种方式，所有的标记进入虚拟服务的服务器应该有相近的资源容量 以及负载形同的应用程序。如果所有的服务器有相同或者相近的性能那么选择这种方式会使服务器负载形同。基于这个前提，轮循调度是一个简单而有效的分配请求 的方式。然而对于服务器不同的情况，选择这种方式就意味着能力比较弱的服务器也会在下一轮循环中接受轮循，即使这个服务器已经不能再处理当前这个请求了。 这可能导致能力较弱的服务器超载。&lt;/p&gt;
&lt;h4&gt;加权轮循&lt;/h4&gt;
&lt;p&gt;&lt;strong&gt;Weighted Round Robin：&lt;/strong&gt; 这种算法解决了简单轮循调度算法的缺点：传入的请求按顺序被分配到集群中服务器，但是会考虑提前为每台服务器分配的权重。管理员只是简单的通过服务 器的处理能力来定义各台服务器的权重。例如，能力最强的服务器 A 给的权重是 100，同时能力最低的服务器给的权重是 50。这意味着在服务器 B 接收到第一个 请求之前前，服务器 A 会连续的接受到 2 个请求，以此类推。&lt;/p&gt;
&lt;h4&gt;最少连接数&lt;/h4&gt;
&lt;p&gt;&lt;strong&gt;Least Connection：&lt;/strong&gt; 以上两种方法都没有考虑的是系统不能识别在给定的时间里保持了多少连接。因此可能发生，服务器 B 服务器收到的连接比服务器 A 少但是它已经超载，因为 服务器 B 上的用户打开连接持续的时间更长。这就是说连接数即服务器的负载是累加的。这种潜在的问题可以通过 “最少连接数” 算法来避免：传入的请求是根据每 台服务器当前所打开的连接数来分配的。即活跃连接数最少的服务器会自动接收下一个传入的请求。接本上和简单轮询的原则相同：所有拥有虚拟服务的服务器资源 容量应该相近。值得注意的是，在流量率低的配置环境中，各服务器的流量并不是相同的，会优先考虑第一台服务器。这是因为，如果所有的服务器是相同的，那么 第一个服务器优先，直到第一台服务器有连续的活跃流量，否则总是会优先选择第一台服务器。&lt;/p&gt;
&lt;h4&gt;最少连接数慢启动时间&lt;/h4&gt;
&lt;p&gt;&lt;strong&gt;Least Connection Slow Start Time：&lt;/strong&gt; 对最少连接数和带权重的最小连接数调度方法来说，当一个服务器刚加入线上环境是，可以为其配置一个时间段，在这段时间内连接数是有限制的而且是缓慢 增加的。这为服务器提供了一个‘过渡时间’以保证这个服务器不会因为刚启动后因为分配的连接数过多而超载。这个值在 L7 配置界面设置。&lt;/p&gt;
&lt;h4&gt;加权最少连接&lt;/h4&gt;
&lt;p&gt;&lt;strong&gt;Weighted Least Connection：&lt;/strong&gt; 如果服务器的资源容量各不相同，那么 “加权最少连接” 方法更合适：由管理员根据服务器情况定制的权重所决定的活跃连接数一般提供了一种对服务器非常 平衡的利用，因为他它借鉴了最少连接和权重两者的优势。通常，这是一个非常公平的分配方式，因为它使用了连接数和服务器权重比例；集群中比例最低的服务器 自动接收下一个请求。但是请注意，在低流量情况中使用这种方法时，请参考 “最小连接数” 方法中的注意事项。&lt;/p&gt;
&lt;h4&gt;基于代理的自适应负载均衡&lt;/h4&gt;
&lt;p&gt;&lt;strong&gt;Agent Based Adaptive Balancing：&lt;/strong&gt; 除了上述方法之外，负载主机包含一个自适用逻辑用来定时监测服务器状态和该服务器的权重。对于非常强大的 “基于代理的自适应负载均衡” 方法来说，负 载主机以这种方式来定时检测所有服务器负载情况：每台服务器都必须提供一个包含文件，这个文件包含一个 0~99 的数字用来标明改服务器的实际负载情况 (0 = 空前，99 = 超载，101 = 失败，102 = 管理员禁用)，而服务器同构 http get 方法来获取这个文件；同时对集群中服务器来说，以二进制文件形式提供自身负载情况也是该服务器工作之一，然而，并没有限制服务器如何计算自身的负载 情况。根据服务器整体负载情况，有两种策略可以选择：在常规的操作中，调度算法通过收集的服务器负载值和分配给该服务器的连接数的比例计算出一个权重比 例。因此，如果一个服务器负载过大，权重会通过系统透明的作重新调整。和加权轮循调度方法一样，不正确的分配可以被记录下来使得可以有效的为不同服务器分 配不同的权重。然而，在流量非常低的环境下，服务器报上来的负载值将不能建立一个有代表性的样本；那么基于这些值来分配负载的话将导致失控以及指令震荡。 因此，在这种情况下更合理的做法是基于静态的权重比来计算负载分配。当所有服务器的负载低于管理员定义的下限时，负载主机就会自动切换为加权轮循方式来分 配请求；如果负载大于管理员定义的下限，那么负载主机又会切换回自适应方式。&lt;/p&gt;
&lt;h4&gt;固定权重&lt;/h4&gt;
&lt;p&gt;&lt;strong&gt;Fixed Weighted：&lt;/strong&gt; 最高权重只有在其他服务器的权重值都很低时才使用。然而，如果最高权重的服务器下降，则下一个最高优先级的服务器将为客户端服务。这种方式中每个真实服务器的权重需要基于服务器优先级来配置。&lt;/p&gt;
&lt;h4&gt;加权响应&lt;/h4&gt;
&lt;p&gt;&lt;strong&gt;Weighted Response：&lt;/strong&gt; 流量的调度是通过加权轮循方式。加权轮循中所使用的权重是根据服务器有效性检测的响应时间来计算。每个有效性检测都会被计时，用来标记它响应成功花 了多长时间。但是需要注意的是，这种方式假定服务器心跳检测是基于机器的快慢，但是这种假设也许不总是能够成立。所有服务器在虚拟服务上的响应时间的总和 加在一起，通过这个值来计算单个服务物理服务器的权重；这个权重值大约每 15 秒计算一次。&lt;/p&gt;
&lt;h4&gt;源 IP 哈希&lt;/h4&gt;
&lt;p&gt;&lt;strong&gt;Source IP Hash：&lt;/strong&gt; 这种方式通过生成请求源 IP 的哈希值，并通过这个哈希值来找到正确的真实服务器。这意味着对于同一主机来说他对应的服务器总是相同。使用这种方式，你不需要保存任何源 IP。但是需要注意，这种方式可能导致服务器负载不平衡。&lt;/p&gt;
&lt;h1&gt;Nacos 分布式配置中心&lt;/h1&gt;
&lt;h2&gt;概述&lt;/h2&gt;
&lt;p&gt;在分布式系统中，由于服务数量巨多，为了方便服务配置文件统一管理，实时更新，所以需要分布式配置中心组件&lt;/p&gt;
&lt;h2&gt;什么是 Nacos Config&lt;/h2&gt;
&lt;p&gt;Nacos 提供用于存储配置和其他元数据的 key/value 存储，为分布式系统中的外部化配置提供服务器端和客户端支持。使用 Spring Cloud Alibaba Nacos Config，您可以在 Nacos Server 集中管理你 Spring Cloud 应用的外部属性配置。&lt;/p&gt;
&lt;p&gt;Spring Cloud Alibaba Nacos Config 是 Spring Cloud Config Server 和 Client 的替代方案，客户端和服务器上的概念与 Spring Environment 和 PropertySource 有着一致的抽象，在特殊的 bootstrap 阶段，配置被加载到 Spring 环境中。当应用程序通过部署管道从开发到测试再到生产时，您可以管理这些环境之间的配置，并确保应用程序具有迁移时需要运行的所有内容。&lt;/p&gt;
&lt;h2&gt;接入配置中心&lt;/h2&gt;
&lt;h3&gt;POM&lt;/h3&gt;
&lt;p&gt;我们以 &lt;code&gt;service-consumer&lt;/code&gt; 项目为例，修改 &lt;code&gt;pom.xml&lt;/code&gt; ，引入 &lt;strong&gt;Nacos Config Starter&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&amp;lt;dependency&amp;gt;    
	&amp;lt;groupId&amp;gt;org.springframework.cloud&amp;lt;/groupId&amp;gt;    
	&amp;lt;artifactId&amp;gt;spring-cloud-starter-alibaba-nacos-config&amp;lt;/artifactId&amp;gt;
&amp;lt;/dependency&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Controller&lt;/h3&gt;
&lt;p&gt;完成上述两步后，应用会从 Nacos Config 中获取相应的配置，并添加在 Spring Environment 的 PropertySources 中。这里我们使用 &lt;code&gt;@Value&lt;/code&gt; 注解来将对应的配置注入到 &lt;code&gt;TestEchoController&lt;/code&gt; 的 &lt;code&gt;username&lt;/code&gt;字段，并添加 &lt;code&gt;@RefreshScope&lt;/code&gt; 打开动态刷新功能&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;package com.funtl.spring.cloud.alibaba.consumer.controller;
import com.funtl.spring.cloud.alibaba.consumer.service.EchoService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.cloud.context.config.annotation.RefreshScope;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RestController;
@RefreshScope
@RestController
public class TestEchoController {
    @Autowired
    private EchoService echoService;
    @Value(&quot;${user.name}&quot;)
    private String username;
    @GetMapping(value = &quot;/config&quot;)
    public String config() {
        return echoService.echo(username);
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;使用控制台发布配置&lt;/h3&gt;
&lt;p&gt;通过浏览器访问 &lt;a href=&quot;http://www.qfdmy.com/wp-content/themes/quanbaike/go.php?url=aHR0cDovLzE5Mi4xNjguMTQxLjEzMjo4ODQ4L25hY29z&quot;&gt;http://192.168.141.132:8848/nacos&lt;/a&gt; ，访问 Nacos Server&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;nacos_config_%E9%85%8D%E7%BD%AE.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;%E6%96%B0%E5%BB%BAnacos%E9%85%8D%E7%BD%AE.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;注意：&lt;/strong&gt; Data ID 的默认扩展名为 &lt;code&gt;.properties&lt;/code&gt; ，希望使用 YAML 配置，此处必须指明是 &lt;code&gt;.yaml&lt;/code&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;&lt;img src=&quot;nacos_%E5%88%97%E8%A1%A8%E5%B1%95%E7%A4%BA.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;spring:
  application:
    # 服务名
    name: service-consumer
  cloud:
    nacos:
      discovery:
        # 服务注册中心
        server-addr: 192.168.141.132:8848
server:
  # 服务端口
  port: 8080
management:
  # 端点检查（健康检查）
  endpoints:
    web:
      exposure:
        include: &quot;*&quot;
user:
  name: &quot;灶门炭治郎&quot;
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;修改客户端配置&lt;/h3&gt;
&lt;p&gt;创建名为 &lt;code&gt;bootstrap.properties&lt;/code&gt; 的配置文件并删除之前创建的 &lt;code&gt;application.yml&lt;/code&gt; 配置文件&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;spring.application.name=service-consumer-configspring.cloud.nacos.config.server-addr=192.168.141.132:8848spring.cloud.nacos.config.file-extension=yaml
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;通过浏览器访问 &lt;a href=&quot;http://www.qfdmy.com/wp-content/themes/quanbaike/go.php?url=aHR0cDovL2xvY2FsaG9zdDo4MDgwL2NvbmZpZw==&quot;&gt;http://localhost:8080/config&lt;/a&gt; ，浏览器输出如下&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;Hello Nacos Provider 灶门炭治郎
&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;注意：&lt;/strong&gt; Spring Boot 配置文件的加载顺序，依次为 &lt;code&gt;bootstrap.properties&lt;/code&gt; -&amp;gt; &lt;code&gt;bootstrap.yml&lt;/code&gt; -&amp;gt; &lt;code&gt;application.properties&lt;/code&gt; -&amp;gt; &lt;code&gt;application.yml&lt;/code&gt; ，其中 &lt;strong&gt;bootstrap.properties&lt;/strong&gt; 配置为最高优先级&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h3&gt;动态刷新配置&lt;/h3&gt;
&lt;p&gt;在 Nacos Server 控制台修改配置文件，将 &lt;code&gt;user.name&lt;/code&gt; 属性修改为 &lt;code&gt;灶门祢豆子&lt;/code&gt;，此时观察控制台日志，你会发现我们已经成功刷新了配置&lt;/p&gt;
&lt;h3&gt;验证是否成功&lt;/h3&gt;
&lt;p&gt;通过浏览器访问 &lt;a href=&quot;http://www.qfdmy.com/wp-content/themes/quanbaike/go.php?url=aHR0cDovL2xvY2FsaG9zdDo4MDgwL2NvbmZpZw==&quot;&gt;http://localhost:8080/config&lt;/a&gt; ，浏览器输出如下&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;Hello Nacos Provider 灶门祢豆子
&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;提示：&lt;/strong&gt; 你可以使用 &lt;code&gt;spring.cloud.nacos.config.refresh.enabled=false&lt;/code&gt; 来关闭动态刷新&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2&gt;附：扩展阅读&lt;/h2&gt;
&lt;h3&gt;Endpoint 信息查看&lt;/h3&gt;
&lt;p&gt;Spring Boot 应用支持通过 Endpoint 来暴露相关信息，Nacos Config Starter 也支持这一点。在使用之前需要在 maven 中添加 &lt;code&gt;spring-boot-starter-actuator&lt;/code&gt; 依赖，并在配置中允许 Endpoints 的访问。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;nacos-discovery：&lt;/strong&gt; &lt;a href=&quot;http://www.qfdmy.com/wp-content/themes/quanbaike/go.php?url=aHR0cDovL2xvY2FsaG9zdDo4MDgwL2FjdHVhdG9yL25hY29zLWRpc2NvdmVyeQ==&quot;&gt;http://localhost:8080/actuator/nacos-discovery&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;{
    &quot;subscribe&quot;: [
        {
            &quot;jsonFromServer&quot;: &quot;&quot;,
            &quot;name&quot;: &quot;service-consumer&quot;,
            &quot;groupName&quot;: &quot;DEFAULT_GROUP&quot;,
            &quot;clusters&quot;: null,
            &quot;cacheMillis&quot;: 1000,
            &quot;hosts&quot;: [],
            &quot;lastRefTime&quot;: 0,
            &quot;checksum&quot;: &quot;&quot;,
            &quot;allIPs&quot;: false,
            &quot;key&quot;: &quot;service-consumer&quot;,
            &quot;valid&quot;: true,
            &quot;keyEncoded&quot;: &quot;service-consumer&quot;
        },
        {
            &quot;jsonFromServer&quot;: &quot;&quot;,
            &quot;name&quot;: &quot;service-provider&quot;,
            &quot;groupName&quot;: &quot;DEFAULT_GROUP&quot;,
            &quot;clusters&quot;: null,
            &quot;cacheMillis&quot;: 1000,
            &quot;hosts&quot;: [],
            &quot;lastRefTime&quot;: 0,
            &quot;checksum&quot;: &quot;&quot;,
            &quot;allIPs&quot;: false,
            &quot;key&quot;: &quot;service-provider&quot;,
            &quot;valid&quot;: true,
            &quot;keyEncoded&quot;: &quot;service-provider&quot;
        }
    ],
    &quot;NacosDiscoveryProperties&quot;: {
        &quot;serverAddr&quot;: &quot;192.168.141.132:8848&quot;,
        &quot;endpoint&quot;: &quot;&quot;,
        &quot;namespace&quot;: &quot;&quot;,
        &quot;watchDelay&quot;: 30000,
        &quot;logName&quot;: &quot;&quot;,
        &quot;service&quot;: &quot;service-consumer&quot;,
        &quot;weight&quot;: 1,
        &quot;clusterName&quot;: &quot;DEFAULT&quot;,
        &quot;namingLoadCacheAtStart&quot;: &quot;false&quot;,
        &quot;metadata&quot;: {
            &quot;preserved.register.source&quot;: &quot;SPRING_CLOUD&quot;
        },
        &quot;registerEnabled&quot;: true,
        &quot;ip&quot;: &quot;192.168.141.1&quot;,
        &quot;networkInterface&quot;: &quot;&quot;,
        &quot;port&quot;: 8080,
        &quot;secure&quot;: false,
        &quot;accessKey&quot;: &quot;&quot;,
        &quot;secretKey&quot;: &quot;&quot;
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;nacos-config：&lt;/strong&gt; &lt;a href=&quot;http://www.qfdmy.com/wp-content/themes/quanbaike/go.php?url=aHR0cDovL2xvY2FsaG9zdDo4MDgwL2FjdHVhdG9yL25hY29zLWNvbmZpZw==&quot;&gt;http://localhost:8080/actuator/nacos-config&lt;/a&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;{
    &quot;NacosConfigProperties&quot;: {
        &quot;serverAddr&quot;: &quot;192.168.141.132:8848&quot;,
        &quot;encode&quot;: null,
        &quot;group&quot;: &quot;DEFAULT_GROUP&quot;,
        &quot;prefix&quot;: null,
        &quot;fileExtension&quot;: &quot;yaml&quot;,
        &quot;timeout&quot;: 3000,
        &quot;endpoint&quot;: null,
        &quot;namespace&quot;: null,
        &quot;accessKey&quot;: null,
        &quot;secretKey&quot;: null,
        &quot;contextPath&quot;: null,
        &quot;clusterName&quot;: null,
        &quot;name&quot;: null,
        &quot;sharedDataids&quot;: null,
        &quot;refreshableDataids&quot;: null,
        &quot;extConfig&quot;: null
    },
    &quot;RefreshHistory&quot;: [
        {
            &quot;timestamp&quot;: &quot;2019-06-30 18:30:45&quot;,
            &quot;dataId&quot;: &quot;service-consumer-config.yaml&quot;,
            &quot;md5&quot;: &quot;690fd78b5ae9fac5b545c1a8d7ec4e2b&quot;
        },
        {
            &quot;timestamp&quot;: &quot;2019-06-30 18:22:08&quot;,
            &quot;dataId&quot;: &quot;service-consumer-config.yaml&quot;,
            &quot;md5&quot;: &quot;40b12ffc9d305ec1cad65d606e8f4708&quot;
        }
    ],
    &quot;Sources&quot;: [
        {
            &quot;lastSynced&quot;: &quot;2019-06-30 18:22:05&quot;,
            &quot;dataId&quot;: &quot;service-consumer-config.yaml&quot;
        }
    ]
}
&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;注意：&lt;/strong&gt; Sources 表示此客户端从哪些 Nacos Config 配置项中获取了信息，RefreshHistory 表示动态刷新的历史记录，最多保存20条，NacosConfigProperties 则为 Nacos Config Starter 本身的配置&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h3&gt;更多配置&lt;/h3&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;配置项&lt;/th&gt;
&lt;th&gt;key&lt;/th&gt;
&lt;th&gt;默认值&lt;/th&gt;
&lt;th&gt;说明&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;服务端地址&lt;/td&gt;
&lt;td&gt;spring.cloud.nacos.config.server-addr&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;DataId前缀&lt;/td&gt;
&lt;td&gt;spring.cloud.nacos.config.prefix&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;td&gt;spring.application.name&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Group&lt;/td&gt;
&lt;td&gt;spring.cloud.nacos.config.group&lt;/td&gt;
&lt;td&gt;DEFAULT_GROUP&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;dataID后缀及内容文件格式&lt;/td&gt;
&lt;td&gt;spring.cloud.nacos.config.file-extension&lt;/td&gt;
&lt;td&gt;properties&lt;/td&gt;
&lt;td&gt;dataId的后缀，同时也是配置内容的文件格式，目前只支持 properties&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;配置内容的编码方式&lt;/td&gt;
&lt;td&gt;spring.cloud.nacos.config.encode&lt;/td&gt;
&lt;td&gt;UTF-8&lt;/td&gt;
&lt;td&gt;配置的编码&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;获取配置的超时时间&lt;/td&gt;
&lt;td&gt;spring.cloud.nacos.config.timeout&lt;/td&gt;
&lt;td&gt;3000&lt;/td&gt;
&lt;td&gt;单位为 ms&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;配置的命名空间&lt;/td&gt;
&lt;td&gt;spring.cloud.nacos.config.namespace&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;td&gt;常用场景之一是不同环境的配置的区分隔离，例如开发测试环境和生产环境的资源隔离等。&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;AccessKey&lt;/td&gt;
&lt;td&gt;spring.cloud.nacos.config.access-key&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;SecretKey&lt;/td&gt;
&lt;td&gt;spring.cloud.nacos.config.secret-key&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;相对路径&lt;/td&gt;
&lt;td&gt;spring.cloud.nacos.config.context-path&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;td&gt;服务端 API 的相对路径&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;接入点&lt;/td&gt;
&lt;td&gt;spring.cloud.nacos.config.endpoint&lt;/td&gt;
&lt;td&gt;UTF-8&lt;/td&gt;
&lt;td&gt;地域的某个服务的入口域名，通过此域名可以动态地拿到服务端地址&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;是否开启监听和自动刷新&lt;/td&gt;
&lt;td&gt;spring.cloud.nacos.config.refresh.enabled&lt;/td&gt;
&lt;td&gt;true&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;h3&gt;更多介绍&lt;/h3&gt;
&lt;p&gt;Nacos为用户提供包括动态服务发现，配置管理，服务管理等服务基础设施，帮助用户更灵活，更轻松地构建，交付和管理他们的微服务平台，基于 Nacos, 用户可以更快速的构建以“服务”为中心的现代云原生应用。Nacos 可以和 &lt;strong&gt;Spring Cloud&lt;/strong&gt;、&lt;strong&gt;Kubernetes/CNCF&lt;/strong&gt;、&lt;strong&gt;Dubbo&lt;/strong&gt; 等微服务生态无缝融合，为用户提供更卓越的体验&lt;/p&gt;
&lt;h1&gt;Nacos 多环境配置&lt;/h1&gt;
&lt;h2&gt;概述&lt;/h2&gt;
&lt;p&gt;我们在做项目开发的时候，生产环境和测试环境的一些配置可能会不一样，有时候一些功能也可能会不一样，所以我们可能会在上线的时候手工修改这些配置信息。Spring 为我们提供了 Spring Boot Profile 这个功能（Maven 也为我们提供了 Maven Profile）。我们只需要在启动的时候添加一个虚拟机参数，激活自己环境所要用的 Profile 就可以了。&lt;/p&gt;
&lt;p&gt;操作起来很简单，只需要为不同的环境编写专门的配置文件，如：&lt;code&gt;application-dev.yml&lt;/code&gt;、&lt;code&gt;application-prod.yml&lt;/code&gt;， 启动项目时只需要增加一个命令参数 &lt;code&gt;--spring.profiles.active=&lt;/code&gt; 环境配置即可&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;java -jar 1.0.0-SNAPSHOT.jar --spring.profiles.active=prod
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;spring-cloud-starter-alibaba-nacos-config&lt;/code&gt; 在加载配置的时候，不仅仅加载了以 dataid 为 &lt;code&gt;${spring.application.name}.${file-extension:properties}&lt;/code&gt; 为前缀的基础配置，还加载了 dataid 为 &lt;code&gt;${spring.application.name}-${profile}.${file-extension:properties}&lt;/code&gt; 的基础配置。在日常开发中如果遇到多套环境下的不同配置，可以通过 Spring 提供的 &lt;code&gt;${spring.profiles.active}&lt;/code&gt; 这个配置项来配置。&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;注意：配置文件的加载顺序(nacos读取配置文件一定要在配置文件中处于第一优先级)&lt;/p&gt;
&lt;p&gt;bootstrap.properties -&amp;gt; bootstrap.yml -&amp;gt; application.properties -&amp;gt; application.yml&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2&gt;使用 Nacos Config Profile&lt;/h2&gt;
&lt;p&gt;我们以 &lt;code&gt;service-provider&lt;/code&gt; 项目为例，演示多环境配置效果，不要忘记依赖 Nacos Config Starter&lt;/p&gt;
&lt;h3&gt;1、依赖&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;&amp;lt;dependency&amp;gt;
    &amp;lt;groupId&amp;gt;org.springframework.cloud&amp;lt;/groupId&amp;gt;
    &amp;lt;artifactId&amp;gt;spring-cloud-starter-alibaba-nacos-config&amp;lt;/artifactId&amp;gt;
&amp;lt;/dependency&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;3、使用控制台发布配置&lt;/h3&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;注意：&lt;/strong&gt; 控制台发布配置时不要加注释，否则打成 Jar 包后运行会报无法解析配置文件的错误&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;通过浏览器访问 &lt;a href=&quot;http://www.qfdmy.com/wp-content/themes/quanbaike/go.php?url=aHR0cDovLzE5Mi4xNjguMTQxLjEzMjo4ODQ4L25hY29z&quot;&gt;http://192.168.141.132:8848/nacos&lt;/a&gt; ，访问 Nacos Server&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;发布一个名为 &lt;code&gt;service-provider-config-dev.yaml&lt;/code&gt; 的 &lt;strong&gt;测试环境&lt;/strong&gt; 配置&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;spring:
  application:
    # 服务名
    name: service-provider
  cloud:
    nacos:
      discovery:
        # 服务注册中心
        server-addr: 192.168.141.132:8848
server:
  # 服务端口
  port: 8070
management:
  # 端点检查（健康检查）
  endpoints:
    web:
      exposure:
        include: &quot;*&quot;
&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;发布一个名为 &lt;code&gt;service-provider-config-prod.yaml&lt;/code&gt; 的 &lt;strong&gt;生产环境&lt;/strong&gt; 配置&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;spring:
  application:
    # 服务名
    name: service-provider
  cloud:
    nacos:
      discovery:
        # 服务注册中心
        server-addr: 192.168.141.132:8848
server:
  # 修改了上面的端口号，区分配置的不同
  port: 8071
management:
  # 端点检查（健康检查）
  endpoints:
    web:
      exposure:
        include: &quot;*&quot;
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;4、修改客户端配置&lt;/h3&gt;
&lt;blockquote&gt;
&lt;p&gt;配置注册中心地址，由于配置中心的优先级高，所有必须将注册中心配置在最底层（bootstrap.properties）中&lt;/p&gt;
&lt;/blockquote&gt;
&lt;ul&gt;
&lt;li&gt;创建名为 &lt;code&gt;bootstrap.properties&lt;/code&gt; 的配置文件并删除之前创建的 &lt;code&gt;application.yml&lt;/code&gt; 配置文件&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;spring.profiles.active=dev
spring.application.name=service-provider-config
spring.cloud.nacos.config.server-addr=192.168.141.132:8848
spring.cloud.nacos.config.file-extension=yaml
&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;创建名为 &lt;code&gt;bootstrap-prod.properties&lt;/code&gt; 的配置文件&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;spring.application.name=service-provider-config
spring.cloud.nacos.config.server-addr=192.168.141.132:8848
spring.cloud.nacos.config.file-extension=yaml
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;测试多环境配置&lt;/h3&gt;
&lt;p&gt;此时我们有两个配置文件，分别为 &lt;code&gt;bootstrap.properties&lt;/code&gt; 和 &lt;code&gt;bootstrap-prod.properties&lt;/code&gt; ，我们需要指定启动时加载哪一个配置文件&lt;/p&gt;
&lt;h4&gt;不指定：&lt;/h4&gt;
&lt;p&gt;不指定默认时 dev，则加载：&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;1、&lt;code&gt;bootstrap.properties&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;2、 service-provider-config-dev.yaml&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h4&gt;指定pro：&lt;/h4&gt;
&lt;blockquote&gt;
&lt;pre&gt;&lt;code&gt; java -Xms512m -Xmx512m -Xmn200m -Xss256k -jar demo996.jar --spring.profiles.active=dev
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;则加载：&lt;/p&gt;
&lt;p&gt;1、&lt;code&gt;bootstrap.properties&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;2、bootstrap-prod.properties&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;bootstrap-prod.properties&lt;/strong&gt;中的&lt;strong&gt;相同内容&lt;/strong&gt;会覆盖&lt;strong&gt;bootstrap.properties&lt;/strong&gt;中的内容，不相同的不会覆盖&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;3、&lt;code&gt;service-provider-config-prod.yaml&lt;/code&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;运行项目并观察日志&lt;/p&gt;
&lt;h2&gt;application 和 bootstrap关系&lt;/h2&gt;
&lt;p&gt;&lt;strong&gt;（一）&lt;/strong&gt;、关系&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Spring Cloud 构建于 Spring Boot 之上，在 Spring Boot 中有两种上下文，一种是 bootstrap,另外一种是 application,&lt;/li&gt;
&lt;li&gt;application 配置文件这个容易理解，主要用于 Spring Boot 项目的自动化配置。&lt;/li&gt;
&lt;li&gt;bootstrap 是应用程序的父上下文，也就是说 bootstrap 加载优先于 applicaton。&lt;/li&gt;
&lt;li&gt;bootstrap 主要用于从额外的资源来加载配置信息，还可以在本地外部配置文件中解密属性。&lt;/li&gt;
&lt;li&gt;这两个上下文共用一个环境，它是任何Spring应用程序的外部属性的来源。&lt;/li&gt;
&lt;li&gt;bootstrap 里面的属性会优先加载，它们默认也不能被本地相同配置覆盖。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;boostrap 由父 ApplicationContext 加载，比 applicaton 优先加载&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;boostrap 里面的属性不能被覆盖&lt;/strong&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;（二）、bootstrap 配置文件有以下几个应用场景。&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;使用 Spring Cloud Config 配置中心时，这时需要在 bootstrap 配置文件中添加连接到配置中心的配置属性来加载外部配置中心的配置信息；&lt;/li&gt;
&lt;li&gt;一些固定的不能被覆盖的属性&lt;/li&gt;
&lt;li&gt;一些加密/解密的场景；&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;注意：一旦bootStrap.yml 被加载，则内容不会被覆盖，即便后期加载的application.yml的内容标签与bootstrap的标签一致，application 也不会覆盖bootstrap, 而application.yml 里面的内容可以动态替换。&lt;/p&gt;
&lt;h1&gt;nacos配置中心命名空间：&lt;/h1&gt;
&lt;h2&gt;1、新增nacos命名空间&lt;/h2&gt;
&lt;p&gt;&lt;img src=&quot;%E6%96%B0%E5%A2%9Enacos%E5%91%BD%E5%90%8D%E7%A9%BA%E9%97%B4.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;每个命名空间都有一个命名空间ID&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;&lt;img src=&quot;%E6%96%B0%E5%A2%9E%E9%85%8D%E7%BD%AE.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;在命名空间下新增配置&lt;/p&gt;
&lt;p&gt;dataid 文件名&lt;/p&gt;
&lt;p&gt;group分组&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2&gt;2、nacos实战配置&lt;/h2&gt;
&lt;p&gt;&lt;img src=&quot;%E5%AE%9E%E6%88%98nacos%E9%85%8D%E7%BD%AE.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;spring:
  application:
    name: gulimall-gateway
  cloud:
    nacos:
      config:
        server-addr: 49.232.154.66:8848
        namespace: 41812423-7274-4c60-a5d0-15244148f1a4
        extension-configs:
          - dataId: gulimall-gateway.yml
            group: dev
&lt;/code&gt;&lt;/pre&gt;
&lt;h1&gt;nacos注册中心&lt;/h1&gt;
&lt;h2&gt;1、pom.xml&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;        &amp;lt;dependency&amp;gt;
            &amp;lt;groupId&amp;gt;com.alibaba.cloud&amp;lt;/groupId&amp;gt;
            &amp;lt;artifactId&amp;gt;spring-cloud-starter-alibaba-nacos-discovery&amp;lt;/artifactId&amp;gt;
        &amp;lt;/dependency&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;2、配置服务注册与发现&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;spring:
  application:
    name: gulimall-gateway
  cloud:
    nacos:
      discovery:
        server-addr: 49.232.154.66:8848
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;3、开启服务注册与发现&lt;/h2&gt;
&lt;p&gt;&lt;code&gt;@EnableDiscoveryClient&lt;/code&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;@EnableDiscoveryClient
@SpringBootApplication(exclude = DataSourceAutoConfiguration.class)
public class GulimallGatewayApplication {

    public static void main(String[] args) {
        SpringApplication.run(GulimallGatewayApplication.class, args);
    }

}
&lt;/code&gt;&lt;/pre&gt;
&lt;h1&gt;nacos动态刷新&lt;/h1&gt;
&lt;p&gt;nacos动态刷新+@value实战&lt;/p&gt;
&lt;h2&gt;一、@value&lt;/h2&gt;
&lt;blockquote&gt;
&lt;p&gt;@Value 读取配置&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h3&gt;1、读集合&lt;/h3&gt;
&lt;p&gt;在日常开发中，经常会遇到需要在配置文件中，存储 &lt;code&gt;List&lt;/code&gt; 或是 &lt;code&gt;Map&lt;/code&gt; 这种类型的数据。&lt;/p&gt;
&lt;p&gt;Spring 原生是支持这种数据类型的，以配置 &lt;code&gt;List&lt;/code&gt; 类型为例，对于 &lt;code&gt;.yml&lt;/code&gt; 文件配置如下：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;test:  
  list:  
    - aaa  
    - bbb  
    - ccc  
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;对于 &lt;code&gt;.properties&lt;/code&gt; 文件配置如下所示：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;test.list[0]=aaa  
test.list[1]=bbb  
test.list[2]=ccc  
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;当我们想要在程序中使用时候，想当然的使用 &lt;code&gt;@Value&lt;/code&gt; 注解去读取这个值，就像下面这种写法一样：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;@Value(&quot;${test.list}&quot;)  
private List testList;  
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;你会发现程序直接报错了，报错信息如下：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;java.lang.IllegalArgumentException: Could not resolve placeholder &apos;test.list&apos; in value &quot;${test.list}&quot;  
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这个问题也是可以解决的，以我们要配置的 key 为 &lt;code&gt;test.list&lt;/code&gt; 为例，新建一个 &lt;code&gt;test&lt;/code&gt; 的配置类，将 &lt;code&gt;list&lt;/code&gt; 作为该配置类的一个属性：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;@Configuration  
@ConfigurationProperties(&quot;test&quot;)  
public class TestListConfig {  
    private List list;  
    public List getList() {  return list;  
    }  
    public void setList(List list) {  
        this.list = list;  
    }  
}  
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;在程序其他地方使用时候。采用自动注入的方式，去获取值：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;@Autowired  
private TestListConfig testListConfig;  
  
// testListConfig.getList();  
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;可以看见，这种方式十分的不方便，最大的问题是配置和代码高耦合了，增加一个配置，还需要对配置类做增减改动。关注公众号Java技术栈可以搜索阅读Spring Boot更多强大的参数配置和读取教程。&lt;/p&gt;
&lt;h3&gt;2、数组怎么样&lt;/h3&gt;
&lt;p&gt;数组？说实话，业务代码写多了，这个“古老”的数据结构远远没有 list 用的多，但是它在解决上面这个问题上，出乎异常的好用。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;test:  
  array1: aaa,bbb,ccc  
  array2: 111,222,333  
  array3: 11.1,22.2,33.3  
@Value(&quot;${test.array1}&quot;)  
private String[] testArray1;  
  
@Value(&quot;${test.array2}&quot;)  
private int[] testArray2;  
  
@Value(&quot;${test.array3}&quot;)  
private double[] testArray3;  
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这样就能够直接使用了，就是这么的简单方便，如果你想要支持不配置 key 程序也能正常运行的话，给它们加上默认值即可：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;@Value(&quot;${test.array1:}&quot;)  
private String[] testArray1;  
  
@Value(&quot;${test.array2:}&quot;)  
private int[] testArray2;  
  
@Value(&quot;${test.array3:}&quot;)  
private double[] testArray3;  
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;仅仅多了一个 &lt;code&gt;:&lt;/code&gt; 号，冒号后的值表示当 key 不存在时候使用的默认值，使用默认值时数组的 length = 0。&lt;/p&gt;
&lt;p&gt;总结下使用数组实现的优缺点：&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;优点&lt;/strong&gt;：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;不需要写配置类&lt;/li&gt;
&lt;li&gt;使用逗号分割，一行配置，即可完成多个数值的注入，配置文件更加精简&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;缺点&lt;/strong&gt;：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;业务代码中数组使用很少，基本需要将其转换为 List，去做 contains、foreach 等操作。&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;3、替代方法&lt;/h3&gt;
&lt;p&gt;那么我们有没有办法，在解析 list、map 这些类型时，像数组一样方便呢？&lt;/p&gt;
&lt;p&gt;答案是可以的，这就依赖于 &lt;code&gt;EL&lt;/code&gt; 表达式。&lt;/p&gt;
&lt;h3&gt;3.1 解析 List&lt;/h3&gt;
&lt;p&gt;以使用 &lt;code&gt;.yml&lt;/code&gt; 文件为例，我们只需要在配置文件中，跟配置数组一样去配置：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;test:  
  list: aaa,bbb,ccc  
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;在调用时，借助 &lt;code&gt;EL&lt;/code&gt; 表达式的 &lt;code&gt;split()&lt;/code&gt; 函数进行切分即可。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;@Value(&quot;#{&apos;${test.list}&apos;.split(&apos;,&apos;)}&quot;)  
private List testList;  
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;同样，为它加上默认值，避免不配置这个 key 时候程序报错：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;@Value(&quot;#{&apos;${test.list:}&apos;.split(&apos;,&apos;)}&quot;)  
private List testList;  
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;但是这样有个问题，当不配置该 key 值，默认值会为空串，它的 length = 1(不同于数组，length = 0)，这样解析出来 list 的元素个数就不是空了。&lt;/p&gt;
&lt;p&gt;这个问题比较严重，因为它会导致代码中的判空逻辑执行错误。这个问题也是可以解决的，在 &lt;code&gt;split()&lt;/code&gt; 之前判断下是否为空即可。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;@Value(&quot;#{&apos;${test.list:}&apos;.empty ? null : &apos;${test.list:}&apos;.split(&apos;,&apos;)}&quot;)  
private List testList;  
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;如上所示，即为&lt;strong&gt;最终的版本&lt;/strong&gt;，它具有数组方式的全部优点，且更容易在业务代码中去应用。线程安全的List，这篇推荐看下。&lt;/p&gt;
&lt;h3&gt;3.2 解析 Set&lt;/h3&gt;
&lt;p&gt;解析 Set 和解析 List 本质上是相同的，唯一的区别是 Set 会做去重操作。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;test:  
  set: 111,222,333,111  
`@Value(&quot;#{&apos;${test.set:}&apos;.empty ? null : &apos;${test.set:}&apos;.split(&apos;,&apos;)}&quot;)  
private Set testSet;  
// output: [111, 222, 333]  
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;3.3 解析 Map&lt;/h3&gt;
&lt;p&gt;解析 Map 的写法如下所示，value 为该 map 的 JSON 格式，注意这里使用的引号：整个 JSON 串使用引号包裹，value 值使用引号包裹。Spring Boot 返回 JSON 数据，推荐大家看下。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;test:  
  map1: &apos;{&quot;name&quot;: &quot;zhangsan&quot;, &quot;sex&quot;: &quot;male&quot;}&apos;  
  map2: &apos;{&quot;math&quot;: &quot;90&quot;, &quot;english&quot;: &quot;85&quot;}&apos;  
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;在程序中，利用 EL 表达式注入：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;@Value(&quot;#{${test.map1}}&quot;)  
private Map map1;  
@Value(&quot;#{${test.map2}}&quot;)  
private Map map2;  
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;注意，使用这种方式，必须得在配置文件中配置该 key 及其 value。我在网上找了许多资料，都没找到利用 EL 表达式支持不配置 key/value 的写法。&lt;/p&gt;
&lt;p&gt;如果你真的很需要这个功能，就得自己写解析方法了，这里以使用 fastjson 进行解析为例：&lt;/p&gt;
&lt;p&gt;(1) 自定义解析方法&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;public class MapDecoder {  
    public static Map decodeMap(String value) {  
        try {  return JSONObject.parseObject(value, new TypeReference&amp;gt;(){});  
        } catch (Exception e) {  return null;  
        }  
    }  
}  
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;(2) 在程序中指定解析方法&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;@Value(&quot;#{T(com.github.jitwxs.demo.MapDecoder).decodeMap(&apos;${test.map1:}&apos;)}&quot;)  
private Map map1;  
@Value(&quot;#{T(com.github.jitwxs.demo.MapDecoder).decodeMap(&apos;${test.map2:}&apos;)}&quot;)  
private Map map2;  
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;二、动态刷新测试&lt;/h2&gt;
&lt;blockquote&gt;
&lt;p&gt;@RefreshScope注解&lt;/p&gt;
&lt;/blockquote&gt;
&lt;pre&gt;&lt;code&gt;@RefreshScope
@Slf4j
@Service(&quot;cmsJwtTokenHandler&quot;)
public class JwtTokenHandlerCmsImpl{
    
	@Value(&quot;#{&apos;${test.list:}&apos;.empty ? null : &apos;${test.list:}&apos;.split(&apos;,&apos;)}&quot;)  
	private List testList;      
    
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;测试：&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;配置中心更改后，路径自动生效&lt;/p&gt;
&lt;h2&gt;遇到的&lt;strong&gt;报错&lt;/strong&gt;：&lt;/h2&gt;
&lt;p&gt;nested exception is java.lang.IllegalArgumentException: Could not resolve placeholder ‘XXX’ in value “&lt;strong&gt;${XXX}&lt;/strong&gt;”&lt;/p&gt;
&lt;p&gt;出现这种问题的几种原因：&lt;/p&gt;
&lt;p&gt;1、你的配置文件properties 或者 yml文件，没有写上对应的XXX及对应的值&lt;/p&gt;
&lt;p&gt;2、在引用的地方加上默认值,格式是 @Value(&quot;${xxx:home}&quot;)&lt;/p&gt;
&lt;p&gt;3、&lt;strong&gt;多环境配置&lt;/strong&gt;的时候，默认的文件没有配置上你调用的自定义值&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;这个情况比较特殊，是因为你的多环境配置文件是这样的时候，记住，是&lt;strong&gt;每个&lt;/strong&gt;环境及&lt;strong&gt;默认&lt;/strong&gt;的配置文件都要加上你的自定义属性。&lt;/p&gt;
&lt;p&gt;一般配置在远端激活的配置文件中可避免出错，eg:  active:dev；则配置参数在nacos配置中心里面的 xx-dev.yml 中&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;如果上述的默认配置文件你没加自定义属性，会导致本地开发的时候报上述错误。
而你打包的时候并不会报错，因为你勾选了环境，只有idea启动的时候会报错，一开始让我还纳闷了好一&lt;/p&gt;
</content:encoded></item><item><title>SpringCloud微服务熔断机制与网关实践</title><link>https://tangtangit.com/posts/springcloud-04-%E7%86%94%E6%96%AD%E4%B8%8E%E7%BD%91%E5%85%B3/</link><guid isPermaLink="true">https://tangtangit.com/posts/springcloud-04-%E7%86%94%E6%96%AD%E4%B8%8E%E7%BD%91%E5%85%B3/</guid><description>深入理解微服务熔断降级机制、Hystrix工作流程、服务雪崩效应、网关原理与实战配置。</description><pubDate>Wed, 11 Mar 2026 00:00:00 GMT</pubDate><content:encoded>&lt;h1&gt;0.学习目标&lt;/h1&gt;
&lt;ul&gt;
&lt;li&gt;会配置Hystix熔断&lt;/li&gt;
&lt;li&gt;会使用Feign进行远程调用&lt;/li&gt;
&lt;li&gt;能独立搭建Zuul网关&lt;/li&gt;
&lt;li&gt;能编写Zuul的拦截器&lt;/li&gt;
&lt;/ul&gt;
&lt;h1&gt;1.Hystix&lt;/h1&gt;
&lt;h2&gt;1.1.简介&lt;/h2&gt;
&lt;p&gt;Hystix，即熔断器。&lt;/p&gt;
&lt;p&gt;主页：https://github.com/Netflix/Hystrix/&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://tangtangit.com/_astro/1525658740266.DCoRlO5F_1Pr7C8.webp&quot; alt=&quot;1525658740266&quot; /&gt;&lt;/p&gt;
&lt;p&gt;Hystix是Netflix开源的一个延迟和容错库，用于隔离访问远程服务、第三方库，防止出现级联失败。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://tangtangit.com/_astro/1525658562507.sjVrnA_n_Z1Oa4w8.webp&quot; alt=&quot;1525658562507&quot; /&gt;&lt;/p&gt;
&lt;h2&gt;1.2.熔断器的工作机制：&lt;/h2&gt;
&lt;p&gt;&lt;img src=&quot;https://tangtangit.com/_astro/1525658640314.C3L62u5Y_Z243tyI.webp&quot; alt=&quot;1525658640314&quot; /&gt;&lt;/p&gt;
&lt;p&gt;正常工作的情况下，客户端请求调用服务API接口：&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://tangtangit.com/_astro/1525658906255._EC3ux41_YUU.webp&quot; alt=&quot;1525658906255&quot; /&gt;&lt;/p&gt;
&lt;p&gt;当有服务出现异常时，直接进行失败回滚，服务降级处理：&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://tangtangit.com/_astro/1525658983518.BRaiMXNr_28Vg85.webp&quot; alt=&quot;1525658983518&quot; /&gt;&lt;/p&gt;
&lt;p&gt;当服务繁忙时，如果服务出现异常，不是粗暴的直接报错，而是返回一个友好的提示，虽然拒绝了用户的访问，但是会返回一个结果。&lt;/p&gt;
&lt;p&gt;这就好比去买鱼，平常超市买鱼会额外赠送杀鱼的服务。等到逢年过节，超时繁忙时，可能就不提供杀鱼服务了，这就是服务的降级。&lt;/p&gt;
&lt;p&gt;系统特别繁忙时，一些次要服务暂时中断，优先保证主要服务的畅通，一切资源优先让给主要服务来使用，在双十一、618时，京东天猫都会采用这样的策略。&lt;/p&gt;
&lt;h2&gt;1.3.动手实践（线程隔离，降级）&lt;/h2&gt;
&lt;h3&gt;1.3.1.引入依赖&lt;/h3&gt;
&lt;p&gt;首先在user-consumer中引入Hystix依赖：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&amp;lt;dependency&amp;gt;
    &amp;lt;groupId&amp;gt;org.springframework.cloud&amp;lt;/groupId&amp;gt;
    &amp;lt;artifactId&amp;gt;spring-cloud-starter-netflix-hystrix&amp;lt;/artifactId&amp;gt;
&amp;lt;/dependency&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;1.3.2.开启熔断&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;谁消费，谁负责熔断处理&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;@SpringCloudApplication 包含了一下注解&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;@SpringBootApplication&lt;/p&gt;
&lt;p&gt;@EnableDiscoveryClient     客户端发现服务&lt;/p&gt;
&lt;p&gt;@EnableCircuitBreaker	开启熔断&lt;/p&gt;
&lt;/blockquote&gt;
&lt;pre&gt;&lt;code&gt;@SpringCloudApplication
public class ConsumerApplication {

    public static void main(String[] args) {
        SpringApplication.run(ConsumerApplication.class, args);
    }

}
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;1.3.2.改造消费者&lt;/h3&gt;
&lt;p&gt;我们改造user-consumer，添加一个用来访问的user服务的DAO，并且声明一个失败时的回滚处理函数：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;@Component
public class UserDao {

    @Autowired
    private RestTemplate restTemplate;

    private static final Logger logger = LoggerFactory.getLogger(UserDao.class);

    @HystrixCommand(fallbackMethod = &quot;queryUserByIdFallback&quot;)
    public User queryUserById(Long id){
        long begin = System.currentTimeMillis();
        String url = &quot;http://user-service/user/&quot; + id;
        User user = this.restTemplate.getForObject(url, User.class);
        long end = System.currentTimeMillis();
        // 记录访问用时：
        logger.info(&quot;访问用时：{}&quot;, end - begin);
        return user;
    }

    public User queryUserByIdFallback(Long id){
        User user = new User();
        user.setId(id);
        user.setName(&quot;用户信息查询出现异常！&quot;);
        return user;
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;@HystrixCommand(fallbackMethod=&quot;queryUserByIdFallback&quot;)&lt;/code&gt;：声明一个失败回滚处理函数queryUserByIdFallback，当queryUserById执行超时（默认是1000毫秒），就会执行fallback函数，返回错误提示。&lt;/li&gt;
&lt;li&gt;为了方便查看熔断的触发时机，我们记录请求访问时间。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;在原来的业务逻辑中调用这个DAO：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;@Service
public class UserService {

    @Autowired
    private UserDao userDao;

    public List&amp;lt;User&amp;gt; queryUserByIds(List&amp;lt;Long&amp;gt; ids) {
        List&amp;lt;User&amp;gt; users = new ArrayList&amp;lt;&amp;gt;();
        ids.forEach(id -&amp;gt; {
            // 我们测试多次查询，
            users.add(this.userDao.queryUserById(id));
        });
        return users;
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;1.3.2.2.改造消费者方式二：&lt;/h3&gt;
&lt;blockquote&gt;
&lt;p&gt;全局配置熔断降级方法&lt;/p&gt;
&lt;/blockquote&gt;
&lt;pre&gt;&lt;code&gt;@Component
//第一步： 统一类中，统一处理降级方法
@DefaultProperties(defaultFallBack = &quot;queryUserByIdFallback&quot;)
public class UserDao {

    @Autowired
    private RestTemplate restTemplate;

    private static final Logger logger = LoggerFactory.getLogger(UserDao.class);

  	//第二步：标记此方法做降级处理
    @HystrixCommand
    public User queryUserById(Long id){
        long begin = System.currentTimeMillis();
        String url = &quot;http://user-service/user/&quot; + id;
        User user = this.restTemplate.getForObject(url, User.class);
        long end = System.currentTimeMillis();
        // 记录访问用时：
        logger.info(&quot;访问用时：{}&quot;, end - begin);
        return user;
    }

    //第三步：该降级方法，因为匹配全部方法，所有入参一定不能有
    public User queryUserByIdFallback(){
        User user = new User();
        user.setId(id);
        user.setName(&quot;用户信息查询出现异常！&quot;);
        return user;
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;1.3.2.3.改造消费者方式三：&lt;/h3&gt;
&lt;blockquote&gt;
&lt;p&gt;全局配置熔断降级，单个配置超时时长&lt;/p&gt;
&lt;/blockquote&gt;
&lt;pre&gt;&lt;code&gt;@Component
//第一步： 统一类中，统一处理降级方法
@DefaultProperties(defaultFallBack = &quot;queryUserByIdFallback&quot;)
public class UserDao {

    @Autowired
    private RestTemplate restTemplate;

    private static final Logger logger = LoggerFactory.getLogger(UserDao.class);

  	//第二步：标记此方法做降级处理，配置超时时长
    @HystrixCommand(
    	@HystrixProperties(name = &quot;execution.isolation.thread.timeoutInMilliseconds&quot;
                           ,value=&quot;2000&quot;)
    )
    public User queryUserById(Long id){
        long begin = System.currentTimeMillis();
        String url = &quot;http://user-service/user/&quot; + id;
        User user = this.restTemplate.getForObject(url, User.class);
        long end = System.currentTimeMillis();
        // 记录访问用时：
        logger.info(&quot;访问用时：{}&quot;, end - begin);
        return user;
    }

    //第三步：该降级方法，因为匹配全部方法，所有入参一定不能有
    public User queryUserByIdFallback(){
        User user = new User();
        user.setId(id);
        user.setName(&quot;用户信息查询出现异常！&quot;);
        return user;
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;1.3.3.改造服务提供者&lt;/h3&gt;
&lt;p&gt;改造服务提供者，随机休眠一段时间，以触发熔断：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;@Service
public class UserService {

    @Autowired
    private UserMapper userMapper;

    public User queryById(Long id) throws InterruptedException {
        // 为了演示超时现象，我们在这里然线程休眠,时间随机 0~2000毫秒
        Thread.sleep(new Random().nextInt(2000));
        return this.userMapper.selectByPrimaryKey(id);
    }
}

&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;1.3.4.启动测试&lt;/h3&gt;
&lt;p&gt;然后运行并查看日志：&lt;/p&gt;
&lt;p&gt;id为9、10、11的访问时间分别是：&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://tangtangit.com/_astro/1525661641660.C86HNzSZ_1BY93o.webp&quot; alt=&quot;1525661641660&quot; /&gt;&lt;/p&gt;
&lt;p&gt;id为12的访问时间：&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://tangtangit.com/_astro/1525661669136.B18Uqgul_Z7BJ7B.webp&quot; alt=&quot;1525661669136&quot; /&gt;&lt;/p&gt;
&lt;p&gt;因此，只有12是正常访问，其它都会触发熔断，我们来查看结果：&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://tangtangit.com/_astro/1525661720656.8dEtTwzb_1THDXS.webp&quot; alt=&quot;1525661720656&quot; /&gt;&lt;/p&gt;
&lt;h3&gt;1.3.5.优化&lt;/h3&gt;
&lt;p&gt;虽然熔断实现了，但是我们的重试机制似乎没有生效，是这样吗？&lt;/p&gt;
&lt;p&gt;其实这里是因为我们的Ribbon超时时间设置的是1000ms:&lt;/p&gt;
&lt;p&gt;​	&lt;img src=&quot;https://tangtangit.com/_astro/1525666632542.CRioCsur_8nvOs.webp&quot; alt=&quot;1525666632542&quot; /&gt;&lt;/p&gt;
&lt;p&gt;而&lt;strong&gt;Hystix的超时时间默认也是1000ms&lt;/strong&gt;，因此重试机制没有被触发，而是先触发了熔断。&lt;/p&gt;
&lt;p&gt;所以，&lt;strong&gt;Ribbon的超时时间一定要小于Hystix的超时时间&lt;/strong&gt;。&lt;/p&gt;
&lt;p&gt;我们可以通过&lt;code&gt;hystrix.command.default.execution.isolation.thread.timeoutInMilliseconds&lt;/code&gt;来设置Hystrix超时时间。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;hystrix:
  command:
  	default:
        execution:
          isolation:
            thread:
              timeoutInMillisecond: 6000 # 设置hystrix的超时时间为6000ms
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;1.4熔断：&lt;/h2&gt;
&lt;p&gt;&lt;img src=&quot;%E7%86%94%E6%96%AD%E6%9C%BA%E5%88%B6.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;状态机制有3个状态：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Closed：关闭状态（断路器关闭），所有请求都正常访问。&lt;/li&gt;
&lt;li&gt;Open：打开状态（断路器打开），所有请求都会被降级。Hystix会对请求情况计数，当一定时间内失败请求百分比达到阈值，则出发熔断，断路器会完全关闭。默认失败比例的阈值是50%，请求次数最少不低于20次。&lt;/li&gt;
&lt;li&gt;Half Open：半开状态，Closed状态不是永远的，关闭后会进入休眠时间（默认5秒）。随后断路器会自动进入半开状态。此时会释放部分请求通过，若这些请求都是健康的，则会完全打开断路器，否则继续保持关闭，再次进行休眠计时。&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;1.4.1手动制造熔断&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;@GetMapping
@HystrixCommand(
  @HystrixProperties(name = &quot;circuitBreaker.requestVolumeThreshold &quot;
                     ,value=&quot;10&quot;
                     name = &quot;circuitBreaker.sleepWindowInMilliseconds&quot;
                     ,value=&quot;10000&quot;
                     name = &quot;circuitBreaker.errorThresholdPercentage&quot;
                     ,value=&quot;60&quot;
                    )
)
public String queryById(@PathVarible(&quot;id&quot;)Long id){
  if(id == 1){
    throw new RuntimeException();
  } 
  String url = &quot;http://user-service/user/&quot;+id
  String user = restTemplate.getForObject(url,String.class);  
  return user;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;如果参数是id为1，一定失败，其他情况都成功。&lt;/p&gt;
&lt;p&gt;我们准备两个请求窗口：&lt;/p&gt;
&lt;p&gt;一个请求：http://localhost:8080/comsumer/1,注定失败&lt;/p&gt;
&lt;p&gt;一个请求：http://localhost:8080/comsumer/2,注定成功&lt;/p&gt;
&lt;p&gt;当我们疯狂访问id为1的请求时（超过20次），就回触发熔断。断路器端口，一起请求都会被降级处理。此时你访问id为2的请求，会发现返回的也是失败，而且失败时间很短，只有20毫秒左右；&lt;/p&gt;
&lt;p&gt;不过，默认的熔断触发要求较高，休眠时间窗较短，为了测试方便，我们可以通过配置修改熔断策略：&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;1  circuitBreaker.requestVolumeThreshold = 10&lt;/p&gt;
&lt;p&gt;2  circuitBreaker.sleepWindowInMilliseconds = 10000&lt;/p&gt;
&lt;p&gt;3  circuitBreaker.errorThresholdPercentage = 50&lt;/p&gt;
&lt;p&gt;解读：&lt;/p&gt;
&lt;p&gt;requestVolumeThreshold ：触发熔断的最小请求次数，默认20&lt;/p&gt;
&lt;p&gt;sleepWindowInMilliseconds ：休眠时长，默认是5000毫秒&lt;/p&gt;
&lt;p&gt;errorThresholdPercentage ：触发熔断的失败请求最小占比，默认50%&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h1&gt;2.Feign&lt;/h1&gt;
&lt;p&gt;在前面的学习中，我们使用了Ribbon的负载均衡功能，大大简化了远程调用时的代码：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;String baseUrl = &quot;http://user-service/user/&quot;;
User user = this.restTemplate.getForObject(baseUrl + id, User.class)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;如果就学到这里，你可能以后需要编写类似的大量重复代码，格式基本相同，无非参数不一样。有没有更优雅的方式，来对这些代码再次优化呢？&lt;/p&gt;
&lt;p&gt;这就是我们接下来要学的Feign的功能了。&lt;/p&gt;
&lt;h2&gt;2.1.简介&lt;/h2&gt;
&lt;p&gt;有道词典的英文解释：&lt;/p&gt;
&lt;p&gt;​	&lt;img src=&quot;https://tangtangit.com/_astro/1525662976679.8s1gt7DH_WD0gp.webp&quot; alt=&quot;1525662976679&quot; /&gt;&lt;/p&gt;
&lt;p&gt;为什么叫伪装？&lt;/p&gt;
&lt;p&gt;Feign可以把Rest的请求进行隐藏，伪装成类似SpringMVC的Controller一样。你不用再自己拼接url，拼接参数等等操作，一切都交给Feign去做。&lt;/p&gt;
&lt;p&gt;项目主页：https://github.com/OpenFeign/feign&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://tangtangit.com/_astro/1525652009416.9p8-fAHj_oN7Xb.webp&quot; alt=&quot;1525652009416&quot; /&gt;&lt;/p&gt;
&lt;h2&gt;2.2.快速入门&lt;/h2&gt;
&lt;h3&gt;2.2.1.导入依赖&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;&amp;lt;dependency&amp;gt;
    &amp;lt;groupId&amp;gt;org.springframework.cloud&amp;lt;/groupId&amp;gt;
    &amp;lt;artifactId&amp;gt;spring-cloud-starter-openfeign&amp;lt;/artifactId&amp;gt;
&amp;lt;/dependency&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;2.2.2.Feign的客户端&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;@FeignClient(&quot;user-service&quot;)
public interface UserFeignClient {

    @GetMapping(&quot;/user/{id}&quot;)
    User queryUserById(@PathVariable(&quot;id&quot;) Long id);
}
&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;首先这是一个接口，Feign会通过动态代理，帮我们生成实现类。这点跟mybatis的mapper很像&lt;/li&gt;
&lt;li&gt;&lt;code&gt;@FeignClient&lt;/code&gt;，声明这是一个Feign客户端，类似&lt;code&gt;@Mapper&lt;/code&gt;注解。同时通过&lt;code&gt;value&lt;/code&gt;属性指定服务名称&lt;/li&gt;
&lt;li&gt;接口中的定义方法，完全采用SpringMVC的注解，Feign会根据注解帮我们生成URL，并访问获取结果&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;改造原来的调用逻辑，不再调用UserDao：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;@Service
public class UserService {

    @Autowired
    private UserFeignClient userFeignClient;

    public List&amp;lt;User&amp;gt; queryUserByIds(List&amp;lt;Long&amp;gt; ids) {
        List&amp;lt;User&amp;gt; users = new ArrayList&amp;lt;&amp;gt;();
        ids.forEach(id -&amp;gt; {
            // 我们测试多次查询，
            users.add(this.userFeignClient.queryUserById(id));
        });
        return users;
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;2.2.3.开启Feign功能&lt;/h3&gt;
&lt;p&gt;我们在启动类上，添加注解，开启Feign功能&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;@SpringBootApplication
@EnableDiscoveryClient
@EnableHystrix
@EnableFeignClients // 开启Feign功能
public class UserConsumerDemoApplication {
    public static void main(String[] args) {
        SpringApplication.run(UserConsumerDemoApplication.class, args);
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;你会发现RestTemplate的注册被我删除了。&lt;strong&gt;Feign已经集成了Ribbon自动实现负载均衡&lt;/strong&gt;，因此我们不需要自己定义RestTemplate了&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;2.2.4.启动测试：&lt;/h3&gt;
&lt;p&gt;访问接口：&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://tangtangit.com/_astro/1525666476326.BWkaX_pX_ZRTXbC.webp&quot; alt=&quot;1525666476326&quot; /&gt;&lt;/p&gt;
&lt;p&gt;正常获取到了结果。&lt;/p&gt;
&lt;h2&gt;2.3.负载均衡&lt;/h2&gt;
&lt;p&gt;Feign中本身已经集成了Ribbon依赖和自动配置：&lt;/p&gt;
&lt;p&gt;​	&lt;img src=&quot;https://tangtangit.com/_astro/1525672070679.eLhDMuDQ_icBjc.webp&quot; alt=&quot;1525672070679&quot; /&gt;&lt;/p&gt;
&lt;p&gt;因此我们不需要额外引入依赖，也不需要再注册&lt;code&gt;RestTemplate&lt;/code&gt;对象。&lt;/p&gt;
&lt;p&gt;另外，我们可以像上节课中讲的那样去配置Ribbon，可以通过&lt;code&gt;ribbon.xx&lt;/code&gt;来进行全局配置。也可以通过&lt;code&gt;服务名.ribbon.xx&lt;/code&gt;来对指定服务配置：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;user-service:
  ribbon:
    ConnectTimeout: 250 # 连接超时时间(ms)
    ReadTimeout: 1000 # 通信超时时间(ms)
    OkToRetryOnAllOperations: true # 是否对所有操作重试
    MaxAutoRetriesNextServer: 1 # 同一服务不同实例的重试次数
    MaxAutoRetries: 1 # 同一实例的重试次数
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;2.4.Hystix支持&lt;/h2&gt;
&lt;p&gt;Feign默认也有对Hystix的集成：&lt;/p&gt;
&lt;p&gt;​	&lt;img src=&quot;https://tangtangit.com/_astro/1525672466192.DeeSmHU2_ZEXxqg.webp&quot; alt=&quot;1525672466192&quot; /&gt;&lt;/p&gt;
&lt;p&gt;只不过，默认情况下是关闭的。我们需要通过下面的参数来开启：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;feign:
  hystrix:
    enabled: true # 开启Feign的熔断功能
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;但是，Feign中的Fallback配置不像Ribbon中那样简单了。&lt;/p&gt;
&lt;p&gt;1）首先，我们要定义一个类，实现刚才编写的UserFeignClient，作为fallback的处理类&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;@Component
public class UserFeignClientFallback implements UserFeignClient {
    @Override
    public User queryUserById(Long id) {
        User user = new User();
        user.setId(id);
        user.setName(&quot;用户查询出现异常！&quot;);
        return user;
    }
}

&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;2）然后在UserFeignClient中，指定刚才编写的实现类&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;@FeignClient(value = &quot;user-service&quot;, fallback = UserFeignClientFallback.class)
public interface UserFeignClient {

    @GetMapping(&quot;/user/{id}&quot;)
    User queryUserById(@PathVariable(&quot;id&quot;) Long id);
}

&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;3）重启测试：&lt;/p&gt;
&lt;p&gt;我们关闭user-service服务，然后在页面访问：&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://tangtangit.com/_astro/1525673049875.DGNEsj2v_Z13UF6n.webp&quot; alt=&quot;1525673049875&quot; /&gt;&lt;/p&gt;
&lt;h2&gt;2.5.请求压缩(了解)&lt;/h2&gt;
&lt;p&gt;Spring Cloud Feign 支持对请求和响应进行GZIP压缩，以减少通信过程中的性能损耗。通过下面的参数即可开启请求与响应的压缩功能：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;feign:
  compression:
    request:
      enabled: true # 开启请求压缩
    response:
      enabled: true # 开启响应压缩
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;同时，我们也可以对请求的数据类型，以及触发压缩的大小下限进行设置：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;feign:
  compression:
    request:
      enabled: true # 开启请求压缩
      mime-types: text/html,application/xml,application/json # 设置压缩的数据类型
      min-request-size: 2048 # 设置触发压缩的大小下限
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;注：上面的数据类型、压缩大小下限均为默认值。&lt;/p&gt;
&lt;h2&gt;2.6.日志级别(了解)&lt;/h2&gt;
&lt;p&gt;前面讲过，通过&lt;code&gt;logging.level.xx=debug&lt;/code&gt;来设置日志级别。然而这个对Fegin客户端而言不会产生效果。因为&lt;code&gt;@FeignClient&lt;/code&gt;注解修改的客户端在被代理时，都会创建一个新的Fegin.Logger实例。我们需要额外指定这个日志的级别才可以。&lt;/p&gt;
&lt;p&gt;1）设置com.leyou包下的日志级别都为debug&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;logging:
  level:
    com.leyou: debug
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;2）编写配置类，定义日志级别&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;@Configuration
public class FeignConfig {
    @Bean
    Logger.Level feignLoggerLevel(){
        return Logger.Level.FULL;
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这里指定的Level级别是FULL，Feign支持4种级别：&lt;/p&gt;
&lt;p&gt;​	&lt;img src=&quot;https://tangtangit.com/_astro/1525674373507.LsJAhVKk_1QVfA5.webp&quot; alt=&quot;1525674373507&quot; /&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;NONE：不记录任何日志信息，这是默认值。&lt;/li&gt;
&lt;li&gt;BASIC：仅记录请求的方法，URL以及响应状态码和执行时间&lt;/li&gt;
&lt;li&gt;HEADERS：在BASIC的基础上，额外记录了请求和响应的头信息&lt;/li&gt;
&lt;li&gt;FULL：记录所有请求和响应的明细，包括头信息、请求体、元数据。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;3）在FeignClient中指定配置类：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;@FeignClient(value = &quot;user-service&quot;, fallback = UserFeignClientFallback.class, configuration = FeignConfig.class)
public interface UserFeignClient {
    @GetMapping(&quot;/user/{id}&quot;)
    User queryUserById(@PathVariable(&quot;id&quot;) Long id);
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;4）重启项目，即可看到每次访问的日志：&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://tangtangit.com/_astro/1525674544569.xtXwCAlU_Z1kGs5U.webp&quot; alt=&quot;1525674544569&quot; /&gt;&lt;/p&gt;
&lt;h1&gt;3.Zuul网关&lt;/h1&gt;
&lt;p&gt;通过前面的学习，使用Spring Cloud实现微服务的架构基本成型，大致是这样的：&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://tangtangit.com/_astro/1525674644660.Cy3oHk7y_Z1XzIux.webp&quot; alt=&quot;1525674644660&quot; /&gt;&lt;/p&gt;
&lt;p&gt;我们使用Spring Cloud Netflix中的Eureka实现了服务注册中心以及服务注册与发现；而服务间通过Ribbon或Feign实现服务的消费以及均衡负载；通过Spring Cloud Config实现了应用多环境的外部化配置以及版本管理。为了使得服务集群更为健壮，使用Hystrix的融断机制来避免在微服务架构中个别服务出现异常时引起的故障蔓延。&lt;/p&gt;
&lt;p&gt;在该架构中，我们的服务集群包含：内部服务Service A和Service B，他们都会注册与订阅服务至Eureka Server，而Open Service是一个对外的服务，通过均衡负载公开至服务调用方。我们把焦点聚集在对外服务这块，直接暴露我们的服务地址，这样的实现是否合理，或者是否有更好的实现方式呢？&lt;/p&gt;
&lt;p&gt;先来说说这样架构需要做的一些事儿以及存在的不足：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;首先，破坏了服务无状态特点。
&lt;ul&gt;
&lt;li&gt;为了保证对外服务的安全性，我们需要实现对服务访问的权限控制，而开放服务的权限控制机制将会贯穿并污染整个开放服务的业务逻辑，这会带来的最直接问题是，破坏了服务集群中REST API无状态的特点。&lt;/li&gt;
&lt;li&gt;从具体开发和测试的角度来说，在工作中除了要考虑实际的业务逻辑之外，还需要额外考虑对接口访问的控制处理。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;其次，无法直接复用既有接口。
&lt;ul&gt;
&lt;li&gt;当我们需要对一个即有的集群内访问接口，实现外部服务访问时，我们不得不通过在原有接口上增加校验逻辑，或增加一个代理调用来实现权限控制，无法直接复用原有的接口。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;面对类似上面的问题，我们要如何解决呢？答案是：服务网关！&lt;/p&gt;
&lt;p&gt;为了解决上面这些问题，我们需要将权限控制这样的东西从我们的服务单元中抽离出去，而最适合这些逻辑的地方就是处于对外访问最前端的地方，我们需要一个更强大一些的均衡负载器的 服务网关。&lt;/p&gt;
&lt;p&gt;服务网关是微服务架构中一个不可或缺的部分。通过服务网关统一向外系统提供REST API的过程中，除了具备服务路由、均衡负载功能之外，它还具备了&lt;code&gt;权限控制&lt;/code&gt;等功能。Spring Cloud Netflix中的Zuul就担任了这样的一个角色，为微服务架构提供了前门保护的作用，同时将权限控制这些较重的非业务逻辑内容迁移到服务路由层面，使得服务集群主体能够具备更高的可复用性和可测试性。&lt;/p&gt;
&lt;h2&gt;3.1.简介&lt;/h2&gt;
&lt;p&gt;官网：https://github.com/Netflix/zuul&lt;/p&gt;
&lt;p&gt;​	&lt;img src=&quot;https://tangtangit.com/_astro/1525675037152.Dlwo4YZM_1MoDGo.webp&quot; alt=&quot;1525675037152&quot; /&gt;&lt;/p&gt;
&lt;p&gt;Zuul：维基百科：&lt;/p&gt;
&lt;p&gt;电影《捉鬼敢死队》中的怪兽，Zuul，在纽约引发了巨大骚乱。&lt;/p&gt;
&lt;p&gt;事实上，在微服务架构中，Zuul就是守门的大Boss！一夫当关，万夫莫开！&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://tangtangit.com/_astro/1525675168152.BfbbQxFT_ZPzDPM.webp&quot; alt=&quot;1525675168152&quot; /&gt;&lt;/p&gt;
&lt;h2&gt;3.2.Zuul加入后的架构&lt;/h2&gt;
&lt;p&gt;&lt;img src=&quot;https://tangtangit.com/_astro/1525675648881.DUF-P6Th_Z1IHO0N.webp&quot; alt=&quot;1525675648881&quot; /&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;不管是来自于客户端（PC或移动端）的请求，还是服务内部调用。一切对服务的请求都会经过Zuul这个网关，然后再由网关来实现 鉴权、动态路由等等操作。Zuul就是我们服务的统一入口。&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;3.3.快速入门&lt;/h2&gt;
&lt;h3&gt;3.3.1.新建工程&lt;/h3&gt;
&lt;p&gt;填写基本信息：&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://tangtangit.com/_astro/1525675928548.Cx34vRpZ_lkxyI.webp&quot; alt=&quot;1525675928548&quot; /&gt;&lt;/p&gt;
&lt;p&gt;添加Zuul依赖：&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://tangtangit.com/_astro/1525675991833.p9KD4y1F_ZmVTBG.webp&quot; alt=&quot;1525675991833&quot; /&gt;&lt;/p&gt;
&lt;h3&gt;3.3.2.编写启动类&lt;/h3&gt;
&lt;p&gt;通过&lt;code&gt;@EnableZuulProxy &lt;/code&gt;注解开启Zuul的功能：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;@SpringBootApplication
@EnableZuulProxy // 开启Zuul的网关功能
public class ZuulDemoApplication {

	public static void main(String[] args) {
		SpringApplication.run(ZuulDemoApplication.class, args);
	}
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;3.3.3.编写配置&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;server:
  port: 10010 #服务端口
spring: 
  application:  
    name: api-gateway #指定服务名
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;3.3.4.编写路由规则&lt;/h3&gt;
&lt;p&gt;我们需要用Zuul来代理user-service服务，先看一下控制面板中的服务状态：&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://tangtangit.com/_astro/1525676797879.dOh01npG_9incB.webp&quot; alt=&quot;1525676797879&quot; /&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;ip为：127.0.0.1&lt;/li&gt;
&lt;li&gt;端口为：8081&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;映射规则：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;zuul:
  routes:
    user-service: # 这里是路由id，随意写
      path: /user-service/** # 这里是映射路径
      url: http://127.0.0.1:8081 # 映射路径对应的实际url地址
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;我们将符合&lt;code&gt;path&lt;/code&gt; 规则的一切请求，都代理到 &lt;code&gt;url&lt;/code&gt;参数指定的地址&lt;/p&gt;
&lt;p&gt;本例中，我们将 &lt;code&gt;/user-service/**&lt;/code&gt;开头的请求，代理到http://127.0.0.1:8081&lt;/p&gt;
&lt;h3&gt;3.3.5.启动测试：&lt;/h3&gt;
&lt;p&gt;访问的路径中需要加上配置规则的映射路径，我们访问：http://127.0.0.1:8081/user-service/user/10&lt;/p&gt;
&lt;p&gt;​	&lt;img src=&quot;https://tangtangit.com/_astro/1525677046705.C4hNHXVc_Z1KV08M.webp&quot; alt=&quot;1525677046705&quot; /&gt;&lt;/p&gt;
&lt;h2&gt;3.4.面向服务的路由&lt;/h2&gt;
&lt;p&gt;在刚才的路由规则中，我们把路径对应的服务地址写死了！如果同一服务有多个实例的话，这样做显然就不合理了。&lt;/p&gt;
&lt;p&gt;我们应该根据服务的名称，去Eureka注册中心查找 服务对应的所有实例列表，然后进行动态路由才对！&lt;/p&gt;
&lt;h3&gt;3.4.1.添加Eureka客户端依赖&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;&amp;lt;dependency&amp;gt;
    &amp;lt;groupId&amp;gt;org.springframework.cloud&amp;lt;/groupId&amp;gt;
    &amp;lt;artifactId&amp;gt;spring-cloud-starter-netflix-eureka-client&amp;lt;/artifactId&amp;gt;
&amp;lt;/dependency&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;3.4.2.开启Eureka客户端发现功能&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;@SpringBootApplication
@EnableZuulProxy // 开启Zuul的网关功能
@EnableDiscoveryClient
public class ZuulDemoApplication {

	public static void main(String[] args) {
		SpringApplication.run(ZuulDemoApplication.class, args);
	}
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;3.4.3.添加Eureka配置，获取服务信息&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;eureka:
  client:
    registry-fetch-interval-seconds: 5 # 获取服务列表的周期：5s
    service-url:
      defaultZone: http://127.0.0.1:10086/eureka
  instance:
    prefer-ip-address: true
    ip-address: 127.0.0.1
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;3.4.4.修改映射配置，通过服务名称获取&lt;/h3&gt;
&lt;p&gt;因为已经有了Eureka客户端，我们可以从Eureka获取服务的地址信息，因此映射时无需指定IP地址，而是通过服务名称来访问，而且Zuul已经集成了Ribbon的负载均衡功能。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;zuul:
  routes:
    user-service: # 这里是路由id，随意写
      path: /user-service/** # 这里是映射路径
      serviceId: user-service # 指定服务名称
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;3.4.5.启动测试&lt;/h3&gt;
&lt;p&gt;再次启动，这次Zuul进行代理时，会利用Ribbon进行负载均衡访问：&lt;/p&gt;
&lt;p&gt;​	&lt;img src=&quot;https://tangtangit.com/_astro/1525677821212.EOvCvj7Y_Z26e9AY.webp&quot; alt=&quot;1525677821212&quot; /&gt;&lt;/p&gt;
&lt;p&gt;日志中可以看到使用了负载均衡器：&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://tangtangit.com/_astro/1525677891119.CncBbj-C_avj0z.webp&quot; alt=&quot;1525677891119&quot; /&gt;&lt;/p&gt;
&lt;h2&gt;3.5.简化的路由配置&lt;/h2&gt;
&lt;p&gt;在刚才的配置中，我们的规则是这样的：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;zuul.routes.&amp;lt;route&amp;gt;.path=/xxx/**&lt;/code&gt;： 来指定映射路径。&lt;code&gt;&amp;lt;route&amp;gt;&lt;/code&gt;是自定义的路由名&lt;/li&gt;
&lt;li&gt;&lt;code&gt;zuul.routes.&amp;lt;route&amp;gt;.serviceId=/user-service&lt;/code&gt;：来指定服务名。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;而大多数情况下，我们的&lt;code&gt;&amp;lt;route&amp;gt;&lt;/code&gt;路由名称往往和 服务名会写成一样的。因此Zuul就提供了一种简化的配置语法：&lt;code&gt;zuul.routes.&amp;lt;serviceId&amp;gt;=&amp;lt;path&amp;gt;&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;比方说上面我们关于user-service的配置可以简化为一条：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;zuul:
  routes:
    user-service: /user-service/** # 这里是映射路径
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;省去了对服务名称的配置。&lt;/p&gt;
&lt;h2&gt;3.6.默认的路由规则&lt;/h2&gt;
&lt;p&gt;在使用Zuul的过程中，上面讲述的规则已经大大的简化了配置项。但是当服务较多时，配置也是比较繁琐的。因此Zuul就指定了默认的路由规则：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;默认情况下，一切服务的映射路径就是服务名本身。
&lt;ul&gt;
&lt;li&gt;例如服务名为：&lt;code&gt;user-service&lt;/code&gt;，则默认的映射路径就是：&lt;code&gt;/user-service/**&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;也就是说，刚才的映射规则我们完全不配置也是OK的，不信就试试看。&lt;/p&gt;
&lt;h2&gt;3.7.路由前缀&lt;/h2&gt;
&lt;p&gt;配置示例：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;zuul:
  prefix: /api # 添加路由前缀
  routes:
      user-service: # 这里是路由id，随意写
        path: /user-service/** # 这里是映射路径
        service-id: user-service # 指定服务名称
  ignored-services:
  	- 忽略的服务id
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;我们通过&lt;code&gt;zuul.prefix=/api&lt;/code&gt;来指定了路由的前缀，这样在发起请求时，路径就要以/api开头。&lt;/p&gt;
&lt;p&gt;路径&lt;code&gt;/api/user-service/user/1&lt;/code&gt;将会被代理到&lt;code&gt;/user-service/user/1&lt;/code&gt;&lt;/p&gt;
&lt;h2&gt;3.8.过滤器&lt;/h2&gt;
&lt;p&gt;Zuul作为网关的其中一个重要功能，就是实现请求的鉴权。而这个动作我们往往是通过Zuul提供的过滤器来实现的。&lt;/p&gt;
&lt;h3&gt;3.8.1.ZuulFilter&lt;/h3&gt;
&lt;p&gt;ZuulFilter是过滤器的顶级父类。在这里我们看一下其中定义的4个最重要的方法：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;public abstract ZuulFilter implements IZuulFilter{

    abstract public String filterType(); //过滤器类型

    abstract public int filterOrder(); //过滤器顺序
    
    boolean shouldFilter();// 来自IZuulFilter，要不要过滤

    Object run() throws ZuulException;// IZuulFilter，过滤逻辑
}
&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;shouldFilter&lt;/code&gt;：返回一个&lt;code&gt;Boolean&lt;/code&gt;值，判断该过滤器是否需要执行。返回true执行，返回false不执行。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;run&lt;/code&gt;：过滤器的具体业务逻辑。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;filterType&lt;/code&gt;：返回字符串，代表过滤器的类型。包含以下4种：
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;pre&lt;/code&gt;：请求在被路由之前执行&lt;/li&gt;
&lt;li&gt;&lt;code&gt;routing&lt;/code&gt;：在路由请求时调用&lt;/li&gt;
&lt;li&gt;&lt;code&gt;post&lt;/code&gt;：在routing和errror过滤器之后调用&lt;/li&gt;
&lt;li&gt;&lt;code&gt;error&lt;/code&gt;：处理请求时发生错误调用&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;code&gt;filterOrder&lt;/code&gt;：通过返回的int值来定义过滤器的执行顺序，数字越小优先级越高。&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;3.8.2.过滤器执行生命周期：&lt;/h3&gt;
&lt;p&gt;这张是Zuul官网提供的请求生命周期图，清晰的表现了一个请求在各个过滤器的执行顺序。&lt;/p&gt;
&lt;p&gt;​	&lt;img src=&quot;https://tangtangit.com/_astro/1525681866862.CWhoAK1p_1ipgxw.webp&quot; alt=&quot;1525681866862&quot; /&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;正常流程：
&lt;ul&gt;
&lt;li&gt;请求到达首先会经过pre类型过滤器，而后到达routing类型，进行路由，请求就到达真正的服务提供者，执行请求，返回结果后，会到达post过滤器。而后返回响应。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;异常流程：
&lt;ul&gt;
&lt;li&gt;整个过程中，pre或者routing过滤器出现异常，都会直接进入error过滤器，再error处理完毕后，会将请求交给POST过滤器，最后返回给用户。&lt;/li&gt;
&lt;li&gt;如果是error过滤器自己出现异常，最终也会进入POST过滤器，而后返回。&lt;/li&gt;
&lt;li&gt;如果是POST过滤器出现异常，会跳转到error过滤器，但是与pre和routing不同的时，请求不会再到达POST过滤器了。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;所有内置过滤器列表：&lt;/p&gt;
&lt;p&gt;​	&lt;img src=&quot;https://tangtangit.com/_astro/1525682427811.DdPnCV5N_Z1FlkFT.webp&quot; alt=&quot;1525682427811&quot; /&gt;&lt;/p&gt;
&lt;h3&gt;3.8.3.使用场景&lt;/h3&gt;
&lt;p&gt;场景非常多：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;请求鉴权：一般放在pre类型，如果发现没有访问权限，直接就拦截了&lt;/li&gt;
&lt;li&gt;异常处理：一般会在error类型和post类型过滤器中结合来处理。&lt;/li&gt;
&lt;li&gt;服务调用时长统计：pre和post结合使用。&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;3.9.自定义过滤器&lt;/h2&gt;
&lt;p&gt;接下来我们来自定义一个过滤器，模拟一个登录的校验。基本逻辑：如果请求中有access-token参数，则认为请求有效，放行。&lt;/p&gt;
&lt;h3&gt;3.9.1.定义过滤器类&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;@Component
public class LoginFilter extends ZuulFilter{
    @Override
    public String filterType() {
        // 登录校验，肯定是在前置拦截
        return &quot;pre&quot;;
    }

    @Override
    public int filterOrder() {
        // 顺序设置为1
        return 1;
    }

    @Override
    public boolean shouldFilter() {
        // 返回true，代表过滤器生效。
        return true;
    }

    @Override
    public Object run() throws ZuulException {
        // 登录校验逻辑。
        // 1）获取Zuul提供的请求上下文对象
        RequestContext ctx = RequestContext.getCurrentContext();
        // 2) 从上下文中获取request对象
        HttpServletRequest req = ctx.getRequest();
        // 3) 从请求中获取token
        String token = req.getParameter(&quot;access-token&quot;);
        // 4) 判断
        if(token == null || &quot;&quot;.equals(token.trim())){
            // 没有token，登录校验失败，拦截
            ctx.setSendZuulResponse(false);
            // 返回401状态码。也可以考虑重定向到登录页。
            ctx.setResponseStatusCode(HttpStatus.UNAUTHORIZED.value());
        }
        // 校验通过，可以考虑把用户信息放入上下文，继续向后执行
        return null;
    }
}

&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;3.9.2.测试&lt;/h3&gt;
&lt;p&gt;没有token参数时，访问失败：&lt;/p&gt;
&lt;p&gt;​	&lt;img src=&quot;https://tangtangit.com/_astro/1525683285697.JvWEiwyj_Z9fypG.webp&quot; alt=&quot;1525683285697&quot; /&gt;&lt;/p&gt;
&lt;p&gt;添加token参数后：&lt;/p&gt;
&lt;p&gt;​	&lt;img src=&quot;https://tangtangit.com/_astro/1525683354113.CSTGYWwb_17sahN.webp&quot; alt=&quot;1525683354113&quot; /&gt;&lt;/p&gt;
&lt;h2&gt;3.10.负载均衡和熔断&lt;/h2&gt;
&lt;p&gt;Zuul中默认就已经集成了Ribbon负载均衡和Hystix熔断机制。但是所有的超时策略都是走的默认值，比如熔断超时时间只有1S，很容易就触发了。因此建议我们手动进行配置：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;zuul:
  retryable: true
ribbon:
  ConnectTimeout: 250 # 连接超时时间(ms)
  ReadTimeout: 2000 # 通信超时时间(ms)
  OkToRetryOnAllOperations: true # 是否对所有操作重试
  MaxAutoRetriesNextServer: 2 # 同一服务不同实例的重试次数
  MaxAutoRetries: 1 # 同一实例的重试次数
hystrix:
  command:
  	default:
        execution:
          isolation:
            thread:
              timeoutInMillisecond: 6000 # 熔断超时时长：6000ms
&lt;/code&gt;&lt;/pre&gt;
</content:encoded></item><item><title>SpringCloud Gateway：新一代微服务网关</title><link>https://tangtangit.com/posts/springcloud-03-gateway/</link><guid isPermaLink="true">https://tangtangit.com/posts/springcloud-03-gateway/</guid><description>系统讲解SpringCloud Gateway网关的核心概念、路由配置、Predicate断言、Filter过滤器、动态路由等特性。</description><pubDate>Sun, 08 Mar 2026 00:00:00 GMT</pubDate><content:encoded>&lt;h1&gt;spring-cloud-gateway&lt;/h1&gt;
&lt;h1&gt;一、引入依赖&lt;/h1&gt;
&lt;pre&gt;&lt;code&gt;    &amp;lt;dependencyManagement&amp;gt;
        &amp;lt;dependencies&amp;gt;
            &amp;lt;dependency&amp;gt;
                &amp;lt;groupId&amp;gt;org.springframework.cloud&amp;lt;/groupId&amp;gt;
                &amp;lt;artifactId&amp;gt;spring-cloud-dependencies&amp;lt;/artifactId&amp;gt;
                &amp;lt;version&amp;gt;${spring-cloud.version}&amp;lt;/version&amp;gt;
                &amp;lt;type&amp;gt;pom&amp;lt;/type&amp;gt;
                &amp;lt;scope&amp;gt;import&amp;lt;/scope&amp;gt;
            &amp;lt;/dependency&amp;gt;
        &amp;lt;/dependencies&amp;gt;
    &amp;lt;/dependencyManagement&amp;gt;

    &amp;lt;dependencies&amp;gt;
        &amp;lt;dependency&amp;gt;
            &amp;lt;groupId&amp;gt;org.springframework.cloud&amp;lt;/groupId&amp;gt;
            &amp;lt;artifactId&amp;gt;spring-cloud-starter-gateway&amp;lt;/artifactId&amp;gt;
        &amp;lt;/dependency&amp;gt;
    &amp;lt;/dependencies&amp;gt;  
&lt;/code&gt;&lt;/pre&gt;
&lt;h1&gt;二、运行即可启动&lt;/h1&gt;
&lt;h1&gt;三、网关功能&lt;/h1&gt;
&lt;p&gt;1、断言配置：只有通过过了断言才继续往下走&lt;/p&gt;
&lt;p&gt;2、路由，断言后指定走哪个路由&lt;/p&gt;
&lt;p&gt;3、路由过程中经过网关过滤器：网关内置了很多过滤器，可以按需选择（例如：路径重写过滤器）&lt;/p&gt;
&lt;h1&gt;四、网关路由配置&lt;/h1&gt;
&lt;pre&gt;&lt;code&gt;spring:
  cloud:
    gateway:
      routes:
        - id: coupon
          uri: lb://gulimall-coupon
          predicates:
            - Path=/api/coupon/**
          filters:
            - RewritePath=/api/coupon(?&amp;lt;segment&amp;gt;/?.*), /$\{segment}
        - id: product
          uri: lb://gulimall-product
          predicates:
            - Path=/api/product/**
          filters:
            - RewritePath=/api/(?&amp;lt;segment&amp;gt;/?.*), /$\{segment}
        - id: fast
          uri: lb://renren-fast
          predicates:
            - Path=/api/**
          filters:
            - RewritePath=/api/(?&amp;lt;segment&amp;gt;/?.*), /renren-fast/$\{segment}
&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote&gt;
&lt;p&gt;id :随便取的名字&lt;/p&gt;
&lt;p&gt;uri: 要路由到的地址（这里写的是服务名），lb:代表 负载均衡&lt;/p&gt;
&lt;p&gt;predicates： 断言规则，符合条件的进入该路由&lt;/p&gt;
&lt;p&gt;filters： 过滤器，配置到路由服务前，或则后的过滤规则（这里用到的是路径重写）&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;示例
- RewritePath=/api/(?&amp;lt;segment&amp;gt;/?.*), /$\{segment}
分析
api/cms/user/add       路径重写后    user/add
&lt;/code&gt;&lt;/pre&gt;
&lt;/blockquote&gt;
&lt;h1&gt;五、网关异常处理&lt;/h1&gt;
&lt;p&gt;Spring Cloud Gateway中的全局异常处理不能直接用@ControllerAdvice来处理，通过跟踪异常信息的抛出，找到对应的源码，自定义一些处理逻辑来符合业务的需求。&lt;/p&gt;
&lt;p&gt;网关都是给接口做代理转发的，后端对应的都是REST API，返回数据格式都是JSON。如果不做处理，当发生异常时，Gateway默认给出的错误信息是页面，不方便前端进行异常处理。&lt;/p&gt;
&lt;p&gt;需要对异常信息进行处理，返回JSON格式的数据给客户端。下面先看实现的代码，后面再跟大家讲下需要注意的地方。
自定义异常处理逻辑：&lt;/p&gt;
&lt;h2&gt;GlobalGatewayExceptionHandler&lt;/h2&gt;
&lt;blockquote&gt;
&lt;p&gt;异常处理器&lt;/p&gt;
&lt;/blockquote&gt;
&lt;pre&gt;&lt;code&gt;package com.easypark.gateway.exception;

import com.easypark.common.exception.EpException;
import com.easypark.common.utils.response.ServiceCodeEnum;
import lombok.Getter;
import lombok.Setter;
import lombok.extern.slf4j.Slf4j;
import org.springframework.boot.autoconfigure.web.ErrorProperties;
import org.springframework.boot.autoconfigure.web.ResourceProperties;
import org.springframework.boot.autoconfigure.web.reactive.error.DefaultErrorWebExceptionHandler;
import org.springframework.boot.web.reactive.error.ErrorAttributes;
import org.springframework.cloud.gateway.support.NotFoundException;
import org.springframework.context.ApplicationContext;
import org.springframework.http.HttpStatus;
import org.springframework.web.reactive.function.server.*;

import java.util.HashMap;
import java.util.Map;

/**
 * @author zy
 * @version 1.0
 * @Description 网关异常全局处理
 * @date 2021/4/29 9:16
 */
@Slf4j
@Getter
@Setter
public class GlobalGatewayExceptionHandler extends DefaultErrorWebExceptionHandler {

    public GlobalGatewayExceptionHandler(ErrorAttributes errorAttributes, ResourceProperties resourceProperties,
                                ErrorProperties errorProperties, ApplicationContext applicationContext) {
        super(errorAttributes, resourceProperties, errorProperties, applicationContext);
    }

    /**
     * 获取异常属性
     */
    @Override
    protected Map&amp;lt;String, Object&amp;gt; getErrorAttributes(ServerRequest request, boolean includeStackTrace) {
        int code = 500;
        Throwable error = super.getError(request);
        String message = this.buildMessage(request, error);
        if(error instanceof EpException){
            EpException epException = (EpException)error;
            code = epException.getErrorCode();
        }
        if(error instanceof NotFoundException){
            log.error(&quot;【服务器异常】url={}&quot;,request.uri().getPath());
            code = 503;
            message = ServiceCodeEnum.SERVICE_UNAVAILABLE.getMessage();
        }

        return response(code,message);
    }

    /**
     * 指定响应处理方法为JSON处理的方法
     * @param errorAttributes errorAttributes
     */
    @Override
    protected RouterFunction&amp;lt;ServerResponse&amp;gt; getRoutingFunction(ErrorAttributes errorAttributes) {
        return RouterFunctions.route(RequestPredicates.all(), this::renderErrorResponse);
    }

    /**
     * 根据code获取对应的HttpStatus
     * @param errorAttributes errorAttributes
     */
    @Override
    protected int getHttpStatus(Map&amp;lt;String, Object&amp;gt; errorAttributes) {
        int statusCode = (int) errorAttributes.get(&quot;code&quot;);
        return HttpStatus.valueOf(statusCode).value();
    }

    /**
     * 构建异常信息
     * @param request request
     * @param ex 异常
     * @return
     */
    private String buildMessage(ServerRequest request, Throwable ex) {
//        StringBuilder message = new StringBuilder(&quot;Failed to handle request [&quot;);
//        message.append(request.methodName());
//        message.append(&quot; &quot;);
//        message.append(request.uri());
//        message.append(&quot;]&quot;);
//        if (ex != null) {
//            message.append(&quot;: &quot;);
//            message.append(ex.getMessage());
//        }
//        return message.toString();
        return ex.getMessage().toString();
    }

    /**
     * 构建返回的JSON数据格式
     * @param status 状态码
     * @param errorMessage 异常信息
     * @return
     */
    public static Map&amp;lt;String, Object&amp;gt; response(int status, String errorMessage) {
        Map&amp;lt;String, Object&amp;gt; map = new HashMap&amp;lt;&amp;gt;();
        map.put(&quot;code&quot;, status);
        map.put(&quot;msg&quot;, errorMessage);
        map.put(&quot;data&quot;, null);
        return map;
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;GlobalGatewayExceptionConfig&lt;/h2&gt;
&lt;blockquote&gt;
&lt;p&gt;配置：异常处理器&lt;/p&gt;
&lt;/blockquote&gt;
&lt;pre&gt;&lt;code&gt;package com.easypark.gateway.exception;

import org.springframework.beans.factory.ObjectProvider;
import org.springframework.boot.autoconfigure.web.ResourceProperties;
import org.springframework.boot.autoconfigure.web.ServerProperties;
import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.boot.web.reactive.error.ErrorAttributes;
import org.springframework.boot.web.reactive.error.ErrorWebExceptionHandler;
import org.springframework.context.ApplicationContext;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Primary;
import org.springframework.core.Ordered;
import org.springframework.core.annotation.Order;
import org.springframework.http.codec.ServerCodecConfigurer;
import org.springframework.web.reactive.result.view.ViewResolver;

import java.util.Collections;
import java.util.List;

/**
 * @author zy
 * @version 1.0
 * @Description 网关异常配置，覆盖默认的异常处理
 * @date 2021/4/29 9:16
 */
@Configuration
@EnableConfigurationProperties({ServerProperties.class, ResourceProperties.class})
public class GlobalGatewayExceptionConfig {

    private final ServerProperties serverProperties;

    private final ApplicationContext applicationContext;

    private final ResourceProperties resourceProperties;

    private final List&amp;lt;ViewResolver&amp;gt; viewResolvers;

    private final ServerCodecConfigurer serverCodecConfigurer;

    public GlobalGatewayExceptionConfig(ServerProperties serverProperties,
                                        ApplicationContext applicationContext,
                                        ResourceProperties resourceProperties,
                                        ObjectProvider&amp;lt;List&amp;lt;ViewResolver&amp;gt;&amp;gt; viewResolversProvider,
                                        ServerCodecConfigurer serverCodecConfigurer) {
        this.serverProperties = serverProperties;
        this.applicationContext = applicationContext;
        this.resourceProperties = resourceProperties;
        this.viewResolvers = viewResolversProvider.getIfAvailable(Collections::emptyList);
        this.serverCodecConfigurer = serverCodecConfigurer;
    }

    @Primary
    @Bean
    @Order(Ordered.HIGHEST_PRECEDENCE)
    public ErrorWebExceptionHandler errorWebExceptionHandler(ErrorAttributes errorAttributes) {
        GlobalGatewayExceptionHandler exceptionHandler = new GlobalGatewayExceptionHandler(
                errorAttributes,
                this.resourceProperties,
                this.serverProperties.getError(),
                this.applicationContext);
        exceptionHandler.setViewResolvers(this.viewResolvers);
        exceptionHandler.setMessageWriters(this.serverCodecConfigurer.getWriters());
        exceptionHandler.setMessageReaders(this.serverCodecConfigurer.getReaders());
        return exceptionHandler;
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;注意点&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;异常时如何返回JSON而不是HTML&lt;/p&gt;
&lt;p&gt;在org.springframework.boot.autoconfigure.web.reactive.error.DefaultErrorWebExceptionHandler中的getRoutingFunction()方法就是控制返回格式的，原代码如下：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;@Override
protected RouterFunction&amp;lt;ServerResponse&amp;gt; getRoutingFunction(
 ErrorAttributes errorAttributes) {
  return RouterFunctions.route(acceptsTextHtml(), this::renderErrorView)
 .andRoute(RequestPredicates.all(), this::renderErrorResponse);
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这边优先是用HTML来显示的，想用JSON的改下就可以了，如下：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;protected RouterFunction&amp;lt;ServerResponse&amp;gt; getRoutingFunction(ErrorAttributes errorAttributes) {
 return RouterFunctions.route(RequestPredicates.all(), this::renderErrorResponse);
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;如果我们定义的格式中没有status字段的话，这么就会报错，找不到对应的响应码，要么返回数据格式中增加status子段，要么重写，我这边返回的是code，所以要重写，代码如下：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;    /**
     * 根据code获取对应的HttpStatus
     * @param errorAttributes errorAttributes
     */
    @Override
    protected int getHttpStatus(Map&amp;lt;String, Object&amp;gt; errorAttributes) {
        int statusCode = (int) errorAttributes.get(&quot;code&quot;);
        return HttpStatus.valueOf(statusCode).value();
    }
&lt;/code&gt;&lt;/pre&gt;
&lt;h1&gt;六、网关限流&lt;/h1&gt;
&lt;p&gt;从1.6.0版本开始，Sentinel提供了SpringCloud Gateway的适配模块，可以提供两种资源维度的限流：&lt;/p&gt;
&lt;p&gt;route维度：即在Spring配置文件中配置的路由条目，资源名为对应的routeId&lt;/p&gt;
&lt;p&gt;自定义API维度：用户可以利用Sentinel提供的API来自定义一些API分组&lt;/p&gt;
&lt;h2&gt;&lt;strong&gt;1&lt;/strong&gt; &lt;strong&gt;导入依赖&lt;/strong&gt;&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;&amp;lt;dependency&amp;gt;
    &amp;lt;groupId&amp;gt;com.alibaba.csp&amp;lt;/groupId&amp;gt;
    &amp;lt;artifactId&amp;gt;sentinel-spring-cloud-gateway-adapter&amp;lt;/artifactId&amp;gt;
&amp;lt;/dependency&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;&lt;strong&gt;2&lt;/strong&gt; &lt;strong&gt;编写配置类&lt;/strong&gt;&lt;/h2&gt;
&lt;p&gt;基于Sentinel 的Gateway限流是通过其提供的Filter来完成的，使用时只需注入对应的&lt;/p&gt;
&lt;p&gt;SentinelGatewayFilter实例以及 SentinelGatewayBlockExceptionHandler 实例即可。&lt;/p&gt;
&lt;h3&gt;路由限流：&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;package com.easypark.gateway.config;

import com.alibaba.csp.sentinel.adapter.gateway.common.rule.GatewayFlowRule;
import com.alibaba.csp.sentinel.adapter.gateway.common.rule.GatewayRuleManager;
import com.alibaba.csp.sentinel.adapter.gateway.sc.SentinelGatewayFilter;
import com.alibaba.csp.sentinel.adapter.gateway.sc.callback.BlockRequestHandler;
import com.alibaba.csp.sentinel.adapter.gateway.sc.callback.GatewayCallbackManager;
import com.alibaba.csp.sentinel.adapter.gateway.sc.exception.SentinelGatewayBlockExceptionHandler;
import com.easypark.common.utils.response.ServiceCodeEnum;
import org.springframework.beans.factory.ObjectProvider;
import org.springframework.cloud.gateway.filter.GlobalFilter;
import org.springframework.context.annotation.Bean;
import org.springframework.core.Ordered;
import org.springframework.core.annotation.Order;
import org.springframework.http.HttpStatus;
import org.springframework.http.MediaType;
import org.springframework.http.codec.ServerCodecConfigurer;
import org.springframework.web.reactive.function.BodyInserters;
import org.springframework.web.reactive.function.server.ServerResponse;
import org.springframework.web.reactive.result.view.ViewResolver;
import org.springframework.web.server.ServerWebExchange;
import reactor.core.publisher.Mono;

import javax.annotation.PostConstruct;
import java.util.*;

/**
 * @author zy
 * @version 1.0
 * @Description 网关配置
 * @date 2021/4/29 14:03
 */
//@Configuration
public class GatewayConfiguration {

    private final List&amp;lt;ViewResolver&amp;gt; viewResolvers;
    private final ServerCodecConfigurer serverCodecConfigurer;

    public GatewayConfiguration(ObjectProvider&amp;lt;List&amp;lt;ViewResolver&amp;gt;&amp;gt; viewResolversProvider, ServerCodecConfigurer serverCodecConfigurer) {
        this.viewResolvers = viewResolversProvider.getIfAvailable(Collections::emptyList);
        this.serverCodecConfigurer = serverCodecConfigurer;
    }

    /**
     * 初始化一个限流的过滤器
     *
     * @return GlobalFilter
     */
    @Bean
    @Order(Ordered.HIGHEST_PRECEDENCE)
    public GlobalFilter sentinelGatewayFilter() {
        return new SentinelGatewayFilter();
    }

    /**
     * 配置初始化的限流参数
     */
    @PostConstruct
    public void initGatewayRules() {
        Set&amp;lt;GatewayFlowRule&amp;gt; rules = new HashSet&amp;lt;&amp;gt;();
        //资源名称,对应路由id
        rules.add(new GatewayFlowRule(&quot;cms&quot;)
                // 限流阈值
                .setCount(100)
                // 统计时间窗口，单位是秒，默认是 1 秒
                .setIntervalSec(1));
        GatewayRuleManager.loadRules(rules);
    }

    /**
     * 配置限流的异常处理器
     *
     * @return SentinelGatewayBlockExceptionHandler
     */
    @Bean
    @Order(Ordered.HIGHEST_PRECEDENCE)
    public SentinelGatewayBlockExceptionHandler sentinelGatewayBlockExceptionHandler() {
        return new SentinelGatewayBlockExceptionHandler(viewResolvers, serverCodecConfigurer);
    }

    /**
     * 自定义限流异常页面
     */
    @PostConstruct
    public void initBlockHandlers() {
        BlockRequestHandler blockRequestHandler = new BlockRequestHandler() {
            @Override
            public Mono&amp;lt;ServerResponse&amp;gt; handleRequest(ServerWebExchange serverWebExchange, Throwable throwable) {
                Map map = new HashMap&amp;lt;&amp;gt;();
                map.put(&quot;code&quot;, ServiceCodeEnum.FAIL.getCode());
                map.put(&quot;msg&quot;, &quot;接口被限流了&quot;);
                return ServerResponse.status(HttpStatus.OK).
                        contentType(MediaType.APPLICATION_JSON_UTF8).
                        body(BodyInserters.fromObject(map));
            }
        };
        GatewayCallbackManager.setBlockHandler(blockRequestHandler);
    }

}
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;路由id加url限流&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;package com.easypark.gateway.config;

import com.alibaba.csp.sentinel.adapter.gateway.common.SentinelGatewayConstants;
import com.alibaba.csp.sentinel.adapter.gateway.common.api.ApiDefinition;
import com.alibaba.csp.sentinel.adapter.gateway.common.api.ApiPathPredicateItem;
import com.alibaba.csp.sentinel.adapter.gateway.common.api.ApiPredicateItem;
import com.alibaba.csp.sentinel.adapter.gateway.common.api.GatewayApiDefinitionManager;
import com.alibaba.csp.sentinel.adapter.gateway.common.rule.GatewayFlowRule;
import com.alibaba.csp.sentinel.adapter.gateway.common.rule.GatewayRuleManager;
import com.alibaba.csp.sentinel.adapter.gateway.sc.SentinelGatewayFilter;
import com.alibaba.csp.sentinel.adapter.gateway.sc.exception.SentinelGatewayBlockExceptionHandler;
import org.springframework.beans.factory.ObjectProvider;
import org.springframework.cloud.gateway.filter.GlobalFilter;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.Ordered;
import org.springframework.core.annotation.Order;
import org.springframework.http.codec.ServerCodecConfigurer;
import org.springframework.web.reactive.result.view.ViewResolver;

import javax.annotation.PostConstruct;
import java.util.Collections;
import java.util.HashSet;
import java.util.List;
import java.util.Set;

/**
 * @author zy
 * @version 1.0
 * @Description 网关配置
 * @date 2021/4/29 14:03
 */
@Configuration
public class GatewayConfigurationV2 {

    private final List&amp;lt;ViewResolver&amp;gt; viewResolvers;
    private final ServerCodecConfigurer serverCodecConfigurer;

    public GatewayConfigurationV2(ObjectProvider&amp;lt;List&amp;lt;ViewResolver&amp;gt;&amp;gt; viewResolversProvider, ServerCodecConfigurer serverCodecConfigurer) {
        this.viewResolvers = viewResolversProvider.getIfAvailable(Collections::emptyList);
        this.serverCodecConfigurer = serverCodecConfigurer;
    }

    /**
     * 初始化一个限流的过滤器
     *
     * @return GlobalFilter
     */
    @Bean
    @Order(Ordered.HIGHEST_PRECEDENCE)
    public GlobalFilter sentinelGatewayFilter() {
        return new SentinelGatewayFilter();
    }

    /*** 配置初始化的限流参数 */
    @PostConstruct
    public void initGatewayRules() {
        Set&amp;lt;GatewayFlowRule&amp;gt; rules = new HashSet&amp;lt;&amp;gt;();
        rules.add(new GatewayFlowRule(&quot;cms&quot;).setCount(1).setIntervalSec(1));
        rules.add(new GatewayFlowRule(&quot;product_api2&quot;).setCount(1).setIntervalSec(1));
        GatewayRuleManager.loadRules(rules);
    }

    /**
     * 配置限流的异常处理器
     *
     * @return SentinelGatewayBlockExceptionHandler
     */
    @Bean
    @Order(Ordered.HIGHEST_PRECEDENCE)
    public SentinelGatewayBlockExceptionHandler sentinelGatewayBlockExceptionHandler() {
        return new SentinelGatewayBlockExceptionHandler(viewResolvers, serverCodecConfigurer);
    }

    //自定义API分组
    @PostConstruct
    private void initCustomizedApis() {
        Set&amp;lt;ApiDefinition&amp;gt; definitions = new HashSet&amp;lt;&amp;gt;();
        ApiDefinition api1 = new ApiDefinition(&quot;cms&quot;)
                .setPredicateItems(new HashSet&amp;lt;ApiPredicateItem&amp;gt;() {{
                    // 以/api/cms/sys/adminUser/** 开头的请求
                    add(new ApiPathPredicateItem()
                            .setPattern(&quot;/api/cms/sys/adminUser/**&quot;)
                            .setMatchStrategy(SentinelGatewayConstants.URL_MATCH_STRATEGY_PREFIX));
                }});
        ApiDefinition api2 = new ApiDefinition(&quot;product_api2&quot;)
                .setPredicateItems(new HashSet&amp;lt;ApiPredicateItem&amp;gt;() {{
                    // 以/product-serv/product/api2/demo1 完成的url路径匹配
                    add(new ApiPathPredicateItem().setPattern(&quot;/product-serv/product/api2/demo1&quot;));
                }});
        definitions.add(api1);
        definitions.add(api2);
        GatewayApiDefinitionManager.loadApiDefinitions(definitions);
    }

}
&lt;/code&gt;&lt;/pre&gt;
</content:encoded></item><item><title>SpringCloud OpenFeign：声明式服务调用</title><link>https://tangtangit.com/posts/springcloud-02-openfeign/</link><guid isPermaLink="true">https://tangtangit.com/posts/springcloud-02-openfeign/</guid><description>讲解Feign与OpenFeign的区别、OpenFeign的配置使用、超时控制、日志增强、GZIP压缩等核心功能。</description><pubDate>Thu, 05 Mar 2026 00:00:00 GMT</pubDate><content:encoded>&lt;h1&gt;Spring Cloud OpenFeign&lt;/h1&gt;
&lt;p&gt;feign异常处理机制流程图：&lt;/p&gt;
&lt;blockquote&gt;
&lt;ul&gt;
&lt;li&gt;服务提供者，会有全局异常包裹&lt;/li&gt;
&lt;li&gt;服务消费者，不会有全局异常，只管往外抛出错误，feign感知到以后，走降级熔断。&lt;/li&gt;
&lt;/ul&gt;
&lt;/blockquote&gt;
&lt;p&gt;&lt;img src=&quot;openFeign%E5%BC%82%E5%B8%B8%E6%9E%B6%E6%9E%84%E5%9B%BE.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;h1&gt;pom文件&lt;/h1&gt;
&lt;pre&gt;&lt;code&gt;&amp;lt;dependencies&amp;gt;    
       &amp;lt;dependency&amp;gt;
            &amp;lt;groupId&amp;gt;org.springframework.cloud&amp;lt;/groupId&amp;gt;
            &amp;lt;artifactId&amp;gt;spring-cloud-starter-openfeign&amp;lt;/artifactId&amp;gt;
        &amp;lt;/dependency&amp;gt;
    &amp;lt;/dependencies&amp;gt;
    
&amp;lt;dependencyManagement&amp;gt;
        &amp;lt;dependencies&amp;gt;
            &amp;lt;dependency&amp;gt;
                &amp;lt;groupId&amp;gt;org.springframework.cloud&amp;lt;/groupId&amp;gt;
                &amp;lt;artifactId&amp;gt;spring-cloud-dependencies&amp;lt;/artifactId&amp;gt;
                &amp;lt;version&amp;gt;${spring-cloud.version}&amp;lt;/version&amp;gt;
                &amp;lt;type&amp;gt;pom&amp;lt;/type&amp;gt;
                &amp;lt;scope&amp;gt;import&amp;lt;/scope&amp;gt;
            &amp;lt;/dependency&amp;gt;
            &amp;lt;dependency&amp;gt;
                &amp;lt;groupId&amp;gt;com.alibaba.cloud&amp;lt;/groupId&amp;gt;
                &amp;lt;artifactId&amp;gt;spring-cloud-alibaba-dependencies&amp;lt;/artifactId&amp;gt;
                &amp;lt;version&amp;gt;${spring-cloud-ali.version}&amp;lt;/version&amp;gt;
                &amp;lt;type&amp;gt;pom&amp;lt;/type&amp;gt;
                &amp;lt;scope&amp;gt;import&amp;lt;/scope&amp;gt;
            &amp;lt;/dependency&amp;gt;
        &amp;lt;/dependencies&amp;gt;
    &amp;lt;/dependencyManagement&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;h1&gt;开启feign&lt;/h1&gt;
&lt;blockquote&gt;
&lt;p&gt;如果不在项目扫码范围内，需要添加&lt;code&gt;basePackages&lt;/code&gt;指定扫码包&lt;/p&gt;
&lt;/blockquote&gt;
&lt;pre&gt;&lt;code&gt;@EnableFeignClients(basePackages = &quot;com.zy.gulimall.common.feign&quot;)
&lt;/code&gt;&lt;/pre&gt;
&lt;h1&gt;开启hystrix&lt;/h1&gt;
&lt;pre&gt;&lt;code&gt;feign:
  hystrix:
    enabled: true
&lt;/code&gt;&lt;/pre&gt;
&lt;h1&gt;Feign logging日志&lt;/h1&gt;
&lt;p&gt;配置文件&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;logging:
  level:
    #com.eim.feign.ProFeig  为 feign包
    com.zy: debug
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;spring配置Log the headers, body, and metadata for both requests and responses.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;    @Configuration
    public class FooConfiguration {
        @Bean
        Logger.Level feignLoggerLevel() {
            return Logger.Level.FULL;
        }
    }
&lt;/code&gt;&lt;/pre&gt;
&lt;h1&gt;超时时间&lt;/h1&gt;
&lt;p&gt;如何配置好&lt;code&gt;Hystrix&lt;/code&gt;和&lt;code&gt;Ribbon&lt;/code&gt;的超时时间呢&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;其实是有套路的。因为&lt;code&gt;Feign&lt;/code&gt;的请求：其实是&lt;code&gt;Hystrix&lt;/code&gt;+&lt;code&gt;Ribbon&lt;/code&gt;。&lt;code&gt;Hystrix&lt;/code&gt;在最外层，然后再到&lt;code&gt;Ribbon&lt;/code&gt;，最后里面的是&lt;code&gt;http&lt;/code&gt;请求。所以说。&lt;code&gt;Hystrix&lt;/code&gt;的熔断时间必须大于&lt;code&gt;Ribbon&lt;/code&gt;的 ( &lt;code&gt;ConnectTimeout&lt;/code&gt; + &lt;code&gt;ReadTimeout&lt;/code&gt; )。而如果&lt;code&gt;Ribbon&lt;/code&gt;开启了重试机制，还需要乘以对应的重试次数，保证在&lt;code&gt;Ribbon&lt;/code&gt;里的请求还没结束时，&lt;code&gt;Hystrix&lt;/code&gt;的熔断时间不会超时。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;pre&gt;&lt;code&gt;#开启熔断降级 hystrix
feign:
  hystrix:
    enabled: true
#openfeign超时时间由ribbon控制
ribbon:
  # 连接超时时间(ms)
  ConnectTimeout: 5000 
  # 通信超时时间(ms)
  ReadTimeout: 5000 
  # 是否对所有操作重试
  OkToRetryOnAllOperations: true 
  # 同一服务不同实例的重试次数
  MaxAutoRetriesNextServer: 1 
  # 同一实例的重试次数    
  MaxAutoRetries: 1 
#设置超时熔断时间（spring-cloud-starter-openfeign中的HystrixCommandProperties默认为1000毫秒）
hystrix:
  command:
    default:
      execution:
        isolation:
          thread:
            timeoutInMilliseconds: 10000
&lt;/code&gt;&lt;/pre&gt;
&lt;h1&gt;写feign接口&lt;/h1&gt;
&lt;blockquote&gt;
&lt;ul&gt;
&lt;li&gt;name  : 需要调用的服务名称&lt;/li&gt;
&lt;li&gt;fallbackFactory： 自定义服务降级工厂&lt;/li&gt;
&lt;li&gt;decode404 ：Feign对于2XX和404 ，都不会走Fallback了。
排除404，已经基本上够用了，如果想把409、400等status也加到例外中，可以重写一下Feign的errorDecoder。&lt;/li&gt;
&lt;li&gt;configuration  自定义feign配置类&lt;/li&gt;
&lt;/ul&gt;
&lt;/blockquote&gt;
&lt;pre&gt;&lt;code&gt;/**
 * &amp;lt;p&amp;gt;DESC: 会员feign&amp;lt;/p&amp;gt;
 * &amp;lt;p&amp;gt;DATE: 2020-04-13 19:24&amp;lt;/p&amp;gt;
 * &amp;lt;p&amp;gt;VERSION:2.0.0&amp;lt;/p&amp;gt;
 * &amp;lt;p&amp;gt;@AUTHOR: ZhengYong&amp;lt;/p&amp;gt;
 */
@FeignClient(name = &quot;gulimall-member&quot;,fallbackFactory = MemberFeignFallbackFactory.class,decode404=true
        ,configuration = FeignConfiguration.class
)
public interface MemberFeign {

    @RequestMapping(&quot;member/growthchangehistory/list&quot;)
    R list(@RequestParam Map&amp;lt;String, Object&amp;gt; params);
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h1&gt;fallbackFactory&lt;/h1&gt;
&lt;blockquote&gt;
&lt;ul&gt;
&lt;li&gt;实现FallbackFactory，泛型填写自己的feign接口&lt;/li&gt;
&lt;li&gt;重写create方法&lt;/li&gt;
&lt;li&gt;BaseFallFactory接收Throwable（其实是异常），调用invoke方法（进行errorCode，message的赋值操作）&lt;/li&gt;
&lt;li&gt;return new MemberFeign()  重写接口中的服务降级方法&lt;/li&gt;
&lt;/ul&gt;
&lt;/blockquote&gt;
&lt;pre&gt;&lt;code&gt;@Slf4j
@Component
public class MemberFeignFallbackFactory implements FallbackFactory&amp;lt;MemberFeign&amp;gt; {

    @Override
    public MemberFeign create(Throwable throwable) {
        //解析错误
        BaseFallFactory invoke = new BaseFallFactory(throwable).invoke();
        final Integer errorCode = invoke.getErrorCode();
        final String message = invoke.getMessage();

        return new MemberFeign() {
            @Override
            public R list(Map&amp;lt;String, Object&amp;gt; params) {
                log.error(&quot;看见我了没有-------猪猪&quot;);
                return R.error(errorCode,message);
            }
        };
    }

}
&lt;/code&gt;&lt;/pre&gt;
&lt;h1&gt;BaseFallFactory&lt;/h1&gt;
&lt;blockquote&gt;
&lt;ul&gt;
&lt;li&gt;接收Throwable异常信息，进行信息绑定&lt;/li&gt;
&lt;li&gt;api业务异常，超时异常，feign失败异常&lt;/li&gt;
&lt;/ul&gt;
&lt;/blockquote&gt;
&lt;pre&gt;&lt;code&gt;@Slf4j
public class BaseFallFactory {
    private Throwable throwable;
    private Integer errorCode;
    private String message;

    public BaseFallFactory(Throwable throwable) {
        this.throwable = throwable;
    }

    public Integer getErrorCode() {
        return errorCode;
    }

    public String getMessage() {
        return message;
    }

    public BaseFallFactory invoke() {
        //是否是api  异常
        if (throwable instanceof ApiException) {
            errorCode = ((ApiException) throwable).getCode();
            message = throwable.getMessage();
        }
        //是否是请求超时异常
        else if (throwable instanceof com.netflix.hystrix.exception.HystrixTimeoutException) {
            log.error(&quot;error={}&quot;, &quot;feign调用超时，抛出RetryableException&quot;);
            errorCode = HttpStatus.REQUEST_TIMEOUT.value();
            message = &quot;Read timed out&quot;;
        }
        else if (throwable instanceof com.netflix.client.ClientException) {
            log.error(&quot;error={}&quot;, &quot;feign调用不可服务，抛出RetryableException&quot;);
            errorCode = HttpStatus.REQUEST_TIMEOUT.value();
            message = &quot;not have available server&quot;;
        }
        //feign调用失败 异常
        else {
            log.error(&quot;error={}，错误信息={}&quot;, &quot;feign调用失败，抛出FeignException&quot;, throwable.getMessage());
            errorCode = 400;
            message = throwable.getMessage();
        }
        return this;
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h1&gt;SpringCloud-feign客户端统一处理下游服务自定义异常&lt;/h1&gt;
&lt;p&gt;经过尝试和查阅资料，Springcloud处理下游服务的异常是是通过默认的ErrorDecoder实现处理的，最终上游业务获取抛出的异常处理都是FeignException处理，到时上游业务统一异常处理造成困扰，下面解决方式为：&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;作用：异常解析器,需要实现ErrorDecoder。作用是,当feign服务调用其他服务出现异常,收到的异常数据流处理类.&lt;/p&gt;
&lt;p&gt;调用流程  A 调用 B ,&lt;/p&gt;
&lt;p&gt;B抛出异常，&lt;/p&gt;
&lt;p&gt;全局异常处理，返回json,&lt;/p&gt;
&lt;p&gt;熔断器触发，返回feignException&lt;/p&gt;
&lt;p&gt;经过默认的ErrorDecoder&lt;/p&gt;
&lt;p&gt;执行fallback降级中的方法&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;ExceptionErrorDecoder&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;/**
 * @author zy
 * @version 1.0
 * @Description  繼續往上抛出自定義異常
 * @date 2021/4/21 16:55
 */
@Slf4j
@Configuration
public class ExceptionErrorDecoder implements ErrorDecoder {

    @Override
    public Exception decode(String methodKey, Response response) {
        try {
            Response.Body body = response.body();
            if (body != null) {
                String str = Util.toString(body.asReader());
                log.info(&quot;string={}&quot;,str);
                ExceptionInfo exceptionInfo = JSON.parseObject(str,ExceptionInfo.class);
                if(exceptionInfo.getMsg() == null){
                    JSONObject jsonObject = JSON.parseObject(str);
                    Integer code = (Integer) jsonObject.get(&quot;status&quot;);
                    String msg  = (String) jsonObject.get(&quot;error&quot;);
                    return new EpException(code,msg);
                }
                return new EpException(exceptionInfo.getCode(),exceptionInfo.getMsg());
            }
        } catch (Exception e) {
            e.printStackTrace();
        }
        return FeignException.errorStatus(methodKey, response);
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;ExceptionInfo&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;@Data
public class ExceptionInfo{

   private int code;

   private String msg;

   private Object data;

}
&lt;/code&gt;&lt;/pre&gt;
&lt;h1&gt;同一个客户端client，配置多个feign&lt;/h1&gt;
&lt;p&gt;1、开启bean重复定义&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;spring.main.allow-bean-definition-overriding=true
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;2、添加 qualifier 属性&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;新增一个@FeignClient(value = &quot;parking&quot;），添加 qualifier（限定）&lt;/p&gt;
&lt;/blockquote&gt;
&lt;pre&gt;&lt;code&gt;@FeignClient(value = &quot;parking&quot;,qualifier = &quot;space&quot;)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;3、导入时指定 qualifier&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;    @Qualifier(&quot;space&quot;)
    @Autowired
    private ParkingSpaceFeign spaceFeign;
&lt;/code&gt;&lt;/pre&gt;
&lt;h1&gt;Feign跨服务调用导出（下载）接口&lt;/h1&gt;
&lt;h2&gt;Feign调用&lt;/h2&gt;
&lt;p&gt;Feign调用导出接口的方法的出参不是和提供端的方法出入参一样就可以了，一般我们的导出接口都是无返回，直接将内容输出到response中，所以我们调用导出接口的feignt方法需要使用feign提供的Response来接收响应，如下&lt;/p&gt;
&lt;h2&gt;Feign接口方法&lt;/h2&gt;
&lt;blockquote&gt;
&lt;p&gt;注意：response为feign的&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;import feign.Response  
&lt;/code&gt;&lt;/pre&gt;
&lt;/blockquote&gt;
&lt;pre&gt;&lt;code&gt;    /**
     * @Author ths
     * @Description 导出公司管理数据
     * @Date 14:10 2021/4/26
     * @Param [commPageDto]
     * @return void
     **/
    @ApiOperation(&quot;导出公司管理数据&quot;)
    @PostMapping( &quot;/epParkManagementCompany/export&quot;)
    Response export(@RequestBody EpParkManagementCompanyPageRequest commPageDto)
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;生产者controller方法：&lt;/h2&gt;
&lt;blockquote&gt;
&lt;p&gt;注意：feign接口上没有HttpServletResponse，生产者有HttpServletResponse&lt;/p&gt;
&lt;/blockquote&gt;
&lt;pre&gt;&lt;code&gt;    /**
     * @Author ths
     * @Description 导出公司管理数据
     * @Date 14:35 2021/4/27
     * @Param [response, commPageDto]
     * @return void
     **/
    @ApiOperation(&quot;导出公司管理数据&quot;)
    @PostMapping( &quot;/export&quot;)
    public void export(@RequestBody EpParkManagementCompanyPageRequest commPageDto,HttpServletResponse response) {
        epParkManagementCompanyService.export(commPageDto,response);
    }
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;消费调用者&lt;/h2&gt;
&lt;blockquote&gt;
&lt;p&gt;注意：重组response&lt;/p&gt;
&lt;/blockquote&gt;
&lt;pre&gt;&lt;code&gt;    @ApiOperation(&quot;导出公司管理数据&quot;)
    @PostMapping(&quot;/export&quot;)
    public void export(@RequestBody EpParkManagementCompanyPageRequest commPageDto, HttpServletResponse servletResponse) {
        //调用
        Response response = parkingFeign.export(commPageDto);
        //重组response
        Response.Body body = response.body();
        for (Object key : response.headers().keySet()) {
            List&amp;lt;String&amp;gt; kList = (List) response.headers().get(key);
            for (String val : kList) {
                servletResponse.setHeader(key.toString(), val);
            }
        }
        try (InputStream inputStream = body.asInputStream();
             OutputStream outputStream = servletResponse.getOutputStream()) {
                byte[] b = new byte[inputStream.available()];
                inputStream.read(b);
                outputStream.write(b);
                outputStream.flush();
        } catch (IOException e) {
            log.error(&quot;导出Excel失败&quot;, e);
        }
    }
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;流程
Feign调用其他服务的导出服务，主要流程是通过Feign的Response来接收提供端导出接口的响应，然后通过resp.body()获取响应体，将响应体通过asInputStream方法转为InputStream 输入流，最主要的过程就是将输入流的内容写入到Feign客户端的response输出流中即可&lt;/p&gt;
</content:encoded></item><item><title>SpringCloud Eureka：服务注册与发现详解</title><link>https://tangtangit.com/posts/springcloud-01-eureka/</link><guid isPermaLink="true">https://tangtangit.com/posts/springcloud-01-eureka/</guid><description>深入理解Eureka服务注册中心的工作原理、集群搭建、自我保护机制、服务注册与续约流程，以及Eureka与Zookeeper的区别。</description><pubDate>Mon, 02 Mar 2026 00:00:00 GMT</pubDate><content:encoded>&lt;h1&gt;0.学习目标&lt;/h1&gt;
&lt;ul&gt;
&lt;li&gt;了解系统架构的演变&lt;/li&gt;
&lt;li&gt;了解RPC与Http的区别&lt;/li&gt;
&lt;li&gt;掌握HttpClient的简单使用&lt;/li&gt;
&lt;li&gt;知道什么是SpringCloud&lt;/li&gt;
&lt;li&gt;独立搭建Eureka注册中心&lt;/li&gt;
&lt;li&gt;独立配置Robbin负载均衡&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;-Xms128m -Xmx128m&lt;/p&gt;
&lt;h1&gt;1.系统架构演变&lt;/h1&gt;
&lt;p&gt;随着互联网的发展，网站应用的规模不断扩大。需求的激增，带来的是技术上的压力。系统架构也因此也不断的演进、升级、迭代。从单一应用，到垂直拆分，到分布式服务，到SOA，以及现在火热的微服务架构，还有在Google带领下来势汹涌的Service Mesh。我们到底是该乘坐微服务的船只驶向远方，还是偏安一隅得过且过？&lt;/p&gt;
&lt;p&gt;其实生活不止眼前的苟且，还有诗和远方。所以我们今天就回顾历史，看一看系统架构演变的历程；把握现在，学习现在最火的技术架构；展望未来，争取成为一名优秀的Java工程师。&lt;/p&gt;
&lt;h2&gt;1.1. 集中式架构&lt;/h2&gt;
&lt;p&gt;当网站流量很小时，只需一个应用，将所有功能都部署在一起，以减少部署节点和成本。此时，用于简化增删改查工作量的数据访问框架(ORM)是影响项目开发的关键。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://tangtangit.com/_astro/1525529091749.B4hHTWIw_iV6wM.webp&quot; alt=&quot;1525529091749&quot; /&gt;&lt;/p&gt;
&lt;p&gt;存在的问题：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;代码耦合，开发维护困难&lt;/li&gt;
&lt;li&gt;无法针对不同模块进行针对性优化&lt;/li&gt;
&lt;li&gt;无法水平扩展&lt;/li&gt;
&lt;li&gt;单点容错率低，并发能力差&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;1.2.垂直拆分&lt;/h2&gt;
&lt;p&gt;当访问量逐渐增大，单一应用无法满足需求，此时为了应对更高的并发和业务需求，我们根据业务功能对系统进行拆分：&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://tangtangit.com/_astro/1525529671801.DELhFDxF_1bafFo.webp&quot; alt=&quot;1525529671801&quot; /&gt;&lt;/p&gt;
&lt;p&gt;优点：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;系统拆分实现了流量分担，解决了并发问题&lt;/li&gt;
&lt;li&gt;可以针对不同模块进行优化&lt;/li&gt;
&lt;li&gt;方便水平扩展，负载均衡，容错率提高&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;缺点：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;系统间相互独立，会有很多重复开发工作，影响开发效率&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;1.3.分布式服务&lt;/h2&gt;
&lt;p&gt;当垂直应用越来越多，应用之间交互不可避免，将核心业务抽取出来，作为独立的服务，逐渐形成稳定的服务中心，使前端应用能更快速的响应多变的市场需求。此时，用于提高业务复用及整合的分布式调用是关键。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://tangtangit.com/_astro/1525530657919.6wICgqfT_ZxYuTC.webp&quot; alt=&quot;1525530657919&quot; /&gt;&lt;/p&gt;
&lt;p&gt;优点：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;将基础服务进行了抽取，系统间相互调用，提高了代码复用和开发效率&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;缺点：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;系统间耦合度变高，调用关系错综复杂，难以维护&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;1.4.服务治理（SOA）&lt;/h2&gt;
&lt;p&gt;当服务越来越多，容量的评估，小服务资源的浪费等问题逐渐显现，此时需增加一个调度中心基于访问压力实时管理集群容量，提高集群利用率。此时，用于提高机器利用率的资源调度和治理中心(SOA)是关键&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://tangtangit.com/_astro/1525530804753.CwiIOPrS_SRI9F.webp&quot; alt=&quot;1525530804753&quot; /&gt;&lt;/p&gt;
&lt;p&gt;以前出现了什么问题？&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;服务越来越多，需要管理每个服务的地址&lt;/li&gt;
&lt;li&gt;调用关系错综复杂，难以理清依赖关系&lt;/li&gt;
&lt;li&gt;服务过多，服务状态难以管理，无法根据服务情况动态管理&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;服务治理要做什么？&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;服务注册中心，实现服务自动注册和发现，无需人为记录服务地址&lt;/li&gt;
&lt;li&gt;服务自动订阅，服务列表自动推送，服务调用透明化，无需关心依赖关系&lt;/li&gt;
&lt;li&gt;动态监控服务状态监控报告，人为控制服务状态&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;缺点：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;服务间会有依赖关系，一旦某个环节出错会影响较大&lt;/li&gt;
&lt;li&gt;服务关系复杂，运维、测试部署困难，不符合DevOps思想&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;1.5.微服务&lt;/h2&gt;
&lt;p&gt;前面说的SOA，英文翻译过来是面向服务。微服务，似乎也是服务，都是对系统进行拆分。因此两者非常容易混淆，但其实缺有一些差别：&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://tangtangit.com/_astro/1525532344817.Bj6LrY1o_ZLH5gD.webp&quot; alt=&quot;1525532344817&quot; /&gt;&lt;/p&gt;
&lt;p&gt;微服务的特点：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;单一职责：微服务中每一个服务都对应唯一的业务能力，做到单一职责&lt;/li&gt;
&lt;li&gt;微：微服务的服务拆分粒度很小，例如一个用户管理就可以作为一个服务。每个服务虽小，但“五脏俱全”。&lt;/li&gt;
&lt;li&gt;面向服务：面向服务是说每个服务都要对外暴露服务接口API。并不关心服务的技术实现，做到与平台和语言无关，也不限定用什么技术实现，只要提供Rest的接口即可。&lt;/li&gt;
&lt;li&gt;自治：自治是说服务间互相独立，互不干扰
&lt;ul&gt;
&lt;li&gt;团队独立：每个服务都是一个独立的开发团队，人数不能过多。&lt;/li&gt;
&lt;li&gt;技术独立：因为是面向服务，提供Rest接口，使用什么技术没有别人干涉&lt;/li&gt;
&lt;li&gt;前后端分离：采用前后端分离开发，提供统一Rest接口，后端不用再为PC、移动段开发不同接口&lt;/li&gt;
&lt;li&gt;数据库分离：每个服务都使用自己的数据源&lt;/li&gt;
&lt;li&gt;部署独立，服务间虽然有调用，但要做到服务重启不影响其它服务。有利于持续集成和持续交付。每个服务都是独立的组件，可复用，可替换，降低耦合，易维护&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;微服务结构图：&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://tangtangit.com/_astro/1526860071166.DnQUpBFn_Z1kIkE9.webp&quot; alt=&quot;1526860071166&quot; /&gt;&lt;/p&gt;
&lt;h1&gt;2.远程调用方式&lt;/h1&gt;
&lt;p&gt;无论是微服务还是SOA，都面临着服务间的远程调用。那么服务间的远程调用方式有哪些呢？&lt;/p&gt;
&lt;p&gt;常见的远程调用方式有以下几种：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;RPC：Remote Produce Call远程过程调用，类似的还有RMI。自定义数据格式，基于原生TCP通信，速度快，效率高。早期的webservice，现在热门的dubbo，都是RPC的典型&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Http：http其实是一种网络传输协议，基于TCP，规定了数据传输的格式。现在客户端浏览器与服务端通信基本都是采用Http协议。也可以用来进行远程服务调用。缺点是消息封装臃肿。&lt;/p&gt;
&lt;p&gt;现在热门的Rest风格，就可以通过http协议来实现。&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;2.1.认识RPC&lt;/h2&gt;
&lt;p&gt;RPC，即 Remote Procedure Call（远程过程调用），是一个计算机通信协议。 该协议允许运行于一台计算机的程序调用另一台计算机的子程序，而程序员无需额外地为这个交互作用编程。说得通俗一点就是：A计算机提供一个服务，B计算机可以像调用本地服务那样调用A计算机的服务。&lt;/p&gt;
&lt;p&gt;通过上面的概念，我们可以知道，实现RPC主要是做到两点：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;实现远程调用其他计算机的服务
&lt;ul&gt;
&lt;li&gt;要实现远程调用，肯定是通过网络传输数据。A程序提供服务，B程序通过网络将请求参数传递给A，A本地执行后得到结果，再将结果返回给B程序。这里需要关注的有两点：
&lt;ul&gt;
&lt;li&gt;1）采用何种网络通讯协议？
&lt;ul&gt;
&lt;li&gt;现在比较流行的RPC框架，都会采用TCP作为底层传输协议&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;2）数据传输的格式怎样？
&lt;ul&gt;
&lt;li&gt;两个程序进行通讯，必须约定好数据传输格式。就好比两个人聊天，要用同一种语言，否则无法沟通。所以，我们必须定义好请求和响应的格式。另外，数据在网路中传输需要进行序列化，所以还需要约定统一的序列化的方式。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;像调用本地服务一样调用远程服务
&lt;ul&gt;
&lt;li&gt;如果仅仅是远程调用，还不算是RPC，因为RPC强调的是过程调用，调用的过程对用户而言是应该是透明的，用户不应该关心调用的细节，可以像调用本地服务一样调用远程服务。所以RPC一定要对调用的过程进行封装&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;RPC调用流程图：&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://tangtangit.com/_astro/1525568965976.Dpj7-ejk_1mcJcj.webp&quot; alt=&quot;1525568965976&quot; /&gt;&lt;/p&gt;
&lt;p&gt;想要了解详细的RPC实现，给大家推荐一篇文章：&lt;a href=&quot;https://legacy.gitbook.com/book/huge0612/tour-of-rpc/details&quot;&gt;自己动手实现RPC&lt;/a&gt;&lt;/p&gt;
&lt;h2&gt;2.2.认识Http&lt;/h2&gt;
&lt;p&gt;Http协议：超文本传输协议，是一种应用层协议。规定了网络传输的请求格式、响应格式、资源定位和操作的方式等。但是底层采用什么网络传输协议，并没有规定，不过现在都是采用TCP协议作为底层传输协议。说到这里，大家可能觉得，Http与RPC的远程调用非常像，都是按照某种规定好的数据格式进行网络通信，有请求，有响应。没错，在这点来看，两者非常相似，但是还是有一些细微差别。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;RPC并没有规定数据传输格式，这个格式可以任意指定，不同的RPC协议，数据格式不一定相同。&lt;/li&gt;
&lt;li&gt;Http中还定义了资源定位的路径，RPC中并不需要&lt;/li&gt;
&lt;li&gt;最重要的一点：RPC需要满足像调用本地服务一样调用远程服务，也就是对调用过程在API层面进行封装。Http协议没有这样的要求，因此请求、响应等细节需要我们自己去实现。
&lt;ul&gt;
&lt;li&gt;优点：RPC方式更加透明，对用户更方便。Http方式更灵活，没有规定API和语言，跨语言、跨平台&lt;/li&gt;
&lt;li&gt;缺点：RPC方式需要在API层面进行封装，限制了开发的语言环境。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;例如我们通过浏览器访问网站，就是通过Http协议。只不过浏览器把请求封装，发起请求以及接收响应，解析响应的事情都帮我们做了。如果是不通过浏览器，那么这些事情都需要自己去完成。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://tangtangit.com/_astro/1525569352313.d5rnUja5_Z1QFcvI.webp&quot; alt=&quot;1525569352313&quot; /&gt;&lt;/p&gt;
&lt;h2&gt;2.3.如何选择？&lt;/h2&gt;
&lt;p&gt;既然两种方式都可以实现远程调用，我们该如何选择呢？&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;速度来看，RPC要比http更快，虽然底层都是TCP，但是http协议的信息往往比较臃肿，不过可以采用gzip压缩。&lt;/li&gt;
&lt;li&gt;难度来看，RPC实现较为复杂，http相对比较简单&lt;/li&gt;
&lt;li&gt;灵活性来看，http更胜一筹，因为它不关心实现细节，跨平台、跨语言。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;因此，两者都有不同的使用场景：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;如果对效率要求更高，并且开发过程使用统一的技术栈，那么用RPC还是不错的。&lt;/li&gt;
&lt;li&gt;如果需要更加灵活，跨语言、跨平台，显然http更合适&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;那么我们该怎么选择呢？&lt;/p&gt;
&lt;p&gt;微服务，更加强调的是独立、自治、灵活。而RPC方式的限制较多，因此微服务框架中，一般都会采用基于Http的Rest风格服务。&lt;/p&gt;
&lt;h1&gt;3.Http客户端工具&lt;/h1&gt;
&lt;p&gt;既然微服务选择了Http，那么我们就需要考虑自己来实现对请求和响应的处理。不过开源世界已经有很多的http客户端工具，能够帮助我们做这些事情，例如：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;HttpClient&lt;/li&gt;
&lt;li&gt;OKHttp&lt;/li&gt;
&lt;li&gt;URLConnection&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;接下来，我们就一起了解一款比较流行的客户端工具：HttpClient&lt;/p&gt;
&lt;h2&gt;3.1.HttpClient&lt;/h2&gt;
&lt;h3&gt;3.1.1.介绍&lt;/h3&gt;
&lt;p&gt;HttpClient是Apache公司的产品，是Http Components下的一个组件。&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;http://hc.apache.org/index.html&quot;&gt;官网地址：http://hc.apache.org/index.html&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://tangtangit.com/_astro/1525570921966.XiOG3wIm_Z1ADRUj.webp&quot; alt=&quot;1525570921966&quot; /&gt;&lt;/p&gt;
&lt;p&gt;特点：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;基于标准、纯净的Java语言。实现了Http1.0和Http1.1&lt;/li&gt;
&lt;li&gt;以可扩展的面向对象的结构实现了Http全部的方法（GET, POST, PUT, DELETE, HEAD, OPTIONS, and TRACE）&lt;/li&gt;
&lt;li&gt;支持HTTPS协议。&lt;/li&gt;
&lt;li&gt;通过Http代理建立透明的连接。&lt;/li&gt;
&lt;li&gt;自动处理Set-Cookie中的Cookie。&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;3.1.2.使用&lt;/h3&gt;
&lt;p&gt;我们导入课前资料提供的demo工程：《http-demo》&lt;/p&gt;
&lt;p&gt;发起get请求：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;    @Test
    public void testGet() throws IOException {
        HttpGet request = new HttpGet(&quot;http://www.baidu.com&quot;);
        String response = this.httpClient.execute(request, new BasicResponseHandler());
        System.out.println(response);
    }

&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;发起Post请求：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;@Test
public void testPost() throws IOException {
    HttpPost request = new HttpPost(&quot;http://www.oschina.net/&quot;);
    request.setHeader(&quot;User-Agent&quot;,
                      &quot;Mozilla/5.0 (Windows NT 10.0; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/56.0.2924.87 Safari/537.36&quot;);
    String response = this.httpClient.execute(request, new BasicResponseHandler());
    System.out.println(response);
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;尝试访问昨天编写的接口：http://localhost/hello&lt;/p&gt;
&lt;p&gt;这个接口返回一个User对象&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;@Test
public void testGetPojo() throws IOException {
    HttpGet request = new HttpGet(&quot;http://localhost/hello&quot;);
    String response = this.httpClient.execute(request, new BasicResponseHandler());
    System.out.println(response);
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;我们实际得到的是一个json字符串：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;{
    &quot;id&quot;: 8,
    &quot;userName&quot;: &quot;liuyan&quot;,
    &quot;password&quot;: &quot;123456&quot;,
    &quot;name&quot;: &quot;柳岩&quot;,
    &quot;age&quot;: 21,
    &quot;sex&quot;: 2,
    &quot;birthday&quot;: &quot;1995-08-07T16:00:00.000+0000&quot;,
    &quot;created&quot;: &quot;2014-09-20T03:41:15.000+0000&quot;,
    &quot;updated&quot;: &quot;2014-09-20T03:41:15.000+0000&quot;,
    &quot;note&quot;: &quot;柳岩同学在传智播客学表演&quot;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;如果想要得到对象，我们还需要手动进行Json反序列化，这一点比较麻烦。&lt;/p&gt;
&lt;h3&gt;3.1.3.Json转换工具&lt;/h3&gt;
&lt;p&gt;HttpClient请求数据后是json字符串，需要我们自己把Json字符串反序列化为对象，我们会使用JacksonJson工具来实现。&lt;/p&gt;
&lt;p&gt;&lt;code&gt;JacksonJson&lt;/code&gt;是SpringMVC内置的json处理工具，其中有一个&lt;code&gt;ObjectMapper&lt;/code&gt;类，可以方便的实现对json的处理：&lt;/p&gt;
&lt;h4&gt;对象转json&lt;/h4&gt;
&lt;pre&gt;&lt;code&gt;// json处理工具
    private ObjectMapper mapper = new ObjectMapper();
    @Test
    public void testJson() throws JsonProcessingException {
        User user = new User();
        user.setId(8L);
        user.setAge(21);
        user.setName(&quot;柳岩&quot;);
        user.setUserName(&quot;liuyan&quot;);
        // 序列化
        String json = mapper.writeValueAsString(user);
        System.out.println(&quot;json = &quot; + json);
    }
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;结果：&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://tangtangit.com/_astro/1526877496885.Tb3C33Uh_1UrXz0.webp&quot; alt=&quot;1526877496885&quot; /&gt;&lt;/p&gt;
&lt;h4&gt;json转普通对象&lt;/h4&gt;
&lt;pre&gt;&lt;code&gt;// json处理工具
private ObjectMapper mapper = new ObjectMapper();
@Test
public void testJson() throws IOException {
    User user = new User();
    user.setId(8L);
    user.setAge(21);
    user.setName(&quot;柳岩&quot;);
    user.setUserName(&quot;liuyan&quot;);
    // 序列化
    String json = mapper.writeValueAsString(user);

    // 反序列化，接收两个参数：json数据，反序列化的目标类字节码
    User result = mapper.readValue(json, User.class);
    System.out.println(&quot;result = &quot; + result);
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;结果：&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://tangtangit.com/_astro/1526877647406.N079BGQ3_Z1LINTx.webp&quot; alt=&quot;1526877647406&quot; /&gt;&lt;/p&gt;
&lt;h4&gt;json转集合&lt;/h4&gt;
&lt;p&gt;json转集合比较麻烦，因为你无法同时把集合的class和元素的class同时传递到一个参数。&lt;/p&gt;
&lt;p&gt;因此Jackson做了一个类型工厂，用来解决这个问题：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;// json处理工具
private ObjectMapper mapper = new ObjectMapper();
@Test
public void testJson() throws IOException {
    User user = new User();
    user.setId(8L);
    user.setAge(21);
    user.setName(&quot;柳岩&quot;);
    user.setUserName(&quot;liuyan&quot;);

    // 序列化,得到对象集合的json字符串
    String json = mapper.writeValueAsString(Arrays.asList(user, user));

    // 反序列化，接收两个参数：json数据，反序列化的目标类字节码
    List&amp;lt;User&amp;gt; users = mapper.readValue(json, mapper.getTypeFactory().constructCollectionType(List.class, User.class));
    for (User u : users) {
        System.out.println(&quot;u = &quot; + u);
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;结果：&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://tangtangit.com/_astro/1526877995530.D8yn9_1E_Z2qmWnU.webp&quot; alt=&quot;1526877995530&quot; /&gt;&lt;/p&gt;
&lt;h4&gt;json转任意复杂类型&lt;/h4&gt;
&lt;p&gt;当对象泛型关系复杂时，类型工厂也不好使了。这个时候Jackson提供了TypeReference来接收类型泛型，然后底层通过反射来获取泛型上的具体类型。实现数据转换。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;// json处理工具
private ObjectMapper mapper = new ObjectMapper();
@Test
public void testJson() throws IOException {
    User user = new User();
    user.setId(8L);
    user.setAge(21);
    user.setName(&quot;柳岩&quot;);
    user.setUserName(&quot;liuyan&quot;);

    // 序列化,得到对象集合的json字符串
    String json = mapper.writeValueAsString(Arrays.asList(user, user));

    // 反序列化，接收两个参数：json数据，反序列化的目标类字节码
    List&amp;lt;User&amp;gt; users = mapper.readValue(json, new TypeReference&amp;lt;List&amp;lt;User&amp;gt;&amp;gt;(){});
    for (User u : users) {
        System.out.println(&quot;u = &quot; + u);
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;结果：&lt;/p&gt;
&lt;p&gt;&amp;lt;!-- 图片不存在，已移除: assets/1526877988488.png --&amp;gt;&lt;/p&gt;
&lt;h2&gt;3.3.Spring的RestTemplate&lt;/h2&gt;
&lt;p&gt;Spring提供了一个RestTemplate模板工具类，对基于Http的客户端进行了封装，并且实现了对象与json的序列化和反序列化，非常方便。RestTemplate并没有限定Http的客户端类型，而是进行了抽象，目前常用的3种都有支持：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;HttpClient&lt;/li&gt;
&lt;li&gt;OkHttp&lt;/li&gt;
&lt;li&gt;JDK原生的URLConnection（默认的）&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;首先在项目中注册一个&lt;code&gt;RestTemplate&lt;/code&gt;对象，可以在启动类位置注册：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;@SpringBootApplication
public class HttpDemoApplication {

	public static void main(String[] args) {
		SpringApplication.run(HttpDemoApplication.class, args);
	}

	@Bean
	public RestTemplate restTemplate() {
        // 默认的RestTemplate，底层是走JDK的URLConnection方式。
		return new RestTemplate();
	}
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;在测试类中直接&lt;code&gt;@Autowired&lt;/code&gt;注入：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;@RunWith(SpringRunner.class)
@SpringBootTest(classes = HttpDemoApplication.class)
public class HttpDemoApplicationTests {

	@Autowired
	private RestTemplate restTemplate;

	@Test
	public void httpGet() {
		User user = this.restTemplate.getForObject(&quot;http://localhost/hello&quot;, User.class);
		System.out.println(user);
	}
}
&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;通过RestTemplate的getForObject()方法，传递url地址及实体类的字节码，RestTemplate会自动发起请求，接收响应，并且帮我们对响应结果进行反序列化。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;img src=&quot;https://tangtangit.com/_astro/1525573702492.C1fC7kUs_ZkV9UY.webp&quot; alt=&quot;1525573702492&quot; /&gt;&lt;/p&gt;
&lt;p&gt;学习完了Http客户端工具，接下来就可以正式学习微服务了。&lt;/p&gt;
&lt;h1&gt;4.初始SpringCloud&lt;/h1&gt;
&lt;p&gt;微服务是一种架构方式，最终肯定需要技术架构去实施。&lt;/p&gt;
&lt;p&gt;微服务的实现方式很多，但是最火的莫过于Spring Cloud了。为什么？&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;后台硬：作为Spring家族的一员，有整个Spring全家桶靠山，背景十分强大。&lt;/li&gt;
&lt;li&gt;技术强：Spring作为Java领域的前辈，可以说是功力深厚。有强力的技术团队支撑，一般人还真比不了&lt;/li&gt;
&lt;li&gt;群众基础好：可以说大多数程序员的成长都伴随着Spring框架，试问：现在有几家公司开发不用Spring？SpringCloud与Spring的各个框架无缝整合，对大家来说一切都是熟悉的配方，熟悉的味道。&lt;/li&gt;
&lt;li&gt;使用方便：相信大家都体会到了SpringBoot给我们开发带来的便利，而SpringCloud完全支持SpringBoot的开发，用很少的配置就能完成微服务框架的搭建&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;4.1.简介&lt;/h2&gt;
&lt;p&gt;SpringCloud是Spring旗下的项目之一，&lt;a href=&quot;http://projects.spring.io/spring-cloud/&quot;&gt;官网地址：http://projects.spring.io/spring-cloud/&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;Spring最擅长的就是集成，把世界上最好的框架拿过来，集成到自己的项目中。&lt;/p&gt;
&lt;p&gt;SpringCloud也是一样，它将现在非常流行的一些技术整合到一起，实现了诸如：配置管理，服务发现，智能路由，负载均衡，熔断器，控制总线，集群状态等等功能。其主要涉及的组件包括：&lt;/p&gt;
&lt;p&gt;netflix&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Eureka：注册中心&lt;/li&gt;
&lt;li&gt;Zuul：服务网关&lt;/li&gt;
&lt;li&gt;Ribbon：负载均衡&lt;/li&gt;
&lt;li&gt;Feign：服务调用&lt;/li&gt;
&lt;li&gt;Hystix：熔断器&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;以上只是其中一部分，架构图：&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://tangtangit.com/_astro/1525575656796.BXQplai7_7oH3h.webp&quot; alt=&quot;1525575656796&quot; /&gt;&lt;/p&gt;
&lt;h2&gt;4.2.版本&lt;/h2&gt;
&lt;p&gt;SpringCloud的版本命名比较特殊，因为它不是一个组件，而是许多组件的集合，它的命名是以A到Z的为首字母的一些单词组成：&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://tangtangit.com/_astro/1525575903675.I1hHAgKE_Z1lTM4q.webp&quot; alt=&quot;1525575903675&quot; /&gt;&lt;/p&gt;
&lt;p&gt;我们在项目中，会是以Finchley的版本。&lt;/p&gt;
&lt;p&gt;其中包含的组件，也都有各自的版本，如下表：&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Component&lt;/th&gt;
&lt;th&gt;Edgware.SR3&lt;/th&gt;
&lt;th&gt;Finchley.RC1&lt;/th&gt;
&lt;th&gt;Finchley.BUILD-SNAPSHOT&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;spring-cloud-aws&lt;/td&gt;
&lt;td&gt;1.2.2.RELEASE&lt;/td&gt;
&lt;td&gt;2.0.0.RC1&lt;/td&gt;
&lt;td&gt;2.0.0.BUILD-SNAPSHOT&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;spring-cloud-bus&lt;/td&gt;
&lt;td&gt;1.3.2.RELEASE&lt;/td&gt;
&lt;td&gt;2.0.0.RC1&lt;/td&gt;
&lt;td&gt;2.0.0.BUILD-SNAPSHOT&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;spring-cloud-cli&lt;/td&gt;
&lt;td&gt;1.4.1.RELEASE&lt;/td&gt;
&lt;td&gt;2.0.0.RC1&lt;/td&gt;
&lt;td&gt;2.0.0.BUILD-SNAPSHOT&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;spring-cloud-commons&lt;/td&gt;
&lt;td&gt;1.3.3.RELEASE&lt;/td&gt;
&lt;td&gt;2.0.0.RC1&lt;/td&gt;
&lt;td&gt;2.0.0.BUILD-SNAPSHOT&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;spring-cloud-contract&lt;/td&gt;
&lt;td&gt;1.2.4.RELEASE&lt;/td&gt;
&lt;td&gt;2.0.0.RC1&lt;/td&gt;
&lt;td&gt;2.0.0.BUILD-SNAPSHOT&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;spring-cloud-config&lt;/td&gt;
&lt;td&gt;1.4.3.RELEASE&lt;/td&gt;
&lt;td&gt;2.0.0.RC1&lt;/td&gt;
&lt;td&gt;2.0.0.BUILD-SNAPSHOT&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;spring-cloud-netflix&lt;/td&gt;
&lt;td&gt;1.4.4.RELEASE&lt;/td&gt;
&lt;td&gt;2.0.0.RC1&lt;/td&gt;
&lt;td&gt;2.0.0.BUILD-SNAPSHOT&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;spring-cloud-security&lt;/td&gt;
&lt;td&gt;1.2.2.RELEASE&lt;/td&gt;
&lt;td&gt;2.0.0.RC1&lt;/td&gt;
&lt;td&gt;2.0.0.BUILD-SNAPSHOT&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;spring-cloud-cloudfoundry&lt;/td&gt;
&lt;td&gt;1.1.1.RELEASE&lt;/td&gt;
&lt;td&gt;2.0.0.RC1&lt;/td&gt;
&lt;td&gt;2.0.0.BUILD-SNAPSHOT&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;spring-cloud-consul&lt;/td&gt;
&lt;td&gt;1.3.3.RELEASE&lt;/td&gt;
&lt;td&gt;2.0.0.RC1&lt;/td&gt;
&lt;td&gt;2.0.0.BUILD-SNAPSHOT&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;spring-cloud-sleuth&lt;/td&gt;
&lt;td&gt;1.3.3.RELEASE&lt;/td&gt;
&lt;td&gt;2.0.0.RC1&lt;/td&gt;
&lt;td&gt;2.0.0.BUILD-SNAPSHOT&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;spring-cloud-stream&lt;/td&gt;
&lt;td&gt;Ditmars.SR3&lt;/td&gt;
&lt;td&gt;Elmhurst.RELEASE&lt;/td&gt;
&lt;td&gt;Elmhurst.BUILD-SNAPSHOT&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;spring-cloud-zookeeper&lt;/td&gt;
&lt;td&gt;1.2.1.RELEASE&lt;/td&gt;
&lt;td&gt;2.0.0.RC1&lt;/td&gt;
&lt;td&gt;2.0.0.BUILD-SNAPSHOT&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;spring-boot&lt;/td&gt;
&lt;td&gt;1.5.10.RELEASE&lt;/td&gt;
&lt;td&gt;2.0.1.RELEASE&lt;/td&gt;
&lt;td&gt;2.0.0.BUILD-SNAPSHOT&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;spring-cloud-task&lt;/td&gt;
&lt;td&gt;1.2.2.RELEASE&lt;/td&gt;
&lt;td&gt;2.0.0.RC1&lt;/td&gt;
&lt;td&gt;2.0.0.RELEASE&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;spring-cloud-vault&lt;/td&gt;
&lt;td&gt;1.1.0.RELEASE&lt;/td&gt;
&lt;td&gt;2.0.0.RC1&lt;/td&gt;
&lt;td&gt;2.0.0.BUILD-SNAPSHOT&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;spring-cloud-gateway&lt;/td&gt;
&lt;td&gt;1.0.1.RELEASE&lt;/td&gt;
&lt;td&gt;2.0.0.RC1&lt;/td&gt;
&lt;td&gt;2.0.0.BUILD-SNAPSHOT&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;spring-cloud-openfeign&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;td&gt;2.0.0.RC1&lt;/td&gt;
&lt;td&gt;2.0.0.BUILD-SNAPSHOT&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;接下来，我们就一一学习SpringCloud中的重要组件。&lt;/p&gt;
&lt;h1&gt;5.微服务场景模拟&lt;/h1&gt;
&lt;p&gt;首先，我们需要模拟一个服务调用的场景。方便后面学习微服务架构&lt;/p&gt;
&lt;h2&gt;5.1.服务提供者&lt;/h2&gt;
&lt;p&gt;我们新建一个项目，对外提供查询用户的服务。&lt;/p&gt;
&lt;h3&gt;5.1.1.Spring脚手架创建工程&lt;/h3&gt;
&lt;p&gt;借助于Spring提供的快速搭建工具：&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://tangtangit.com/_astro/1525576816916.CS9lAngy_1JTLuR.webp&quot; alt=&quot;1525576816916&quot; /&gt;&lt;/p&gt;
&lt;p&gt;填写项目信息：&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://tangtangit.com/_astro/1525576909381.CbcS9X-t_Z2021se.webp&quot; alt=&quot;1525576909381&quot; /&gt;&lt;/p&gt;
&lt;p&gt;添加web依赖：&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://tangtangit.com/_astro/1525576950842.KL4bPjVs_Z1mLmwg.webp&quot; alt=&quot;1525576950842&quot; /&gt;&lt;/p&gt;
&lt;p&gt;添加mybatis依赖：&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://tangtangit.com/_astro/1525576999052.D0LGKK9E_ZXNmrK.webp&quot; alt=&quot;1525576999052&quot; /&gt;&lt;/p&gt;
&lt;p&gt;填写项目位置：&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://tangtangit.com/_astro/1525577029150.D0kO9v_l_Z1gWU5e.webp&quot; alt=&quot;1525577029150&quot; /&gt;&lt;/p&gt;
&lt;p&gt;生成的项目结构：&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://tangtangit.com/_astro/1525577106711.B0-3QOdK_c74Pa.webp&quot; alt=&quot;1525577106711&quot; /&gt;&lt;/p&gt;
&lt;p&gt;依赖也已经全部自动引入：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&amp;lt;?xml version=&quot;1.0&quot; encoding=&quot;UTF-8&quot;?&amp;gt;
&amp;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;&amp;gt;
	&amp;lt;modelVersion&amp;gt;4.0.0&amp;lt;/modelVersion&amp;gt;

	&amp;lt;groupId&amp;gt;com.leyou.demo&amp;lt;/groupId&amp;gt;
	&amp;lt;artifactId&amp;gt;user-service-demo&amp;lt;/artifactId&amp;gt;
	&amp;lt;version&amp;gt;0.0.1-SNAPSHOT&amp;lt;/version&amp;gt;
	&amp;lt;packaging&amp;gt;jar&amp;lt;/packaging&amp;gt;

	&amp;lt;name&amp;gt;user-service-demo&amp;lt;/name&amp;gt;
	&amp;lt;description&amp;gt;Demo project for Spring Boot&amp;lt;/description&amp;gt;

	&amp;lt;parent&amp;gt;
		&amp;lt;groupId&amp;gt;org.springframework.boot&amp;lt;/groupId&amp;gt;
		&amp;lt;artifactId&amp;gt;spring-boot-starter-parent&amp;lt;/artifactId&amp;gt;
		&amp;lt;version&amp;gt;2.0.1.RELEASE&amp;lt;/version&amp;gt;
		&amp;lt;relativePath/&amp;gt; &amp;lt;!-- lookup parent from repository --&amp;gt;
	&amp;lt;/parent&amp;gt;

	&amp;lt;properties&amp;gt;
		&amp;lt;project.build.sourceEncoding&amp;gt;UTF-8&amp;lt;/project.build.sourceEncoding&amp;gt;
		&amp;lt;project.reporting.outputEncoding&amp;gt;UTF-8&amp;lt;/project.reporting.outputEncoding&amp;gt;
		&amp;lt;java.version&amp;gt;1.8&amp;lt;/java.version&amp;gt;
	&amp;lt;/properties&amp;gt;

	&amp;lt;dependencies&amp;gt;
		&amp;lt;dependency&amp;gt;
			&amp;lt;groupId&amp;gt;org.springframework.boot&amp;lt;/groupId&amp;gt;
			&amp;lt;artifactId&amp;gt;spring-boot-starter-jdbc&amp;lt;/artifactId&amp;gt;
		&amp;lt;/dependency&amp;gt;
		&amp;lt;dependency&amp;gt;
			&amp;lt;groupId&amp;gt;org.springframework.boot&amp;lt;/groupId&amp;gt;
			&amp;lt;artifactId&amp;gt;spring-boot-starter-web&amp;lt;/artifactId&amp;gt;
		&amp;lt;/dependency&amp;gt;
		&amp;lt;dependency&amp;gt;
			&amp;lt;groupId&amp;gt;org.mybatis.spring.boot&amp;lt;/groupId&amp;gt;
			&amp;lt;artifactId&amp;gt;mybatis-spring-boot-starter&amp;lt;/artifactId&amp;gt;
			&amp;lt;version&amp;gt;1.3.2&amp;lt;/version&amp;gt;
		&amp;lt;/dependency&amp;gt;
		&amp;lt;dependency&amp;gt;
			&amp;lt;groupId&amp;gt;mysql&amp;lt;/groupId&amp;gt;
			&amp;lt;artifactId&amp;gt;mysql-connector-java&amp;lt;/artifactId&amp;gt;
			&amp;lt;scope&amp;gt;runtime&amp;lt;/scope&amp;gt;
		&amp;lt;/dependency&amp;gt;
		&amp;lt;dependency&amp;gt;
			&amp;lt;groupId&amp;gt;org.springframework.boot&amp;lt;/groupId&amp;gt;
			&amp;lt;artifactId&amp;gt;spring-boot-starter-test&amp;lt;/artifactId&amp;gt;
			&amp;lt;scope&amp;gt;test&amp;lt;/scope&amp;gt;
		&amp;lt;/dependency&amp;gt;
	&amp;lt;/dependencies&amp;gt;

	&amp;lt;build&amp;gt;
		&amp;lt;plugins&amp;gt;
			&amp;lt;plugin&amp;gt;
				&amp;lt;groupId&amp;gt;org.springframework.boot&amp;lt;/groupId&amp;gt;
				&amp;lt;artifactId&amp;gt;spring-boot-maven-plugin&amp;lt;/artifactId&amp;gt;
			&amp;lt;/plugin&amp;gt;
		&amp;lt;/plugins&amp;gt;
	&amp;lt;/build&amp;gt;
&amp;lt;/project&amp;gt;

&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;当然，因为要使用通用mapper，所以我们需要手动加一条依赖：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&amp;lt;dependency&amp;gt;
    &amp;lt;groupId&amp;gt;tk.mybatis&amp;lt;/groupId&amp;gt;
    &amp;lt;artifactId&amp;gt;mapper-spring-boot-starter&amp;lt;/artifactId&amp;gt;
    &amp;lt;version&amp;gt;2.0.2&amp;lt;/version&amp;gt;
&amp;lt;/dependency&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;非常快捷啊！&lt;/p&gt;
&lt;h3&gt;5.1.2.编写代码&lt;/h3&gt;
&lt;p&gt;添加一个对外查询的接口：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;@RestController
@RequestMapping(&quot;user&quot;)
public class UserController {

    @Autowired
    private UserService userService;

    @GetMapping(&quot;/{id}&quot;)
    public User queryById(@PathVariable(&quot;id&quot;) Long id) {
        return this.userService.queryById(id);
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Service：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;@Service
public class UserService {

    @Autowired
    private UserMapper userMapper;

    public User queryById(Long id) {
        return this.userMapper.selectByPrimaryKey(id);
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;mapper:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;@Mapper
public interface UserMapper extends tk.mybatis.mapper.common.Mapper&amp;lt;User&amp;gt;{
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;实体类：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;@Table(name = &quot;tb_user&quot;)
public class User implements Serializable {

    private static final long serialVersionUID = 1L;

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;

    // 用户名
    private String userName;

    // 密码
    private String password;

    // 姓名
    private String name;

    // 年龄
    private Integer age;

    // 性别，1男性，2女性
    private Integer sex;

    // 出生日期
    private Date birthday;

    // 创建时间
    private Date created;

    // 更新时间
    private Date updated;

    // 备注
    private String note;

   // 。。。省略getters和setters
}

&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;属性文件,这里我们采用了yaml语法，而不是properties：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;server:
  port: 8081
spring:
  datasource:
    url: jdbc:mysql://localhost:3306/mydb01
    username: root
    password: 123
    hikari:
      maximum-pool-size: 20
      minimum-idle: 10
mybatis:
  type-aliases-package: com.leyou.userservice.pojo
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;项目结构：&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://tangtangit.com/_astro/1525577911331.CYI3RwOc_1pfKAF.webp&quot; alt=&quot;1525577911331&quot; /&gt;&lt;/p&gt;
&lt;h3&gt;5.1.3.启动并测试：&lt;/h3&gt;
&lt;p&gt;启动项目，访问接口：http://localhost:8081/user/7&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://tangtangit.com/_astro/1525593139364.B4gqqm5F_Z1Jk75.webp&quot; alt=&quot;1525593139364&quot; /&gt;&lt;/p&gt;
&lt;h2&gt;5.2.服务调用者&lt;/h2&gt;
&lt;h3&gt;5.2.1.创建工程&lt;/h3&gt;
&lt;p&gt;与上面类似，这里不再赘述，需要注意的是，我们调用user-service的功能，因此不需要mybatis相关依赖了。&lt;/p&gt;
&lt;p&gt;pom：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&amp;lt;?xml version=&quot;1.0&quot; encoding=&quot;UTF-8&quot;?&amp;gt;
&amp;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;&amp;gt;
	&amp;lt;modelVersion&amp;gt;4.0.0&amp;lt;/modelVersion&amp;gt;

	&amp;lt;groupId&amp;gt;com.leyou.demo&amp;lt;/groupId&amp;gt;
	&amp;lt;artifactId&amp;gt;user-consumer-demo&amp;lt;/artifactId&amp;gt;
	&amp;lt;version&amp;gt;0.0.1-SNAPSHOT&amp;lt;/version&amp;gt;
	&amp;lt;packaging&amp;gt;jar&amp;lt;/packaging&amp;gt;

	&amp;lt;name&amp;gt;user-consumer-demo&amp;lt;/name&amp;gt;
	&amp;lt;description&amp;gt;Demo project for Spring Boot&amp;lt;/description&amp;gt;

	&amp;lt;parent&amp;gt;
		&amp;lt;groupId&amp;gt;org.springframework.boot&amp;lt;/groupId&amp;gt;
		&amp;lt;artifactId&amp;gt;spring-boot-starter-parent&amp;lt;/artifactId&amp;gt;
		&amp;lt;version&amp;gt;2.0.1.RELEASE&amp;lt;/version&amp;gt;
		&amp;lt;relativePath/&amp;gt; &amp;lt;!-- lookup parent from repository --&amp;gt;
	&amp;lt;/parent&amp;gt;

	&amp;lt;properties&amp;gt;
		&amp;lt;project.build.sourceEncoding&amp;gt;UTF-8&amp;lt;/project.build.sourceEncoding&amp;gt;
		&amp;lt;project.reporting.outputEncoding&amp;gt;UTF-8&amp;lt;/project.reporting.outputEncoding&amp;gt;
		&amp;lt;java.version&amp;gt;1.8&amp;lt;/java.version&amp;gt;
	&amp;lt;/properties&amp;gt;

	&amp;lt;dependencies&amp;gt;
		&amp;lt;dependency&amp;gt;
			&amp;lt;groupId&amp;gt;org.springframework.boot&amp;lt;/groupId&amp;gt;
			&amp;lt;artifactId&amp;gt;spring-boot-starter-web&amp;lt;/artifactId&amp;gt;
		&amp;lt;/dependency&amp;gt;
        &amp;lt;!-- 添加OkHttp支持 --&amp;gt;
		&amp;lt;dependency&amp;gt;
			&amp;lt;groupId&amp;gt;com.squareup.okhttp3&amp;lt;/groupId&amp;gt;
			&amp;lt;artifactId&amp;gt;okhttp&amp;lt;/artifactId&amp;gt;
			&amp;lt;version&amp;gt;3.9.0&amp;lt;/version&amp;gt;
		&amp;lt;/dependency&amp;gt;
	&amp;lt;/dependencies&amp;gt;

	&amp;lt;build&amp;gt;
		&amp;lt;plugins&amp;gt;
			&amp;lt;plugin&amp;gt;
				&amp;lt;groupId&amp;gt;org.springframework.boot&amp;lt;/groupId&amp;gt;
				&amp;lt;artifactId&amp;gt;spring-boot-maven-plugin&amp;lt;/artifactId&amp;gt;
			&amp;lt;/plugin&amp;gt;
		&amp;lt;/plugins&amp;gt;
	&amp;lt;/build&amp;gt;

&amp;lt;/project&amp;gt;

&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;5.2.2.编写代码&lt;/h3&gt;
&lt;p&gt;首先在启动类中注册&lt;code&gt;RestTemplate&lt;/code&gt;：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;@SpringBootApplication
public class UserConsumerDemoApplication {

    @Bean
    public RestTemplate restTemplate() {
        // 这次我们使用了OkHttp客户端,只需要注入工厂即可
        return new RestTemplate(new OkHttp3ClientHttpRequestFactory());
    }

    public static void main(String[] args) {
        SpringApplication.run(UserConsumerDemoApplication.class, args);
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;然后编写UserDao，注意，这里不是调用mapper查数据库，而是通过RestTemplate远程查询user-service-demo中的接口：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;@Component
public class UserDao {

    @Autowired
    private RestTemplate restTemplate;

    public User queryUserById(Long id){
        String url = &quot;http://localhost:8081/user/&quot; + id;
        return this.restTemplate.getForObject(url, User.class);
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;然后编写user-service，循环查询UserDAO信息：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;@Service
public class UserService {

    @Autowired
    private UserDao userDao;

    public List&amp;lt;User&amp;gt; querUserByIds(List&amp;lt;Long&amp;gt; ids){
        List&amp;lt;User&amp;gt; users = new ArrayList&amp;lt;&amp;gt;();
        for (Long id : ids) {
            User user = this.userDao.queryUserById(id);
            users.add(user);
        }
        return users;
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;编写controller：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;@RestController
@RequestMapping(&quot;consume&quot;)
public class ConsumerController {

    @Autowired
    private UserService userService;

    @GetMapping
    public List&amp;lt;User&amp;gt; consume(@RequestParam(&quot;ids&quot;) List&amp;lt;Long&amp;gt; ids) {
        return this.userService.queryUserByIds(ids);
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;5.2.3.启动测试：&lt;/h3&gt;
&lt;p&gt;因为我们没有配置端口，那么默认就是8080，我们访问：http://localhost:8080/consume?ids=6,7,8&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://tangtangit.com/_astro/1525594222408.ClUN5RFl_LIYGe.webp&quot; alt=&quot;1525594222408&quot; /&gt;&lt;/p&gt;
&lt;p&gt;一个简单的远程服务调用案例就实现了。&lt;/p&gt;
&lt;h2&gt;5.3.有没有问题？&lt;/h2&gt;
&lt;p&gt;简单回顾一下，刚才我们写了什么：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;use-service-demo：一个提供根据id查询用户的微服务&lt;/li&gt;
&lt;li&gt;consumer-demo：一个服务调用者，通过RestTemplate远程调用user-service-demo&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;流程如下：&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://tangtangit.com/_astro/1525595012668.o7BG83I5_1PnxL5.webp&quot; alt=&quot;1525595012668&quot; /&gt;&lt;/p&gt;
&lt;p&gt;存在什么问题？&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;在consumer中，我们把url地址硬编码到了代码中，不方便后期维护&lt;/li&gt;
&lt;li&gt;consumer需要记忆user-service的地址，如果出现变更，可能得不到通知，地址将失效&lt;/li&gt;
&lt;li&gt;consumer不清楚user-service的状态，服务宕机也不知道&lt;/li&gt;
&lt;li&gt;user-service只有1台服务，不具备高可用性&lt;/li&gt;
&lt;li&gt;即便user-service形成集群，consumer还需自己实现负载均衡&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;其实上面说的问题，概括一下就是分布式服务必然要面临的问题：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;服务管理
&lt;ul&gt;
&lt;li&gt;如何自动注册和发现&lt;/li&gt;
&lt;li&gt;如何实现状态监管&lt;/li&gt;
&lt;li&gt;如何实现动态路由&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;服务如何实现负载均衡&lt;/li&gt;
&lt;li&gt;服务如何解决容灾问题&lt;/li&gt;
&lt;li&gt;服务如何实现统一配置&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;以上的问题，我们都将在SpringCloud中得到答案。&lt;/p&gt;
&lt;h1&gt;6.Eureka注册中心&lt;/h1&gt;
&lt;h2&gt;6.1.认识Eureka&lt;/h2&gt;
&lt;p&gt;首先我们来解决第一问题，服务的管理。&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;问题分析&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;在刚才的案例中，user-service对外提供服务，需要对外暴露自己的地址。而consumer（调用者）需要记录服务提供者的地址。将来地址出现变更，还需要及时更新。这在服务较少的时候并不觉得有什么，但是在现在日益复杂的互联网环境，一个项目肯定会拆分出十几，甚至数十个微服务。此时如果还人为管理地址，不仅开发困难，将来测试、发布上线都会非常麻烦，这与DevOps的思想是背道而驰的。&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;网约车&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;这就好比是 网约车出现以前，人们出门叫车只能叫出租车。一些私家车想做出租却没有资格，被称为黑车。而很多人想要约车，但是无奈出租车太少，不方便。私家车很多却不敢拦，而且满大街的车，谁知道哪个才是愿意载人的。一个想要，一个愿意给，就是缺少引子，缺乏管理啊。&lt;/p&gt;
&lt;p&gt;此时滴滴这样的网约车平台出现了，所有想载客的私家车全部到滴滴注册，记录你的车型（服务类型），身份信息（联系方式）。这样提供服务的私家车，在滴滴那里都能找到，一目了然。&lt;/p&gt;
&lt;p&gt;此时要叫车的人，只需要打开APP，输入你的目的地，选择车型（服务类型），滴滴自动安排一个符合需求的车到你面前，为你服务，完美！&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;Eureka做什么？&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;Eureka就好比是滴滴，负责管理、记录服务提供者的信息。服务调用者无需自己寻找服务，而是把自己的需求告诉Eureka，然后Eureka会把符合你需求的服务告诉你。&lt;/p&gt;
&lt;p&gt;同时，服务提供方与Eureka之间通过&lt;code&gt;“心跳”&lt;/code&gt;机制进行监控，当某个服务提供方出现问题，Eureka自然会把它从服务列表中剔除。&lt;/p&gt;
&lt;p&gt;这就实现了服务的自动注册、发现、状态监控。&lt;/p&gt;
&lt;h2&gt;6.2.原理图&lt;/h2&gt;
&lt;blockquote&gt;
&lt;p&gt;基本架构：&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;&lt;img src=&quot;https://tangtangit.com/_astro/1525597885059.Ctlo_o9t_HG3qx.webp&quot; alt=&quot;1525597885059&quot; /&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Eureka：就是服务注册中心（可以是一个集群），对外暴露自己的地址&lt;/li&gt;
&lt;li&gt;提供者：启动后向Eureka注册自己信息（地址，提供什么服务）&lt;/li&gt;
&lt;li&gt;消费者：向Eureka订阅服务，Eureka会将对应服务的所有提供者地址列表发送给消费者，并且定期更新&lt;/li&gt;
&lt;li&gt;心跳(续约)：提供者定期通过http方式向Eureka刷新自己的状态&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;6.3.入门案例&lt;/h2&gt;
&lt;h3&gt;6.3.1.编写EurekaServer&lt;/h3&gt;
&lt;p&gt;接下来我们创建一个项目，启动一个EurekaServer：&lt;/p&gt;
&lt;p&gt;依然使用spring提供的快速搭建工具：&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://tangtangit.com/_astro/1525598231170.B5A0BXTM_Z1QAycw.webp&quot; alt=&quot;1525598231170&quot; /&gt;&lt;/p&gt;
&lt;p&gt;选择依赖：&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://tangtangit.com/_astro/1525598312368.Cq4lSzhF_2aE5GN.webp&quot; alt=&quot;1525598312368&quot; /&gt;&lt;/p&gt;
&lt;p&gt;完整的Pom文件：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&amp;lt;?xml version=&quot;1.0&quot; encoding=&quot;UTF-8&quot;?&amp;gt;
&amp;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;&amp;gt;
	&amp;lt;modelVersion&amp;gt;4.0.0&amp;lt;/modelVersion&amp;gt;

	&amp;lt;groupId&amp;gt;com.leyou.demo&amp;lt;/groupId&amp;gt;
	&amp;lt;artifactId&amp;gt;eureka-demo&amp;lt;/artifactId&amp;gt;
	&amp;lt;version&amp;gt;0.0.1-SNAPSHOT&amp;lt;/version&amp;gt;
	&amp;lt;packaging&amp;gt;jar&amp;lt;/packaging&amp;gt;

	&amp;lt;name&amp;gt;eureka-demo&amp;lt;/name&amp;gt;
	&amp;lt;description&amp;gt;Demo project for Spring Boot&amp;lt;/description&amp;gt;

	&amp;lt;parent&amp;gt;
		&amp;lt;groupId&amp;gt;org.springframework.boot&amp;lt;/groupId&amp;gt;
		&amp;lt;artifactId&amp;gt;spring-boot-starter-parent&amp;lt;/artifactId&amp;gt;
		&amp;lt;version&amp;gt;2.0.1.RELEASE&amp;lt;/version&amp;gt;
		&amp;lt;relativePath/&amp;gt; &amp;lt;!-- lookup parent from repository --&amp;gt;
	&amp;lt;/parent&amp;gt;

	&amp;lt;properties&amp;gt;
		&amp;lt;project.build.sourceEncoding&amp;gt;UTF-8&amp;lt;/project.build.sourceEncoding&amp;gt;
		&amp;lt;project.reporting.outputEncoding&amp;gt;UTF-8&amp;lt;/project.reporting.outputEncoding&amp;gt;
		&amp;lt;java.version&amp;gt;1.8&amp;lt;/java.version&amp;gt;
        &amp;lt;!-- SpringCloud版本，是最新的F系列 --&amp;gt;
		&amp;lt;spring-cloud.version&amp;gt;Finchley.RC1&amp;lt;/spring-cloud.version&amp;gt;
	&amp;lt;/properties&amp;gt;

	&amp;lt;dependencies&amp;gt;
        &amp;lt;!-- Eureka服务端 --&amp;gt;
		&amp;lt;dependency&amp;gt;
			&amp;lt;groupId&amp;gt;org.springframework.cloud&amp;lt;/groupId&amp;gt;
			&amp;lt;artifactId&amp;gt;spring-cloud-starter-netflix-eureka-server&amp;lt;/artifactId&amp;gt;
		&amp;lt;/dependency&amp;gt;
	&amp;lt;/dependencies&amp;gt;

	&amp;lt;dependencyManagement&amp;gt;
		&amp;lt;dependencies&amp;gt;
            &amp;lt;!-- SpringCloud依赖，一定要放到dependencyManagement中，起到管理版本的作用即可 --&amp;gt;
			&amp;lt;dependency&amp;gt;
				&amp;lt;groupId&amp;gt;org.springframework.cloud&amp;lt;/groupId&amp;gt;
				&amp;lt;artifactId&amp;gt;spring-cloud-dependencies&amp;lt;/artifactId&amp;gt;
				&amp;lt;version&amp;gt;${spring-cloud.version}&amp;lt;/version&amp;gt;
				&amp;lt;type&amp;gt;pom&amp;lt;/type&amp;gt;
				&amp;lt;scope&amp;gt;import&amp;lt;/scope&amp;gt;
			&amp;lt;/dependency&amp;gt;
		&amp;lt;/dependencies&amp;gt;
	&amp;lt;/dependencyManagement&amp;gt;

	&amp;lt;build&amp;gt;
		&amp;lt;plugins&amp;gt;
			&amp;lt;plugin&amp;gt;
				&amp;lt;groupId&amp;gt;org.springframework.boot&amp;lt;/groupId&amp;gt;
				&amp;lt;artifactId&amp;gt;spring-boot-maven-plugin&amp;lt;/artifactId&amp;gt;
			&amp;lt;/plugin&amp;gt;
		&amp;lt;/plugins&amp;gt;
	&amp;lt;/build&amp;gt;

	&amp;lt;repositories&amp;gt;
		&amp;lt;repository&amp;gt;
			&amp;lt;id&amp;gt;spring-milestones&amp;lt;/id&amp;gt;
			&amp;lt;name&amp;gt;Spring Milestones&amp;lt;/name&amp;gt;
			&amp;lt;url&amp;gt;https://repo.spring.io/milestone&amp;lt;/url&amp;gt;
			&amp;lt;snapshots&amp;gt;
				&amp;lt;enabled&amp;gt;false&amp;lt;/enabled&amp;gt;
			&amp;lt;/snapshots&amp;gt;
		&amp;lt;/repository&amp;gt;
	&amp;lt;/repositories&amp;gt;
&amp;lt;/project&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;编写启动类：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;@SpringBootApplication
@EnableEurekaServer // 声明这个应用是一个EurekaServer
public class EurekaDemoApplication {

	public static void main(String[] args) {
		SpringApplication.run(EurekaDemoApplication.class, args);
	}
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;编写配置：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;server:
  port: 10086 # 端口
spring:
  application:
    name: eureka-server # 应用名称，会在Eureka中显示
eureka:
  client:
    register-with-eureka: false # 是否注册自己的信息到EurekaServer，默认是true
    fetch-registry: false # 是否拉取其它服务的信息，默认是true
    service-url: # EurekaServer的地址，现在是自己的地址，如果是集群，需要加上其它Server的地址。
      defaultZone: http://127.0.0.1:${server.port}/eureka

&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;启动服务，并访问：http://127.0.0.1:10086/eureka&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://tangtangit.com/_astro/1525604959508.8UfloFWu_Z2lesB0.webp&quot; alt=&quot;1525604959508&quot; /&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://tangtangit.com/_astro/1525605081129.DyUlTB8b_233H90.webp&quot; alt=&quot;1525605081129&quot; /&gt;&lt;/p&gt;
&lt;h3&gt;6.3.2.将user-service注册到Eureka&lt;/h3&gt;
&lt;p&gt;注册服务，就是在服务上添加Eureka的客户端依赖，客户端代码会自动把服务注册到EurekaServer中。&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;我们在user-service-demo中添加Eureka客户端依赖：&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;先添加SpringCloud依赖：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&amp;lt;!-- SpringCloud的依赖 --&amp;gt;
&amp;lt;dependencyManagement&amp;gt;
    &amp;lt;dependencies&amp;gt;
        &amp;lt;dependency&amp;gt;
            &amp;lt;groupId&amp;gt;org.springframework.cloud&amp;lt;/groupId&amp;gt;
            &amp;lt;artifactId&amp;gt;spring-cloud-dependencies&amp;lt;/artifactId&amp;gt;
            &amp;lt;version&amp;gt;Finchley.RC1&amp;lt;/version&amp;gt;
            &amp;lt;type&amp;gt;pom&amp;lt;/type&amp;gt;
            &amp;lt;scope&amp;gt;import&amp;lt;/scope&amp;gt;
        &amp;lt;/dependency&amp;gt;
    &amp;lt;/dependencies&amp;gt;
&amp;lt;/dependencyManagement&amp;gt;
&amp;lt;!-- Spring的仓库地址 --&amp;gt;
&amp;lt;repositories&amp;gt;
    &amp;lt;repository&amp;gt;
        &amp;lt;id&amp;gt;spring-milestones&amp;lt;/id&amp;gt;
        &amp;lt;name&amp;gt;Spring Milestones&amp;lt;/name&amp;gt;
        &amp;lt;url&amp;gt;https://repo.spring.io/milestone&amp;lt;/url&amp;gt;
        &amp;lt;snapshots&amp;gt;
            &amp;lt;enabled&amp;gt;false&amp;lt;/enabled&amp;gt;
        &amp;lt;/snapshots&amp;gt;
    &amp;lt;/repository&amp;gt;
&amp;lt;/repositories&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;然后是Eureka客户端：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&amp;lt;!-- Eureka客户端 --&amp;gt;
&amp;lt;dependency&amp;gt;
    &amp;lt;groupId&amp;gt;org.springframework.cloud&amp;lt;/groupId&amp;gt;
    &amp;lt;artifactId&amp;gt;spring-cloud-starter-netflix-eureka-client&amp;lt;/artifactId&amp;gt;
&amp;lt;/dependency&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote&gt;
&lt;p&gt;在启动类上开启Eureka客户端功能&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;通过添加&lt;code&gt;@EnableDiscoveryClient&lt;/code&gt;来开启Eureka客户端功能&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;@SpringBootApplication
@EnableDiscoveryClient // 开启EurekaClient功能
public class UserServiceDemoApplication {
	public static void main(String[] args) {
		SpringApplication.run(UserServiceDemoApplication.class, args);
	}
}
&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote&gt;
&lt;p&gt;编写配置&lt;/p&gt;
&lt;/blockquote&gt;
&lt;pre&gt;&lt;code&gt;server:
  port: 8081
spring:
  datasource:
    url: jdbc:mysql://localhost:3306/mydb01
    username: root
    password: 123
    hikari:
      maximum-pool-size: 20
      minimum-idle: 10
  application:
    name: user-service # 应用名称
mybatis:
  type-aliases-package: com.leyou.userservice.pojo
eureka:
  client:
    service-url: # EurekaServer地址
      defaultZone: http://127.0.0.1:10086/eureka
  instance:
    prefer-ip-address: true # 当调用getHostname获取实例的hostname时，返回ip而不是host名称
    ip-address: 127.0.0.1 # 指定自己的ip信息，不指定的话会自己寻找
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;注意：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;这里我们添加了spring.application.name属性来指定应用名称，将来会作为应用的id使用。&lt;/li&gt;
&lt;li&gt;不用指定register-with-eureka和fetch-registry，因为默认是true&lt;/li&gt;
&lt;/ul&gt;
&lt;blockquote&gt;
&lt;p&gt;重启项目，访问&lt;a href=&quot;http://127.0.0.1:10086/eureka&quot;&gt;Eureka监控页面&lt;/a&gt;查看&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;&lt;img src=&quot;https://tangtangit.com/_astro/1525609225152.DZHjFnIy_Z2fw3yQ.webp&quot; alt=&quot;1525609225152&quot; /&gt;&lt;/p&gt;
&lt;p&gt;我们发现user-service服务已经注册成功了&lt;/p&gt;
&lt;h3&gt;6.3.3.消费者从Eureka获取服务&lt;/h3&gt;
&lt;p&gt;接下来我们修改consumer-demo，尝试从EurekaServer获取服务。&lt;/p&gt;
&lt;p&gt;方法与消费者类似，只需要在项目中添加EurekaClient依赖，就可以通过服务名称来获取信息了！&lt;/p&gt;
&lt;p&gt;1）添加依赖：&lt;/p&gt;
&lt;p&gt;先添加SpringCloud依赖：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&amp;lt;!-- SpringCloud的依赖 --&amp;gt;
&amp;lt;dependencyManagement&amp;gt;
    &amp;lt;dependencies&amp;gt;
        &amp;lt;dependency&amp;gt;
            &amp;lt;groupId&amp;gt;org.springframework.cloud&amp;lt;/groupId&amp;gt;
            &amp;lt;artifactId&amp;gt;spring-cloud-dependencies&amp;lt;/artifactId&amp;gt;
            &amp;lt;version&amp;gt;Finchley.RC1&amp;lt;/version&amp;gt;
            &amp;lt;type&amp;gt;pom&amp;lt;/type&amp;gt;
            &amp;lt;scope&amp;gt;import&amp;lt;/scope&amp;gt;
        &amp;lt;/dependency&amp;gt;
    &amp;lt;/dependencies&amp;gt;
&amp;lt;/dependencyManagement&amp;gt;
&amp;lt;!-- Spring的仓库地址 --&amp;gt;
&amp;lt;repositories&amp;gt;
    &amp;lt;repository&amp;gt;
        &amp;lt;id&amp;gt;spring-milestones&amp;lt;/id&amp;gt;
        &amp;lt;name&amp;gt;Spring Milestones&amp;lt;/name&amp;gt;
        &amp;lt;url&amp;gt;https://repo.spring.io/milestone&amp;lt;/url&amp;gt;
        &amp;lt;snapshots&amp;gt;
            &amp;lt;enabled&amp;gt;false&amp;lt;/enabled&amp;gt;
        &amp;lt;/snapshots&amp;gt;
    &amp;lt;/repository&amp;gt;
&amp;lt;/repositories&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;然后是Eureka客户端：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&amp;lt;!-- Eureka客户端 --&amp;gt;
&amp;lt;dependency&amp;gt;
    &amp;lt;groupId&amp;gt;org.springframework.cloud&amp;lt;/groupId&amp;gt;
    &amp;lt;artifactId&amp;gt;spring-cloud-starter-netflix-eureka-client&amp;lt;/artifactId&amp;gt;
&amp;lt;/dependency&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;2）在启动类开启Eureka客户端&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;@SpringBootApplication
@EnableDiscoveryClient // 开启Eureka客户端
public class UserConsumerDemoApplication {
    @Bean
    public RestTemplate restTemplate() {
        return new RestTemplate(new OkHttp3ClientHttpRequestFactory());
    }
    public static void main(String[] args) {
        SpringApplication.run(UserConsumerDemoApplication.class, args);
    }
}

&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;3）修改配置：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;server:
  port: 8080
spring:
  application:
    name: consumer # 应用名称
eureka:
  client:
    service-url: # EurekaServer地址
      defaultZone: http://127.0.0.1:10086/eureka
  instance:
    prefer-ip-address: true # 当其它服务获取地址时提供ip而不是hostname
    ip-address: 127.0.0.1 # 指定自己的ip信息，不指定的话会自己寻找
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;4）修改代码，用DiscoveryClient类的方法，根据服务名称，获取服务实例：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;@Service
public class UserService {

    @Autowired
    private RestTemplate restTemplate;

    @Autowired
    private DiscoveryClient discoveryClient;// Eureka客户端，可以获取到服务实例信息

    public List&amp;lt;User&amp;gt; queryUserByIds(List&amp;lt;Long&amp;gt; ids) {
        List&amp;lt;User&amp;gt; users = new ArrayList&amp;lt;&amp;gt;();
        // String baseUrl = &quot;http://localhost:8081/user/&quot;;
        // 根据服务名称，获取服务实例
        List&amp;lt;ServiceInstance&amp;gt; instances = discoveryClient.getInstances(&quot;user-service&quot;);
        // 因为只有一个UserService,因此我们直接get(0)获取
        ServiceInstance instance = instances.get(0);
        // 获取ip和端口信息
        String baseUrl = &quot;http://&quot;+instance.getHost() + &quot;:&quot; + instance.getPort()+&quot;/user/&quot;;
        ids.forEach(id -&amp;gt; {
            // 我们测试多次查询，
            users.add(this.restTemplate.getForObject(baseUrl + id, User.class));
            // 每次间隔500毫秒
            try {
                Thread.sleep(500);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        });
        return users;
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;5）Debug跟踪运行：&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://tangtangit.com/_astro/1525613025086.Dl5IFDKT_1BEUkQ.webp&quot; alt=&quot;1525613025086&quot; /&gt;&lt;/p&gt;
&lt;p&gt;生成的URL：&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://tangtangit.com/_astro/1525613051210.CvCQk0xs_2jgQtU.webp&quot; alt=&quot;1525613051210&quot; /&gt;&lt;/p&gt;
&lt;p&gt;访问结果：&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://tangtangit.com/_astro/1525613160920.Dk2WvmJQ_Z2aONNg.webp&quot; alt=&quot;1525613160920&quot; /&gt;&lt;/p&gt;
&lt;h2&gt;6.4.Eureka详解&lt;/h2&gt;
&lt;p&gt;接下来我们详细讲解Eureka的原理及配置。&lt;/p&gt;
&lt;h3&gt;6.4.1.基础架构&lt;/h3&gt;
&lt;p&gt;Eureka架构中的三个核心角色：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;服务注册中心&lt;/p&gt;
&lt;p&gt;Eureka的服务端应用，提供服务注册和发现功能，就是刚刚我们建立的eureka-demo&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;服务提供者&lt;/p&gt;
&lt;p&gt;提供服务的应用，可以是SpringBoot应用，也可以是其它任意技术实现，只要对外提供的是Rest风格服务即可。本例中就是我们实现的user-service-demo&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;服务消费者&lt;/p&gt;
&lt;p&gt;消费应用从注册中心获取服务列表，从而得知每个服务方的信息，知道去哪里调用服务方。本例中就是我们实现的consumer-demo&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;6.4.2.高可用的Eureka Server&lt;/h3&gt;
&lt;p&gt;Eureka Server即服务的注册中心，在刚才的案例中，我们只有一个EurekaServer，事实上EurekaServer也可以是一个集群，形成高可用的Eureka中心。&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;服务同步&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;多个Eureka Server之间也会互相注册为服务，当服务提供者注册到Eureka Server集群中的某个节点时，该节点会把服务的信息同步给集群中的每个节点，从而实现&lt;strong&gt;数据同步&lt;/strong&gt;。因此，无论客户端访问到Eureka Server集群中的任意一个节点，都可以获取到完整的服务列表信息。&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;动手搭建高可用的EurekaServer&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;我们假设要搭建两条EurekaServer的集群，端口分别为：10086和10087&lt;/p&gt;
&lt;p&gt;1）我们修改原来的EurekaServer配置：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;server:
  port: 10086 # 端口
spring:
  application:
    name: eureka-server # 应用名称，会在Eureka中显示
eureka:
  client:
    service-url: # 配置其他Eureka服务的地址，而不是自己，比如10087
      defaultZone: http://127.0.0.1:10087/eureka

&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;所谓的高可用注册中心，其实就是把EurekaServer自己也作为一个服务进行注册，这样多个EurekaServer之间就能互相发现对方，从而形成集群。因此我们做了以下修改：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;删除了register-with-eureka=false和fetch-registry=false两个配置。因为默认值是true，这样就会吧自己注册到注册中心了。&lt;/li&gt;
&lt;li&gt;把service-url的值改成了另外一台EurekaServer的地址，而不是自己&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;2）另外一台配置恰好相反：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;server:
  port: 10087 # 端口
spring:
  application:
    name: eureka-server # 应用名称，会在Eureka中显示
eureka:
  client:
    service-url: # 配置其他Eureka服务的地址，而不是自己，比如10087
      defaultZone: http://127.0.0.1:10086/eureka

&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;注意：idea中一个应用不能启动两次，我们需要重新配置一个启动器：&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://tangtangit.com/_astro/1525615070033.Eof0vO8d_1Ui6KR.webp&quot; alt=&quot;1525615070033&quot; /&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://tangtangit.com/_astro/1525615095693.D2CXpXH9_M4IB3.webp&quot; alt=&quot;1525615095693&quot; /&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://tangtangit.com/_astro/1525615026937.B_WO41v8_QKDU0.webp&quot; alt=&quot;1525615026937&quot; /&gt;&lt;/p&gt;
&lt;p&gt;然后启动即可。&lt;/p&gt;
&lt;p&gt;3）启动测试：&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://tangtangit.com/_astro/1525615165157.CvfT-rGE_Z1u7kpO.webp&quot; alt=&quot;1525615165157&quot; /&gt;&lt;/p&gt;
&lt;p&gt;4）客户端注册服务到集群&lt;/p&gt;
&lt;p&gt;因为EurekaServer不止一个，因此注册服务的时候，service-url参数需要变化：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;eureka:
  client:
    service-url: # EurekaServer地址,多个地址以&apos;,&apos;隔开
      defaultZone: http://127.0.0.1:10086/eureka,http://127.0.0.1:10087/eureka
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;6.4.3.服务提供者&lt;/h3&gt;
&lt;p&gt;服务提供者要向EurekaServer注册服务，并且完成服务续约等工作。&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;服务注册&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;服务提供者在启动时，会检测配置属性中的：&lt;code&gt;eureka.client.register-with-erueka=true&lt;/code&gt;参数是否正确，事实上默认就是true。如果值确实为true，则会向EurekaServer发起一个Rest请求，并携带自己的元数据信息，Eureka Server会把这些信息保存到一个双层Map结构中。第一层Map的Key就是服务名称，第二层Map的key是服务的实例id。&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;服务续约&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;在注册服务完成以后，服务提供者会维持一个心跳（定时向EurekaServer发起Rest请求），告诉EurekaServer：“我还活着”。这个我们称为服务的续约（renew）；&lt;/p&gt;
&lt;p&gt;有两个重要参数可以修改服务续约的行为：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;eureka:
  instance:
    lease-expiration-duration-in-seconds: 90
    lease-renewal-interval-in-seconds: 30
&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;lease-renewal-interval-in-seconds：服务续约(renew)的间隔，默认为30秒&lt;/li&gt;
&lt;li&gt;lease-expiration-duration-in-seconds：服务失效时间，默认值90秒&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;也就是说，默认情况下每个30秒服务会向注册中心发送一次心跳，证明自己还活着。如果超过90秒没有发送心跳，EurekaServer就会认为该服务宕机，会从服务列表中移除，这两个值在生产环境不要修改，默认即可。&lt;/p&gt;
&lt;p&gt;但是在开发时，这个值有点太长了，经常我们关掉一个服务，会发现Eureka依然认为服务在活着。所以我们在开发阶段可以适当调小。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;eureka:
  instance:
    lease-expiration-duration-in-seconds: 10 # 10秒即过期
    lease-renewal-interval-in-seconds: 5 # 5秒一次心跳
&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote&gt;
&lt;p&gt;实例id&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;先来看一下服务状态信息：&lt;/p&gt;
&lt;p&gt;在Eureka监控页面，查看服务注册信息：&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://tangtangit.com/_astro/1525617060656.BzrJgwae_Z4klku.webp&quot; alt=&quot;1525617060656&quot; /&gt;&lt;/p&gt;
&lt;p&gt;在status一列中，显示以下信息：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;UP(1)：代表现在是启动了1个示例，没有集群&lt;/li&gt;
&lt;li&gt;DESKTOP-2MVEC12:user-service:8081：是示例的名称（instance-id），
&lt;ul&gt;
&lt;li&gt;默认格式是：&lt;code&gt;${hostname} + ${spring.application.name} + ${server.port}&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;instance-id是区分同一服务的不同实例的唯一标准，因此不能重复。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;我们可以通过instance-id属性来修改它的构成：&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;这里需要注意的地方就是，谁去注册想要改名字，谁就去设置这个属性&lt;/p&gt;
&lt;/blockquote&gt;
&lt;pre&gt;&lt;code&gt;eureka:
  instance:
    instance-id: ${spring.application.name}:${server.port}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;更加完整的配置&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;修改成使用 IP + 端口 形式 每次重启后就不会再cosnul上重复注册一个新的服务&lt;/p&gt;
&lt;/blockquote&gt;
&lt;pre&gt;&lt;code&gt;  instance:
    instance-id: ${spring.application.name}:${spring.cloud.client.ip-address}:${server.port}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;重启服务再试试看：&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://tangtangit.com/_astro/1525617542081.Cy3kZmnN_CujKc.webp&quot; alt=&quot;1525617542081&quot; /&gt;&lt;/p&gt;
&lt;h3&gt;6.4.4.服务消费者&lt;/h3&gt;
&lt;blockquote&gt;
&lt;p&gt;获取服务列表&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;当服务消费者启动是，会检测&lt;code&gt;eureka.client.fetch-registry=true&lt;/code&gt;参数的值，如果为true，则会从Eureka Server服务的列表只读备份，然后缓存在本地。并且&lt;code&gt;每隔30秒&lt;/code&gt;会重新获取并更新数据。我们可以通过下面的参数来修改：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;eureka:
  client:
    registry-fetch-interval-seconds: 5
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;生产环境中，我们不需要修改这个值。&lt;/p&gt;
&lt;p&gt;但是为了开发环境下，能够快速得到服务的最新状态，我们可以将其设置小一点。&lt;/p&gt;
&lt;h3&gt;6.4.5.失效剔除和自我保护&lt;/h3&gt;
&lt;blockquote&gt;
&lt;p&gt;失效剔除&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;有些时候，我们的服务提供方并不一定会正常下线，可能因为内存溢出、网络故障等原因导致服务无法正常工作。Eureka Server需要将这样的服务剔除出服务列表。因此它会开启一个定时任务，每隔60秒对所有失效的服务（超过90秒未响应）进行剔除。&lt;/p&gt;
&lt;p&gt;可以通过&lt;code&gt;eureka.server.eviction-interval-timer-in-ms&lt;/code&gt;参数对其进行修改，单位是毫秒，生成环境不要修改。&lt;/p&gt;
&lt;p&gt;这个会对我们开发带来极大的不变，你对服务重启，隔了60秒Eureka才反应过来。开发阶段可以适当调整，比如10S&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;自我保护&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;我们关停一个服务，就会在Eureka面板看到一条警告：&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://tangtangit.com/_astro/1525618396076.JD2fPyoh_Z2rSNxr.webp&quot; alt=&quot;1525618396076&quot; /&gt;&lt;/p&gt;
&lt;p&gt;这是触发了Eureka的自我保护机制。当一个服务未按时进行心跳续约时，Eureka会统计最近15分钟心跳失败的服务实例的比例是否超过了85%。在生产环境下，因为网络延迟等原因，心跳失败实例的比例很有可能超标，但是此时就把服务剔除列表并不妥当，因为服务可能没有宕机。Eureka就会把当前实例的注册信息保护起来，不予剔除。生产环境下这很有效，保证了大多数服务依然可用。&lt;/p&gt;
&lt;p&gt;但是这给我们的开发带来了麻烦， 因此开发阶段我们都会关闭自我保护模式：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;eureka:
  server:
    enable-self-preservation: false # 关闭自我保护模式（缺省为打开）
    eviction-interval-timer-in-ms: 1000 # 扫描失效服务的间隔时间（缺省为60*1000ms）
&lt;/code&gt;&lt;/pre&gt;
&lt;h1&gt;7.负载均衡Robbin&lt;/h1&gt;
&lt;p&gt;在刚才的案例中，我们启动了一个user-service，然后通过DiscoveryClient来获取服务实例信息，然后获取ip和端口来访问。&lt;/p&gt;
&lt;p&gt;但是实际环境中，我们往往会开启很多个user-service的集群。此时我们获取的服务列表中就会有多个，到底该访问哪一个呢？&lt;/p&gt;
&lt;p&gt;一般这种情况下我们就需要编写负载均衡算法，在多个实例列表中进行选择。&lt;/p&gt;
&lt;p&gt;不过Eureka中已经帮我们集成了负载均衡组件：Ribbon，简单修改代码即可使用。&lt;/p&gt;
&lt;p&gt;什么是Ribbon：&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://tangtangit.com/_astro/1525619257397.BrYllvPk_Z2aS1wE.webp&quot; alt=&quot;1525619257397&quot; /&gt;&lt;/p&gt;
&lt;p&gt;接下来，我们就来使用Ribbon实现负载均衡。&lt;/p&gt;
&lt;h2&gt;7.1.启动两个服务实例&lt;/h2&gt;
&lt;p&gt;首先我们启动两个user-service实例，一个8081，一个8082。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://tangtangit.com/_astro/1525619515586.CkW_hxy5_ZlcEAJ.webp&quot; alt=&quot;1525619515586&quot; /&gt;&lt;/p&gt;
&lt;p&gt;Eureka监控面板：&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://tangtangit.com/_astro/1525619546904.BGvGrGHL_kVjDc.webp&quot; alt=&quot;1525619546904&quot; /&gt;&lt;/p&gt;
&lt;h2&gt;7.2.开启负载均衡&lt;/h2&gt;
&lt;p&gt;因为Eureka中已经集成了Ribbon，所以我们无需引入新的依赖。直接修改代码：&lt;/p&gt;
&lt;p&gt;在RestTemplate的配置方法上添加&lt;code&gt;@LoadBalanced&lt;/code&gt;注解：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;@Bean
@LoadBalanced
public RestTemplate restTemplate() {
    return new RestTemplate(new OkHttp3ClientHttpRequestFactory());
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;修改调用方式，不再手动获取ip和端口，而是直接通过服务名称调用：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;@Service
public class UserService {

    @Autowired
    private RestTemplate restTemplate;

    @Autowired
    private DiscoveryClient discoveryClient;

    public List&amp;lt;User&amp;gt; queryUserByIds(List&amp;lt;Long&amp;gt; ids) {
        List&amp;lt;User&amp;gt; users = new ArrayList&amp;lt;&amp;gt;();
        // 地址直接写服务名称即可
        String baseUrl = &quot;http://user-service/user/&quot;;
        ids.forEach(id -&amp;gt; {
            // 我们测试多次查询，
            users.add(this.restTemplate.getForObject(baseUrl + id, User.class));
            // 每次间隔500毫秒
            try {
                Thread.sleep(500);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        });
        return users;
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;访问页面，查看结果：&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://tangtangit.com/_astro/1525620305704.Df8lCPTH_Z7uh3n.webp&quot; alt=&quot;1525620305704&quot; /&gt;&lt;/p&gt;
&lt;p&gt;完美！&lt;/p&gt;
&lt;h2&gt;7.3.源码跟踪&lt;/h2&gt;
&lt;p&gt;为什么我们只输入了service名称就可以访问了呢？之前还要获取ip和端口。&lt;/p&gt;
&lt;p&gt;显然有人帮我们根据service名称，获取到了服务实例的ip和端口。它就是&lt;code&gt;LoadBalancerInterceptor&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;我们进行源码跟踪：&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://tangtangit.com/_astro/1525620483637.D1Zo88aQ_Z1UYUkC.webp&quot; alt=&quot;1525620483637&quot; /&gt;&lt;/p&gt;
&lt;p&gt;继续跟入execute方法：发现获取了8082端口的服务&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://tangtangit.com/_astro/1525620787090.Cep3X9ie_2dQM1N.webp&quot; alt=&quot;1525620787090&quot; /&gt;&lt;/p&gt;
&lt;p&gt;再跟下一次，发现获取的是8081：&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://tangtangit.com/_astro/1525620835911.D62_yU79_Z2gwy4I.webp&quot; alt=&quot;1525620835911&quot; /&gt;&lt;/p&gt;
&lt;h2&gt;7.4.负载均衡策略&lt;/h2&gt;
&lt;p&gt;Ribbon默认的负载均衡策略是简单的轮询，我们可以测试一下：&lt;/p&gt;
&lt;p&gt;编写测试类，在刚才的源码中我们看到拦截中是使用RibbonLoadBalanceClient来进行负载均衡的，其中有一个choose方法，是这样介绍的：&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://tangtangit.com/_astro/1525622320277.B_ey53-6_1X7XxW.webp&quot; alt=&quot;1525622320277&quot; /&gt;&lt;/p&gt;
&lt;p&gt;现在这个就是负载均衡获取实例的方法。&lt;/p&gt;
&lt;p&gt;我们对注入这个类的对象，然后对其测试：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;@RunWith(SpringRunner.class)
@SpringBootTest(classes = UserConsumerDemoApplication.class)
public class LoadBalanceTest {

    @Autowired
    RibbonLoadBalancerClient client;

    @Test
    public void test(){
        for (int i = 0; i &amp;lt; 100; i++) {
            ServiceInstance instance = this.client.choose(&quot;user-service&quot;);
            System.out.println(instance.getHost() + &quot;:&quot; + instance.getPort());
        }
    }
}

&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;结果：&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://tangtangit.com/_astro/1525622357371.y4_6BDXG_22mrcO.webp&quot; alt=&quot;1525622357371&quot; /&gt;&lt;/p&gt;
&lt;p&gt;符合了我们的预期推测，确实是轮询方式。&lt;/p&gt;
&lt;p&gt;我们是否可以修改负载均衡的策略呢？&lt;/p&gt;
&lt;p&gt;继续跟踪源码，发现这么一段代码：&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://tangtangit.com/_astro/1525622652849.DYtSsEax_1YzoG5.webp&quot; alt=&quot;1525622652849&quot; /&gt;&lt;/p&gt;
&lt;p&gt;我们看看这个rule是谁：&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://tangtangit.com/_astro/1525622699666.DWVxBgPT_Z3xRQP.webp&quot; alt=&quot;1525622699666&quot; /&gt;&lt;/p&gt;
&lt;p&gt;这里的rule默认值是一个&lt;code&gt;RoundRobinRule&lt;/code&gt;，看类的介绍：&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://tangtangit.com/_astro/1525622754316.Cj-nB18Z_1WwWLP.webp&quot; alt=&quot;1525622754316&quot; /&gt;&lt;/p&gt;
&lt;p&gt;这不就是轮询的意思嘛。&lt;/p&gt;
&lt;p&gt;我们注意到，这个类其实是实现了接口IRule的，查看一下：&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://tangtangit.com/_astro/1525622817451.CUyMIYop_cRLDK.webp&quot; alt=&quot;1525622817451&quot; /&gt;&lt;/p&gt;
&lt;p&gt;定义负载均衡的规则接口。&lt;/p&gt;
&lt;p&gt;它有以下实现：&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://tangtangit.com/_astro/1525622876842.T4U_1byF_1HNdds.webp&quot; alt=&quot;1525622876842&quot; /&gt;&lt;/p&gt;
&lt;p&gt;SpringBoot也帮我们提供了修改负载均衡规则的配置入口：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;user-service:
  ribbon:
    NFLoadBalancerRuleClassName: com.netflix.loadbalancer.RandomRule
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;格式是：&lt;code&gt;{服务名称}.ribbon.NFLoadBalancerRuleClassName&lt;/code&gt;，值就是IRule的实现类。&lt;/p&gt;
&lt;p&gt;再次测试，发现结果变成了随机：&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://tangtangit.com/_astro/1525623193949.pUE4sbju_Z2ivdL5.webp&quot; alt=&quot;1525623193949&quot; /&gt;&lt;/p&gt;
&lt;h2&gt;7.5.重试机制&lt;/h2&gt;
&lt;p&gt;Eureka的服务治理强调了CAP原则中的AP，即可用性和可靠性。它与Zookeeper这一类强调CP（一致性，可靠性）的服务治理框架最大的区别在于：Eureka为了实现更高的服务可用性，牺牲了一定的一致性，极端情况下它宁愿接收故障实例也不愿丢掉健康实例，正如我们上面所说的自我保护机制。&lt;/p&gt;
&lt;p&gt;但是，此时如果我们调用了这些不正常的服务，调用就会失败，从而导致其它服务不能正常工作！这显然不是我们愿意看到的。&lt;/p&gt;
&lt;p&gt;我们现在关闭一个user-service实例：&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://tangtangit.com/_astro/1525653565855.Wf8VxxIS_ZXx43k.webp&quot; alt=&quot;1525653565855&quot; /&gt;&lt;/p&gt;
&lt;p&gt;因为服务剔除的延迟，consumer并不会立即得到最新的服务列表，此时再次访问你会得到错误提示：&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://tangtangit.com/_astro/1525653715488.DK_4fmUj_PcKC0.webp&quot; alt=&quot;1525653715488&quot; /&gt;&lt;/p&gt;
&lt;p&gt;但是此时，8081服务其实是正常的。&lt;/p&gt;
&lt;p&gt;因此Spring Cloud 整合了Spring Retry 来增强RestTemplate的重试能力，当一次服务调用失败后，不会立即抛出一次，而是再次重试另一个服务。&lt;/p&gt;
&lt;p&gt;只需要简单配置即可实现Ribbon的重试：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;spring:
  cloud:
    loadbalancer:
      retry:
        enabled: true # 开启Spring Cloud的重试功能
user-service:
  ribbon:
    ConnectTimeout: 250 # Ribbon的连接超时时间
    ReadTimeout: 1000 # Ribbon的数据读取超时时间
    OkToRetryOnAllOperations: true # 是否对所有操作都进行重试
    MaxAutoRetriesNextServer: 1 # 切换实例的重试次数
    MaxAutoRetries: 1 # 对当前实例的重试次数
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;根据如上配置，当访问到某个服务超时后，它会再次尝试访问下一个服务实例，如果不行就再换一个实例，如果不行，则返回失败。切换次数取决于&lt;code&gt;MaxAutoRetriesNextServer&lt;/code&gt;参数的值&lt;/p&gt;
&lt;p&gt;引入spring-retry依赖&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&amp;lt;dependency&amp;gt;
    &amp;lt;groupId&amp;gt;org.springframework.retry&amp;lt;/groupId&amp;gt;
    &amp;lt;artifactId&amp;gt;spring-retry&amp;lt;/artifactId&amp;gt;
&amp;lt;/dependency&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;我们重启user-consumer-demo，测试，发现即使user-service2宕机，也能通过另一台服务实例获取到结果！&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://tangtangit.com/_astro/1525658269456.kov1You9_Z11qrzc.webp&quot; alt=&quot;1525658269456&quot; /&gt;&lt;/p&gt;
</content:encoded></item><item><title>SpringSecurity与OAuth2安全框架完全指南</title><link>https://tangtangit.com/posts/spring%E7%94%9F%E6%80%81-05-springsecurity/</link><guid isPermaLink="true">https://tangtangit.com/posts/spring%E7%94%9F%E6%80%81-05-springsecurity/</guid><description>从SpringSecurity入门到精通、OAuth2认证授权、JWT令牌、微服务安全实战，涵盖认证授权流程、过滤器链、RBAC权限模型等核心安全知识。</description><pubDate>Sat, 14 Feb 2026 00:00:00 GMT</pubDate><content:encoded>&lt;h2&gt;SpringSecurity入门到精通&lt;/h2&gt;
&lt;h2&gt;SpringSecurity从入门到精通&lt;/h2&gt;
&lt;h3&gt;课程介绍&lt;/h3&gt;
&lt;p&gt;&lt;img src=&quot;https://tangtangit.com/_astro/image-20211219121555979.6Gs2oyQb_ZhrqUw.webp&quot; alt=&quot;image-20211219121555979&quot; /&gt;&lt;/p&gt;
&lt;h3&gt;0. 简介&lt;/h3&gt;
&lt;p&gt;​	&lt;strong&gt;Spring Security&lt;/strong&gt; 是 Spring 家族中的一个安全管理框架。相比与另外一个安全框架&lt;strong&gt;Shiro&lt;/strong&gt;，它提供了更丰富的功能，社区资源也比Shiro丰富。&lt;/p&gt;
&lt;p&gt;​	一般来说中大型的项目都是使用&lt;strong&gt;SpringSecurity&lt;/strong&gt; 来做安全框架。小项目有Shiro的比较多，因为相比与SpringSecurity，Shiro的上手更加的简单。&lt;/p&gt;
&lt;p&gt;​	 一般Web应用的需要进行&lt;strong&gt;认证&lt;/strong&gt;和&lt;strong&gt;授权&lt;/strong&gt;。&lt;/p&gt;
&lt;p&gt;​		&lt;strong&gt;认证：验证当前访问系统的是不是本系统的用户，并且要确认具体是哪个用户&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;​		&lt;strong&gt;授权：经过认证后判断当前用户是否有权限进行某个操作&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;​	而认证和授权也是SpringSecurity作为安全框架的核心功能。&lt;/p&gt;
&lt;h3&gt;1. 快速入门&lt;/h3&gt;
&lt;h4&gt;1.1 准备工作&lt;/h4&gt;
&lt;p&gt;​	我们先要搭建一个简单的SpringBoot工程&lt;/p&gt;
&lt;p&gt;① 设置父工程 添加依赖&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;    &amp;lt;parent&amp;gt;
        &amp;lt;groupId&amp;gt;org.springframework.boot&amp;lt;/groupId&amp;gt;
        &amp;lt;artifactId&amp;gt;spring-boot-starter-parent&amp;lt;/artifactId&amp;gt;
        &amp;lt;version&amp;gt;2.5.0&amp;lt;/version&amp;gt;
    &amp;lt;/parent&amp;gt;
    &amp;lt;dependencies&amp;gt;
        &amp;lt;dependency&amp;gt;
            &amp;lt;groupId&amp;gt;org.springframework.boot&amp;lt;/groupId&amp;gt;
            &amp;lt;artifactId&amp;gt;spring-boot-starter-web&amp;lt;/artifactId&amp;gt;
        &amp;lt;/dependency&amp;gt;
        &amp;lt;dependency&amp;gt;
            &amp;lt;groupId&amp;gt;org.projectlombok&amp;lt;/groupId&amp;gt;
            &amp;lt;artifactId&amp;gt;lombok&amp;lt;/artifactId&amp;gt;
            &amp;lt;optional&amp;gt;true&amp;lt;/optional&amp;gt;
        &amp;lt;/dependency&amp;gt;
    &amp;lt;/dependencies&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;② 创建启动类&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;@SpringBootApplication
public class SecurityApplication {

    public static void main(String[] args) {
        SpringApplication.run(SecurityApplication.class,args);
    }
}

&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;③ 创建Controller&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

@RestController
public class HelloController {

    @RequestMapping(&quot;/hello&quot;)
    public String hello(){
        return &quot;hello&quot;;
    }
}

&lt;/code&gt;&lt;/pre&gt;
&lt;h4&gt;1.2 引入SpringSecurity&lt;/h4&gt;
&lt;p&gt;​	在SpringBoot项目中使用SpringSecurity我们只需要引入依赖即可实现入门案例。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;        &amp;lt;dependency&amp;gt;
            &amp;lt;groupId&amp;gt;org.springframework.boot&amp;lt;/groupId&amp;gt;
            &amp;lt;artifactId&amp;gt;spring-boot-starter-security&amp;lt;/artifactId&amp;gt;
        &amp;lt;/dependency&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;​	引入依赖后我们在尝试去访问之前的接口就会自动跳转到一个SpringSecurity的默认登陆页面，默认用户名是user,密码会输出在控制台。&lt;/p&gt;
&lt;p&gt;​	必须登陆之后才能对接口进行访问。&lt;/p&gt;
&lt;h3&gt;2. 认证&lt;/h3&gt;
&lt;h4&gt;2.1 登陆校验流程&lt;/h4&gt;
&lt;p&gt;&lt;img src=&quot;https://tangtangit.com/_astro/image-20211215094003288.QjmigmWV_ZP2vxi.webp&quot; alt=&quot;image-20211215094003288&quot; /&gt;&lt;/p&gt;
&lt;h4&gt;2.2 原理初探&lt;/h4&gt;
&lt;p&gt;​	想要知道如何实现自己的登陆流程就必须要先知道入门案例中SpringSecurity的流程。&lt;/p&gt;
&lt;h5&gt;2.2.1 SpringSecurity完整流程&lt;/h5&gt;
&lt;p&gt;​	SpringSecurity的原理其实就是一个过滤器链，内部包含了提供各种功能的过滤器。这里我们可以看看入门案例中的过滤器。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://tangtangit.com/_astro/image-20211214144425527.Dhj4iJW7_Z2gG6Rk.webp&quot; alt=&quot;image-20211214144425527&quot; /&gt;&lt;/p&gt;
&lt;p&gt;​	图中只展示了核心过滤器，其它的非核心过滤器并没有在图中展示。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;UsernamePasswordAuthenticationFilter&lt;/strong&gt;:负责处理我们在登陆页面填写了用户名密码后的登陆请求。入门案例的认证工作主要有它负责。&lt;/p&gt;
&lt;p&gt;**ExceptionTranslationFilter：**处理过滤器链中抛出的任何AccessDeniedException和AuthenticationException 。&lt;/p&gt;
&lt;p&gt;**FilterSecurityInterceptor：**负责权限校验的过滤器。&lt;/p&gt;
&lt;p&gt;​&lt;/p&gt;
&lt;p&gt;​	我们可以通过Debug查看当前系统中SpringSecurity过滤器链中有哪些过滤器及它们的顺序。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://tangtangit.com/_astro/image-20211214145824903.Bhs1jeiP_Z1HaCRf.webp&quot; alt=&quot;image-20211214145824903&quot; /&gt;&lt;/p&gt;
&lt;h5&gt;2.2.2 认证流程详解&lt;/h5&gt;
&lt;p&gt;&lt;img src=&quot;https://tangtangit.com/_astro/image-20211214151515385.BXUE74sV_23XbUN.webp&quot; alt=&quot;image-20211214151515385&quot; /&gt;&lt;/p&gt;
&lt;p&gt;概念速查:&lt;/p&gt;
&lt;p&gt;Authentication接口: 它的实现类，表示当前访问系统的用户，封装了用户相关信息。&lt;/p&gt;
&lt;p&gt;AuthenticationManager接口：定义了认证Authentication的方法&lt;/p&gt;
&lt;p&gt;UserDetailsService接口：加载用户特定数据的核心接口。里面定义了一个根据用户名查询用户信息的方法。&lt;/p&gt;
&lt;p&gt;UserDetails接口：提供核心用户信息。通过UserDetailsService根据用户名获取处理的用户信息要封装成UserDetails对象返回。然后将这些信息封装到Authentication对象中。&lt;/p&gt;
&lt;h4&gt;2.3 解决问题&lt;/h4&gt;
&lt;h5&gt;2.3.1 思路分析&lt;/h5&gt;
&lt;p&gt;登录&lt;/p&gt;
&lt;p&gt;​	①自定义登录接口&lt;/p&gt;
&lt;p&gt;​				调用ProviderManager的方法进行认证 如果认证通过生成jwt&lt;/p&gt;
&lt;p&gt;​				把用户信息存入redis中&lt;/p&gt;
&lt;p&gt;​	②自定义UserDetailsService&lt;/p&gt;
&lt;p&gt;​				在这个实现类中去查询数据库&lt;/p&gt;
&lt;p&gt;校验：&lt;/p&gt;
&lt;p&gt;​	①定义Jwt认证过滤器&lt;/p&gt;
&lt;p&gt;​				获取token&lt;/p&gt;
&lt;p&gt;​				解析token获取其中的userid&lt;/p&gt;
&lt;p&gt;​				从redis中获取用户信息&lt;/p&gt;
&lt;p&gt;​				存入SecurityContextHolder&lt;/p&gt;
&lt;h5&gt;2.3.2 准备工作&lt;/h5&gt;
&lt;p&gt;①添加依赖&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;        &amp;lt;!--redis依赖--&amp;gt;
        &amp;lt;dependency&amp;gt;
            &amp;lt;groupId&amp;gt;org.springframework.boot&amp;lt;/groupId&amp;gt;
            &amp;lt;artifactId&amp;gt;spring-boot-starter-data-redis&amp;lt;/artifactId&amp;gt;
        &amp;lt;/dependency&amp;gt;
        &amp;lt;!-- redis依赖commons-pool 这个依赖一定要添加 --&amp;gt;
        &amp;lt;dependency&amp;gt;
            &amp;lt;groupId&amp;gt;org.apache.commons&amp;lt;/groupId&amp;gt;
            &amp;lt;artifactId&amp;gt;commons-pool2&amp;lt;/artifactId&amp;gt;
        &amp;lt;/dependency&amp;gt;
        &amp;lt;!--fastjson依赖--&amp;gt;
        &amp;lt;dependency&amp;gt;
            &amp;lt;groupId&amp;gt;com.alibaba&amp;lt;/groupId&amp;gt;
            &amp;lt;artifactId&amp;gt;fastjson&amp;lt;/artifactId&amp;gt;
            &amp;lt;version&amp;gt;1.2.33&amp;lt;/version&amp;gt;
        &amp;lt;/dependency&amp;gt;
        &amp;lt;!--jwt依赖--&amp;gt;
        &amp;lt;dependency&amp;gt;
            &amp;lt;groupId&amp;gt;io.jsonwebtoken&amp;lt;/groupId&amp;gt;
            &amp;lt;artifactId&amp;gt;jjwt&amp;lt;/artifactId&amp;gt;
            &amp;lt;version&amp;gt;0.9.0&amp;lt;/version&amp;gt;
        &amp;lt;/dependency&amp;gt;
        &amp;lt;!--   基于 Java 语言实现的各种 JWT 类库 主要用于验证    --&amp;gt;
        &amp;lt;dependency&amp;gt;
            &amp;lt;groupId&amp;gt;com.auth0&amp;lt;/groupId&amp;gt;
            &amp;lt;artifactId&amp;gt;java-jwt&amp;lt;/artifactId&amp;gt;
            &amp;lt;version&amp;gt;4.4.0&amp;lt;/version&amp;gt;
        &amp;lt;/dependency&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;② 添加Redis相关配置&lt;/p&gt;
&lt;h6&gt;FastJsonRedisSerializer&lt;/h6&gt;
&lt;blockquote&gt;
&lt;p&gt;注意fastjson版本为：1.2.33时生效，其他版本可能不生效&lt;/p&gt;
&lt;/blockquote&gt;
&lt;pre&gt;&lt;code&gt;
import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.serializer.SerializerFeature;
import com.fasterxml.jackson.databind.JavaType;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.type.TypeFactory;
import org.springframework.data.redis.serializer.RedisSerializer;
import org.springframework.data.redis.serializer.SerializationException;
import com.alibaba.fastjson.parser.ParserConfig;
import org.springframework.util.Assert;
import java.nio.charset.Charset;

/**
 * Redis使用FastJson序列化  注意fastjson版本为：1.2.33时生效，其他版本可能不生效
 * 
 * @author zy
 */
public class FastJsonRedisSerializer&amp;lt;T&amp;gt; implements RedisSerializer&amp;lt;T&amp;gt;
{

    public static final Charset DEFAULT_CHARSET = StandardCharsets.UTF_8;

    private Class&amp;lt;T&amp;gt; clazz;

    static
    {
        ParserConfig.getGlobalInstance().setAutoTypeSupport(true);
    }

    public FastJsonRedisSerializer(Class&amp;lt;T&amp;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, SerializerFeature.WriteClassName).getBytes(DEFAULT_CHARSET);
    }

    @Override
    public T deserialize(byte[] bytes) throws SerializationException
    {
        if (bytes == null || bytes.length &amp;lt;= 0)
        {
            return null;
        }
        String str = new String(bytes, DEFAULT_CHARSET);

        return JSON.parseObject(str, clazz);
    }


    protected JavaType getJavaType(Class&amp;lt;?&amp;gt; clazz)
    {
        return TypeFactory.defaultInstance().constructType(clazz);
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h6&gt;RedisConfig&lt;/h6&gt;
&lt;pre&gt;&lt;code&gt;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.redis.connection.RedisConnectionFactory;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.serializer.StringRedisSerializer;

@Configuration
public class RedisConfig {

    @Bean
    @SuppressWarnings(value = { &quot;unchecked&quot;, &quot;rawtypes&quot; })
    public RedisTemplate&amp;lt;Object, Object&amp;gt; redisTemplate(RedisConnectionFactory connectionFactory)
    {
        RedisTemplate&amp;lt;Object, Object&amp;gt; template = new RedisTemplate&amp;lt;&amp;gt;();
        template.setConnectionFactory(connectionFactory);

        FastJsonRedisSerializer serializer = new FastJsonRedisSerializer(Object.class);

        // 使用StringRedisSerializer来序列化和反序列化redis的key值
        template.setKeySerializer(new StringRedisSerializer());
        template.setValueSerializer(serializer);

        // Hash的key也采用StringRedisSerializer的序列化方式
        template.setHashKeySerializer(new StringRedisSerializer());
        template.setHashValueSerializer(serializer);

        template.afterPropertiesSet();
        return template;
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;③ 响应类&lt;/p&gt;
&lt;h6&gt;响应类&lt;/h6&gt;
&lt;pre&gt;&lt;code&gt;
import com.fasterxml.jackson.annotation.JsonInclude;

/**
 * @Author 三更  B站： https://space.bilibili.com/663528522
 */
@JsonInclude(JsonInclude.Include.NON_NULL)
public class ResponseResult&amp;lt;T&amp;gt; {
    /**
     * 状态码
     */
    private Integer code;
    /**
     * 提示信息，如果有错误时，前端可以获取该字段进行提示
     */
    private String msg;
    /**
     * 查询到的结果数据，
     */
    private T data;

    public ResponseResult(Integer code, String msg) {
        this.code = code;
        this.msg = msg;
    }

    public ResponseResult(Integer code, T data) {
        this.code = code;
        this.data = data;
    }

    public Integer getCode() {
        return code;
    }

    public void setCode(Integer code) {
        this.code = code;
    }

    public String getMsg() {
        return msg;
    }

    public void setMsg(String msg) {
        this.msg = msg;
    }

    public T getData() {
        return data;
    }

    public void setData(T data) {
        this.data = data;
    }

    public ResponseResult(Integer code, String msg, T data) {
        this.code = code;
        this.msg = msg;
        this.data = data;
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;④工具类&lt;/p&gt;
&lt;h6&gt;jwt工具&lt;/h6&gt;
&lt;pre&gt;&lt;code&gt;package cn.zyroot.domain.security.utils;

import com.auth0.jwt.JWT;
import com.auth0.jwt.JWTVerifier;
import com.auth0.jwt.algorithms.Algorithm;
import io.jsonwebtoken.Claims;
import io.jsonwebtoken.JwtBuilder;
import io.jsonwebtoken.Jwts;
import io.jsonwebtoken.SignatureAlgorithm;
import lombok.Data;

import javax.crypto.SecretKey;
import javax.crypto.spec.SecretKeySpec;
import java.util.*;

/**
 * JWT工具类
 */
@Data
public class JwtUtil {

    /**
     * 默认算法
     */
    private static final SignatureAlgorithm defaultSignatureAlgorithm = SignatureAlgorithm.HS256;
    // 有效期为
    public static final Long JWT_TTL = 60 * 60 * 1000L;// 60 * 60 *1000  一个小时
    // 设置秘钥明文  默认密钥
    public static final String JWT_KEY = &quot;singing&quot;;
    // 签发者
    public static final String JWT_ISSUER = &quot;zy&quot;;

    // 算法
    private SignatureAlgorithm signatureAlgorithm;

    public JwtUtil(SignatureAlgorithm signatureAlgorithm) {
        this.signatureAlgorithm = signatureAlgorithm;
    }

    /**
     * 获取uuid
     *
     * @return 唯一标识
     */
    public static String getUUID() {
        return UUID.randomUUID().toString().replaceAll(&quot;-&quot;, &quot;&quot;);
    }

    /**
     * 生成加密后的秘钥 secretKey
     *
     * @return secretKey
     */
    public static SecretKey generalKey() {
        byte[] encodedKey = Base64.getDecoder().decode(JwtUtil.JWT_KEY);
        return new SecretKeySpec(encodedKey, 0, encodedKey.length, &quot;AES&quot;);
    }

    /**
     * 获取jwt构造器
     *
     * @param subject   主题
     * @param ttlMillis 过期时间
     * @param uuid      uuid
     * @return JwtBuilder
     */
    private static JwtBuilder getJwtBuilder(String subject, Long ttlMillis, String uuid, Map&amp;lt;String, Object&amp;gt; chaim) {
        SecretKey secretKey = generalKey();
        long nowMillis = System.currentTimeMillis();
        Date now = new Date(nowMillis);
        if (ttlMillis == null) {
            ttlMillis = JwtUtil.JWT_TTL;
        }
        long expMillis = nowMillis + ttlMillis;
        Date expDate = new Date(expMillis);
        return Jwts.builder()
                .setClaims(chaim)
                // 唯一的ID
                .setId(uuid)
                // 主题  可以是JSON数据
                .setSubject(subject)
                // 签发者
                .setIssuer(JwtUtil.JWT_ISSUER)
                // 签发时间
                .setIssuedAt(now)
                // 使用HS256对称加密算法签名, 第二个参数为秘钥
                .signWith(defaultSignatureAlgorithm, secretKey)
                .setExpiration(expDate);
    }

    /**
     * 生成jtw  无过期时间
     *
     * @param subject token中要存放的数据（json格式）
     * @return jwtStr
     */
    public static String createJWT(String subject) {
        // 无过期时间
        JwtBuilder builder = getJwtBuilder(subject, null, getUUID(),null);
        return builder.compact();
    }

    /**
     * 生成jtw  指定过期时间、指定载荷
     *
     * @param subject   token中要存放的数据（json格式）
     * @param ttlMillis token超时时间
     * @return jwtStr
     */
    public static String createJWT(String subject, Long ttlMillis, Map&amp;lt;String, Object&amp;gt; chaim) {
        // 设置过期时间
        JwtBuilder builder = getJwtBuilder(subject, ttlMillis, getUUID(),chaim);
        return builder.compact();
    }


    /**
     * 创建token  指定过期时间、id(唯一标识)
     *
     * @param id        唯一标识
     * @param subject   主题
     * @param ttlMillis 过期时间
     * @return jwtStr
     */
    public static String createJWT(String subject, Long ttlMillis, String id) {
        JwtBuilder builder = getJwtBuilder(subject, ttlMillis, id,null);
        return builder.compact();
    }

    /**
     * 创建token  指定过期时间、id(唯一标识)、指定载荷
     *
     * @param id        唯一标识
     * @param subject   主题
     * @param ttlMillis 过期时间
     * @return jwtStr
     */
    public static String createJWT(String subject, Long ttlMillis, String id, Map&amp;lt;String, Object&amp;gt; chaim) {
        JwtBuilder builder = getJwtBuilder(subject, ttlMillis, id, chaim);
        return builder.compact();
    }

    /**
     * 解析
     *
     * @param jwt 字符串
     * @return Claims
     * @throws Exception 异常
     */
    public static Claims parseJWT(String jwt) throws Exception {
        SecretKey secretKey = generalKey();
        return Jwts.parser()
                .setSigningKey(secretKey)
                .parseClaimsJws(jwt)
                .getBody();
    }

    // 判断jwtToken是否合法
    public static boolean isVerify(String jwtToken) {
        // 这个是官方的校验规则，这里只写了一个”校验算法“，可以自己加
        Algorithm algorithm = null;
        switch (JwtUtil.defaultSignatureAlgorithm) {
            case HS256:
                algorithm = Algorithm.HMAC256(JwtUtil.JWT_KEY);
                break;
            default:
                throw new RuntimeException(&quot;不支持该算法&quot;);
        }
        JWTVerifier verifier = JWT.require(algorithm).build();
        verifier.verify(jwtToken);
        // 校验不通过会抛出异常
        // 判断合法的标准：1. 头部和荷载部分没有篡改过。2. 没有过期
        return true;
    }

    // 测试
    public static void main(String[] args) throws Exception {
        String jwt = createJWT(&quot;123&quot;);
        System.out.println(jwt);
        Claims claims = parseJWT(jwt);
        System.out.println(claims);
    }


}
&lt;/code&gt;&lt;/pre&gt;
&lt;h6&gt;RedisCache&lt;/h6&gt;
&lt;pre&gt;&lt;code&gt;import java.util.*;
import java.util.concurrent.TimeUnit;

@SuppressWarnings(value = { &quot;unchecked&quot;, &quot;rawtypes&quot; })
@Component
public class RedisCache
{
    @Autowired
    public RedisTemplate redisTemplate;

    /**
     * 缓存基本的对象，Integer、String、实体类等
     *
     * @param key 缓存的键值
     * @param value 缓存的值
     */
    public &amp;lt;T&amp;gt; void setCacheObject(final String key, final T value)
    {
        redisTemplate.opsForValue().set(key, value);
    }

    /**
     * 缓存基本的对象，Integer、String、实体类等
     *
     * @param key 缓存的键值
     * @param value 缓存的值
     * @param timeout 时间
     * @param timeUnit 时间颗粒度
     */
    public &amp;lt;T&amp;gt; void setCacheObject(final String key, final T value, final Integer timeout, final TimeUnit timeUnit)
    {
        redisTemplate.opsForValue().set(key, value, timeout, timeUnit);
    }

    /**
     * 设置有效时间
     *
     * @param key Redis键
     * @param timeout 超时时间
     * @return true=设置成功；false=设置失败
     */
    public boolean expire(final String key, final long timeout)
    {
        return expire(key, timeout, TimeUnit.SECONDS);
    }

    /**
     * 设置有效时间
     *
     * @param key Redis键
     * @param timeout 超时时间
     * @param unit 时间单位
     * @return true=设置成功；false=设置失败
     */
    public boolean expire(final String key, final long timeout, final TimeUnit unit)
    {
        return redisTemplate.expire(key, timeout, unit);
    }

    /**
     * 获得缓存的基本对象。
     *
     * @param key 缓存键值
     * @return 缓存键值对应的数据
     */
    public &amp;lt;T&amp;gt; T getCacheObject(final String key)
    {
        ValueOperations&amp;lt;String, T&amp;gt; operation = redisTemplate.opsForValue();
        return operation.get(key);
    }

    /**
     * 删除单个对象
     *
     * @param key
     */
    public boolean deleteObject(final String key)
    {
        return redisTemplate.delete(key);
    }

    /**
     * 删除集合对象
     *
     * @param collection 多个对象
     * @return
     */
    public long deleteObject(final Collection collection)
    {
        return redisTemplate.delete(collection);
    }

    /**
     * 缓存List数据
     *
     * @param key 缓存的键值
     * @param dataList 待缓存的List数据
     * @return 缓存的对象
     */
    public &amp;lt;T&amp;gt; long setCacheList(final String key, final List&amp;lt;T&amp;gt; dataList)
    {
        Long count = redisTemplate.opsForList().rightPushAll(key, dataList);
        return count == null ? 0 : count;
    }

    /**
     * 获得缓存的list对象
     *
     * @param key 缓存的键值
     * @return 缓存键值对应的数据
     */
    public &amp;lt;T&amp;gt; List&amp;lt;T&amp;gt; getCacheList(final String key)
    {
        return redisTemplate.opsForList().range(key, 0, -1);
    }

    /**
     * 缓存Set
     *
     * @param key 缓存键值
     * @param dataSet 缓存的数据
     * @return 缓存数据的对象
     */
    public &amp;lt;T&amp;gt; BoundSetOperations&amp;lt;String, T&amp;gt; setCacheSet(final String key, final Set&amp;lt;T&amp;gt; dataSet)
    {
        BoundSetOperations&amp;lt;String, T&amp;gt; setOperation = redisTemplate.boundSetOps(key);
        Iterator&amp;lt;T&amp;gt; it = dataSet.iterator();
        while (it.hasNext())
        {
            setOperation.add(it.next());
        }
        return setOperation;
    }

    /**
     * 获得缓存的set
     *
     * @param key
     * @return
     */
    public &amp;lt;T&amp;gt; Set&amp;lt;T&amp;gt; getCacheSet(final String key)
    {
        return redisTemplate.opsForSet().members(key);
    }

    /**
     * 缓存Map
     *
     * @param key
     * @param dataMap
     */
    public &amp;lt;T&amp;gt; void setCacheMap(final String key, final Map&amp;lt;String, T&amp;gt; dataMap)
    {
        if (dataMap != null) {
            redisTemplate.opsForHash().putAll(key, dataMap);
        }
    }

    /**
     * 获得缓存的Map
     *
     * @param key
     * @return
     */
    public &amp;lt;T&amp;gt; Map&amp;lt;String, T&amp;gt; getCacheMap(final String key)
    {
        return redisTemplate.opsForHash().entries(key);
    }

    /**
     * 往Hash中存入数据
     *
     * @param key Redis键
     * @param hKey Hash键
     * @param value 值
     */
    public &amp;lt;T&amp;gt; void setCacheMapValue(final String key, final String hKey, final T value)
    {
        redisTemplate.opsForHash().put(key, hKey, value);
    }

    /**
     * 获取Hash中的数据
     *
     * @param key Redis键
     * @param hKey Hash键
     * @return Hash中的对象
     */
    public &amp;lt;T&amp;gt; T getCacheMapValue(final String key, final String hKey)
    {
        HashOperations&amp;lt;String, String, T&amp;gt; opsForHash = redisTemplate.opsForHash();
        return opsForHash.get(key, hKey);
    }

    /**
     * 删除Hash中的数据
     * 
     * @param key
     * @param hkey
     */
    public void delCacheMapValue(final String key, final String hkey)
    {
        HashOperations hashOperations = redisTemplate.opsForHash();
        hashOperations.delete(key, hkey);
    }

    /**
     * 获取多个Hash中的数据
     *
     * @param key Redis键
     * @param hKeys Hash键集合
     * @return Hash对象集合
     */
    public &amp;lt;T&amp;gt; List&amp;lt;T&amp;gt; getMultiCacheMapValue(final String key, final Collection&amp;lt;Object&amp;gt; hKeys)
    {
        return redisTemplate.opsForHash().multiGet(key, hKeys);
    }

    /**
     * 获得缓存的基本对象列表
     *
     * @param pattern 字符串前缀
     * @return 对象列表
     */
    public Collection&amp;lt;String&amp;gt; keys(final String pattern)
    {
        return redisTemplate.keys(pattern);
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h6&gt;WebUtils&lt;/h6&gt;
&lt;pre&gt;&lt;code&gt;import javax.servlet.http.HttpServletResponse;
import java.io.IOException;

public class WebUtils
{
    /**
     * 将字符串渲染到客户端
     * 
     * @param response 渲染对象
     * @param string 待渲染的字符串
     * @return null
     */
    public static String renderString(HttpServletResponse response, String string) {
        try
        {
            response.setStatus(200);
            response.setContentType(&quot;application/json&quot;);
            response.setCharacterEncoding(&quot;utf-8&quot;);
            response.getWriter().print(string);
        }
        catch (IOException e)
        {
            e.printStackTrace();
        }
        return null;
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h6&gt;用户实体类&lt;/h6&gt;
&lt;pre&gt;&lt;code&gt;import java.io.Serializable;
import java.util.Date;


/**
 * 用户表(User)实体类
 *
 * @author 三更
 */
@Data
@AllArgsConstructor
@NoArgsConstructor
public class User implements Serializable {
    private static final long serialVersionUID = -40356785423868312L;
    
    /**
    * 主键
    */
    private Long id;
    /**
    * 用户名
    */
    private String userName;
    /**
    * 昵称
    */
    private String nickName;
    /**
    * 密码
    */
    private String password;
    /**
    * 账号状态（0正常 1停用）
    */
    private String status;
    /**
    * 邮箱
    */
    private String email;
    /**
    * 手机号
    */
    private String phonenumber;
    /**
    * 用户性别（0男，1女，2未知）
    */
    private String sex;
    /**
    * 头像
    */
    private String avatar;
    /**
    * 用户类型（0管理员，1普通用户）
    */
    private String userType;
    /**
    * 创建人的用户id
    */
    private Long createBy;
    /**
    * 创建时间
    */
    private Date createTime;
    /**
    * 更新人
    */
    private Long updateBy;
    /**
    * 更新时间
    */
    private Date updateTime;
    /**
    * 删除标志（0代表未删除，1代表已删除）
    */
    private Integer delFlag;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h6&gt;用户工具类&lt;/h6&gt;
&lt;pre&gt;&lt;code&gt;public class SecurityUtils
{

    /**
     * 获取用户
     **/
    public static LoginUser getLoginUser()
    {
        return (LoginUser) getAuthentication().getPrincipal();
    }

    /**
     * 获取Authentication
     */
    public static Authentication getAuthentication() {
        return SecurityContextHolder.getContext().getAuthentication();
    }

    public static Boolean isAdmin(){
        Long id = getLoginUser().getUser().getId();
        return id != null &amp;amp;&amp;amp; 1L == id;
    }

    public static Long getUserId() {
        return getLoginUser().getUser().getId();
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h5&gt;2.3.3 实现&lt;/h5&gt;
&lt;h6&gt;2.3.3.1 数据库校验用户&lt;/h6&gt;
&lt;p&gt;​	从之前的分析我们可以知道，我们可以自定义一个UserDetailsService,让SpringSecurity使用我们的UserDetailsService。我们自己的UserDetailsService可以从数据库中查询用户名和密码。&lt;/p&gt;
&lt;h6&gt;准备工作&lt;/h6&gt;
&lt;p&gt;​	我们先创建一个用户表， 建表语句如下：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;CREATE TABLE `sys_user` (
  `id` BIGINT(20) NOT NULL AUTO_INCREMENT COMMENT &apos;主键&apos;,
  `user_name` VARCHAR(64) NOT NULL DEFAULT &apos;NULL&apos; COMMENT &apos;用户名&apos;,
  `nick_name` VARCHAR(64) NOT NULL DEFAULT &apos;NULL&apos; COMMENT &apos;昵称&apos;,
  `password` VARCHAR(64) NOT NULL DEFAULT &apos;NULL&apos; COMMENT &apos;密码&apos;,
  `status` CHAR(1) DEFAULT &apos;0&apos; COMMENT &apos;账号状态（0正常 1停用）&apos;,
  `email` VARCHAR(64) DEFAULT NULL COMMENT &apos;邮箱&apos;,
  `phonenumber` VARCHAR(32) DEFAULT NULL COMMENT &apos;手机号&apos;,
  `sex` CHAR(1) DEFAULT NULL COMMENT &apos;用户性别（0男，1女，2未知）&apos;,
  `avatar` VARCHAR(128) DEFAULT NULL COMMENT &apos;头像&apos;,
  `user_type` CHAR(1) NOT NULL DEFAULT &apos;1&apos; COMMENT &apos;用户类型（0管理员，1普通用户）&apos;,
  `create_by` BIGINT(20) DEFAULT NULL COMMENT &apos;创建人的用户id&apos;,
  `create_time` DATETIME DEFAULT NULL COMMENT &apos;创建时间&apos;,
  `update_by` BIGINT(20) DEFAULT NULL COMMENT &apos;更新人&apos;,
  `update_time` DATETIME DEFAULT NULL COMMENT &apos;更新时间&apos;,
  `del_flag` INT(11) DEFAULT &apos;0&apos; COMMENT &apos;删除标志（0代表未删除，1代表已删除）&apos;,
  PRIMARY KEY (`id`)
) ENGINE=INNODB AUTO_INCREMENT=2 DEFAULT CHARSET=utf8mb4 COMMENT=&apos;用户表&apos;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;​		引入MybatisPuls和mysql驱动的依赖&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;        &amp;lt;dependency&amp;gt;
            &amp;lt;groupId&amp;gt;com.baomidou&amp;lt;/groupId&amp;gt;
            &amp;lt;artifactId&amp;gt;mybatis-plus-boot-starter&amp;lt;/artifactId&amp;gt;
            &amp;lt;version&amp;gt;3.4.3&amp;lt;/version&amp;gt;
        &amp;lt;/dependency&amp;gt;
        &amp;lt;dependency&amp;gt;
            &amp;lt;groupId&amp;gt;mysql&amp;lt;/groupId&amp;gt;
            &amp;lt;artifactId&amp;gt;mysql-connector-java&amp;lt;/artifactId&amp;gt;
        &amp;lt;/dependency&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;​		配置数据库信息&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;spring:
  datasource:
    url: jdbc:mysql://localhost:3306/sg_security?characterEncoding=utf-8&amp;amp;serverTimezone=UTC
    username: root
    password: root
    driver-class-name: com.mysql.cj.jdbc.Driver
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;​		定义Mapper接口&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;public interface UserMapper extends BaseMapper&amp;lt;User&amp;gt; {
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;​		修改User实体类&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;类名上加@TableName(value = &quot;sys_user&quot;) ,id字段上加 @TableId
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;​		配置Mapper扫描&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;@SpringBootApplication
@MapperScan(&quot;com.sangeng.mapper&quot;)
public class SimpleSecurityApplication {
    public static void main(String[] args) {
        ConfigurableApplicationContext run = SpringApplication.run(SimpleSecurityApplication.class);
        System.out.println(run);
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;​		添加junit依赖&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;        &amp;lt;dependency&amp;gt;
            &amp;lt;groupId&amp;gt;org.springframework.boot&amp;lt;/groupId&amp;gt;
            &amp;lt;artifactId&amp;gt;spring-boot-starter-test&amp;lt;/artifactId&amp;gt;
        &amp;lt;/dependency&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;​	   测试MP是否能正常使用&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;/**
 * @Author 三更  B站： https://space.bilibili.com/663528522
 */
@SpringBootTest
public class MapperTest {

    @Autowired
    private UserMapper userMapper;

    @Test
    public void testUserMapper(){
        List&amp;lt;User&amp;gt; users = userMapper.selectList(null);
        System.out.println(users);
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h6&gt;核心代码实现&lt;/h6&gt;
&lt;p&gt;创建一个类实现UserDetailsService接口，重写其中的方法。更加用户名从数据库中查询用户信息&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;/**
 * @Author 三更  B站： https://space.bilibili.com/663528522
 */
@Service
public class UserDetailsServiceImpl implements UserDetailsService {

    @Autowired
    private UserMapper userMapper;

    @Override
    public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
        //根据用户名查询用户信息
        LambdaQueryWrapper&amp;lt;User&amp;gt; wrapper = new LambdaQueryWrapper&amp;lt;&amp;gt;();
        wrapper.eq(User::getUserName,username);
        User user = userMapper.selectOne(wrapper);
        //如果查询不到数据就通过抛出异常来给出提示
        if(Objects.isNull(user)){
            throw new RuntimeException(&quot;用户名或密码错误&quot;);
        }
        //TODO 根据用户查询权限信息 添加到LoginUser中
        
        //封装成UserDetails对象返回 
        //往后走，security内部逻辑在做秘密验证
        return new LoginUser(user);
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;因为UserDetailsService方法的返回值是UserDetails类型，所以需要定义一个类，实现该接口，把用户信息封装在其中。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;LoginUser&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;/**
 * @Author 三更  B站： https://space.bilibili.com/663528522
 */
@Data
@NoArgsConstructor
@AllArgsConstructor
public class LoginUser implements UserDetails {

    private User user;


    @Override
    public Collection&amp;lt;? extends GrantedAuthority&amp;gt; getAuthorities() {
        return null;
    }

    @Override
    public String getPassword() {
        return user.getPassword();
    }

    @Override
    public String getUsername() {
        return user.getUserName();
    }

    @Override
    public boolean isAccountNonExpired() {
        return true;
    }

    @Override
    public boolean isAccountNonLocked() {
        return true;
    }

    @Override
    public boolean isCredentialsNonExpired() {
        return true;
    }

    @Override
    public boolean isEnabled() {
        return true;
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;注意：如果要测试，需要往用户表中写入用户数据，并且如果你想让用户的密码是明文存储，需要在密码前加{noop}。例如&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://tangtangit.com/_astro/image-20211216123945882.jZjzF0Ml_qsbSs.webp&quot; alt=&quot;image-20211216123945882&quot; /&gt;&lt;/p&gt;
&lt;p&gt;这样登陆的时候就可以用sg作为用户名，1234作为密码来登陆了。&lt;/p&gt;
&lt;h6&gt;2.3.3.2 密码加密存储&lt;/h6&gt;
&lt;p&gt;​	实际项目中我们不会把密码明文存储在数据库中。&lt;/p&gt;
&lt;p&gt;​	默认使用的PasswordEncoder要求数据库中的密码格式为：{id}password 。它会根据id去判断密码的加密方式。但是我们一般不会采用这种方式。所以就需要替换PasswordEncoder。&lt;/p&gt;
&lt;p&gt;​	我们一般使用SpringSecurity为我们提供的BCryptPasswordEncoder。&lt;/p&gt;
&lt;p&gt;​	我们只需要使用把BCryptPasswordEncoder对象注入Spring容器中，SpringSecurity就会使用该PasswordEncoder来进行密码校验。&lt;/p&gt;
&lt;p&gt;​	我们可以定义一个SpringSecurity的配置类，SpringSecurity要求这个配置类要继承WebSecurityConfigurerAdapter。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;/**
 * @Author 三更  B站： https://space.bilibili.com/663528522
 */
@Configuration
public class SecurityConfig extends WebSecurityConfigurerAdapter {


    @Bean
    public PasswordEncoder passwordEncoder(){
        return new BCryptPasswordEncoder();
    }

}
&lt;/code&gt;&lt;/pre&gt;
&lt;h6&gt;2.3.3.3 登陆接口&lt;/h6&gt;
&lt;p&gt;​	接下我们需要自定义登陆接口，然后让SpringSecurity对这个接口放行,让用户访问这个接口的时候不用登录也能访问。&lt;/p&gt;
&lt;p&gt;​	在接口中我们通过AuthenticationManager的authenticate方法来进行用户认证,所以需要在SecurityConfig中配置把AuthenticationManager注入容器。&lt;/p&gt;
&lt;p&gt;​	认证成功的话要生成一个jwt，放入响应中返回。并且为了让用户下回请求时能通过jwt识别出具体的是哪个用户，我们需要把用户信息存入redis，可以把用户id作为key。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;@RestController
public class LoginController {

    @Autowired
    private LoginServcie loginServcie;

    @PostMapping(&quot;/user/login&quot;)
    public ResponseResult login(@RequestBody User user){
        return loginServcie.login(user);
    }
}

&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;/**
 * @Author 三更  B站： https://space.bilibili.com/663528522
 */
@Configuration
public class SecurityConfig extends WebSecurityConfigurerAdapter {


    @Bean
    public PasswordEncoder passwordEncoder(){
        return new BCryptPasswordEncoder();
    }

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http
                //关闭csrf
                .csrf().disable()
                //不通过Session获取SecurityContext
                .sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS)
                .and()
                .authorizeRequests()
                // 对于登录接口 允许匿名访问  必须以斜杠开头  不包含context-path路径
                .antMatchers(&quot;/user/login&quot;).anonymous()
                // 除上面外的所有请求全部需要鉴权认证
                .anyRequest().authenticated();
    }

    @Bean
    @Override
    public AuthenticationManager authenticationManagerBean() throws Exception {
        return super.authenticationManagerBean();
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;​&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;@Service
public class LoginServiceImpl implements LoginServcie {

    @Autowired
    private AuthenticationManager authenticationManager;
    @Autowired
    private RedisCache redisCache;

    @Override
    public ResponseResult login(User user) {
        UsernamePasswordAuthenticationToken authenticationToken = new UsernamePasswordAuthenticationToken(user.getUserName(),user.getPassword());
        Authentication authenticate = authenticationManager.authenticate(authenticationToken);
        if(Objects.isNull(authenticate)){
            throw new RuntimeException(&quot;用户名或密码错误&quot;);
        }
        //使用userid生成token
        LoginUser loginUser = (LoginUser) authenticate.getPrincipal();
        String userId = loginUser.getUser().getId().toString();
        String jwt = JwtUtil.createJWT(userId);
        //authenticate存入redis
        redisCache.setCacheObject(&quot;login:&quot;+userId,loginUser);
        //把token响应给前端
        HashMap&amp;lt;String,String&amp;gt; map = new HashMap&amp;lt;&amp;gt;();
        map.put(&quot;token&quot;,jwt);
        return new ResponseResult(200,&quot;登陆成功&quot;,map);
    }
}

&lt;/code&gt;&lt;/pre&gt;
&lt;h6&gt;2.3.3.4 认证过滤器&lt;/h6&gt;
&lt;p&gt;​	我们需要自定义一个过滤器，这个过滤器会去获取请求头中的token，对token进行解析取出其中的userid。&lt;/p&gt;
&lt;p&gt;​	使用userid去redis中获取对应的LoginUser对象。&lt;/p&gt;
&lt;p&gt;​	然后封装Authentication对象存入SecurityContextHolder&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;@Component
public class JwtAuthenticationTokenFilter extends OncePerRequestFilter implements HandlerInterceptor {

    @Autowired
    private RedisCache redisCache;
    
    //拦截器的前置处理
    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
        //如果是预检请求，手动加上请求状态200
        if (request.getMethod().equals(RequestMethod.OPTIONS.name())) {
            response.setStatus(HttpStatus.OK.value());
            return false;
        }
        return true;
    }

    //过滤器的内部处理
    @Override
    protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
        // 从header中获取token
        String token = request.getHeader(&quot;token&quot;);
        // 从请求参数中获取token
        String[] values = request.getParameterValues(&quot;token&quot;);
        if (!Objects.isNull(values)) {
            token = values[0];
        }
        if (!StringUtils.hasText(token)) {
            //放行
            filterChain.doFilter(request, response);
            return;
        }
        //解析token
        String userid;
        try {
            Claims claims = JwtUtil.parseJWT(token);
            userid = claims.getSubject();
        } catch (Exception e) {
            e.printStackTrace();
            throw new RuntimeException(&quot;token非法&quot;);
        }
        //从redis中获取用户信息
        String redisKey = &quot;login:&quot; + userid;
        LoginUser loginUser = redisCache.getCacheObject(redisKey);
        if(Objects.isNull(loginUser)){
            throw new RuntimeException(&quot;用户未登录&quot;);
        }
        //存入SecurityContextHolder
        //TODO 获取权限信息封装到Authentication中
        UsernamePasswordAuthenticationToken authenticationToken =
                new UsernamePasswordAuthenticationToken(loginUser,null,null);
        SecurityContextHolder.getContext().setAuthentication(authenticationToken);
        //放行
        filterChain.doFilter(request, response);
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;/**
 * @Author 三更  B站： https://space.bilibili.com/663528522
 */
@Configuration
public class SecurityConfig extends WebSecurityConfigurerAdapter {


    @Bean
    public PasswordEncoder passwordEncoder(){
        return new BCryptPasswordEncoder();
    }


    @Autowired
    JwtAuthenticationTokenFilter jwtAuthenticationTokenFilter;

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http
                //关闭csrf
                .csrf().disable()
                //不通过Session获取SecurityContext
                .sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS)
                .and()
                .authorizeRequests()
                // 对于登录接口 允许匿名访问  不包含context-path路径
                .antMatchers(&quot;/user/login&quot;).anonymous()
                // 除上面外的所有请求全部需要鉴权认证
                .anyRequest().authenticated();

        //把token校验过滤器添加到过滤器链中
        http.addFilterBefore(jwtAuthenticationTokenFilter, UsernamePasswordAuthenticationFilter.class);
    }

    @Bean
    @Override
    public AuthenticationManager authenticationManagerBean() throws Exception {
        return super.authenticationManagerBean();
    }
}

&lt;/code&gt;&lt;/pre&gt;
&lt;h6&gt;2.3.3.5 退出登陆&lt;/h6&gt;
&lt;p&gt;​	我们只需要定义一个登陆接口，然后获取SecurityContextHolder中的认证信息，删除redis中对应的数据即可。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;/**
 * @Author 三更  B站： https://space.bilibili.com/663528522
 */
@Service
public class LoginServiceImpl implements LoginServcie {

    @Autowired
    private AuthenticationManager authenticationManager;
    @Autowired
    private RedisCache redisCache;

    @Override
    public ResponseResult login(User user) {
        UsernamePasswordAuthenticationToken authenticationToken = new UsernamePasswordAuthenticationToken(user.getUserName(),user.getPassword());
        Authentication authenticate = authenticationManager.authenticate(authenticationToken);
        if(Objects.isNull(authenticate)){
            throw new RuntimeException(&quot;用户名或密码错误&quot;);
        }
        //使用userid生成token
        LoginUser loginUser = (LoginUser) authenticate.getPrincipal();
        String userId = loginUser.getUser().getId().toString();
        String jwt = JwtUtil.createJWT(userId);
        //authenticate存入redis
        redisCache.setCacheObject(&quot;login:&quot;+userId,loginUser);
        //把token响应给前端
        HashMap&amp;lt;String,String&amp;gt; map = new HashMap&amp;lt;&amp;gt;();
        map.put(&quot;token&quot;,jwt);
        return new ResponseResult(200,&quot;登陆成功&quot;,map);
    }

    @Override
    public ResponseResult logout() {
        Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
        LoginUser loginUser = (LoginUser) authentication.getPrincipal();
        Long userid = loginUser.getUser().getId();
        redisCache.deleteObject(&quot;login:&quot;+userid);
        return new ResponseResult(200,&quot;退出成功&quot;);
    }
}

&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;3. 授权&lt;/h3&gt;
&lt;h4&gt;3.0 权限系统的作用&lt;/h4&gt;
&lt;p&gt;​	例如一个学校图书馆的管理系统，如果是普通学生登录就能看到借书还书相关的功能，不可能让他看到并且去使用添加书籍信息，删除书籍信息等功能。但是如果是一个图书馆管理员的账号登录了，应该就能看到并使用添加书籍信息，删除书籍信息等功能。&lt;/p&gt;
&lt;p&gt;​	总结起来就是&lt;strong&gt;不同的用户可以使用不同的功能&lt;/strong&gt;。这就是权限系统要去实现的效果。&lt;/p&gt;
&lt;p&gt;​	我们不能只依赖前端去判断用户的权限来选择显示哪些菜单哪些按钮。因为如果只是这样，如果有人知道了对应功能的接口地址就可以不通过前端，直接去发送请求来实现相关功能操作。&lt;/p&gt;
&lt;p&gt;​	所以我们还需要在后台进行用户权限的判断，判断当前用户是否有相应的权限，必须具有所需权限才能进行相应的操作。&lt;/p&gt;
&lt;p&gt;​&lt;/p&gt;
&lt;h4&gt;3.1 授权基本流程&lt;/h4&gt;
&lt;p&gt;​	在SpringSecurity中，会使用默认的FilterSecurityInterceptor来进行权限校验。在FilterSecurityInterceptor中会从SecurityContextHolder获取其中的Authentication，然后获取其中的权限信息。当前用户是否拥有访问当前资源所需的权限。&lt;/p&gt;
&lt;p&gt;​	所以我们在项目中只需要把当前登录用户的权限信息也存入Authentication。&lt;/p&gt;
&lt;p&gt;​	然后设置我们的资源所需要的权限即可。&lt;/p&gt;
&lt;h4&gt;3.2 授权实现&lt;/h4&gt;
&lt;h5&gt;3.2.1 限制访问资源所需权限&lt;/h5&gt;
&lt;p&gt;​	SpringSecurity为我们提供了基于注解的权限控制方案，这也是我们项目中主要采用的方式。我们可以使用注解去指定访问对应的资源所需的权限。&lt;/p&gt;
&lt;p&gt;​	但是要使用它我们需要先开启相关配置。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;@EnableGlobalMethodSecurity(prePostEnabled = true)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;​	然后就可以使用对应的注解。@PreAuthorize&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;@RestController
public class HelloController {

    @RequestMapping(&quot;/hello&quot;)
    @PreAuthorize(&quot;hasAuthority(&apos;test&apos;)&quot;)
    public String hello(){
        return &quot;hello&quot;;
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h5&gt;3.2.2 封装权限信息&lt;/h5&gt;
&lt;p&gt;​	我们前面在写UserDetailsServiceImpl的时候说过，在查询出用户后还要获取对应的权限信息，封装到UserDetails中返回。&lt;/p&gt;
&lt;p&gt;​	我们先直接把权限信息写死封装到UserDetails中进行测试。&lt;/p&gt;
&lt;p&gt;​	我们之前定义了UserDetails的实现类LoginUser，想要让其能封装权限信息就要对其进行修改。&lt;/p&gt;
&lt;h6&gt;LoginUser&lt;/h6&gt;
&lt;pre&gt;&lt;code&gt;package com.sangeng.domain;

import com.alibaba.fastjson.annotation.JSONField;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.security.core.userdetails.UserDetails;

import java.util.Collection;
import java.util.List;
import java.util.stream.Collectors;

/**
 * @Author 三更  B站： https://space.bilibili.com/663528522
 */
@Data
@NoArgsConstructor
public class LoginUser implements UserDetails {

    private User user;
        
    //存储权限信息
    private List&amp;lt;String&amp;gt; permissions;
    
    
    public LoginUser(User user,List&amp;lt;String&amp;gt; permissions) {
        this.user = user;
        this.permissions = permissions;
    }

   /**
     * 该属性为了提高效率
     * 不能参与序列化
     */
    //存储SpringSecurity所需要的权限信息的集合
    @JSONField(serialize = false)
    private List&amp;lt;GrantedAuthority&amp;gt; authorities;

    @Override
    public  Collection&amp;lt;? extends GrantedAuthority&amp;gt; getAuthorities() {
        if(authorities!=null){
            return authorities;
        }
        //把permissions中字符串类型的权限信息转换成GrantedAuthority对象存入authorities中
        authorities = permissions.stream().
                map(SimpleGrantedAuthority::new)
                .collect(Collectors.toList());
        return authorities;
    }

    @Override
    public String getPassword() {
        return user.getPassword();
    }

    @Override
    public String getUsername() {
        return user.getUserName();
    }

    @Override
    public boolean isAccountNonExpired() {
        return true;
    }

    @Override
    public boolean isAccountNonLocked() {
        return true;
    }

    @Override
    public boolean isCredentialsNonExpired() {
        return true;
    }

    @Override
    public boolean isEnabled() {
        return true;
    }
}

&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;​		LoginUser修改完后我们就可以在UserDetailsServiceImpl中去把权限信息封装到LoginUser中了。我们写死权限进行测试，后面我们再从数据库中查询权限信息。&lt;/p&gt;
&lt;h6&gt;UserDetailsServiceImpl&lt;/h6&gt;
&lt;pre&gt;&lt;code&gt;package com.sangeng.service.impl;

import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import com.baomidou.mybatisplus.extension.conditions.query.LambdaQueryChainWrapper;
import com.sangeng.domain.LoginUser;
import com.sangeng.domain.User;
import com.sangeng.mapper.UserMapper;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.core.userdetails.UsernameNotFoundException;
import org.springframework.stereotype.Service;

import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.Objects;

/**
 * @Author 三更  B站： https://space.bilibili.com/663528522
 */
@Service
public class UserDetailsServiceImpl implements UserDetailsService {

    @Autowired
    private UserMapper userMapper;

    @Override
    public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
        LambdaQueryWrapper&amp;lt;User&amp;gt; wrapper = new LambdaQueryWrapper&amp;lt;&amp;gt;();
        wrapper.eq(User::getUserName,username);
        User user = userMapper.selectOne(wrapper);
        if(Objects.isNull(user)){
            throw new RuntimeException(&quot;用户名或密码错误&quot;);
        }
        //TODO 根据用户查询权限信息 添加到LoginUser中
        List&amp;lt;String&amp;gt; list = new ArrayList&amp;lt;&amp;gt;(Arrays.asList(&quot;test&quot;));
        return new LoginUser(user,list);
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h5&gt;3.2.3 从数据库查询权限信息&lt;/h5&gt;
&lt;h6&gt;3.2.3.1 RBAC权限模型&lt;/h6&gt;
&lt;p&gt;​	RBAC权限模型（Role-Based Access Control）即：基于角色的权限控制。这是目前最常被开发者使用也是相对易用、通用权限模型。&lt;/p&gt;
&lt;p&gt;​	&lt;img src=&quot;https://tangtangit.com/_astro/image-20211222110249727.BMjb0UM2_187xbb.webp&quot; alt=&quot;image-20211222110249727&quot; /&gt;&lt;/p&gt;
&lt;h6&gt;3.2.3.2 准备工作&lt;/h6&gt;
&lt;pre&gt;&lt;code&gt;
CREATE DATABASE /*!32312 IF NOT EXISTS*/`sg_security` /*!40100 DEFAULT CHARACTER SET utf8mb4 */;

USE `sg_security`;

/*Table structure for table `sys_menu` */

DROP TABLE IF EXISTS `sys_menu`;

CREATE TABLE `sys_menu` (
  `id` bigint(20) NOT NULL AUTO_INCREMENT,
  `menu_name` varchar(64) NOT NULL DEFAULT &apos;NULL&apos; COMMENT &apos;菜单名&apos;,
  `path` varchar(200) DEFAULT NULL COMMENT &apos;路由地址&apos;,
  `component` varchar(255) DEFAULT NULL COMMENT &apos;组件路径&apos;,
  `visible` char(1) DEFAULT &apos;0&apos; COMMENT &apos;菜单状态（0显示 1隐藏）&apos;,
  `status` char(1) DEFAULT &apos;0&apos; COMMENT &apos;菜单状态（0正常 1停用）&apos;,
  `perms` varchar(100) DEFAULT NULL COMMENT &apos;权限标识&apos;,
  `icon` varchar(100) DEFAULT &apos;#&apos; COMMENT &apos;菜单图标&apos;,
  `create_by` bigint(20) DEFAULT NULL,
  `create_time` datetime DEFAULT NULL,
  `update_by` bigint(20) DEFAULT NULL,
  `update_time` datetime DEFAULT NULL,
  `del_flag` int(11) DEFAULT &apos;0&apos; COMMENT &apos;是否删除（0未删除 1已删除）&apos;,
  `remark` varchar(500) DEFAULT NULL COMMENT &apos;备注&apos;,
  PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=2 DEFAULT CHARSET=utf8mb4 COMMENT=&apos;菜单表&apos;;

/*Table structure for table `sys_role` */

DROP TABLE IF EXISTS `sys_role`;

CREATE TABLE `sys_role` (
  `id` bigint(20) NOT NULL AUTO_INCREMENT,
  `name` varchar(128) DEFAULT NULL,
  `role_key` varchar(100) DEFAULT NULL COMMENT &apos;角色权限字符串&apos;,
  `status` char(1) DEFAULT &apos;0&apos; COMMENT &apos;角色状态（0正常 1停用）&apos;,
  `del_flag` int(1) DEFAULT &apos;0&apos; COMMENT &apos;del_flag&apos;,
  `create_by` bigint(200) DEFAULT NULL,
  `create_time` datetime DEFAULT NULL,
  `update_by` bigint(200) DEFAULT NULL,
  `update_time` datetime DEFAULT NULL,
  `remark` varchar(500) DEFAULT NULL COMMENT &apos;备注&apos;,
  PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=3 DEFAULT CHARSET=utf8mb4 COMMENT=&apos;角色表&apos;;

/*Table structure for table `sys_role_menu` */

DROP TABLE IF EXISTS `sys_role_menu`;

CREATE TABLE `sys_role_menu` (
  `role_id` bigint(200) NOT NULL AUTO_INCREMENT COMMENT &apos;角色ID&apos;,
  `menu_id` bigint(200) NOT NULL DEFAULT &apos;0&apos; COMMENT &apos;菜单id&apos;,
  PRIMARY KEY (`role_id`,`menu_id`)
) ENGINE=InnoDB AUTO_INCREMENT=2 DEFAULT CHARSET=utf8mb4;

/*Table structure for table `sys_user` */

DROP TABLE IF EXISTS `sys_user`;

CREATE TABLE `sys_user` (
  `id` bigint(20) NOT NULL AUTO_INCREMENT COMMENT &apos;主键&apos;,
  `user_name` varchar(64) NOT NULL DEFAULT &apos;NULL&apos; COMMENT &apos;用户名&apos;,
  `nick_name` varchar(64) NOT NULL DEFAULT &apos;NULL&apos; COMMENT &apos;昵称&apos;,
  `password` varchar(64) NOT NULL DEFAULT &apos;NULL&apos; COMMENT &apos;密码&apos;,
  `status` char(1) DEFAULT &apos;0&apos; COMMENT &apos;账号状态（0正常 1停用）&apos;,
  `email` varchar(64) DEFAULT NULL COMMENT &apos;邮箱&apos;,
  `phonenumber` varchar(32) DEFAULT NULL COMMENT &apos;手机号&apos;,
  `sex` char(1) DEFAULT NULL COMMENT &apos;用户性别（0男，1女，2未知）&apos;,
  `avatar` varchar(128) DEFAULT NULL COMMENT &apos;头像&apos;,
  `user_type` char(1) NOT NULL DEFAULT &apos;1&apos; COMMENT &apos;用户类型（0管理员，1普通用户）&apos;,
  `create_by` bigint(20) DEFAULT NULL COMMENT &apos;创建人的用户id&apos;,
  `create_time` datetime DEFAULT NULL COMMENT &apos;创建时间&apos;,
  `update_by` bigint(20) DEFAULT NULL COMMENT &apos;更新人&apos;,
  `update_time` datetime DEFAULT NULL COMMENT &apos;更新时间&apos;,
  `del_flag` int(11) DEFAULT &apos;0&apos; COMMENT &apos;删除标志（0代表未删除，1代表已删除）&apos;,
  PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=3 DEFAULT CHARSET=utf8mb4 COMMENT=&apos;用户表&apos;;

/*Table structure for table `sys_user_role` */

DROP TABLE IF EXISTS `sys_user_role`;

CREATE TABLE `sys_user_role` (
  `user_id` bigint(200) NOT NULL AUTO_INCREMENT COMMENT &apos;用户id&apos;,
  `role_id` bigint(200) NOT NULL DEFAULT &apos;0&apos; COMMENT &apos;角色id&apos;,
  PRIMARY KEY (`user_id`,`role_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;

&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;SELECT 
	DISTINCT m.`perms`
FROM
	sys_user_role ur
	LEFT JOIN `sys_role` r ON ur.`role_id` = r.`id`
	LEFT JOIN `sys_role_menu` rm ON ur.`role_id` = rm.`role_id`
	LEFT JOIN `sys_menu` m ON m.`id` = rm.`menu_id`
WHERE
	user_id = 2
	AND r.`status` = 0
	AND m.`status` = 0
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;package com.sangeng.domain;

import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableName;
import com.fasterxml.jackson.annotation.JsonInclude;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;

import java.io.Serializable;
import java.util.Date;

/**
 * 菜单表(Menu)实体类
 *
 * @author makejava
 * @since 2021-11-24 15:30:08
 */
@TableName(value=&quot;sys_menu&quot;)
@Data
@AllArgsConstructor
@NoArgsConstructor
@JsonInclude(JsonInclude.Include.NON_NULL)
public class Menu implements Serializable {
    private static final long serialVersionUID = -54979041104113736L;
    
        @TableId
    private Long id;
    /**
    * 菜单名
    */
    private String menuName;
    /**
    * 路由地址
    */
    private String path;
    /**
    * 组件路径
    */
    private String component;
    /**
    * 菜单状态（0显示 1隐藏）
    */
    private String visible;
    /**
    * 菜单状态（0正常 1停用）
    */
    private String status;
    /**
    * 权限标识
    */
    private String perms;
    /**
    * 菜单图标
    */
    private String icon;
    
    private Long createBy;
    
    private Date createTime;
    
    private Long updateBy;
    
    private Date updateTime;
    /**
    * 是否删除（0未删除 1已删除）
    */
    private Integer delFlag;
    /**
    * 备注
    */
    private String remark;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h6&gt;3.2.3.3 代码实现&lt;/h6&gt;
&lt;p&gt;​	我们只需要根据用户id去查询到其所对应的权限信息即可。&lt;/p&gt;
&lt;p&gt;​	所以我们可以先定义个mapper，其中提供一个方法可以根据userid查询权限信息。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.sangeng.domain.Menu;

import java.util.List;

/**
 * @Author 三更  B站： https://space.bilibili.com/663528522
 */
public interface MenuMapper extends BaseMapper&amp;lt;Menu&amp;gt; {
    List&amp;lt;String&amp;gt; selectPermsByUserId(Long id);
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;​	尤其是自定义方法，所以需要创建对应的mapper文件，定义对应的sql语句&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&amp;lt;?xml version=&quot;1.0&quot; encoding=&quot;UTF-8&quot; ?&amp;gt;
&amp;lt;!DOCTYPE mapper PUBLIC &quot;-//mybatis.org//DTD Mapper 3.0//EN&quot; &quot;http://mybatis.org/dtd/mybatis-3-mapper.dtd&quot; &amp;gt;
&amp;lt;mapper namespace=&quot;com.sangeng.mapper.MenuMapper&quot;&amp;gt;


    &amp;lt;select id=&quot;selectPermsByUserId&quot; resultType=&quot;java.lang.String&quot;&amp;gt;
        SELECT
            DISTINCT m.`perms`
        FROM
            sys_user_role ur
            LEFT JOIN `sys_role` r ON ur.`role_id` = r.`id`
            LEFT JOIN `sys_role_menu` rm ON ur.`role_id` = rm.`role_id`
            LEFT JOIN `sys_menu` m ON m.`id` = rm.`menu_id`
        WHERE
            user_id = #{userid}
            AND r.`status` = 0
            AND m.`status` = 0
    &amp;lt;/select&amp;gt;
&amp;lt;/mapper&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;​	在application.yml中配置mapperXML文件的位置&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;spring:
  datasource:
    url: jdbc:mysql://localhost:3306/sg_security?characterEncoding=utf-8&amp;amp;serverTimezone=UTC
    username: root
    password: root
    driver-class-name: com.mysql.cj.jdbc.Driver
  redis:
    host: localhost
    port: 6379
mybatis-plus:
  mapper-locations: classpath*:/mapper/**/*.xml 

&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;​	然后我们可以在UserDetailsServiceImpl中去调用该mapper的方法查询权限信息封装到LoginUser对象中即可。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;/**
 * @Author 三更  B站： https://space.bilibili.com/663528522
 */
@Service
public class UserDetailsServiceImpl implements UserDetailsService {

    @Autowired
    private UserMapper userMapper;

    @Autowired
    private MenuMapper menuMapper;

    @Override
    public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
        LambdaQueryWrapper&amp;lt;User&amp;gt; wrapper = new LambdaQueryWrapper&amp;lt;&amp;gt;();
        wrapper.eq(User::getUserName,username);
        User user = userMapper.selectOne(wrapper);
        if(Objects.isNull(user)){
            throw new RuntimeException(&quot;用户名或密码错误&quot;);
        }
        List&amp;lt;String&amp;gt; permissionKeyList =  menuMapper.selectPermsByUserId(user.getId());
//        //测试写法
//        List&amp;lt;String&amp;gt; list = new ArrayList&amp;lt;&amp;gt;(Arrays.asList(&quot;test&quot;));
        return new LoginUser(user,permissionKeyList);
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;4. 自定义失败处理&lt;/h3&gt;
&lt;p&gt;​	我们还希望在认证失败或者是授权失败的情况下也能和我们的接口一样返回相同结构的json，这样可以让前端能对响应进行统一的处理。要实现这个功能我们需要知道SpringSecurity的异常处理机制。&lt;/p&gt;
&lt;p&gt;​	在SpringSecurity中，如果我们在认证或者授权的过程中出现了异常会被ExceptionTranslationFilter捕获到。在ExceptionTranslationFilter中会去判断是认证失败还是授权失败出现的异常。&lt;/p&gt;
&lt;p&gt;​	如果是认证过程中出现的异常会被封装成AuthenticationException然后调用&lt;strong&gt;AuthenticationEntryPoint&lt;/strong&gt;对象的方法去进行异常处理。&lt;/p&gt;
&lt;p&gt;​	如果是授权过程中出现的异常会被封装成AccessDeniedException然后调用&lt;strong&gt;AccessDeniedHandler&lt;/strong&gt;对象的方法去进行异常处理。&lt;/p&gt;
&lt;p&gt;​	所以如果我们需要自定义异常处理，我们只需要自定义AuthenticationEntryPoint和AccessDeniedHandler然后配置给SpringSecurity即可。&lt;/p&gt;
&lt;h4&gt;①自定义实现类&lt;/h4&gt;
&lt;h5&gt;认证过程中出现的异常&lt;/h5&gt;
&lt;pre&gt;&lt;code&gt;package com.zy.security01.handler;

import com.alibaba.fastjson.JSON;
import com.zy.security01.utils.ResponseResult;
import com.zy.security01.utils.WebUtils;
import org.springframework.http.HttpStatus;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.web.AuthenticationEntryPoint;
import org.springframework.stereotype.Component;

import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;

@Component
public class AuthenticationEntryPointHandler implements AuthenticationEntryPoint {

    @Override
    public void commence(HttpServletRequest request, HttpServletResponse response, AuthenticationException authException) throws IOException, ServletException {
        ResponseResult&amp;lt;Object&amp;gt; result = new ResponseResult&amp;lt;&amp;gt;(HttpStatus.UNAUTHORIZED.value(), &quot;用户还未登录&quot;);
        WebUtils.renderString(response, JSON.toJSONString(result));
    }
    
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h5&gt;授权过程中出现的异常&lt;/h5&gt;
&lt;pre&gt;&lt;code&gt;package com.zy.security01.handler;

import com.alibaba.fastjson.JSON;
import com.zy.security01.utils.ResponseResult;
import com.zy.security01.utils.WebUtils;
import org.springframework.http.HttpStatus;
import org.springframework.security.access.AccessDeniedException;
import org.springframework.security.web.access.AccessDeniedHandler;
import org.springframework.stereotype.Component;

import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;

@Component
public class MyAccessDeniedHandler implements AccessDeniedHandler {
    @Override
    public void handle(HttpServletRequest request, HttpServletResponse response, AccessDeniedException accessDeniedException) throws IOException, ServletException {
        ResponseResult&amp;lt;Object&amp;gt; result = new ResponseResult&amp;lt;&amp;gt;(HttpStatus.FORBIDDEN.value(), &quot;用户没有权限&quot;);
        WebUtils.renderString(response, JSON.toJSONString(result));
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h4&gt;②配置给SpringSecurity&lt;/h4&gt;
&lt;p&gt;​	我们可以使用HttpSecurity对象的方法去配置。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;package com.zy.security01.config;

import com.zy.security01.handler.AuthenticationEntryPointHandler;
import com.zy.security01.handler.MyAccessDeniedHandler;
import com.zy.security01.security.JwtAuthenticationTokenFilter;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.config.annotation.authentication.configuration.AuthenticationConfiguration;
import org.springframework.security.config.annotation.method.configuration.EnableGlobalMethodSecurity;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.http.SessionCreationPolicy;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.web.SecurityFilterChain;
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;

import javax.annotation.Resource;

@Configuration
@EnableGlobalMethodSecurity(prePostEnabled = true)
public class SecurityConfig {

    /**
     * 认证配置类（security自带）
     */
    @Resource
    private AuthenticationConfiguration authenticationConfiguration;

    /**
     * 自定义jwttoken过滤器
     */
    @Resource
    private JwtAuthenticationTokenFilter jwtAuthenticationTokenFilter;

    @Resource
    private AuthenticationEntryPointHandler authenticationEntryPointHandler;

    @Resource
    private MyAccessDeniedHandler myAccessDeniedHandler;

    /**
     * 加密解密
     * @return PasswordEncoder
     */
    @Bean
    public PasswordEncoder passwordEncoder() {
        return new BCryptPasswordEncoder();
    }

    /**
     * 认证管理器
     * @return AuthenticationManager
     * @throws Exception 异常
     */
    @Bean
    public AuthenticationManager authenticationManager() throws Exception {
        return authenticationConfiguration.getAuthenticationManager();
    }

    /**
     * 过滤器链
     * @param http HttpSecurity
     * @return SecurityFilterChain
     * @throws Exception 异常
     */
    @Bean
    public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
        return http.csrf().disable()
                .sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS)
                .and()
                .authorizeRequests()
                .antMatchers(&quot;/sysUser/user/login&quot;).anonymous()
                .anyRequest().authenticated()
                .and()
                .addFilterBefore(jwtAuthenticationTokenFilter, UsernamePasswordAuthenticationFilter.class)
                //授权异常
                .exceptionHandling().authenticationEntryPoint(authenticationEntryPointHandler)
                //访问异常
                .accessDeniedHandler(myAccessDeniedHandler)
                .and()
                .build();
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;5. 跨域&lt;/h3&gt;
&lt;p&gt;​	浏览器出于安全的考虑，使用 XMLHttpRequest对象发起 HTTP请求时必须遵守同源策略，否则就是跨域的HTTP请求，默认情况下是被禁止的。 同源策略要求源相同才能正常进行通信，即协议、域名、端口号都完全一致。&lt;/p&gt;
&lt;p&gt;​	前后端分离项目，前端项目和后端项目一般都不是同源的，所以肯定会存在跨域请求的问题。&lt;/p&gt;
&lt;p&gt;​	所以我们就要处理一下，让前端能进行跨域请求。&lt;/p&gt;
&lt;h4&gt;①先对SpringBoot配置，运行跨域请求&lt;/h4&gt;
&lt;pre&gt;&lt;code&gt;@Configuration
public class CorsConfig implements WebMvcConfigurer {

    @Override
    public void addCorsMappings(CorsRegistry registry) {
      // 设置允许跨域的路径
        registry.addMapping(&quot;/**&quot;)
                // 设置允许跨域请求的域名
                .allowedOriginPatterns(&quot;*&quot;)
                // 是否允许cookie
                .allowCredentials(true)
                // 设置允许的请求方式
                .allowedMethods(&quot;GET&quot;, &quot;POST&quot;, &quot;DELETE&quot;, &quot;PUT&quot;)
                // 设置允许的header属性
                .allowedHeaders(&quot;*&quot;)
                // 跨域允许时间
                .maxAge(3600);
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h4&gt;②开启SpringSecurity的跨域访问&lt;/h4&gt;
&lt;p&gt;由于我们的资源都会收到SpringSecurity的保护，所以想要跨域访问还要让SpringSecurity运行跨域访问。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http
                //关闭csrf
                .csrf().disable()
                //不通过Session获取SecurityContext
                .sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS)
                .and()
                .authorizeRequests()
                // 对于登录接口 允许匿名访问  不包含context-path路径
                .antMatchers(&quot;/user/login&quot;).anonymous()
                // 除上面外的所有请求全部需要鉴权认证
                .anyRequest().authenticated();

        //添加过滤器
        http.addFilterBefore(jwtAuthenticationTokenFilter, UsernamePasswordAuthenticationFilter.class);

        //配置异常处理器
        http.exceptionHandling()
                //配置认证失败处理器
                .authenticationEntryPoint(authenticationEntryPoint)
                .accessDeniedHandler(accessDeniedHandler);

        //允许跨域
        http.cors();
    }

&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;6. 遗留小问题&lt;/h3&gt;
&lt;h4&gt;RequestMatcher(请求匹配)&lt;/h4&gt;
&lt;p&gt;&lt;code&gt;HttpSecurity&lt;/code&gt;
内置了&lt;code&gt;RequestMatcher&lt;/code&gt;
属性来处理路径匹配问题。&lt;code&gt;RequestMatcher&lt;/code&gt;
可总结为以下几大类：&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://tangtangit.com/_astro/modb_20220214_e5d9adc0-8d79-11ec-8a1f-fa163eb4f6be.EGUqmbqB_2s2MEK.webp&quot; alt=&quot;img&quot; /&gt;&lt;/p&gt;
&lt;p&gt;使用Ant路径：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt; httpSecurity.antMatcher(&quot;/foo/**&quot;);
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;如果你配置了全局的&lt;strong&gt;Servlet Path&lt;/strong&gt;的话，例如&lt;code&gt;/v1&lt;/code&gt;
，配置ant路径的话就要&lt;code&gt;/v1/foo/**&lt;/code&gt;
，使用MVC风格可以保持一致：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt; httpSecurity.mvcMatcher(&quot;/foo/**&quot;);
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;另外MVC风格可以自动匹配后缀，例如&lt;code&gt;/foo/hello&lt;/code&gt;
可以匹配&lt;code&gt;/foo/hello.do&lt;/code&gt;、&lt;code&gt;/foo/hello.action&lt;/code&gt;
等等。另外你也可以使用正则表达式来进行路径匹配：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt; httpSecurity.regexMatcher(&quot;/foo/.+&quot;);
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;如果上面的都满足不了需要的话，你可以通过&lt;code&gt;HttpSecurity.requestMatcher&lt;/code&gt;
方法自定义匹配规则；如果你想匹配多个规则的话可以借助于&lt;code&gt;HttpSecurity.requestMatchers&lt;/code&gt;
方法来自由组合匹配规则，就像这样：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;        httpSecurity.requestMatchers(requestMatchers -&amp;gt;
        requestMatchers.mvcMatchers(&quot;/foo/**&quot;)
        .antMatchers(&quot;/admin/*get&quot;));
&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote&gt;
&lt;p&gt;❝&lt;/p&gt;
&lt;p&gt;一旦你配置了路径匹配规则的话，你会发现默认的表单登录&lt;strong&gt;404&lt;/strong&gt;了，因为默认是&lt;code&gt;/login&lt;/code&gt;
，你加了前缀后当然访问不到了。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h5&gt;使用场景&lt;/h5&gt;
&lt;p&gt;比如你后台管理系统和前端应用各自走不同的过滤器链，你可以根据访问路径来配置各自的过滤器链。例如：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;    /**
     * Admin 过滤器链.
     *
     * @param http the http
     * @return the security filter chain
     * @throws Exception the exception
     */
    @Bean
    SecurityFilterChain adminSecurityFilterChain(HttpSecurity http) throws Exception {
        http.requestMatchers(requestMatchers -&amp;gt; requestMatchers.mvcMatchers(&quot;/admin/**&quot;))
            //todo 其它配置
        return http.build();
    }

    /**
     * App 过滤器链.
     *
     * @param http the http
     * @return the security filter chain
     * @throws Exception the exception
     */
    @Bean
    SecurityFilterChain appSecurityFilterChain(HttpSecurity http) throws Exception {
        http.requestMatchers(requestMatchers -&amp;gt; requestMatchers.mvcMatchers(&quot;/app/**&quot;));
        //todo 其它配置
        return http.build();
    }
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;另外也可以使用该特性降低不同规则URI之间的耦合性。&lt;/p&gt;
&lt;h4&gt;spring security的permitAll以及webIgnore的区别&lt;/h4&gt;
&lt;h5&gt;permitAll配置实例&lt;/h5&gt;
&lt;pre&gt;&lt;code&gt;@EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter {
    @Override
    public void configure(HttpSecurity http) throws Exception {
        http
                .authorizeRequests()
                .antMatchers(&quot;/css/**&quot;, &quot;/js/**&quot;,&quot;/fonts/**&quot;).permitAll()
                .anyRequest().authenticated();
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h5&gt;web ignore配置实例&lt;/h5&gt;
&lt;pre&gt;&lt;code&gt;@EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter {
    @Override
    public void configure(WebSecurity web) throws Exception {
        web.ignoring().antMatchers(&quot;/css/**&quot;);
        web.ignoring().antMatchers(&quot;/js/**&quot;);
        web.ignoring().antMatchers(&quot;/fonts/**&quot;);
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h5&gt;二者区别&lt;/h5&gt;
&lt;p&gt;顾名思义，WebSecurity主要是配置跟web资源相关的，比如css、js、images等等，但是这个还不是本质的区别，关键的区别如下：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;ingore是完全绕过了spring security的所有filter，相当于不走spring security&lt;/li&gt;
&lt;li&gt;permitall没有绕过spring security，其中包含了登录的以及匿名的。&lt;/li&gt;
&lt;/ul&gt;
&lt;h5&gt;AnonymousAuthenticationFilter&lt;/h5&gt;
&lt;p&gt;spring-security-web-4.2.3.RELEASE-sources.jar!/org/springframework/security/web/authentication/AnonymousAuthenticationFilter.java&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;/**
 * Detects if there is no {@code Authentication} object in the
 * {@code SecurityContextHolder}, and populates it with one if needed.
 *
 * @author Ben Alex
 * @author Luke Taylor
 */
public class AnonymousAuthenticationFilter extends GenericFilterBean implements
        InitializingBean {
    //......
    public void doFilter(ServletRequest req, ServletResponse res, FilterChain chain)
            throws IOException, ServletException {

        if (SecurityContextHolder.getContext().getAuthentication() == null) {
            SecurityContextHolder.getContext().setAuthentication(
                    createAuthentication((HttpServletRequest) req));

            if (logger.isDebugEnabled()) {
                logger.debug(&quot;Populated SecurityContextHolder with anonymous token: &apos;&quot;
                        + SecurityContextHolder.getContext().getAuthentication() + &quot;&apos;&quot;);
            }
        }
        else {
            if (logger.isDebugEnabled()) {
                logger.debug(&quot;SecurityContextHolder not populated with anonymous token, as it already contained: &apos;&quot;
                        + SecurityContextHolder.getContext().getAuthentication() + &quot;&apos;&quot;);
            }
        }

        chain.doFilter(req, res);
    }

    protected Authentication createAuthentication(HttpServletRequest request) {
        AnonymousAuthenticationToken auth = new AnonymousAuthenticationToken(key,
                principal, authorities);
        auth.setDetails(authenticationDetailsSource.buildDetails(request));

        return auth;
    }
    
    //......
}
&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote&gt;
&lt;p&gt;这个filter的主要功能就是给没有登陆的用户，填充AnonymousAuthenticationToken到SecurityContextHolder的Authentication，后续依赖Authentication的代码可以统一处理。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h5&gt;FilterComparator&lt;/h5&gt;
&lt;p&gt;spring-security-config-4.1.4.RELEASE-sources.jar!/org/springframework/security/config/annotation/web/builders/FilterComparator.java&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;final class FilterComparator implements Comparator&amp;lt;Filter&amp;gt;, Serializable {
    private static final int STEP = 100;
    private Map&amp;lt;String, Integer&amp;gt; filterToOrder = new HashMap&amp;lt;String, Integer&amp;gt;();

    FilterComparator() {
        int order = 100;
        put(ChannelProcessingFilter.class, order);
        order += STEP;
        put(ConcurrentSessionFilter.class, order);
        order += STEP;
        put(WebAsyncManagerIntegrationFilter.class, order);
        order += STEP;
        put(SecurityContextPersistenceFilter.class, order);
        order += STEP;
        put(HeaderWriterFilter.class, order);
        order += STEP;
        put(CorsFilter.class, order);
        order += STEP;
        put(CsrfFilter.class, order);
        order += STEP;
        put(LogoutFilter.class, order);
        order += STEP;
        put(X509AuthenticationFilter.class, order);
        order += STEP;
        put(AbstractPreAuthenticatedProcessingFilter.class, order);
        order += STEP;
        filterToOrder.put(&quot;org.springframework.security.cas.web.CasAuthenticationFilter&quot;,
                order);
        order += STEP;
        put(UsernamePasswordAuthenticationFilter.class, order);
        order += STEP;
        put(ConcurrentSessionFilter.class, order);
        order += STEP;
        filterToOrder.put(
                &quot;org.springframework.security.openid.OpenIDAuthenticationFilter&quot;, order);
        order += STEP;
        put(DefaultLoginPageGeneratingFilter.class, order);
        order += STEP;
        put(ConcurrentSessionFilter.class, order);
        order += STEP;
        put(DigestAuthenticationFilter.class, order);
        order += STEP;
        put(BasicAuthenticationFilter.class, order);
        order += STEP;
        put(RequestCacheAwareFilter.class, order);
        order += STEP;
        put(SecurityContextHolderAwareRequestFilter.class, order);
        order += STEP;
        put(JaasApiIntegrationFilter.class, order);
        order += STEP;
        put(RememberMeAuthenticationFilter.class, order);
        order += STEP;
        put(AnonymousAuthenticationFilter.class, order);
        order += STEP;
        put(SessionManagementFilter.class, order);
        order += STEP;
        put(ExceptionTranslationFilter.class, order);
        order += STEP;
        put(FilterSecurityInterceptor.class, order);
        order += STEP;
        put(SwitchUserFilter.class, order);
    }

    //......
}
&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote&gt;
&lt;p&gt;这个类定义了spring security内置的filter的优先级，AnonymousAuthenticationFilter在倒数第五个执行，在FilterSecurityInterceptor这个类之前。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h5&gt;FilterSecurityInterceptor&lt;/h5&gt;
&lt;p&gt;spring-security-web-4.2.3.RELEASE-sources.jar!/org/springframework/security/web/access/intercept/FilterSecurityInterceptor.java&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;/**
 * Performs security handling of HTTP resources via a filter implementation.
 * &amp;lt;p&amp;gt;
 * The &amp;lt;code&amp;gt;SecurityMetadataSource&amp;lt;/code&amp;gt; required by this security interceptor is of
 * type {@link FilterInvocationSecurityMetadataSource}.
 * &amp;lt;p&amp;gt;
 * Refer to {@link AbstractSecurityInterceptor} for details on the workflow.
 * &amp;lt;/p&amp;gt;
 *
 * @author Ben Alex
 * @author Rob Winch
 */
public class FilterSecurityInterceptor extends AbstractSecurityInterceptor implements
        Filter {
    //......
}
&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote&gt;
&lt;p&gt;这个相当于spring security的核心处理类了，它继承抽象类AbstractSecurityInterceptor&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;spring-security-core-4.2.3.RELEASE-sources.jar!/org/springframework/security/access/intercept/AbstractSecurityInterceptor.java&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;public abstract class AbstractSecurityInterceptor implements InitializingBean,
        ApplicationEventPublisherAware, MessageSourceAware {
    //......
    protected InterceptorStatusToken beforeInvocation(Object object) {
        Assert.notNull(object, &quot;Object was null&quot;);
        final boolean debug = logger.isDebugEnabled();

        if (!getSecureObjectClass().isAssignableFrom(object.getClass())) {
            throw new IllegalArgumentException(
                    &quot;Security invocation attempted for object &quot;
                            + object.getClass().getName()
                            + &quot; but AbstractSecurityInterceptor only configured to support secure objects of type: &quot;
                            + getSecureObjectClass());
        }

        Collection&amp;lt;ConfigAttribute&amp;gt; attributes = this.obtainSecurityMetadataSource()
                .getAttributes(object);

        if (attributes == null || attributes.isEmpty()) {
            if (rejectPublicInvocations) {
                throw new IllegalArgumentException(
                        &quot;Secure object invocation &quot;
                                + object
                                + &quot; was denied as public invocations are not allowed via this interceptor. &quot;
                                + &quot;This indicates a configuration error because the &quot;
                                + &quot;rejectPublicInvocations property is set to &apos;true&apos;&quot;);
            }

            if (debug) {
                logger.debug(&quot;Public object - authentication not attempted&quot;);
            }

            publishEvent(new PublicInvocationEvent(object));

            return null; // no further work post-invocation
        }

        if (debug) {
            logger.debug(&quot;Secure object: &quot; + object + &quot;; Attributes: &quot; + attributes);
        }

        if (SecurityContextHolder.getContext().getAuthentication() == null) {
            credentialsNotFound(messages.getMessage(
                    &quot;AbstractSecurityInterceptor.authenticationNotFound&quot;,
                    &quot;An Authentication object was not found in the SecurityContext&quot;),
                    object, attributes);
        }

        Authentication authenticated = authenticateIfRequired();

        // Attempt authorization
        try {
            this.accessDecisionManager.decide(authenticated, object, attributes);
        }
        catch (AccessDeniedException accessDeniedException) {
            publishEvent(new AuthorizationFailureEvent(object, attributes, authenticated,
                    accessDeniedException));

            throw accessDeniedException;
        }

        if (debug) {
            logger.debug(&quot;Authorization successful&quot;);
        }

        if (publishAuthorizationSuccess) {
            publishEvent(new AuthorizedEvent(object, attributes, authenticated));
        }

        // Attempt to run as a different user
        Authentication runAs = this.runAsManager.buildRunAs(authenticated, object,
                attributes);

        if (runAs == null) {
            if (debug) {
                logger.debug(&quot;RunAsManager did not change Authentication object&quot;);
            }

            // no further work post-invocation
            return new InterceptorStatusToken(SecurityContextHolder.getContext(), false,
                    attributes, object);
        }
        else {
            if (debug) {
                logger.debug(&quot;Switching to RunAs Authentication: &quot; + runAs);
            }

            SecurityContext origCtx = SecurityContextHolder.getContext();
            SecurityContextHolder.setContext(SecurityContextHolder.createEmptyContext());
            SecurityContextHolder.getContext().setAuthentication(runAs);

            // need to revert to token.Authenticated post-invocation
            return new InterceptorStatusToken(origCtx, true, attributes, object);
        }
    }
    //......
}
&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote&gt;
&lt;p&gt;主要的逻辑在这个beforeInvocation方法，它就依赖了authentication&lt;/p&gt;
&lt;/blockquote&gt;
&lt;pre&gt;&lt;code&gt;private Authentication authenticateIfRequired() {
        Authentication authentication = SecurityContextHolder.getContext()
                .getAuthentication();

        if (authentication.isAuthenticated() &amp;amp;&amp;amp; !alwaysReauthenticate) {
            if (logger.isDebugEnabled()) {
                logger.debug(&quot;Previously Authenticated: &quot; + authentication);
            }

            return authentication;
        }

        authentication = authenticationManager.authenticate(authentication);

        // We don&apos;t authenticated.setAuthentication(true), because each provider should do
        // that
        if (logger.isDebugEnabled()) {
            logger.debug(&quot;Successfully Authenticated: &quot; + authentication);
        }

        SecurityContextHolder.getContext().setAuthentication(authentication);

        return authentication;
    }
&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote&gt;
&lt;p&gt;这个方法判断authentication如果是已经校验过的，则返回；没有校验过的话，则调用authenticationManager进行鉴权。&lt;/p&gt;
&lt;p&gt;而AnonymousAuthenticationFilter设置的authentication在这个时候就派上用场了
spring-security-core-4.2.3.RELEASE-sources.jar!/org/springframework/security/authentication/AnonymousAuthenticationToken.java&lt;/p&gt;
&lt;/blockquote&gt;
&lt;pre&gt;&lt;code&gt;public class AnonymousAuthenticationToken extends AbstractAuthenticationToken implements
        Serializable {
    private AnonymousAuthenticationToken(Integer keyHash, Object principal,
                                        Collection&amp;lt;? extends GrantedAuthority&amp;gt; authorities) {
        super(authorities);

        if (principal == null || &quot;&quot;.equals(principal)) {
            throw new IllegalArgumentException(&quot;principal cannot be null or empty&quot;);
        }
        Assert.notEmpty(authorities, &quot;authorities cannot be null or empty&quot;);

        this.keyHash = keyHash;
        this.principal = principal;
        setAuthenticated(true);
    }
    //......
}
&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote&gt;
&lt;p&gt;它默认就是authenticated&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h5&gt;小结&lt;/h5&gt;
&lt;ul&gt;
&lt;li&gt;web ignore比较适合配置前端相关的静态资源，它是完全绕过spring security的所有filter的；&lt;/li&gt;
&lt;li&gt;而permitAll，会给没有登录的用户适配一个AnonymousAuthenticationToken，设置到SecurityContextHolder，方便后面的filter可以统一处理authentication。&lt;/li&gt;
&lt;/ul&gt;
&lt;h4&gt;其它权限校验方法&lt;/h4&gt;
&lt;p&gt;​	我们前面都是使用@PreAuthorize注解，然后在在其中使用的是hasAuthority方法进行校验。SpringSecurity还为我们提供了其它方法例如：hasAnyAuthority，hasRole，hasAnyRole等。&lt;/p&gt;
&lt;p&gt;​&lt;/p&gt;
&lt;p&gt;​	这里我们先不急着去介绍这些方法，我们先去理解hasAuthority的原理，然后再去学习其他方法你就更容易理解，而不是死记硬背区别。并且我们也可以选择定义校验方法，实现我们自己的校验逻辑。&lt;/p&gt;
&lt;p&gt;​	hasAuthority方法实际是执行到了SecurityExpressionRoot的hasAuthority，大家只要断点调试既可知道它内部的校验原理。&lt;/p&gt;
&lt;p&gt;​	它内部其实是调用authentication的getAuthorities方法获取用户的权限列表。然后判断我们存入的方法参数数据在权限列表中。&lt;/p&gt;
&lt;p&gt;​	hasAnyAuthority方法可以传入多个权限，只有用户有其中任意一个权限都可以访问对应资源。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;    @PreAuthorize(&quot;hasAnyAuthority(&apos;admin&apos;,&apos;test&apos;,&apos;system:dept:list&apos;)&quot;)
    public String hello(){
        return &quot;hello&quot;;
    }
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;​	hasRole要求有对应的角色才可以访问，但是它内部会把我们传入的参数拼接上 &lt;strong&gt;ROLE_&lt;/strong&gt; 后再去比较。所以这种情况下要用用户对应的权限也要有 &lt;strong&gt;ROLE_&lt;/strong&gt; 这个前缀才可以。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;    @PreAuthorize(&quot;hasRole(&apos;system:dept:list&apos;)&quot;)
    public String hello(){
        return &quot;hello&quot;;
    }
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;​	hasAnyRole 有任意的角色就可以访问。它内部也会把我们传入的参数拼接上 &lt;strong&gt;ROLE_&lt;/strong&gt; 后再去比较。所以这种情况下要用用户对应的权限也要有 &lt;strong&gt;ROLE_&lt;/strong&gt; 这个前缀才可以。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;    @PreAuthorize(&quot;hasAnyRole(&apos;admin&apos;,&apos;system:dept:list&apos;)&quot;)
    public String hello(){
        return &quot;hello&quot;;
    }
&lt;/code&gt;&lt;/pre&gt;
&lt;h4&gt;自定义权限校验方法&lt;/h4&gt;
&lt;p&gt;​	我们也可以定义自己的权限校验方法，在@PreAuthorize注解中使用我们的方法。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;@Component(&quot;ex&quot;)
public class SGExpressionRoot {

    public boolean hasAuthority(String authority){
        //获取当前用户的权限
        Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
        LoginUser loginUser = (LoginUser) authentication.getPrincipal();
        List&amp;lt;String&amp;gt; permissions = loginUser.getPermissions();
        //判断用户权限集合中是否存在authority
        return permissions.contains(authority);
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;​	 在SPEL表达式中使用 @ex相当于获取容器中bean的名字未ex的对象。然后再调用这个对象的hasAuthority方法&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;    @RequestMapping(&quot;/hello&quot;)
    @PreAuthorize(&quot;@ex.hasAuthority(&apos;system:dept:list&apos;)&quot;)
    public String hello(){
        return &quot;hello&quot;;
    }
&lt;/code&gt;&lt;/pre&gt;
&lt;h4&gt;基于配置的权限控制&lt;/h4&gt;
&lt;p&gt;​	我们也可以在配置类中使用使用配置的方式对资源进行权限控制。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http
                //关闭csrf
                .csrf().disable()
                //不通过Session获取SecurityContext
                .sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS)
                .and()
                .authorizeRequests()
                // 对于登录接口 允许匿名访问  不包含context-path路径
                .antMatchers(&quot;/user/login&quot;).anonymous()
                .antMatchers(&quot;/testCors&quot;).hasAuthority(&quot;system:dept:list222&quot;)
                // 除上面外的所有请求全部需要鉴权认证
                .anyRequest().authenticated();

        //添加过滤器
        http.addFilterBefore(jwtAuthenticationTokenFilter, UsernamePasswordAuthenticationFilter.class);

        //配置异常处理器
        http.exceptionHandling()
                //配置认证失败处理器
                .authenticationEntryPoint(authenticationEntryPoint)
                .accessDeniedHandler(accessDeniedHandler);

        //允许跨域
        http.cors();
    }
&lt;/code&gt;&lt;/pre&gt;
&lt;h4&gt;CSRF&lt;/h4&gt;
&lt;p&gt;​	CSRF是指跨站请求伪造（Cross-site request forgery），是web常见的攻击之一。&lt;/p&gt;
&lt;p&gt;​	https://blog.csdn.net/freeking101/article/details/86537087&lt;/p&gt;
&lt;p&gt;​	SpringSecurity去防止CSRF攻击的方式就是通过csrf_token。后端会生成一个csrf_token，前端发起请求的时候需要携带这个csrf_token,后端会有过滤器进行校验，如果没有携带或者是伪造的就不允许访问。&lt;/p&gt;
&lt;p&gt;​	我们可以发现CSRF攻击依靠的是cookie中所携带的认证信息。但是在前后端分离的项目中我们的认证信息其实是token，而token并不是存储中cookie中，并且需要前端代码去把token设置到请求头中才可以，所以CSRF攻击也就不用担心了。&lt;/p&gt;
&lt;h4&gt;认证成功处理器&lt;/h4&gt;
&lt;p&gt;​	实际上在UsernamePasswordAuthenticationFilter进行登录认证的时候，如果登录成功了是会调用AuthenticationSuccessHandler的方法进行认证成功后的处理的。AuthenticationSuccessHandler就是登录成功处理器。&lt;/p&gt;
&lt;p&gt;​	我们也可以自己去自定义成功处理器进行成功后的相应处理。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;@Component
public class SGSuccessHandler implements AuthenticationSuccessHandler {

    @Override
    public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws IOException, ServletException {
        System.out.println(&quot;认证成功了&quot;);
    }
}

&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;@Configuration
public class SecurityConfig extends WebSecurityConfigurerAdapter {

    @Autowired
    private AuthenticationSuccessHandler successHandler;

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http.formLogin().successHandler(successHandler);

        http.authorizeRequests().anyRequest().authenticated();
    }
}

&lt;/code&gt;&lt;/pre&gt;
&lt;h4&gt;认证失败处理器&lt;/h4&gt;
&lt;p&gt;​	实际上在UsernamePasswordAuthenticationFilter进行登录认证的时候，如果认证失败了是会调用AuthenticationFailureHandler的方法进行认证失败后的处理的。AuthenticationFailureHandler就是登录失败处理器。&lt;/p&gt;
&lt;p&gt;​	我们也可以自己去自定义失败处理器进行失败后的相应处理。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;@Component
public class SGFailureHandler implements AuthenticationFailureHandler {
    @Override
    public void onAuthenticationFailure(HttpServletRequest request, HttpServletResponse response, AuthenticationException exception) throws IOException, ServletException {
        System.out.println(&quot;认证失败了&quot;);
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;@Configuration
public class SecurityConfig extends WebSecurityConfigurerAdapter {

    @Autowired
    private AuthenticationSuccessHandler successHandler;

    @Autowired
    private AuthenticationFailureHandler failureHandler;

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http.formLogin()
//                配置认证成功处理器
                .successHandler(successHandler)
//                配置认证失败处理器
                .failureHandler(failureHandler);

        http.authorizeRequests().anyRequest().authenticated();
    }
}

&lt;/code&gt;&lt;/pre&gt;
&lt;h4&gt;登出成功处理器&lt;/h4&gt;
&lt;pre&gt;&lt;code&gt;@Component
public class SGLogoutSuccessHandler implements LogoutSuccessHandler {
    @Override
    public void onLogoutSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws IOException, ServletException {
        System.out.println(&quot;注销成功&quot;);
    }
}

&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;@Configuration
public class SecurityConfig extends WebSecurityConfigurerAdapter {

    @Autowired
    private AuthenticationSuccessHandler successHandler;

    @Autowired
    private AuthenticationFailureHandler failureHandler;

    @Autowired
    private LogoutSuccessHandler logoutSuccessHandler;

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http.formLogin()
//                配置认证成功处理器
                .successHandler(successHandler)
//                配置认证失败处理器
                .failureHandler(failureHandler);

        http.logout()
                //配置注销成功处理器
                .logoutSuccessHandler(logoutSuccessHandler);

        http.authorizeRequests().anyRequest().authenticated();
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h4&gt;其他认证方案畅想&lt;/h4&gt;
&lt;h3&gt;7. 源码讲解&lt;/h3&gt;
&lt;p&gt;​	投票过50更新源码讲解&lt;/p&gt;
&lt;h3&gt;8.修改security的loadUserByUsername&lt;/h3&gt;
&lt;h3&gt;9.添加短信验证码过滤器&lt;/h3&gt;
&lt;h2&gt;整体把握&lt;/h2&gt;
&lt;h3&gt;1、简介&lt;/h3&gt;
&lt;p&gt;SpringSecurity是一个能够基于Spring的应用程序提供声明式安全保护的安全性框架，它提供了一组可以在Spring应用上下文中配置的Bean，充分利用了SpringIOC（控制反转）和AOP（面向切面编程）功能，为应用系统提供安全访问控制功能，减少了为系统安全控制编写大量重复代码的工作。&lt;/p&gt;
&lt;p&gt;官网地址：https://spring.io/projects/spring-security&lt;/p&gt;
&lt;p&gt;安全框架主要包含两个操作。&lt;/p&gt;
&lt;p&gt;认证：确认用户可以访问当前系统。&lt;/p&gt;
&lt;p&gt;授权：确定用户在当前系统中是否能够执行某个操作，即用户所拥有的功能权限。&lt;/p&gt;
&lt;h3&gt;2、Security适配器&lt;/h3&gt;
&lt;p&gt;创建一个自定义类继承WebSecurityConfigurerAdapter，并在改类中使用@EnableWebSecurity注解就可以通过重写config方法类配置所需要的安全配置。&lt;/p&gt;
&lt;p&gt;WebSecurityConfigurerAdapter是SpringSecurity为Web应用提供的一个适配器，实现了WebSecurityConfigurerAdapter接口，提供了两个方法用于重写开发者需要的安全配置。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;protected void configure(HttpSecurity http) throws Exception {
}
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;configure(HttpSecurity http)方法中可以通过HTTPSecurity的authorizeRequests()方法定义那些URL需要被保护、那些不需要被保护；通过formLogin()方法定义当前需要用户登录的时候，跳转到的登录页面。&lt;/p&gt;
&lt;p&gt;configure(AuthenticationManagerBuilder auth)方法用于创建用户和用户的角色。&lt;/p&gt;
&lt;h4&gt;用户认证&lt;/h4&gt;
&lt;p&gt;SpringSecurity是通过configure(AuthenticationManagerBuilder auth)完成用户认证的。使用AuthenticationManagerBuilder的inMemoryAuthentication()方法可以添加用户，并给用户指定权限。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
    auth.inMemoryAuthentication().withUser(&quot;aaa&quot;).password(&quot;{noop}1234&quot;).roles(&quot;USER&quot;);
    auth.inMemoryAuthentication().withUser(&quot;admin&quot;).password(&quot;{noop}admin&quot;).roles(&quot;ADMIN&quot;, &quot;DBA&quot;);
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;上面的代码中添加了两个用户，其中一个用户名是“aaa”，密码是“1234”，用户权限是“USER”；另一个用户名是“admin”，密码是“admin”，用户权限有两个，分别是“ADMIN”和“DBA”。需要注意的是SpringSecurity保存用户权限的时候，会默认使用“ROLE_”，也就是说“USER”实际上是“ROLE_USER”，“ADMIN”实际是“ROLE_ADMIN”，“DBA”实际上是“ROLE_DBA”。&lt;/p&gt;
&lt;p&gt;当然，也可以查询数据库获取用户和权限。下面有写到。&lt;/p&gt;
&lt;h4&gt;用户授权&lt;/h4&gt;
&lt;p&gt;SpringSecurity是通过configure(HttpSecurity http)完成用户授权的。&lt;/p&gt;
&lt;p&gt;HTTPSecurity的authorizeRequests()方法有多个子节点，每个macher按照它们的声明顺序执行，指定用户可以访问的多个URL模式。&lt;/p&gt;
&lt;p&gt;antMatchers使用Ant风格匹配路径。
regexMatchers使用正则表达式匹配路径。
在匹配了请求路径后，可以针对当前用户的信息对请求路径进行安全处理。下表是SpringSecurity提供的安全处理方法。&lt;/p&gt;
&lt;p&gt;方法	用途
anyRequest	匹配所有请求路径
access(String)	Spring EL 表达式结果为true时可以访问
anonymous()	匿名可以访问
denyAll()	用户不能访问
fullyAuthenticated()	用户完全认证可以访问（非remember-me下自动登录）
hasAnyAuthority(String...)	如果有参数，参数表示权限，则其中任何一个权限可以访问
hasAnyRole(String...)	如果有参数，参数表示角色，则其中任何一个角色可以访问
hasAuthority(String...)	如果有参数，参数表示权限，则其权限可以访问
hasIpAddress(String)	如果有参数，参数表示IP地址，如果用户IP和参数匹配，则可以访问
hasRole(String)	如果有参数，参数表示角色，则其角色可以访问
permitAll()	用户可以任意访问
rememberMe()	允许通过remember-me登录的用户访问
authenticated()	用户登录后可访问
示例代码如下：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;@Override
protected void configure(HttpSecurity http) throws Exception {
    http.csrf().disable();    //安全器令牌
    http.authorizeRequests()
            .antMatchers(&quot;/login&quot;).permitAll()
            .antMatchers(&quot;/&quot;, &quot;/home&quot;).hasRole(&quot;USER&quot;)
            .antMatchers(&quot;/admin/**&quot;).hasAnyRole(&quot;ADMIN&quot;, &quot;DBA&quot;)
            .anyRequest().authenticated()
            .and()
            .formLogin()
            .loginPage(&quot;/login&quot;)
            .usernameParameter(&quot;loginName&quot;).passwordParameter(&quot;password&quot;)
            .defaultSuccessUrl(&quot;/main&quot;)
            .failureUrl(&quot;/login?error&quot;)
            .and()
            .logout()
            .permitAll()
            .and()
            .exceptionHandling().accessDeniedPage(&quot;/accessDenied&quot;);
}
&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;http.authorizeRequests() 开始请求权限配置。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;antMatchers(&quot;/login&quot;).permitAll() 请求匹配“/login”，所有用户都可以访问。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;antMatchers(&quot;/&quot;, &quot;/home&quot;).hasRole(&quot;USER&quot;)  请求匹配“/”和“/home”，拥有“ROLE_USER”角色的用户可以访问。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;antMatchers(&quot;/admin/&lt;strong&gt;&quot;).hasAnyRole(&quot;ADMIN&quot;, &quot;DBA&quot;)  请求匹配“/admin/&lt;/strong&gt;”，拥有“ROLE_ADMIN”或“ROLE_DBA”角色的用户可以访问。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;anyRequest().authenticated()  其余所有的请求都需要认证（用户登录）之后才可以访问。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;formLogin()  开始设置登录操作&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;loginPage(&quot;/login&quot;)  设置登录页面的访问地址&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;usernameParameter(&quot;loginName&quot;).passwordParameter(&quot;password&quot;)  登录时接收传递的参数“loginName”的值作为用户名，接收传递参数的“password”的值作为密码。如果不设置，默认“username”为用户名，“password”为密码&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;defaultSuccessUrl(&quot;/main&quot;)  指定登录成功后转向的页面。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;failureUrl(&quot;/login?error&quot;)  指定登录失败后转向的页面和传递的参数。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;logout()   设置注销操作&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;permitAll()  所有用户都可以访问。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;exceptionHandling().accessDeniedPage(&quot;/accessDenied&quot;)  指定异常处理页面&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;SpringSecurity核心类&lt;/h3&gt;
&lt;blockquote&gt;
&lt;p&gt;SpringSecurity核心类包括Authentication、SecurityContextHolder、UserDetails、UserDetailsService、GrantedAuthority、DaoAuthenticationProvider和PasswordEncoder。只要掌握了这些SpringSecurity核心类，SpringSecurity就会变得非常简单。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h4&gt;1、Authentication类&lt;/h4&gt;
&lt;p&gt;Authentication用来表示用户认证信息，用户登录认证之前，SpringSecurity会将相关信息封装为一个Authentication具体实现类的对象，在登录认证成功之后又会生成一个信息更全面、包含用户权限等信息的Authentication对象，然后把它保存在SecurityContextHolder所持有的SecurityContext中，供后续的程序进行调用，如访问权限的鉴定等。&lt;/p&gt;
&lt;p&gt;SecurityContextHolder中的getContext()方法&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;public static SecurityContext getContext() {
	return strategy.getContext();
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h4&gt;2、SecurityContextHolder类&lt;/h4&gt;
&lt;p&gt;SecurityContextHolder是用来保存SecurityContext的。SecurityContext中含有当前所访问系统的用户的详细信息。默认情况下，SecurityContextHolder将使用ThreadLocal来保存SecurityContext，这也就意味着在处于同一线程的方法中，可以从ThreadLocal获取到当前的SecurityContext。&lt;/p&gt;
&lt;p&gt;SpringSecurity使用一个Authentication对象来描述当前用户的相关信息。SecurityContextHolder中持有的是当前用户的SecurityContext，而SecurityContext持有的是代表当前用户相关信息的Authentication的引用。这个Authentication对象不需要我们自己创建，在与系统交互的过程中，SpringSecurity会自动创建相应的Authentication对象，然后赋值给当前的SecurityContext。开发过程中常常需要在程序中获取当前用户的相关信息，比如最常见的获取当前登录用户的用户名。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt; String username = SecurityContextHolder.getContext().getAuthentication().getName();
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;获取UserDetails类，该类中包含用户认证相关等信息。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;UserDetails userDetails = (UserDetails) SecurityContextHolder.getContext().getAuthentication().getPrincipal();
&lt;/code&gt;&lt;/pre&gt;
&lt;h4&gt;3、UserDetails类&lt;/h4&gt;
&lt;p&gt;UserDetails是SpringSecurity的一个核心接口。其中定义了一些可以获取用户名，密码、权限等与认证相关的信息的方法。SpringSecurity内部使用UserDetails实现类大都是内置的User类，要使用UserDetails，也可以直接使用该类。在SpringSecurity内部，很多需要使用用户信息的时候，基本上都是使用UserDetails，比如在登录认证的时候。&lt;/p&gt;
&lt;p&gt;通常需要在应用中获取当前用户的其他信息，如E-mail、电话等。这时存放在Authentication中的principal只包含认证相关信息的UserDetails对象可能就不能满足我们的要求了。这时可以实现自己的UserDetails，在该实现类中可以定义一些获取用户其他信息的方法，这样将来就可以直接从当前SecurityContext的Authentication的principal中获取这些信息。&lt;/p&gt;
&lt;p&gt;UserDetails是通过UserDetailsService的loadUserByUsername()方法进行加载的，UserDetailsService也是一个接口，我们也需要实现自己的UserDetailsService来加载自定义的UserDetails信息。&lt;/p&gt;
&lt;p&gt;新建用户表的Service类实现UserDetailsService接口，来重写UserDetailsService的loadUserByUsername()方法，根据用户名查询当前用户信息并返回，返回的类必须继承Security内置的User类。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;@Service
public class SysUserService implements UserDetailsService{
    @Override
    public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException{
       
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h4&gt;4、UserDetailsService类&lt;/h4&gt;
&lt;p&gt;Authentication.getPrincipal() 的返回类型是Object。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;Object getPrincipal();
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;但很多情况下返回的其实是一个UserDetails的实例。登录认证的时候SpringSecurity会通过UserDetailsService的loadUserByUsername()方法获取对应的UserDetails进行认证，认证通过后会将该UserDetails赋给认证通过的Authentication的Principal，然后在把该Authentication存入SecurityContext。之后如果需要使用用户信息，可以通过SecurityContextHolder获取存放在SecurityContext中的Authentication的principal，转为UserDetails类。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;UserDetails userDetails = (UserDetails) SecurityContextHolder.getContext().getAuthentication().getPrincipal();
&lt;/code&gt;&lt;/pre&gt;
&lt;h4&gt;5、GrantedAuthority类&lt;/h4&gt;
&lt;p&gt;Authentication的getAuthorities()方法可以返回当前Authentication对象用户的所有的权限。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;Collection&amp;lt;? extends GrantedAuthority&amp;gt; getAuthorities();
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;即当前用户拥有的权限。其返回值是一个GrantedAuthority类型的数组，每一个GrantedAuthority对象代表赋予给当前用户的一种权限。GrantedAuthority是一个接口，其通常是通过UserDetailsService进行加载，然后赋予UserDetails的。&lt;/p&gt;
&lt;p&gt;GrantedAuthority中只定义了一个getAuthority()方法，该方法返回一个字符串，表示对应的权限，如果对应权限不能用字符串表示，则返回null&lt;/p&gt;
&lt;p&gt;获取当前用户的所有权限，存放到List集合中。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;UserDetails userDetails = (UserDetails) SecurityContextHolder.getContext().getAuthentication().getPrincipal();
List&amp;lt;String&amp;gt; roleCodes=new ArrayList&amp;lt;&amp;gt;();
for (GrantedAuthority authority : userDetails.getAuthorities()) {
	roleCodes.add(authority.getAuthority());
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h4&gt;6、DaoAuthenticationProvider类&lt;/h4&gt;
&lt;p&gt;SpringSecurity默认了会使用DaoAuthenticationProvider实现AuthenticationProvider接口，专门进行用户认证的处理。DaoAuthenticationProvider在进行认证的时候需要一个UserDetailsService来获取用户的信息UserDetails，其中包括用户名，密码和所拥有的权限等。如果需要改变认证的方式，开发者可以实现自己的AuthenticationProvider。&lt;/p&gt;
&lt;h4&gt;7、PasswordEncoder类&lt;/h4&gt;
&lt;p&gt;在SpringSecurity中，对密码的加密都是由PasswordEncoder来完成的。在SpringSecurity中，已经对PasswordEncoder有了很多实现，包括md5加密，SHA-256加密等，开发者只需要直接拿来用就可以。在DaoAuthenticationProvider中，有一个就是PasswordEncoder熟悉，密码加密功能主义靠它来完成。&lt;/p&gt;
&lt;h3&gt;SpringSecurity的验证机制&lt;/h3&gt;
&lt;p&gt;1、SpringSecurity的验证机制
SpringSecurity大体上是由一堆Filter实现的，Filter会在SpringMVC前拦截请求。Filter包括登出Filter(LogoutFilter)、用户名密码验证Filter(UsernamePasswordAuthenticationFilter)之类。Filter在交由其他组件完成细分的功能，最常用的UsernamePasswordAuthenticationFilter会持有一个AuthenticationManager引用，AuthenticationManager是一个验证管理器，专门负责验证。但AuthenticationManager本身并不做具体的验证工作，AuthenticationManager持有一个AuthenticationProvider集合，AuthenticationProvider才是做验证工作的组件，验证成功或失败之后调用对应的Handler。
————————————————
版权声明：本文为CSDN博主「小马 同学」的原创文章，遵循CC 4.0 BY-SA版权协议，转载请附上原文出处链接及本声明。
原文链接：https://blog.csdn.net/qq_40205116/article/details/103439326&lt;/p&gt;
&lt;h2&gt;SpringBoot2.7 WebSecurityConfigurerAdapter类过期配置&lt;/h2&gt;
&lt;h3&gt;前言&lt;/h3&gt;
&lt;p&gt;进入到 SpringBoot2.7 时代，有小伙伴发现有一个常用的类忽然过期了：&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://tangtangit.com/_astro/2022061716454501.BZ8Wu1KC_Z1LzrBB.webp&quot; alt=&quot;img&quot; /&gt;&lt;/p&gt;
&lt;p&gt;在 Spring Security 时代，这个类可太重要了。过期的类当然可以继续使用，但是你要是决定别扭，只需要稍微看一下注释，基本上就明白该怎么玩了。&lt;/p&gt;
&lt;h4&gt;WebSecurityConfigurerAdapter 的注释&lt;/h4&gt;
&lt;p&gt;我们来看下 WebSecurityConfigurerAdapter 的注释：&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://tangtangit.com/_astro/2022061716454502.5Y4FryY8_Z258kuz.webp&quot; alt=&quot;img&quot; /&gt;&lt;/p&gt;
&lt;p&gt;从这段注释中我们大概就明白了咋回事了。&lt;/p&gt;
&lt;h4&gt;配置Spring Security&lt;/h4&gt;
&lt;p&gt;以前我们自定义类继承自 WebSecurityConfigurerAdapter 来配置我们的 Spring Security，我们主要是配置两个东西：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;configure(HttpSecurity)&lt;/li&gt;
&lt;li&gt;configure(WebSecurity)&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;前者主要是配置 Spring Security 中的过滤器链，后者则主要是配置一些路径放行规则。&lt;/p&gt;
&lt;p&gt;现在在 WebSecurityConfigurerAdapter 的注释中，人家已经把意思说的很明白了：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;以后如果想要配置过滤器链，可以通过自定义 SecurityFilterChain Bean 来实现。&lt;/li&gt;
&lt;li&gt;以后如果想要配置 WebSecurity，可以通过 WebSecurityCustomizer Bean 来实现。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;那么接下来我们就通过一个简单的例子来看下。&lt;/p&gt;
&lt;h4&gt;引入Web和Spring Security依赖&lt;/h4&gt;
&lt;p&gt;首先我们新建一个 Spring Boot 工程，引入 Web 和 Spring Security 依赖，注意 Spring Boot 选择最新的 2.7。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://tangtangit.com/_astro/2022061716454503.CGAc2Gr2_Z2dD4OA.webp&quot; alt=&quot;img&quot; /&gt;&lt;/p&gt;
&lt;p&gt;接下来我们提供一个简单的测试接口，如下：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;@RestController
public class HelloController {
    @GetMapping(&quot;/hello&quot;)
    public String hello() {
        return &quot;hello 江南一点雨!&quot;;
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;小伙伴们知道，在 Spring Security 中，默认情况下，只要添加了依赖，我们项目的所有接口就已经被统统保护起来了，现在启动项目，访问 &lt;code&gt;/hello&lt;/code&gt; 接口，就需要登录之后才可以访问，登录的用户名是 user，密码则是随机生成的，在项目的启动日志中。&lt;/p&gt;
&lt;p&gt;现在我们的第一个需求是使用自定义的用户，而不是系统默认提供的，这个简单，我们只需要向 Spring 容器中注册一个 UserDetailsService 的实例即可，像下面这样：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;@Configuration
public class SecurityConfig {
    @Bean
    UserDetailsService userDetailsService() {
        InMemoryUserDetailsManager users = new InMemoryUserDetailsManager();
        users.createUser(User.withUsername(&quot;javaboy&quot;).password(&quot;{noop}123&quot;).roles(&quot;admin&quot;).build());
        users.createUser(User.withUsername(&quot;江南一点雨&quot;).password(&quot;{noop}123&quot;).roles(&quot;admin&quot;).build());
        return users;
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这就可以了。&lt;/p&gt;
&lt;p&gt;当然我现在的用户是存在内存中的，如果你的用户是存在数据库中，那么只需要提供 UserDetailsService 接口的实现类并注入 Spring 容器即可，这个之前在 vhr 视频中讲过多次了（公号后台回复 666 有视频介绍），这里就不再赘述了。&lt;/p&gt;
&lt;h4&gt;重写configure(WebSecurity方法进行配置&lt;/h4&gt;
&lt;p&gt;但是假如说我希望 &lt;code&gt;/hello&lt;/code&gt; 这个接口能够匿名访问，并且我希望这个匿名访问还不经过 Spring Security 过滤器链，要是在以前，我们可以重写 &lt;code&gt;configure(WebSecurity)&lt;/code&gt; 方法进行配置，但是现在，得换一种玩法：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;@Configuration
public class SecurityConfig {
    @Bean
    UserDetailsService userDetailsService() {
        InMemoryUserDetailsManager users = new InMemoryUserDetailsManager();
        users.createUser(User.withUsername(&quot;javaboy&quot;).password(&quot;{noop}123&quot;).roles(&quot;admin&quot;).build());
        users.createUser(User.withUsername(&quot;江南一点雨&quot;).password(&quot;{noop}123&quot;).roles(&quot;admin&quot;).build());
        return users;
    }
    
    @Bean
    WebSecurityCustomizer webSecurityCustomizer() {
        return new WebSecurityCustomizer() {
            @Override
            public void customize(WebSecurity web) {
                web.ignoring().antMatchers(&quot;/hello&quot;);
            }
        };
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;以前位于 &lt;code&gt;configure(WebSecurity)&lt;/code&gt; 方法中的内容，现在位于 WebSecurityCustomizer Bean 中，该配置的东西写在这里就可以了。&lt;/p&gt;
&lt;h4&gt;定制登录页面参数等&lt;/h4&gt;
&lt;p&gt;那如果我还希望对登录页面，参数等，进行定制呢？继续往下看：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;@Configuration
public class SecurityConfig {
    @Bean
    UserDetailsService userDetailsService() {
        InMemoryUserDetailsManager users = new InMemoryUserDetailsManager();
        users.createUser(User.withUsername(&quot;javaboy&quot;).password(&quot;{noop}123&quot;).roles(&quot;admin&quot;).build());
        users.createUser(User.withUsername(&quot;江南一点雨&quot;).password(&quot;{noop}123&quot;).roles(&quot;admin&quot;).build());
        return users;
    }
    
    
    @Bean
    SecurityFilterChain securityFilterChain() {
        List&amp;amp;lt;Filter&amp;amp;gt; filters = new ArrayList&amp;amp;lt;&amp;amp;gt;();
        return new DefaultSecurityFilterChain(new AntPathRequestMatcher(&quot;/**&quot;), filters);
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Spring Security 的底层实际上就是一堆过滤器，所以我们之前在 configure(HttpSecurity) 方法中的配置，实际上就是配置过滤器链。现在过滤器链的配置，我们通过提供一个 SecurityFilterChain Bean 来配置过滤器链，SecurityFilterChain 是一个接口，这个接口只有一个实现类 DefaultSecurityFilterChain，构建 DefaultSecurityFilterChain 的第一个参数是拦截规则，也就是哪些路径需要拦截，第二个参数则是过滤器链，这里我给了一个空集合，也就是我们的 Spring Security 会拦截下所有的请求，然后在一个空集合中走一圈就结束了，相当于不拦截任何请求。&lt;/p&gt;
&lt;p&gt;此时重启项目，你会发现 &lt;code&gt;/hello&lt;/code&gt; 也是可以直接访问的，就是因为这个路径不经过任何过滤器。&lt;/p&gt;
&lt;p&gt;其实我觉得目前这中新写法比以前老的写法更直观，更容易让大家理解到 Spring Security 底层的过滤器链工作机制。&lt;/p&gt;
&lt;p&gt;有小伙伴会说，这写法跟我以前写的也不一样呀！这么配置，我也不知道 Spring Security 中有哪些过滤器，其实，换一个写法，我们就可以将这个配置成以前那种样子：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;@Configuration
public class SecurityConfig {
    @Bean
    UserDetailsService userDetailsService() {
        InMemoryUserDetailsManager users = new InMemoryUserDetailsManager();
        users.createUser(User.withUsername(&quot;javaboy&quot;).password(&quot;{noop}123&quot;).roles(&quot;admin&quot;).build());
        users.createUser(User.withUsername(&quot;江南一点雨&quot;).password(&quot;{noop}123&quot;).roles(&quot;admin&quot;).build());
        return users;
    }
    
    @Bean
    SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
        http.authorizeRequests()
                .anyRequest().authenticated()
                .and()
                .formLogin()
                .permitAll()
                .and()
                .csrf().disable();
        return http.build();
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这么写，就跟以前的写法其实没啥大的差别了。&lt;/p&gt;
&lt;p&gt;好啦，多余的废话我就不多说了，小伙伴们可以去试试 Spring Boot2.7 的最新玩法啦～&lt;/p&gt;
&lt;h3&gt;实践配置&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;package com.zy.security01.config;

import com.zy.security01.security.JwtAuthenticationTokenFilter;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.config.annotation.authentication.configuration.AuthenticationConfiguration;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.web.SecurityFilterChain;
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;

import javax.annotation.Resource;

@Configuration
public class SecurityConfig {

    /**
     * 认证配置类（security自带）
     */
    @Resource
    private AuthenticationConfiguration authenticationConfiguration;

    /**
     * 自定义jwttoken过滤器
     */
    @Resource
    private JwtAuthenticationTokenFilter jwtAuthenticationTokenFilter;

    /**
     * 加密解密
     * @return PasswordEncoder
     */
    @Bean
    public PasswordEncoder passwordEncoder() {
        return new BCryptPasswordEncoder();
    }

    /**
     * 认证管理器
     * @return AuthenticationManager
     * @throws Exception 异常
     */
    @Bean
    public AuthenticationManager authenticationManager() throws Exception {
        return authenticationConfiguration.getAuthenticationManager();
    }

    /**
     * 过滤器链
     * @param http HttpSecurity
     * @return SecurityFilterChain
     * @throws Exception 异常
     */
    @Bean
    public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
        return http.csrf().disable()
                // .sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS)
                // .and()
                .authorizeRequests()
            	//必须以斜杠开头
                .antMatchers(&quot;/sysUser/user/login&quot;).anonymous()
                .anyRequest().authenticated()
                .and()
                .addFilterBefore(jwtAuthenticationTokenFilter, UsernamePasswordAuthenticationFilter.class)
                .build();
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;springsecurity怎么放掉swaager2和knife4j的静态资源&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;@Override
public void configure(WebSecurity web) throws Exception {
    web.ignoring().antMatchers(&quot;/swagger/**&quot;)
            .antMatchers(&quot;/swagger-ui.html&quot;)
            .antMatchers(&quot;/webjars/**&quot;)
            .antMatchers(&quot;/v2/**&quot;)
            .antMatchers(&quot;/v2/api-docs-ext/**&quot;)
            .antMatchers(&quot;/swagger-resources/**&quot;)
            .antMatchers(&quot;/doc.html&quot;);

}
&lt;/code&gt;&lt;/pre&gt;
&lt;hr /&gt;
&lt;h2&gt;Spring Security面试题&lt;/h2&gt;
&lt;p&gt;&lt;img src=&quot;https://tangtangit.com/_astro/image-20240401123114171.BtF2FMhu_Z1nh9vQ.webp&quot; alt=&quot;image-20240401123114171&quot; /&gt;&lt;/p&gt;
&lt;h2&gt;&lt;strong&gt;核心概念&lt;/strong&gt;&lt;/h2&gt;
&lt;h3&gt;&lt;strong&gt;spring security是如何完成身份验证的？&lt;/strong&gt;&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;用户名和密码被过滤器获取到，封装成Authentication，通常情况下是UsernamePasswordAuthenticationToken这个实现类。&lt;/li&gt;
&lt;li&gt;AuthenticationManager 身份管理器负责验证这个Authentication&lt;/li&gt;
&lt;li&gt;认证成功后，AuthenticationManager身份管理器返回一个被填充满了信息的（包括上面提到的权限信息，身份信息，细节信息，但密码通常会被移除）Authentication实例。&lt;/li&gt;
&lt;li&gt;SecurityContextHolder安全上下文容器将第3步填充了信息的Authentication，通过SecurityContextHolder.getContext().setAuthentication(…)方法，设置到其中。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;SecurityContextHolder&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;SecurityContextHolder用于存储安全上下文（security context）的信息。当前操作的用户是谁，该用户是否已经被认证，他拥有哪些角色权限，这些都被保存在SecurityContextHolder中。SecurityContextHolder默认使用ThreadLocal 策略来存储认证信息。看到ThreadLocal 也就意味着，这是一种与线程绑定的策略。Spring Security在用户登录时自动绑定认证信息到当前线程，在用户退出时，自动清除当前线程的认证信息。但这一切的前提，是你在web场景下使用Spring Security。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Authentication&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;Authentication在spring security中是最高级别的身份/认证的抽象。由这个顶级接口，我们可以得到用户拥有的权限信息列表，密码，用户细节信息，用户身份信息，认证信息。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;AuthenticationManager&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;AuthenticationManager（接口）是认证相关的核心接口，也是发起认证的出发点&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;AuthenticationProvider&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;认证具体实现&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;UserDetailService&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;负责从特定的地方（通常是数据库）加载用户信息&lt;/p&gt;
&lt;h3&gt;&lt;strong&gt;比较一下 Spring Security 和 Shiro 各自的优缺点 ?&lt;/strong&gt;&lt;/h3&gt;
&lt;p&gt;由于 Spring Boot 官方提供了大量的非常方便的开箱即用的 Starter ，包括 Spring Security 的 Starter ，使得在 Spring Boot 中使用 Spring Security 变得更加容易，甚至只需要添加一个依赖就可以保护所有的接口，所以，如果是 Spring Boot 项目，一般选择 Spring Security 。当然这只是一个建议的组合，单纯从技术上来说，无论怎么组合，都是没有问题的。Shiro 和 Spring Security 相比，主要有如下一些特点：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Spring Security 是一个重量级的安全管理框架；Shiro 则是一个轻量级的安全管理框架&lt;/li&gt;
&lt;li&gt;Spring Security 概念复杂，配置繁琐；Shiro 概念简单、配置简单&lt;/li&gt;
&lt;li&gt;Spring Security 功能强大；Shiro 功能简单&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;1.什么是Spring Security？核心功能？&lt;/h2&gt;
&lt;p&gt;Spring Security是一个基于Spring框架的安全框架，提供了完整的安全解决方案，包括认证、授权、攻击防护等功能。&lt;/p&gt;
&lt;p&gt;其核心功能包括：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;认证&lt;/strong&gt;：提供了多种认证方式，如表单认证、HTTP Basic认证、OAuth2认证等，可以与多种身份验证机制集成。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;授权&lt;/strong&gt;：提供了多种授权方式，如角色授权、基于表达式的授权等，可以对应用程序中的不同资源进行授权。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;攻击防护&lt;/strong&gt;：提供了多种防护机制，如跨站点请求伪造（CSRF）防护、注入攻击防护等。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;会话管理&lt;/strong&gt;：提供了会话管理机制，如令牌管理、并发控制等。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;监视与管理&lt;/strong&gt;：提供了监视与管理机制，如访问日志记录、审计等。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Spring Security通过配置安全规则和过滤器链来实现以上功能，可以轻松地为Spring应用程序提供安全性和保护机制。&lt;/p&gt;
&lt;h2&gt;2.Spring Security的原理?&lt;/h2&gt;
&lt;p&gt;Spring Security是一个基于Spring框架的安全性认证和授权框架，它提供了全面的安全性解决方案，可以保护Web应用程序中的所有关键部分。&lt;/p&gt;
&lt;p&gt;Spring Security的&lt;strong&gt;核心原理是拦截器（Filter）&lt;/strong&gt;。Spring Security会在Web应用程序的过滤器链中添加一组自定义的过滤器，这些过滤器可以实现身份验证和授权功能。当用户请求资源时，Spring Security会拦截请求，并使用配置的身份验证机制来验证用户身份。如果身份验证成功，Spring Security会授权用户访问所请求的资源。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://tangtangit.com/_astro/image-20240401122903035.CvNitfNG_ZDXKHp.webp&quot; alt=&quot;image-20240401122903035&quot; /&gt;&lt;/p&gt;
&lt;p&gt;Spring Security的具体工作原理如下：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;用户请求Web应用程序的受保护资源。&lt;/li&gt;
&lt;li&gt;Spring Security拦截请求，并尝试获取用户的身份验证信息。&lt;/li&gt;
&lt;li&gt;如果用户没有经过身份验证，Spring Security将向用户显示一个登录页面，并要求用户提供有效的凭据（用户名和密码）。&lt;/li&gt;
&lt;li&gt;一旦用户提供了有效的凭据，Spring Security将验证这些凭据，并创建一个已认证的安全上下文（SecurityContext）对象。&lt;/li&gt;
&lt;li&gt;安全上下文对象包含已认证的用户信息，包括用户名、角色和授权信息。&lt;/li&gt;
&lt;li&gt;在接下来的请求中，Spring Security将使用已经认证的安全上下文对象来判断用户是否有权访问受保护的资源。&lt;/li&gt;
&lt;li&gt;如果用户有权访问资源，Spring Security将允许用户访问资源，否则将返回一个错误信息。&lt;/li&gt;
&lt;/ol&gt;
&lt;h2&gt;3.有哪些控制请求访问权限的方法？&lt;/h2&gt;
&lt;p&gt;在Spring Security中，可以使用以下方法来控制请求访问权限：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;permitAll()&lt;/code&gt;：&lt;strong&gt;允许所有&lt;/strong&gt;用户访问该请求，不需要进行任何身份验证。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;denyAll()&lt;/code&gt;：&lt;strong&gt;拒绝所有&lt;/strong&gt;用户访问该请求。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;anonymous()&lt;/code&gt;：&lt;strong&gt;允许匿名&lt;/strong&gt;用户访问该请求。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;authenticated()&lt;/code&gt;：要求用户进行&lt;strong&gt;身份验证&lt;/strong&gt;，但是不要求用户具有任何特定的角色。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;hasRole(String role)&lt;/code&gt;：要求用户具有&lt;strong&gt;特定的角色&lt;/strong&gt;才能访问该请求。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;hasAnyRole(String... roles)&lt;/code&gt;：要求用户具有多个角色中的至少一个角色才能访问该请求。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;hasAuthority(String authority)&lt;/code&gt;：要求用户具有特定的权限才能访问该请求。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;hasAnyAuthority(String... authorities)&lt;/code&gt;：要求用户具有多个权限中的至少一个权限才能访问该请求。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;可以将这些方法应用于Spring Security的配置类或者在Spring Security注解中使用。&lt;/p&gt;
&lt;h2&gt;4.hasRole 和 hasAuthority 有区别吗？&lt;/h2&gt;
&lt;p&gt;在Spring Security中，hasRole和hasAuthority都可以用来控制用户的访问权限，但它们有一些细微的差别。&lt;/p&gt;
&lt;p&gt;hasRole方法是基于角色进行访问控制的。它检查用户是否有指定的角色，并且这些角色以&quot;ROLE_&quot;前缀作为前缀（例如&quot;ROLE_ADMIN&quot;）。&lt;/p&gt;
&lt;p&gt;hasAuthority方法是基于权限进行访问控制的。它检查用户是否有指定的权限，并且这些权限没有前缀。&lt;/p&gt;
&lt;p&gt;因此，使用hasRole方法需要在用户的角色名称前添加&quot;ROLE_&quot;前缀，而使用hasAuthority方法不需要这样做。&lt;/p&gt;
&lt;p&gt;例如，假设用户有一个角色为&quot;ADMIN&quot;和一个权限为&quot;VIEW_REPORTS&quot;，可以使用以下方式控制用户对页面的访问权限：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;.antMatchers(&quot;/admin/**&quot;).hasRole(&quot;ADMIN&quot;)
.antMatchers(&quot;/reports/**&quot;).hasAuthority(&quot;VIEW_REPORTS&quot;)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;在这个例子中，只有具有&quot;ROLE_ADMIN&quot;角色的用户才能访问/admin/路径下的页面，而具有&quot;VIEW_REPORTS&quot;权限的用户才能访问/reports/路径下的页面。&lt;/p&gt;
&lt;h2&gt;5.如何对密码进行加密？&lt;/h2&gt;
&lt;p&gt;在 Spring Security 中对密码进行加密通常使用的是&lt;strong&gt;密码编码器&lt;/strong&gt;（PasswordEncoder）。PasswordEncoder 的作用是将明文密码加密成密文密码，以便于存储和校验。Spring Security 提供了多种常见的密码编码器，例如 BCryptPasswordEncoder、SCryptPasswordEncoder、StandardPasswordEncoder 等。&lt;/p&gt;
&lt;p&gt;以 BCryptPasswordEncoder 为例，使用步骤如下：&lt;/p&gt;
&lt;h6&gt;1.在 pom.xml 文件中添加 BCryptPasswordEncoder 的依赖：&lt;/h6&gt;
&lt;pre&gt;&lt;code&gt;&amp;lt;dependency&amp;gt;
    &amp;lt;groupId&amp;gt;org.springframework.security&amp;lt;/groupId&amp;gt;
    &amp;lt;artifactId&amp;gt;spring-security-crypto&amp;lt;/artifactId&amp;gt;
    &amp;lt;version&amp;gt;5.6.1&amp;lt;/version&amp;gt;
&amp;lt;/dependency&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;2.在 Spring 配置文件中注入 BCryptPasswordEncoder：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;@Configuration
public class SecurityConfig extends WebSecurityConfigurerAdapter {
    
    @Bean
    public PasswordEncoder passwordEncoder() {
        return new BCryptPasswordEncoder();
    }
    
    // ...
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;3.在使用密码的地方调用 &lt;code&gt;passwordEncoder.encode()&lt;/code&gt; 方法对密码进行加密，例如注册时对密码进行加密：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;@Service
public class UserServiceImpl implements UserService {
 
    @Autowired
    private PasswordEncoder passwordEncoder;
 
    @Override
    public User register(User user) {
        String encodedPassword = passwordEncoder.encode(user.getPassword());
        user.setPassword(encodedPassword);
        // ...
        return user;
    }
 
    // ...
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;以上就是使用 BCryptPasswordEncoder 对密码进行加密的步骤。使用其他密码编码器的步骤类似，只需将 BCryptPasswordEncoder 替换为相应的密码编码器即可。&lt;/p&gt;
&lt;h2&gt;6.Spring Security基于用户名和密码的认证模式流程？&lt;/h2&gt;
&lt;p&gt;请求的用户名密码可以通过表单登录，基础认证，数字认证三种方式从HttpServletRequest中获得，用于认证的数据源策略有内存，数据库，ldap,自定义等。&lt;/p&gt;
&lt;p&gt;拦截未授权的请求，重定向到登录页面的过程：&lt;/p&gt;
&lt;p&gt;当用户访问需要授权的资源时，Spring Security会检查用户是否已经认证（即是否已登录），如果没有登录则会重定向到登录页面。&lt;/p&gt;
&lt;p&gt;重定向到登录页面时，用户需要输入用户名和密码进行认证。&lt;/p&gt;
&lt;p&gt;表单登录的过程：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;用户在登录页面输入用户名和密码，提交表单。&lt;/li&gt;
&lt;li&gt;Spring Security的UsernamePasswordAuthenticationFilter拦截表单提交的请求，并将用户名和密码封装成一个Authentication对象。&lt;/li&gt;
&lt;li&gt;AuthenticationManager接收到Authentication对象后，会根据用户名和密码查询用户信息，并将用户信息封装成一个UserDetails对象。&lt;/li&gt;
&lt;li&gt;如果查询到用户信息，则将UserDetails对象封装成一个已认证的Authentication对象并返回，如果查询不到用户信息，则抛出相应的异常。&lt;/li&gt;
&lt;li&gt;认证成功后，用户会被重定向到之前访问的资源。如果之前访问的资源需要特定的角色或权限才能访问，则还需要进行授权的过程。&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;Spring Security的认证流程大致可以分为两个过程，首先是用户登录认证的过程，然后是用户访问受保护资源时的授权过程。在认证过程中，用户需要提供用户名和密码，Spring Security通过UsernamePasswordAuthenticationFilter将用户名和密码封装成Authentication对象，并交由AuthenticationManager进行认证。&lt;/p&gt;
&lt;p&gt;如果认证成功，则认证结果会存储在SecurityContextHolder中。在授权过程中，Spring Security会检查用户是否有访问受保护资源的权限，如果没有则会重定向到登录页面进行认证。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;授权&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;拦截未授权的请求，重定向到登录页面&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://tangtangit.com/_astro/640-17119457221271.2sbs1cjq_29vu6n.webp&quot; alt=&quot;img&quot; /&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;认证&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;表单登录的过程，进行账号密码认证&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://tangtangit.com/_astro/image-20240401124159423.DUfr0cww_1g5Sde.webp&quot; alt=&quot;image-20240401124159423&quot; /&gt;&lt;/p&gt;
&lt;hr /&gt;
&lt;h2&gt;SpringCloud Security（上）&lt;/h2&gt;
&lt;h2&gt;spring-cloud-security&lt;/h2&gt;
&lt;h2&gt;是什么：&lt;/h2&gt;
&lt;blockquote&gt;
&lt;p&gt;Spring Cloud Security提供了一组原语，用于构建安全的应用程序和服务，而且操作简便。可以在外部（或集中）进行大量配置的声明性模型有助于实现大型协作的远程组件系统，通常具有中央身份管理服务。它也非常易于在Cloud Foundry等服务平台中使用。&lt;strong&gt;在Spring Boot和Spring Security OAuth2的基础上&lt;/strong&gt;，可以快速创建实现常见模式的系统，如单点登录，令牌中继和令牌交换。&lt;/p&gt;
&lt;p&gt;它是基于spring-security之上的产物，对应的它的功能更强大&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;&lt;strong&gt;功能：&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;安全的认证、授权&lt;/li&gt;
&lt;/ul&gt;
&lt;ul&gt;
&lt;li&gt;从Zuul代理中的前端到后端服务中继SSO令牌&lt;/li&gt;
&lt;li&gt;资源服务器之间的中继令牌&lt;/li&gt;
&lt;li&gt;使Feign客户端表现得像&lt;code&gt;OAuth2RestTemplate&lt;/code&gt;（获取令牌等）的拦截器&lt;/li&gt;
&lt;li&gt;在Zuul代理中配置下游身份验证&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;cloud版本控制流程图：&lt;/h2&gt;
&lt;p&gt;&lt;img src=&quot;cloud%E7%89%88%E6%9C%AC%E6%8E%A7%E5%88%B6%E6%B5%81%E7%A8%8B%E5%9B%BE.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;注：&lt;/strong&gt; spring-cloud-starter-oauth 也是由security-dependencies管理&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;        &amp;lt;dependency&amp;gt;
            &amp;lt;groupId&amp;gt;org.springframework.cloud&amp;lt;/groupId&amp;gt;
            &amp;lt;artifactId&amp;gt;spring-cloud-starter-oauth2&amp;lt;/artifactId&amp;gt;
        &amp;lt;/dependency&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;spring social 依赖：&lt;/p&gt;
&lt;p&gt;官方文档显示需要如下一下：&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Name&lt;/th&gt;
&lt;th&gt;Description&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;spring-social-core&lt;/td&gt;
&lt;td&gt;Spring Social’s Connect Framework and OAuth client support.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;spring-social-config&lt;/td&gt;
&lt;td&gt;Java and XML configuration support for Spring Social.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;spring-social-security&lt;/td&gt;
&lt;td&gt;Spring Security integration support.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;spring-social-web&lt;/td&gt;
&lt;td&gt;Spring Social’s ConnectController which uses the Connect Framework to manage connections in a web application environment.1&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;h2&gt;怎么做？&lt;/h2&gt;
&lt;p&gt;一：引入依赖&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;    &amp;lt;dependencyManagement&amp;gt;
        &amp;lt;dependencies&amp;gt;
            &amp;lt;dependency&amp;gt;
                &amp;lt;groupId&amp;gt;org.springframework.cloud&amp;lt;/groupId&amp;gt;
                &amp;lt;artifactId&amp;gt;spring-cloud-dependencies&amp;lt;/artifactId&amp;gt;
                &amp;lt;version&amp;gt;Greenwich.SR2&amp;lt;/version&amp;gt;
                &amp;lt;type&amp;gt;pom&amp;lt;/type&amp;gt;
                &amp;lt;scope&amp;gt;import&amp;lt;/scope&amp;gt;
            &amp;lt;/dependency&amp;gt;
        &amp;lt;/dependencies&amp;gt;
    &amp;lt;/dependencyManagement&amp;gt;

    &amp;lt;dependencies&amp;gt;
        &amp;lt;dependency&amp;gt;
            &amp;lt;groupId&amp;gt;org.springframework.cloud&amp;lt;/groupId&amp;gt;
            &amp;lt;artifactId&amp;gt;spring-cloud-starter-oauth2&amp;lt;/artifactId&amp;gt;
        &amp;lt;/dependency&amp;gt;
	&amp;lt;/dependencies&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&amp;lt;!--完整配置--&amp;gt;：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&amp;lt;?xml version=&quot;1.0&quot; encoding=&quot;UTF-8&quot;?&amp;gt;
&amp;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;&amp;gt;
    &amp;lt;modelVersion&amp;gt;4.0.0&amp;lt;/modelVersion&amp;gt;

    &amp;lt;parent&amp;gt;
        &amp;lt;groupId&amp;gt;org.springframework.boot&amp;lt;/groupId&amp;gt;
        &amp;lt;artifactId&amp;gt;spring-boot-starter-parent&amp;lt;/artifactId&amp;gt;
        &amp;lt;version&amp;gt;2.1.6.RELEASE&amp;lt;/version&amp;gt;
        &amp;lt;relativePath/&amp;gt; &amp;lt;!-- lookup parent from repository --&amp;gt;
    &amp;lt;/parent&amp;gt;

    &amp;lt;groupId&amp;gt;com.eim&amp;lt;/groupId&amp;gt;
    &amp;lt;artifactId&amp;gt;my_security&amp;lt;/artifactId&amp;gt;
    &amp;lt;packaging&amp;gt;pom&amp;lt;/packaging&amp;gt;
    &amp;lt;version&amp;gt;1.0-SNAPSHOT&amp;lt;/version&amp;gt;
    &amp;lt;modules&amp;gt;
        &amp;lt;module&amp;gt;core&amp;lt;/module&amp;gt;
        &amp;lt;module&amp;gt;brower&amp;lt;/module&amp;gt;
        &amp;lt;module&amp;gt;demo&amp;lt;/module&amp;gt;
    &amp;lt;/modules&amp;gt;

    &amp;lt;dependencyManagement&amp;gt;
        &amp;lt;dependencies&amp;gt;
            &amp;lt;dependency&amp;gt;
                &amp;lt;groupId&amp;gt;org.springframework.cloud&amp;lt;/groupId&amp;gt;
                &amp;lt;artifactId&amp;gt;spring-cloud-dependencies&amp;lt;/artifactId&amp;gt;
                &amp;lt;version&amp;gt;Greenwich.SR2&amp;lt;/version&amp;gt;
                &amp;lt;type&amp;gt;pom&amp;lt;/type&amp;gt;
                &amp;lt;scope&amp;gt;import&amp;lt;/scope&amp;gt;
            &amp;lt;/dependency&amp;gt;
        &amp;lt;/dependencies&amp;gt;
    &amp;lt;/dependencyManagement&amp;gt;

    &amp;lt;dependencies&amp;gt;
        &amp;lt;dependency&amp;gt;
            &amp;lt;groupId&amp;gt;org.springframework.cloud&amp;lt;/groupId&amp;gt;
            &amp;lt;artifactId&amp;gt;spring-cloud-starter-oauth2&amp;lt;/artifactId&amp;gt;
        &amp;lt;/dependency&amp;gt;
        &amp;lt;dependency&amp;gt;
            &amp;lt;groupId&amp;gt;org.springframework.boot&amp;lt;/groupId&amp;gt;
            &amp;lt;artifactId&amp;gt;spring-boot-starter-web&amp;lt;/artifactId&amp;gt;
        &amp;lt;/dependency&amp;gt;
        &amp;lt;dependency&amp;gt;
            &amp;lt;groupId&amp;gt;org.projectlombok&amp;lt;/groupId&amp;gt;
            &amp;lt;artifactId&amp;gt;lombok&amp;lt;/artifactId&amp;gt;
        &amp;lt;/dependency&amp;gt;
        &amp;lt;dependency&amp;gt;
            &amp;lt;groupId&amp;gt;mysql&amp;lt;/groupId&amp;gt;
            &amp;lt;artifactId&amp;gt;mysql-connector-java&amp;lt;/artifactId&amp;gt;
        &amp;lt;/dependency&amp;gt;
        &amp;lt;!--    social start    --&amp;gt;
        &amp;lt;dependency&amp;gt;
            &amp;lt;groupId&amp;gt;org.springframework.social&amp;lt;/groupId&amp;gt;
            &amp;lt;artifactId&amp;gt;spring-social-config&amp;lt;/artifactId&amp;gt;
            &amp;lt;version&amp;gt;1.1.6.RELEASE&amp;lt;/version&amp;gt;
        &amp;lt;/dependency&amp;gt;
        &amp;lt;!--    提供社交连接框架和OAuth 客户端支持    --&amp;gt;
        &amp;lt;dependency&amp;gt;
            &amp;lt;groupId&amp;gt;org.springframework.social&amp;lt;/groupId&amp;gt;
            &amp;lt;artifactId&amp;gt;spring-social-core&amp;lt;/artifactId&amp;gt;
            &amp;lt;version&amp;gt;1.1.6.RELEASE&amp;lt;/version&amp;gt;
        &amp;lt;/dependency&amp;gt;
        &amp;lt;!--   社交安全的一些支持   --&amp;gt;
        &amp;lt;dependency&amp;gt;
            &amp;lt;groupId&amp;gt;org.springframework.social&amp;lt;/groupId&amp;gt;
            &amp;lt;artifactId&amp;gt;spring-social-security&amp;lt;/artifactId&amp;gt;
            &amp;lt;version&amp;gt;1.1.6.RELEASE&amp;lt;/version&amp;gt;
        &amp;lt;/dependency&amp;gt;
		&amp;lt;!-- 管理web应用程序的连接 --&amp;gt;
        &amp;lt;dependency&amp;gt;
            &amp;lt;groupId&amp;gt;org.springframework.social&amp;lt;/groupId&amp;gt;
            &amp;lt;artifactId&amp;gt;spring-social-web&amp;lt;/artifactId&amp;gt;
            &amp;lt;version&amp;gt;1.1.6.RELEASE&amp;lt;/version&amp;gt;
        &amp;lt;/dependency&amp;gt;
      &amp;lt;!--    social end    --&amp;gt;
    &amp;lt;/dependencies&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;引入依赖后启动项目，springsecurity会自动配置安全，任何请求都会要求登录，且默认登录为表单登录方式（spirngboot2.0开始）。&lt;/p&gt;
&lt;p&gt;第一个类：webSecurity&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;/**
 * &amp;lt;p&amp;gt;@Description:web安全配置&amp;lt;/p&amp;gt;
 * &amp;lt;p&amp;gt;@Author: zhengyong&amp;lt;/p&amp;gt;
 * &amp;lt;p&amp;gt;@Date: 2019/7/20 11:10&amp;lt;/p&amp;gt;
 * &amp;lt;p&amp;gt;@Version: 1.0.0&amp;lt;/p&amp;gt;
 **/
@Component
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http
          		//表单登录
                .formLogin()
          		//basic登录
//                .httpBasic()
          		//认证请求+所有请求+鉴定---》认证鉴定所有请求
                .and().authorizeRequests().anyRequest().authenticated()
                ;
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;spring-security基本原理：&lt;/h2&gt;
&lt;p&gt;&lt;img src=&quot;security%E5%9F%BA%E6%9C%AC%E5%8E%9F%E7%90%86.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;UsernamePasswordAuthenticationFilter
//表单登录过滤器
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;BasicAuthenticationFilter
//httpbacis登录过滤器
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;FilterSecurityInterceptor
//过滤器安全拦截器---》守门员
&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote&gt;
&lt;p&gt;只有绿色的过滤器可以控制，其他的是无法进行控制的。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2&gt;自定义用户认证逻辑&lt;/h2&gt;
&lt;h3&gt;一：处理用户信息获取逻辑&lt;/h3&gt;
&lt;p&gt;封装在一个UserDetailsService接口来获取用户信息的：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;public interface UserDetailsService {
	// ~ Methods
	// **=****=****=****=****=****=****=****=****=****=****=****=****=****=****=****=****=****=****=****=**====

	/**
	 * Locates the user based on the username. In the actual implementation, the search
	 * may possibly be case sensitive, or case insensitive depending on how the
	 * implementation instance is configured. In this case, the &amp;lt;code&amp;gt;UserDetails&amp;lt;/code&amp;gt;
	 * object that comes back may have a username that is of a different case than what
	 * was actually requested..
	 *
	 * @param username the username identifying the user whose data is required.
	 *
	 * @return a fully populated user record (never &amp;lt;code&amp;gt;null&amp;lt;/code&amp;gt;)
	 *
	 * @throws UsernameNotFoundException if the user could not be found or the user has no
	 * GrantedAuthority
	 */
 	 //获取用户名字、返回一个用户信息（封装在了UserDetails中）--》最终存入session中
	UserDetails loadUserByUsername(String username) throws UsernameNotFoundException;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;返回的UserDetails是一个接口，User（security.core.userdetails包下）的类实现了这个接口&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;public class User implements UserDetails, CredentialsContainer {

	private static final long serialVersionUID = SpringSecurityCoreVersion.SERIAL_VERSION_UID;

	private static final Log logger = LogFactory.getLog(User.class);

	// ~ Instance fields
	// **=****=****=****=****=****=****=****=****=****=****=****=****=****=****=****=****=****=****=**=
	private String password;//密码
	private final String username;//用户名
	private final Set&amp;lt;GrantedAuthority&amp;gt; authorities;//授权的权限集合
	private final boolean accountNonExpired;//账号没过期
	private final boolean accountNonLocked;//账号没被锁
	private final boolean credentialsNonExpired;//认证没有过去
	private final boolean enabled;//是可用
  /**
   *四个Boolean值是默认为true的，有一个返回为false则视为校验失败
   *
   /
}  
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;eg:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;@Component
public class UserServiceImpl implements UserDetailsService {
	//加密类，新版本必须制定一个
    @Bean
    public PasswordEncoder passwordEncoder(){
        return PasswordEncoderFactories.createDelegatingPasswordEncoder();
    }

	//重写的方法，返回UserDetails的实现类（这么默认使用security自带的user类，可以自定义）
    @Override
    public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
        return new User(username,passwordEncoder().encode(&quot;123&quot;), AuthorityUtils.createAuthorityList(&quot;admin&quot;));
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;二：处理用户校验逻辑&lt;/h3&gt;
&lt;p&gt;用户的校验逻辑封装在UserDetails这个接口中，可以选择自己实现这个接口，也可以使用自带的User类&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;@Component
public class UserServiceImpl implements UserDetailsService {

    @Resource
    private ClientUserDAO clientUserDAO;

    @Bean
    public PasswordEncoder passwordEncoder(){
        return new BCryptPasswordEncoder();
    }


    @Override
    public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
        QueryWrapper&amp;lt;ClientUser&amp;gt; wrapper = new QueryWrapper&amp;lt;&amp;gt;();
        wrapper.eq(&quot;username&quot;,username);
        ClientUser clientUser = clientUserDAO.selectOne(wrapper);
        if(clientUser == null){
            throw new UsernameNotFoundException(&quot;用户不存在&quot;);
        }
        return new User(username,clientUser.getPassword(),
                //设置用户的其他信息
                true,true,true,true
                ,AuthorityUtils.createAuthorityList(&quot;admin&quot;));
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;三：处理密码加密解密&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;PasswordEncoder
//处理密码的加密和解密的接口,现有版本是必须制定一个加密的方式，不然会报错
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;个性化用户认证流程&lt;/h2&gt;
&lt;h3&gt;一：自定义登录页面&lt;/h3&gt;
&lt;p&gt;1）、新建一个html登录页面&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&amp;lt;!DOCTYPE html&amp;gt;
&amp;lt;html lang=&quot;en&quot;&amp;gt;
&amp;lt;head&amp;gt;
    &amp;lt;meta charset=&quot;UTF-8&quot;&amp;gt;
    &amp;lt;title&amp;gt;自定义登录页面&amp;lt;/title&amp;gt;
&amp;lt;/head&amp;gt;
&amp;lt;body&amp;gt;
&amp;lt;h2&amp;gt;标准登录页面&amp;lt;/h2&amp;gt;
&amp;lt;form action=&quot;/form&quot; method=&quot;post&quot;&amp;gt;
    &amp;lt;input type=&quot;text&quot; name=&quot;username&quot;&amp;gt;
    &amp;lt;input type=&quot;password&quot; name=&quot;password&quot;&amp;gt;
    &amp;lt;button type=&quot;submit&quot;&amp;gt;登录&amp;lt;/button&amp;gt;
&amp;lt;/form&amp;gt;
&amp;lt;/body&amp;gt;
&amp;lt;/html&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;2）、配置自定义登录页面&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;/**
 * &amp;lt;p&amp;gt;@Description:web安全配置&amp;lt;/p&amp;gt;
 * &amp;lt;p&amp;gt;@Author: zhengyong&amp;lt;/p&amp;gt;
 * &amp;lt;p&amp;gt;@Date: 2019/7/20 11:10&amp;lt;/p&amp;gt;
 * &amp;lt;p&amp;gt;@Version: 1.0.0&amp;lt;/p&amp;gt;
 **/
@Component
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http
             //springsecurity配置了csrf()，csrf会拦截我所有的post请求，这涉及到csrf攻击.。暂时关闭
                .csrf().disable()
                .formLogin()
                //自定义登录页面
                .loginPage(&quot;/eim-login.html&quot;)
                //自定义表单登录请求地址（让security知道何时使用UserNamePasswordFilter）
                .loginProcessingUrl(&quot;/form&quot;)
                .and()
                .authorizeRequests()
                //放行自定义登录页
                .antMatchers(&quot;/eim-login.html&quot;).permitAll()
                .anyRequest()
                .authenticated()
                ;
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;根据不同的请求方式，做不同的响应：&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;%E5%A4%84%E7%90%86%E4%B8%8D%E5%90%8C%E7%B1%BB%E5%9E%8B%E7%9A%84%E8%AF%B7%E6%B1%82.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;是html请求则跳转到登录页面&lt;/li&gt;
&lt;li&gt;是请求则返回401和状态码&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;①、修改http自定义登录页面的url，.loginPage&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;/**
 * &amp;lt;p&amp;gt;@Description:web安全配置&amp;lt;/p&amp;gt;
 * &amp;lt;p&amp;gt;@Author: zhengyong&amp;lt;/p&amp;gt;
 * &amp;lt;p&amp;gt;@Date: 2019/7/20 11:10&amp;lt;/p&amp;gt;
 * &amp;lt;p&amp;gt;@Version: 1.0.0&amp;lt;/p&amp;gt;
 **/
@Component
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http
                //springsecurity配置了csrf()，csrf会拦截我所有的post请求，这涉及到csrf攻击.。暂时关闭
                .csrf().disable()
                .formLogin()
                //自定义登录页面
                .loginPage(&quot;/authentication/require&quot;)
                //自定义表单登录请求地址（让security知道何时使用UserNamePasswordFilter）
                .loginProcessingUrl(&quot;/form&quot;)
                .and()
                .authorizeRequests()
                //放行自定义登录页
                .antMatchers(&quot;/authentication/require&quot;).permitAll()
                .anyRequest()
                .authenticated()
                ;
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;②、自定义返回内容&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;/**
 * &amp;lt;p&amp;gt;@Description:浏览安全跳转控制器&amp;lt;/p&amp;gt;
 * &amp;lt;p&amp;gt;@Author: zhengyong&amp;lt;/p&amp;gt;
 * &amp;lt;p&amp;gt;@Date: 2019/7/22 18:18&amp;lt;/p&amp;gt;
 * &amp;lt;p&amp;gt;@Version: 1.0.0&amp;lt;/p&amp;gt;
 **/
@RestController
public class BrowserSecurityController {

    //可以拿到上一次请求中的数据
    private RequestCache requestCache = new HttpSessionRequestCache();

    //重定向的工具类
    private RedirectStrategy redirectStrategy = new DefaultRedirectStrategy();

    /**
     * 当需要身份认证时，跳转到这里
     *
     * @param request  请求
     * @param response 响应
     * @return         状态码或者登录页面
     */
    @ResponseStatus(code = HttpStatus.UNAUTHORIZED)
    @RequestMapping(&quot;/authentication/require&quot;)
    public ResponseEntity&amp;lt;String&amp;gt; requireAuthentication(HttpServletRequest request, HttpServletResponse response) throws IOException {
        //获取上一次请求
        SavedRequest savedRequest = requestCache.getRequest(request,response);
        if(savedRequest != null){
            //获取到引发跳转的URL
            String targetUrl = savedRequest.getRedirectUrl();
            if(StringUtils.endsWithIgnoreCase(targetUrl,&quot;.html&quot;)){
                //是html跳转到登录页
                redirectStrategy.sendRedirect(request,response,&quot;可以自己配置&quot;);
            }
        }
        return ResponseEntity.ok(&quot;请引导用户到登录页面&quot;);
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;二：自定义登录成功处理&lt;/h3&gt;
&lt;p&gt;自定义登录成功的配置抽象类：AbstractAuthenticationTargetUrlRequestHandler（顶层）&lt;/p&gt;
&lt;p&gt;继承这个类的子类SavedRequestAwareAuthenticationSuccessHandler，并实现方法&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;package com.eim.demo.config;

import com.eim.demo.properties.MySecurityProperties;
import com.eim.demo.properties.ResponseTypeEnum;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.springframework.http.MediaType;
import org.springframework.security.core.Authentication;
import org.springframework.security.web.authentication.SavedRequestAwareAuthenticationSuccessHandler;
import org.springframework.stereotype.Component;

import javax.annotation.Resource;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;

/**
 * &amp;lt;p&amp;gt;DESC: 授权成功处理器&amp;lt;/p&amp;gt;
 * &amp;lt;p&amp;gt;DATE: 2019-07-25 15:37&amp;lt;/p&amp;gt;
 * &amp;lt;p&amp;gt;VERSION:2.0.0&amp;lt;/p&amp;gt;
 * &amp;lt;p&amp;gt;@AUTHOR: ZhengYong&amp;lt;/p&amp;gt;
 */
@Component
public class MyAuthenticationSuccessHandler extends SavedRequestAwareAuthenticationSuccessHandler {

    private final ObjectMapper objectMapper;

    public MyAuthenticationSuccessHandler(ObjectMapper objectMapper) {
        this.objectMapper = objectMapper;
    }

  	//自定义配置类
    @Resource
    private MySecurityProperties mySecurityProperties;

    @Override
    public void onAuthenticationSuccess(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, Authentication authentication) throws IOException, ServletException {
      	//判断是返回json还是默认跳转到上请求
        if(ResponseTypeEnum.JSON.equals(mySecurityProperties.getBrowser().getType())){
            String valueAsString = objectMapper.writeValueAsString(authentication);
            httpServletResponse.setContentType(MediaType.APPLICATION_JSON_UTF8_VALUE);
            httpServletResponse.getWriter().write(valueAsString);
        }
        super.onAuthenticationSuccess(httpServletRequest,httpServletResponse,authentication);
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;三：自定义登录失败处理&lt;/h3&gt;
&lt;p&gt;自定义登录失败抽象类：SimpleUrlAuthenticationFailureHandler（顶层）&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;package com.eim.demo.config;

import com.eim.demo.properties.MySecurityProperties;
import com.eim.demo.properties.ResponseTypeEnum;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.springframework.http.HttpStatus;
import org.springframework.http.MediaType;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.web.authentication.SimpleUrlAuthenticationFailureHandler;
import org.springframework.stereotype.Component;

import javax.annotation.Resource;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;

/**
 * &amp;lt;p&amp;gt;DESC: 自定义认证失败&amp;lt;/p&amp;gt;
 * &amp;lt;p&amp;gt;DATE: 2019-07-25 16:27&amp;lt;/p&amp;gt;
 * &amp;lt;p&amp;gt;VERSION:2.0.0&amp;lt;/p&amp;gt;
 * &amp;lt;p&amp;gt;@AUTHOR: ZhengYong&amp;lt;/p&amp;gt;
 */
@Component
public class MyAuthenticationFailureHandler extends SimpleUrlAuthenticationFailureHandler {

    @Resource
    private ObjectMapper objectMapper;

  	//配置类
    @Resource
    private MySecurityProperties mySecurityProperties;

  	//AuthenticationException 返回异常信息
    @Override
    public void onAuthenticationFailure(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, AuthenticationException e) throws IOException, ServletException {
      	//判断返回json,还是默认跳转
        if(ResponseTypeEnum.JSON.equals(mySecurityProperties.getBrowser().getType())){
            String value = objectMapper.writeValueAsString(e);
            httpServletResponse.setStatus(HttpStatus.UNAUTHORIZED.value());
            httpServletResponse.setContentType(MediaType.APPLICATION_JSON_UTF8_VALUE);
            httpServletResponse.getWriter().write(value);
        }
        super.onAuthenticationFailure(httpServletRequest,httpServletResponse,e);
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;四：生效自定义登录成功、失败&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;/**
 * &amp;lt;p&amp;gt;DESC: web安全配置&amp;lt;/p&amp;gt;
 * &amp;lt;p&amp;gt;DATE: 2019-07-24 16:47&amp;lt;/p&amp;gt;
 * &amp;lt;p&amp;gt;VERSION:2.0.0&amp;lt;/p&amp;gt;
 * &amp;lt;p&amp;gt;@AUTHOR: ZhengYong&amp;lt;/p&amp;gt;
 */
@Component
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {

    @Resource
    private MySecurityProperties securityProperties;

  	//注入自定成功处理器
    @Resource
    private MyAuthenticationSuccessHandler myAuthenticationSuccessHandler;
	//注入自定失败处理器
    @Resource
    private MyAuthenticationFailureHandler myAuthenticationFailureHandler;

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http.formLogin()
                .loginPage(&quot;/authentication/require&quot;)
                //form表单登录请求
                .loginProcessingUrl(&quot;/authentication/login&quot;)
                //自定义成功处理器
                .successHandler(myAuthenticationSuccessHandler)
                //自定义失败处理器
                .failureHandler(myAuthenticationFailureHandler)
                .and().authorizeRequests()
                .antMatchers(&quot;/authentication/require&quot;,securityProperties.getBrowser().getLoginPage()).permitAll()
                .anyRequest().authenticated()
                .and()
                .csrf().disable();
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;个性化用户认证流程（自定义过滤器链）&lt;/h2&gt;
&lt;h3&gt;一、设置打印日志&lt;/h3&gt;
&lt;p&gt;设置security打印日志，方便查看&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;logging:
  level:
    org:
      springframework:
        security: DEBUG
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;@EnableWebSecurity(debug = true)//开启debug
@Configuration
public class SecurityConfig extends WebSecurityConfigurerAdapter
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;二、配置自定义过滤链&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;package com.imooc.uaa.config;

import com.fasterxml.jackson.databind.ObjectMapper;
import com.imooc.uaa.security.filter.RestAuthenticationFilter;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.boot.autoconfigure.security.servlet.PathRequest;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.http.HttpStatus;
import org.springframework.http.MediaType;
import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.builders.WebSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.web.authentication.AuthenticationFailureHandler;
import org.springframework.security.web.authentication.AuthenticationSuccessHandler;
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;
import org.springframework.security.web.authentication.logout.LogoutSuccessHandler;

import java.util.HashMap;

@Slf4j
@RequiredArgsConstructor
@EnableWebSecurity(debug = true)
@Configuration
public class SecurityConfig extends WebSecurityConfigurerAdapter {
    private final ObjectMapper objectMapper;
    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http
            //禁用csrf攻击
            .csrf(csrf -&amp;gt; csrf.disable())
            .authorizeRequests(authorizeRequests -&amp;gt; authorizeRequests
                .antMatchers(&quot;/authorize/**&quot;).permitAll()
                .antMatchers(&quot;/admin/**&quot;).hasRole(&quot;ADMIN&quot;)
                .antMatchers(&quot;/api/**&quot;).hasRole(&quot;USER&quot;)
                .anyRequest().authenticated())
            /**
             * addFilterAt 在什么位置设置过滤器
             * 在指定的筛选器类的位置添加筛选器。 
             * 例如，如果你想要CustomFilter被注册在UsernamePasswordAuthenticationFilter的相同位置，你可以调用:  
             * UsernamePasswordAuthenticationFilter.class addFilterAt(新CustomFilter ())  
             */
            .addFilterAt(restAuthenticationFilter(), UsernamePasswordAuthenticationFilter.class);
    }

    //自定义认证过滤器
    private RestAuthenticationFilter restAuthenticationFilter() throws Exception {
        RestAuthenticationFilter filter = new RestAuthenticationFilter(objectMapper);
        //设置过滤器链
        filter.setAuthenticationSuccessHandler(jsonLoginSuccessHandler());
        filter.setAuthenticationFailureHandler(jsonLoginFailureHandler());
        //认证管理器 系统默认
        filter.setAuthenticationManager(authenticationManager());
        //该过滤器链只处理此路径下的请求
        filter.setFilterProcessesUrl(&quot;/authorize/login&quot;);
        return filter;
    }

    //登出成功处理器
    private LogoutSuccessHandler jsonLogoutSuccessHandler() {
        return (req, res, auth) -&amp;gt; {
            if (auth != null &amp;amp;&amp;amp; auth.getDetails() != null) {
                req.getSession().invalidate();
            }
            res.setStatus(HttpStatus.OK.value());
            res.getWriter().println();
            log.debug(&quot;成功退出登录&quot;);
        };
    }
    //登录成功处理器
    private AuthenticationSuccessHandler jsonLoginSuccessHandler() {
        return (req, res, auth) -&amp;gt; {
            ObjectMapper objectMapper = new ObjectMapper();
            res.setStatus(HttpStatus.OK.value());
            res.getWriter().println(objectMapper.writeValueAsString(auth));
            log.debug(&quot;认证成功&quot;);
        };
    }
    //登录失败处理器
    private AuthenticationFailureHandler jsonLoginFailureHandler() {
        return (req, res, exp) -&amp;gt; {
            res.setStatus(HttpStatus.UNAUTHORIZED.value());
            res.setContentType(MediaType.APPLICATION_JSON_VALUE);
            res.setCharacterEncoding(&quot;UTF-8&quot;);
            HashMap&amp;lt;String, Object&amp;gt; errData = new HashMap&amp;lt;&amp;gt;();
            errData.put(&quot;title&quot;, &quot;认证失败&quot;);
            errData.put(&quot;details&quot;, exp.getMessage());
            res.getWriter().println(objectMapper.writeValueAsString(errData));
        };
    }

    //配置放行路径
    @Override
    public void configure(WebSecurity web) throws Exception {
        web
            .ignoring()
            .antMatchers(&quot;/public/**&quot;)
            .requestMatchers(PathRequest.toStaticResources().atCommonLocations());
    }

    //固定配置用户密码
    @Override
    protected void configure(AuthenticationManagerBuilder auth) throws Exception {
        auth.inMemoryAuthentication()
            .withUser(&quot;user&quot;)
            .password(passwordEncoder().encode(&quot;12345678&quot;))
            .roles(&quot;USER&quot;, &quot;ADMIN&quot;);
    }

    //设置密码器
    @Bean
    public PasswordEncoder passwordEncoder() {
        return new BCryptPasswordEncoder();
    }

}
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;spring-security源码流程详解&lt;/h2&gt;
&lt;h3&gt;一：认证处理流程说明&lt;/h3&gt;
&lt;p&gt;&lt;img src=&quot;%E8%AE%A4%E8%AF%81%E6%B5%81%E7%A8%8B.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;&lt;code&gt;AuthenticationManager&lt;/code&gt;用于管理 &lt;code&gt;AuthenticationProvider&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;AuthenticationManager拿到所有的AuthenticationProvider,一个一个取出来，询问当前provider是否支持当前认证方式（传入的Authentication），&lt;/p&gt;
&lt;p&gt;支持则进入认证，列如表单登录，最终会调用UserDetailsService接口实现类中loadUserByUserName方法(和我们自定义的认证逻辑接上了),&lt;/p&gt;
&lt;p&gt;如果所有都成功，会创建一个SuccessAuthentication，此时会反向传递到起始位置，如果认证成功，调用我们自己的成功处理器，如果认证失败，调用我们自己失败处理器。&lt;/p&gt;
&lt;h3&gt;二：认证结果如何在多个请求之间共享&lt;/h3&gt;
&lt;p&gt;&lt;img src=&quot;%E8%AF%B7%E6%B1%82%E4%B9%8B%E9%97%B4%E7%9A%84%E5%85%B1%E4%BA%AB.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;secutityContext和SeCurityHolder,&lt;/p&gt;
&lt;p&gt;将我们认证成功的Authentication放在SecurityContext和SecurityContextHolder&lt;/p&gt;
&lt;p&gt;SecurityContextHolder中有静态方法，可以将同线程中存入认证信息，和取出认证信息&lt;/p&gt;
&lt;h3&gt;三：获取用户信息&lt;/h3&gt;
&lt;p&gt;&lt;img src=&quot;%E6%9C%80%E5%90%8E%E8%BF%87%E6%BB%A4%E5%99%A8.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;securityContextPersistenceFilter&lt;/p&gt;
&lt;p&gt;作用：&lt;/p&gt;
&lt;p&gt;①：请求进来的时候，检查session中是否有SecurityContext,如果有，就把securityContext拿出来，放入线程中&lt;/p&gt;
&lt;p&gt;②：最后响应前，检查线程中是否是有securityContext,如果有，就拿出来，放入session中&lt;/p&gt;
&lt;p&gt;因为，是在一个线程中完成的，所以可以任意位置通过SecurityContext中获取用户信息&lt;/p&gt;
&lt;p&gt;获取用户信息eg(三种):&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;    @GetMapping(&quot;me&quot;)
    public Object me(){
        return SecurityContextHolder.getContext().getAuthentication();
    }
    @GetMapping(&quot;mi&quot;)
    public Object mi(Authentication authentication){
        return authentication;
    }
    @GetMapping(&quot;mp&quot;)
    public Object mp(@AuthenticationPrincipal UserDetails user){
        return user;
    }
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;实现图片验证码&lt;/h2&gt;
&lt;h3&gt;一：开发生成图形验证码接口&lt;/h3&gt;
&lt;h4&gt;1）：生成图形验证码&lt;/h4&gt;
&lt;p&gt;​	 根据随机数生成验图片&lt;/p&gt;
&lt;p&gt;​	将随机数存入session中&lt;/p&gt;
&lt;p&gt;​	再将生成的图片写到接口的响应中&lt;/p&gt;
&lt;p&gt;a、新建图片对象&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;package com.eim.demo.validate;

import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;

import java.awt.image.BufferedImage;
import java.time.LocalDateTime;

/**
 * &amp;lt;p&amp;gt;@Description:图片验证码对象&amp;lt;/p&amp;gt;
 * &amp;lt;p&amp;gt;@Author: zhengyong&amp;lt;/p&amp;gt;
 * &amp;lt;p&amp;gt;@Date: 2019/7/28 22:50&amp;lt;/p&amp;gt;
 * &amp;lt;p&amp;gt;@Version: 1.0.0&amp;lt;/p&amp;gt;
 **/
@AllArgsConstructor
@NoArgsConstructor
@Data
public class ImageCode {
    /**
     * 图片对象
     */
    private BufferedImage image;
    /**
     * 验证码
     */
    private String code;
    /**
     * 过期时间
     */
    private LocalDateTime expireTime;

    public ImageCode(BufferedImage image, String code, int expireTime) {
        this.image = image;
        this.code = code;
        this.expireTime = LocalDateTime.now().plusSeconds(expireTime);
    }
}

&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;b、新建验证码controller&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;package com.eim.demo.controller;

import com.eim.demo.validate.ImageCode;
import org.springframework.security.web.authentication.session.SessionAuthenticationStrategy;
import org.springframework.social.connect.web.HttpSessionSessionStrategy;
import org.springframework.social.connect.web.SessionStrategy;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.context.request.ServletRequestAttributes;
import org.springframework.web.context.request.ServletWebRequest;

import javax.imageio.ImageIO;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.awt.*;
import java.awt.image.BufferedImage;
import java.io.FileOutputStream;
import java.io.IOException;
import java.util.Random;

/**
 * &amp;lt;p&amp;gt;@Description:图片验证码控制器&amp;lt;/p&amp;gt;
 * &amp;lt;p&amp;gt;@Author: zhengyong&amp;lt;/p&amp;gt;
 * &amp;lt;p&amp;gt;@Date: 2019/7/28 23:02&amp;lt;/p&amp;gt;
 * &amp;lt;p&amp;gt;@Version: 1.0.0&amp;lt;/p&amp;gt;
 **/
@RestController
public class CodeController {
    
    private static final String SESSION_KEY= &quot;IMAGE_CODE_SESSION_KEY&quot;;
    //操作session
    private SessionStrategy strategy = new HttpSessionSessionStrategy();

    @GetMapping(&quot;/code/image&quot;)
    public void createCode(HttpServletRequest request, HttpServletResponse response) throws IOException {
        //获取图片验证码对象
        ImageCode imageCode = createImageCode();
        //往session中存入code
        strategy.setAttribute(new ServletWebRequest(request),SESSION_KEY,imageCode.getCode());
        //以流的方式写回前端
        ImageIO.write(imageCode.getImage(), &quot;JPEG&quot;, response.getOutputStream());
    }

&amp;lt;&amp;lt;&amp;lt;&amp;lt;&amp;lt;&amp;lt;&amp;lt; HEAD
      private ImageCode createImageCode() {
**=**==
    /**
     * 创建图片验证码
     *
     * @return 图片验证码对象
     */
    public ImageCode createCode(HttpServletRequest request) {
&amp;gt;&amp;gt;&amp;gt;&amp;gt;&amp;gt;&amp;gt;&amp;gt; a260bfea592f0e0e5b2a34611c3eabe817ab9730
        //从请求中获取宽度和高度，若没有则加载默认配置
//        int width = ServletRequestUtils.getIntParameter(request, &quot;width&quot;, mySecurityProperties.getBrowser().getValidateCode().getImage().getWidth());
//        int height = ServletRequestUtils.getIntParameter(request, &quot;height&quot;, mySecurityProperties.getBrowser().getValidateCode().getImage().getHeight());
        int width = 200;
&amp;lt;&amp;lt;&amp;lt;&amp;lt;&amp;lt;&amp;lt;&amp;lt; HEAD
        int height = 80;
**=**==
        int height = 30;
&amp;gt;&amp;gt;&amp;gt;&amp;gt;&amp;gt;&amp;gt;&amp;gt; a260bfea592f0e0e5b2a34611c3eabe817ab9730
        Random random = new Random();
//	    默认背景为黑色
        BufferedImage image = new BufferedImage(width, height, BufferedImage.TYPE_INT_RGB);
//		获取画笔
        Graphics graphics = image.getGraphics();
//		默认填充为白色
        graphics.fillRect(0, 0, width, height);
//	            验证码是由	数字 字母 干扰线 干扰点组成
//		文字素材
        String words = &quot;ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz123456789&quot;;
        char[] cs = words.toCharArray();
//		一般验证码为4位数
//      字母+数字
        StringBuilder randomStr = new StringBuilder();
        for (int i = 0; i &amp;lt; 4; i++) {
            //设置随机的颜色
            graphics.setColor(new Color(random.nextInt(256), random.nextInt(256), random.nextInt(256)));
            graphics.setFont(new Font(&quot;微软雅黑&quot;, Font.BOLD, 30));
            char c = cs[random.nextInt(cs.length)];
            graphics.drawString(c + &quot;&quot;, i * 20, 30);
            randomStr.append(c);
        }
        //画干扰线
        int max = random.nextInt(10);
        for (int i = 0; i &amp;lt; max; i++) {
            graphics.setColor(new Color(random.nextInt(256), random.nextInt(256), random.nextInt(256)));
            graphics.drawLine(random.nextInt(100), random.nextInt(50), random.nextInt(100), random.nextInt(50));
        }
//		画干扰点
        int max2 = random.nextInt(10);
        for (int i = 0; i &amp;lt; max2; i++) {
            graphics.setColor(new Color(random.nextInt(256), random.nextInt(256), random.nextInt(256)));
            graphics.drawOval(random.nextInt(80), random.nextInt(40), random.nextInt(5), random.nextInt(10));
        }

&amp;lt;&amp;lt;&amp;lt;&amp;lt;&amp;lt;&amp;lt;&amp;lt; HEAD
        return new ImageCode(randomStr.toString(),image,100);
**=**==
        return new ImageCode(image, randomStr.toString(), 10);
&amp;gt;&amp;gt;&amp;gt;&amp;gt;&amp;gt;&amp;gt;&amp;gt; a260bfea592f0e0e5b2a34611c3eabe817ab9730
    }

}

&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;二：在认证流程中加入图形验证码校验&lt;/h3&gt;
&lt;h4&gt;2）、新建自定义过滤器&lt;/h4&gt;
&lt;p&gt;写一个validateCodeFilter去继承OncePerRequestFilter（保证我们的过滤器只会被调用一次）&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;package com.eim.demo.validate;

import org.apache.commons.lang3.StringUtils;
import org.springframework.security.web.authentication.AuthenticationFailureHandler;
import org.springframework.social.connect.web.HttpSessionSessionStrategy;
import org.springframework.social.connect.web.SessionStrategy;
import org.springframework.stereotype.Component;
import org.springframework.web.bind.ServletRequestBindingException;
import org.springframework.web.bind.ServletRequestUtils;
import org.springframework.web.context.request.ServletWebRequest;
import org.springframework.web.filter.OncePerRequestFilter;

import javax.annotation.Resource;
import javax.servlet.FilterChain;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;

/**
 * &amp;lt;p&amp;gt;@Description:自定义验证码过滤器&amp;lt;/p&amp;gt;
 * &amp;lt;p&amp;gt;@Author: zhengyong&amp;lt;/p&amp;gt;
 * &amp;lt;p&amp;gt;@Date: 2019/7/30 21:45&amp;lt;/p&amp;gt;
 * &amp;lt;p&amp;gt;@Version: 1.0.0&amp;lt;/p&amp;gt;
 **/
@Component
public class ValidateCodeFilter extends OncePerRequestFilter {

    private static final String SESSION_KEY = &quot;IMAGE_CODE_SESSION_KEY&quot;;

    private SessionStrategy sessionStrategy = new HttpSessionSessionStrategy();

    /**
     * 失败处理器
     */
    @Resource
    private AuthenticationFailureHandler failureHandler;

    /**
     * 业务逻辑
     *
     * @param request     request
     * @param response    response
     * @param filterChain 過濾
     * @throws ServletException 異常
     * @throws IOException      異常
     */
    @Override
    protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
        //拦截符合标准的请求
        if (StringUtils.equals(&quot;/authentication/login&quot;, request.getRequestURI())
                &amp;amp;&amp;amp; StringUtils.endsWithIgnoreCase(request.getMethod(), &quot;post&quot;)) {
            try {
                //校验逻辑
                validate(new ServletWebRequest(request));
                //自定义异常
            } catch (ValidateException e) {
                //失败处理逻辑
                failureHandler.onAuthenticationFailure(request, response, e);
                return;
            }
        }
        //放行
        filterChain.doFilter(request, response);
    }

    /**
     * 校验逻辑
     *
     * @param request ServletWebRequest
     * @throws ServletRequestBindingException 异常
     */
    private void validate(ServletWebRequest request) throws ServletRequestBindingException {
        //从session中获取imageCode对象
        ImageCode codeInSession = (ImageCode) sessionStrategy.getAttribute(request, SESSION_KEY);
        //从请求中获取验证码数字
        String codeInRequest = ServletRequestUtils.getStringParameter(request.getRequest(), &quot;imageCode&quot;);

        if (StringUtils.isNotBlank(codeInRequest)) {
            throw new ValidateException(&quot;验证码不能为空&quot;);
        }
        if (StringUtils.isNotBlank(codeInSession.getCode())) {
            throw new ValidateException(&quot;验证码不存在&quot;);
        }
        if (codeInSession.isExpired()) {
            sessionStrategy.removeAttribute(request, SESSION_KEY);
            throw new ValidateException(&quot;验证码已过期&quot;);
        }
        if (!StringUtils.equals(codeInRequest, codeInSession.getCode())) {
            throw new ValidateException(&quot;验证码不匹配&quot;);
        }
        sessionStrategy.removeAttribute(request, SESSION_KEY);
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h4&gt;3）、自定义异常继承AuthenticationException&lt;/h4&gt;
&lt;pre&gt;&lt;code&gt;package com.eim.demo.validate;

import org.springframework.security.core.AuthenticationException;

/**
 * &amp;lt;p&amp;gt;@Description:验证码异常信息&amp;lt;/p&amp;gt;
 * &amp;lt;p&amp;gt;@Author: zhengyong&amp;lt;/p&amp;gt;
 * &amp;lt;p&amp;gt;@Date: 2019/7/30 21:52&amp;lt;/p&amp;gt;
 * &amp;lt;p&amp;gt;@Version: 1.0.0&amp;lt;/p&amp;gt;
 **/
public class ValidateException extends AuthenticationException {
    public ValidateException(String msg, Throwable t) {
        super(msg, t);
    }

    public ValidateException(String msg) {
        super(msg);
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h4&gt;4）、web安全配置，使自定义filter生效&lt;/h4&gt;
&lt;ul&gt;
&lt;li&gt;核心代码&lt;/li&gt;
&lt;/ul&gt;
&lt;blockquote&gt;
&lt;pre&gt;&lt;code&gt;   //自定义过滤器
   ValidateCodeFilter filter = new ValidateCodeFilter();
   //设置失败处理器
   filter.setFailureHandler(myAuthenticationFailureHandler);
   //在用户名密码过滤器前面加上自定义过滤器
   http.addFilterBefore(filter, UsernamePasswordAuthenticationFilter.class)
&lt;/code&gt;&lt;/pre&gt;
&lt;/blockquote&gt;
&lt;ul&gt;
&lt;li&gt;完整实例：&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;package com.eim.demo.config;

import com.eim.demo.properties.MySecurityProperties;
import com.eim.demo.validate.ValidateCodeFilter;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;
import org.springframework.stereotype.Component;

import javax.annotation.Resource;

/**
 * &amp;lt;p&amp;gt;DESC: web安全配置&amp;lt;/p&amp;gt;
 * &amp;lt;p&amp;gt;DATE: 2019-07-24 16:47&amp;lt;/p&amp;gt;
 * &amp;lt;p&amp;gt;VERSION:2.0.0&amp;lt;/p&amp;gt;
 * &amp;lt;p&amp;gt;@AUTHOR: ZhengYong&amp;lt;/p&amp;gt;
 */
@Component
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {

    @Resource
    private MySecurityProperties securityProperties;

    @Resource
    private MyAuthenticationSuccessHandler myAuthenticationSuccessHandler;

    @Resource
    private MyAuthenticationFailureHandler myAuthenticationFailureHandler;


    @Override
    protected void configure(HttpSecurity http) throws Exception {

        //自定义过滤器
        ValidateCodeFilter filter = new ValidateCodeFilter();
        //设置失败处理器
        filter.setFailureHandler(myAuthenticationFailureHandler);

        http
                //在用户名密码过滤器前面加上自定义过滤器
                .addFilterBefore(filter, UsernamePasswordAuthenticationFilter.class)
                .formLogin()
                    .loginPage(&quot;/authentication/require&quot;)
                    //form表单登录请求
                    .loginProcessingUrl(&quot;/authentication/login&quot;)
                    //自定义成功处理器
                    .successHandler(myAuthenticationSuccessHandler)
                    //自定义失败处理器
                    .failureHandler(myAuthenticationFailureHandler)
                .and()
                    .authorizeRequests()
                    .antMatchers(&quot;/code/image&quot;,&quot;/authentication/require&quot;,securityProperties.getBrowser().getLoginPage()).permitAll()
                    .anyRequest().authenticated()
                .and()
                    .csrf().disable();
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;三：重构代码&lt;/h3&gt;
&lt;h4&gt;一：验证码基本参数可配&lt;/h4&gt;
&lt;p&gt;&lt;img src=&quot;%E5%A4%9A%E7%BA%A7%E5%8F%82%E6%95%B0%E9%85%8D%E7%BD%AE.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;h5&gt;1）、默认配置&lt;/h5&gt;
&lt;p&gt;新建配置对象类验证码的基类：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;/**
 * &amp;lt;p&amp;gt;DESC: 验证码配置&amp;lt;/p&amp;gt;
 * &amp;lt;p&amp;gt;DATE: 2019-08-01 13:15&amp;lt;/p&amp;gt;
 * &amp;lt;p&amp;gt;VERSION:2.0.0&amp;lt;/p&amp;gt;
 * &amp;lt;p&amp;gt;@AUTHOR: ZhengYong&amp;lt;/p&amp;gt;
 */
@Data
public class ValidateCodeProperties {

    private ImageCodeProperties image = new ImageCodeProperties();
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;新建图片验证码配置类，并赋默认值：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;/**
 * &amp;lt;p&amp;gt;DESC: 图片验证码配置&amp;lt;/p&amp;gt;
 * &amp;lt;p&amp;gt;DATE: 2019-08-01 13:12&amp;lt;/p&amp;gt;
 * &amp;lt;p&amp;gt;VERSION:2.0.0&amp;lt;/p&amp;gt;
 * &amp;lt;p&amp;gt;@AUTHOR: ZhengYong&amp;lt;/p&amp;gt;
 */
@Data
public class ImageCodeProperties {
    /**
     * 宽度
     */
    private Integer width = 100;
    /**
     * 高度
     */
    private Integer height = 50;
    /**
     *  长度
     */
    private Integer length = 4;
    /**
     * 过期时间
     */
    private int expireTime = 100;
    /**
     * 拦截的url
     */
    private List&amp;lt;String&amp;gt; url;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;在浏览器配置类中添加新增验证码基类&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;/**
 * &amp;lt;p&amp;gt;DESC: 浏览器配置文件&amp;lt;/p&amp;gt;
 * &amp;lt;p&amp;gt;DATE: 2019-07-25 12:38&amp;lt;/p&amp;gt;
 * &amp;lt;p&amp;gt;VERSION:2.0.0&amp;lt;/p&amp;gt;
 * &amp;lt;p&amp;gt;@AUTHOR: ZhengYong&amp;lt;/p&amp;gt;
 */
@Data
public class BrowserSecurityProperties {

    private String loginPage = &quot;/default_sigIn.html&quot;;

    private ResponseTypeEnum type = JSON;

    private ValidateCodeProperties validateCode = new ValidateCodeProperties();

}
&lt;/code&gt;&lt;/pre&gt;
&lt;h5&gt;2）、应用参数配置，请求参数可配&lt;/h5&gt;
&lt;p&gt;在图片验证码生成中添加配置&lt;/p&gt;
&lt;p&gt;主要代码：&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;int width = ServletRequestUtils.getIntParameter(request, &quot;width&quot;, mySecurityProperties.getBrowser().getValidateCode().getImage().getWidth());&lt;/p&gt;
&lt;p&gt;从请求中拿width没有的话从配置文件类中获取&lt;/p&gt;
&lt;p&gt;这里一共配置了width，height，length，expireTime&lt;/p&gt;
&lt;/blockquote&gt;
&lt;pre&gt;&lt;code&gt;/**
 * &amp;lt;p&amp;gt;DESC: 图片验证码实现类&amp;lt;/p&amp;gt;
 * &amp;lt;p&amp;gt;DATE: 2019-08-02 16:41&amp;lt;/p&amp;gt;
 * &amp;lt;p&amp;gt;VERSION:2.0.0&amp;lt;/p&amp;gt;
 * &amp;lt;p&amp;gt;@AUTHOR: ZhengYong&amp;lt;/p&amp;gt;
 */
public class ImageCodeGeneratorImpl implements ImageCodeGenerator{

    @Resource
    private MySecurityProperties mySecurityProperties;

    /**
     * 创建图片验证码
     *
     * @return 图片验证码对象
     */
    @Override
    public ImageCode createImageCode(HttpServletRequest request) {
            //从请求中获取宽度和高度，若没有则加载默认配置
            int width = ServletRequestUtils.getIntParameter(request, &quot;width&quot;, mySecurityProperties.getBrowser().getValidateCode().getImage().getWidth());
            int height = ServletRequestUtils.getIntParameter(request, &quot;height&quot;, mySecurityProperties.getBrowser().getValidateCode().getImage().getHeight());
            Random random = new Random();
//	    默认背景为黑色
            BufferedImage image = new BufferedImage(width, height, BufferedImage.TYPE_INT_RGB);
//		获取画笔
            Graphics graphics = image.getGraphics();
//		默认填充为白色
            graphics.fillRect(0, 0, width, height);
//	            验证码是由	数字 字母 干扰线 干扰点组成
//		文字素材
            String words = &quot;ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz123456789&quot;;
            char[] cs = words.toCharArray();
//		一般验证码为4位数
//      字母+数字
            StringBuilder randomStr = new StringBuilder();
            for (int i = 0; i &amp;lt; mySecurityProperties.getBrowser().getValidateCode().getImage().getLength(); i++) {
                //设置随机的颜色
                graphics.setColor(new Color(random.nextInt(256), random.nextInt(256), random.nextInt(256)));
                graphics.setFont(new Font(&quot;微软雅黑&quot;, Font.BOLD, 30));
                char c = cs[random.nextInt(cs.length)];
                graphics.drawString(c + &quot;&quot;, i * 20, 30);
                randomStr.append(c);
            }
            //画干扰线
            int max = random.nextInt(10);
            for (int i = 0; i &amp;lt; max; i++) {
                graphics.setColor(new Color(random.nextInt(256), random.nextInt(256), random.nextInt(256)));
                graphics.drawLine(random.nextInt(100), random.nextInt(50), random.nextInt(100), random.nextInt(50));
            }
//		画干扰点
            int max2 = random.nextInt(10);
            for (int i = 0; i &amp;lt; max2; i++) {
                graphics.setColor(new Color(random.nextInt(256), random.nextInt(256), random.nextInt(256)));
                graphics.drawOval(random.nextInt(80), random.nextInt(40), random.nextInt(5), random.nextInt(10));
            }

            return new ImageCode(image, randomStr.toString(), mySecurityProperties.getBrowser().getValidateCode().getImage().getExpireTime());
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h4&gt;二：验证码拦截的接口可配&lt;/h4&gt;
&lt;p&gt;核心代码&lt;/p&gt;
&lt;blockquote&gt;
&lt;pre&gt;&lt;code&gt;   List&amp;lt;String&amp;gt; urlList = mySecurityProperties.getBrowser().getValidateCode().getImage().getUrl();
   //判断是否需要校验
   AtomicBoolean isFilter = new AtomicBoolean(false);
   urlList.forEach(url -&amp;gt; {
       if(pathPattern.match(url,request.getRequestURI())){
           isFilter.set(true);
       }
   });
&lt;/code&gt;&lt;/pre&gt;
&lt;/blockquote&gt;
&lt;pre&gt;&lt;code&gt;/**
 * &amp;lt;p&amp;gt;@Description:自定义验证码过滤器&amp;lt;/p&amp;gt;
 * &amp;lt;p&amp;gt;@Author: zhengyong&amp;lt;/p&amp;gt;
 * &amp;lt;p&amp;gt;@Date: 2019/7/30 21:45&amp;lt;/p&amp;gt;
 * &amp;lt;p&amp;gt;@Version: 1.0.0&amp;lt;/p&amp;gt;
 **/
@EqualsAndHashCode(callSuper = true)
@Data
@Component
public class ValidateCodeFilter extends OncePerRequestFilter {

    private static final String SESSION_KEY= &quot;IMAGE_CODE_SESSION_KEY&quot;;

    private SessionStrategy sessionStrategy = new HttpSessionSessionStrategy();

    /**
     * 失败处理器
     */
    private AuthenticationFailureHandler failureHandler;


    @Resource
    private MySecurityProperties mySecurityProperties;

    private AntPathMatcher pathPattern = new AntPathMatcher();

    /**
     * 业务逻辑
     * @param request request
     * @param response response
     * @param filterChain filterChain
     * @throws ServletException  异常
     * @throws IOException  异常
     */
    @Override
    protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {

        List&amp;lt;String&amp;gt; urlList = mySecurityProperties.getBrowser().getValidateCode().getImage().getUrl();
        //判断是否需要校验
        AtomicBoolean isFilter = new AtomicBoolean(false);
        urlList.forEach(url -&amp;gt; {
            if(pathPattern.match(url,request.getRequestURI())){
                isFilter.set(true);
            }
        });

        //拦截符合标准的请求
        if(isFilter.get()){
            try{
                //校验逻辑
                validate(new ServletWebRequest(request));
            }catch (ValidateException e){
                //失败处理逻辑
                failureHandler.onAuthenticationFailure(request,response,e);
                return;
            }
        }
        //放行
        filterChain.doFilter(request,response);
    }
&lt;/code&gt;&lt;/pre&gt;
&lt;h4&gt;三：验证码的生成逻辑可配&lt;/h4&gt;
&lt;h5&gt;1）、新建一个生成图片验证码接口&lt;/h5&gt;
&lt;pre&gt;&lt;code&gt;/**
 * &amp;lt;p&amp;gt;DESC: 图片验证码生成器&amp;lt;/p&amp;gt;
 * &amp;lt;p&amp;gt;DATE: 2019-08-02 16:38&amp;lt;/p&amp;gt;
 * &amp;lt;p&amp;gt;VERSION:2.0.0&amp;lt;/p&amp;gt;
 * &amp;lt;p&amp;gt;@AUTHOR: ZhengYong&amp;lt;/p&amp;gt;
 */
public interface ImageCodeGenerator {

    /**
     * 创建图片验证码
     * @param  request request域
     * @return 图片验证码对象
     */
    ImageCode createImageCode(HttpServletRequest request);
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h5&gt;2）、新建一个图片验证码接口实现类&lt;/h5&gt;
&lt;ul&gt;
&lt;li&gt;注意：没有添加到spring容器中的任何注解&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;package com.eim.demo.validate;

import com.eim.demo.properties.MySecurityProperties;
import org.springframework.web.bind.ServletRequestUtils;

import javax.annotation.Resource;
import javax.servlet.http.HttpServletRequest;
import java.awt.*;
import java.awt.image.BufferedImage;
import java.util.Random;

/**
 * &amp;lt;p&amp;gt;DESC: 图片验证码实现类&amp;lt;/p&amp;gt;
 * &amp;lt;p&amp;gt;DATE: 2019-08-02 16:41&amp;lt;/p&amp;gt;
 * &amp;lt;p&amp;gt;VERSION:2.0.0&amp;lt;/p&amp;gt;
 * &amp;lt;p&amp;gt;@AUTHOR: ZhengYong&amp;lt;/p&amp;gt;
 */
public class ImageCodeGeneratorImpl implements ImageCodeGenerator{

    @Resource
    private MySecurityProperties mySecurityProperties;

    /**
     * 创建图片验证码
     *
     * @return 图片验证码对象
     */
    @Override
    public ImageCode createImageCode(HttpServletRequest request) {
            //从请求中获取宽度和高度，若没有则加载默认配置
            int width = ServletRequestUtils.getIntParameter(request, &quot;width&quot;, mySecurityProperties.getBrowser().getValidateCode().getImage().getWidth());
            int height = ServletRequestUtils.getIntParameter(request, &quot;height&quot;, mySecurityProperties.getBrowser().getValidateCode().getImage().getHeight());
            Random random = new Random();
//	    默认背景为黑色
            BufferedImage image = new BufferedImage(width, height, BufferedImage.TYPE_INT_RGB);
//		获取画笔
            Graphics graphics = image.getGraphics();
//		默认填充为白色
            graphics.fillRect(0, 0, width, height);
//	            验证码是由	数字 字母 干扰线 干扰点组成
//		文字素材
            String words = &quot;ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz123456789&quot;;
            char[] cs = words.toCharArray();
//		一般验证码为4位数
//      字母+数字
            StringBuilder randomStr = new StringBuilder();
            for (int i = 0; i &amp;lt; mySecurityProperties.getBrowser().getValidateCode().getImage().getLength(); i++) {
                //设置随机的颜色
                graphics.setColor(new Color(random.nextInt(256), random.nextInt(256), random.nextInt(256)));
                graphics.setFont(new Font(&quot;微软雅黑&quot;, Font.BOLD, 30));
                char c = cs[random.nextInt(cs.length)];
                graphics.drawString(c + &quot;&quot;, i * 20, 30);
                randomStr.append(c);
            }
            //画干扰线
            int max = random.nextInt(10);
            for (int i = 0; i &amp;lt; max; i++) {
                graphics.setColor(new Color(random.nextInt(256), random.nextInt(256), random.nextInt(256)));
                graphics.drawLine(random.nextInt(100), random.nextInt(50), random.nextInt(100), random.nextInt(50));
            }
//		画干扰点
            int max2 = random.nextInt(10);
            for (int i = 0; i &amp;lt; max2; i++) {
                graphics.setColor(new Color(random.nextInt(256), random.nextInt(256), random.nextInt(256)));
                graphics.drawOval(random.nextInt(80), random.nextInt(40), random.nextInt(5), random.nextInt(10));
            }

            return new ImageCode(image, randomStr.toString(), mySecurityProperties.getBrowser().getValidateCode().getImage().getExpireTime());
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h5&gt;3）、图片验证码注入spring容器配置&lt;/h5&gt;
&lt;pre&gt;&lt;code&gt;package com.eim.demo.validate;

import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

/**
 * &amp;lt;p&amp;gt;DESC: 图片验证码注入配置‘&amp;lt;/p&amp;gt;
 * &amp;lt;p&amp;gt;DATE: 2019-08-02 16:44&amp;lt;/p&amp;gt;
 * &amp;lt;p&amp;gt;VERSION:2.0.0&amp;lt;/p&amp;gt;
 * &amp;lt;p&amp;gt;@AUTHOR: ZhengYong&amp;lt;/p&amp;gt;
 */
@Configuration
public class ImageCodeConfig {

    /**
     * 注入默认的图片验证码生成器
     * @return 图片验证码
     */
    @Bean
    //在spring中缺失一个bean叫imageCodeGenerator的时候自动注入spring容器
    @ConditionalOnMissingBean(name = &quot;imageCodeGenerator&quot;)
    public ImageCodeGenerator imageCodeGenerator(){
        return new ImageCodeGeneratorImpl();
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;测试覆盖&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;一个类去实现接口，加上注解@Component(value = &quot;imageCodeGenerator&quot;)&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;/**
 * &amp;lt;p&amp;gt;DESC: 测试图片验证码覆盖&amp;lt;/p&amp;gt;
 * &amp;lt;p&amp;gt;DATE: 2019-08-02 16:49&amp;lt;/p&amp;gt;
 * &amp;lt;p&amp;gt;VERSION:2.0.0&amp;lt;/p&amp;gt;
 * &amp;lt;p&amp;gt;@AUTHOR: ZhengYong&amp;lt;/p&amp;gt;
 */
@Slf4j
@Component(value = &quot;imageCodeGenerator&quot;)
public class ConvertImageConvert implements ImageCodeGenerator {
    /**
     * 创建图片验证码
     *
     * @param request request域
     * @return 图片验证码对象
     */
    @Override
    public ImageCode createImageCode(HttpServletRequest request) {
        log.info(&quot;覆盖了----------哦&quot;);
        return null;
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;实现记住我登录&lt;/h2&gt;
&lt;h3&gt;一、配置webSecurityConfig&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;核心代码&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;    /**
     * 数据源(sql包下的)
     */
    @Resource
    private DataSource dataSource;

    /**
     * 配置一个持久化token仓库
     * @return
     */
    @Bean
    public PersistentTokenRepository persistentTokenRepository(){
        //新建一个PersistentTokenRepository的实现类
        JdbcTokenRepositoryImpl tokenRepository = new JdbcTokenRepositoryImpl();
        //需要设置数据源
        tokenRepository.setDataSource(dataSource);
        //启动的时候去创建表（默认存储token的表）
//        tokenRepository.setCreateTableOnStartup(true);
        return tokenRepository;
    }

.rememberMe()
  //配置持久化token
  .tokenRepository(persistentTokenRepository())
  //配置token验证秒数
  .tokenValiditySeconds(securityProperties.getBrowser().getRememberMeSeconds())
  //配置用于登录的UserDetailsService
  .userDetailsService(userDetailsService)
&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;完整配置&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;package com.eim.demo.config;

import com.eim.demo.handler.MyAuthenticationFailureHandler;
import com.eim.demo.handler.MyAuthenticationSuccessHandler;
import com.eim.demo.properties.MySecurityProperties;
import com.eim.demo.validate.SmsCodeFilter;
import com.eim.demo.validate.ValidateCodeFilter;
import com.eim.demo.validate.mobile.SmsCodeAuthenticationSecurityConfig;
import org.springframework.context.annotation.Bean;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;
import org.springframework.security.web.authentication.rememberme.JdbcTokenRepositoryImpl;
import org.springframework.security.web.authentication.rememberme.PersistentTokenRepository;
import org.springframework.stereotype.Component;

import javax.annotation.Resource;
import javax.sql.DataSource;

/**
 * &amp;lt;p&amp;gt;DESC: web安全配置&amp;lt;/p&amp;gt;
 * &amp;lt;p&amp;gt;DATE: 2019-07-24 16:47&amp;lt;/p&amp;gt;
 * &amp;lt;p&amp;gt;VERSION:2.0.0&amp;lt;/p&amp;gt;
 * &amp;lt;p&amp;gt;@AUTHOR: ZhengYong&amp;lt;/p&amp;gt;
 */
@Component
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {

    @Resource
    private MySecurityProperties securityProperties;

    @Resource
    private MyAuthenticationSuccessHandler myAuthenticationSuccessHandler;

    @Resource
    private MyAuthenticationFailureHandler myAuthenticationFailureHandler;

    @Resource
    private ValidateCodeFilter validateCodeFilter;

    @Resource
    private SmsCodeFilter smsCodeFilter;

    @Resource
    private SmsCodeAuthenticationSecurityConfig smsCodeAuthenticationSecurityConfig;

    /**
     * 配置用户登录逻辑
     */
    @Resource
    private UserDetailsService userDetailsService;
    /**
     * 数据源(sql包下的)
     */
    @Resource
    private DataSource dataSource;
    /**
     * 配置一个持久化token仓库
     * @return
     */
    @Bean
    public PersistentTokenRepository persistentTokenRepository(){
        //新建一个PersistentTokenRepository的实现类
        JdbcTokenRepositoryImpl tokenRepository = new JdbcTokenRepositoryImpl();
        //需要设置数据源
        tokenRepository.setDataSource(dataSource);
        //启动的时候去创建表（默认存储token的表）
//        tokenRepository.setCreateTableOnStartup(true);
        return tokenRepository;
    }

    @Override
    protected void configure(HttpSecurity http) throws Exception {


        http
                .addFilterBefore(smsCodeFilter,UsernamePasswordAuthenticationFilter.class)
                //在用户名密码过滤器前面加上自定义过滤器
                .addFilterBefore(validateCodeFilter, UsernamePasswordAuthenticationFilter.class)
                .formLogin()
                    .loginPage(&quot;/authentication/require&quot;)
                    //form表单登录请求
                    .loginProcessingUrl(&quot;/authentication/login&quot;)
                    //自定义成功处理器
                    .successHandler(myAuthenticationSuccessHandler)
                    //自定义失败处理器
                    .failureHandler(myAuthenticationFailureHandler)
                    .and()
                .rememberMe()
                    //配置持久化token
                    .tokenRepository(persistentTokenRepository())
                    //配置token验证秒数
                    .tokenValiditySeconds(securityProperties.getBrowser().getRememberMeSeconds())
                    //配置用于登录的UserDetailsService
                    .userDetailsService(userDetailsService)
                .and()
                    .authorizeRequests()
                    .antMatchers(
                            &quot;/code/*&quot;,
                                &quot;/authentication/require&quot;,
                            securityProperties.getBrowser().getLoginPage()
                                ).permitAll()
                    .anyRequest().authenticated()
                .and()
                    .csrf().disable()
                //注入自定义短信验证码的安全配置
                .apply(smsCodeAuthenticationSecurityConfig)
                    ;
    }
}

&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;二、配置html&lt;/h3&gt;
&lt;p&gt;name是默认的&lt;code&gt;remember-me&lt;/code&gt;&lt;/p&gt;
&lt;blockquote&gt;
&lt;pre&gt;&lt;code&gt;&amp;lt;td&amp;gt;&amp;lt;input name=&quot;remember-me&quot; type=&quot;checkbox&quot; value=&quot;true&quot;/&amp;gt;记住我&amp;lt;/td&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;/blockquote&gt;
&lt;p&gt;完整代码&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;    &amp;lt;form action=&quot;/authentication/login&quot; method=&quot;post&quot;&amp;gt;
        &amp;lt;table&amp;gt;
            &amp;lt;tr&amp;gt;
                &amp;lt;td&amp;gt;用户名：&amp;lt;/td&amp;gt;
                &amp;lt;td&amp;gt;&amp;lt;input type=&quot;text&quot; name=&quot;username&quot;&amp;gt;&amp;lt;/td&amp;gt;
            &amp;lt;/tr&amp;gt;
            &amp;lt;tr&amp;gt;
                &amp;lt;td&amp;gt;密码：&amp;lt;/td&amp;gt;
                &amp;lt;td&amp;gt;&amp;lt;input type=&quot;text&quot; name=&quot;password&quot;&amp;gt;&amp;lt;/td&amp;gt;
            &amp;lt;/tr&amp;gt;
            &amp;lt;tr&amp;gt;
                &amp;lt;td&amp;gt;&amp;lt;input type=&quot;text&quot; name=&quot;imageCode&quot;&amp;gt;&amp;lt;/td&amp;gt;
                &amp;lt;td&amp;gt;&amp;lt;!-- 图片不存在，已移除: image --&amp;gt;&amp;lt;/td&amp;gt;
            &amp;lt;/tr&amp;gt;
            &amp;lt;tr&amp;gt;
                &amp;lt;td&amp;gt;&amp;lt;input name=&quot;remember-me&quot; type=&quot;checkbox&quot; value=&quot;true&quot;/&amp;gt;记住我&amp;lt;/td&amp;gt;
            &amp;lt;/tr&amp;gt;
            &amp;lt;tr&amp;gt;
                &amp;lt;td&amp;gt;&amp;lt;button type=&quot;submit&quot;&amp;gt;登录&amp;lt;/button&amp;gt;&amp;lt;/td&amp;gt;
            &amp;lt;/tr&amp;gt;
        &amp;lt;/table&amp;gt;
    &amp;lt;/form&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;实现短信验证码发送接口&lt;/h2&gt;
&lt;h3&gt;一、看懂处理器结构&lt;/h3&gt;
&lt;p&gt;&lt;img src=&quot;%E9%AA%8C%E8%AF%81%E7%A0%81%E5%A4%84%E7%90%86%E5%99%A8%E4%BB%A3%E7%A0%81%E7%BB%93%E6%9E%84.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;ValidateCodeController是用来统一创建返回的请求地址&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;ValidateCodeProcessor是验证码处理器接口。&lt;/p&gt;
&lt;p&gt;接口中包含一个方法，create方法。由实现类去实现&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;AbstractValidateCodeProcessor,是接口的抽象实现类&lt;/p&gt;
&lt;p&gt;该类实现了create方法。1：生成，2：保存到session，3：发送&lt;/p&gt;
&lt;p&gt;2：保存方法是公用的，自己实现。3是一个抽象方法，由子类去实现。1：是生成方法，有ValidateCodeGenerator接口的实现类去生成&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;ValidateCodeGenerator是验证码生成器接口，其中有个createCode方法。&lt;/p&gt;
&lt;p&gt;这个接口的实现类，去具体实现不同的创建验证码方法（1：生成方法）&lt;/p&gt;
&lt;p&gt;会根据请求中参数选择生成器（spring开发技巧：依赖查找）&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;ImageCodeProcessor和SmsCodeProcessor是抽象类AbstractValidateCodeProcessor实现。&lt;/p&gt;
&lt;p&gt;主要是实现3发送方法。&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;验证码架构&lt;/h2&gt;
&lt;h3&gt;准备工作&lt;/h3&gt;
&lt;p&gt;&lt;img src=&quot;%E9%AA%8C%E8%AF%81%E7%A0%81%E5%87%86%E5%A4%87%E5%B7%A5%E4%BD%9C.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;h3&gt;验证码架构&lt;/h3&gt;
&lt;p&gt;&lt;img src=&quot;%E9%AA%8C%E8%AF%81%E7%A0%81%E6%9E%B6%E6%9E%84.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;h3&gt;二、创建ValidateCodeController&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;package com.eim.demo.validate;

import com.eim.demo.validate.code.ImageCode;
import com.eim.demo.validate.code.ValidateCode;
import com.eim.demo.validate.generator.ValidateCodeGenerator;
import com.eim.demo.validate.processor.ValidateCodeProcessor;
import com.eim.demo.validate.sms.SmsCodeSender;
import lombok.extern.slf4j.Slf4j;
import org.springframework.social.connect.web.HttpSessionSessionStrategy;
import org.springframework.social.connect.web.SessionStrategy;
import org.springframework.web.bind.ServletRequestBindingException;
import org.springframework.web.bind.ServletRequestUtils;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.context.request.ServletWebRequest;

import javax.annotation.Resource;
import javax.imageio.ImageIO;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.util.Map;

/**
 * &amp;lt;p&amp;gt;@Description:图片验证码控制器&amp;lt;/p&amp;gt;
 * &amp;lt;p&amp;gt;@Author: zhengyong&amp;lt;/p&amp;gt;
 * &amp;lt;p&amp;gt;@Date: 2019/7/28 23:02&amp;lt;/p&amp;gt;
 * &amp;lt;p&amp;gt;@Version: 1.0.0&amp;lt;/p&amp;gt;
 **/
@Slf4j
@RestController
public class CodeController {

  	//	依赖查询技巧
    @Resource
    private Map&amp;lt;String, ValidateCodeProcessor&amp;gt; validateCodeProcessorMap;

    /**
     * 穿件验证码，根据验证码类型不同，调用不同的接口实现
     *
     * @param request  请求
     * @param response 返回
     * @param type     类型
     * @throws Exception 异常
     */
    @GetMapping(&quot;/code/{type}&quot;)
    public void createCodeByType(HttpServletRequest request, HttpServletResponse response, @PathVariable String type) throws Exception {
      
        validateCodeProcessorMap.get(type + &quot;CodeProcessor&quot;).create(new ServletWebRequest(request, response));
      
    }

}
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;三：创建ValidateCodeProcessor&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;package com.eim.demo.validate.processor;

import org.springframework.web.context.request.ServletWebRequest;

/**
 * &amp;lt;p&amp;gt;DESC: 验证码处理器&amp;lt;/p&amp;gt;
 * &amp;lt;p&amp;gt;DATE: 2019-08-07 16:30&amp;lt;/p&amp;gt;
 * &amp;lt;p&amp;gt;VERSION:2.0.0&amp;lt;/p&amp;gt;
 * &amp;lt;p&amp;gt;@AUTHOR: ZhengYong&amp;lt;/p&amp;gt;
 */
public interface ValidateCodeProcessor {

    /**
     * 验证码放入session的前缀
     */
    String SESSION_KEY_PREFIX = &quot;SESSION_KEY_FOR_CODE_&quot;;

    /**
     * 创建验证码
     * @param request spring工具类，request和response封装类
     * @throws Exception 异常
     */
    void create(ServletWebRequest request) throws Exception;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;四：创建AbstractValidateCodeProcessor&lt;/h3&gt;
&lt;p&gt;注：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;泛型C（指的是验证码的类型）,继承的子类去指定泛型是什么类型。&lt;/li&gt;
&lt;li&gt;创建验证码的类是通过类型匹配、依赖查找的方式调用接口下不同的实现类。&lt;/li&gt;
&lt;li&gt;发送方法是抽象的，需要子类去实现。&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;package com.eim.demo.validate.processor;

import com.eim.demo.validate.generator.ValidateCodeGenerator;
import org.apache.commons.lang3.StringUtils;
import org.springframework.social.connect.web.HttpSessionSessionStrategy;
import org.springframework.social.connect.web.SessionStrategy;
import org.springframework.web.context.request.ServletWebRequest;

import javax.annotation.Resource;
import java.util.Map;

/**
 * &amp;lt;p&amp;gt;DESC: 验证码处理器抽象实现类&amp;lt;/p&amp;gt;
 * &amp;lt;p&amp;gt;DATE: 2019-08-07 16:35&amp;lt;/p&amp;gt;
 * &amp;lt;p&amp;gt;VERSION:2.0.0&amp;lt;/p&amp;gt;
 * &amp;lt;p&amp;gt;@AUTHOR: ZhengYong&amp;lt;/p&amp;gt;
 */
public abstract class AbstractValidateCodeProcessor&amp;lt;C&amp;gt; implements ValidateCodeProcessor {


    private SessionStrategy sessionStrategy = new  HttpSessionSessionStrategy();
    /**
     * 系统启动的时候回收集所有的ValidateCodeGenerator接口的实现。
     */
    @Resource
    private Map&amp;lt;String, ValidateCodeGenerator&amp;gt; validateCodeGeneratorMap;

    /**
     * 创建验证码（实现接口方法）
     *
     * @param request request和response封装类
     * @throws Exception 异常
     */
    @Override
    public void create(ServletWebRequest request) throws Exception {
       //创建验证码（创建验证码的子类实现）
       C validateCode = generator(request);
      //存入session（公共代码由抽象类自行实现）
       save(request,validateCode);
      //发送验证码（子类实现）
       send(request,validateCode);
    }

    /**
     * 保存验证码
     * @param request 请求
     * @param validateCode 验证码对象
     */
    private void save(ServletWebRequest request, C validateCode){
        sessionStrategy.setAttribute(request,SESSION_KEY_PREFIX+getProcessorType(request).toUpperCase(),validateCode);
    };

    /**
     * 生成验证码
     * @param request 请求
     * @return 类型
     */
    @SuppressWarnings(&quot;unchecked&quot;)
    private C generator(ServletWebRequest request){
        String type=getProcessorType(request);
        ValidateCodeGenerator codeGenerator = validateCodeGeneratorMap.get(type + &quot;CodeGenerator&quot;);
        return (C)codeGenerator.createCode(request.getRequest());
    }

    /**
     * 根据请求的url获取验证码的类型
     * @param request 请求
     * @return  类型
     */
    public static String getProcessorType(ServletWebRequest request){
        return StringUtils.substringAfter(request.getRequest().getRequestURI(),&quot;/code/&quot;);
    };

    /**
     * 发送验证码抽象方法
     * @param request       请求
     * @param validateCode  验证码对象
     */
    protected abstract void send(ServletWebRequest request, C validateCode) throws Exception;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;五（1）、创建ImageCodeProcessor和SmsCodeProcessor&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;/**
 * &amp;lt;p&amp;gt;DESC: 图片验证码处理器&amp;lt;/p&amp;gt;
 * &amp;lt;p&amp;gt;DATE: 2019-08-08 10:39&amp;lt;/p&amp;gt;
 * &amp;lt;p&amp;gt;VERSION:2.0.0&amp;lt;/p&amp;gt;
 * &amp;lt;p&amp;gt;@AUTHOR: ZhengYong&amp;lt;/p&amp;gt;
 */
@Component(&quot;imageCodeProcessor&quot;)
public class ImageCodeProcessor extends AbstractValidateCodeProcessor&amp;lt;ImageCode&amp;gt; {

    /**
     * 发送验证码抽象方法
     *
     * @param request      请求
     * @param imageCode 验证码对象
     */
    @Override
    protected void send(ServletWebRequest request, ImageCode imageCode) throws Exception {
        ImageIO.write(imageCode.getImage(),&quot;JPEG&quot;,request.getResponse().getOutputStream());
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;/**
 * &amp;lt;p&amp;gt;DESC: 短信验证码处理器&amp;lt;/p&amp;gt;
 * &amp;lt;p&amp;gt;DATE: 2019-08-08 10:40&amp;lt;/p&amp;gt;
 * &amp;lt;p&amp;gt;VERSION:2.0.0&amp;lt;/p&amp;gt;
 * &amp;lt;p&amp;gt;@AUTHOR: ZhengYong&amp;lt;/p&amp;gt;
 */
@Component(&quot;smsCodeProcessor&quot;)
public class SmsCodeProcessor extends AbstractValidateCodeProcessor&amp;lt;ValidateCode&amp;gt; {

    @Resource
    private SmsCodeSender smsCodeSender;

    /**
     * 发送验证码抽象方法
     *
     * @param request      请求
     * @param validateCode 验证码对象
     */
    @Override
    protected void send(ServletWebRequest request, ValidateCode validateCode) throws Exception {
        String mobile = ServletRequestUtils.getRequiredStringParameter(request.getRequest(), &quot;mobile&quot;);
        smsCodeSender.send(mobile,validateCode.getCode());
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;五（2）、创建存放验证码类&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;/**
 * &amp;lt;p&amp;gt;DESC: 验证码基类&amp;lt;/p&amp;gt;
 * &amp;lt;p&amp;gt;DATE: 2019-10-09 17:42&amp;lt;/p&amp;gt;
 * &amp;lt;p&amp;gt;VERSION:2.0.0&amp;lt;/p&amp;gt;
 * &amp;lt;p&amp;gt;@AUTHOR: ZhengYong&amp;lt;/p&amp;gt;
 */
@Data
public class ValidateCode {

    private String code;

    private LocalDateTime expireTime;

    public ValidateCode(String code,LocalDateTime expireTime) {
        this.code = code;
        this.expireTime = expireTime;
    }

    public ValidateCode(String code, int expireTime) {
        this.code = code;
        this.expireTime = LocalDateTime.now().plusSeconds(expireTime);
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;/**
 * &amp;lt;p&amp;gt;DESC: 图片验证码类&amp;lt;/p&amp;gt;
 * &amp;lt;p&amp;gt;DATE: 2019-10-07 15:05&amp;lt;/p&amp;gt;
 * &amp;lt;p&amp;gt;VERSION:2.0.0&amp;lt;/p&amp;gt;
 * &amp;lt;p&amp;gt;@AUTHOR: ZhengYong&amp;lt;/p&amp;gt;
 */
@EqualsAndHashCode(callSuper = true)
@Data
public class ImageCode extends ValidateCode{

    private BufferedImage image;

    public ImageCode(String code,BufferedImage bufferedImage, LocalDateTime expireTime) {
        super(code, expireTime);
        this.image = bufferedImage;
    }

    public ImageCode(String code,BufferedImage bufferedImage, int expireTime) {
        super(code, expireTime);
        this.image = bufferedImage;
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;六、验证码生成器&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;/**
 * &amp;lt;p&amp;gt;DESC: 图片验证码生成器&amp;lt;/p&amp;gt;
 * &amp;lt;p&amp;gt;DATE: 2019-08-02 16:38&amp;lt;/p&amp;gt;
 * &amp;lt;p&amp;gt;VERSION:2.0.0&amp;lt;/p&amp;gt;
 * &amp;lt;p&amp;gt;@AUTHOR: ZhengYong&amp;lt;/p&amp;gt;
 */
public interface ValidateCodeGenerator {

    /**
     * 创建验证码
     * @param  request request域
     * @return 验证码对象
     */
    ValidateCode createCode(HttpServletRequest request);
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;7、图验证码生成器，短信验证码生成器&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;/**
 * &amp;lt;p&amp;gt;DESC: 短信验证码生成器实现类&amp;lt;/p&amp;gt;
 * &amp;lt;p&amp;gt;DATE: 2019-08-02 16:41&amp;lt;/p&amp;gt;
 * &amp;lt;p&amp;gt;VERSION:2.0.0&amp;lt;/p&amp;gt;
 * &amp;lt;p&amp;gt;@AUTHOR: ZhengYong&amp;lt;/p&amp;gt;
 */
@Component(value = &quot;smsCodeGenerator&quot;)
public class SmsCodeGeneratorImpl implements ValidateCodeGenerator {

    @Resource
    private MySecurityProperties mySecurityProperties;

    /**
     * 创建短信验证码
     *
     * @return 验证码对象
     */
    @Override
    public ValidateCode createCode(HttpServletRequest request) {
        String random = RandomStringUtils.randomNumeric(mySecurityProperties.getBrowser().getValidateCode().getSms().getLength());
        return new ValidateCode(random,mySecurityProperties.getBrowser().getValidateCode().getSms().getExpireTime());
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;/**
 * &amp;lt;p&amp;gt;DESC: 图片验证码实现类&amp;lt;/p&amp;gt;
 * &amp;lt;p&amp;gt;DATE: 2019-08-02 16:41&amp;lt;/p&amp;gt;
 * &amp;lt;p&amp;gt;VERSION:2.0.0&amp;lt;/p&amp;gt;
 * &amp;lt;p&amp;gt;@AUTHOR: ZhengYong&amp;lt;/p&amp;gt;
 */
public class ValidateCodeGeneratorImpl implements ValidateCodeGenerator {

    @Resource
    private MySecurityProperties mySecurityProperties;

    /**
     * 创建图片验证码
     *
     * @return 图片验证码对象
     */
    @Override
    public ImageCode createCode(HttpServletRequest request) {
            //从请求中获取宽度和高度，若没有则加载默认配置
            int width = ServletRequestUtils.getIntParameter(request, &quot;width&quot;, mySecurityProperties.getBrowser().getValidateCode().getImage().getWidth());
            int height = ServletRequestUtils.getIntParameter(request, &quot;height&quot;, mySecurityProperties.getBrowser().getValidateCode().getImage().getHeight());
            Random random = new Random();
//	    默认背景为黑色
            BufferedImage image = new BufferedImage(width, height, BufferedImage.TYPE_INT_RGB);
//		获取画笔
            Graphics graphics = image.getGraphics();
//		默认填充为白色
            graphics.fillRect(0, 0, width, height);
//	            验证码是由	数字 字母 干扰线 干扰点组成
//		文字素材
            String words = &quot;ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz123456789&quot;;
            char[] cs = words.toCharArray();
//		一般验证码为4位数
//      字母+数字
            StringBuilder randomStr = new StringBuilder();
            for (int i = 0; i &amp;lt; mySecurityProperties.getBrowser().getValidateCode().getImage().getLength(); i++) {
                //设置随机的颜色
                graphics.setColor(new Color(random.nextInt(256), random.nextInt(256), random.nextInt(256)));
                graphics.setFont(new Font(&quot;微软雅黑&quot;, Font.BOLD, 30));
                char c = cs[random.nextInt(cs.length)];
                graphics.drawString(c + &quot;&quot;, i * 20, 30);
                randomStr.append(c);
            }
            //画干扰线
            int max = random.nextInt(10);
            for (int i = 0; i &amp;lt; max; i++) {
                graphics.setColor(new Color(random.nextInt(256), random.nextInt(256), random.nextInt(256)));
                graphics.drawLine(random.nextInt(100), random.nextInt(50), random.nextInt(100), random.nextInt(50));
            }
//		画干扰点
            int max2 = random.nextInt(10);
            for (int i = 0; i &amp;lt; max2; i++) {
                graphics.setColor(new Color(random.nextInt(256), random.nextInt(256), random.nextInt(256)));
                graphics.drawOval(random.nextInt(80), random.nextInt(40), random.nextInt(5), random.nextInt(10));
            }

            return new ImageCode(image, randomStr.toString(), mySecurityProperties.getBrowser().getValidateCode().getImage().getExpireTime());
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;八、添加过滤路径到webSecurityConfig&lt;/h3&gt;
&lt;p&gt;核心： &lt;code&gt;/code/*&lt;/code&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;                .and()
                    .authorizeRequests()
                    .antMatchers(
                            &quot;/code/*&quot;,
                                &quot;/authentication/require&quot;,
                            securityProperties.getBrowser().getLoginPage()
                                ).permitAll()
                    .anyRequest().authenticated()
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;实现短信验证码&lt;/h2&gt;
&lt;h3&gt;一、流程图&lt;/h3&gt;
&lt;p&gt;&lt;img src=&quot;%E7%9F%AD%E4%BF%A1%E9%AA%8C%E8%AF%81%E7%A0%81%E8%AE%A4%E8%AF%81%E6%B5%81%E7%A8%8B.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;仿照UsernamePasswrodAuthenticationFilter写自己的Sms过滤器&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;二、创建SmsAuthenticationFilterToken&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;/**
 * &amp;lt;p&amp;gt;DESC: 短信验证码--认证Token信息--对象类&amp;lt;/p&amp;gt;
 * &amp;lt;p&amp;gt;DATE: 2019-08-08 15:19&amp;lt;/p&amp;gt;
 * &amp;lt;p&amp;gt;VERSION:2.0.0&amp;lt;/p&amp;gt;
 * &amp;lt;p&amp;gt;@AUTHOR: ZhengYong&amp;lt;/p&amp;gt;
 */
public class SmsCodeAuthenticationToken extends AbstractAuthenticationToken {

    private static final long serialVersionUID = 510L;
    /**
     * 存放认证信息的对象
     */
    private final Object principal;

    //实际上放的是密码
///    private Object credentials;

    /**
     * 没登录的时候存入手机号
     * 将我们的手机号放入Token
     *
     * @param mobile 手机号
     */
    public SmsCodeAuthenticationToken(String mobile) {
        super(null);
        this.principal = mobile;
        this.setAuthenticated(false);
    }

    /**
     * 登录成功后，放入用户信息
     * @param principal 用户信息
     * @param authorities 用户授权信息
     */
    public SmsCodeAuthenticationToken(Object principal, Collection&amp;lt;? extends GrantedAuthority&amp;gt; authorities) {
        super(authorities);
        this.principal = principal;
        super.setAuthenticated(true);
    }

    @Override
    public Object getCredentials() {
        return null;
    }

    @Override
    public Object getPrincipal() {
        return this.principal;
    }

    @Override
    public void setAuthenticated(boolean isAuthenticated) throws IllegalArgumentException {
        if (isAuthenticated) {
            throw new IllegalArgumentException(&quot;Cannot set this token to trusted - use constructor which takes a GrantedAuthority list instead&quot;);
        } else {
            super.setAuthenticated(false);
        }
    }

    @Override
    public void eraseCredentials() {
        super.eraseCredentials();
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;三、创建SmsAuthenticationFilter&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;/**
 * &amp;lt;p&amp;gt;DESC: 短信验证码认证过滤器&amp;lt;/p&amp;gt;
 * &amp;lt;p&amp;gt;DATE: 2019-08-08 15:56&amp;lt;/p&amp;gt;
 * &amp;lt;p&amp;gt;VERSION:2.0.0&amp;lt;/p&amp;gt;
 * &amp;lt;p&amp;gt;@AUTHOR: ZhengYong&amp;lt;/p&amp;gt;
 */
public class SmsCodeAuthenticationFilter extends AbstractAuthenticationProcessingFilter {

    public static final String EIM_SMS_MOBILE_KEY = &quot;mobile&quot;;

    /**
     * 在请求中，携带参数的名字叫什么
     */
    private String mobileParameter = EIM_SMS_MOBILE_KEY;
    /**
     * 当前过滤器只处理 post请求
     */
    private boolean postOnly = true;

    public SmsCodeAuthenticationFilter() {
        super(new AntPathRequestMatcher(&quot;/authentication/mobile&quot;, &quot;POST&quot;));
    }

    /**
     * 真正处理请求
     * @param request 请求
     * @param response 响应
     * @return 认证信息
     * @throws AuthenticationException 认证信息异常
     */
    @Override
    public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response) throws AuthenticationException {
        if (this.postOnly &amp;amp;&amp;amp; !request.getMethod().equals(&quot;POST&quot;)) {
            throw new AuthenticationServiceException(&quot;Authentication method not supported: &quot; + request.getMethod());
        } else {
            String mobile = this.obtainMobile(request);

            if (mobile == null) {
                mobile = &quot;&quot;;
            }
            mobile = mobile.trim();
            SmsCodeAuthenticationToken authRequest = new SmsCodeAuthenticationToken(mobile);
            this.setDetails(request, authRequest);
            //实际上是在用authenticationToken去调用authenticationManager
            return this.getAuthenticationManager().authenticate(authRequest);
        }
    }


    protected String obtainMobile(HttpServletRequest request) {
        return request.getParameter(this.mobileParameter);
    }

    protected void setDetails(HttpServletRequest request, SmsCodeAuthenticationToken authRequest) {
        authRequest.setDetails(this.authenticationDetailsSource.buildDetails(request));
    }

    public void setUsernameParameter(String mobileParameter) {
        Assert.hasText(mobileParameter, &quot;Username parameter must not be empty or null&quot;);
        this.mobileParameter = mobileParameter;
    }

    public void setPostOnly(boolean postOnly) {
        this.postOnly = postOnly;
    }

    public final String getMobileParameter() {
        return this.mobileParameter;
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;四、创建SmsCodeAuthenticationProvider&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;/**
 * &amp;lt;p&amp;gt;DESC: 短信验证码服务提供类&amp;lt;/p&amp;gt;
 * &amp;lt;p&amp;gt;DATE: 2019-08-08 16:09&amp;lt;/p&amp;gt;
 * &amp;lt;p&amp;gt;VERSION:2.0.0&amp;lt;/p&amp;gt;
 * &amp;lt;p&amp;gt;@AUTHOR: ZhengYong&amp;lt;/p&amp;gt;
 */
@Data
public class SmsCodeAuthenticationProvider implements AuthenticationProvider {

    private UserDetailsService userDetailsService;

    /**
     * 认证用户逻辑
     * @param authentication 用户信息
     * @return 授权后的用户认证信息
     * @throws AuthenticationException 认证异常
     */
    @Override
    public Authentication authenticate(Authentication authentication) throws AuthenticationException {
        SmsCodeAuthenticationToken smsCodeAuthenticationToken = (SmsCodeAuthenticationToken) authentication;
        Object principal = smsCodeAuthenticationToken.getPrincipal();
        String mobile = (String) principal;
        //根据手机号读取用户信息
        UserDetails userDetails = userDetailsService.loadUserByUsername(mobile);
        if(userDetails == null){
            throw new InternalAuthenticationServiceException(&quot;无法获取用户信息&quot;);
        }

        //如果读到了用户信息，重新封装一个已认证Token
        SmsCodeAuthenticationToken tokenResult = new SmsCodeAuthenticationToken(userDetails,userDetails.getAuthorities());
        //将未认证的details添加到已认证的token中
        tokenResult.setDetails(smsCodeAuthenticationToken.getDetails());
        return tokenResult;
    }

    /**
     * 判断当前Provider是否支持SmsToken
     * @param aClass 类型
     * @return true支持，false不支持
     */
    @Override
    public boolean supports(Class&amp;lt;?&amp;gt; aClass) {
        return SmsCodeAuthenticationToken.class.isAssignableFrom(aClass);
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;五、配置验证码过滤器&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;/**
 * &amp;lt;p&amp;gt;@Description:短信验证码过滤器&amp;lt;/p&amp;gt;
 * &amp;lt;p&amp;gt;@Author: zhengyong&amp;lt;/p&amp;gt;
 * &amp;lt;p&amp;gt;@Date: 2019/7/30 21:45&amp;lt;/p&amp;gt;
 * &amp;lt;p&amp;gt;@Version: 1.0.0&amp;lt;/p&amp;gt;
 **/
@EqualsAndHashCode(callSuper = true)
@Data
@Component
public class SmsCodeFilter extends OncePerRequestFilter {

    private SessionStrategy sessionStrategy = new HttpSessionSessionStrategy();

    /**
     * 失败处理器
     */
    @Resource
    private AuthenticationFailureHandler failureHandler;


    @Resource
    private MySecurityProperties mySecurityProperties;

    private AntPathMatcher pathPattern = new AntPathMatcher();

    private static final String IMAGE_SESSION_KEY = SESSION_KEY_PREFIX + &quot;SMS&quot;;

    /**
     * 业务逻辑
     * @param request request
     * @param response response
     * @param filterChain filterChain
     * @throws ServletException  异常
     * @throws IOException  异常
     */
    @Override
    protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {

///        List&amp;lt;String&amp;gt; urlList = mySecurityProperties.getBrowser().getValidateCode().getImage().getUrl();
        List&amp;lt;String&amp;gt; urlList = Collections.singletonList(&quot;/authentication/mobile&quot;);

        //判断是否需要校验
        AtomicBoolean isFilter = new AtomicBoolean(false);
        urlList.forEach(url -&amp;gt; {
            if(pathPattern.match(url,request.getRequestURI())){
                isFilter.set(true);
            }
        });

        //拦截符合标准的请求
        if(isFilter.get()){
            try{
                //校验逻辑
                validate(new ServletWebRequest(request));
            }catch (ValidateException e){
                //失败处理逻辑
                failureHandler.onAuthenticationFailure(request,response,e);
                return;
            }
        }
        //放行
        filterChain.doFilter(request,response);
    }

    /**
     * 校验逻辑
     * @param request ServletWebRequest
     * @throws ServletRequestBindingException 异常
     */
    private void validate(ServletWebRequest request) throws ServletRequestBindingException {
        //从session中获取imageCode对象
        ValidateCode codeInSession = (ValidateCode) sessionStrategy.getAttribute(request,IMAGE_SESSION_KEY);
        //从请求中获取验证码数字
        String codeInRequest = ServletRequestUtils.getStringParameter(request.getRequest(), &quot;smsCode&quot;);

         if(StringUtils.isBlank(codeInRequest)){
            throw new ValidateException(&quot;验证码不能为空&quot;);
        }
        if(StringUtils.isBlank(codeInSession.getCode())){
            throw new ValidateException(&quot;验证码不存在&quot;);
        }
        if(codeInSession.isExpired()){
            sessionStrategy.removeAttribute(request,IMAGE_SESSION_KEY);
            throw new ValidateException(&quot;验证码已过期&quot;);
        }
        if(!StringUtils.equals(codeInRequest,codeInSession.getCode())){
            throw new ValidateException(&quot;验证码不匹配&quot;);
        }
        sessionStrategy.removeAttribute(request,IMAGE_SESSION_KEY);
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;六、配置自己的流程到security&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;/**
 * &amp;lt;p&amp;gt;DESC: 短信验证码安全配置类&amp;lt;/p&amp;gt;
 * &amp;lt;p&amp;gt;DATE: 2019-08-08 16:28&amp;lt;/p&amp;gt;
 * &amp;lt;p&amp;gt;VERSION:2.0.0&amp;lt;/p&amp;gt;
 * &amp;lt;p&amp;gt;@AUTHOR: ZhengYong&amp;lt;/p&amp;gt;
 */
@Component
public class SmsCodeAuthenticationSecurityConfig extends SecurityConfigurerAdapter&amp;lt;DefaultSecurityFilterChain, HttpSecurity&amp;gt; {

    @Resource
    private MyAuthenticationFailureHandler failureHandler;

    @Resource
    private MyAuthenticationSuccessHandler successHandler;

    @Resource
    private UserDetailsService userDetailsService;

    @Override
    public void configure(HttpSecurity http) throws Exception {
        SmsCodeAuthenticationFilter smsCodeAuthenticationFilter = new SmsCodeAuthenticationFilter();
        //filter中需要配置ProviderManager
        smsCodeAuthenticationFilter.setAuthenticationManager(http.getSharedObject(AuthenticationManager.class));
        //配置成功处理器
        smsCodeAuthenticationFilter.setAuthenticationFailureHandler(failureHandler);
        //配置失败处理器
        smsCodeAuthenticationFilter.setAuthenticationSuccessHandler(successHandler);

        //设置Provider
        SmsCodeAuthenticationProvider smsCodeAuthenticationProvider = new SmsCodeAuthenticationProvider();
        //设置用户信息
        smsCodeAuthenticationProvider.setUserDetailsService(userDetailsService);

        //添加进ProviderManager
        http.authenticationProvider(smsCodeAuthenticationProvider)
            .addFilterAfter(smsCodeAuthenticationFilter, UsernamePasswordAuthenticationFilter.class);

    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;七、将配置生效到对应模块（browser）&lt;/h3&gt;
&lt;p&gt;核心：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;//添加过滤器在usernamePasswordAuthenticationFilter
.addFilterBefore(smsCodeFilter,UsernamePasswordAuthenticationFilter.class)
//注入自定义短信验证码的安全配置
                .apply(smsCodeAuthenticationSecurityConfig)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;完整:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;/**
 * &amp;lt;p&amp;gt;DESC: web安全配置&amp;lt;/p&amp;gt;
 * &amp;lt;p&amp;gt;DATE: 2019-07-24 16:47&amp;lt;/p&amp;gt;
 * &amp;lt;p&amp;gt;VERSION:2.0.0&amp;lt;/p&amp;gt;
 * &amp;lt;p&amp;gt;@AUTHOR: ZhengYong&amp;lt;/p&amp;gt;
 */
@Component
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {

    @Resource
    private MySecurityProperties securityProperties;

    @Resource
    private MyAuthenticationSuccessHandler myAuthenticationSuccessHandler;

    @Resource
    private MyAuthenticationFailureHandler myAuthenticationFailureHandler;

    @Resource
    private ValidateCodeFilter validateCodeFilter;

    @Resource
    private SmsCodeFilter smsCodeFilter;

    @Resource
    private SmsCodeAuthenticationSecurityConfig smsCodeAuthenticationSecurityConfig;

    /**
     * 配置用户登录逻辑
     */
    @Resource
    private UserDetailsService userDetailsService;
    /**
     * 数据源(sql包下的)
     */
    @Resource
    private DataSource dataSource;
    /**
     * 配置一个持久化token仓库
     * @return
     */
    @Bean
    public PersistentTokenRepository persistentTokenRepository(){
        //新建一个PersistentTokenRepository的实现类
        JdbcTokenRepositoryImpl tokenRepository = new JdbcTokenRepositoryImpl();
        //需要设置数据源
        tokenRepository.setDataSource(dataSource);
        //启动的时候去创建表（默认存储token的表）
//        tokenRepository.setCreateTableOnStartup(true);
        return tokenRepository;
    }

    @Override
    protected void configure(HttpSecurity http) throws Exception {


        http
                .addFilterBefore(smsCodeFilter,UsernamePasswordAuthenticationFilter.class)
                //在用户名密码过滤器前面加上自定义过滤器
                .addFilterBefore(validateCodeFilter, UsernamePasswordAuthenticationFilter.class)
                .formLogin()
                    .loginPage(&quot;/authentication/require&quot;)
                    //form表单登录请求
                    .loginProcessingUrl(&quot;/authentication/login&quot;)
                    //自定义成功处理器
                    .successHandler(myAuthenticationSuccessHandler)
                    //自定义失败处理器
                    .failureHandler(myAuthenticationFailureHandler)
                    .and()
                .rememberMe()
                    //配置持久化token
                    .tokenRepository(persistentTokenRepository())
                    //配置token验证秒数
                    .tokenValiditySeconds(securityProperties.getBrowser().getRememberMeSeconds())
                    //配置用于登录的UserDetailsService
                    .userDetailsService(userDetailsService)
                .and()
                    .authorizeRequests()
                    .antMatchers(
                            &quot;/code/*&quot;,
                                &quot;/authentication/require&quot;,
                            securityProperties.getBrowser().getLoginPage()
                                ).permitAll()
                    .anyRequest().authenticated()
                .and()
                    .csrf().disable()
                //注入自定义短信验证码的安全配置
                .apply(smsCodeAuthenticationSecurityConfig)
                    ;
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;抽取配置config(http)&lt;/h2&gt;
&lt;h3&gt;一、使用apply&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;继承SecurityConfigurerAdapter，配置http&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;@Component
public class SmsCodeAuthenticationSecurityConfig extends SecurityConfigurerAdapter&amp;lt;DefaultSecurityFilterChain, HttpSecurity&amp;gt; {

    /**
     * 配置
     * @param http http
     * @throws Exception 异常
     */
    @Override
    public void configure(HttpSecurity http) throws Exception {
&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;使用apply加入主配置&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;@Component
public class BrowserSecurityConfig extends WebSecurityConfigurerAdapter {
  
    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http
                .apply(smsCodeAuthenticationSecurityConfig).and()
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;二、使用抽象配置，适用于表单&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;编写一个类加入spring容器，&lt;/li&gt;
&lt;li&gt;写一个 public void configure(HttpSecurity http)  方法&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;@Component
public class FormSecurityConfig  {

    @Resource
    private MySecurityProperties properties;

    @Resource
    private MySuccessHandler successHandler;

    @Resource
    private MyFailHandler failHandler;

    @Resource
    private UserDetailsService userDetailsService;

    @Resource
    private DataSource dataSource;

    /**
     * 配置token持久化
     * @return 向spring中添加
     */
    @Bean
    public PersistentTokenRepository persistentTokenRepository(){
        JdbcTokenRepositoryImpl tokenRepository = new JdbcTokenRepositoryImpl();
        tokenRepository.setDataSource(dataSource);
        return tokenRepository;
    }

    public void configure(HttpSecurity http) throws Exception {
        http
                .formLogin()
                .loginPage(properties.getBrowser().getLoginPage())
          .........
&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;加入主配置类&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;@Component
public class BrowserSecurityConfig extends WebSecurityConfigurerAdapter {

    @Resource
    private FormSecurityConfig formSecurityConfig;

    @Override
    protected void configure(HttpSecurity http) throws Exception {

        //第一优先级，手动加入，不能apply进来会报错。
        formSecurityConfig.configure(http);
      
        http...........
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;oauth&lt;/h2&gt;
&lt;h3&gt;一、oauth协议要解决的问题&lt;/h3&gt;
&lt;p&gt;&lt;img src=&quot;oauth%E5%8D%8F%E8%AE%AE%E5%9C%BA%E6%99%AF.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;通过令牌而不是密码的方式，解决一下问题。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;应用可以访问用户在微信上的所有数据&lt;/li&gt;
&lt;li&gt;用户只有修改密码，才能收回授权&lt;/li&gt;
&lt;li&gt;密码泄露的可能性大大提高&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;二、oauth协议中的各种角色&lt;/h3&gt;
&lt;p&gt;&lt;img src=&quot;oauth%E8%A7%92%E8%89%B2.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;角色：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;服务提供商（provider），谁提供令牌谁就是服务提供商。&lt;/li&gt;
&lt;li&gt;服务提供商中--认证服务器&lt;/li&gt;
&lt;li&gt;服务提供商中--资源服务器（eg,提供用户自拍数据）&lt;/li&gt;
&lt;li&gt;资源所有者（resource owner）&lt;/li&gt;
&lt;li&gt;第三方应用&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;三、oauth协议运行流程&lt;/h3&gt;
&lt;p&gt;&lt;img src=&quot;oauth%E5%8D%8F%E8%AE%AE%E5%9F%BA%E6%9C%AC%E8%BF%90%E8%A1%8C%E6%B5%81%E7%A8%8B.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;注：&lt;/p&gt;
&lt;p&gt;第二步统一授权，有四种方式来实现：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;授权码模式（最完整的）&lt;/li&gt;
&lt;li&gt;简化模式&lt;/li&gt;
&lt;li&gt;密码模式&lt;/li&gt;
&lt;li&gt;客户端模式&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;授权码模式&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;%E6%8E%88%E6%9D%83%E7%A0%81%E6%A8%A1%E5%BC%8F%E8%BF%90%E8%A1%8C%E6%B5%81%E7%A8%8B.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;1）、用户同意授权动作在服务提供商中的认证服务器上完成的&lt;/p&gt;
&lt;p&gt;2）、授权成功后导向第三方应用时，携带的是code（授权码）。并通过授权再次请求认证服务器，去申请令牌。（安全更高）&lt;/p&gt;
&lt;h2&gt;springsocial基本原理&lt;/h2&gt;
&lt;p&gt;&lt;img src=&quot;social%E5%9F%BA%E6%9C%AC%E6%B5%81%E7%A8%8B.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;ServiceProvider(服务提供商的抽象，eg:QQ,WEIBO),springsocial提供了一个AbstractOauth2ServiceProvider一个抽象类。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;serviceProvider中的操作类：&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;OAuth2Operations(oauth2协议相关的一下操作)，实际上封装了第一步到第五步的处理流程，springsocial提供一个默认实现&lt;code&gt;OAuth2Template&lt;/code&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;blockquote&gt;
&lt;p&gt;API(封装了用户信息)没有固定的接口。需要自己手动写。实际上是封装第6步的行为。springsocial提供一个抽象类：AbstractOAuth2ApiBinding&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;第七步相关的接口和类：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Connection (OAuth2Connection) ：作用封装第六步获取到用户信息。实际用的实现类OAuth2Connection.&lt;/li&gt;
&lt;li&gt;ConnectionFactory：Connection由ConnectionFactory创建出来的。实现类：OAuth2ConnectionFactory。&lt;/li&gt;
&lt;/ul&gt;
&lt;blockquote&gt;
&lt;p&gt;ConnectionFactory中包含了一个ServiceProvider的实例的&lt;/p&gt;
&lt;p&gt;ConnectionFactory中会有一个ServiceProvider去封装1到5步，然后获取到用户信息，封装到Connection中。&lt;/p&gt;
&lt;p&gt;Connection是一个标准的数据接口，但是每一个服务的用户信息都是不一样的，所有此时需要一个适配器来操作。（ApiAdaptor）&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;服务提供商的用户和业务系统中的用户有一个关联关系：&lt;/p&gt;
&lt;p&gt;DB（UserConnetion）数据库中有一张表在维护。&lt;/p&gt;
&lt;p&gt;由谁来操作这张维护表呢？UserConnectionRepository(jdbcUserConnectionRepository)来操作&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;springSocial%E5%AE%9E%E7%8E%B0%E9%9C%80%E8%A6%81%E7%9A%84%E6%8E%A5%E5%8F%A3%E5%92%8C%E7%B1%BB.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;h2&gt;实现QQ的登录&lt;/h2&gt;
&lt;p&gt;&lt;code&gt;开发流程&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;QQ%E7%99%BB%E5%BD%95%E5%88%9D%E7%89%88%E5%BC%80%E5%8F%91%E6%B5%81%E7%A8%8B.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;h3&gt;QQ互联官网查看文档（https://connect.qq.com/）&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;查看get_user_info  接口&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;img src=&quot;qq%E7%94%A8%E6%88%B7%E4%BF%A1%E6%81%AF.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;3.2输入参数说明&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;各个参数请进行URL 编码，编码时请遵守 &lt;a href=&quot;http://tools.ietf.org/html/rfc1738&quot;&gt;RFC 1738&lt;/a&gt;。
（1）通用参数
-OAuth2.0协议必须传入的通用参数，详见&lt;a href=&quot;http://wiki.connect.qq.com/openapi%E8%B0%83%E7%94%A8%E8%AF%B4%E6%98%8E_oauth2-0&quot;&gt;这里&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;3.3请求示例&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;以OAuth2.0协议为例（敏感信息都用*号进行了处理，实际请求中需要替换成真实的值）：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;https://graph.qq.com/user/get_user_info?
access_token=*************&amp;amp;
oauth_consumer_key=12345&amp;amp;
openid=****************
&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;解读：获取用户信息需要，token（调用j接口获取）,openid（调用接口获取）,appid（QQ分配）。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;使用Authorization_Code获取Access_Token&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;### 2. 过程详解

#### Step1：获取Authorization Code

**请求地址**：
PC网站：https://graph.qq.com/oauth2.0/authorize
**请求方法**：
GET

Step2：通过Authorization Code获取Access Token
请求地址：
PC网站：https://graph.qq.com/oauth2.0/token
请求方法：
GET
&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;获取openId是关键，官方文档也给出解释&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;1 请求地址
PC网站：https://graph.qq.com/oauth2.0/me

2 请求方法
GET

3 请求参数
请求参数请包含如下内容：

参数	是否必须	含义
access_token	必须	在Step1中获取到的access token。
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;先创建serviceProvider中的 API 和 Oauth2Opreations&lt;/code&gt;&lt;/p&gt;
&lt;h3&gt;API&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;/**
 * &amp;lt;p&amp;gt;DESC: &amp;lt;/p&amp;gt;
 * &amp;lt;p&amp;gt;DATE: 2019-11-04 22:37&amp;lt;/p&amp;gt;
 * &amp;lt;p&amp;gt;VERSION:1.0.0&amp;lt;/p&amp;gt;
 * &amp;lt;p&amp;gt;@AUTHOR: ZhengYong&amp;lt;/p&amp;gt;
 */
public interface QQ {

    QQUserInfo getQQUserInfo();
}
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;/**
 * &amp;lt;p&amp;gt;DESC: &amp;lt;/p&amp;gt;
 * &amp;lt;p&amp;gt;DATE: 2019-11-04 22:38&amp;lt;/p&amp;gt;
 * &amp;lt;p&amp;gt;VERSION:1.0.0&amp;lt;/p&amp;gt;
 * &amp;lt;p&amp;gt;@AUTHOR: ZhengYong&amp;lt;/p&amp;gt;
 */
public class QQImpl extends AbstractOAuth2ApiBinding implements QQ {

    private String appId;

    private String openId;

    //获取openid
    private static final String GET_OPEN_ID_URL = &quot;https://graph.qq.com/oauth2.0/me?access_token=%s&quot;;

    //获取用户信息
    private static final String GET_USER_INFO = &quot;https://graph.qq.com/user/get_user_info?oauth_consumer_key=%s&amp;amp;openid=%s&quot;;

    public QQImpl(String accessToken,String appId) {
        //将accessToken作为参数写到url 上
        super(accessToken, TokenStrategy.ACCESS_TOKEN_PARAMETER);
        this.appId = appId;

        String format = String.format(GET_OPEN_ID_URL, accessToken);
        String result = getRestTemplate().getForObject(format, String.class);
        JSONObject jsonObject = JSONObject.parseObject(result);
        String openid = (String) jsonObject.get(&quot;openid&quot;);
        this.openId = openid;
    }

    @Override
    public QQUserInfo getQQUserInfo() {
        String format = String.format(GET_USER_INFO, this.appId,this.openId);
        String result = getRestTemplate().getForObject(format, String.class);
        JSONObject jsonObject = JSONObject.parseObject(result);
        if(!((Integer)jsonObject.get(&quot;ret&quot;)).equals(0)){
            throw new RuntimeException(&quot;无法获取&quot;);
        }
        return JSONObject.parseObject(result,QQUserInfo.class);
    }

}
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;/**
 * &amp;lt;p&amp;gt;DESC: qq用户信息&amp;lt;/p&amp;gt;
 * &amp;lt;p&amp;gt;DATE: 2019-11-04 22:37&amp;lt;/p&amp;gt;
 * &amp;lt;p&amp;gt;VERSION:1.0.0&amp;lt;/p&amp;gt;
 * &amp;lt;p&amp;gt;@AUTHOR: ZhengYong&amp;lt;/p&amp;gt;
 */
@Data
public class QQUserInfo {
    private Integer ret;
    private String msg;
    private String nickname;
    private String figureurl;
    private String figureurl_1;
    private String figureurl_2;
    private String figureurl_qq_1;
    private String figureurl_qq_2;
    private String gender;
    private String is_yellow_vip;
    private String vip;
    private String yellow_vip_level;
    private String level;
    private String is_yellow_year_vip;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Oauth2Opreations&lt;/h3&gt;
&lt;p&gt;&lt;code&gt;使用默认的OAuth2Template&lt;/code&gt;&lt;/p&gt;
&lt;h3&gt;serviceProvider&lt;/h3&gt;
&lt;blockquote&gt;
&lt;p&gt;需要制定api泛型&lt;/p&gt;
&lt;/blockquote&gt;
&lt;pre&gt;&lt;code&gt;/**
 * &amp;lt;p&amp;gt;DESC: qq，serviceProvider&amp;lt;/p&amp;gt;
 * &amp;lt;p&amp;gt;DATE: 2019-11-04 23:10&amp;lt;/p&amp;gt;
 * &amp;lt;p&amp;gt;VERSION:1.0.0&amp;lt;/p&amp;gt;
 * &amp;lt;p&amp;gt;@AUTHOR: ZhengYong&amp;lt;/p&amp;gt;
 */
public class QQServiceProvider extends AbstractOAuth2ServiceProvider&amp;lt;QQ&amp;gt; {

    private String appId;

    /**
     * 获取code
     */
    private static final String URL_AUTHORIZE = &quot;https://graph.qq.com/oauth2.0/authorize&quot;;

    /**
     * 获取token
     */
    private static final String URL_ACCESS_TOKEN= &quot;https://graph.qq.com/oauth2.0/token&quot;;

    //provider构造方法
    public QQServiceProvider(String appId,String appSecret) {
        //需要 Oauth2Opreations
        super(new OAuth2Template(appId,appSecret,URL_AUTHORIZE,URL_ACCESS_TOKEN));
    }

    // api
    @Override
    public QQ getApi(String accessToken) {
        return new QQImpl(accessToken,appId);
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;ApiAdapter&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;/**
 * &amp;lt;p&amp;gt;DESC: qq api适配器&amp;lt;/p&amp;gt;
 * &amp;lt;p&amp;gt;DATE: 2019-11-05 21:12&amp;lt;/p&amp;gt;
 * &amp;lt;p&amp;gt;VERSION:1.0.0&amp;lt;/p&amp;gt;
 * &amp;lt;p&amp;gt;@AUTHOR: ZhengYong&amp;lt;/p&amp;gt;
 */
public class QQAdapter implements ApiAdapter&amp;lt;QQ&amp;gt; {

    /**
     * 测试api获取用户信息接口是否通的
     * @param qq  qq 实现
     * @return  boolean
     */
    @Override
    public boolean test(QQ qq) {
        return false;
    }

    /**
     * 将我们的第三方信息和ConnectionValues 适配
     * 设置 ConnectionValues
     * @param qq   api
     * @param connectionValues ConnectionValues
     */
    @Override
    public void setConnectionValues(QQ qq, ConnectionValues connectionValues) {
        QQUserInfo qqUserInfo = qq.getQQUserInfo();
        //设置展示名字
        connectionValues.setDisplayName(qqUserInfo.getNickname());
        //设置头像URL
        connectionValues.setImageUrl(qqUserInfo.getFigureurl_qq_1());
        //设置首页地址
        connectionValues.setProfileUrl(null);
        //设置openID
        connectionValues.setProviderUserId(qqUserInfo.getOpenId());
    }

    /**
     * 获取用户的概要信息
     * @param qq api
     * @return 用户概要信息
     */
    @Override
    public UserProfile fetchUserProfile(QQ qq) {
        return null;
    }

    /**
     * 更新状态
     * @param qq
     * @param s
     */
    @Override
    public void updateStatus(QQ qq, String s) {
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;ConnectionFactory&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;/**
 * &amp;lt;p&amp;gt;DESC: QQConnectionFactory&amp;lt;/p&amp;gt;
 * &amp;lt;p&amp;gt;DATE: 2019-11-05 21:22&amp;lt;/p&amp;gt;
 * &amp;lt;p&amp;gt;VERSION:1.0.0&amp;lt;/p&amp;gt;
 * &amp;lt;p&amp;gt;@AUTHOR: ZhengYong&amp;lt;/p&amp;gt;
 */
public class QQConnectionFactory extends OAuth2ConnectionFactory&amp;lt;QQ&amp;gt; {

    /**
     * providerId 应用ID  这里为  qq
     */
    public QQConnectionFactory(String providerId,String appId,String appSecret) {
        super(providerId, new QQServiceProvider(appId,appSecret), new QQAdapter());
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Connection对象&lt;/h3&gt;
&lt;p&gt;Connection对象的获取，不用我们去管，只要我们弄好ConnectionFactory对象，springsocial会自动根据ConnectionFactory对象生成相对应的Connection对象。&lt;/p&gt;
&lt;h3&gt;配置数据库操作&lt;/h3&gt;
&lt;p&gt;还有： 添加到security过滤链中&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;/**
 * &amp;lt;p&amp;gt;DESC: social 配置&amp;lt;/p&amp;gt;
 * &amp;lt;p&amp;gt;DATE: 2019-11-05 21:26&amp;lt;/p&amp;gt;
 * &amp;lt;p&amp;gt;VERSION:1.0.0&amp;lt;/p&amp;gt;
 * &amp;lt;p&amp;gt;@AUTHOR: ZhengYong&amp;lt;/p&amp;gt;
 */
@Configuration
@EnableSocial
public class SocialConfig extends SocialConfigurerAdapter {

    @Resource
    private DataSource dataSource;

    /**
     * 将我们 JdbcUsersConnectionRepository 配置好
     * @param connectionFactoryLocator locator
     * @return UsersConnectionRepository
     */
    @Override
    public UsersConnectionRepository getUsersConnectionRepository(ConnectionFactoryLocator connectionFactoryLocator) {
         /**
         * 第二个参数的作用：根据条件查找该用哪个ConnectionFactory来构建Connection对象
         * 第三个参数的作用: 对插入到userconnection表中的数据进行加密和解密
         */
        JdbcUsersConnectionRepository repository = new JdbcUsersConnectionRepository(dataSource, connectionFactoryLocator, Encryptors.noOpText());
      //设置userconnection表的前缀,也可以不设置
        repository.setTablePrefix(&quot;dftc_&quot;);
        return repository;
    }

    @Bean
    public SpringSocialConfigurer imoocSocialSecurityConfig() {
        // 默认配置类，进行组件的组装
        // 包括了过滤器SocialAuthenticationFilter 添加到security过滤链中
        return new SpringSocialConfigurer();
    }

}
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;UserConnection表&lt;/h3&gt;
&lt;p&gt;是springsocial为我们准备的一张用来维护我们的系统和QQ、微信、微博等第三方应用之间关系的表，建表语句可在如下包目录结构下找到：&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://tangtangit.com/_astro/userConnection.2U94NjBT_Z1Ydmge.webp&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;h3&gt;QQ 自动配置&lt;/h3&gt;
&lt;blockquote&gt;
&lt;p&gt;特别注意： 我最近写的一系列关于spring-security、springsocial、oauth2的文章都算是学习慕课网《Spring Security技术栈开发企业级认证与授权》这个课程的笔记，但是我使用的springboot是2.1.6.RELEASE版本，而老师课程中使用的是1.5.6.RELEASE版本。在标题对应的这个问题上，两个版本之间存在一些比较大的差异。
差异原因: 2.1.6.RELEASE版本的springboot相比于1.5.6.RELEASE版本来说org.springframework.boot.autoconfigure里没有social相关的配置简化类。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h4&gt;springboot为1.5.6.RELEASE的版本&lt;/h4&gt;
&lt;pre&gt;&lt;code&gt;package com.imooc.security.core.social.qq.config;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.boot.autoconfigure.social.SocialAutoConfigurerAdapter;
import org.springframework.context.annotation.Configuration;
import org.springframework.social.connect.ConnectionFactory;

import com.imooc.security.core.properties.QQProperties;
import com.imooc.security.core.properties.SecurityProperties;
import com.imooc.security.core.social.qq.connet.QQConnectionFactory;
@Configuration
@ConditionalOnProperty(prefix = &quot;imooc.security.social.qq&quot;, name = &quot;app-id&quot;)
public class QQAutoConfig extends SocialAutoConfigurerAdapter {
	/**这里不多解释，就是将providerId,appId和appSecret写入到配置类，
	然后这里注入配置类，就可以读到这些信息
	可以下载源码进行查看*/
	@Autowired
	private SecurityProperties securityProperties;
	@Override
	protected ConnectionFactory&amp;lt;?&amp;gt; createConnectionFactory() {
		QQProperties qqConfig = securityProperties.getSocial().getQq();
		return new QQConnectionFactory(qqConfig.getProviderId(), qqConfig.getAppId(), qqConfig.getAppSecret());
	}
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;注意看import&lt;/code&gt;: 上面的SocialAutoConfigurerAdapter 来自于&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;org.springframework.boot.autoconfigure.social.SocialAutoConfigurerAdapter;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;其实老师在配置providerId,appId和appSecret属性时也用到了org.springframework.boot.autoconfigure.social里的一个类，代码如下，（完整代码可以从老师的github地址下载：https://github.com/jojozhai/security）&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;package com.imooc.security.core.properties;
import org.springframework.boot.autoconfigure.social.SocialProperties;
public class QQProperties extends SocialProperties {
	private String providerId = &quot;qq&quot;;
	public String getProviderId() {
		return providerId;
	}
	public void setProviderId(String providerId) {
		this.providerId = providerId;
	}
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h4&gt;springboot1.5.6和2.1.5中autoconfigure包对比及2.1.5中的解决方法&lt;/h4&gt;
&lt;p&gt;&lt;img src=&quot;1.5.6%E5%92%8C2.1.6%E7%89%88%E6%9C%AC%E5%8C%BA%E5%88%AB.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;仔细看一下源码，可以发现SocialAutoConfigurerAdapter和SocialProperties特别简单。&lt;/p&gt;
&lt;p&gt;其实看到social这个包下面的类，我们解决这个问题的方式就多了，比如说&lt;/p&gt;
&lt;p&gt;参照Facebook、LinkedIn或Twitter的栗子再联系一下各个类之间的依赖关系去自己写配置类
或者&lt;/p&gt;
&lt;p&gt;解决方法：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;直接将SocialAutoConfigurerAdapter、SocialProperties和SocialWebAutoConfiguration这三个类拷贝到我们项目里 —&amp;gt; 我采用的这种方式（因为比较简单☺），可以去github查看代码commit记录https://github.com/nieandsun/security&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;注意1： 其实我一开始没有拷贝SocialWebAutoConfiguration这个类，因为觉得好像没用，但是后来启动项目时报了如下错误，大体意思就是必须有一个配置类实现getUserIdSource方法,后来发现SocialWebAutoConfiguration类中正好实现了该方法，所以也将其拷贝了过来。&lt;/p&gt;
&lt;p&gt;&lt;code&gt;注意2&lt;/code&gt; SocialWebAutoConfiguration类中有个方法如下，飘红，说明有问题，看该方法的意思是如果有&lt;code&gt;SpringResourceResourceResolver&lt;/code&gt;类型的类时，该方法才会生效。源码里都没该类，所以肯定可以将该方法注释掉。&lt;/p&gt;
&lt;p&gt;SocialAutoConfigurerAdapter(可参照自己实现)&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;public abstract class SocialAutoConfigurerAdapter extends SocialConfigurerAdapter {
    public SocialAutoConfigurerAdapter() {
    }

    @Override
    public void addConnectionFactories(ConnectionFactoryConfigurer configurer, Environment environment) {
        configurer.addConnectionFactory(this.createConnectionFactory());
    }

    protected abstract ConnectionFactory&amp;lt;?&amp;gt; createConnectionFactory();
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;SocialProperties&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;public abstract class SocialProperties {
    private String appId;
    private String appSecret;

    public SocialProperties() {
    }

    public String getAppId() {
        return this.appId;
    }

    public void setAppId(String appId) {
        this.appId = appId;
    }

    public String getAppSecret() {
        return this.appSecret;
    }

    public void setAppSecret(String appSecret) {
        this.appSecret = appSecret;
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;SocialWebAutoConfiguration&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;@Configuration
@ConditionalOnClass({ConnectController.class, SocialConfigurerAdapter.class})
@ConditionalOnBean({ConnectionFactoryLocator.class, UsersConnectionRepository.class})
@AutoConfigureBefore({ThymeleafAutoConfiguration.class})
@AutoConfigureAfter({WebMvcAutoConfiguration.class})
public class SocialWebAutoConfiguration {
    public SocialWebAutoConfiguration() {
    }

    private static class SecurityContextUserIdSource implements UserIdSource {
        private SecurityContextUserIdSource() {
        }

        @Override
        public String getUserId() {
            SecurityContext context = SecurityContextHolder.getContext();
            Authentication authentication = context.getAuthentication();
            Assert.state(authentication != null, &quot;Unable to get a ConnectionRepository: no user signed in&quot;);
            return authentication.getName();
        }
    }

    //@Configuration
    ////@ConditionalOnClass({SpringResourceResourceResolver.class})
    //protected static class SpringSocialThymeleafConfig {
    //    protected SpringSocialThymeleafConfig() {
    //    }
    //
    //    @Bean
    //    @ConditionalOnMissingBean
    //    public SpringSocialDialect springSocialDialect() {
    //        return new SpringSocialDialect();
    //    }
    //}

    @Configuration
    @EnableSocial
    @ConditionalOnWebApplication
    @ConditionalOnClass({SecurityContextHolder.class})
    protected static class AuthenticationUserIdSourceConfig extends SocialConfigurerAdapter {
        protected AuthenticationUserIdSourceConfig() {
        }

        @Override
        public UserIdSource getUserIdSource() {
            return new SocialWebAutoConfiguration.SecurityContextUserIdSource();
        }
    }

    @Configuration
    @EnableSocial
    @ConditionalOnWebApplication
    @ConditionalOnMissingClass({&quot;org.springframework.security.core.context.SecurityContextHolder&quot;})
    protected static class AnonymousUserIdSourceConfig extends SocialConfigurerAdapter {
        protected AnonymousUserIdSourceConfig() {
        }

        @Override
        public UserIdSource getUserIdSource() {
            return new UserIdSource() {
                @Override
                public String getUserId() {
                    return &quot;anonymous&quot;;
                }
            };
        }
    }

    @Configuration
    @EnableSocial
    @ConditionalOnWebApplication
    protected static class SocialAutoConfigurationAdapter extends SocialConfigurerAdapter {
        private final List&amp;lt;ConnectInterceptor&amp;lt;?&amp;gt;&amp;gt; connectInterceptors;
        private final List&amp;lt;DisconnectInterceptor&amp;lt;?&amp;gt;&amp;gt; disconnectInterceptors;
        private final List&amp;lt;ProviderSignInInterceptor&amp;lt;?&amp;gt;&amp;gt; signInInterceptors;

        public SocialAutoConfigurationAdapter(ObjectProvider&amp;lt;List&amp;lt;ConnectInterceptor&amp;lt;?&amp;gt;&amp;gt;&amp;gt; connectInterceptorsProvider, ObjectProvider&amp;lt;List&amp;lt;DisconnectInterceptor&amp;lt;?&amp;gt;&amp;gt;&amp;gt; disconnectInterceptorsProvider, ObjectProvider&amp;lt;List&amp;lt;ProviderSignInInterceptor&amp;lt;?&amp;gt;&amp;gt;&amp;gt; signInInterceptorsProvider) {
            this.connectInterceptors = (List)connectInterceptorsProvider.getIfAvailable();
            this.disconnectInterceptors = (List)disconnectInterceptorsProvider.getIfAvailable();
            this.signInInterceptors = (List)signInInterceptorsProvider.getIfAvailable();
        }

        @Bean
        @ConditionalOnMissingBean({ConnectController.class})
        public ConnectController connectController(ConnectionFactoryLocator factoryLocator, ConnectionRepository repository) {
            ConnectController controller = new ConnectController(factoryLocator, repository);
            if (!CollectionUtils.isEmpty(this.connectInterceptors)) {
                controller.setConnectInterceptors(this.connectInterceptors);
            }

            if (!CollectionUtils.isEmpty(this.disconnectInterceptors)) {
                controller.setDisconnectInterceptors(this.disconnectInterceptors);
            }

            return controller;
        }

        @Bean
        @ConditionalOnMissingBean
        @ConditionalOnProperty(
            prefix = &quot;spring.social&quot;,
            name = {&quot;auto-connection-views&quot;}
        )
        public BeanNameViewResolver beanNameViewResolver() {
            BeanNameViewResolver viewResolver = new BeanNameViewResolver();
            viewResolver.setOrder(-2147483648);
            return viewResolver;
        }

        @Bean
        @ConditionalOnBean({SignInAdapter.class})
        @ConditionalOnMissingBean
        public ProviderSignInController signInController(ConnectionFactoryLocator factoryLocator, UsersConnectionRepository usersRepository, SignInAdapter signInAdapter) {
            ProviderSignInController controller = new ProviderSignInController(factoryLocator, usersRepository, signInAdapter);
            if (!CollectionUtils.isEmpty(this.signInInterceptors)) {
                controller.setSignInInterceptors(this.signInInterceptors);
            }

            return controller;
        }
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;配置BrowserSecurityConfig  。。SocialAuthenticationFilter&lt;/p&gt;
&lt;h3&gt;将SocialAuthenticationFilter通过apply()加入到spring-security过滤器链中&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;    
    @Resource
    private SpringSocialConfigurer imoocSocialSecurityConfig;

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http
                .apply(imoocSocialSecurityConfig).and().....
    }       
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;增加一个QQ登陆按钮&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;&amp;lt;h3&amp;gt;社交登录&amp;lt;/h3&amp;gt;
&amp;lt;a href=&quot;/auth/qq&quot;&amp;gt;QQ登录&amp;lt;/a&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;h4&gt;QQ登陆url中/auth的含义&lt;/h4&gt;
&lt;p&gt;SocialAuthenticationFilter会拦截所有路径中含有/auth的请求&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;auth%E5%90%AB%E4%B9%89.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;h4&gt;QQ登陆url中/qq的含义&lt;/h4&gt;
&lt;p&gt;/qq其实是在构建&lt;code&gt;ConnectionFactory&lt;/code&gt;对象时第一个参数对应的值&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;qq%E5%90%AB%E4%B9%89.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;即当一个请求的url中有/auth/qq时，spring-security会让SocialAuthenticationFilter过滤器将其拦截下来，然后利用我们写的QQConnectionFactory对象来走oauth2流程+构建connection对象。&lt;/p&gt;
&lt;p&gt;在spring social中，该路径 有两个很重要的作用&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;一是跳转应用最终引导用户到登录页面的路径&lt;/li&gt;
&lt;li&gt;二是用户授权后，跳转回来的路径。redirect _url&lt;/li&gt;
&lt;/ul&gt;
&lt;/blockquote&gt;
&lt;h3&gt;QQ登录测试&lt;/h3&gt;
&lt;p&gt;点击QQ登陆会跳转到QQ的授权页面，但我们跳转到的页面里显示&lt;code&gt;redirect uri is illegal(100010)&lt;/code&gt;,具体原因将在接下来的博客中进行解决。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://tangtangit.com/_astro/redirect_illegal.SssVO8Gj_1gC468.webp&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;h3&gt;1 redirect uri is illegal(100010)错误的原因&lt;/h3&gt;
&lt;h4&gt;1、redirect uri是什么，怎么配置&lt;/h4&gt;
&lt;blockquote&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;首先我们要明确Oauth2协议的整个流程： 用户点击QQ进行QQ登陆----》用户通过扫码等方式进行授权 —》授权成功后QQ会调用一个回调地址，将授权码给我们的系统 —》然后我们的系统会拿着授权码等去换accesstoken —》换到accesstoken后再通过accesstoken等拿到openid—》接着再拿着accesstoken，openid等去QQ换取用户信息。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;而&lt;code&gt;redirect uri&lt;/code&gt;就是上诉过程中的&lt;code&gt;回调地址&lt;/code&gt;。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;同时我们还要明确&lt;code&gt;redirect uri&lt;/code&gt;不是随意生成的，而是在我们的系统在QQ上进行注册时需要填写的必填信息之一，它的填写有一定的规则，可参考&lt;a href=&quot;https://wiki.connect.qq.com/%E5%9B%9E%E8%B0%83%E5%9C%B0%E5%9D%80%E5%B8%B8%E8%A7%81%E9%97%AE%E9%A2%98%E5%8F%8A%E4%BF%AE%E6%94%B9%E6%96%B9%E6%B3%95&quot;&gt;QQ互联官网&lt;/a&gt;。&lt;strong&gt;这里要注意，官网上明确说明了域名要包含&quot;http：//&quot;部分&lt;/strong&gt;。&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/blockquote&gt;
&lt;p&gt;&lt;img src=&quot;QQ%E7%99%BB%E5%BD%95%E5%9B%9E%E8%B0%83.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;h4&gt;2、redirect uri与我们项目的关系&lt;/h4&gt;
&lt;p&gt;我们把请求的路径拷贝如下（这个请求具体怎么生成的???为什么要这要生成???与QQ互联官网上获取授权码接口有什么关系???建议大家好好看看QQ互联的官网并仔细阅读一下源码：org.springframework.social.oauth2.OAuth2Template）：&lt;/p&gt;
&lt;p&gt;https://graph.qq.com/oauth2.0/show?which=error&amp;amp;display=pc&amp;amp;error=100010&amp;amp;client_id=100550231&amp;amp;response_type=code&amp;amp;redirect_uri=http%3A%2F%2Flocalhost%3A8080%2Fauth%2Fqq&amp;amp;state=8f3b0533-e5a7-4dfe-a2b8-94f0119b0ad2&lt;/p&gt;
&lt;p&gt;从中我们可以看到redirect_uri的路径如下：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;http%3A%2F%2Flocalhost%3A8080%2Fauth%2Fqq
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;其实可以很容易地猜出来就是localhost://8080/auth/qq,这肯定不可能是我们在QQ互联上配置的redirect_uri，因此当我们的项目拿着这个redirect_uri去请求QQ的接口时就报出了redirect uri is illegal(100010)的错误。&lt;/p&gt;
&lt;p&gt;同时我们也可以看出redirect_uri在我们项目中的生成规则是:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;IP+端口号（或者域名+端口）+/auth/qq
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;其中&lt;code&gt;/auth和/qq&lt;/code&gt;在[上文中]讲过它俩分别由&lt;code&gt;SocialAuthenticationFilter和providerId&lt;/code&gt;控制。&lt;/p&gt;
&lt;h3&gt;2、解决 redirect uri错误&lt;/h3&gt;
&lt;p&gt;通过前面的讲解，就可以很容易地知道假如我们在QQ互联上配置的网站地址和回调域名（redirect_uri）如下&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;%E5%81%87%E8%AE%BE%E5%9B%9E%E8%B0%83%E5%9F%9F%E5%90%8D.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;那么为了让我们的项目不会报redirect_uri错误，我们需要做如下四件事。&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;(1) 修改我们项目的访问域名+端口&lt;/p&gt;
&lt;p&gt;本地测试开发时可以通过SwitchHosts来将域名映射为IP，端口号需在项目里指定为80。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;本机指定域名和IP的映射关系&lt;/li&gt;
&lt;li&gt;将项目的访问端口号改为80&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;(2)  &lt;strong&gt;将SocialAuthenticationFilter默认拦截的请求/auth改为/qqLogin(可配置)&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;这个可以通过继承springsocialSocial为我们提供的SocialAuthenticationFilter的配置类SpringSocialConfigurer，并重写其后置处理方法的方式来完成，具体代码如下：&lt;/p&gt;
&lt;p&gt;新建一个NrscSpringSocialConfigurer 类继承SpringSocialConfigurer并重写其后置处理方法&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;/**
 * @author : Sun Chuan
 * @date : 2019/8/19 22:48
 * Description：继承springsocialSocial为我们提供的SocialAuthenticationFilter的配置类SpringSocialConfigurer
 * 并重写其后置处理方法，让其默认拦截包含我们配置字符串的请求
 */
public class NrscSpringSocialConfigurer extends SpringSocialConfigurer {

    private String filterProcessesUrl;

    public NrscSpringSocialConfigurer(String filterProcessesUrl) {
        this.filterProcessesUrl = filterProcessesUrl;
    }

    /**
     * T 其实就是指的SocialAuthenticationFilter类型
     *
     * @param object
     * @param &amp;lt;T&amp;gt;
     * @return
     */
    @Override
    protected &amp;lt;T&amp;gt; T postProcess(T object) {
        //在父类处理完SocialAuthenticationFilter之后的基础上，将其默认拦截url改成我们配置的值
        SocialAuthenticationFilter filter = (SocialAuthenticationFilter) super.postProcess(object);
        filter.setFilterProcessesUrl(filterProcessesUrl);
        return (T) filter;
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;可能会报错：&lt;/p&gt;
&lt;p&gt;报错：cannot access javax.servlet.Filter   无法找找 这个东东&lt;/p&gt;
&lt;p&gt;最近项目中需要用到javax.servlet.*，发现IDEA默认的没jar包&lt;/p&gt;
&lt;p&gt;这个是Geronimo对外分发的可以给开源代码的servlet的jar，经过了sun的ctk兼容性测试。省去了原来加载JNDI,JDBC,SERVLET,JSP,JTA&lt;/p&gt;
&lt;p&gt;解决办法：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&amp;lt;dependency&amp;gt;
    &amp;lt;groupId&amp;gt;org.apache.geronimo.specs&amp;lt;/groupId&amp;gt;
    &amp;lt;artifactId&amp;gt;geronimo-servlet_2.4_spec&amp;lt;/artifactId&amp;gt;
    &amp;lt;version&amp;gt;1.1.1&amp;lt;/version&amp;gt;
    &amp;lt;scope&amp;gt;provided&amp;lt;/scope&amp;gt;
&amp;lt;/dependency&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;将我们写的NrscSpringSocialConfigurer 类作为SocialAuthenticationFilter的配置类注入到spring容器，并加入到spring-security过滤器链中&lt;/p&gt;
&lt;p&gt;注：在 SpringSocialConfigurer 注入的地方改造如下&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;    @Autowired
    private NrscSecurityProperties nrscSecurityProperties;
    /**
     * 通过apply()该实例，可以将SocialAuthenticationFilter加入到spring-security的过滤器链
     */
    @Bean
    public SpringSocialConfigurer nrscSocialSecurityConfig() {
        // 默认配置类，进行组件的组装
        // 包括了过滤器SocialAuthenticationFilter 添加到security过滤链中
        String filterProcessesUrl = nrscSecurityProperties.getSocial().getFilterProcessesUrl();
        NrscSpringSocialConfigurer configurer = new NrscSpringSocialConfigurer(filterProcessesUrl);
        return configurer;
    }
&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote&gt;
&lt;p&gt;在配置文件里配置filterProcessesUrl让SocialA&lt;/p&gt;
&lt;p&gt;uthenticationFilter默认拦截含有/qqLogin的请求&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;&lt;img src=&quot;%E6%8B%A6%E6%88%AAqqlogin.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;(3) 配置providerId将qq改为callback.do 配置如上图。&lt;/p&gt;
&lt;p&gt;(4) 修改QQ登陆按钮的请求路径&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&amp;lt;h3&amp;gt;社交登录&amp;lt;/h3&amp;gt;
&amp;lt;a href=&quot;/qqLogin/callback.do&quot;&amp;gt;QQ登录&amp;lt;/a&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;/signin错误解决+springsocial源码解读&lt;/h3&gt;
&lt;blockquote&gt;
&lt;p&gt;虽然解决了redirect uri is illegal(100010)错误,但是扫码进行授权后，发现仍然不能完成第三方登陆的流程。而是会报出下图所示的错误，通过后端打印日志可以看到服务请求了XXX/signin,而在项目里并未对该url进行放行，因此前端页面展示出这样的错误是很好理解的。那为什么在进行QQ扫码授权后，会跳到该url上呢?这需要去springsocial源码中去寻求答案。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;&lt;img src=&quot;signin%E9%94%99%E8%AF%AF.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;h4&gt;springsocial源码解读&lt;/h4&gt;
&lt;blockquote&gt;
&lt;p&gt;从用户点击QQ登陆 —》到程序引导用户进入QQ授权页面 —》到用户在QQ授权页面进行扫码授权—》到程序拿着获取到的用户信息与我们自己数据库里的用户信息进行关联+校验 —》再到校验成功并构建一个标识为校验成功的 Authentication对象 的整个过程springsocial/springsecurity所使用到的主要类大致如下图。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;&lt;img src=&quot;social%E6%BA%90%E7%A0%81%E8%A7%A3%E8%AF%BB.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;h4&gt;AbstractAuthenticationProcessingFilter&lt;/h4&gt;
&lt;p&gt;这个Filter我在《spring-security入门6—表单登陆认证原理源码解析》这篇文章里详细地介绍过，它其实是一个抽象类，利用模版模式定义了认证的整个整体框架，但是具体的认证过程、认证成功后的行为和认证失败后的行为需要子类进行具体实现。其伪代码如下：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;try{
	//尝试进行登陆认证---抽象方法具体实现交给交给继承的子类,
	//在springsocial的流程里就是SocialAuthenticationFilter
	//用户名+密码登陆的流程里为UsernamePasswordAuthenticationFilter
	Authentication	authResult = attemptAuthentication(request, response);
}catch(认证失败){
	//这里会讲两个认证失败相关的逻辑
	// 	  (1)点击QQ登陆,将用户引导到QQ授权页面时
	//    (2)用户在QQ授权页面进行扫码,最终未成功登陆而是访问XXX/signin路径的逻辑
	unsuccessfulAuthentication(request, response, failed);
}
//认证成功
//	--&amp;gt;将认证成功的Authentication对象放到线程的SecurityContext对象中
//  --&amp;gt;走记住我相关的逻辑等
successfulAuthentication(request, response, chain, authResult);
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;SocialAuthenticationFilter
SocialAuthenticationFilter和用户名+密码认证校验方式时用到的UsernamePasswordAuthenticationFilter一样，也是AbstractAuthenticationProcessingFilter的子类。当请求的URL为SocialAuthenticationFilter指定要拦截的URL时springsecurity/springsocial就会走SocialAuthenticationFilter中的attemptAuthentication((request, response))进行认证登陆。其伪代码如下：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;attemptAuthentication — 认证校验的准备工作&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response) throws AuthenticationException {
	if (detectRejection(request)) {
		if (logger.isDebugEnabled()) {
			logger.debug(&quot;A rejection was detected. Failing authentication.&quot;);
		}
		throw new SocialAuthenticationException(&quot;Authentication failed because user rejected authorization.&quot;);
	}
	
	Authentication auth = null;
	Set&amp;lt;String&amp;gt; authProviders = authServiceLocator.registeredAuthenticationProviderIds();
	//获取到ProviderId，就是我们在yml配置文件里指定的ProviderId
	String authProviderId = getRequestedProviderId(request);
	if (!authProviders.isEmpty() &amp;amp;&amp;amp; authProviderId != null &amp;amp;&amp;amp; authProviders.contains(authProviderId)) {
	//根据ProviderId获取相应的SocialAuthenticationService
		SocialAuthenticationService&amp;lt;?&amp;gt; authService = authServiceLocator.getAuthenticationService(authProviderId);
		//真正进行认证校验的方法
		auth = attemptAuthService(authService, request, response);
		if (auth == null) {
			throw new AuthenticationServiceException(&quot;authentication failed&quot;);
		}
	}
	return auth;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;attemptAuthService — 定义了真正认证校验的整体框架&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;	private Authentication attemptAuthService(final SocialAuthenticationService&amp;lt;?&amp;gt; authService, final HttpServletRequest request, HttpServletResponse response) 
			throws SocialAuthenticationRedirectException, AuthenticationException {
		//这里的token其实是用从QQ获取的用户信息（被封装到一个Connection对象里）进一步封装后得到的未被校验的对象
		//类比一下，在用户名+密码登陆时也会有一个未校验的token
		final SocialAuthenticationToken token = authService.getAuthToken(request, response);
		if (token == null) return null;
		
		Assert.notNull(token.getConnection());
		//即Authentication auth=SecurityContextHolder.getContext().getAuthentication();
		//即先去SecurityContextHolder里看看有没有Authentication对象
		Authentication auth = getAuthentication();
		if (auth == null || !auth.isAuthenticated()) {
			//进行密码是否过期，用户是否可用等校验，如校验成功，返回一个标识为校验成功的Authentication对象
			return doAuthentication(authService, request, token);
		} else {
			//暂时不进行追究
			addConnection(authService, request, token, auth);
			return null;
		}		
	}	
&lt;/code&gt;&lt;/pre&gt;
&lt;h4&gt;Auth2AuthenticationService&lt;/h4&gt;
&lt;p&gt;getAuthToken方法 — 走Oauth2协议流程很重要的一个方法&lt;/p&gt;
&lt;p&gt;OAuth2AuthenticationService对上面&lt;code&gt;attemptAuthService&lt;/code&gt;方法调用的 getAuthToken方法进行了具体实现，其主要代码如下：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;public SocialAuthenticationToken getAuthToken(HttpServletRequest request, HttpServletResponse response) throws SocialAuthenticationRedirectException {
		//获取授权码
		String code = request.getParameter(&quot;code&quot;);
		//用户点击QQ登陆，我们的程序里将用户引导到QQ授权页面时请求的URL（Oauth2协议中获取授权码的过程） 和  用户在QQ授权页面	
		//进行扫码授权后,QQ服务器回调我们服务的URL是相同的
		// 两者不同的就是我们引导用户到QQ授权页面时的请求里没有授权码，而用户点击授权QQ回调时会携带者授权码
		//因此下面其实就是在走用户点击QQ登陆，我们的程序将用户引导到QQ授权页面的逻辑
		if (!StringUtils.hasText(code)) { 
			OAuth2Parameters params =  new OAuth2Parameters();
			params.setRedirectUri(buildReturnToUrl(request));
			setScope(request, params);
			params.add(&quot;state&quot;, generateState(connectionFactory, request));
			addCustomParameters(params);
			//拼装成访问QQ授权页面的路径，并throw出去
			throw new SocialAuthenticationRedirectException(getConnectionFactory().getOAuthOperations().buildAuthenticateUrl(params));
		} else if (StringUtils.hasText(code)) {
			try {
				//从request中拿出redirect_uri
				String returnToUrl = buildReturnToUrl(request);
				//拿着redirect_uri、授权码等去QQ服务器交换accessToken
				AccessGrant accessGrant = getConnectionFactory().getOAuthOperations().exchangeForAccess(code, returnToUrl, null);
				// TODO avoid API call if possible (auth using token would be fine)
				//获取到从QQ拿到的封装成Connection对象的用户信息
				Connection&amp;lt;S&amp;gt; connection = getConnectionFactory().createConnection(accessGrant);
				//将Connection对象进一步封装成SocialAuthenticationToken对象，便于2.2中的attemptAuthService方法进行校验
				return new SocialAuthenticationToken(connection, null);
			} catch (RestClientException e) {
				logger.debug(&quot;failed to exchange for access&quot;, e);
				return null;
			}
		} else {
			return null;
		}
	}
&lt;/code&gt;&lt;/pre&gt;
&lt;h4&gt;用户点击QQ登陆,引导用户到QQ授权页面的具体逻辑&lt;/h4&gt;
&lt;blockquote&gt;
&lt;p&gt;当用户点击QQ登陆时,会走到没有code的if语句块中,在这个语句块里其实就做了一件事,就是拼接访问QQ授权页面的路径（注意，这里拼接路径的规则和QQ互联上获取Authorization Code指明的规则是一致的）,并将该路径封装到SocialAuthenticationRedirectException异常中抛出去。那将异常抛出去之后怎么就会访问到了QQ授权页面呢？这里就不得不提一下springsocial所定义的处理异常的类SocialAuthenticationFailureHandler了。首先抛出的异常会一路向上抛到AbstractAuthenticationProcessingFilter里，然后在该Filter里会捕获到抛出的异常，并根据异常类型，指定用SocialAuthenticationFailureHandler来进行处理，SocialAuthenticationFailureHandler的源码如下：&lt;/p&gt;
&lt;/blockquote&gt;
&lt;pre&gt;&lt;code&gt;public class SocialAuthenticationFailureHandler implements AuthenticationFailureHandler {
	//异常处理器
	private AuthenticationFailureHandler delegate;

	public SocialAuthenticationFailureHandler(AuthenticationFailureHandler delegate) {
		this.delegate = delegate;
	}
	//具体处理异常的方法
	public void onAuthenticationFailure(HttpServletRequest request, HttpServletResponse response, AuthenticationException failed) throws IOException, ServletException {
		//处理SocialAuthenticationRedirectException异常的逻辑
		if (failed instanceof SocialAuthenticationRedirectException) {
		//将异常中的URL取出，并进行重定向----》这里就是引导用户到QQ授权页面的奥秘！！！
			response.sendRedirect(((SocialAuthenticationRedirectException) failed).getRedirectUrl()); 
			return;
		}
		//处理其他springsocial认证失败的逻辑-----》就包含了本文标题中所说的/signin错误
		delegate.onAuthenticationFailure(request, response, failed);
	}

}
&lt;/code&gt;&lt;/pre&gt;
&lt;h4&gt;/signin错误的具体原因&lt;/h4&gt;
&lt;p&gt;&lt;img src=&quot;signin%E6%8A%A5%E9%94%99%E9%97%AE%E9%A2%98%E5%88%86%E6%9E%90.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;h5&gt;为什么会跳转（重定向）到/signin路径&lt;/h5&gt;
&lt;blockquote&gt;
&lt;p&gt;通过断点调试可以发现，用户在QQ页面进行扫码授权后，会走到OAuth2AuthenticationService的getAuthToken方法&lt;code&gt;else if语句块&lt;/code&gt; ，但是在运行到下面的方法时就会报错了&lt;/p&gt;
&lt;/blockquote&gt;
&lt;pre&gt;&lt;code&gt;AccessGrant accessGrant = getConnectionFactory().getOAuthOperations().exchangeForAccess(code, returnToUrl, null);
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;报错信息为：
Could not extract response: no suitable HttpMessageConverter found for response type [interface java.util.Map] and content type [text/html]&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;异常会被catch住（可以注意一下，这里的异常为RestClientException，它其实是请求相关的异常），catch住之后，只是打印了一个logger信息，然后返回了一个null。&lt;/p&gt;
&lt;p&gt;—》这里返回null
—》2.2中的attemptAuthService方法就会返回null
—》然后2.2中attemptAuthentication方法里获得的auth就是null，当auth为null时就会抛出SocialAuthenticationException异常
—》AbstractAuthenticationProcessingFilter会捕捉到该异常，然后根据异常类型，指定用SocialAuthenticationFailureHandler（即2.3.2中所说的SocialAuthenticationFailureHandler）来进行处理
—》走delegate.onAuthenticationFailure(request, response, failed);语句进行处理
—》真正的处理该异常的具体实现在SimpleUrlAuthenticationFailureHandler类里，重定向到XXX/signin的具体逻辑就在该类里。(感兴趣的童鞋可以自己追踪着看一下，这里不再赘述！！！)&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h5&gt;为什么获取accessToken会抛出RestClientException异常&lt;/h5&gt;
&lt;p&gt;想要知道具体的原因必须要深入到报错的语句中，exchangeForAccess方法具体的实现（&lt;code&gt;OAuth2Template类中&lt;/code&gt;）如下：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;exchangeForAccess方法 — 预备工作：拼装请求参数&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;	public AccessGrant exchangeForAccess(String authorizationCode, String redirectUri, MultiValueMap&amp;lt;String, String&amp;gt; additionalParameters) {
	//拼接请求参数
		MultiValueMap&amp;lt;String, String&amp;gt; params = new LinkedMultiValueMap&amp;lt;String, String&amp;gt;();
		//注意这里的useParametersForClientAuthentication要为true才会将client_id 和 client_secret拼接为请求参数
		//QQ互联（https://wiki.connect.qq.com/%E4%BD%BF%E7%94%A8authorization_code%E8%8E%B7%E5%8F%96access_token）上
		//明确指出获取accessToken这两个参数为必传参数，因此要想办法保证该值在这里必须为true
		if (useParametersForClientAuthentication) {
			params.set(&quot;client_id&quot;, clientId);
			params.set(&quot;client_secret&quot;, clientSecret);
		}
		params.set(&quot;code&quot;, authorizationCode);
		params.set(&quot;redirect_uri&quot;, redirectUri);
		params.set(&quot;grant_type&quot;, &quot;authorization_code&quot;);
		if (additionalParameters != null) {
			params.putAll(additionalParameters);
		}
		//真正发送请求获取accessToken
		return postForAccessGrant(accessTokenUrl, params);
	}
&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;postForAccessGrant — &lt;code&gt;RestClientException异常发生的地方&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;protected AccessGrant postForAccessGrant(String accessTokenUrl, MultiValueMap&amp;lt;String, String&amp;gt; parameters) {
//方法很简单，就是利用restTemplate发送一个rest请求，然后将结果交给 extractAccessGrant函数进行构建AccessGrant对象
//RestClientException异常就发生在这里
	return extractAccessGrant(getRestTemplate().postForObject(accessTokenUrl, parameters, Map.class));
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;到底为什么会发生这个异常呢？通过异常信息Could not extract response: no suitable HttpMessageConverter found for response type [interface java.util.Map] and content type [text/html]其实可以很清楚的知道，是因为当前的RestTemplate没有适合的HttpMessageConverter 去处理reponse为Map，content type为 [text/html]的返回信息。看一下OAuth2Template中创建RestTemplate对象的方法，可以发现，它只加了如下三个HttpMessageConverter，这三个HttpMessageConverter并不能处理content type为 [text/html]的返回信息，因此走到上面的postForAccessGrant方法会抛出RestClientException异常。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;	protected RestTemplate createRestTemplate() {
		ClientHttpRequestFactory requestFactory = ClientHttpRequestFactorySelector.getRequestFactory();
		RestTemplate restTemplate = new RestTemplate(requestFactory);
		List&amp;lt;HttpMessageConverter&amp;lt;?&amp;gt;&amp;gt; converters = new ArrayList&amp;lt;HttpMessageConverter&amp;lt;?&amp;gt;&amp;gt;(2);
		converters.add(new FormHttpMessageConverter());
		converters.add(new FormMapHttpMessageConverter());
		converters.add(new MappingJackson2HttpMessageConverter());
		restTemplate.setMessageConverters(converters);
		restTemplate.setErrorHandler(new LoggingErrorHandler());
		if (!useParametersForClientAuthentication) {
			List&amp;lt;ClientHttpRequestInterceptor&amp;gt; interceptors = restTemplate.getInterceptors();
			if (interceptors == null) {   // defensively initialize list if it is null. (See SOCIAL-430)
				interceptors = new ArrayList&amp;lt;ClientHttpRequestInterceptor&amp;gt;();
				restTemplate.setInterceptors(interceptors);
			}
			interceptors.add(new PreemptiveBasicAuthClientHttpRequestInterceptor(clientId, clientSecret));
		}
		return restTemplate;
	}
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;/signin错误解决方式&lt;/h3&gt;
&lt;p&gt;找到问题的具体原因，我们就可以着手去解决该问题了，其实解决方式也比较简单，就是我们只需要写一个类，继承OAuth2Template，并重写createRestTemplate方法，给RestTemplate对象加上一个可以处理content type为 [text/html]的HttpMessageConverter就可以了。&lt;/p&gt;
&lt;p&gt;在此之前还要说明一点，OAuth2Template类里postForAccessGrant 发送post请求获取accessToken期待的返回值为Map，但通过QQ互联官网可以看到获取accessToken返回的是如下的一个字符串，并不是一个json，那就无法直接转成Map了，因此我们还需要重写一下postForAccessGrant方法。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;access_token=FE04************************CCE2&amp;amp;expires_in=7776000&amp;amp;refresh_token=88E4************************BE14。
&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;继承了OAuth2Template的实体类&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;/**
 * 
 */
package com.nrsc.security.core.social.qq.connect;

import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang.StringUtils;
import org.springframework.http.converter.StringHttpMessageConverter;
import org.springframework.social.oauth2.AccessGrant;
import org.springframework.social.oauth2.OAuth2Template;
import org.springframework.util.MultiValueMap;
import org.springframework.web.client.RestTemplate;
import java.nio.charset.Charset;
@Slf4j
public class QQOAuth2Template extends OAuth2Template {
    public QQOAuth2Template(String clientId, String clientSecret, String authorizeUrl, String accessTokenUrl) {
        super(clientId, clientSecret, authorizeUrl, accessTokenUrl);
        //只有这样才会将client_id 和 client_secret拼接为请求参数
        setUseParametersForClientAuthentication(true);
    }

    /**
     * 通过QQ互联（https://wiki.connect.qq.com/%E4%BD%BF%E7%94%A8authorization_code%E8%8E%B7%E5%8F%96access_token）
     * 可以知道获取accessToken这一步其实获取到的是一个字符串，而OAuth2Template里的postForAccessGrant方法默认获取的类型
     * 为一个Map（获取的信息为json格式才可直接转成Map），因此需要重写postForAccessGrant方法，将获取到的accessToken等
     * 信息封装到AccessGrant对象里
     * @param accessTokenUrl
     * @param parameters
     * @return
     */
    @Override
    protected AccessGrant postForAccessGrant(String accessTokenUrl, MultiValueMap&amp;lt;String, String&amp;gt; parameters) {
        String responseStr = getRestTemplate().postForObject(accessTokenUrl, parameters, String.class);

        log.info(&quot;获取accessToke的响应：&quot; + responseStr);

        String[] items = StringUtils.splitByWholeSeparatorPreserveAllTokens(responseStr, &quot;&amp;amp;&quot;);

        String accessToken = StringUtils.substringAfterLast(items[0], &quot;=&quot;);
        Long expiresIn = new Long(StringUtils.substringAfterLast(items[1], &quot;=&quot;));
        String refreshToken = StringUtils.substringAfterLast(items[2], &quot;=&quot;);

        return new AccessGrant(accessToken, null, refreshToken, expiresIn);
    }

    /**
     * 重写createRestTemplate方法，使其加上可以处理content type为 [text/html]的HttpMessageConverter
     * @return
     */
    @Override
    protected RestTemplate createRestTemplate() {
        RestTemplate restTemplate = super.createRestTemplate();
        restTemplate.getMessageConverters().add(new StringHttpMessageConverter(Charset.forName(&quot;UTF-8&quot;)));
        return restTemplate;
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;用我们自己的QQOAuth2Template对象来构建ServiceProvider对象&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;**
 * &amp;lt;p&amp;gt;DESC: qq，serviceProvider&amp;lt;/p&amp;gt;
 * &amp;lt;p&amp;gt;DATE: 2019-11-04 23:10&amp;lt;/p&amp;gt;
 * &amp;lt;p&amp;gt;VERSION:1.0.0&amp;lt;/p&amp;gt;
 * &amp;lt;p&amp;gt;@AUTHOR: ZhengYong&amp;lt;/p&amp;gt;
 */
public class QQServiceProvider extends AbstractOAuth2ServiceProvider&amp;lt;QQ&amp;gt; {

    private String appId;

    /**
     * 获取code
     */
    private static final String URL_AUTHORIZE = &quot;https://graph.qq.com/oauth2.0/authorize&quot;;

    /**
     * 获取token
     */
    private static final String URL_ACCESS_TOKEN= &quot;https://graph.qq.com/oauth2.0/token&quot;;

    /**
     * provider构造方法
     * @param appId appId
     * @param appSecret appSecret
     */
    public QQServiceProvider(String appId,String appSecret) {
        //需要 Oauth2Opreations
        super(new QQOAuth2Template(appId,appSecret,URL_AUTHORIZE,URL_ACCESS_TOKEN));
        this.appId = appId;
    }

    /**
     * api 重写方法
     * @param accessToken token
     * @return
     */
    @Override
    public QQ getApi(String accessToken) {
        return new QQImpl(accessToken,appId);
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;/signup错误&lt;/h3&gt;
&lt;p&gt;当解决了/signin错误后，我们就已经从QQ获取到了用户信息，即Connection对象，而SpringSocial/SpringSecurity又将其进一步封装成了一个SocialAuthenticationToken对象进行后续的校验工作&lt;/p&gt;
&lt;p&gt;attemptAuthService — 定义了真正认证校验的整体框架&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;	private Authentication attemptAuthService(final SocialAuthenticationService&amp;lt;?&amp;gt; authService, final HttpServletRequest request, HttpServletResponse response) 
			throws SocialAuthenticationRedirectException, AuthenticationException {
		//这里的token其实是用从QQ获取的用户信息（被封装到一个Connection对象里）进一步封装后得到的未被校验的对象
		//类比一下，在用户名+密码登陆时也会有一个未校验的token
		final SocialAuthenticationToken token = authService.getAuthToken(request, response);
		if (token == null) return null;
		
		Assert.notNull(token.getConnection());
		//即Authentication auth=SecurityContextHolder.getContext().getAuthentication();
		//即先去SecurityContextHolder里看看有没有Authentication对象
		Authentication auth = getAuthentication();
		if (auth == null || !auth.isAuthenticated()) {
			//进行密码是否过期，用户是否可用等校验，如校验成功，返回一个标识为校验成功的Authentication对象
			return doAuthentication(authService, request, token);
		} else {
			//暂时不进行追究
			addConnection(authService, request, token, auth);
			return null;
		}		
	}	
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;在获取到了封装了QQ用户信息的SocialAuthenticationToken对象后会走&lt;code&gt;doAuthentication(authService, request, token);&lt;/code&gt;方法进行进一步的校验工作。该方法的具体实现如下：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;	private Authentication doAuthentication(SocialAuthenticationService&amp;lt;?&amp;gt; authService, HttpServletRequest request, SocialAuthenticationToken token) {
		try {
			if (!authService.getConnectionCardinality().isAuthenticatePossible()) return null;
			token.setDetails(authenticationDetailsSource.buildDetails(request));
			//下面这句话在&amp;lt;&amp;lt;spring-security入门6---表单登陆认证原理源码解析&amp;gt;&amp;gt;那篇文章里详细地介绍过,有兴趣的可以看一下
			//它的作用是循环遍历各个Provider,并根据token的类型找到相应的Provider进行下一步的校验工作
			//在springsocial的认证过程中用到的是SocialAuthenticationProvider
			Authentication success = getAuthenticationManager().authenticate(token);
			Assert.isInstanceOf(SocialUserDetails.class, success.getPrincipal(), &quot;unexpected principle type&quot;);
			updateConnections(authService, token, success);			
			return success;
		} 
	// 从下面源代码中的注释,其实可以很清楚地看到/signup错误发生的时机
	//即当上面的代码出现BadCredentialsException异常时，SpringSocial/SpringSecurity会catch住该异常，
	//并抛出一个重定向异常---》系统默认的重定向url就是XXX/signup(即注册),而我们未对/signup进行授权，
	//所以最后就会出现一个/signup异常。
		catch (BadCredentialsException e) {
			// connection unknown, register new user?
			if (signupUrl != null) {
				// store ConnectionData in session and redirect to register page
				sessionStrategy.setAttribute(new ServletWebRequest(request), ProviderSignInAttempt.SESSION_ATTRIBUTE, new ProviderSignInAttempt(token.getConnection()));
				throw new SocialAuthenticationRedirectException(buildSignupUrl(request));
			}
			throw e;
		}
	}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;再来看一下SocialAuthenticationProvider中的authenticate方法，看一下为什么会报出BadCredentialsException异常&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;public Authentication authenticate(Authentication authentication) throws AuthenticationException {
		Assert.isInstanceOf(SocialAuthenticationToken.class, authentication, &quot;unsupported authentication type&quot;);
		Assert.isTrue(!authentication.isAuthenticated(), &quot;already authenticated&quot;);
		SocialAuthenticationToken authToken = (SocialAuthenticationToken) authentication;
		String providerId = authToken.getProviderId();
		Connection&amp;lt;?&amp;gt; connection = authToken.getConnection();
		//拿着Connection对象去userconnection表里去查相应的userId，由于我们的系统里暂时还没有一个用户，
		//所以查到的userId肯定为null----》为null就会报出BadCredentialsException---》然后就会回到上面的类
		//让你跳转到一个XXX/signup的url----》其实说白了就是引导用户进入注册用户的页面（这一块需要我们自己实现）	
		String userId = toUserId(connection);
		if (userId == null) {
			throw new BadCredentialsException(&quot;Unknown access token&quot;);
		}
		//当能从userconnection表里查到数据时会走下面的语句
		//即拿着userId去我们的业务表里拿到用户信息
		UserDetails userDetails = userDetailsService.loadUserByUserId(userId);
		if (userDetails == null) {
			throw new UsernameNotFoundException(&quot;Unknown connected account id&quot;);
		}
		//校验用户是否过期等，并将校验成功后的用户对象封装到一个标记为已经校验成功的Authentication对象中
		return new SocialAuthenticationToken(connection, userDetails, authToken.getProviderAccountData(), getAuthorities(providerId, userDetails));
	}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;代码解读到这里，其实已经可以很清楚地知道为什么项目会报出XXX/signup的错误了。那解决该问题的思路自然也就有了，具体思路如下：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;我们需要自己写一个注册页面&lt;/li&gt;
&lt;li&gt;让signupUrl对应的url可以指向我们写的注册页&lt;/li&gt;
&lt;li&gt;对signupUrl进行授权&lt;/li&gt;
&lt;li&gt;开发用户注册相关的逻辑&lt;/li&gt;
&lt;/ul&gt;
&lt;h4&gt;系统注册页面&lt;/h4&gt;
&lt;pre&gt;&lt;code&gt;&amp;lt;!DOCTYPE html&amp;gt;
&amp;lt;html&amp;gt;
&amp;lt;head&amp;gt;
    &amp;lt;meta charset=&quot;UTF-8&quot;&amp;gt;
    &amp;lt;title&amp;gt;注册&amp;lt;/title&amp;gt;
&amp;lt;/head&amp;gt;
&amp;lt;body&amp;gt;
&amp;lt;h2&amp;gt;标准注册页面&amp;lt;/h2&amp;gt;
&amp;lt;h3&amp;gt;这是系统注册页面，请配置nrsc.security.browser.signUpUrl属性来设置自己的注册页&amp;lt;/h3&amp;gt;
&amp;lt;/body&amp;gt;
&amp;lt;/html&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;h4&gt;指定默认的注册页面为系统注册页&lt;/h4&gt;
&lt;p&gt;（1）在BrowserProperties里指定默认注册页所在的路径&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;package com.nrsc.security.core.properties;

import lombok.Data;

/**
 * @author : Sun Chuan
 * @date : 2019/6/20 22:13
 */
@Data
public class BrowserProperties {

    /**指定默认的注册页面*/
    private String signUpUrl = &quot;/defaultPage/default-signUp.html&quot;;

    /**指定默认的登陆页面*/
    private String loginPage = SecurityConstants.DEFAULT_LOGIN_PAGE_URL;

    /**指定默认的处理成功与处理失败的方法*/
    private LoginType loginType = LoginType.JSON;

    /**记住我的时间3600秒即1小时*/
    private int rememberMeSeconds = 3600;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;当然我们可以通过在yml里进行配置来修改具体的注册页面地址&lt;/p&gt;
&lt;p&gt;（2）指定SpringSocial/SpringSecurity跳向注册页面时的url为我们配置的url&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;    /**
     * 通过apply()该实例，可以将SocialAuthenticationFilter加入到spring-security的过滤器链
     */
    @Bean
    public SpringSocialConfigurer nrscSocialSecurityConfig() {
        // 默认配置类，进行组件的组装
        // 包括了过滤器SocialAuthenticationFilter 添加到security过滤链中
        String filterProcessesUrl = nrscSecurityProperties.getSocial().getFilterProcessesUrl();
        NrscSpringSocialConfigurer configurer = new NrscSpringSocialConfigurer(filterProcessesUrl);

        //指定SpringSocial/SpringSecurity跳向注册页面时的url为我们配置的url
        configurer.signupUrl(nrscSecurityProperties.getBrowser().getSignUpUrl());
        return configurer;
    }
&lt;/code&gt;&lt;/pre&gt;
&lt;h4&gt;对signupUrl进行授权&lt;/h4&gt;
&lt;pre&gt;&lt;code&gt; .authorizeRequests()
 .antMatchers(
         SecurityConstants.DEFAULT_UNAUTHENTICATION_URL,
         SecurityConstants.DEFAULT_LOGIN_PROCESSING_URL_MOBILE,
         nrscSecurityProperties.getBrowser().getLoginPage(),
         SecurityConstants.DEFAULT_VALIDATE_CODE_URL_PREFIX + &quot;/*&quot;,
         nrscSecurityProperties.getBrowser().getSignUpUrl() //对注册url进行授权
 )
&lt;/code&gt;&lt;/pre&gt;
&lt;h4&gt;开发注册相关的逻辑&lt;/h4&gt;
&lt;pre&gt;&lt;code&gt;    @PostMapping(&quot;/register&quot;)
    public void register(NrscUser user) {
        //注册用户相关逻辑 
    }
&lt;/code&gt;&lt;/pre&gt;
&lt;h4&gt;未配置nrsc.security.browser.signUpUrl属性时&lt;/h4&gt;
&lt;p&gt;扫码进行授权后会跳转到如下页面，说明开发的代码已经生效。&lt;/p&gt;
&lt;h4&gt;配置了signUpUrl时&lt;/h4&gt;
&lt;p&gt;配置效果2.2（1）中的插图所示，nrsc-signUp.html的源码如下：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&amp;lt;!DOCTYPE html&amp;gt;
&amp;lt;html&amp;gt;
&amp;lt;head&amp;gt;
    &amp;lt;meta charset=&quot;UTF-8&quot;&amp;gt;
    &amp;lt;title&amp;gt;注册&amp;lt;/title&amp;gt;
&amp;lt;/head&amp;gt;
&amp;lt;body&amp;gt;
&amp;lt;h2&amp;gt;Demo注册页&amp;lt;/h2&amp;gt;

&amp;lt;form action=&quot;/user/regist&quot; method=&quot;post&quot;&amp;gt;
    &amp;lt;table&amp;gt;
        &amp;lt;tr&amp;gt;
            &amp;lt;td&amp;gt;用户名:&amp;lt;/td&amp;gt;
            &amp;lt;td&amp;gt;&amp;lt;input type=&quot;text&quot; name=&quot;username&quot;&amp;gt;&amp;lt;/td&amp;gt;
        &amp;lt;/tr&amp;gt;
        &amp;lt;tr&amp;gt;
            &amp;lt;td&amp;gt;密码:&amp;lt;/td&amp;gt;
            &amp;lt;td&amp;gt;&amp;lt;input type=&quot;password&quot; name=&quot;password&quot;&amp;gt;&amp;lt;/td&amp;gt;
        &amp;lt;/tr&amp;gt;
        &amp;lt;tr&amp;gt;
            &amp;lt;td colspan=&quot;2&quot;&amp;gt;
                &amp;lt;button type=&quot;submit&quot; name=&quot;type&quot; value=&quot;register&quot;&amp;gt;注册&amp;lt;/button&amp;gt;
                &amp;lt;button type=&quot;submit&quot; name=&quot;type&quot; value=&quot;binding&quot;&amp;gt;绑定&amp;lt;/button&amp;gt;
            &amp;lt;/td&amp;gt;
        &amp;lt;/tr&amp;gt;
    &amp;lt;/table&amp;gt;
&amp;lt;/form&amp;gt;
&amp;lt;/body&amp;gt;
&amp;lt;/html&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;此时扫码进行授权后会跳转到自定义注册页面，说明配置已经生效。&lt;/p&gt;
&lt;h2&gt;QQ登录和系统绑定&lt;/h2&gt;
&lt;p&gt;由于这里的注册其实是从第三方登陆过程中重定向而来的注册，因此这种情况下的注册肯定与普通的用户注册有一定的区别。都要有哪些区别呢？其实主要在两块：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;一块是上篇文章说所的当用户注册完，肯定要与该第三方（即QQ、微信等）建立起关系，也就是说下次用户直接用QQ或微信等进行登陆，那肯定直接就可以登陆了。—》落实到代码上，就是用户注册完，需要同时往userconnection表里插入一条数据。&lt;/li&gt;
&lt;li&gt;第二块在于注册页面的展示与逻辑。下面是我直接在当当网进行QQ登陆时导向的注册页面，我们可以看到人家在这种情况下的注册页面里就不光有注册功能，还同时把从QQ获取到的你的头像和昵称信息给显示了出来，让你有种亲切感☺。当然下面的注册页面还包括一块逻辑是绑定，即当你已经有用户了，只是没与QQ进行过关联，所以需要绑定，说白了就是需要在userconnection表里插入一条你的用户与QQ号的关联数据，这块逻辑我会在下文进行讲解。
&lt;img src=&quot;%E5%BD%93%E5%BD%93%E7%BD%91.png&quot; alt=&quot;&quot; /&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;其实大家除了见过像当当这种直接点击QQ登陆进行登陆，由于没有进行注册或绑定过，而被导向一个用户注册页面的网站，肯定也见过那种从来就没上过的网站直接点击QQ登陆或者微信登陆，但是却可以直接登陆成功而不需要注册的网站，而且很多。那这种又是怎么弄得呢？这一块也是本文需要讲解的内容。
因此，本文将主要讲解如下三块内容：&lt;/p&gt;
&lt;p&gt;第三方登陆时用户注册页面如何拿到QQ头像和昵称等信息
用户注册时如何将QQ与用户信息建立关联–》插入userconnection表
第三方登陆时，如何跳过注册逻辑&lt;/p&gt;
&lt;h3&gt;第三方登陆时用户注册页面如何拿到QQ头像和昵称等信息&lt;/h3&gt;
&lt;h4&gt;配置获取QQ用户信息的工具类 —ProviderSignInUtils&lt;/h4&gt;
&lt;p&gt;上篇文章解读&lt;code&gt;doAuthentication&lt;/code&gt; 方法时，有下面这样一段代码：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;catch (BadCredentialsException e) {
			// connection unknown, register new user?
			if (signupUrl != null) {
				// store ConnectionData in session and redirect to register page
				sessionStrategy.setAttribute(new ServletWebRequest(request), ProviderSignInAttempt.SESSION_ATTRIBUTE, new ProviderSignInAttempt(token.getConnection()));
				throw new SocialAuthenticationRedirectException(buildSignupUrl(request));
			}
			throw e;
		}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;即SpringSocial/SpringSecurity在抛出跳转向/signup的重定向异常SocialAuthenticationRedirectException之前其实做了一件事，就是将从QQ获取到的Connection对象存到了session里，因此如果想在注册页面上显示QQ的头像和昵称信息，是可以从session里获取到的。springsocial其实提供了一个从session获取Connection对象的工具类，但我们需要配置一下：&lt;/p&gt;
&lt;p&gt;ProviderSignInUtils&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;    /**
     * ProviderSignInUtils有两个作用：
     * （1）从session里获取封装了第三方用户信息的Connection对象
     * （2）将注册的用户信息与第三方用户信息（QQ信息）建立关系并将该关系插入到userconnection表里
     * 
     * 需要的两个参数：
     * （1）ConnectionFactoryLocator 可以直接注册进来，用来定位ConnectionFactory
     * （2）UsersConnectionRepository----》getUsersConnectionRepository(connectionFactoryLocator)
     *     即我们配置的用来处理userconnection表的bean
     * @param connectionFactoryLocator
     * @return
     */
    @Bean
    public ProviderSignInUtils providerSignInUtils(ConnectionFactoryLocator connectionFactoryLocator) {
        return new ProviderSignInUtils(connectionFactoryLocator,
                getUsersConnectionRepository(connectionFactoryLocator)) {
        };
    }
&lt;/code&gt;&lt;/pre&gt;
&lt;h4&gt;创建获取QQ用户信息的接口&lt;/h4&gt;
&lt;p&gt;注册页面展示QQ用户信息其实只需要头像和昵称就可以，我们可以简单封装一个实体类如下：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;package com.nrsc.security.browser.beans;

import lombok.Data;

/**
 * @author : Sun Chuan
 * @date : 2019/9/11 23:03
 * Description：展示给前端的第三方用户信息
 */
@Data
public class SocialUserInfo {
    /**提供商唯一标识*/
    private String providerId;

    /***用户在提供商的唯一标识（其实就是openId）*/
    private String providerUserId;

    /**用户在提供商的昵称*/
    private String nickName;

    /**用户在提供商的头像*/
    private String headImg;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h4&gt;获取用户信息&lt;/h4&gt;
&lt;p&gt;则获取QQ用户信息的接口如下：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;    /**
     * 获取第三方用户信息
     *
     * @param request
     * @return
     */
    @GetMapping(&quot;/social/user&quot;)
    public SocialUserInfo getSocialUserInfo(HttpServletRequest request) {
        SocialUserInfo userInfo = new SocialUserInfo();
        //从session里取出封装了第三方信息QQ用户信息）的Connection对象
        Connection&amp;lt;?&amp;gt; connection = providerSignInUtils.getConnectionFromSession(new ServletWebRequest(request));
        userInfo.setProviderId(connection.getKey().getProviderId());
        userInfo.setProviderUserId(connection.getKey().getProviderUserId());
        userInfo.setNickName(connection.getDisplayName());
        userInfo.setHeadImg(connection.getImageUrl());
        return userInfo;
    }
&lt;/code&gt;&lt;/pre&gt;
&lt;h4&gt;注册页面获取QQ头像+昵称&lt;/h4&gt;
&lt;pre&gt;&lt;code&gt;&amp;lt;!DOCTYPE html&amp;gt;
&amp;lt;html&amp;gt;
&amp;lt;head&amp;gt;
    &amp;lt;meta charset=&quot;UTF-8&quot;&amp;gt;
    &amp;lt;title&amp;gt;注册&amp;lt;/title&amp;gt;
    &amp;lt;script type=&quot;text/javascript&quot; src=&quot;/js/jquery-3.4.1.min.js&quot;&amp;gt;&amp;lt;/script&amp;gt;
&amp;lt;/head&amp;gt;
&amp;lt;body&amp;gt;
&amp;lt;h2&amp;gt;Demo注册页&amp;lt;/h2&amp;gt;
&amp;lt;img id=&quot;headImage&quot;&amp;gt;hi, &amp;lt;span id=&quot;nickName&quot;&amp;gt;&amp;lt;/span&amp;gt; ,欢迎登陆XXX网
&amp;lt;hr&amp;gt;
&amp;lt;form action=&quot;/user/register&quot; method=&quot;post&quot;&amp;gt;
    &amp;lt;table&amp;gt;
        &amp;lt;tr&amp;gt;
            &amp;lt;td&amp;gt;用户名:&amp;lt;/td&amp;gt;
            &amp;lt;td&amp;gt;&amp;lt;input type=&quot;text&quot; name=&quot;username&quot;&amp;gt;&amp;lt;/td&amp;gt;
        &amp;lt;/tr&amp;gt;
        &amp;lt;tr&amp;gt;
            &amp;lt;td&amp;gt;密码:&amp;lt;/td&amp;gt;
            &amp;lt;td&amp;gt;&amp;lt;input type=&quot;password&quot; name=&quot;password&quot;&amp;gt;&amp;lt;/td&amp;gt;
        &amp;lt;/tr&amp;gt;
        &amp;lt;tr&amp;gt;
            &amp;lt;td colspan=&quot;2&quot;&amp;gt;
                &amp;lt;button type=&quot;submit&quot; name=&quot;type&quot; value=&quot;register&quot;&amp;gt;注册&amp;lt;/button&amp;gt;
                &amp;lt;button type=&quot;submit&quot; name=&quot;type&quot; value=&quot;binding&quot;&amp;gt;绑定&amp;lt;/button&amp;gt;
            &amp;lt;/td&amp;gt;
        &amp;lt;/tr&amp;gt;
    &amp;lt;/table&amp;gt;
&amp;lt;/form&amp;gt;

&amp;lt;script type=&quot;text/javascript&quot;&amp;gt;
    function getSocialUserInfo() {
        $.get({
            url: &quot;/social/user&quot;,
            type: &quot;get&quot;,
            success: function (res) {
                $(&quot;#headImage&quot;).attr(&quot;src&quot;, res.headImg);
                $(&quot;#nickName&quot;).text(res.nickName);
            }
        })
    }

    window.onload = getSocialUserInfo;
&amp;lt;/script&amp;gt;
&amp;lt;/body&amp;gt;
&amp;lt;/html&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;h4&gt;对注册页面获取第三方信息接口进行授权&lt;/h4&gt;
&lt;h4&gt;测试&lt;/h4&gt;
&lt;p&gt;再次进行QQ登陆并扫码授权，进入的注册页面如下，即页面上已经获得到了QQ头像和QQ昵称信息。&lt;/p&gt;
&lt;h3&gt;用户注册时如何将QQ与用户信息建立关联–》插入userconnection表&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;h2&gt;对注册接口进行授权&lt;/h2&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;h2&gt;处理注册逻辑+将QQ与用户信息进行关联&lt;/h2&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;    @PostMapping(&quot;/register&quot;)
    public void register(NrscUser user, HttpServletRequest request) {
        //注册用户相关逻辑-----》即向用户表里插入一条用户数据-----》这里不写了

        //不管是注册用户还是绑定用户，都会拿到一个用户唯一标识，如用户名。
        String userId = user.getUsername();
        //将用户userId和第三方用户信息建立关系并将其插入到userconnection表
        providerSignInUtils.doPostSignUp(userId, new ServletWebRequest(request));
    }
&lt;/code&gt;&lt;/pre&gt;
&lt;h4&gt;mysql里rank为关键字问题解决&lt;/h4&gt;
&lt;blockquote&gt;
&lt;p&gt;其实rank在我的mysql版本里是一个保留关键字，因此需要再将rank弄成 &lt;code&gt;rank&lt;/code&gt; 的样子才可以。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;为什么会报出这个bug呢？跟踪springsocial源码发现在执行 providerSignInUtils.doPostSignUp(userId, new ServletWebRequest(request));语句进行存储数据时，会调用JdbcConnectionRepository类里的如下方法：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;	@Transactional
	public void addConnection(Connection&amp;lt;?&amp;gt; connection) {
		try {
			ConnectionData data = connection.createData();
			int rank = jdbcTemplate.queryForObject(&quot;select coalesce(max(rank) + 1, 1) as rank from &quot; + tablePrefix + &quot;UserConnection where userId = ? and providerId = ?&quot;, new Object[]{ userId, data.getProviderId() }, Integer.class);
			jdbcTemplate.update(&quot;insert into &quot; + tablePrefix + &quot;UserConnection (userId, providerId, providerUserId, rank, displayName, profileUrl, imageUrl, accessToken, secret, refreshToken, expireTime) values (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)&quot;,
					userId, data.getProviderId(), data.getProviderUserId(), rank, data.getDisplayName(), data.getProfileUrl(), data.getImageUrl(), encrypt(data.getAccessToken()), encrypt(data.getSecret()), encrypt(data.getRefreshToken()), data.getExpireTime());
		} catch (DuplicateKeyException e) {
			throw new DuplicateConnectionException(connection.getKey());
		}
	}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;追踪到该方法时，真有种不看不知道一看吓一跳的感觉，因为JdbcConnectionRepository这个类里很多方法都用到了rank字段，那操作数据库时肯定就会有很多的问题了。但是仔细分析一下可以看到该类是一个没有被public，private等字段修饰的类，即该类只允许同一个包中的类进行访问。那它在哪个包呢？其实就是JdbcUsersConnectionRepository所在的包，看下图：&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://tangtangit.com/_astro/jdbcConnection.BmyGgPtL_Z1dRo0t.webp&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;再仔细一看，可以在JdbcUsersConnectionRepository类里看到如下方法&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;	public ConnectionRepository createConnectionRepository(String userId) {
		if (userId == null) {
			throw new IllegalArgumentException(&quot;userId cannot be null&quot;);
		}
		return new JdbcConnectionRepository(userId, jdbcTemplate, connectionFactoryLocator, textEncryptor, tablePrefix);
	}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;即JdbcConnectionRepository类是在JdbcUsersConnectionRepository类里new出来的。那这个问题就好解决了&lt;/p&gt;
&lt;p&gt;3.3.3 我的解决方式&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;将数据库里的rank字段改成了sequence —》感觉尽量不要用mysql的保留关键字&lt;/li&gt;
&lt;li&gt;将org.springframework.social.connect.jdbc包下的JdbcUsersConnectionRepository类和JdbcConnectionRepository类拷贝了一份放到了我的项目目录中—》放在了com.nrsc.security.core.social.jdbc包路径下&lt;/li&gt;
&lt;li&gt;将JdbcConnectionRepository中的所有rank改成了sequence&lt;/li&gt;
&lt;li&gt;将UsersConnectionRepository换成了我自己的刚复制的NrscJdbcUsersConnectionRepository即&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;    @Override
    public UsersConnectionRepository getUsersConnectionRepository(ConnectionFactoryLocator connectionFactoryLocator) {

        /**
         * 第二个参数的作用：根据条件查找该用哪个ConnectionFactory来构建Connection对象
         * 第三个参数的作用: 对插入到userconnection表中的数据进行加密和解密
         */
        NrscJdbcUsersConnectionRepository repository = new NrscJdbcUsersConnectionRepository(dataSource, connectionFactoryLocator, Encryptors.noOpText());
        //设置userconnection表的前缀
        repository.setTablePrefix(&quot;nrsc_&quot;);
        return repository;
    }
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;QQ免注册（自动注册）&lt;/h2&gt;
&lt;blockquote&gt;
&lt;p&gt;总结：1、实现ConnectionSignUp接口，并实现execute方法 ； 2、ConnectionSignUp具体实现类注入到JdbcUsersConnectionRepository&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h3&gt;1 概述&lt;/h3&gt;
&lt;p&gt;正如上文所诉，现实中，我们肯定碰到过很多网站或APP，虽然之前我们从未访问过这些网站，但是当我们用QQ或微信等进行登陆时，却可以直接登陆成功，根本不会被提醒让我们再注册一个新的用户。其实我觉得这样体验好像反而更好一些☺。&lt;/p&gt;
&lt;p&gt;在springsocial/springsecurity的逻辑里，这要如何去实现呢？我们必须还是要从springsocial/springsecurity的源码里去探寻答案。&lt;/p&gt;
&lt;p&gt;SocialAuthenticationProvider中的authenticate方法，其部分源码如下：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;		Connection&amp;lt;?&amp;gt; connection = authToken.getConnection();
		//拿着Connection对象去userconnection表里去查相应的userId，由于我们的系统里暂时还没有一个用户，
		//所以查到的userId肯定为null----》为null就会报出BadCredentialsException---》然后就会回到上面的类
		//让你跳转到一个XXX/signup的url----》其实说白了就是引导用户进入注册用户的页面（这一块需要我们自己实现）	
		String userId = toUserId(connection);
		if (userId == null) {
			throw new BadCredentialsException(&quot;Unknown access token&quot;);
		}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;当时文章只是说由于我们的系统里还没有一个用户，所以查到的userId肯定为null，实际上深入该代码深处，可以看到其逻辑不止这么简单，而本篇文章标题所说的免注册的解决方法也正是隐藏于此。&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;这里需要注意：如果throw new BadCredentialsException(&quot;Unknown access token&quot;); 这个样的话，就会到 /sigin,登录页面。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;ul&gt;
&lt;li&gt;先看一下toUserId的具体代码&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;protected String toUserId(Connection&amp;lt;?&amp;gt; connection) {
	//拿着Connection对象去找userIds
	List&amp;lt;String&amp;gt; userIds = usersConnectionRepository.findUserIdsWithConnection(connection);
	// only if a single userId is connected to this providerUserId
	return (userIds.size() == 1) ? userIds.iterator().next() : null;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;再看一下findUserIdsWithConnection的具体代码（代码所在类JdbcUsersConnectionRepository）:&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;	public List&amp;lt;String&amp;gt; findUserIdsWithConnection(Connection&amp;lt;?&amp;gt; connection) {
		ConnectionKey key = connection.getKey();
		//通过providerId和providerUserId（openId）去userconnection表里查找useIds
		List&amp;lt;String&amp;gt; localUserIds = jdbcTemplate.queryForList(&quot;select userId from &quot; + tablePrefix + &quot;UserConnection where providerId = ? and providerUserId = ?&quot;, String.class, key.getProviderId(), key.getProviderUserId());	
		//如果没找到useId（没关联过QQ肯定找不到userId）且connectionSignUp不为null 
		if (localUserIds.size() == 0 &amp;amp;&amp;amp; connectionSignUp != null) {
			//调用connectionSignUp的execute方法生成一个useId
			String newUserId = connectionSignUp.execute(connection);
			//如果生成成功
			if (newUserId != null)
			{	//则将生成的userId连同Connection对象，插入到userconnection表，即利用代码“偷偷地”注册一个用户
				createConnectionRepository(newUserId).addConnection(connection);
				//将生成的useId返回 
				return Arrays.asList(newUserId);
			}
		}
		//走到这里说明通过providerId和providerUserId（openId）找到了useId
		return localUserIds;
	}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;通过上面的源代码解读，我想大家肯定已经知道，只要我们在JdbcUsersConnectionRepository类里注入一个 &lt;code&gt;connectionSignUp&lt;/code&gt; ，并实现其 &lt;code&gt;execute&lt;/code&gt; 方法就可以让用户在进行第三方登陆的过程中，不再走注册新用户的逻辑，而直接登陆我们的网站了。&lt;/p&gt;
&lt;h3&gt;具体代码实现&lt;/h3&gt;
&lt;h4&gt;编写ConnectionSignUp的具体实现&lt;/h4&gt;
&lt;pre&gt;&lt;code&gt;package com.nrsc.security.server.security;

import org.springframework.social.connect.Connection;
import org.springframework.social.connect.ConnectionSignUp;
import org.springframework.stereotype.Component;

/**
 * @author : Sun Chuan
 * @date : 2019/9/14 23:29
 * Description：ConnectionSignUp类，需要注入到JdbcUsersConnectionRepository类里，
 * 以实现第三方登陆时免注册的逻辑功能
 */
@Component
public class DemoConnectionSignUp implements ConnectionSignUp {
    @Override
    public String execute(Connection&amp;lt;?&amp;gt; connection) {
        //真实项目中： TODO
        //这里其实应该向我们的用户业务表（user表）里插入一条数据
        //可以将插入user数据后的主键作为userId进行返回
        //新增之前，需要查询用户是否已存在。
        //这里为了简单起见，我就直接将connection对象中的displayName作为useId进行返回了
        return connection.getDisplayName();
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h4&gt;将编写的ConnectionSignUp具体实现类注入到JdbcUsersConnectionRepository&lt;/h4&gt;
&lt;ul&gt;
&lt;li&gt;这里将JdbcUsersConnectionRepository类注入到spring容器的全部代码贴出如下&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;package com.nrsc.security.core.social;

import com.nrsc.security.core.properties.NrscSecurityProperties;
import com.nrsc.security.core.social.jdbc.NrscJdbcUsersConnectionRepository;
import com.nrsc.security.core.social.qq.NrscSpringSocialConfigurer;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.crypto.encrypt.Encryptors;
import org.springframework.social.config.annotation.EnableSocial;
import org.springframework.social.config.annotation.SocialConfigurerAdapter;
import org.springframework.social.connect.ConnectionFactoryLocator;
import org.springframework.social.connect.ConnectionSignUp;
import org.springframework.social.connect.UsersConnectionRepository;
import org.springframework.social.connect.web.ProviderSignInUtils;
import org.springframework.social.security.SpringSocialConfigurer;

import javax.sql.DataSource;

/**
 * @author : Sun Chuan
 * @date : 2019/8/7 20:57
 * Description:
 * UsersConnectionRepository的实现类，用来拿着Connection对象查找UserConnection表中是否与之相对应的userId
 * userId就是我们系统中的唯一标识，这个应该由各个系统自己根据业务去指定，比如说你系统里的username是唯一的，
 * 则这个useId就可以是username
 */
@Configuration
@EnableSocial
public class SocialConfig extends SocialConfigurerAdapter {

    @Autowired
    private DataSource dataSource;
    @Autowired
    private NrscSecurityProperties nrscSecurityProperties;
    /**
     * 并不一定所有的系统都会在用户进行第三方登陆时“偷偷地”给用户注册一个新用户
     * 所以这里需要标明required = false
     */
    @Autowired(required = false)
    private ConnectionSignUp connectionSignUp;


    @Override
    public UsersConnectionRepository getUsersConnectionRepository(ConnectionFactoryLocator connectionFactoryLocator) {

        /**
         * 第二个参数的作用：根据条件查找该用哪个ConnectionFactory来构建Connection对象
         * 第三个参数的作用: 对插入到userconnection表中的数据进行加密和解密
         */
        NrscJdbcUsersConnectionRepository repository = new NrscJdbcUsersConnectionRepository(dataSource, connectionFactoryLocator, Encryptors.noOpText());
        //设置userconnection表的前缀
        repository.setTablePrefix(&quot;nrsc_&quot;);

        if (connectionSignUp != null) {
            //如果有spring容器里connectionSignUp这个bean时，将其注入到UsersConnectionRepository
            repository.setConnectionSignUp(connectionSignUp);
        }
        return repository;
    }

    /**
     * 通过apply()该实例，可以将SocialAuthenticationFilter加入到spring-security的过滤器链
     */
    @Bean
    public SpringSocialConfigurer nrscSocialSecurityConfig() {
        // 默认配置类，进行组件的组装
        // 包括了过滤器SocialAuthenticationFilter 添加到security过滤链中
        String filterProcessesUrl = nrscSecurityProperties.getSocial().getFilterProcessesUrl();
        NrscSpringSocialConfigurer configurer = new NrscSpringSocialConfigurer(filterProcessesUrl);

        //指定SpringSocial/SpringSecurity跳向注册页面时的url为我们配置的url
        configurer.signupUrl(nrscSecurityProperties.getBrowser().getSignUpUrl());
        return configurer;
    }

    /**
     * ProviderSignInUtils有两个作用：
     * （1）从session里获取封装了第三方用户信息的Connection对象
     * （2）将注册的用户信息与第三方用户信息（QQ信息）建立关系并将该关系插入到userconnection表里
     * &amp;lt;p&amp;gt;
     * 需要的两个参数：
     * （1）ConnectionFactoryLocator 可以直接注册进来，用来定位ConnectionFactory
     * （2）UsersConnectionRepository----》getUsersConnectionRepository(connectionFactoryLocator)
     * 即我们配置的用来处理userconnection表的bean
     *
     * @param connectionFactoryLocator
     * @return
     */
    @Bean
    public ProviderSignInUtils providerSignInUtils(ConnectionFactoryLocator connectionFactoryLocator) {
        return new ProviderSignInUtils(connectionFactoryLocator,
                getUsersConnectionRepository(connectionFactoryLocator)) {
        };
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;测试&lt;/h3&gt;
&lt;p&gt;&lt;img src=&quot;%E6%B5%8B%E8%AF%95QQ%E7%99%BB%E5%BD%95%E9%BB%98%E8%AE%A4%E6%B3%A8%E5%86%8C.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;h2&gt;QQ登录成功处理器和失败处理器&lt;/h2&gt;
&lt;p&gt;源码分析：&lt;/p&gt;
&lt;p&gt;非常重要的过滤器链 SocialAuthenticationFilter  部分源码如下。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;public class SocialAuthenticationFilter extends AbstractAuthenticationProcessingFilter {
    private SocialAuthenticationServiceLocator authServiceLocator;
    private String signupUrl = &quot;/signup&quot;;
    private String connectionAddedRedirectUrl = &quot;/&quot;;
    private boolean updateConnections = true;
    private UserIdSource userIdSource;
    private UsersConnectionRepository usersConnectionRepository;
    private SimpleUrlAuthenticationFailureHandler delegateAuthenticationFailureHandler;
    private SessionStrategy sessionStrategy = new HttpSessionSessionStrategy();
    private String filterProcessesUrl = &quot;/auth&quot;;
    private static final String DEFAULT_FAILURE_URL = &quot;/signin&quot;;
    private static final String DEFAULT_FILTER_PROCESSES_URL = &quot;/auth&quot;;
&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;此过滤器中只有失败处理器 SimpleUrlAuthenticationFailureHandler。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;由此联想，此过滤器继承的 AbstractAuthenticationProcessingFilter中有默认的成功处理器。&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;我们设置SocialAuthenticationFilter的时候，去覆盖对应的成功处理器和失败处理器，即完成我们想法。自定义跳转。&lt;/p&gt;
&lt;h3&gt;1、继承默认成功处理器&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;/**
 * &amp;lt;p&amp;gt;DESC: 成功登录处理器&amp;lt;/p&amp;gt;
 * &amp;lt;p&amp;gt;DATE: 2019-11-08 13:36&amp;lt;/p&amp;gt;
 * &amp;lt;p&amp;gt;VERSION:2.0.0&amp;lt;/p&amp;gt;
 * &amp;lt;p&amp;gt;@AUTHOR: ZhengYong&amp;lt;/p&amp;gt;
 */
@Component
public class QQSuccessHandler extends SimpleUrlAuthenticationSuccessHandler {

    @Override
    public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws IOException, ServletException {
        this.getRedirectStrategy().sendRedirect(request, response, &quot;http://www.baidu.com&quot;);
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;2、将我们的成功处理器加到SocialAuthenticationFilter&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;/**
 * &amp;lt;p&amp;gt;DESC: 覆盖后置方法，修改拦截地址&amp;lt;/p&amp;gt;
 * &amp;lt;p&amp;gt;DATE: 2019-11-06 16:38&amp;lt;/p&amp;gt;
 * &amp;lt;p&amp;gt;VERSION:2.0.0&amp;lt;/p&amp;gt;
 * &amp;lt;p&amp;gt;@AUTHOR: ZhengYong&amp;lt;/p&amp;gt;
 */
public class MySpringSocialConfigurer extends SpringSocialConfigurer {

    private String url;
    private AuthenticationSuccessHandler handler;

    public MySpringSocialConfigurer(String url, AuthenticationSuccessHandler redUrl) {
        this.url = url;
        this.handler = redUrl;
    }

    @Override
    protected &amp;lt;T&amp;gt; T postProcess(T object) {
        //在父类处理完SocialAuthenticationFilter之后的基础上，将其默认拦截url改成我们配置的值
        SocialAuthenticationFilter filter = (SocialAuthenticationFilter) super.postProcess(object);
        filter.setFilterProcessesUrl(url);
        filter.setAuthenticationSuccessHandler(handler);
        return (T) filter;
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;构建自定义 config的时候，传入对应数据即可&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;    @Bean
    public SpringSocialConfigurer imoocSocialSecurityConfig() {
        // 默认配置类，进行组件的组装
        // 包括了过滤器SocialAuthenticationFilter 添加到security过滤链中
        QQProperties qq = properties.getSocial().getQq();
        MySpringSocialConfigurer config = new MySpringSocialConfigurer(qq.getFilterProcessesUrl(),qqSuccessHandler);
        config.signupUrl(properties.getBrowser().getSignUpUrl());
        return config;
    }
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;实现微信公号登录&lt;/h2&gt;
&lt;blockquote&gt;
&lt;p&gt;相比QQ标准的oauth2开发流程，微信的有一些变化。&lt;/p&gt;
&lt;p&gt;1、接口认证步骤又之前的 4 步变为3步&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;    /**
     * 跳转认证页面
     */
    private static final String AUTHORIZE_URL = &quot;https://open.weixin.qq.com/connect/oauth2/authorize&quot;;
    /**
     * 获取token路径
     */
    private static final String ACCESS_TOKEN_URL = &quot;https://api.weixin.qq.com/sns/oauth2/access_token&quot;;
    /**
     * 获取用户信息
     */
    private String infoUrl = &quot;https://api.weixin.qq.com/sns/userinfo?openid=%s&amp;amp;lang=zh_CN&quot;;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;2、social中的标准字段和微信的不匹配，例如：client_id  ---》 appid&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;&lt;img src=&quot;%E5%BE%AE%E4%BF%A1%E5%85%AC%E4%BC%97%E5%8F%B7%E6%B5%81%E7%A8%8B%E8%A7%A3%E6%9E%90.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;h2&gt;一、api实现&lt;/h2&gt;
&lt;h3&gt;1）、WeChat 接口（6步）&lt;/h3&gt;
&lt;p&gt;注：&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;此处需要传入openId。原因：因为微信认证流程少了一步专门获取openid。直接在第二步获取token中就返回了openID。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;pre&gt;&lt;code&gt;/**
 * &amp;lt;p&amp;gt;DESC: 微信api&amp;lt;/p&amp;gt;
 * &amp;lt;p&amp;gt;DATE: 2019-11-08 14:17&amp;lt;/p&amp;gt;
 * &amp;lt;p&amp;gt;VERSION:2.0.0&amp;lt;/p&amp;gt;
 * &amp;lt;p&amp;gt;@AUTHOR: ZhengYong&amp;lt;/p&amp;gt;
 */
public interface WeChat {

    /**
     * 获取用户信息
     * @param openId  openID
     * @return 用户
     */
    WeChatUserInfo getUserInfo(String openId);
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;2）、WeChat实现&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;/**
 * &amp;lt;p&amp;gt;DESC: 实现&amp;lt;/p&amp;gt;
 * &amp;lt;p&amp;gt;DATE: 2019-11-08 14:21&amp;lt;/p&amp;gt;
 * &amp;lt;p&amp;gt;VERSION:2.0.0&amp;lt;/p&amp;gt;
 * &amp;lt;p&amp;gt;@AUTHOR: ZhengYong&amp;lt;/p&amp;gt;
 */
@Slf4j
@EqualsAndHashCode(callSuper = true)
@Data
public class WeChatImpl extends AbstractOAuth2ApiBinding implements WeChat {

    /**
     * 获取用户信息
     */
    private String infoUrl = &quot;https://api.weixin.qq.com/sns/userinfo?openid=%s&amp;amp;lang=zh_CN&quot;;

    public WeChatImpl(String accessToken) {
        super(accessToken, TokenStrategy.ACCESS_TOKEN_PARAMETER);
    }

    /**
     * 设置发送请求的 messageConverters
     * 这里是重写的父类的方法
     * @return list
     */
    @Override
    protected List&amp;lt;HttpMessageConverter&amp;lt;?&amp;gt;&amp;gt; getMessageConverters() {
        List&amp;lt;HttpMessageConverter&amp;lt;?&amp;gt;&amp;gt; messageConverters = super.getMessageConverters();
        messageConverters.remove(0);
        messageConverters.add(new StringHttpMessageConverter(StandardCharsets.UTF_8));
        return messageConverters;
    }

    /**
     * 获取用户信息
     * @param openId  openID
     * @return 自己封装的用户信息
     */
    @Override
    public WeChatUserInfo getUserInfo(String openId) {
        String url = String.format(infoUrl,openId);
        String result = getRestTemplate().getForObject(url, String.class);
        WeChatUserInfo userInfo = JSON.parseObject(result, WeChatUserInfo.class);
        log.info(&quot;【获取微信用户信息】={}&quot;,userInfo);
        return userInfo;
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;3）、WeChatUserInfo 微信用户信息&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;/**
 * &amp;lt;p&amp;gt;DESC: 微信用户信息&amp;lt;/p&amp;gt;
 * &amp;lt;p&amp;gt;DATE: 2019-11-08 14:18&amp;lt;/p&amp;gt;
 * &amp;lt;p&amp;gt;VERSION:2.0.0&amp;lt;/p&amp;gt;
 * &amp;lt;p&amp;gt;@AUTHOR: ZhengYong&amp;lt;/p&amp;gt;
 */
@Data
public class WeChatUserInfo {
    private String openid;
    private String nickname;
    private String sex;
    private String province;
    private String city;
    private String country;
    private String headimgurl;
    private String privilege;
    private String unionid;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;二、connect实现&lt;/h2&gt;
&lt;h3&gt;1）、OAuth2Template (1到5步)&lt;/h3&gt;
&lt;blockquote&gt;
&lt;p&gt;默认的OAuth2Template不满足需求，需要重写父类的方法&lt;/p&gt;
&lt;p&gt;1、buildAuthorizeUrl 认证地址URL&lt;/p&gt;
&lt;p&gt;2、exchangeForAccess 获取token的URL&lt;/p&gt;
&lt;p&gt;3、postForAccessGrant 获取token接收返回参数&lt;/p&gt;
&lt;p&gt;4、createRestTemplate,添加StringHttpMessageConverter&lt;/p&gt;
&lt;p&gt;5、AccessGrant，标准的存储令牌类不满足要求，继承并添加需要的 openID 属性&lt;/p&gt;
&lt;/blockquote&gt;
&lt;pre&gt;&lt;code&gt;/**
 * &amp;lt;p&amp;gt;DESC: 操作&amp;lt;/p&amp;gt;
 * &amp;lt;p&amp;gt;DATE: 2019-11-08 15:50&amp;lt;/p&amp;gt;
 * &amp;lt;p&amp;gt;VERSION:2.0.0&amp;lt;/p&amp;gt;
 * &amp;lt;p&amp;gt;@AUTHOR: ZhengYong&amp;lt;/p&amp;gt;
 */
@Slf4j
public class WeChatOauth2Template extends OAuth2Template {

    private String clientId;
    private String clientSecret;
    private String accessTokenUrl;


    public WeChatOauth2Template(String clientId, String clientSecret, String authorizeUrl, String accessTokenUrl) {
        super(clientId, clientSecret, authorizeUrl, null, accessTokenUrl);
        setUseParametersForClientAuthentication(true);
        this.clientId = clientId;
        this.clientSecret = clientSecret;
        this.accessTokenUrl = accessTokenUrl;
    }
  
      /**
     * 后续绑定微信构建认证url 要使用的方法，
     * 和下面一样，需要重构
     * @param parameters  参数
     * @return 认证地址URL
     */
    @Override
    public String buildAuthorizeUrl(OAuth2Parameters parameters) {
        String authorizeUrl = super.buildAuthorizeUrl(parameters);
        log.info(&quot;【微信认证地址】 ={}&quot;,authorizeUrl);
        String replace = authorizeUrl.replace(&quot;client_id&quot;, &quot;appid&quot;);
        replace = replace + &quot;&amp;amp;scope=snsapi_userinfo#wechat_redirect&quot;;
        log.info(&quot;【修复后微信认证地址】 ={}&quot;,replace);
        return replace;
    }

    /**
     * 重写构建 认证地址URL  ---自定义
     * @param grantType 授权类型
     * @param parameters 参数
     * @return 认证地址URL
     */
    @Override
    public String buildAuthorizeUrl(GrantType grantType, OAuth2Parameters parameters) {
        String authorizeUrl = super.buildAuthorizeUrl(grantType, parameters);
        log.info(&quot;【微信认证地址】 ={}&quot;,authorizeUrl);
        String replace = authorizeUrl.replace(&quot;client_id&quot;, &quot;appid&quot;);
        replace = replace + &quot;&amp;amp;scope=snsapi_userinfo#wechat_redirect&quot;;
        log.info(&quot;【修复后微信认证地址】 ={}&quot;,replace);
        return replace;
    }

    /**
     *
     * 前置交换令牌token，的方法  --- 此处需要自定义，因为微信提供接口和social有出入
     * @param authorizationCode  code
     * @param redirectUri        重定向地址
     * @param additionalParameters  参数
     * @return                    保存令牌的封装类
     */
    @Override
    public AccessGrant exchangeForAccess(String authorizationCode, String redirectUri, MultiValueMap&amp;lt;String, String&amp;gt; additionalParameters) {
        StringBuilder accessTokenUrl = new StringBuilder(this.accessTokenUrl);
        accessTokenUrl.append(&quot;?appid=&quot;).append(this.clientId);
        accessTokenUrl.append(&quot;&amp;amp;secret=&quot;).append(this.clientSecret);
        accessTokenUrl.append(&quot;&amp;amp;code=&quot;).append(authorizationCode);
        accessTokenUrl.append(&quot;&amp;amp;grant_type=authorization_code&quot;);
        return this.postForAccessGrant(String.valueOf(accessTokenUrl),null);
    }

    /**
     * 后置组装令牌的方法   ----此处需要自定义，微信返回的参数和标准有出入
     * AccessGrant 是保存令牌的封装类
     * @param accessTokenUrl  获取tokenUrl
     * @param parameters      参数
     * @return                保存令牌的封装类
     */
    @Override
    protected AccessGrant postForAccessGrant(String accessTokenUrl, MultiValueMap&amp;lt;String, String&amp;gt; parameters) {
        String result = getRestTemplate().getForObject(accessTokenUrl,String.class);
        log.info(&quot;获取accessToke的响应：&quot; + result);
        assert result != null;
        if(result.contains(&quot;errcode&quot;)){
            log.error(&quot;【获取token】 失败 result={}&quot;,result);
            throw new RuntimeException(&quot;获取token 失败&quot;);
        }
        JSONObject jsonObject = JSONObject.parseObject(result);
        String accessToken = (String) jsonObject.get(&quot;access_token&quot;);
        String refreshToken = (String) jsonObject.get(&quot;refresh_token&quot;);
        Integer expiresIn = (Integer) jsonObject.get(&quot;expires_in&quot;);
        String scope = (String) jsonObject.get(&quot;scope&quot;);
        //应为微信是获取token的时候直接返回openID，所有需要另做处理
        String openid = (String) jsonObject.get(&quot;openid&quot;);
        return new WeChatAccessGrant(accessToken,scope,refreshToken,Long.valueOf(expiresIn),openid);
    }

    /**
     * 修改发送 请求的 RestTemplate
     * @return RestTemplate
     */
    @Override
    protected RestTemplate createRestTemplate() {
        RestTemplate restTemplate = super.createRestTemplate();
        restTemplate.getMessageConverters().add(new StringHttpMessageConverter(StandardCharsets.UTF_8));
        return restTemplate;
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;自定义AccessGrant&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;/**
 * &amp;lt;p&amp;gt;DESC: 自定义的AccessGrant&amp;lt;/p&amp;gt;
 * &amp;lt;p&amp;gt;DATE: 2019-11-08 16:02&amp;lt;/p&amp;gt;
 * &amp;lt;p&amp;gt;VERSION:2.0.0&amp;lt;/p&amp;gt;
 * &amp;lt;p&amp;gt;@AUTHOR: ZhengYong&amp;lt;/p&amp;gt;
 */
@EqualsAndHashCode(callSuper = true)
@Data
public class WeChatAccessGrant extends AccessGrant {

    private String openid;
    
    public WeChatAccessGrant(String accessToken, String scope, String refreshToken, Long expiresIn, String openid) {
        super(accessToken, scope, refreshToken, expiresIn);
        this.openid = openid;
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;2）、OAuth2ServiceProvider&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;/**
 * &amp;lt;p&amp;gt;DESC: provider&amp;lt;/p&amp;gt;
 * &amp;lt;p&amp;gt;DATE: 2019-11-08 16:05&amp;lt;/p&amp;gt;
 * &amp;lt;p&amp;gt;VERSION:2.0.0&amp;lt;/p&amp;gt;
 * &amp;lt;p&amp;gt;@AUTHOR: ZhengYong&amp;lt;/p&amp;gt;
 */
@Data
public class WeChatServiceProvider extends AbstractOAuth2ServiceProvider&amp;lt;WeChat&amp;gt; {


    /**
     * 跳转认证页面
     */
    private static final String AUTHORIZE_URL = &quot;https://open.weixin.qq.com/connect/oauth2/authorize&quot;;
    /**
     * 获取token路径
     */
    private static final String ACCESS_TOKEN_URL = &quot;https://api.weixin.qq.com/sns/oauth2/access_token&quot;;

    public WeChatServiceProvider(String appId,String appSecret) {
        super(new WeChatOauth2Template(appId,appSecret,AUTHORIZE_URL,ACCESS_TOKEN_URL));
    }

    @Override
    public WeChat getApi(String accessToken) {
        return new WeChatImpl(accessToken);
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;3）、ApiAdapter&lt;/h3&gt;
&lt;blockquote&gt;
&lt;p&gt;注意 openID 在此处存放，也就意味着每个用户来，都应该对应一个自己的 ApiAdapter,保证openid不重复&lt;/p&gt;
&lt;p&gt;创建连接工厂去构建connection的时候，都会去重写生成一个新的apiAdapter，这样保证openid唯一，多实例属性&lt;/p&gt;
&lt;/blockquote&gt;
&lt;pre&gt;&lt;code&gt;/**
 * &amp;lt;p&amp;gt;DESC: 微信API Adapter&amp;lt;/p&amp;gt;
 * &amp;lt;p&amp;gt;DATE: 2019-11-12 10:34&amp;lt;/p&amp;gt;
 * &amp;lt;p&amp;gt;VERSION:2.0.0&amp;lt;/p&amp;gt;
 * &amp;lt;p&amp;gt;@AUTHOR: ZhengYong&amp;lt;/p&amp;gt;
 */
public class WeChatApiAdapter implements ApiAdapter&amp;lt;WeChat&amp;gt; {

    private String openId;

    /**
     * 从微信api中获取用户信息，
     * 然后设置标准连接的值
     * @param weChat    微信api,获取用户信息
     * @param connectionValues 标准连接的值
     */
    @Override
    public void setConnectionValues(WeChat weChat, ConnectionValues connectionValues) {
        WeChatUserInfo userInfo = weChat.getUserInfo(openId);
        connectionValues.setDisplayName(userInfo.getNickname());
        connectionValues.setImageUrl(userInfo.getHeadimgurl());
        connectionValues.setProfileUrl(null);
        connectionValues.setProviderUserId(openId);
    }

    public WeChatApiAdapter() {
    }

    public WeChatApiAdapter(String openId) {
        this.openId = openId;
    }

    @Override
    public boolean test(WeChat weChat) {
        return false;
    }

    @Override
    public UserProfile fetchUserProfile(WeChat weChat) {
        return null;
    }

    @Override
    public void updateStatus(WeChat weChat, String s) {
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;4）、OAuth2ConnectionFactory连接工厂&lt;/h3&gt;
&lt;blockquote&gt;
&lt;p&gt;1、 AccessGrant 中有自定义的openid,需要获取出来&lt;/p&gt;
&lt;p&gt;​	重写父类的：extractProviderUserId&lt;/p&gt;
&lt;p&gt;2、 为将自己的serviceProvider和apiAdapter添加到connection中&lt;/p&gt;
&lt;p&gt;​	覆盖父类测创建连接方法： createConnection&lt;/p&gt;
&lt;p&gt;3、 getApiAdapter   将自己 apiAdapter 添加上&lt;/p&gt;
&lt;p&gt;4、 getOAuth2ServiceProvider 将自己serviceProvider添加上&lt;/p&gt;
&lt;/blockquote&gt;
&lt;pre&gt;&lt;code&gt;/**
 * &amp;lt;p&amp;gt;DESC: 微信连接工厂&amp;lt;/p&amp;gt;
 * &amp;lt;p&amp;gt;DATE: 2019-11-12 10:36&amp;lt;/p&amp;gt;
 * &amp;lt;p&amp;gt;VERSION:2.0.0&amp;lt;/p&amp;gt;
 * &amp;lt;p&amp;gt;@AUTHOR: ZhengYong&amp;lt;/p&amp;gt;
 */
public class WeChatConnectionFactory extends OAuth2ConnectionFactory&amp;lt;WeChat&amp;gt; {

    /**
     * 自己的 serviceProvider,apiAdapter
     * @param providerId 服务ID
     * @param appId      appid
     * @param appSecret  secret
     */
    public WeChatConnectionFactory(String providerId, String appId, String appSecret) {
        super(providerId, new WeChatServiceProvider(appId, appSecret), new WeChatApiAdapter());
    }

    /**
     * 提取服务商用户ID （openid）
     * @param accessGrant 存放token令牌
     * @return openID
     */
    @Override
    protected String extractProviderUserId(AccessGrant accessGrant) {
        if (accessGrant instanceof WeChatAccessGrant) {
            return ((WeChatAccessGrant) accessGrant).getOpenid();
        }
        return null;
    }

    /**
     * 创建连接
     * @param accessGrant 存放token令牌
     * @return 返回连接
     */
    @Override
    public Connection&amp;lt;WeChat&amp;gt; createConnection(AccessGrant accessGrant) {
        return new OAuth2Connection&amp;lt;&amp;gt;(getProviderId(), extractProviderUserId(accessGrant), accessGrant.getAccessToken(),
                accessGrant.getRefreshToken(), accessGrant.getExpireTime(), getOAuth2ServiceProvider(), getApiAdapter(extractProviderUserId(accessGrant)));
    }

    /**
     * 创建连接
     * @param data 连接数据
     * @return 返回连接
     */
    @Override
    public Connection&amp;lt;WeChat&amp;gt; createConnection(ConnectionData data) {
        return new OAuth2Connection(data, getOAuth2ServiceProvider(), getApiAdapter(data.getProviderUserId()));
    }

    /**
     * 拼装ApiAdapter 讲 openID 塞进去。
     * @param providerUserId openid
     * @return  ApiAdapter
     */
    private ApiAdapter&amp;lt;WeChat&amp;gt; getApiAdapter(String providerUserId) {
        return new WeChatApiAdapter(providerUserId);
    }

    /**
     * 获取 serviceProvider
     * @return serviceProvider
     */
    private OAuth2ServiceProvider&amp;lt;WeChat&amp;gt; getOAuth2ServiceProvider() {
        return (OAuth2ServiceProvider&amp;lt;WeChat&amp;gt;) getServiceProvider();
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;三、config实现&lt;/h2&gt;
&lt;h3&gt;WeChatAutoConfig（创建工厂）&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;/**
 * &amp;lt;p&amp;gt;DESC: 微信自动配置类&amp;lt;/p&amp;gt;
 * &amp;lt;p&amp;gt;DATE: 2019-11-12 10:41&amp;lt;/p&amp;gt;
 * &amp;lt;p&amp;gt;VERSION:2.0.0&amp;lt;/p&amp;gt;
 * &amp;lt;p&amp;gt;@AUTHOR: ZhengYong&amp;lt;/p&amp;gt;
 */
@Configuration
public class WeChatAutoConfig extends SocialAutoConfigurerAdapter {

    @Resource
    private MySecurityProperties properties;

    @Override
    protected ConnectionFactory&amp;lt;?&amp;gt; createConnectionFactory() {
        WeChatProperties wechat = properties.getSocial().getWechat();
        return new WeChatConnectionFactory(wechat.getProviderId(),wechat.getAppId(),wechat.getAppSecret());
    }
}

&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;SocialAutoConfigurerAdapter 是自己写的抽象类&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;public abstract class SocialAutoConfigurerAdapter extends SocialConfigurerAdapter {
    public SocialAutoConfigurerAdapter() {
    }

    @Override
    public void addConnectionFactories(ConnectionFactoryConfigurer configurer, Environment environment) {
        configurer.addConnectionFactory(this.createConnectionFactory());
    }

    protected abstract ConnectionFactory&amp;lt;?&amp;gt; createConnectionFactory();
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;绑定和解绑&lt;/h2&gt;
&lt;h3&gt;场景：&lt;/h3&gt;
&lt;blockquote&gt;
&lt;p&gt;绑定和解绑是在用户已经登陆的情况下（即用户信息已经存在于session中），对QQ，微信等进行绑定和解绑&lt;/p&gt;
&lt;p&gt;这里简单畅想一下我说的这种场景的绑定的实现步骤：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;在进入绑定页面前session里已经存了封装了QQ信息的Connection对象&lt;/li&gt;
&lt;li&gt;输入用户名+密码后，点击绑定按钮&lt;/li&gt;
&lt;li&gt;点击绑定按钮后第一个要做的事其实是用户名+密码登陆&lt;/li&gt;
&lt;li&gt;登陆成功后应该在走登陆成功事件之前从session里取出Connection对象和用户名或用户id&lt;/li&gt;
&lt;li&gt;利用ProviderSignInUtils工具类将Connection对象+用户名或用户id建立关系并插入到userConnection表 —》完成绑定工作&lt;/li&gt;
&lt;/ul&gt;
&lt;/blockquote&gt;
&lt;h3&gt;ConnectController&lt;/h3&gt;
&lt;p&gt;ConnectController是springsocial为我们提供的一个专门用于处理绑定和解绑的Controller类。在该类里主要定义了如下几个方法：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;获取当前用户所有第三方账号的绑定状态的方法&lt;/li&gt;
&lt;li&gt;将当前用户与第三方账号进行绑定的方法&lt;/li&gt;
&lt;li&gt;解绑的方法&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;controller 方法解读&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;//
// Source code recreated from a .class file by IntelliJ IDEA
// (powered by Fernflower decompiler)
//

package org.springframework.social.connect.web;

import java.util.Collections;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import javax.inject.Inject;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.springframework.beans.factory.InitializingBean;
import org.springframework.core.GenericTypeResolver;
import org.springframework.social.connect.Connection;
import org.springframework.social.connect.ConnectionFactory;
import org.springframework.social.connect.ConnectionFactoryLocator;
import org.springframework.social.connect.ConnectionKey;
import org.springframework.social.connect.ConnectionRepository;
import org.springframework.social.connect.DuplicateConnectionException;
import org.springframework.social.connect.support.OAuth1ConnectionFactory;
import org.springframework.social.connect.support.OAuth2ConnectionFactory;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.util.LinkedMultiValueMap;
import org.springframework.util.MultiValueMap;
import org.springframework.util.StringUtils;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.context.request.NativeWebRequest;
import org.springframework.web.context.request.WebRequest;
import org.springframework.web.servlet.view.RedirectView;
import org.springframework.web.util.UrlPathHelper;

@Controller
@RequestMapping({&quot;/connect&quot;})
public class ConnectController implements InitializingBean {
    private static final Log logger = LogFactory.getLog(ConnectController.class);
    private final ConnectionFactoryLocator connectionFactoryLocator;
    private final ConnectionRepository connectionRepository;
    private final MultiValueMap&amp;lt;Class&amp;lt;?&amp;gt;, ConnectInterceptor&amp;lt;?&amp;gt;&amp;gt; connectInterceptors = new LinkedMultiValueMap();
    private final MultiValueMap&amp;lt;Class&amp;lt;?&amp;gt;, DisconnectInterceptor&amp;lt;?&amp;gt;&amp;gt; disconnectInterceptors = new LinkedMultiValueMap();
    private ConnectSupport connectSupport;
    private final UrlPathHelper urlPathHelper = new UrlPathHelper();
    private String viewPath = &quot;connect/&quot;;
    private SessionStrategy sessionStrategy = new HttpSessionSessionStrategy();
    private String applicationUrl = null;
    protected static final String DUPLICATE_CONNECTION_ATTRIBUTE = &quot;social_addConnection_duplicate&quot;;
    protected static final String PROVIDER_ERROR_ATTRIBUTE = &quot;social_provider_error&quot;;
    protected static final String AUTHORIZATION_ERROR_ATTRIBUTE = &quot;social_authorization_error&quot;;

    @Inject
    public ConnectController(ConnectionFactoryLocator connectionFactoryLocator, ConnectionRepository connectionRepository) {
        this.connectionFactoryLocator = connectionFactoryLocator;
        this.connectionRepository = connectionRepository;
    }

    /** @deprecated */
    @Deprecated
    public void setInterceptors(List&amp;lt;ConnectInterceptor&amp;lt;?&amp;gt;&amp;gt; interceptors) {
        this.setConnectInterceptors(interceptors);
    }

    public void setConnectInterceptors(List&amp;lt;ConnectInterceptor&amp;lt;?&amp;gt;&amp;gt; interceptors) {
        Iterator var2 = interceptors.iterator();

        while(var2.hasNext()) {
            ConnectInterceptor&amp;lt;?&amp;gt; interceptor = (ConnectInterceptor)var2.next();
            this.addInterceptor(interceptor);
        }

    }

    public void setDisconnectInterceptors(List&amp;lt;DisconnectInterceptor&amp;lt;?&amp;gt;&amp;gt; interceptors) {
        Iterator var2 = interceptors.iterator();

        while(var2.hasNext()) {
            DisconnectInterceptor&amp;lt;?&amp;gt; interceptor = (DisconnectInterceptor)var2.next();
            this.addDisconnectInterceptor(interceptor);
        }

    }

    public void setApplicationUrl(String applicationUrl) {
        this.applicationUrl = applicationUrl;
    }

    public void setViewPath(String viewPath) {
        this.viewPath = viewPath;
    }

    public void setSessionStrategy(SessionStrategy sessionStrategy) {
        this.sessionStrategy = sessionStrategy;
    }

    public void addInterceptor(ConnectInterceptor&amp;lt;?&amp;gt; interceptor) {
        Class&amp;lt;?&amp;gt; serviceApiType = GenericTypeResolver.resolveTypeArgument(interceptor.getClass(), ConnectInterceptor.class);
        this.connectInterceptors.add(serviceApiType, interceptor);
    }

    public void addDisconnectInterceptor(DisconnectInterceptor&amp;lt;?&amp;gt; interceptor) {
        Class&amp;lt;?&amp;gt; serviceApiType = GenericTypeResolver.resolveTypeArgument(interceptor.getClass(), DisconnectInterceptor.class);
        this.disconnectInterceptors.add(serviceApiType, interceptor);
    }

  //查看所有绑定状态的控制器
    @RequestMapping(
        method = {RequestMethod.GET}
    )
    public String connectionStatus(NativeWebRequest request, Model model) {
        this.setNoCache(request);
        this.processFlash(request, model);
        Map&amp;lt;String, List&amp;lt;Connection&amp;lt;?&amp;gt;&amp;gt;&amp;gt; connections = this.connectionRepository.findAllConnections();
        model.addAttribute(&quot;providerIds&quot;, this.connectionFactoryLocator.registeredProviderIds());
        model.addAttribute(&quot;connectionMap&quot;, connections);
        return this.connectView();
    }

  //绑定应用回调controler
    @RequestMapping(
        value = {&quot;/{providerId}&quot;},
        method = {RequestMethod.GET}
    )
    public String connectionStatus(@PathVariable String providerId, NativeWebRequest request, Model model) {
        this.setNoCache(request);
        this.processFlash(request, model);
        List&amp;lt;Connection&amp;lt;?&amp;gt;&amp;gt; connections = this.connectionRepository.findConnections(providerId);
        this.setNoCache(request);
        if (connections.isEmpty()) {
            return this.connectView(providerId);
        } else {
            model.addAttribute(&quot;connections&quot;, connections);
            return this.connectedView(providerId);
        }
    }

  //post 请求绑定指定应用的方法
    @RequestMapping(
        value = {&quot;/{providerId}&quot;},
        method = {RequestMethod.POST}
    )
    public RedirectView connect(@PathVariable String providerId, NativeWebRequest request) {
        ConnectionFactory&amp;lt;?&amp;gt; connectionFactory = this.connectionFactoryLocator.getConnectionFactory(providerId);
        MultiValueMap&amp;lt;String, String&amp;gt; parameters = new LinkedMultiValueMap();
        this.preConnect(connectionFactory, parameters, request);

        try {
            return new RedirectView(this.connectSupport.buildOAuthUrl(connectionFactory, request, parameters));
        } catch (Exception var6) {
            this.sessionStrategy.setAttribute(request, &quot;social_provider_error&quot;, var6);
            return this.connectionStatusRedirect(providerId, request);
        }
    }

    @RequestMapping(
        value = {&quot;/{providerId}&quot;},
        method = {RequestMethod.GET},
        params = {&quot;oauth_token&quot;}
    )
    public RedirectView oauth1Callback(@PathVariable String providerId, NativeWebRequest request) {
        try {
            OAuth1ConnectionFactory&amp;lt;?&amp;gt; connectionFactory = (OAuth1ConnectionFactory)this.connectionFactoryLocator.getConnectionFactory(providerId);
            Connection&amp;lt;?&amp;gt; connection = this.connectSupport.completeConnection(connectionFactory, request);
            this.addConnection(connection, connectionFactory, request);
        } catch (Exception var5) {
            this.sessionStrategy.setAttribute(request, &quot;social_provider_error&quot;, var5);
            logger.warn(&quot;Exception while handling OAuth1 callback (&quot; + var5.getMessage() + &quot;). Redirecting to &quot; + providerId + &quot; connection status page.&quot;);
        }

        return this.connectionStatusRedirect(providerId, request);
    }

    @RequestMapping(
        value = {&quot;/{providerId}&quot;},
        method = {RequestMethod.GET},
        params = {&quot;code&quot;}
    )
    public RedirectView oauth2Callback(@PathVariable String providerId, NativeWebRequest request) {
        try {
            OAuth2ConnectionFactory&amp;lt;?&amp;gt; connectionFactory = (OAuth2ConnectionFactory)this.connectionFactoryLocator.getConnectionFactory(providerId);
            Connection&amp;lt;?&amp;gt; connection = this.connectSupport.completeConnection(connectionFactory, request);
            this.addConnection(connection, connectionFactory, request);
        } catch (Exception var5) {
            this.sessionStrategy.setAttribute(request, &quot;social_provider_error&quot;, var5);
            logger.warn(&quot;Exception while handling OAuth2 callback (&quot; + var5.getMessage() + &quot;). Redirecting to &quot; + providerId + &quot; connection status page.&quot;);
        }

        return this.connectionStatusRedirect(providerId, request);
    }

    @RequestMapping(
        value = {&quot;/{providerId}&quot;},
        method = {RequestMethod.GET},
        params = {&quot;error&quot;}
    )
    public RedirectView oauth2ErrorCallback(@PathVariable String providerId, @RequestParam(&quot;error&quot;) String error, @RequestParam(value = &quot;error_description&quot;,required = false) String errorDescription, @RequestParam(value = &quot;error_uri&quot;,required = false) String errorUri, NativeWebRequest request) {
        Map&amp;lt;String, String&amp;gt; errorMap = new HashMap();
        errorMap.put(&quot;error&quot;, error);
        if (errorDescription != null) {
            errorMap.put(&quot;errorDescription&quot;, errorDescription);
        }

        if (errorUri != null) {
            errorMap.put(&quot;errorUri&quot;, errorUri);
        }

        this.sessionStrategy.setAttribute(request, &quot;social_authorization_error&quot;, errorMap);
        return this.connectionStatusRedirect(providerId, request);
    }

    @RequestMapping(
        value = {&quot;/{providerId}&quot;},
        method = {RequestMethod.DELETE}
    )
    public RedirectView removeConnections(@PathVariable String providerId, NativeWebRequest request) {
        ConnectionFactory&amp;lt;?&amp;gt; connectionFactory = this.connectionFactoryLocator.getConnectionFactory(providerId);
        this.preDisconnect(connectionFactory, request);
        this.connectionRepository.removeConnections(providerId);
        this.postDisconnect(connectionFactory, request);
        return this.connectionStatusRedirect(providerId, request);
    }

    @RequestMapping(
        value = {&quot;/{providerId}/{providerUserId}&quot;},
        method = {RequestMethod.DELETE}
    )
    public RedirectView removeConnection(@PathVariable String providerId, @PathVariable String providerUserId, NativeWebRequest request) {
        ConnectionFactory&amp;lt;?&amp;gt; connectionFactory = this.connectionFactoryLocator.getConnectionFactory(providerId);
        this.preDisconnect(connectionFactory, request);
        this.connectionRepository.removeConnection(new ConnectionKey(providerId, providerUserId));
        this.postDisconnect(connectionFactory, request);
        return this.connectionStatusRedirect(providerId, request);
    }

  /*
  * 创建视图的方法（查看绑定状态的视图）
  */
    protected String connectView() {
        return this.getViewPath() + &quot;status&quot;;
    }
  /*
  * 创建视图的方法（解绑成功视图）
  */
    protected String connectView(String providerId) {
        return this.getViewPath() + providerId + &quot;Connect&quot;;
    }
  /*
  * 创建视图的方法（绑定成功的视图）
  */
    protected String connectedView(String providerId) {
        return this.getViewPath() + providerId + &quot;Connected&quot;;
    }

    protected RedirectView connectionStatusRedirect(String providerId, NativeWebRequest request) {
        HttpServletRequest servletRequest = (HttpServletRequest)request.getNativeRequest(HttpServletRequest.class);
        String path = &quot;/connect/&quot; + providerId + this.getPathExtension(servletRequest);
        if (this.prependServletPath(servletRequest)) {
            path = servletRequest.getServletPath() + path;
        }

        return new RedirectView(path, true);
    }

    public void afterPropertiesSet() throws Exception {
        this.connectSupport = new ConnectSupport(this.sessionStrategy);
        if (this.applicationUrl != null) {
            this.connectSupport.setApplicationUrl(this.applicationUrl);
        }

    }

    private boolean prependServletPath(HttpServletRequest request) {
        return !this.urlPathHelper.getPathWithinServletMapping(request).equals(&quot;&quot;);
    }

    private String getPathExtension(HttpServletRequest request) {
        String fileName = this.extractFullFilenameFromUrlPath(request.getRequestURI());
        String extension = StringUtils.getFilenameExtension(fileName);
        return extension != null ? &quot;.&quot; + extension : &quot;&quot;;
    }

    private String extractFullFilenameFromUrlPath(String urlPath) {
        int end = urlPath.indexOf(63);
        if (end == -1) {
            end = urlPath.indexOf(35);
            if (end == -1) {
                end = urlPath.length();
            }
        }

        int begin = urlPath.lastIndexOf(47, end) + 1;
        int paramIndex = urlPath.indexOf(59, begin);
        end = paramIndex != -1 &amp;amp;&amp;amp; paramIndex &amp;lt; end ? paramIndex : end;
        return urlPath.substring(begin, end);
    }

    private String getViewPath() {
        return this.viewPath;
    }

    private void addConnection(Connection&amp;lt;?&amp;gt; connection, ConnectionFactory&amp;lt;?&amp;gt; connectionFactory, WebRequest request) {
        try {
            this.connectionRepository.addConnection(connection);
            this.postConnect(connectionFactory, connection, request);
        } catch (DuplicateConnectionException var5) {
            this.sessionStrategy.setAttribute(request, &quot;social_addConnection_duplicate&quot;, var5);
        }

    }

    private void preConnect(ConnectionFactory&amp;lt;?&amp;gt; connectionFactory, MultiValueMap&amp;lt;String, String&amp;gt; parameters, WebRequest request) {
        Iterator var4 = this.interceptingConnectionsTo(connectionFactory).iterator();

        while(var4.hasNext()) {
            ConnectInterceptor interceptor = (ConnectInterceptor)var4.next();
            interceptor.preConnect(connectionFactory, parameters, request);
        }

    }

    private void postConnect(ConnectionFactory&amp;lt;?&amp;gt; connectionFactory, Connection&amp;lt;?&amp;gt; connection, WebRequest request) {
        Iterator var4 = this.interceptingConnectionsTo(connectionFactory).iterator();

        while(var4.hasNext()) {
            ConnectInterceptor interceptor = (ConnectInterceptor)var4.next();
            interceptor.postConnect(connection, request);
        }

    }

    private void preDisconnect(ConnectionFactory&amp;lt;?&amp;gt; connectionFactory, WebRequest request) {
        Iterator var3 = this.interceptingDisconnectionsTo(connectionFactory).iterator();

        while(var3.hasNext()) {
            DisconnectInterceptor interceptor = (DisconnectInterceptor)var3.next();
            interceptor.preDisconnect(connectionFactory, request);
        }

    }

    private void postDisconnect(ConnectionFactory&amp;lt;?&amp;gt; connectionFactory, WebRequest request) {
        Iterator var3 = this.interceptingDisconnectionsTo(connectionFactory).iterator();

        while(var3.hasNext()) {
            DisconnectInterceptor interceptor = (DisconnectInterceptor)var3.next();
            interceptor.postDisconnect(connectionFactory, request);
        }

    }

    private List&amp;lt;ConnectInterceptor&amp;lt;?&amp;gt;&amp;gt; interceptingConnectionsTo(ConnectionFactory&amp;lt;?&amp;gt; connectionFactory) {
        Class&amp;lt;?&amp;gt; serviceType = GenericTypeResolver.resolveTypeArgument(connectionFactory.getClass(), ConnectionFactory.class);
        List&amp;lt;ConnectInterceptor&amp;lt;?&amp;gt;&amp;gt; typedInterceptors = (List)this.connectInterceptors.get(serviceType);
        if (typedInterceptors == null) {
            typedInterceptors = Collections.emptyList();
        }

        return typedInterceptors;
    }

    private List&amp;lt;DisconnectInterceptor&amp;lt;?&amp;gt;&amp;gt; interceptingDisconnectionsTo(ConnectionFactory&amp;lt;?&amp;gt; connectionFactory) {
        Class&amp;lt;?&amp;gt; serviceType = GenericTypeResolver.resolveTypeArgument(connectionFactory.getClass(), ConnectionFactory.class);
        List&amp;lt;DisconnectInterceptor&amp;lt;?&amp;gt;&amp;gt; typedInterceptors = (List)this.disconnectInterceptors.get(serviceType);
        if (typedInterceptors == null) {
            typedInterceptors = Collections.emptyList();
        }

        return typedInterceptors;
    }

    private void processFlash(WebRequest request, Model model) {
        this.convertSessionAttributeToModelAttribute(&quot;social_addConnection_duplicate&quot;, request, model);
        this.convertSessionAttributeToModelAttribute(&quot;social_provider_error&quot;, request, model);
        model.addAttribute(&quot;social_authorization_error&quot;, this.sessionStrategy.getAttribute(request, &quot;social_authorization_error&quot;));
        this.sessionStrategy.removeAttribute(request, &quot;social_authorization_error&quot;);
    }

    private void convertSessionAttributeToModelAttribute(String attributeName, WebRequest request, Model model) {
        if (this.sessionStrategy.getAttribute(request, attributeName) != null) {
            model.addAttribute(attributeName, Boolean.TRUE);
            this.sessionStrategy.removeAttribute(request, attributeName);
        }

    }

    private void setNoCache(NativeWebRequest request) {
        HttpServletResponse response = (HttpServletResponse)request.getNativeResponse(HttpServletResponse.class);
        if (response != null) {
            response.setHeader(&quot;Pragma&quot;, &quot;no-cache&quot;);
            response.setDateHeader(&quot;Expires&quot;, 1L);
            response.setHeader(&quot;Cache-Control&quot;, &quot;no-cache&quot;);
            response.addHeader(&quot;Cache-Control&quot;, &quot;no-store&quot;);
        }

    }
}

&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这个Controller有如下几个特点&lt;/p&gt;
&lt;p&gt;（1）该Controller里的方法只提供了数据，并没有提供视图，比如说下面的方法为&lt;code&gt;获取当前用户所有第三方账号绑定状态的Controller方法&lt;/code&gt;，它虽然向Model对象里写了数据并指定了返回视图的名称 —》connect/status，但并没提供该视图。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;	@RequestMapping(method=RequestMethod.GET)
	public String connectionStatus(NativeWebRequest request, Model model) {
		setNoCache(request);
		processFlash(request, model);
		Map&amp;lt;String, List&amp;lt;Connection&amp;lt;?&amp;gt;&amp;gt;&amp;gt; connections = connectionRepository.findAllConnections();
		//将providerIds放到model中---Set&amp;lt;String&amp;gt;
		model.addAttribute(&quot;providerIds&quot;, connectionFactoryLocator.registeredProviderIds());	
		//将Connection放到model中	
		model.addAttribute(&quot;connectionMap&quot;, connections);
		//下面返回视图名其实是connect/status----但是springsocial并没有提供该名称的视图
		//需要我们自己写代码实现该视图，并返回给前端特定的数据
		return connectView();
	}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;(2) 该Controller提供的方法都是基于session的 。&lt;/p&gt;
&lt;p&gt;(3) 该Controller提供的方法如解绑&lt;code&gt;貌似&lt;/code&gt;只能用form请求&lt;/p&gt;
&lt;p&gt;基于以上3个特点，其实我感觉在真实的项目里，为了灵活应对项目需求，我们完全可以模仿这个ConnectController来写一个适用于我们的项目的Controller类来处理绑定和解绑的业务&lt;/p&gt;
&lt;h3&gt;获取当前用户所有第三方账号的绑定状态&lt;/h3&gt;
&lt;h4&gt;当前用户所有第三方账号绑定状态绑定状态的处理视图&lt;/h4&gt;
&lt;p&gt;如上所说，其实ConnectController提供的&lt;code&gt;获取当前用户所有第三方账号绑定状态&lt;/code&gt;的方法已经为我们封装好了数据，我们只要写一个视图，将数据接收并返回给浏览器就好了，示例代码如下：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;@Component(&quot;connect/status&quot;)
@Slf4j
public class NrscConnectionStatusView extends AbstractView {

    @Autowired
    private ObjectMapper objectMapper;

    @Override
    protected void renderMergedOutputModel(Map&amp;lt;String, Object&amp;gt; map, HttpServletRequest request,
                                           HttpServletResponse response) throws Exception {
        //取出所有的Connection对象
        Map&amp;lt;String, List&amp;lt;Connection&amp;lt;?&amp;gt;&amp;gt;&amp;gt; connections = (Map&amp;lt;String, List&amp;lt;Connection&amp;lt;?&amp;gt;&amp;gt;&amp;gt;) map.get(&quot;connectionMap&quot;);
        //取出所有的providerId
        Set&amp;lt;String&amp;gt; providerIds = (Set&amp;lt;String&amp;gt;) map.get(&quot;providerIds&quot;);
        log.info(&quot;providerIds:{}&quot;,providerIds);

        //封装当前用户的第三方账号绑定状态 key为providerId ， value为true或false
        Map&amp;lt;String, Boolean&amp;gt; result = new HashMap&amp;lt;&amp;gt;();
        for (String key : connections.keySet()) {
            result.put(key, CollectionUtils.isNotEmpty(connections.get(key)));
        }
        //将当前用户的第三方账号绑定状态返回给浏览器
        response.setContentType(&quot;application/json;charset=UTF-8&quot;);
        response.getWriter().write(objectMapper.writeValueAsString(ResultVOUtil.success(result)));
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;测试：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;使用用户名密码登录&lt;/li&gt;
&lt;li&gt;访问：&amp;lt;http://localhost/connect&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;结果：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;{
&quot;wechat&quot;: false,
&quot;callback.do&quot;: false
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;将当前用户与第三方账号进行绑定&lt;/h3&gt;
&lt;p&gt;需要一个post请求,请求url为/connect/{providerId},以微信为例页面可按照如下方式发送请求&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;  &amp;lt;form action=&quot;/connect/weixin&quot; method=&quot;post&quot;&amp;gt;
      &amp;lt;button type=&quot;submit&quot;&amp;gt;绑定微信&amp;lt;/button&amp;gt;
  &amp;lt;/form&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;当ConnectController接收到该post请求后会拼接一个重定向到微信授权页面的url，并根据该url重定向到微信授权页面。用户在授权页面进行扫码授权后微信会利用url中的redirect_uri(回调地址)发送一个get请求回调我们的项目（接收这个回调的Controller也在ConnectController中,有兴趣的可以跟一下源码）—》 接着我们的项目会再去请求微信获取微信用户信息并将其封装成一个Connection对象 —》 然后将Connection对象和session中的用户信息进行关联，并将该关联关系存到userconnection表里（其实这一步已经完成了绑定） —》 接着调用一个视图，视图名为connect/+providerId+Connected（当然spingsocial没提供该视图）。&lt;/p&gt;
&lt;p&gt;注：&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;这里可能会发生参数错误，原因是构建认证URL参数调用的方法是：buildAuthorizeUrl  需要手动修改&lt;/p&gt;
&lt;/blockquote&gt;
&lt;pre&gt;&lt;code&gt;    /**
     * 后续绑定微信构建认证url 要使用的方法，
     * 和下面一样，需要重构
     * @param parameters  参数
     * @return 认证地址URL
     */
    @Override
    public String buildAuthorizeUrl(OAuth2Parameters parameters) {
        String authorizeUrl = super.buildAuthorizeUrl(parameters);
        log.info(&quot;【微信认证地址】 ={}&quot;,authorizeUrl);
        String replace = authorizeUrl.replace(&quot;client_id&quot;, &quot;appid&quot;);
        replace = replace + &quot;&amp;amp;scope=snsapi_userinfo#wechat_redirect&quot;;
        log.info(&quot;【修复后微信认证地址】 ={}&quot;,replace);
        return replace;
    }
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;&lt;strong&gt;下面提供一种各个第三方账号统一使用一个绑定和解绑逻辑的方法&lt;/strong&gt;&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;绑定成功和绑定失败的处理逻辑&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;public class SuccessFailView extends AbstractView {

    @Resource
    private ObjectMapper objectMapper;

    @Override
    protected void renderMergedOutputModel(Map&amp;lt;String, Object&amp;gt; map, HttpServletRequest request, HttpServletResponse response) throws Exception {
        response.setContentType(&quot;application/json;charset=UTF-8&quot;);
        //如果map（Model对象）里有Connection对象，则表示绑定，否则表示解绑
        if (map.get(&quot;connections&quot;) == null) {
            response.getWriter().write(objectMapper.writeValueAsString(ResponseEntity.ok().body(&quot;解绑成功&quot;)));
        } else {
            response.getWriter().write(objectMapper.writeValueAsString(ResponseEntity.ok().body(&quot;绑定成功&quot;)));
        }
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;视图配置&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;@Configuration
public class ViewConfig {

    /***
     * connect/wechatConnected 绑定成功的视图
     * connect/wechatConnect 解绑成功的视图
     *
     * 两个视图可以写在一起，通过判断Model对象里有没有Connection对象来确定究竟是解绑还是绑定
     */
    @Bean({&quot;connect/wechatConnected&quot;, &quot;connect/wechatConnected&quot;})
    //下面的注解的意思是当程序里有名字为weixinConnectedView的bean
    // 我写的默认的weixinConnectedView这个bean不会生效，也就是你可以写一个更好的bean来覆盖掉我的
    @ConditionalOnMissingBean(name = &quot;weixinConnectedView&quot;)
    public View weixinConnectedView() {
        return new SuccessFailView();
    }

}
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;测试成功&lt;/h3&gt;
&lt;p&gt;&lt;img src=&quot;%E7%99%BB%E5%BD%95%E5%90%8E%E7%BB%91%E5%AE%9A.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;插入表中的数据 userId是 用户名，怎么能自定义呢？&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;SocialConfiguration 类中，在调用这个方法&lt;/p&gt;
&lt;p&gt;this.userIdSource().getUserId() 这个属性可以在之前的，自己导入的 SocialWebAutoConfiguration中进行修改&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;    @Bean
    @Scope(
        value = &quot;request&quot;,
        proxyMode = ScopedProxyMode.INTERFACES
    )
    public ConnectionRepository connectionRepository(UsersConnectionRepository usersConnectionRepository) {
        return usersConnectionRepository.createConnectionRepository(this.userIdSource().getUserId());
    }
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;SocialWebAutoConfiguration  部分代码如下：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;public class SocialWebAutoConfiguration {
    public SocialWebAutoConfiguration() {
    }

    private static class SecurityContextUserIdSource implements UserIdSource {
        private SecurityContextUserIdSource() {
        }

        @Override
        public String getUserId() {
            SecurityContext context = SecurityContextHolder.getContext();
            Authentication authentication = context.getAuthentication();
            Assert.state(authentication != null, &quot;Unable to get a ConnectionRepository: no user signed in&quot;);
           //可以自定义
            return authentication.getName();
        }
    }
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;QQ绑定 — redirect uri is illegal(100010)错误分析&lt;/h3&gt;
&lt;p&gt;在进行完微信绑定之后，我试着写了QQ绑定的代码，发现点击QQ绑定时又出现了 redirect uri is illegal(100010)错误,追踪源代码发现在进行QQ绑定时拼接的跳向QQ授权页面的url如下：&lt;/p&gt;
&lt;p&gt;https://graph.qq.com/oauth2.0/authorize?client_id=100550231&amp;amp;response_type=code&amp;amp;redirect_uri=http%3A%2F%2Fwww.pinzhi365.com%2Fconnect%2Fcallback.do&amp;amp;state=f0a0f313-310a-4c31-8365-2e903740445c&lt;/p&gt;
&lt;p&gt;即redirect_uri解码后其实为 ：http://www.pinzhi365.com/connect/callback.do ，但是对于QQ来说，QQ互联上明确说了，其回调域名并不是简单的/www.pinzhi365.com而是http://www.pinzhi365.com/connect/callback.do 这样一整串 — 》 具体规则可以参考我的文章《springsocial/oauth2—第三方登陆之QQ登陆4【redirect uri is illegal(100010)错误解决方式】》，当然也可以直接参考QQ互联官网。&lt;/p&gt;
&lt;p&gt;但是这个项目在QQ上配置的redirect_uri为http://www.pinzhi365.com/qqLogin/callback.do,因此报redirect uri is illegal(100010)错误就很好理解了，其实大家可以试一下将QQ绑定拼接的url中的redirect_uri换成http://www.pinzhi365.com/qqLogin/callback.do是可以跳转到QQ授权页面的，但是这样QQ回调就无法回调到ConnectController里的方法了。&lt;/p&gt;
&lt;p&gt;那该怎么解决这个问题呢？我觉得有如下两种方法：&lt;/p&gt;
&lt;p&gt;自己徒手写代码实现绑定的逻辑
在QQ互联上多配置一个redirect_uri —》 下图是QQ互联上关于域名配置的介绍，可以看到redirect_uri 是可以配置多个的。&lt;/p&gt;
&lt;p&gt;到这里不知道大家会不会想那微信为啥没有这个问题呢？？？
其实很简单，因为微信中让用户指定的不是完整的回调地址，而是域名因此无论是下面这种地址，&lt;/p&gt;
&lt;p&gt;https://open.weixin.qq.com/connect/qrconnect?client_id=wxd99431bbff8305a0&amp;amp;response_type=code&amp;amp;redirect_uri=http%3A%2F%2Fwww.pinzhi365.com%2Fconnect%2Fweixin&amp;amp;state=ebb12821-f2bd-4903-b609-9403f429e2ac&amp;amp;appid=wxd99431bbff8305a0&amp;amp;scope=snsapi_login
还是
https://open.weixin.qq.com/connect/qrconnect?client_id=wxd99431bbff8305a0&amp;amp;response_type=code&amp;amp;redirect_uri=http%3A%2F%2Fwww.pinzhi365.com%2Fconnect1111111111111%2Fweixin&amp;amp;state=ebb12821-f2bd-4903-b609-9403f429e2ac&amp;amp;appid=wxd99431bbff8305a0&amp;amp;scope=snsapi_login
都是可以访问到微信的授权页面的。&lt;/p&gt;
&lt;h3&gt;解绑&lt;/h3&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;code&gt;绑貌似比较简单&lt;/code&gt;，只需要发送一个url为/connect/{provideId}的delete请求就可以了&lt;/p&gt;
&lt;/blockquote&gt;
&lt;pre&gt;&lt;code&gt; &amp;lt;form action=&quot;/connect/weixin&quot; method=&quot;post&quot;&amp;gt;
     &amp;lt;input id=&quot;method&quot; type=&quot;hidden&quot; name=&quot;_method&quot; value=&quot;delete&quot;/&amp;gt;
     &amp;lt;button&amp;gt;微信解绑&amp;lt;/button&amp;gt;
 &amp;lt;/form&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;session管理&lt;/h2&gt;
&lt;h3&gt;1、springboot项目session超时时间设置&lt;/h3&gt;
&lt;p&gt;springboot项目session超时时间设置很简单，只需要在yml或properties文件里进行配置一下就可以了，我现在用的&lt;code&gt;springboot的版本是2.1.7.RELEASE&lt;/code&gt;，在yml里配置如下&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;server:
  servlet:
    session:
      timeout: 600 # session超时时间为600秒
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;注意1：&lt;/code&gt;早一点的springboot版本如1.5.6.RELEASE的配置可能如下：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;server:
  session:
    timeout: 600 # session超时时间为600秒
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;注意2：如果设置的超时时间不满一分钟，将按一分钟来算，超过1分钟才按照你设置的超时时间来算。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;springboot的版本为2.1.7.RELEASE的可以参看TomcatServletWebServerFactory这个类里对session-timeout的控制&lt;/li&gt;
&lt;li&gt;springboot的版本为1.5.6.RELEASE的可以参看TomcatEmbeddedServletContainerFactory这个类&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;2、 springsecurity下如何通知用户session超时&lt;/h3&gt;
&lt;p&gt;在之前实现的代码里，是没对用户session超时做过特殊处理的，因此当session超时的时候会走之前写的用户认证失败的逻辑，即&lt;/p&gt;
&lt;p&gt;访问url里以.html结尾时重定向到登陆页
不以.html结尾时则返回一个json字符串
那如果是session超时的情况下，如何给用户一个特殊的提示，告诉他们是因为session超时引起的认证、校验失败呢？需要如下三步2.1 在配置文件&lt;/p&gt;
&lt;h4&gt;2.1、BrowserSecurityConfig里加上session超时跳向的url&lt;/h4&gt;
&lt;pre&gt;&lt;code&gt; .sessionManagement() //session超时管理
 .invalidSessionUrl(&quot;/session/invalid&quot;) //session超时跳向的url
&lt;/code&gt;&lt;/pre&gt;
&lt;h4&gt;2.2 在配置文件BrowserSecurityConfig里为指定session超时跳向的url授权(也就是该接口不走授权)&lt;/h4&gt;
&lt;h4&gt;2.3 写一个controller方法来处理session超时的提示逻辑&lt;/h4&gt;
&lt;pre&gt;&lt;code&gt;    /***
     * session超时的时候会请求该方法
     * @return
     */
    @GetMapping(&quot;/session/invalid&quot;)
    public ResultVO sessionInvalid(){
        return ResultVOUtil.error(ResultEnum.SESSION_INVALID.getCode(), ResultEnum.SESSION_INVALID.getMessage());
    }
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;3、关闭session管理&lt;/h2&gt;
&lt;p&gt;设置无状态模式&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS)
  
  @Override
  protected void configure(HttpSecurity http) throws Exception {

  http.
    csrf().disable()
    //.addFilterAt(new MyUsernamePasswordAuthenticationFilter(), UsernamePasswordAuthenticationFilter.class)
    .formLogin(form -&amp;gt; {
      form
        .loginProcessingUrl(&quot;/test/login/req&quot;)
        .successHandler(formLoginSuccessHandler)
        .failureHandler(formLoginFailHandler)
        ;
    })
    .sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS).and()
    .authorizeRequests()
    .antMatchers(
    &quot;/swagger-ui.html/**&quot;, &quot;/swagger-ui.html/**&quot;, &quot;/webjars/**&quot;, &quot;/swagger-resources/**&quot;, &quot;/v2/**&quot;, &quot;/csrf&quot;).permitAll()
    .anyRequest().authenticated();
  ;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;springsecurity中session的并发控制&lt;/h2&gt;
&lt;h3&gt;1 是什么？&lt;/h3&gt;
&lt;blockquote&gt;
&lt;p&gt;以腾讯视频会员为例，假如我的账号买了会员，则我可以享受看视频免广告，某些热门电视剧提前看等福利。若我的朋友也想享受这些福利又不想花钱买会员，则我可以让他用我的号登陆。但是假如我有很多很多的朋友都不想买会员，都想用我的号登陆，那腾讯肯定感觉自己就亏了，所以腾讯视频最大可同时登陆的设备是有数量限制的，超过这个限制，前面的账户就会被踢下来，这就是所谓的session并发控制。
springsecurity在解决该问题时有两种策略&lt;/p&gt;
&lt;p&gt;超过设置的最大session并发数量时，把之前的session失效掉，即踢掉前面的登陆
超过设置的最大session并发数量时，阻止后面的登陆&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h3&gt;2 、超过最大并发数量时，踢掉前面的登陆&lt;/h3&gt;
&lt;p&gt;在配置文件BrowserSecurityConfig里指定最大的并发数量&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;//session相关的控制
.sessionManagement()
  //指定session超时跳向的url
  .invalidSessionUrl(&quot;/session/invalid&quot;)
  //指定最大的session并发数量---即一个用户只能同时在一处登陆（腾讯视频的账号好像就只能同时允许2-3个手机同时登陆）
  .maximumSessions(1)
  //超过最大session并发数量时的策略
  .expiredSessionStrategy(new NRSCExpiredSessionStrategy())
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;超过最大session并发数量时的策略&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;package com.nrsc.security.browser.session;

import com.fasterxml.jackson.databind.ObjectMapper;
import com.nrsc.security.enums.ResultEnum;
import com.nrsc.security.utils.ResultVOUtil;
import com.nrsc.security.vo.ResultVO;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.web.session.SessionInformationExpiredEvent;
import org.springframework.security.web.session.SessionInformationExpiredStrategy;

import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import java.io.IOException;

/**
 * @author : Sun Chuan
 * @date : 2019/9/21 17:14
 * Description：
 * 如果设置的session并发策略为一个账户第二次登陆会将第一次给踢下来
 * 则第一次登陆的用户再访问我们的项目时会进入到该类
 * event里封装了request、response信息
 */
@Slf4j
public class NRSCExpiredSessionStrategy implements SessionInformationExpiredStrategy {

    @Override
    public void onExpiredSessionDetected(SessionInformationExpiredEvent event) throws IOException, ServletException {
        String header = event.getRequest().getHeader(&quot;user-agent&quot;);
        log.info(&quot;浏览器信息为：{}&quot;, header);

        //告诉前端并发登陆异常
        event.getResponse().setContentType(&quot;application/json;charset=UTF-8&quot;);
        event.getResponse().getWriter().write(&quot;并发登陆！！！&quot;);
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;3 超过最大并发数量时，阻止后面的登陆&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;//session相关的控制
.sessionManagement()
  //指定session超时跳向的url
  .invalidSessionUrl(&quot;/session/invalid&quot;)
  //指定最大的session并发数量---即一个用户只能同时在一处登陆（腾讯视频的账号好像就只能同时允许2-3个手机同时登陆）
  .maximumSessions(1)
  //当超过指定的最大session并发数量时，阻止后面的登陆（感觉貌似很少会用到这种策略）
  .maxSessionsPreventsLogin(true)
  //超过最大session并发数量时的策略
  .expiredSessionStrategy(new NRSCExpiredSessionStrategy())
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;springsecurity借助redis完成session集群管理&lt;/h2&gt;
&lt;h3&gt;1 spring-session+redis配置&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;首先需要在pom.xml里再引入如下两个依赖&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;  &amp;lt;dependency&amp;gt;
      &amp;lt;groupId&amp;gt;org.springframework.session&amp;lt;/groupId&amp;gt;
      &amp;lt;artifactId&amp;gt;spring-session-data-redis&amp;lt;/artifactId&amp;gt;
  &amp;lt;/dependency&amp;gt;

  &amp;lt;dependency&amp;gt;
      &amp;lt;groupId&amp;gt;org.springframework.boot&amp;lt;/groupId&amp;gt;
      &amp;lt;artifactId&amp;gt;spring-boot-starter-data-redis&amp;lt;/artifactId&amp;gt;
  &amp;lt;/dependency&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;其次需要在yml或properties文件里指定存储session信息的为redis、并配置redis的数据库信息&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;spring:
  ### 指定用什么存储session信息---可选项可参看源码StoreType枚举类
  session:
    store-type: redis
  ###本地环境下不配置也可以
##  redis:
##    host: 127.0.0.1
##    port: 6379
##    password: 123
##    database: 0
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;其实完成了以上配置，我们的项目就已经完成了借助redis进行session集群管理的开发。
但是这里必须要指出的一点是存放到redis里的对象，必须是要序列化的，即必须实现 Serializable 接口。&lt;/p&gt;
&lt;p&gt;再强调一次：存到redis里的对象必须是序列化的即实现了Serializable 接口 — 其属性如果是一个对象的话，也必须实现了Serializable 接口。&lt;/p&gt;
&lt;h3&gt;2 ImageCode&lt;code&gt;序列化&lt;/code&gt;报错问题&lt;/h3&gt;
&lt;p&gt;启动我们的项目，访问登陆页发现图形验证码显示不出来，后端报如下错误：
验证码显示不出的效果&lt;/p&gt;
&lt;p&gt;后端报错信息
org.springframework.data.redis.serializer.SerializationException: Cannot serialize; nested exception is org.springframework.core.serializer.support.SerializationFailedException: Failed to serialize object using DefaultSerializer; nested exception is java.lang.IllegalArgumentException: DefaultSerializer requires a Serializable payload but received an object of type [com.nrsc.security.core.validate.code.image.ImageCode]&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;原因(非常重要)&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;由于我们项目里ImageCode对象没有实现Serializable 接口，且该类里有一个属性 — BufferedImage对象，该对象也没有实现Serializable 接口，因此才会报出序列化错误&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;再次强调：存到redis里的对象必须是序列化的即实现了Serializable 接口 — 其属性如果是一个对象的话，也必须实现了Serializable 接口。&lt;/strong&gt;&lt;/p&gt;
&lt;h2&gt;spring-security退出登陆&lt;/h2&gt;
&lt;h3&gt;1 spring-security默认退出处理逻辑&lt;/h3&gt;
&lt;p&gt;Spring Security的退出登陆功能由LogoutFilter过滤器拦截处理，默认的退出登陆url为 /logout。&lt;/p&gt;
&lt;p&gt;在退出登陆时会做如下几件事：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;使当前session失效&lt;/li&gt;
&lt;li&gt;清除当前用户的remember-me记录&lt;/li&gt;
&lt;li&gt;清空当前的SecurityContext&lt;/li&gt;
&lt;li&gt;重定向到登录界面&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;2 自定义退出登陆的一些处理逻辑&lt;/h3&gt;
&lt;p&gt;在配置文件里自定义退出登陆的一些处理逻辑&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;.and()
   //退出登陆相关的逻辑
   .logout()
   //自定义退出的url---默认的为/logout
   .logoutUrl(&quot;/signOut&quot;)
   //自定义退出成功处理器
   .logoutSuccessHandler(logoutSuccessHandler)
   //自定义退出成功后跳转的url与logoutSuccessHandler互斥
   //.logoutSuccessUrl(&quot;/index&quot;)
   //指定退出成功后删除的cookie
   .deleteCookies(&quot;JSESSIONID&quot;)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;不要忘记对涉及到的URL进行授权&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;//配置不用进行认证校验的url
.antMatchers(
   SecurityConstants.DEFAULT_UNAUTHENTICATION_URL,
   SecurityConstants.DEFAULT_LOGIN_PROCESSING_URL_MOBILE,
   nrscSecurityProperties.getBrowser().getLoginPage(),
   SecurityConstants.DEFAULT_VALIDATE_CODE_URL_PREFIX + &quot;/*&quot;,
   nrscSecurityProperties.getBrowser().getSignUpUrl(),
   //session失效默认的跳转地址
   nrscSecurityProperties.getBrowser().getSession().getSessionInvalidUrl(),
   //获取第三方账号的用户信息的默认url
   SecurityConstants.DEFAULT_GET_SOCIAL_USERINFO_URL,
   //退出登陆默认跳转的url
   nrscSecurityProperties.getBrowser().getSignOutUrl(),
   &quot;/user/register&quot;,
   &quot;/js/**&quot;
)
.permitAll()

&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;自定义的退出成功处理器&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;package com.nrsc.security.browser.logout;

import com.fasterxml.jackson.databind.ObjectMapper;
import com.nrsc.security.utils.ResultVOUtil;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang.StringUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.security.core.Authentication;
import org.springframework.security.web.authentication.logout.LogoutSuccessHandler;

import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import javax.xml.transform.Result;
import java.io.IOException;

/**
 * @author : Sun Chuan
 * @date : 2019/9/22 12:00
 * Description：退出成功处理器
 */
@Slf4j
public class NRSCLogoutSuccessHandler implements LogoutSuccessHandler {
    /**
     * 退出登陆url
     *      可以在yml或properties文件里通过nrsc.security.browser.signOutUrl 进行指定
     *      我指定的默认值为&quot;/&quot; --- 因为如果不指定一个默认的url时，配置授权那一块会报错
     */
    private String signOutSuccessUrl;
    private ObjectMapper objectMapper = new ObjectMapper();
    public NRSCLogoutSuccessHandler(String signOutSuccessUrl) {
        this.signOutSuccessUrl = signOutSuccessUrl;
    }

    @Override
    public void onLogoutSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication)
            throws IOException, ServletException {

        log.info(&quot;退出成功&quot;);
        //如果没有指定退出成功的页面则返回前端一个json字符串
        if (StringUtils.equalsIgnoreCase(&quot;/&quot;,signOutSuccessUrl)) {
            response.setContentType(&quot;application/json;charset=UTF-8&quot;);
            response.getWriter().write(objectMapper.writeValueAsString(ResultVOUtil.success(&quot;退出成功&quot;)));
        } else {
            //重定向到退出成功登陆页面
            response.sendRedirect(signOutSuccessUrl);
        }
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;hr /&gt;
&lt;h2&gt;SpringCloud Security（下）&lt;/h2&gt;
&lt;h2&gt;&lt;a href=&quot;https://www.cnblogs.com/xiaoqi/p/spring-security.html&quot;&gt;Spring Security 架构与源码分析&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;Spring Security 主要实现了Authentication（认证，解决who are you? ） 和 Access Control（访问控制，也就是what are you allowed to do？，也称为Authorization）。Spring Security在架构上将认证与授权分离，并提供了扩展点。&lt;/p&gt;
&lt;h2&gt;核心对象&lt;/h2&gt;
&lt;p&gt;主要代码在&lt;code&gt;spring-security-core&lt;/code&gt;包下面。要了解Spring Security，需要先关注里面的核心对象。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;SecurityContextHolder&lt;/li&gt;
&lt;li&gt;SecurityContext&lt;/li&gt;
&lt;li&gt;Authentication&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;SecurityContextHolder&lt;/h3&gt;
&lt;p&gt;SecurityContextHolder 是 SecurityContext的存放容器，默认使用ThreadLocal 存储，意味SecurityContext在相同线程中的方法都可用。
SecurityContext主要是存储应用的principal信息，在Spring Security中用Authentication 来表示。&lt;/p&gt;
&lt;p&gt;获取principal：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;Object principal = SecurityContextHolder.getContext().getAuthentication().getPrincipal();

if (principal instanceof UserDetails) {
String username = ((UserDetails)principal).getUsername();
} else {
String username = principal.toString();
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Authentication&lt;/h3&gt;
&lt;p&gt;在Spring Security中，可以看一下Authentication定义：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;public interface Authentication extends Principal, Serializable {

    Collection&amp;lt;? extends GrantedAuthority&amp;gt; getAuthorities();

    /**
     * 通常是密码
     */
    Object getCredentials();

    /**
     * Stores additional details about the authentication request. These might be an IP
     * address, certificate serial number etc.
     */
    Object getDetails();

    /**
     * 用来标识是否已认证，如果使用用户名和密码登录,通常是用户名 
     */
    Object getPrincipal();

    /**
     * 是否已认证
     */
    boolean isAuthenticated();

    void setAuthenticated(boolean isAuthenticated) throws IllegalArgumentException;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Token&lt;/h3&gt;
&lt;p&gt;在实际应用中，通常使用&lt;code&gt;UsernamePasswordAuthenticationToken&lt;/code&gt;：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;//抽象token
public abstract class AbstractAuthenticationToken implements Authentication,
        CredentialsContainer {
        }
//UsernamePasswordAuthenticationToken
public class UsernamePasswordAuthenticationToken extends AbstractAuthenticationToken {
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;认证流程&lt;/h3&gt;
&lt;p&gt;一个常见的认证过程通常是这样的，创建一个UsernamePasswordAuthenticationToken，然后交给authenticationManager认证（后面详细说明），认证通过则通过SecurityContextHolder存放Authentication信息。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;UsernamePasswordAuthenticationToken authenticationToken =
            new UsernamePasswordAuthenticationToken(loginVM.getUsername(), loginVM.getPassword());

Authentication authentication = this.authenticationManager.authenticate(authenticationToken);
SecurityContextHolder.getContext().setAuthentication(authentication);
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;UserDetails与UserDetailsService&lt;/h3&gt;
&lt;p&gt;UserDetails 是Spring Security里的一个关键接口，他用来表示一个principal。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;public interface UserDetails extends Serializable {
    /**
     * 用户的授权信息，可以理解为角色
     */
    Collection&amp;lt;? extends GrantedAuthority&amp;gt; getAuthorities();

    /**
     * 用户密码
     *
     * @return the password
     */
    String getPassword();

    /**
     * 用户名 
     *   */
    String getUsername();

    boolean isAccountNonExpired();

    boolean isAccountNonLocked();

    boolean isCredentialsNonExpired();

    boolean isEnabled();
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;UserDetails提供了认证所需的必要信息，在实际使用里，可以自己实现UserDetails，并增加额外的信息，比如email、mobile等信息。&lt;/p&gt;
&lt;p&gt;在Authentication中的principal通常是用户名，我们可以通过UserDetailsService来通过principal获取UserDetails：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;public interface UserDetailsService {
    UserDetails loadUserByUsername(String username) throws UsernameNotFoundException;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;GrantedAuthority&lt;/h3&gt;
&lt;p&gt;在UserDetails里说了，GrantedAuthority可以理解为角色，例如 &lt;code&gt;ROLE_ADMINISTRATOR&lt;/code&gt; or &lt;code&gt;ROLE_HR_SUPERVISOR&lt;/code&gt;。&lt;/p&gt;
&lt;h3&gt;小结&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;SecurityContextHolder&lt;/code&gt;, 用来访问 &lt;code&gt;SecurityContext&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;SecurityContext&lt;/code&gt;, 用来存储&lt;code&gt;Authentication&lt;/code&gt; .&lt;/li&gt;
&lt;li&gt;&lt;code&gt;Authentication&lt;/code&gt;, 代表凭证.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;GrantedAuthority&lt;/code&gt;, 代表权限.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;UserDetails&lt;/code&gt;, 用户信息.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;UserDetailsService&lt;/code&gt;,获取用户信息.&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;Authentication认证&lt;/h2&gt;
&lt;h3&gt;AuthenticationManager&lt;/h3&gt;
&lt;p&gt;实现认证主要是通过AuthenticationManager接口，它只包含了一个方法：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;public interface AuthenticationManager {
  Authentication authenticate(Authentication authentication)
    throws AuthenticationException;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;authenticate()方法主要做三件事：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;如果验证通过，返回Authentication（通常带上authenticated=true）。&lt;/li&gt;
&lt;li&gt;认证失败抛出&lt;code&gt;AuthenticationException&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;如果无法确定，则返回null&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;&lt;code&gt;AuthenticationException&lt;/code&gt;是运行时异常,它通常由应用程序按通用方式处理，用户代码通常不用特意被捕获和处理这个异常。&lt;/p&gt;
&lt;p&gt;&lt;code&gt;AuthenticationManager&lt;/code&gt;的默认实现是&lt;code&gt;ProviderManager&lt;/code&gt;，它委托一组&lt;code&gt;AuthenticationProvider&lt;/code&gt;实例来实现认证。&lt;/p&gt;
&lt;p&gt;&lt;code&gt;AuthenticationProvider&lt;/code&gt;和&lt;code&gt;AuthenticationManager&lt;/code&gt;类似，都包含&lt;code&gt;authenticate&lt;/code&gt;，但它有一个额外的方法&lt;code&gt;supports&lt;/code&gt;，以允许查询调用方是否支持给定&lt;code&gt;Authentication&lt;/code&gt;类型：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;public interface AuthenticationProvider {

    Authentication authenticate(Authentication authentication)
            throws AuthenticationException;
    boolean supports(Class&amp;lt;?&amp;gt; authentication);
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;ProviderManager包含一组&lt;code&gt;AuthenticationProvider&lt;/code&gt;，执行authenticate时，遍历Providers，然后调用supports，如果支持，则执行遍历当前provider的authenticate方法，如果一个provider认证成功，则break。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;public Authentication authenticate(Authentication authentication)
            throws AuthenticationException {
        Class&amp;lt;? extends Authentication&amp;gt; toTest = authentication.getClass();
        AuthenticationException lastException = null;
        Authentication result = null;
        boolean debug = logger.isDebugEnabled();

        for (AuthenticationProvider provider : getProviders()) {
            if (!provider.supports(toTest)) {
                continue;
            }

            if (debug) {
                logger.debug(&quot;Authentication attempt using &quot;
                        + provider.getClass().getName());
            }

            try {
                result = provider.authenticate(authentication);

                if (result != null) {
                    copyDetails(authentication, result);
                    break;
                }
            }
            catch (AccountStatusException e) {
                prepareException(e, authentication);
                // SEC-546: Avoid polling additional providers if auth failure is due to
                // invalid account status
                throw e;
            }
            catch (InternalAuthenticationServiceException e) {
                prepareException(e, authentication);
                throw e;
            }
            catch (AuthenticationException e) {
                lastException = e;
            }
        }

        if (result == null &amp;amp;&amp;amp; parent != null) {
            // Allow the parent to try.
            try {
                result = parent.authenticate(authentication);
            }
            catch (ProviderNotFoundException e) {
                // ignore as we will throw below if no other exception occurred prior to
                // calling parent and the parent
                // may throw ProviderNotFound even though a provider in the child already
                // handled the request
            }
            catch (AuthenticationException e) {
                lastException = e;
            }
        }

        if (result != null) {
            if (eraseCredentialsAfterAuthentication
                    &amp;amp;&amp;amp; (result instanceof CredentialsContainer)) {
                // Authentication is complete. Remove credentials and other secret data
                // from authentication
                ((CredentialsContainer) result).eraseCredentials();
            }

            eventPublisher.publishAuthenticationSuccess(result);
            return result;
        }

        // Parent was null, or didn&apos;t authenticate (or throw an exception).

        if (lastException == null) {
            lastException = new ProviderNotFoundException(messages.getMessage(
                    &quot;ProviderManager.providerNotFound&quot;,
                    new Object[] { toTest.getName() },
                    &quot;No AuthenticationProvider found for {0}&quot;));
        }

        prepareException(lastException, authentication);

        throw lastException;
    }
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;从上面的代码可以看出， &lt;code&gt;ProviderManager&lt;/code&gt;有一个可选parent，如果parent不为空，则调用&lt;code&gt;parent.authenticate(authentication)&lt;/code&gt;&lt;/p&gt;
&lt;h3&gt;AuthenticationProvider&lt;/h3&gt;
&lt;p&gt;&lt;code&gt;AuthenticationProvider&lt;/code&gt;有多种实现，大家最关注的通常是&lt;code&gt;DaoAuthenticationProvider&lt;/code&gt;，继承于&lt;code&gt;AbstractUserDetailsAuthenticationProvider&lt;/code&gt;，核心是通过&lt;code&gt;UserDetails&lt;/code&gt;来实现认证,&lt;code&gt;DaoAuthenticationProvider&lt;/code&gt;默认会自动加载，不用手动配。&lt;/p&gt;
&lt;p&gt;先来看&lt;code&gt;AbstractUserDetailsAuthenticationProvider&lt;/code&gt;，看最核心的&lt;code&gt;authenticate&lt;/code&gt;：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;public Authentication authenticate(Authentication authentication)
            throws AuthenticationException {
        // 必须是UsernamePasswordAuthenticationToken
        Assert.isInstanceOf(UsernamePasswordAuthenticationToken.class, authentication,
                messages.getMessage(
                        &quot;AbstractUserDetailsAuthenticationProvider.onlySupports&quot;,
                        &quot;Only UsernamePasswordAuthenticationToken is supported&quot;));

        //  获取用户名
        String username = (authentication.getPrincipal() == null) ? &quot;NONE_PROVIDED&quot;
                : authentication.getName();

        boolean cacheWasUsed = true;
        // 从缓存获取
        UserDetails user = this.userCache.getUserFromCache(username);

        if (user == null) {
            cacheWasUsed = false;

            try {
               // retrieveUser 抽象方法，获取用户
                user = retrieveUser(username,
                        (UsernamePasswordAuthenticationToken) authentication);
            }
            catch (UsernameNotFoundException notFound) {
                logger.debug(&quot;User &apos;&quot; + username + &quot;&apos; not found&quot;);

                if (hideUserNotFoundExceptions) {
                    throw new BadCredentialsException(messages.getMessage(
                            &quot;AbstractUserDetailsAuthenticationProvider.badCredentials&quot;,
                            &quot;Bad credentials&quot;));
                }
                else {
                    throw notFound;
                }
            }
  
            Assert.notNull(user,
                    &quot;retrieveUser returned null - a violation of the interface contract&quot;);
        }

        try {
            // 预先检查，DefaultPreAuthenticationChecks，检查用户是否被lock或者账号是否可用
            preAuthenticationChecks.check(user);
            
            // 抽象方法，自定义检验
            additionalAuthenticationChecks(user,
                    (UsernamePasswordAuthenticationToken) authentication);
        }
        catch (AuthenticationException exception) {
            if (cacheWasUsed) {
                // There was a problem, so try again after checking
                // we&apos;re using latest data (i.e. not from the cache)
                cacheWasUsed = false;
                user = retrieveUser(username,
                        (UsernamePasswordAuthenticationToken) authentication);
                preAuthenticationChecks.check(user);
                additionalAuthenticationChecks(user,
                        (UsernamePasswordAuthenticationToken) authentication);
            }
            else {
                throw exception;
            }
        }
      
        // 后置检查 DefaultPostAuthenticationChecks，检查isCredentialsNonExpired
        postAuthenticationChecks.check(user);

        if (!cacheWasUsed) {
            this.userCache.putUserInCache(user);
        }

        Object principalToReturn = user;

        if (forcePrincipalAsString) {
            principalToReturn = user.getUsername();
        }
   
        return createSuccessAuthentication(principalToReturn, authentication, user);
    }
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;上面的检验主要基于UserDetails实现，其中获取用户和检验逻辑由具体的类去实现，默认实现是DaoAuthenticationProvider，这个类的核心是让开发者提供UserDetailsService来获取UserDetails以及 PasswordEncoder来检验密码是否有效：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;private UserDetailsService userDetailsService;
private PasswordEncoder passwordEncoder;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;看具体的实现，&lt;code&gt;retrieveUser&lt;/code&gt;,直接调用userDetailsService获取用户：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;protected final UserDetails retrieveUser(String username,
            UsernamePasswordAuthenticationToken authentication)
            throws AuthenticationException {
        UserDetails loadedUser;

        try {
            loadedUser = this.getUserDetailsService().loadUserByUsername(username);
        }
        catch (UsernameNotFoundException notFound) {
            if (authentication.getCredentials() != null) {
                String presentedPassword = authentication.getCredentials().toString();
                passwordEncoder.isPasswordValid(userNotFoundEncodedPassword,
                        presentedPassword, null);
            }
            throw notFound;
        }
        catch (Exception repositoryProblem) {
            throw new InternalAuthenticationServiceException(
                    repositoryProblem.getMessage(), repositoryProblem);
        }

        if (loadedUser == null) {
            throw new InternalAuthenticationServiceException(
                    &quot;UserDetailsService returned null, which is an interface contract violation&quot;);
        }
        return loadedUser;
    }
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;再来看验证：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;protected void additionalAuthenticationChecks(UserDetails userDetails,
            UsernamePasswordAuthenticationToken authentication)
            throws AuthenticationException {
        Object salt = null;

        if (this.saltSource != null) {
            salt = this.saltSource.getSalt(userDetails);
        }

        if (authentication.getCredentials() == null) {
            logger.debug(&quot;Authentication failed: no credentials provided&quot;);

            throw new BadCredentialsException(messages.getMessage(
                    &quot;AbstractUserDetailsAuthenticationProvider.badCredentials&quot;,
                    &quot;Bad credentials&quot;));
        }
        // 获取用户密码
        String presentedPassword = authentication.getCredentials().toString();
        // 比较passwordEncoder后的密码是否和userdetails的密码一致
        if (!passwordEncoder.isPasswordValid(userDetails.getPassword(),
                presentedPassword, salt)) {
            logger.debug(&quot;Authentication failed: password does not match stored value&quot;);

            throw new BadCredentialsException(messages.getMessage(
                    &quot;AbstractUserDetailsAuthenticationProvider.badCredentials&quot;,
                    &quot;Bad credentials&quot;));
        }
    }
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;小结：要自定义认证，使用DaoAuthenticationProvider，只需要为其提供PasswordEncoder和UserDetailsService就可以了。&lt;/p&gt;
&lt;h3&gt;定制 Authentication Managers&lt;/h3&gt;
&lt;p&gt;Spring Security提供了一个Builder类&lt;code&gt;AuthenticationManagerBuilder&lt;/code&gt;，借助它可以快速实现自定义认证。&lt;/p&gt;
&lt;p&gt;看官方源码说明：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;SecurityBuilder used to create an AuthenticationManager . Allows for easily building in memory authentication, LDAP authentication, JDBC based authentication, adding UserDetailsService , and adding AuthenticationProvider&apos;s.
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;AuthenticationManagerBuilder可以用来Build一个AuthenticationManager，可以创建基于内存的认证、LDAP认证、 JDBC认证，以及添加UserDetailsService和AuthenticationProvider。&lt;/p&gt;
&lt;p&gt;简单使用：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;@Configuration
@EnableWebSecurity
@EnableGlobalMethodSecurity(prePostEnabled = true, securedEnabled = true)
public class ApplicationSecurity extends WebSecurityConfigurerAdapter {


  public SecurityConfiguration(AuthenticationManagerBuilder authenticationManagerBuilder, UserDetailsService userDetailsService,TokenProvider tokenProvider,CorsFilter corsFilter, SecurityProblemSupport problemSupport) {
        this.authenticationManagerBuilder = authenticationManagerBuilder;
        this.userDetailsService = userDetailsService;
        this.tokenProvider = tokenProvider;
        this.corsFilter = corsFilter;
        this.problemSupport = problemSupport;
    }

    @PostConstruct
    public void init() {
        try {
            authenticationManagerBuilder
                .userDetailsService(userDetailsService)
                .passwordEncoder(passwordEncoder());
        } catch (Exception e) {
            throw new BeanInitializationException(&quot;Security configuration failed&quot;, e);
        }
    }

   @Override
    protected void configure(HttpSecurity http) throws Exception {
        http
            .addFilterBefore(corsFilter, UsernamePasswordAuthenticationFilter.class)
            .exceptionHandling()
            .authenticationEntryPoint(problemSupport)
            .accessDeniedHandler(problemSupport)
        .and()
            .csrf()
            .disable()
            .headers()
            .frameOptions()
            .disable()
        .and()
            .sessionManagement()
            .sessionCreationPolicy(SessionCreationPolicy.STATELESS)
        .and()
            .authorizeRequests()
            .antMatchers(&quot;/api/register&quot;).permitAll()
            .antMatchers(&quot;/api/activate&quot;).permitAll()
            .antMatchers(&quot;/api/authenticate&quot;).permitAll()
            .antMatchers(&quot;/api/account/reset-password/init&quot;).permitAll()
            .antMatchers(&quot;/api/account/reset-password/finish&quot;).permitAll()
            .antMatchers(&quot;/api/profile-info&quot;).permitAll()
            .antMatchers(&quot;/api/**&quot;).authenticated()
            .antMatchers(&quot;/management/health&quot;).permitAll()
            .antMatchers(&quot;/management/**&quot;).hasAuthority(AuthoritiesConstants.ADMIN)
            .antMatchers(&quot;/v2/api-docs/**&quot;).permitAll()
            .antMatchers(&quot;/swagger-resources/configuration/ui&quot;).permitAll()
            .antMatchers(&quot;/swagger-ui/index.html&quot;).hasAuthority(AuthoritiesConstants.ADMIN)
        .and()
            .apply(securityConfigurerAdapter());

    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;授权与访问控制&lt;/h3&gt;
&lt;p&gt;一旦认证成功，我们可以继续进行授权，授权是通过&lt;code&gt;AccessDecisionManager&lt;/code&gt;来实现的。框架有三种实现，默认是AffirmativeBased，通过&lt;code&gt;AccessDecisionVoter&lt;/code&gt;决策，有点像&lt;code&gt;ProviderManager&lt;/code&gt;委托给&lt;code&gt;AuthenticationProviders&lt;/code&gt;来认证。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;public void decide(Authentication authentication, Object object,
            Collection&amp;lt;ConfigAttribute&amp;gt; configAttributes) throws AccessDeniedException {
        int deny = 0;
        // 遍历DecisionVoter 
        for (AccessDecisionVoter voter : getDecisionVoters()) {
            // 投票
            int result = voter.vote(authentication, object, configAttributes);

            if (logger.isDebugEnabled()) {
                logger.debug(&quot;Voter: &quot; + voter + &quot;, returned: &quot; + result);
            }

            switch (result) {
            case AccessDecisionVoter.ACCESS_GRANTED:
                return;

            case AccessDecisionVoter.ACCESS_DENIED:
                deny++;

                break;

            default:
                break;
            }
        }
       
        // 一票否决
        if (deny &amp;gt; 0) {
            throw new AccessDeniedException(messages.getMessage(
                    &quot;AbstractAccessDecisionManager.accessDenied&quot;, &quot;Access is denied&quot;));
        }

        // To get this far, every AccessDecisionVoter abstained
        checkAllowIfAllAbstainDecisions();
    }
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;来看AccessDecisionVoter：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;boolean supports(ConfigAttribute attribute);

boolean supports(Class&amp;lt;?&amp;gt; clazz);

int vote(Authentication authentication, S object,
        Collection&amp;lt;ConfigAttribute&amp;gt; attributes);
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;object是用户要访问的资源，ConfigAttribute则是访问object要满足的条件，通常payload是字符串，比如ROLE_ADMIN 。所以我们来看下RoleVoter的实现，其核心就是从authentication提取出GrantedAuthority，然后和ConfigAttribute比较是否满足条件。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;public boolean supports(ConfigAttribute attribute) {
        if ((attribute.getAttribute() != null)
                &amp;amp;&amp;amp; attribute.getAttribute().startsWith(getRolePrefix())) {
            return true;
        }
        else {
            return false;
        }
    }
    
public boolean supports(Class&amp;lt;?&amp;gt; clazz) {
        return true;
    }


public int vote(Authentication authentication, Object object,
            Collection&amp;lt;ConfigAttribute&amp;gt; attributes) {
        if(authentication == null) {
            return ACCESS_DENIED;
        }
        int result = ACCESS_ABSTAIN;
        
        // 获取GrantedAuthority信息
        Collection&amp;lt;? extends GrantedAuthority&amp;gt; authorities = extractAuthorities(authentication);

        for (ConfigAttribute attribute : attributes) {
            if (this.supports(attribute)) {
                // 默认拒绝访问
                result = ACCESS_DENIED;

                // Attempt to find a matching granted authority
                for (GrantedAuthority authority : authorities) {
                     // 判断是否有匹配的 authority
                    if (attribute.getAttribute().equals(authority.getAuthority())) {
                        // 可访问
                        return ACCESS_GRANTED;
                    }
                }
            }
        }

        return result;
    }
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这里要疑问，ConfigAttribute哪来的？其实就是上面ApplicationSecurity的configure里的。&lt;/p&gt;
&lt;h3&gt;web security 如何实现&lt;/h3&gt;
&lt;p&gt;Web层中的Spring Security（用于UI和HTTP后端）基于Servlet &lt;code&gt;Filters&lt;/code&gt;，下图显示了单个HTTP请求的处理程序的典型分层。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://tangtangit.com/_astro/filters.CAJ88l4-_ZJ6Dg5.webp&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;Spring Security通过&lt;code&gt;FilterChainProxy&lt;/code&gt;作为单一的Filter注册到web层，Proxy内部的Filter。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://tangtangit.com/_astro/security-filters.Cu4kmCzn_ZCWvfM.webp&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;FilterChainProxy相当于一个filter的容器，通过VirtualFilterChain来依次调用各个内部filter&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;
public void doFilter(ServletRequest request, ServletResponse response,
            FilterChain chain) throws IOException, ServletException {
        boolean clearContext = request.getAttribute(FILTER_APPLIED) == null;
        if (clearContext) {
            try {
                request.setAttribute(FILTER_APPLIED, Boolean.TRUE);
                doFilterInternal(request, response, chain);
            }
            finally {
                SecurityContextHolder.clearContext();
                request.removeAttribute(FILTER_APPLIED);
            }
        }
        else {
            doFilterInternal(request, response, chain);
        }
    }

    private void doFilterInternal(ServletRequest request, ServletResponse response,
            FilterChain chain) throws IOException, ServletException {

        FirewalledRequest fwRequest = firewall
                .getFirewalledRequest((HttpServletRequest) request);
        HttpServletResponse fwResponse = firewall
                .getFirewalledResponse((HttpServletResponse) response);

        List&amp;lt;Filter&amp;gt; filters = getFilters(fwRequest);

        if (filters ** null || filters.size() ** 0) {
            if (logger.isDebugEnabled()) {
                logger.debug(UrlUtils.buildRequestUrl(fwRequest)
                        + (filters == null ? &quot; has no matching filters&quot;
                                : &quot; has an empty filter list&quot;));
            }

            fwRequest.reset();

            chain.doFilter(fwRequest, fwResponse);

            return;
        }

        VirtualFilterChain vfc = new VirtualFilterChain(fwRequest, chain, filters);
        vfc.doFilter(fwRequest, fwResponse);
    }
    
    private static class VirtualFilterChain implements FilterChain {
        private final FilterChain originalChain;
        private final List&amp;lt;Filter&amp;gt; additionalFilters;
        private final FirewalledRequest firewalledRequest;
        private final int size;
        private int currentPosition = 0;

        private VirtualFilterChain(FirewalledRequest firewalledRequest,
                FilterChain chain, List&amp;lt;Filter&amp;gt; additionalFilters) {
            this.originalChain = chain;
            this.additionalFilters = additionalFilters;
            this.size = additionalFilters.size();
            this.firewalledRequest = firewalledRequest;
        }

        public void doFilter(ServletRequest request, ServletResponse response)
                throws IOException, ServletException {
            if (currentPosition == size) {
                if (logger.isDebugEnabled()) {
                    logger.debug(UrlUtils.buildRequestUrl(firewalledRequest)
                            + &quot; reached end of additional filter chain; proceeding with original chain&quot;);
                }

                // Deactivate path stripping as we exit the security filter chain
                this.firewalledRequest.reset();

                originalChain.doFilter(request, response);
            }
            else {
                currentPosition++;

                Filter nextFilter = additionalFilters.get(currentPosition - 1);

                if (logger.isDebugEnabled()) {
                    logger.debug(UrlUtils.buildRequestUrl(firewalledRequest)
                            + &quot; at position &quot; + currentPosition + &quot; of &quot; + size
                            + &quot; in additional filter chain; firing Filter: &apos;&quot;
                            + nextFilter.getClass().getSimpleName() + &quot;&apos;&quot;);
                }

                nextFilter.doFilter(request, response, this);
            }
        }
    }
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;&lt;a href=&quot;https://www.cnblogs.com/xiaoqi/p/spring-security-rabc.html&quot;&gt;spring security实现动态配置url权限的两种方法&lt;/a&gt;&lt;/h2&gt;
&lt;blockquote&gt;
&lt;p&gt;标准的RABC, 权限需要支持动态配置，spring security默认是在代码里约定好权限，真实的业务场景通常需要可以支持动态配置角色访问权限，即在运行时去配置url对应的访问角色。&lt;/p&gt;
&lt;p&gt;基于spring security，如何实现这个需求呢？&lt;/p&gt;
&lt;p&gt;最简单的方法就是自定义一个Filter去完成权限判断，但这脱离了spring security框架，如何基于spring security优雅的实现呢？&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h3&gt;spring security 授权回顾&lt;/h3&gt;
&lt;p&gt;spring security 通过FilterChainProxy作为注册到web的filter，FilterChainProxy里面一次包含了内置的多个过滤器，我们首先需要了解spring security内置的各种filter：&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Alias&lt;/th&gt;
&lt;th&gt;Filter Class&lt;/th&gt;
&lt;th&gt;Namespace Element or Attribute&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;CHANNEL_FILTER&lt;/td&gt;
&lt;td&gt;ChannelProcessingFilter&lt;/td&gt;
&lt;td&gt;http/intercept-url@requires-channel&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;SECURITY_CONTEXT_FILTER&lt;/td&gt;
&lt;td&gt;SecurityContextPersistenceFilter&lt;/td&gt;
&lt;td&gt;http&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;CONCURRENT_SESSION_FILTER&lt;/td&gt;
&lt;td&gt;ConcurrentSessionFilter&lt;/td&gt;
&lt;td&gt;session-management/concurrency-control&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;HEADERS_FILTER&lt;/td&gt;
&lt;td&gt;HeaderWriterFilter&lt;/td&gt;
&lt;td&gt;http/headers&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;CSRF_FILTER&lt;/td&gt;
&lt;td&gt;CsrfFilter&lt;/td&gt;
&lt;td&gt;http/csrf&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;LOGOUT_FILTER&lt;/td&gt;
&lt;td&gt;LogoutFilter&lt;/td&gt;
&lt;td&gt;http/logout&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;X509_FILTER&lt;/td&gt;
&lt;td&gt;X509AuthenticationFilter&lt;/td&gt;
&lt;td&gt;http/x509&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;PRE_AUTH_FILTER&lt;/td&gt;
&lt;td&gt;AbstractPreAuthenticatedProcessingFilter Subclasses&lt;/td&gt;
&lt;td&gt;N/A&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;CAS_FILTER&lt;/td&gt;
&lt;td&gt;CasAuthenticationFilter&lt;/td&gt;
&lt;td&gt;N/A&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;FORM_LOGIN_FILTER&lt;/td&gt;
&lt;td&gt;UsernamePasswordAuthenticationFilter&lt;/td&gt;
&lt;td&gt;http/form-login&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;BASIC_AUTH_FILTER&lt;/td&gt;
&lt;td&gt;BasicAuthenticationFilter&lt;/td&gt;
&lt;td&gt;http/http-basic&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;SERVLET_API_SUPPORT_FILTER&lt;/td&gt;
&lt;td&gt;SecurityContextHolderAwareRequestFilter&lt;/td&gt;
&lt;td&gt;http/@servlet-api-provision&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;JAAS_API_SUPPORT_FILTER&lt;/td&gt;
&lt;td&gt;JaasApiIntegrationFilter&lt;/td&gt;
&lt;td&gt;http/@jaas-api-provision&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;REMEMBER_ME_FILTER&lt;/td&gt;
&lt;td&gt;RememberMeAuthenticationFilter&lt;/td&gt;
&lt;td&gt;http/remember-me&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;ANONYMOUS_FILTER&lt;/td&gt;
&lt;td&gt;AnonymousAuthenticationFilter&lt;/td&gt;
&lt;td&gt;http/anonymous     匿名认证过滤器&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;SESSION_MANAGEMENT_FILTER&lt;/td&gt;
&lt;td&gt;SessionManagementFilter&lt;/td&gt;
&lt;td&gt;session-management&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;EXCEPTION_TRANSLATION_FILTER&lt;/td&gt;
&lt;td&gt;ExceptionTranslationFilter&lt;/td&gt;
&lt;td&gt;http&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;FILTER_SECURITY_INTERCEPTOR&lt;/td&gt;
&lt;td&gt;FilterSecurityInterceptor&lt;/td&gt;
&lt;td&gt;http    安全拦截器&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;SWITCH_USER_FILTER&lt;/td&gt;
&lt;td&gt;SwitchUserFilter&lt;/td&gt;
&lt;td&gt;N/A&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;最重要的是&lt;code&gt;FilterSecurityInterceptor&lt;/code&gt;，该过滤器实现了主要的鉴权逻辑，最核心的代码在这里：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;protected InterceptorStatusToken beforeInvocation(Object object) {
    
        // 获取访问URL所需权限   默认从config中获取
        Collection&amp;lt;ConfigAttribute&amp;gt; attributes = this.obtainSecurityMetadataSource()
                .getAttributes(object);

    
        Authentication authenticated = authenticateIfRequired();

        // 通过accessDecisionManager鉴权,投票选出通过还是不通过
        try {
            this.accessDecisionManager.decide(authenticated, object, attributes);
        }
        catch (AccessDeniedException accessDeniedException) {
           //认证失败，异常捕获
            publishEvent(new AuthorizationFailureEvent(object, attributes, authenticated,
                    accessDeniedException));

            throw accessDeniedException;
        }

        if (debug) {
            logger.debug(&quot;Authorization successful&quot;);
        }

        if (publishAuthorizationSuccess) {
            publishEvent(new AuthorizedEvent(object, attributes, authenticated));
        }

        // Attempt to run as a different user
        Authentication runAs = this.runAsManager.buildRunAs(authenticated, object,
                attributes);

        if (runAs == null) {
            if (debug) {
                logger.debug(&quot;RunAsManager did not change Authentication object&quot;);
            }

            // no further work post-invocation
            return new InterceptorStatusToken(SecurityContextHolder.getContext(), false,
                    attributes, object);
        }
        else {
            if (debug) {
                logger.debug(&quot;Switching to RunAs Authentication: &quot; + runAs);
            }

            SecurityContext origCtx = SecurityContextHolder.getContext();
            SecurityContextHolder.setContext(SecurityContextHolder.createEmptyContext());
            SecurityContextHolder.getContext().setAuthentication(runAs);

            // need to revert to token.Authenticated post-invocation
            return new InterceptorStatusToken(origCtx, true, attributes, object);
        }
    }
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;从上面可以看出，要实现动态鉴权，可以从两方面着手：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;自定义SecurityMetadataSource，实现从数据库加载ConfigAttribute&lt;/li&gt;
&lt;li&gt;另外就是可以自定义accessDecisionManager，官方的UnanimousBased其实足够使用，并且他是基于AccessDecisionVoter来实现权限认证的，因此我们只需要自定义一个AccessDecisionVoter就可以了&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;下面来看分别如何实现。&lt;/p&gt;
&lt;h3&gt;自定义AccessDecisionManager&lt;/h3&gt;
&lt;p&gt;官方的三个AccessDecisionManager都是基于AccessDecisionVoter来实现权限认证的，因此我们只需要自定义一个AccessDecisionVoter就可以了。&lt;/p&gt;
&lt;p&gt;自定义主要是实现&lt;code&gt;AccessDecisionVoter&lt;/code&gt;接口，我们可以仿照官方的RoleVoter实现一个：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;public class RoleBasedVoter implements AccessDecisionVoter&amp;lt;Object&amp;gt; {

    @Override
    public boolean supports(ConfigAttribute attribute) {
        return true;
    }

    @Override
    public int vote(Authentication authentication, Object object, Collection&amp;lt;ConfigAttribute&amp;gt; attributes) {
        if(authentication == null) {
            return ACCESS_DENIED;
        }
        int result = ACCESS_ABSTAIN;
        Collection&amp;lt;? extends GrantedAuthority&amp;gt; authorities = extractAuthorities(authentication);

        for (ConfigAttribute attribute : attributes) {
            if(attribute.getAttribute()==null){
                continue;
            }
            if (this.supports(attribute)) {
                result = ACCESS_DENIED;

                // Attempt to find a matching granted authority
                for (GrantedAuthority authority : authorities) {
                    if (attribute.getAttribute().equals(authority.getAuthority())) {
                        return ACCESS_GRANTED;
                    }
                }
            }
        }

        return result;
    }

    Collection&amp;lt;? extends GrantedAuthority&amp;gt; extractAuthorities(
        Authentication authentication) {
        return authentication.getAuthorities();
    }

    @Override
    public boolean supports(Class clazz) {
        return true;
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;如何加入动态权限呢？&lt;/p&gt;
&lt;p&gt;&lt;code&gt;vote(Authentication authentication, Object object, Collection&amp;lt;ConfigAttribute&amp;gt; attributes)&lt;/code&gt;里的&lt;code&gt;Object object&lt;/code&gt;的类型是&lt;code&gt;FilterInvocation&lt;/code&gt;，可以通过&lt;code&gt;getRequestUrl&lt;/code&gt;获取当前请求的URL:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt; FilterInvocation fi = (FilterInvocation) object;
  String url = fi.getRequestUrl();
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;因此这里扩展空间就大了，可以从DB动态加载，然后判断URL的ConfigAttribute就可以了。&lt;/p&gt;
&lt;p&gt;如何使用这个RoleBasedVoter呢？在configure里使用accessDecisionManager方法自定义，我们还是使用官方的&lt;code&gt;UnanimousBased&lt;/code&gt;，然后将自定义的RoleBasedVoter加入即可。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;@EnableWebSecurity
@EnableGlobalMethodSecurity(prePostEnabled = true, securedEnabled = true)
public class SecurityConfiguration extends WebSecurityConfigurerAdapter {

 
    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http
            .addFilterBefore(corsFilter, UsernamePasswordAuthenticationFilter.class)
            .exceptionHandling()
            .authenticationEntryPoint(problemSupport)
            .accessDeniedHandler(problemSupport)
        .and()
            .csrf()
            .disable()
            .headers()
            .frameOptions()
            .disable()
        .and()
            .sessionManagement()
            .sessionCreationPolicy(SessionCreationPolicy.STATELESS)
        .and()
            .authorizeRequests()
            // 自定义accessDecisionManager
            .accessDecisionManager(accessDecisionManager())
          
        .and()
            .apply(securityConfigurerAdapter());

    }


    @Bean
    public AccessDecisionManager accessDecisionManager() {
        List&amp;lt;AccessDecisionVoter&amp;lt;? extends Object&amp;gt;&amp;gt; decisionVoters
            = Arrays.asList(
            new WebExpressionVoter(),
            // new RoleVoter(),
            new RoleBasedVoter(),
            new AuthenticatedVoter());
        return new UnanimousBased(decisionVoters);
    }
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;自定义SecurityMetadataSource&lt;/h3&gt;
&lt;p&gt;自定义FilterInvocationSecurityMetadataSource只要实现接口即可，在接口里从DB动态加载规则。&lt;/p&gt;
&lt;p&gt;为了复用代码里的定义，我们可以将代码里生成的SecurityMetadataSource带上，在构造函数里传入默认的FilterInvocationSecurityMetadataSource。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;public class AppFilterInvocationSecurityMetadataSource implements org.springframework.security.web.access.intercept.FilterInvocationSecurityMetadataSource {

    private FilterInvocationSecurityMetadataSource  superMetadataSource;

    @Override
    public Collection&amp;lt;ConfigAttribute&amp;gt; getAllConfigAttributes() {
        return null;
    }

    public AppFilterInvocationSecurityMetadataSource(FilterInvocationSecurityMetadataSource expressionBasedFilterInvocationSecurityMetadataSource){
         this.superMetadataSource = expressionBasedFilterInvocationSecurityMetadataSource;

         // TODO 从数据库加载权限配置
    }

    private final AntPathMatcher antPathMatcher = new AntPathMatcher();
    
    // 这里的需要从DB加载
    private final Map&amp;lt;String,String&amp;gt; urlRoleMap = new HashMap&amp;lt;String,String&amp;gt;(){{
        put(&quot;/open/**&quot;,&quot;ROLE_ANONYMOUS&quot;);
        put(&quot;/health&quot;,&quot;ROLE_ANONYMOUS&quot;);
        put(&quot;/restart&quot;,&quot;ROLE_ADMIN&quot;);
        put(&quot;/demo&quot;,&quot;ROLE_USER&quot;);
    }};

    @Override
    public Collection&amp;lt;ConfigAttribute&amp;gt; getAttributes(Object object) throws IllegalArgumentException {
        FilterInvocation fi = (FilterInvocation) object;
        String url = fi.getRequestUrl();

        for(Map.Entry&amp;lt;String,String&amp;gt; entry:urlRoleMap.entrySet()){
            if(antPathMatcher.match(entry.getKey(),url)){
                return SecurityConfig.createList(entry.getValue());
            }
        }

        //  返回代码定义的默认配置
        return superMetadataSource.getAttributes(object);
    }



    @Override
    public boolean supports(Class&amp;lt;?&amp;gt; clazz) {
        return FilterInvocation.class.isAssignableFrom(clazz);
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;怎么使用？和&lt;code&gt;accessDecisionManager&lt;/code&gt;不一样，&lt;code&gt;ExpressionUrlAuthorizationConfigurer&lt;/code&gt; 并没有提供set方法设置&lt;code&gt;FilterSecurityInterceptor&lt;/code&gt;的&lt;code&gt;FilterInvocationSecurityMetadataSource&lt;/code&gt;，how to do?&lt;/p&gt;
&lt;p&gt;发现一个扩展方法&lt;code&gt;withObjectPostProcessor&lt;/code&gt;，通过该方法自定义一个处理&lt;code&gt;FilterSecurityInterceptor&lt;/code&gt;类型的&lt;code&gt;ObjectPostProcessor&lt;/code&gt;就可以修改&lt;code&gt;FilterSecurityInterceptor&lt;/code&gt;。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;@EnableWebSecurity
@EnableGlobalMethodSecurity(prePostEnabled = true, securedEnabled = true)
public class SecurityConfiguration extends WebSecurityConfigurerAdapter {

 
    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http
            .addFilterBefore(corsFilter, UsernamePasswordAuthenticationFilter.class)
            .exceptionHandling()
            .authenticationEntryPoint(problemSupport)
            .accessDeniedHandler(problemSupport)
        .and()
            .csrf()
            .disable()
            .headers()
            .frameOptions()
            .disable()
        .and()
            .sessionManagement()
            .sessionCreationPolicy(SessionCreationPolicy.STATELESS)
        .and()
            .authorizeRequests()
            // 自定义FilterInvocationSecurityMetadataSource
            .withObjectPostProcessor(new ObjectPostProcessor&amp;lt;FilterSecurityInterceptor&amp;gt;() {
                @Override
                public &amp;lt;O extends FilterSecurityInterceptor&amp;gt; O postProcess(
                    O fsi) {
                    fsi.setSecurityMetadataSource(mySecurityMetadataSource(fsi.getSecurityMetadataSource()));
                    return fsi;
                }
            })
        .and()
            .apply(securityConfigurerAdapter());

    }


    @Bean
    public AppFilterInvocationSecurityMetadataSource mySecurityMetadataSource(FilterInvocationSecurityMetadataSource filterInvocationSecurityMetadataSource) {
        AppFilterInvocationSecurityMetadataSource securityMetadataSource = new AppFilterInvocationSecurityMetadataSource(filterInvocationSecurityMetadataSource);
        return securityMetadataSource;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;本文介绍了两种基于spring security实现动态权限的方法，一是自定义accessDecisionManager，二是自定义FilterInvocationSecurityMetadataSource。实际项目里可以根据需要灵活选择&lt;/p&gt;
&lt;h2&gt;消化security 框架源码--重点&lt;/h2&gt;
&lt;h3&gt;一、认证流程&lt;/h3&gt;
&lt;h4&gt;1、基于表单登陆认证所要经过的类&lt;/h4&gt;
&lt;p&gt;&lt;img src=&quot;%E8%A1%A8%E5%8D%95%E7%99%BB%E5%BD%95%E8%A6%81%E7%BB%8F%E8%BF%87%E7%9A%84%E6%B5%81%E7%A8%8B.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;下面将对各个类进行解析&lt;/p&gt;
&lt;h4&gt;1.1 SecurityContextPersistenceFilter&lt;/h4&gt;
&lt;p&gt;请求进来时，检查session，如果有SecurityContext(已认证用户对象的一个封装类)拿出来放到线程里即SecurityContextHolder中，返回时校验SecurityContextHolder中是否有securityContext，有则放入session，从而实现认证信息在多个请求中共享。&lt;/p&gt;
&lt;h4&gt;1.2 AbstractAuthenticationProcessingFilter★★★----模版模式&lt;/h4&gt;
&lt;p&gt;我觉得AbstractAuthenticationProcessingFilter是理解表单表单登陆认证原理最重要的一个类
其部分源码和注释如下:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;	public void doFilter(ServletRequest req, ServletResponse res, FilterChain chain)
			throws IOException, ServletException {
		HttpServletRequest request = (HttpServletRequest) req;
		HttpServletResponse response = (HttpServletResponse) res;
		if (!requiresAuthentication(request, response)) {//如果不需要认证直接放行
			chain.doFilter(request, response);
			return;
		}
		if (logger.isDebugEnabled()) {logger.debug(&quot;Request is to process authentication&quot;);}
		Authentication authResult;
		try {//走到这里说明用户没有进行登陆认证,而下面的方法就是去尝试进行登陆认证
			authResult = attemptAuthentication(request, response);
			if (authResult == null) {return;}
			sessionStrategy.onAuthentication(authResult, request, response);
		}
		catch (InternalAuthenticationServiceException failed) {//认证失败走unsuccessfulAuthentication
			logger.error(&quot;An internal error occurred while trying to authenticate the user.&quot;,failed);
			unsuccessfulAuthentication(request, response, failed);
			return;
		}
		catch (AuthenticationException failed) {//认证失败走unsuccessfulAuthentication
			unsuccessfulAuthentication(request, response, failed);
			return;
		}
		// Authentication success--&amp;gt;应该是如果走到这,发现不需要再走successfulAuthentication方法了也直接放行
		if (continueChainBeforeSuccessfulAuthentication) {chain.doFilter(request, response);}
		//认证成功后需要做的事
		successfulAuthentication(request, response, chain, authResult);
	}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;我们再看一下如果认证成功后具体干了什么事 ,successfulAuthentication源码如下:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;	protected void successfulAuthentication(HttpServletRequest request,
			HttpServletResponse response, FilterChain chain, Authentication authResult)
			throws IOException, ServletException {
		if (logger.isDebugEnabled()) {
			logger.debug(&quot;Authentication success. Updating SecurityContextHolder to contain: &quot;
					+ authResult);
		}
		//将认证后的结果放入到SecurityContextHolder中的SecurityContext中
		SecurityContextHolder.getContext().setAuthentication(authResult);
		//与记住我功能相关认证方式------之后的博客中应该会有涉及
		rememberMeServices.loginSuccess(request, response, authResult);
		// Fire event----还没具体研究是干什么的
		if (this.eventPublisher != null) {
			eventPublisher.publishEvent(new InteractiveAuthenticationSuccessEvent(
					authResult, this.getClass()));
		}
		//执行登陆成功后的逻辑-----即实现了AuthenticationSuccessHandler接口的类中定义的逻辑
		successHandler.onAuthenticationSuccess(request, response, authResult);
	}
&lt;/code&gt;&lt;/pre&gt;
&lt;h4&gt;1.2小结：&lt;/h4&gt;
&lt;p&gt;&lt;strong&gt;通过上面的解读可以知道AbstractAuthenticationProcessingFilter这个类定义了认证的整个整体框架,但是具体的认证过程、认证成功后的行为和认证失败后的行为需要其他类进行具体实现，该类的伪代码如下&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;abstractAuthenticationProcessingFilter%E4%BC%AA%E4%BB%A3%E7%A0%81.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;h4&gt;1.3 UsernamePasswordAuthenticationFilter算不上一个真正的Filter&lt;/h4&gt;
&lt;p&gt;按照上述分析，要想进行登陆认证，必然会有一个类继承AbstractAuthenticationProcessingFilter类，并实现其attemptAuthentication抽象方法。而UsernamePasswordAuthenticationFilter正是用户名、密码登录认证过程中的这个类，因此实际上UsernamePasswordAuthenticationFilter并不是一个Filter,它只是继承了AbstractAuthenticationProcessingFilter并实现了其attemptAuthentication抽象方法。
UsernamePasswordAuthenticationFilter关键源代码：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;	public Authentication attemptAuthentication(HttpServletRequest request,
			HttpServletResponse response) throws AuthenticationException {
		if (postOnly &amp;amp;&amp;amp; !request.getMethod().equals(&quot;POST&quot;)) {
			throw new AuthenticationServiceException(&quot;Authentication method not supported: &quot;
			 + request.getMethod());
		}
		String username = obtainUsername(request);
		String password = obtainPassword(request);
		if (username == null) {
			username = &quot;&quot;;
		}
		if (password == null) {
			password = &quot;&quot;;
		}
		username = username.trim();
		//利用用户名和密码组装成一个未校验的token
		UsernamePasswordAuthenticationToken authRequest = new UsernamePasswordAuthenticationToken(
				username, password);
		// Allow subclasses to set the &quot;details&quot; property
		setDetails(request, authRequest);
		//拿着用户名和密码组成的token进行校验，注意返回值为Authentication
		//同时注意AuthenticationManager来自父类AbstractAuthenticationProcessingFilter
		return this.getAuthenticationManager().authenticate(authRequest);
	}

&lt;/code&gt;&lt;/pre&gt;
&lt;h4&gt;1.3 小结&lt;/h4&gt;
&lt;p&gt;UsernamePasswordAuthenticationFilter很简单，主要做了三件事：&lt;/p&gt;
&lt;p&gt;继承AbstractAuthenticationProcessingFilter，并实现attemptAuthentication方法，准备进行认证工作；
利用用户名和密码组装成一个未校验的token
利用父类提供的AuthenticationManager进行实际地认证工作生成未认证的token的源代码,这里需要注意的是返回值UsernamePasswordAuthenticationToken实际上是一个Authentication，它继承了AbstractAuthenticationToken，而AbstractAuthenticationToken实现了Authentication接口&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;	public UsernamePasswordAuthenticationToken(Object principal, Object credentials) {
		super(null);
		this.principal = principal;
		this.credentials = credentials;
		setAuthenticated(false);
	}
&lt;/code&gt;&lt;/pre&gt;
&lt;h4&gt;1.4 AuthenticationManager、ProviderManager和AuthenticationProvider&lt;/h4&gt;
&lt;p&gt;其实AuthenticationManager为一个接口，它里面只有一个抽象方法authenticate；&lt;/p&gt;
&lt;p&gt;ProviderManager为AuthenticationManager的一个实现类，它实现了AuthenticationManager的authenticate方法，但是它并不是直接去进行登录认证，而是去循环所有的AuthenticationProvider;&lt;/p&gt;
&lt;p&gt;那AuthenticationProvider又是干什么的呢？其实它也是一个接口，里面包含两个抽象方法&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;public interface AuthenticationProvider {
	//登陆认证
	Authentication authenticate(Authentication authentication) throws AuthenticationException;
	//判断传入的token是否为支持的认证类型
	boolean supports(Class&amp;lt;?&amp;gt; authentication);
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h4&gt;1.5 AbstractUserDetailsAuthenticationProvider和DaoAuthenticationProvider—模板模式&lt;/h4&gt;
&lt;p&gt;AbstractUserDetailsAuthenticationProvider类为用户名和密码方式登陆最后选择出的进行登陆校验的实际执行类，主要的校验逻辑如下(我对源代码进行了删减)：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;	public Authentication authenticate(Authentication authentication)
			throws AuthenticationException {
			try {
				//获取用户信息
				user = retrieveUser(username,(UsernamePasswordAuthenticationToken) authentication);
			}catch (UsernameNotFoundException notFound) {}
		try {
			//预检查---四个权限中的三个是否为false
			preAuthenticationChecks.check(user);
			//用户名密码是否正确
			additionalAuthenticationChecks(user,
					(UsernamePasswordAuthenticationToken) authentication);
		}}catch (UsernameNotFoundException notFound) {}
		//后检查---四个权限中的最后一个是否为false
		postAuthenticationChecks.check(user);
		//都校验通过后创建一个校验成功后的Authentication对象
		return createSuccessAuthentication(principalToReturn, authentication, user);
	}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;检验成功的Authentication创建源码：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;	protected Authentication createSuccessAuthentication(Object principal,
			Authentication authentication, UserDetails user) {
		UsernamePasswordAuthenticationToken result = new UsernamePasswordAuthenticationToken(
				principal, authentication.getCredentials(),
				authoritiesMapper.mapAuthorities(user.getAuthorities()));
		result.setDetails(authentication.getDetails());
		return result;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;其实AbstractUserDetailsAuthenticationProvider的设计很像AbstractAuthenticationProcessingFilter，它里面也定义了一个主要的框架，但retrieveUser、additionalAuthenticationChecks方法的具体实现都在DaoAuthenticationProvider中，当然AbstractUserDetailsAuthenticationProvider里面还有一些方法的实现在其他类中。&lt;/p&gt;
&lt;h4&gt;1.5 UserDetailsService和NRSCDetailsService&lt;/h4&gt;
&lt;p&gt;首先来看一下DaoAuthenticationProvider中关于retrieveUser方法的具体实现：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;		try {
			//调用UserDetailsService的loadUserByUsername方法获取用户详情信息
			UserDetails loadedUser = this.getUserDetailsService().loadUserByUsername(username);
			if (loadedUser == null) {
				throw new InternalAuthenticationServiceException(
						&quot;UserDetailsService returned null, which is an interface contract violation&quot;);
			}
			return loadedUser;
		}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;由此我们可以定位到UserDetailsService其实是一个接口，它里面只有一个方法loadUserByUsername，当我们实现了该接口时spring-security就会调用我们实现的接口，进行用户的信息校验，这在前面的文章-spring-security入门2—自定义用户认证逻辑部分进行过讲解。&lt;/p&gt;
&lt;h4&gt;2、总结&lt;/h4&gt;
&lt;p&gt;上面的内容已经对表单登陆认证的源码进行了比较全面的剖析，为了能更好地与前面几篇文章串起来，我将文章开篇展示的图片进行了如下标注，以求能更好的串联最近所学知识点。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;%E8%AE%A4%E8%AF%81%E5%89%96%E6%9E%90%E5%9B%BE.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;h3&gt;二、授权流程&lt;/h3&gt;
&lt;h4&gt;1、流程示意图&lt;/h4&gt;
&lt;p&gt;&lt;img src=&quot;%E6%8E%88%E6%9D%83%E6%B5%81%E7%A8%8B%E5%9B%BE%E6%84%8F.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;接下来看一下授权管理在spring security整个架构中所处的位置:&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;%E6%8E%88%E6%9D%83%E5%9C%A8security%E6%A1%86%E6%9E%B6%E4%B8%AD%E4%BD%8D%E7%BD%AE.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;h4&gt;2 、权限表达式&lt;/h4&gt;
&lt;h4&gt;2.1 什么是权限表达式?&lt;/h4&gt;
&lt;p&gt;首先来看一下,下面的配置:&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;%E6%9D%83%E9%99%90%E8%A1%A8%E8%BE%BE%E5%BC%8F.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;上面的配置是我们之前代码中的配置,图中第一个适配器 — antMatchers因为调用了permitAll()方法—&amp;gt;访问它所包含的那些url就不需要认证+授权; 第二个适配器因为调用了hasRole()方法 —&amp;gt; 只有有ADMIN权限的用户才能访问它包含的url.那其具体实现原理是什么呢?跟踪spring security源码时可以很容易发现其秘密,原来如果某个适配器(antMatchers)调用了permitAll()方法,则将该适配器包含的所有URL的权限指定为一个字符串permitAll;如果某个适配器(antMatchers)调用了hasRole(“AAA”)方法,则将该适配器包含的所有URL的权限指定为一个字符串hasRole(ROLE_AAA);如果某个适配器(anyRequest也是一个适配器,表示所有请求)调用了authenticated()方法,则将该适配器包含的所有URL的权限指定为一个字符串authenticated. 等等等 然后spring security会将项目里所有的url与这些权限字符串放到一个大Map里 —&amp;gt; Map的key为服务器中的各个url,value为permitAll、hasRole(ROLE_AAA)、authenticated等权限字符串的集合.
如下图所示:&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;%E8%A7%92%E8%89%B2%E5%AD%97%E7%AC%A6%E4%B8%B2.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;当服务器接收到一个请求后,spring security会拿着当前请求去遍历上面所说得url为key,权限字符串集合为value的Map —&amp;gt; 以此找到当前访问URL所需要的用户权限.&lt;/p&gt;
&lt;p&gt;一言以蔽之,权限表达式就是一个个标识URL所需访问权限的字符串如permitAll、hasRole(ROLE_AAA)、authenticated等,我们可以在适配器(antMatchers/anyRequest)后调用相应的方法为某个或某些URL指定相应的权限字符串.&lt;/p&gt;
&lt;h4&gt;2.2 权限表达式总结&lt;/h4&gt;
&lt;p&gt;除了permitAll、hasRole(ROLE_AAA)、authenticated三个权限表达式(权限字符串)之外,其实还有好多,总结如下:&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;%E6%9D%83%E9%99%90%E5%AD%97%E7%AC%A6%E4%B8%B2.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;&lt;code&gt;注意:&lt;/code&gt; hasRole()方法生成权限表达式的源码如下:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt; private static String hasRole(String role) {
     Assert.notNull(role, &quot;role cannot be null&quot;);
     if (role.startsWith(&quot;ROLE_&quot;)) {
         throw new IllegalArgumentException(&quot;role should not start with &apos;ROLE_&apos; since it is automatically inserted. Got &apos;&quot; + role + &quot;&apos;&quot;);
     } else {
         return &quot;hasRole(&apos;ROLE_&quot; + role + &quot;&apos;)&quot;;
     }
 }
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;因此如果指定了一个URL的访问权限为hasRole(“ADMIN”)时,该用户应该配置的权限为ROLE_ADMIN&lt;/p&gt;
&lt;p&gt;如果使用hasAuthority和hasAnyAuthority就会不加ROLE_ 前缀，直接使用即可。&lt;/p&gt;
&lt;h4&gt;2.3 如何联合使用权限表达式&lt;/h4&gt;
&lt;pre&gt;&lt;code&gt;.antMatchers(&quot;/user/*&quot;).access(&quot;hasRole(&apos;ROLE_ADMIN&apos;) and hasIpAddress(&apos;127.0.0.1&apos;)&quot;)
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;剥离权限表达式&lt;/h2&gt;
&lt;p&gt;&lt;img src=&quot;%E5%89%A5%E7%A6%BB%E6%9D%83%E9%99%90%E6%9E%B6%E6%9E%84%E5%9B%BE.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;AuthorizeConfigManager  统一管理所有 AuthorizeConfigProvider&lt;/li&gt;
&lt;li&gt;AuthorizeConfigProvider 针对每个模块都有自己的实现&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;AuthorizeConfigProvider&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;/**
 * &amp;lt;p&amp;gt;DESC: 授权Provider&amp;lt;/p&amp;gt;
 * &amp;lt;p&amp;gt;DATE: 2019-11-15 13:31&amp;lt;/p&amp;gt;
 * &amp;lt;p&amp;gt;VERSION:2.0.0&amp;lt;/p&amp;gt;
 * &amp;lt;p&amp;gt;@AUTHOR: ZhengYong&amp;lt;/p&amp;gt;
 */
public interface AuthorizeConfigProvider {

    /**
     * 配置
     * @param registry 实则对应 WebSecurityConfigurerAdapter  configure 中的authorizeRequests()返回对象
     */
    void config(ExpressionUrlAuthorizationConfigurer&amp;lt;HttpSecurity&amp;gt;.ExpressionInterceptUrlRegistry registry);
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;provider 实现类&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;/**
 * &amp;lt;p&amp;gt;DESC: provider实现&amp;lt;/p&amp;gt;
 * &amp;lt;p&amp;gt;DATE: 2019-11-15 13:34&amp;lt;/p&amp;gt;
 * &amp;lt;p&amp;gt;VERSION:2.0.0&amp;lt;/p&amp;gt;
 * &amp;lt;p&amp;gt;@AUTHOR: ZhengYong&amp;lt;/p&amp;gt;
 */
@Component
public class MyAuthorizeConfigProvider implements AuthorizeConfigProvider {

    @Resource
    private MySecurityProperties properties;

    @Override
    public void config(ExpressionUrlAuthorizationConfigurer&amp;lt;HttpSecurity&amp;gt;.ExpressionInterceptUrlRegistry registry) {
            registry.antMatchers(
                    properties.getBrowser().getDefaultLoginHtml(),
                    properties.getBrowser().getLoginPage(),
                    properties.getBrowser().getLoginProcessingUrl(),
                    properties.getBrowser().getImageCode(),
                    properties.getBrowser().getSmsCodeValid(),
                    properties.getBrowser().getSignUpUrl()
            ).permitAll();
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;AuthorizeConfigManager&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;/**
 * &amp;lt;p&amp;gt;DESC: provider管理&amp;lt;/p&amp;gt;
 * &amp;lt;p&amp;gt;DATE: 2019-11-15 13:37&amp;lt;/p&amp;gt;
 * &amp;lt;p&amp;gt;VERSION:2.0.0&amp;lt;/p&amp;gt;
 * &amp;lt;p&amp;gt;@AUTHOR: ZhengYong&amp;lt;/p&amp;gt;
 */
public interface AuthorizeConfigManager {

    /**
     * 配置
     * @param config 实则对应 WebSecurityConfigurerAdapter  configure 中的authorizeRequests()返回对象
     */
    void config(ExpressionUrlAuthorizationConfigurer&amp;lt;HttpSecurity&amp;gt;.ExpressionInterceptUrlRegistry config);
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;manager实现&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;/**
 * &amp;lt;p&amp;gt;DESC: &amp;lt;/p&amp;gt;
 * &amp;lt;p&amp;gt;DATE: 2019-11-15 13:37&amp;lt;/p&amp;gt;
 * &amp;lt;p&amp;gt;VERSION:2.0.0&amp;lt;/p&amp;gt;
 * &amp;lt;p&amp;gt;@AUTHOR: ZhengYong&amp;lt;/p&amp;gt;
 */
@Component
public class MyAuthorizeConfigManager implements AuthorizeConfigManager {

    @Resource
    private Set&amp;lt;AuthorizeConfigProvider&amp;gt; providers;

    @Override
    public void config(ExpressionUrlAuthorizationConfigurer&amp;lt;HttpSecurity&amp;gt;.ExpressionInterceptUrlRegistry config) {
        providers.forEach( provider -&amp;gt;{
                //遍历我们所有的Provider，并配置         
                provider.config(config);
        });
       //除了Provider以外的都需要权限
        config.anyRequest().authenticated();
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;使我们的配置生效：&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;	@Component
	public class BrowserSecurityConfig extends WebSecurityConfigurerAdapter  //配置类中
    //注入
    @Resource
    private AuthorizeConfigManager authorizeConfigManager;

    @Override
    protected void configure(HttpSecurity http) throws Exception {
      		 http.............;
              //传入请求
              authorizeConfigManager.config(http.authorizeRequests());
    }
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;对接我们自己RBAC和spring security&lt;/h2&gt;
&lt;h3&gt;前置知识;&lt;/h3&gt;
&lt;blockquote&gt;
&lt;pre&gt;&lt;code&gt;.anyRequest().access(&quot;@rbacService.hasPermission(request,authentication)&quot;);
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;access中可以写入权限表达式，也可以指定方法（自定义校验）。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;官方文档如下：&lt;/p&gt;
&lt;h5&gt;Referring to Beans in Web Security Expressions&lt;/h5&gt;
&lt;p&gt;If you wish to extend the expressions that are available, you can easily refer to any Spring Bean you expose. For example, assuming you have a Bean with the name of &lt;code&gt;webSecurity&lt;/code&gt; that contains the following method signature:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;public class WebSecurity {
        public boolean check(Authentication authentication, HttpServletRequest request) {
                ...
        }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;You could refer to the method using:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;http
        .authorizeRequests()
                .antMatchers(&quot;/user/**&quot;).access(&quot;@webSecurity.check(authentication,request)&quot;)
                ...
&lt;/code&gt;&lt;/pre&gt;
&lt;h5&gt;Path Variables in Web Security Expressions&lt;/h5&gt;
&lt;p&gt;At times it is nice to be able to refer to path variables within a URL. For example, consider a RESTful application that looks up a user by id from the URL path in the format &lt;code&gt;/user/{userId}&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;You can easily refer to the path variable by placing it in the pattern. For example, if you had a Bean with the name of &lt;code&gt;webSecurity&lt;/code&gt; that contains the following method signature:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;public class WebSecurity {
        public boolean checkUserId(Authentication authentication, int id) {
                ...
        }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;You could refer to the method using:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;http
        .authorizeRequests()
                .antMatchers(&quot;/user/{userId}/**&quot;).access(&quot;@webSecurity.checkUserId(authentication,#userId)&quot;)
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;实际使用：&lt;/h3&gt;
&lt;p&gt;自定义接口，判断是否有权限&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;/**
 * &amp;lt;p&amp;gt;DESC: 判断接口是否通过&amp;lt;/p&amp;gt;
 * &amp;lt;p&amp;gt;DATE: 2019-11-15 14:15&amp;lt;/p&amp;gt;
 * &amp;lt;p&amp;gt;VERSION:2.0.0&amp;lt;/p&amp;gt;
 * &amp;lt;p&amp;gt;@AUTHOR: ZhengYong&amp;lt;/p&amp;gt;
 */
public interface RbacService {

    boolean hasPermission(HttpServletRequest request, Authentication authentication);
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;校验权限实现类&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;/**
 * &amp;lt;p&amp;gt;DESC: &amp;lt;/p&amp;gt;
 * &amp;lt;p&amp;gt;DATE: 2019-11-15 14:16&amp;lt;/p&amp;gt;
 * &amp;lt;p&amp;gt;VERSION:2.0.0&amp;lt;/p&amp;gt;
 * &amp;lt;p&amp;gt;@AUTHOR: ZhengYong&amp;lt;/p&amp;gt;
 */
@Component(value = &quot;rbacService&quot;)  
public class RbacServiceImpl implements RbacService {

    private AntPathMatcher antPathMatcher = new AntPathMatcher();

    @Override
    public boolean hasPermission(HttpServletRequest request, Authentication authentication) {
        String requestUrl = request.getRequestURI();
        Object principal = authentication.getPrincipal();
        AtomicBoolean hasPermission = new AtomicBoolean(false);
        //用户信息才做判断
        if(principal instanceof UserDetails){
            String username = ((UserDetails) principal).getUsername();
            //查询数据库，获取用户信息，用户角色，用户可以访问的权限
            //这里假设是set
            Set&amp;lt;String&amp;gt; set = new HashSet&amp;lt;&amp;gt;();
            set.forEach(e -&amp;gt;{
                if(antPathMatcher.match(e,requestUrl)){
                    hasPermission.set(true);
                }
            });
        }
        return hasPermission.get();
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;自定义校验逻辑加入security&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;/**
 * &amp;lt;p&amp;gt;DESC: &amp;lt;/p&amp;gt;
 * &amp;lt;p&amp;gt;DATE: 2019-11-15 13:52&amp;lt;/p&amp;gt;
 * &amp;lt;p&amp;gt;VERSION:2.0.0&amp;lt;/p&amp;gt;
 * &amp;lt;p&amp;gt;@AUTHOR: ZhengYong&amp;lt;/p&amp;gt;
 */
@Component
@Order(2)
public class DomeProviderImpl implements AuthorizeConfigProvider {

    @Override
    public void config(ExpressionUrlAuthorizationConfigurer&amp;lt;HttpSecurity&amp;gt;.ExpressionInterceptUrlRegistry registry) {
        registry
                .antMatchers(&quot;/test&quot;).hasAuthority(&quot;admin&quot;)
                .antMatchers(
                        &quot;/user/info&quot;,
                        &quot;/user/register&quot;,
                        &quot;/auth/wechat&quot;
                ).permitAll()
                //扩展校验请求  @ + 实例名 + 方法名（参数，会自动注入不用管）
                .anyRequest().access(&quot;@rbacService.hasPermission(request,authentication)&quot;);
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;问题一：&lt;/p&gt;
&lt;p&gt;.anyRequest() 只能出现一次，将前面的.anyRequest()注释掉&lt;/p&gt;
&lt;p&gt;问题二：&lt;/p&gt;
&lt;p&gt;.antMatchers 加载问题，.anyRequest() 必须要最后加入。&lt;/p&gt;
&lt;p&gt;也就是说Provider 存在加载问题，需要我们将 Provider 上加上注解@Order(2)   加载顺序改变一下。&lt;/p&gt;
&lt;p&gt;注意：&lt;/p&gt;
&lt;p&gt;manager中的存放Provider的集合，一定要用list有序集合，不可用set；&lt;/p&gt;
&lt;h2&gt;自定义无权访问处理器&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;http
.authorizeRequests()
.anyRequest().authenticated()
.and()
//无权限用户提示字符串消息设置
.exceptionHandling()
// getAccessDeniedHandler()是上文的方法
.accessDeniedHandler(getAccessDeniedHandler())
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;点进去看源码，我们只需要实现AccessDeniedHandler ，并加入到配置中即可。&lt;/p&gt;
&lt;h3&gt;实际操作&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;/**
 * &amp;lt;p&amp;gt;DESC: 访问拒绝处理器&amp;lt;/p&amp;gt;
 * &amp;lt;p&amp;gt;DATE: 2019-11-15 15:38&amp;lt;/p&amp;gt;
 * &amp;lt;p&amp;gt;VERSION:2.0.0&amp;lt;/p&amp;gt;
 * &amp;lt;p&amp;gt;@AUTHOR: ZhengYong&amp;lt;/p&amp;gt;
 */
@Component
public class MyAccessDeniedHandler implements AccessDeniedHandler {
    
    @Override
    public void handle(HttpServletRequest request, HttpServletResponse response, AccessDeniedException e) throws IOException, ServletException {
        response.setContentType(MediaType.APPLICATION_JSON_UTF8_VALUE);
        response.getWriter().write(&quot;{无权访问资源}&quot;);
        System.out.println(e);
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这里我配置在剥离的Provider中&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;/**
 * &amp;lt;p&amp;gt;DESC: &amp;lt;/p&amp;gt;
 * &amp;lt;p&amp;gt;DATE: 2019-11-15 13:52&amp;lt;/p&amp;gt;
 * &amp;lt;p&amp;gt;VERSION:2.0.0&amp;lt;/p&amp;gt;
 * &amp;lt;p&amp;gt;@AUTHOR: ZhengYong&amp;lt;/p&amp;gt;
 */
@Component
@Order(2)
public class DomeProviderImpl implements AuthorizeConfigProvider {

    @Resource
    private MyAccessDeniedHandler myAccessDeniedHandler;

    @Override
    public void config(ExpressionUrlAuthorizationConfigurer&amp;lt;HttpSecurity&amp;gt;.ExpressionInterceptUrlRegistry registry) {
        try {
            registry
                    .antMatchers(&quot;/test&quot;).hasAuthority(&quot;admin&quot;)
                    .antMatchers(
                            &quot;/user/info&quot;,
                            &quot;/user/register&quot;,
                            &quot;/auth/wechat&quot;
                    ).permitAll()
                    //扩展校验请求
                    .anyRequest().access(&quot;@rbacService.hasPermission(request,authentication)&quot;)
              //我在在这里----------添加拒绝访问处理器
                    .and().exceptionHandling().accessDeniedHandler(myAccessDeniedHandler);
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;security auth2 源码分析&lt;/h2&gt;
&lt;h3&gt;1 明确目标 + 获取token核心流程梳理&lt;/h3&gt;
&lt;blockquote&gt;
&lt;p&gt;Spring Security OAuth的原理大致如下图左半部分，而我们的目标就是将Spring Security OAuth默认的走授权流程后发放token的步骤替换成走我们自定义的认证方式后发放token，除此之外，其他的认证校验步骤（如访问资源服务器的认证、授权等）仍然可以使用Spring Security OAuth提供的默认实现来完成。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;&lt;img src=&quot;security_auth2%E5%AE%9E%E7%8E%B0.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;为了达到这个目的，我们肯定要对Spring Security OAuth发放token的源码进行剖析。下面是其主要流程：&lt;/p&gt;
&lt;p&gt;(绿色的是类，蓝色的是接口)&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;token%E6%BA%90%E7%A0%81.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;h3&gt;2 源码解读&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;获取token的整体流程 — &lt;strong&gt;所在类TokenEndpoint&lt;/strong&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;	@RequestMapping(value = &quot;/oauth/token&quot;, method=RequestMethod.POST)
	public ResponseEntity&amp;lt;OAuth2AccessToken&amp;gt; postAccessToken(Principal principal, @RequestParam
	Map&amp;lt;String, String&amp;gt; parameters) throws HttpRequestMethodNotSupportedException {

		if (!(principal instanceof Authentication)) {
			throw new InsufficientAuthenticationException(
					&quot;There is no client authentication. Try adding an appropriate authentication filter.&quot;);
		}
		//从请求头中获取到clientId
		String clientId = getClientId(principal);
		//根据clientId获取ClientDetails
		ClientDetails authenticatedClient = getClientDetailsService().loadClientByClientId(clientId);
		//将获取的第三方应用的信息和grant_type、scope等参数封装成一个TokenRequest对象---该对象其实是new 出来的
		TokenRequest tokenRequest = getOAuth2RequestFactory().createTokenRequest(parameters, authenticatedClient);

		if (clientId != null &amp;amp;&amp;amp; !clientId.equals(&quot;&quot;)) {
			// Only validate the client details if a client authenticated during this
			// request. --- 英文注释的很详细，这里不再多解释
			if (!clientId.equals(tokenRequest.getClientId())) {
				// double check to make sure that the client ID in the token request is the same as that in the
				// authenticated client
				throw new InvalidClientException(&quot;Given client ID does not match authenticated client&quot;);
			}
		}
		//校验请求中的scope是否合法
		if (authenticatedClient != null) {
			oAuth2RequestValidator.validateScope(tokenRequest, authenticatedClient);
		}
		//没传哪种授权模式 ---&amp;gt; 直接抛异常
		if (!StringUtils.hasText(tokenRequest.getGrantType())) {
			throw new InvalidRequestException(&quot;Missing grant type&quot;);
		}
		//简化模式在用户请求授权时就获取到令牌了，不会进到该方法里，所以直接抛异常
		if (tokenRequest.getGrantType().equals(&quot;implicit&quot;)) {
			throw new InvalidGrantException(&quot;Implicit grant type not supported from token endpoint&quot;);
		}
		//是否为授权码模式的请求
		if (isAuthCodeRequest(parameters)) {
			// The scope was requested or determined during the authorization step
			//授权码模式在获取授权码的过程中需要传递scope参数，那个参数就是用来获取授权的，
			//也就是说授权码模式请求的授权是在获取授权码的过程中就已经定了---因为此步是用户真正在授权
			//在获取token的过程中只能使用获取授权码时得到的权限
			//因此这里的scope置空，稍后通过申请到的授权码获取其在授权过程中所请求的权限
			if (!tokenRequest.getScope().isEmpty()) {
				logger.debug(&quot;Clearing scope of incoming token request&quot;);
				tokenRequest.setScope(Collections.&amp;lt;String&amp;gt; emptySet());
			}
		}
		//如果是刷新令牌的请求 --- 重新设置其scope
		if (isRefreshTokenRequest(parameters)) {
			// A refresh token has its own default scopes, so we should ignore any added by the factory here.
			tokenRequest.setScope(OAuth2Utils.parseParameterList(parameters.get(OAuth2Utils.SCOPE)));
		}
		//真正生成token的步骤
		OAuth2AccessToken token = getTokenGranter().grant(tokenRequest.getGrantType(), tokenRequest);
		if (token == null) {
			throw new UnsupportedGrantTypeException(&quot;Unsupported grant type: &quot; + tokenRequest.getGrantType());
		}
		//将生成的token返回
		return getResponse(token);

	}
&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;TokenRequest 的生成 — 很简单，其实是通过ClientDetails 和其他请求参数new出来的&lt;/p&gt;
&lt;p&gt;-— &lt;strong&gt;所在类DefaultOAuth2RequestFactory&lt;/strong&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;本段代码对应主流程图中的第③步&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;	public TokenRequest createTokenRequest(Map&amp;lt;String, String&amp;gt; requestParameters, ClientDetails authenticatedClient) {
		//获取clientId并校验其是否合法
		String clientId = requestParameters.get(OAuth2Utils.CLIENT_ID);
		if (clientId == null) {
			// if the clientId wasn&apos;t passed in in the map, we add pull it from the authenticated client object
			clientId = authenticatedClient.getClientId();
		}
		else {
			// otherwise, make sure that they match
			if (!clientId.equals(authenticatedClient.getClientId())) {
				throw new InvalidClientException(&quot;Given client ID does not match authenticated client&quot;);
			}
		}
		//从请求参数中获取到grant_type
		String grantType = requestParameters.get(OAuth2Utils.GRANT_TYPE);
		//从请求中根据clientId获取到所有的权限 即 scopes
		Set&amp;lt;String&amp;gt; scopes = extractScopes(requestParameters, clientId);
		//利用请求参数，clientId，scopes和grantType  new出一个TokenRequest 
		TokenRequest tokenRequest = new TokenRequest(requestParameters, clientId, scopes, grantType);
		//将token返回给请求端
		return tokenRequest;
	}
&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;下面真正生成token步1-3对应了主流程图中的第④步&lt;/li&gt;
&lt;li&gt;&lt;code&gt;真正生成token的步骤1&lt;/code&gt; — getTokenGranter().grant(tokenRequest.getGrantType(), tokenRequest)具体实现
— &lt;strong&gt;所在类CompositeTokenGranter&lt;/strong&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;img src=&quot;%E7%94%9F%E4%BA%A7token.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;真正生成token的步骤2&lt;/code&gt; — 上图中OAuth2AccessToken grant = granter.grant(grantType, tokenRequest); 的具体实现
— &lt;strong&gt;所在类ClientCredentialsTokenGranter&lt;/strong&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;@Override
public OAuth2AccessToken grant(String grantType, TokenRequest tokenRequest) {
	//去具体的实现类里去拿token
	OAuth2AccessToken token = super.grant(grantType, tokenRequest);
	//如果获取到token--对刷新令牌进行处理 --- &amp;gt;不过多解释
	if (token != null) {
		DefaultOAuth2AccessToken norefresh = new DefaultOAuth2AccessToken(token);
		// The spec says that client credentials should not be allowed to get a refresh token
		if (!allowRefresh) {
			norefresh.setRefreshToken(null);
		}
		token = norefresh;
	}
	//无论拿到没拿到都返回，如果拿到的token为null，则上图中的grant就是null，然后就会再利用其他的具体实现类尝试获取token，
	//一旦获取成功,就会将token返回给请求端
	return token;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;真正生成token的步骤3&lt;/code&gt; — 上面OAuth2AccessToken token = super.grant(grantType, tokenRequest);的具体实现
— &lt;strong&gt;所在类AbstractTokenGranter&lt;/strong&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;	public OAuth2AccessToken grant(String grantType, TokenRequest tokenRequest) {
		//判断当前grantType是否为本实现类指定要处理的grantType---如果不是直接返回null
		if (!this.grantType.equals(grantType)) {
			return null;
		}
		//获取clientId
		String clientId = tokenRequest.getClientId();
		//通过clientId再获取一遍ClientDetails对象
		ClientDetails client = clientDetailsService.loadClientByClientId(clientId);
		//判断grantType是否合法
		validateGrantType(grantType, client);
		//打个注释
		if (logger.isDebugEnabled()) {
			logger.debug(&quot;Getting access token for: &quot; + clientId);
		}
		//真正去具体的实现类里去拿token
		return getAccessToken(client, tokenRequest);

	}
	//上面的getAccessToken方法的具体实现
	protected OAuth2AccessToken getAccessToken(ClientDetails client, TokenRequest tokenRequest) {
		return tokenServices.createAccessToken(getOAuth2Authentication(client, tokenRequest));
	}
	//上面getOAuth2Authentication方法的具体实现 --- 生成OAuth2Authentication，
	//但是按照方法的加载机制如果具体实现里有该方法会先走实现类里的
	//在简化+授权码+密码模式的情况下不会走下面的方法  **=****=****=****=****=《 特别注意 》****=****=****=**==
	protected OAuth2Authentication getOAuth2Authentication(ClientDetails client, TokenRequest tokenRequest) {
		OAuth2Request storedOAuth2Request = requestFactory.createOAuth2Request(client, tokenRequest);
		return new OAuth2Authentication(storedOAuth2Request, null);
	}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;注意：&lt;/code&gt;上面的方法其实写在了package org.springframework.security.oauth2.provider.token下的抽像类AbstractTokenGranter里，其子类才是四种授权模式+RefreshToken的具体实现类 — &lt;code&gt;模版模式&lt;/code&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;真正生成token的步骤4 — 上面getOAuth2Authentication(client, tokenRequest)；方法的具体实现 — 不同授权模式具体实现不一样&lt;/li&gt;
&lt;li&gt;本段代码对应主流程图中的第⑤、⑥两步
— 所在类ResourceOwnerPasswordTokenGranter
这里分析的是密码模式的下的代码：&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;	@Override
	protected OAuth2Authentication getOAuth2Authentication(ClientDetails client, TokenRequest tokenRequest) {

		Map&amp;lt;String, String&amp;gt; parameters = new LinkedHashMap&amp;lt;String, String&amp;gt;(tokenRequest.getRequestParameters());
		String username = parameters.get(&quot;username&quot;);
		String password = parameters.get(&quot;password&quot;);
		// Protect from downstream leaks of password
		parameters.remove(&quot;password&quot;);
		//下面这部分代码在用户名+密码登陆时其实我们已经见过了，就是利用请求中的用户名+密码进行认证+授权校验
		//校验成功后会生成一个已经认证了的Authentication对象  --- 这里不细分析了
		//与之对应的是授权码模式会利用授权码进行校验并生成一个认证成功的Authentication对象 
		
		Authentication userAuth = new UsernamePasswordAuthenticationToken(username, password);
		((AbstractAuthenticationToken) userAuth).setDetails(parameters);
		try {
			userAuth = authenticationManager.authenticate(userAuth);
		}
		catch (AccountStatusException ase) {
			//covers expired, locked, disabled cases (mentioned in section 5.2, draft 31)
			throw new InvalidGrantException(ase.getMessage());
		}
		catch (BadCredentialsException e) {
			// If the username/password are wrong the spec says we should send 400/invalid grant
			throw new InvalidGrantException(e.getMessage());
		}
		if (userAuth == null || !userAuth.isAuthenticated()) {
			throw new InvalidGrantException(&quot;Could not authenticate user: &quot; + username);
		}
		//将ClientDetails 对象和TokenRequest 对象封装成一个OAuth2Request 对象
		OAuth2Request storedOAuth2Request = getRequestFactory().createOAuth2Request(client, tokenRequest);	
		//利用OAuth2Request和已经认证校验成功的Authentication 对象new 一个OAuth2Authentication对象
		return new OAuth2Authentication(storedOAuth2Request, userAuth);
	}
&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;真正生成token的步骤5&lt;/code&gt; — tokenServices.createAccessToken(getOAuth2Authentication(client, tokenRequest)); &lt;code&gt;createAccessToken具体实现&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;本段代码对应主流程图中的第⑦步&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;TokenStore的具体实现（为了方便看，放在了这里）&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;tokenStore%E7%9A%84%E5%85%B7%E4%BD%93%E5%AE%9E%E7%8E%B0.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;— &lt;strong&gt;所在类DefaultTokenServices&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;@Transactional
public OAuth2AccessToken createAccessToken(OAuth2Authentication authentication) throws AuthenticationException {
	//先去存储里看有没有已经存在的token --- 存储可以有多种，上图是TokenStore的具体实现类，可以看到既有内存、Jdbc、也有redis
	//并且还有Jwt --- 》 这是用Jwt替换spring security oauth中默认token的依据
	OAuth2AccessToken existingAccessToken = tokenStore.getAccessToken(authentication);
	OAuth2RefreshToken refreshToken = null;
	//如果从存储里找到了已经存在的token
	if (existingAccessToken != null) {
		//如果找到的token已经过期 -- 》在tokenStore里将其RefreshToken和该token删掉
		if (existingAccessToken.isExpired()) {
			if (existingAccessToken.getRefreshToken() != null) {
				refreshToken = existingAccessToken.getRefreshToken();
				// The token store could remove the refresh token when the
				// access token is removed, but we want to
				// be sure...
				tokenStore.removeRefreshToken(refreshToken);
			}
			tokenStore.removeAccessToken(existingAccessToken);
		}
		else {
			// Re-store the access token in case the authentication has changed
			//重新存一下获取到的token，因为你有可能是之前用授权码模式获取的token，但现在用密码模式获取的token，
			//这时要存的信息是不一样的
			tokenStore.storeAccessToken(existingAccessToken, authentication);
			//将没过期的token返回给请求
			return existingAccessToken;
		}
	}

	// Only create a new refresh token if there wasn&apos;t an existing one
	// associated with an expired access token.
	// Clients might be holding existing refresh tokens, so we re-use it in
	// the case that the old access token
	// expired.
	//首先看看有没有刷新令牌，如果没有就新建一个，注意一下上面的英文注释
	if (refreshToken == null) {
		refreshToken = createRefreshToken(authentication);
	}
	// But the refresh token itself might need to be re-issued if it has
	// expired.
	//如果有刷新令牌，但是已经过期，需要重新发放一个刷新令牌
	else if (refreshToken instanceof ExpiringOAuth2RefreshToken) {
		ExpiringOAuth2RefreshToken expiring = (ExpiringOAuth2RefreshToken) refreshToken;
		if (System.currentTimeMillis() &amp;gt; expiring.getExpiration().getTime()) {
			refreshToken = createRefreshToken(authentication);
		}
	}
	//真正生成token的逻辑
	OAuth2AccessToken accessToken = createAccessToken(authentication, refreshToken);
	//将生成的token存入到存储里
	tokenStore.storeAccessToken(accessToken, authentication);
	//重生成的令牌里取出刷新令牌，将刷新令牌存到存储里，防止它在上一步被改变
	// In case it was modified
	refreshToken = accessToken.getRefreshToken();
	if (refreshToken != null) {
		tokenStore.storeRefreshToken(refreshToken, authentication);
	}
	return accessToken;

}
&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;真正生成token的逻辑&lt;/code&gt; — OAuth2AccessToken accessToken = createAccessToken(authentication, refreshToken);的具体实现 — &lt;strong&gt;所在类DefaultTokenServices&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;本段代码对应主流程图中的第⑦步下面的TokenStore和TokenEnhancer&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;private OAuth2AccessToken createAccessToken(OAuth2Authentication authentication, OAuth2RefreshToken refreshToken) {
	//利用UUID当成构造函数的参数生成一个令牌
	DefaultOAuth2AccessToken token = new DefaultOAuth2AccessToken(UUID.randomUUID().toString());
	//获取过期时间并设置到token里
	int validitySeconds = getAccessTokenValiditySeconds(authentication.getOAuth2Request());
	if (validitySeconds &amp;gt; 0) {
		token.setExpiration(new Date(System.currentTimeMillis() + (validitySeconds * 1000L)));
	}
	//设置刷新令牌
	token.setRefreshToken(refreshToken);
	//设置令牌的权限
	token.setScope(authentication.getOAuth2Request().getScope());
	//看是否配置了AccessTokenEnhancer的具体实现
	//如果配置了对生成的token进行增强 --- 设置Jwt时会讲
	return accessTokenEnhancer != null ? accessTokenEnhancer.enhance(token, authentication) : token;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;简单配置调通oauth2&lt;/h2&gt;
&lt;h3&gt;一、认证服务器&lt;/h3&gt;
&lt;p&gt;pom.xml&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;        &amp;lt;dependency&amp;gt;
            &amp;lt;groupId&amp;gt;org.apache.commons&amp;lt;/groupId&amp;gt;
            &amp;lt;artifactId&amp;gt;commons-lang3&amp;lt;/artifactId&amp;gt;
        &amp;lt;/dependency&amp;gt;
        &amp;lt;dependency&amp;gt;
            &amp;lt;groupId&amp;gt;org.springframework.cloud&amp;lt;/groupId&amp;gt;
            &amp;lt;artifactId&amp;gt;spring-cloud-starter-oauth2&amp;lt;/artifactId&amp;gt;
        &amp;lt;/dependency&amp;gt;
        &amp;lt;dependency&amp;gt;
            &amp;lt;groupId&amp;gt;org.springframework.boot&amp;lt;/groupId&amp;gt;
            &amp;lt;artifactId&amp;gt;spring-boot-starter-web&amp;lt;/artifactId&amp;gt;
        &amp;lt;/dependency&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;认证服务配置&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;/**
 * &amp;lt;p&amp;gt;DESC: 服务配置&amp;lt;/p&amp;gt;
 * &amp;lt;p&amp;gt;DATE: 2019-11-19 14:26&amp;lt;/p&amp;gt;
 * &amp;lt;p&amp;gt;VERSION:2.0.0&amp;lt;/p&amp;gt;
 * &amp;lt;p&amp;gt;@AUTHOR: ZhengYong&amp;lt;/p&amp;gt;
 */
@Configuration
@EnableAuthorizationServer
public class ServiceConfig extends AuthorizationServerConfigurerAdapter {
    

    @Override
    public void configure(ClientDetailsServiceConfigurer clients) throws Exception {
      //将客户端写入内存中
        clients.inMemory().withClient(&quot;client&quot;)
                .secret(&quot;secret&quot;)
                .scopes(&quot;app&quot;)
                .redirectUris(&quot;http://www.baidu.com&quot;)
                .authorizedGrantTypes(&quot;authorization_code&quot;);
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;认证服务安全配置&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;/**
 * &amp;lt;p&amp;gt;DESC: web配置类&amp;lt;/p&amp;gt;
 * &amp;lt;p&amp;gt;DATE: 2019-11-19 15:08&amp;lt;/p&amp;gt;
 * &amp;lt;p&amp;gt;VERSION:2.0.0&amp;lt;/p&amp;gt;
 * &amp;lt;p&amp;gt;@AUTHOR: ZhengYong&amp;lt;/p&amp;gt;
 */
@Configuration
@EnableWebSecurity
@EnableGlobalMethodSecurity(prePostEnabled = true, securedEnabled = true, jsr250Enabled = true)
public class AppWebSecurityConfig extends WebSecurityConfigurerAdapter {

    @Override
    public void configure(WebSecurity web) throws Exception {
        web.ignoring().antMatchers(&quot;/oauth/check_token&quot;);
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;二、资源服务器&lt;/h3&gt;
&lt;p&gt;pom.xml和认证服务器一致&lt;/p&gt;
&lt;p&gt;application.yml&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;server:
  port: 8090
security:
  oauth2:
    client:
      client-id: client
      client-secret: secret
      access-token-uri: http://localhost/oauth/token
      user-authorization-uri: http://localhost/oauth/authorize
    resource:
      token-info-uri: http://localhost/oauth/check_token
spring:
  application:
    name: resources
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;资源服务配置&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;/**
 * &amp;lt;p&amp;gt;DESC: 资源配置类&amp;lt;/p&amp;gt;
 * &amp;lt;p&amp;gt;DATE: 2019-11-19 16:31&amp;lt;/p&amp;gt;
 * &amp;lt;p&amp;gt;VERSION:2.0.0&amp;lt;/p&amp;gt;
 * &amp;lt;p&amp;gt;@AUTHOR: ZhengYong&amp;lt;/p&amp;gt;
 */
@Configuration
@EnableResourceServer
@EnableGlobalMethodSecurity(prePostEnabled = true, securedEnabled = true, jsr250Enabled = true)
public class ResourcesConfig extends ResourceServerConfigurerAdapter {
  
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;controller&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;/**
 * &amp;lt;p&amp;gt;DESC: 测试&amp;lt;/p&amp;gt;
 * &amp;lt;p&amp;gt;DATE: 2019-11-19 15:37&amp;lt;/p&amp;gt;
 * &amp;lt;p&amp;gt;VERSION:2.0.0&amp;lt;/p&amp;gt;
 * &amp;lt;p&amp;gt;@AUTHOR: ZhengYong&amp;lt;/p&amp;gt;
 */
@RestController
public class ResourceControllerTest {

    @GetMapping(&quot;/test&quot;)
    public Object test(){
        return &quot;你好呀&quot;;
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;三、资源服务器配置自定义权限检查&lt;/h3&gt;
&lt;blockquote&gt;
&lt;p&gt;安装上文的权限剥离来做配置，需要AuthorizeConfigManager、AuthorizeConfigProvider、ResourceServerConfigurerAdapter&lt;/p&gt;
&lt;p&gt;还有自定义的provider和自定义权限校验，这里不再重复&lt;/p&gt;
&lt;p&gt;这里的配置需要写到ResourceServerConfigurerAdapter中，不能是WebSecurityConfigurerAdapter。因为是资源服务器。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;manager和Provider上文已有，不再重复。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;资源服务器配置&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;/**
 * &amp;lt;p&amp;gt;DESC: 资源配置类&amp;lt;/p&amp;gt;
 * &amp;lt;p&amp;gt;DATE: 2019-11-19 16:31&amp;lt;/p&amp;gt;
 * &amp;lt;p&amp;gt;VERSION:2.0.0&amp;lt;/p&amp;gt;
 * &amp;lt;p&amp;gt;@AUTHOR: ZhengYong&amp;lt;/p&amp;gt;
 */
@Configuration
@EnableResourceServer
@EnableGlobalMethodSecurity(prePostEnabled = true, securedEnabled = true, jsr250Enabled = true)
public class ResourcesConfig extends ResourceServerConfigurerAdapter {

    @Resource
    private AuthorizeConfigManager authorizeConfigManager;

    @Override
    public void configure(HttpSecurity http) throws Exception {
        authorizeConfigManager.config(http.authorizeRequests());
    }
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;解决：无法识别表达式的异常&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;正常流程，获取token访问资源服务器，会无法识别表达式的异常抛出&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;java.lang.IllegalArgumentException: Failed to evaluate expression &apos;#oauth2.throwOnError(@rbacService.hasPermission(request,authentication))&apos;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;参考： &lt;a href=&quot;https://github.com/spring-projects/spring-security-oauth/issues/730&quot;&gt;https://github.com/spring-projects/spring-security-oauth/issues/730&lt;/a&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;解决办法，在config中加入默认表达式处理器&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;    @Autowired
    private OAuth2WebSecurityExpressionHandler expressionHandler;

    @Bean
    public OAuth2WebSecurityExpressionHandler oAuth2WebSecurityExpressionHandler(ApplicationContext applicationContext) {
        OAuth2WebSecurityExpressionHandler expressionHandler = new OAuth2WebSecurityExpressionHandler();
        expressionHandler.setApplicationContext(applicationContext);
        return expressionHandler;
    }

    @Override
    public void configure(ResourceServerSecurityConfigurer resources) throws Exception {
        resources.expressionHandler(expressionHandler);
    }
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;用户名密码登陆嫁接到Spring Security OAuth&lt;/h2&gt;
&lt;h3&gt;1 将自定义认证方式嫁接到Spring Security OAuth的原理&lt;/h3&gt;
&lt;p&gt;Spring Security OAuth生成token的核心源码，其主要流程如下图：&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;token%E6%BA%90%E7%A0%81.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;而我们的目标是将Spring Security OAuth默认的走授权流程后发放token的步骤替换成走我们自定义的认证方式后发放token。既然要在我们自定义的认证方式认证成功后才发放token，则我们发放token的代码应该写在&lt;code&gt;认证成功处理器&lt;/code&gt;里，回看上图联系其源码，可以知道：&lt;/p&gt;
&lt;p&gt;（1）ClientDetails是通过请求头中的client-id获得的
（2）TokenRequest对象是通过请求参数+ClientDetails对象new出来的
（3）第④步是真正与授权模式相关的
（4）走完第④步会生成两个对象OAuth2Request和Authentication，而OAuth2Request是对ClientDetails对象和TokenRequest对象的封装Authentication对象即授权用户的对象 — 走完我们自定义的认证方式其实就会获得一个Authentication对象，因此在我们将自定义的认证方式嫁接到Spring Security OAuth框架的过程中不需要考虑该对象
（5）获得了OAuth2Reques和tAuthentication对象可以直接new 出一个OAuth2Authentication对象
（6）有了OAuth2Authentication对象我们就可以调用Spring Security OAuth默认的获取token的方法了&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;参照上面的步骤将我们自定义的认证方式嫁接到Spring Security OAuth的大致流程如下&lt;/strong&gt;：&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;%E7%99%BB%E5%BD%95%E6%96%B9%E5%BC%8F%E5%AB%81%E6%8E%A5.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;h3&gt;2 将用户名密码登陆嫁接到Spring Security OAuth&lt;/h3&gt;
&lt;h4&gt;2.1 认证成功处理器 — 认证成功后生成token&lt;/h4&gt;
&lt;pre&gt;&lt;code&gt;/**
 * &amp;lt;p&amp;gt;DESC: 成功处理器&amp;lt;/p&amp;gt;
 * &amp;lt;p&amp;gt;DATE: 2019-11-20 10:27&amp;lt;/p&amp;gt;
 * &amp;lt;p&amp;gt;VERSION:2.0.0&amp;lt;/p&amp;gt;
 * &amp;lt;p&amp;gt;@AUTHOR: ZhengYong&amp;lt;/p&amp;gt;
 */
@Slf4j
@Component
public class AppSuccessHandler implements AuthenticationSuccessHandler {

    @Resource
    private ClientDetailsService clientDetailsService;

    @Resource
    private AuthorizationServerTokenServices authorizationServerTokenServices;

    @Override
    public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws IOException, ServletException {
        log.info(&quot;【登录成功】&quot;);

        String header = request.getHeader(&quot;Authorization&quot;);
        if (header == null || !header.toLowerCase().startsWith(&quot;basic &quot;)) {
            throw new UnapprovedClientAuthenticationException(&quot;请求头无authentization信息&quot;);
        }
        try {
            String[] tokens = this.extractAndDecodeHeader(header, request);

            assert tokens.length == 2;

            String clientId = tokens[0];
            String secret = tokens[1];

            ClientDetails clientDetails = clientDetailsService.loadClientByClientId(clientId);
            if (clientDetails == null) {
                throw new UnapprovedClientAuthenticationException(&quot;clientid不存在&quot;);
            } else if (!StringUtils.equals(clientDetails.getClientSecret(), secret)) {
                throw new UnapprovedClientAuthenticationException(&quot;secret不匹配&quot;);
            }

            // 第一个参数为请求参数的一个Map集合，
            //  在Spring Security OAuth的源码里要用这个Map里的用户名+密码或授权码来生成Authentication对象,
            //  但我们已经获取到了Authentication对象，所以这里可以直接传一个空的Map
            // 第三个参数为scope即请求的权限 ---》这里的策略是获得的ClientDetails对象里配了什么权限就给什么权限 //todo
            // 第四个参数为指定什么模式 比如密码模式为password，授权码模式为authorization_code，
            TokenRequest tokenRequest = new TokenRequest(new HashMap&amp;lt;&amp;gt;(), clientId, clientDetails.getScope(), &quot;自定义的模式&quot;);

            //获取OAuth2Request对象
            OAuth2Request oAuth2Request = tokenRequest.createOAuth2Request(clientDetails);

            //new出一个OAuth2Authentication对象
            OAuth2Authentication oAuth2Authentication = new OAuth2Authentication(oAuth2Request, authentication);

            //生成token
            OAuth2AccessToken accessToken = authorizationServerTokenServices.createAccessToken(oAuth2Authentication);

            //回写token
            response.setContentType(MediaType.APPLICATION_JSON_UTF8_VALUE);
            String tokenStr = JSON.toJSONString(accessToken);
            response.getWriter().write(tokenStr);

        } catch (Exception e) {
            throw new RuntimeException(&quot;失败&quot;);
        }
    }

    /**
     * 从header获取Authentication信息 --- 》 clientId和clientSecret
     * @param header 请求头
     * @param request 请求
     * @return  clientID，secret
     * @throws IOException 异常
     */
    private String[] extractAndDecodeHeader(String header, HttpServletRequest request) throws IOException {
        byte[] base64Token = header.substring(6).getBytes(StandardCharsets.UTF_8);

        byte[] decoded;
        try {
            decoded = Base64.getDecoder().decode(base64Token);
        } catch (IllegalArgumentException var7) {
            throw new BadCredentialsException(&quot;Failed to decode basic authentication token&quot;);
        }

        String token = new String(decoded, StandardCharsets.UTF_8);
        int delim = token.indexOf(&quot;:&quot;);
        if (delim == -1) {
            throw new BadCredentialsException(&quot;Invalid basic authentication token&quot;);
        } else {
            return new String[]{token.substring(0, delim), token.substring(delim + 1)};
        }
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h4&gt;2.2 app模块加入安全配置信息 — 让我们自定义的认证方式生效&lt;/h4&gt;
&lt;pre&gt;&lt;code&gt;/**
 * &amp;lt;p&amp;gt;DESC: web配置类&amp;lt;/p&amp;gt;
 * &amp;lt;p&amp;gt;DATE: 2019-11-19 15:08&amp;lt;/p&amp;gt;
 * &amp;lt;p&amp;gt;VERSION:2.0.0&amp;lt;/p&amp;gt;
 * &amp;lt;p&amp;gt;@AUTHOR: ZhengYong&amp;lt;/p&amp;gt;
 */
@Configuration
@EnableWebSecurity
@EnableGlobalMethodSecurity(prePostEnabled = true, securedEnabled = true, jsr250Enabled = true)
public class AppWebSecurityConfig extends WebSecurityConfigurerAdapter {

    @Resource
    private AppSuccessHandler appSuccessHandler;

    @Override
    protected void configure(HttpSecurity http) throws Exception {
       //只是加入了成功处理器，其他均为默认
        http.csrf().disable().formLogin().successHandler(appSuccessHandler);
    }

    @Override
    public void configure(WebSecurity web) throws Exception {
        web.ignoring().antMatchers(&quot;/oauth/check_token&quot;);
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;3 测试&lt;/h3&gt;
&lt;p&gt;发送的登陆请求为POST请求,路径为http://client:secret@localhost:80/login ，同时需要再请求头里加上Client-Id和Client-Secret，请求参数为username和password
可以看到已经获取到了token信息&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;%E5%AB%81%E6%8E%A5%E7%94%A8%E6%88%B7%E7%99%BB%E5%BD%95%E6%B5%8B%E8%AF%95.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;访问资源服务器&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;http://localhost:8090/test?access_token=8fa94489-372e-4b27-871f-372f39c112a1
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;短信登陆嫁接到Spring Security OAuth&lt;/h2&gt;
&lt;h3&gt;一、短信登陆需要解决的问题和解决方式&lt;/h3&gt;
&lt;p&gt;&lt;img src=&quot;%E7%9F%AD%E4%BF%A1%E9%AA%8C%E8%AF%81%E7%A0%81%E9%87%8D%E6%9E%84.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;即之前开发的短信登陆和图形验证码都是基于cookie和session的，但是基于token的认证方式根本就不会cookie和session。&lt;/p&gt;
&lt;p&gt;其实解决方式也简单，就是把原来存放到session中的验证码，我们直接存到数据库（如redis）中，然后验证的时候再去数据库里把这个验证码拿出来进行验证就可以了。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h3&gt;二、将短信登陆嫁接到Spring Security OAuth&lt;/h3&gt;
&lt;h4&gt;1、加入验证码配置&lt;/h4&gt;
&lt;p&gt;&lt;code&gt;.apply(smsCodeAuthenticationSecurityConfig())&lt;/code&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;    
@Configuration
@EnableWebSecurity
@EnableGlobalMethodSecurity(prePostEnabled = true, securedEnabled = true, jsr250Enabled = true)
public class AppWebSecurityConfig extends WebSecurityConfigurerAdapter {

	@Bean
    public SmsCodeAuthenticationSecurityConfig smsCodeAuthenticationSecurityConfig(){
        return new SmsCodeAuthenticationSecurityConfig(appSuccessHandler,appFailHandler);
    }

    @Override
    protected void configure(HttpSecurity http) throws Exception {

        http.csrf().disable()
                .apply(smsCodeAuthenticationSecurityConfig())
                .and()
                .formLogin().successHandler(appSuccessHandler)
        ;
    }
&lt;/code&gt;&lt;/pre&gt;
&lt;h4&gt;2、 修改保存、获取和移出验证码的逻辑&lt;/h4&gt;
&lt;p&gt;由于浏览器项目还是可以使用之前的cookie+session的方式保存、获取和移出验证码，而app项目需要将用到数据库（如redis）+deviceId（数据库相当于session、deviceId相当于cookie）的方式来保存、获取和移出验证码。两种项目都需要做保存、获取和移出验证码的动作，因此可以采用模版模式+策略模式的方式进行开发。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;package com.nrsc.security.core.validate.code;

import org.springframework.web.context.request.ServletWebRequest;

/**
 * @author : Sun Chuan
 * @date : 2019/10/19 21:55
 * Description：
 */
public interface ValidateCodeRepository {
    /**
     * 保存验证码
     * @param request
     * @param code
     * @param validateCodeType
     */
    void save(ServletWebRequest request, ValidateCode code, ValidateCodeType validateCodeType);
    /**
     * 获取验证码
     * @param request
     * @param validateCodeType
     * @return
     */
    ValidateCode get(ServletWebRequest request, ValidateCodeType validateCodeType);
    /**
     * 移除验证码
     * @param request
     * @param codeType
     */
    void remove(ServletWebRequest request, ValidateCodeType codeType);
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;浏览器项目使用cookie+session的方式进行验证码的存、取、删&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;/**
 * &amp;lt;p&amp;gt;DESC: session验证码仓库&amp;lt;/p&amp;gt;
 * &amp;lt;p&amp;gt;DATE: 2019-11-20 13:44&amp;lt;/p&amp;gt;
 * &amp;lt;p&amp;gt;VERSION:2.0.0&amp;lt;/p&amp;gt;
 * &amp;lt;p&amp;gt;@AUTHOR: ZhengYong&amp;lt;/p&amp;gt;
 */
@Component
public class SessionValidateCodeRepository implements ValidateCodeRepository {

    /**
     * 验证码放入session时的前缀
     */
    private static  final String SESSION_KEY_PREFIX = &quot;SESSION_KEY_FOR_CODE_&quot;;

    /**
     * 操作session的工具类
     */
    private SessionStrategy sessionStrategy = new HttpSessionSessionStrategy();

    @Override
    public void save(ServletWebRequest request, ValidateCode code, ValidateCodeType validateCodeType) {
        sessionStrategy.setAttribute(request,getSessionKey(request,validateCodeType),code);
    }

    private String getSessionKey(ServletWebRequest request,ValidateCodeType validateCodeType){
        return SESSION_KEY_PREFIX + validateCodeType.toString();
    }

    @Override
    public ValidateCode get(ServletWebRequest request, ValidateCodeType validateCodeType) {
        return (ValidateCode) sessionStrategy.getAttribute(request,getSessionKey(request,validateCodeType));
    }

    @Override
    public void remove(ServletWebRequest request, ValidateCodeType codeType) {
        sessionStrategy.removeAttribute(request,getSessionKey(request,codeType));
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;app项目使用redis+deviceId的方式进行验证码的存、取、删&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;/**
 * &amp;lt;p&amp;gt;DESC: app验证码仓库&amp;lt;/p&amp;gt;
 * &amp;lt;p&amp;gt;DATE: 2019-11-20 13:56&amp;lt;/p&amp;gt;
 * &amp;lt;p&amp;gt;VERSION:2.0.0&amp;lt;/p&amp;gt;
 * &amp;lt;p&amp;gt;@AUTHOR: ZhengYong&amp;lt;/p&amp;gt;
 */
@Component
public class AppValidateCodeRepository implements ValidateCodeRepository {

    @Resource
    private RedisTemplate&amp;lt;Object,Object&amp;gt; redisTemplate;

    @Override
    public void save(ServletWebRequest request, ValidateCode code, ValidateCodeType validateCodeType) {
        redisTemplate.opsForValue().set(buildKey(request,validateCodeType),code,30, TimeUnit.MINUTES);
    }

    private String buildKey(ServletWebRequest request,ValidateCodeType validateCodeType){
        String deviceId = request.getHeader(&quot;deviceId&quot;);
        if(StringUtils.isBlank(deviceId)){
            throw new ValidateCodeException(&quot;deviceId不能为空&quot;);
        }
        return &quot;code:&quot;+validateCodeType.toString()+&quot;:&quot;+deviceId;
    }

    @Override
    public ValidateCode get(ServletWebRequest request, ValidateCodeType validateCodeType) {
        return (ValidateCode) redisTemplate.opsForValue().get(buildKey(request,validateCodeType));
    }

    @Override
    public void remove(ServletWebRequest request, ValidateCodeType codeType) {
        redisTemplate.delete(buildKey(request,codeType));
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h4&gt;3、 修改原来保存、获取和移出验证码的代码&lt;/h4&gt;
&lt;pre&gt;&lt;code&gt;/**
 * &amp;lt;p&amp;gt;DESC: 验证码抽象处理器&amp;lt;/p&amp;gt;
 * &amp;lt;p&amp;gt;DATE: 2019-10-09 15:49&amp;lt;/p&amp;gt;
 * &amp;lt;p&amp;gt;VERSION:2.0.0&amp;lt;/p&amp;gt;
 * &amp;lt;p&amp;gt;@AUTHOR: ZhengYong&amp;lt;/p&amp;gt;
 */
public abstract class AbstractValidateCodeProcessor&amp;lt;C&amp;gt; implements ValidateCodeProcessor  {
  
      @Resource
    private ValidateCodeRepository validateCodeRepository;
  
      /**
     * 创建方法
     *
     * @param request 封装request和response
     */
    @Override
    public void create(ServletWebRequest request,ValidateCodeType validateCodeType) {
        //创建验证码（创建验证码的子类实现）
        C validateCode = generator(request);
        //存入session（公共代码由抽象类自行实现）
        save(request,validateCode,validateCodeType);
        //发送验证码（子类实现）
        send(request,validateCode);
    }
  
      /**
     * 保存验证码到session中
     * @param request 包装请求
     */
    private void save(ServletWebRequest request, C validateCode, ValidateCodeType validateCodeType){
        ValidateCode code = (ValidateCode) validateCode;
        ValidateCode code1 = new ValidateCode(code.getCode(), code.getExpireTime());
        validateCodeRepository.save(request,code1,validateCodeType);
    };
&lt;/code&gt;&lt;/pre&gt;
&lt;h4&gt;4、验证码类型&lt;/h4&gt;
&lt;pre&gt;&lt;code&gt;/**
 * &amp;lt;p&amp;gt;DESC: 验证码类型&amp;lt;/p&amp;gt;
 * &amp;lt;p&amp;gt;DATE: 2019-11-20 13:42&amp;lt;/p&amp;gt;
 * &amp;lt;p&amp;gt;VERSION:2.0.0&amp;lt;/p&amp;gt;
 * &amp;lt;p&amp;gt;@AUTHOR: ZhengYong&amp;lt;/p&amp;gt;
 */
public enum ValidateCodeType {
    /**
     * 短信验证码
     */
    SMS,
    /**
     * 图形验证码
     */
    IMAGE
    ;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;三、测试&lt;/h3&gt;
&lt;p&gt;（1）获取短信验证码&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;localhost/validate/code/sms?mobile=18281975746&lt;/p&gt;
&lt;p&gt;header :
deviceId  = 123456&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;此时后端会打印获取到的短息验证码，如下图&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;手机号:18281975746|发送手机验证码：6635&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;此时redis里已经保存了我们的验证码 — &lt;code&gt;验证码保存成功&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;（2）发送通过短信验证码登陆的请求&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;client:secret@localhost/sms/check&lt;/p&gt;
&lt;p&gt;post&lt;/p&gt;
&lt;p&gt;请求表单：&lt;/p&gt;
&lt;p&gt;mobile:18281975746
smsCode:3986
deviceId:123456&lt;/p&gt;
&lt;p&gt;hearder:&lt;/p&gt;
&lt;p&gt;Authorization  = Basic Y2xpZW50OnNlY3JldA==&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;(3) 登录成功，返回token&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;{
    &quot;additionalInformation&quot;: {},
    &quot;expiration&quot;: 1574284411988,
    &quot;expired&quot;: false,
    &quot;expiresIn&quot;: 43199,
    &quot;scope&quot;: [
        &quot;app&quot;
    ],
    &quot;tokenType&quot;: &quot;bearer&quot;,
    &quot;value&quot;: &quot;aa946e1c-18d5-49b4-9449-4b84453706f5&quot;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;(4) 访问资源服务器&lt;/p&gt;
&lt;p&gt;http://localhost:8090/test?access_token=aa946e1c-18d5-49b4-9449-4b84453706f5&lt;/p&gt;
&lt;h2&gt;一、社交登录嫁接到Security OAuth2--服务提供商SDK使用简化模式时&lt;/h2&gt;
&lt;h3&gt;1 原理&lt;/h3&gt;
&lt;p&gt;首先要明确SDK是服务提供商（如QQ、微信等）提供的用于引导第三方应用（比如说我们自己的APP）完成授权流程的工具包，不同的服务提供商的SDK的具体实现是不一样的，选择的授权模式也是不一样的，一般会有两种模式：①简化模式，②授权码模式。&lt;/p&gt;
&lt;p&gt;如果服务提供商（如QQ、微信等）提供的SDK使用的是简化模式，开发APP社交登陆的原理如下：
&lt;img src=&quot;%E9%87%8D%E6%9E%84%E7%A4%BE%E4%BA%A4%E7%99%BB%E5%BD%951%E5%8E%9F%E7%90%86.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;即用户同意授权后就可以获得到openId（用户在服务提供商上的唯一标识）和服务提供商生成的accessToken了，但是我们并不能拿着这个token访问我们的应用（上图中的第三方应用），因为这个token是服务提供商发放的，不是我们的认证服务器发放的。 — 对应上图1、2、3三步&lt;/p&gt;
&lt;p&gt;但是与此同时由于已经获得了openId，我们就可以拿着openId去我们存放本系统用户账号和社交账号对应关系的userconnection表(如下图)去验证该账户的信息了。
&lt;img src=&quot;userconnection%E8%A1%A8.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;如果验证成功，就可以生成一个Authentication对象，然后我们就可以去我们的自己的认证服务器（上图中的第三方应用）获取token了。 — 对应上图4、5两步&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;假设已经走完1-3步，即我们已经获取到了openId，那如何拿着获取到的openId进行认证校验并获取到token呢？其实原理很简单，基本和短信登陆的原理一致：短信登陆是我们获取到了一个短信验证码，然后拿着这个验证码进行校验，并获取到一个token；而这里是拿着openId进行校验，然后获取一个token。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h3&gt;2 基于openId的认证开发&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;用来封装openId和providerId的token — OpenIdAuthenticationToken&lt;/strong&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;/**
 * &amp;lt;p&amp;gt;DESC: openid的token&amp;lt;/p&amp;gt;
 * &amp;lt;p&amp;gt;DATE: 2019-11-26 14:58&amp;lt;/p&amp;gt;
 * &amp;lt;p&amp;gt;VERSION:2.0.0&amp;lt;/p&amp;gt;
 * &amp;lt;p&amp;gt;@AUTHOR: ZhengYong&amp;lt;/p&amp;gt;
 */
public class OpenIdAuthenticationToken extends AbstractAuthenticationToken {

    private static final long serialVersionUID = 510L;
    /**
     * openid
     */
    private final Object principal;
    /**
     * providerId
     */
    private Object credentials;

    public OpenIdAuthenticationToken(Object openid, Object providerId) {
        super((Collection)null);
        this.principal = openid;
        this.credentials = providerId;
        this.setAuthenticated(false);
    }

    public OpenIdAuthenticationToken(Object openid, Object providerId, Collection&amp;lt;? extends GrantedAuthority&amp;gt; authorities) {
        super(authorities);
        this.principal = openid;
        this.credentials = providerId;
        super.setAuthenticated(true);
    }

    @Override
    public Object getCredentials() {
        return this.credentials;
    }

    @Override
    public Object getPrincipal() {
        return this.principal;
    }

    @Override
    public void setAuthenticated(boolean isAuthenticated) throws IllegalArgumentException {
        if (isAuthenticated) {
            throw new IllegalArgumentException(&quot;Cannot set this token to trusted - use constructor which takes a GrantedAuthority list instead&quot;);
        } else {
            super.setAuthenticated(false);
        }
    }

    @Override
    public void eraseCredentials() {
        super.eraseCredentials();
        this.credentials = null;
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;定义过滤器，拦截指定请求，并处理&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;/**
 * &amp;lt;p&amp;gt;DESC: openid过滤器&amp;lt;/p&amp;gt;
 * &amp;lt;p&amp;gt;DATE: 2019-11-26 15:03&amp;lt;/p&amp;gt;
 * &amp;lt;p&amp;gt;VERSION:2.0.0&amp;lt;/p&amp;gt;
 * &amp;lt;p&amp;gt;@AUTHOR: ZhengYong&amp;lt;/p&amp;gt;
 */
@EqualsAndHashCode(callSuper = true)
@Data
public class OpenIdAuthenticationFilter extends AbstractAuthenticationProcessingFilter {

    public static final String SPRING_SECURITY_FORM_USERNAME_KEY = &quot;username&quot;;
    public static final String SPRING_SECURITY_FORM_PASSWORD_KEY = &quot;password&quot;;
    private String openid = &quot;openid&quot;;
    private String providerId = &quot;providerId&quot;;
    private boolean postOnly = true;

    public OpenIdAuthenticationFilter() {
        super(new AntPathRequestMatcher(&quot;/openid&quot;, &quot;POST&quot;));
    }

    @Override
    public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response) throws AuthenticationException {
        if (this.postOnly &amp;amp;&amp;amp; !request.getMethod().equals(&quot;POST&quot;)) {
            throw new AuthenticationServiceException(&quot;Authentication method not supported: &quot; + request.getMethod());
        } else {
            String openid = this.obtainOpenid(request);
            String providerId = this.obtainProviderId(request);
            if (openid == null) {
                openid = &quot;&quot;;
            }

            if (providerId == null) {
                providerId = &quot;&quot;;
            }

            openid = openid.trim();
            OpenIdAuthenticationToken authRequest = new OpenIdAuthenticationToken(openid, providerId);
            this.setDetails(request, authRequest);
            return this.getAuthenticationManager().authenticate(authRequest);
        }
    }

    protected String obtainProviderId(HttpServletRequest request) {
        return request.getParameter(this.providerId);
    }

    protected String obtainOpenid(HttpServletRequest request) {
        return request.getParameter(this.openid);
    }

    protected void setDetails(HttpServletRequest request, OpenIdAuthenticationToken authRequest) {
        authRequest.setDetails(this.authenticationDetailsSource.buildDetails(request));
    }

    public void setPostOnly(boolean postOnly) {
        this.postOnly = postOnly;
    }

}
&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;自定义Provider，实现真正认证逻辑&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;/**
 * &amp;lt;p&amp;gt;DESC: openid的provider&amp;lt;/p&amp;gt;
 * &amp;lt;p&amp;gt;DATE: 2019-11-26 15:10&amp;lt;/p&amp;gt;
 * &amp;lt;p&amp;gt;VERSION:2.0.0&amp;lt;/p&amp;gt;
 * &amp;lt;p&amp;gt;@AUTHOR: ZhengYong&amp;lt;/p&amp;gt;
 */
@Data
public class OpenIdAuthenticationProvider implements AuthenticationProvider {

    private SocialUserDetailsService socialUserDetailsService;

    private UsersConnectionRepository usersConnectionRepository;

    @Override
    public Authentication authenticate(Authentication authentication) throws AuthenticationException {
        //未认证的token
        OpenIdAuthenticationToken authenticationToken = (OpenIdAuthenticationToken) authentication;
        //解析token中的参数
        String openid = (String) authenticationToken.getPrincipal();
        String providerId = (String) authenticationToken.getCredentials();
        Set&amp;lt;String&amp;gt; providerUserIds = new HashSet&amp;lt;&amp;gt;();
        providerUserIds.add(openid);
        //根据参数查找是否存在用户
        Set&amp;lt;String&amp;gt; idsConnectedTo = usersConnectionRepository.findUserIdsConnectedTo(providerId, providerUserIds);
        String userId = idsConnectedTo.iterator().next();
        SocialUserDetails user = socialUserDetailsService.loadUserByUserId(userId);
        if(user == null){
            throw new InternalAuthenticationServiceException(&quot;无法获取用户信息&quot;);
        }
        //组装token
        OpenIdAuthenticationToken authenticationResult = new OpenIdAuthenticationToken(openid, providerId, user.getAuthorities());
        //保证成认证成功的token 返回
        authenticationResult.setDetails(authenticationToken.getDetails());
        return authenticationResult;
    }

    @Override
    public boolean supports(Class&amp;lt;?&amp;gt; aClass) {
        return aClass.isAssignableFrom(OpenIdAuthenticationToken.class);
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;将openId校验逻辑加入到spring-security过滤器的关键配置类 — OpenIdAuthenticationSecurityConfig&lt;/strong&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;/**
 * &amp;lt;p&amp;gt;DESC: openID认证配置类&amp;lt;/p&amp;gt;
 * &amp;lt;p&amp;gt;DATE: 2019-11-26 15:30&amp;lt;/p&amp;gt;
 * &amp;lt;p&amp;gt;VERSION:2.0.0&amp;lt;/p&amp;gt;
 * &amp;lt;p&amp;gt;@AUTHOR: ZhengYong&amp;lt;/p&amp;gt;
 */
@Component
public class OpenIdAuthenticationSecurityConfig extends SecurityConfigurerAdapter&amp;lt;DefaultSecurityFilterChain, HttpSecurity&amp;gt; {

    @Resource
    private AppSuccessHandler successHandler;

    @Resource
    private MyFailHandler failHandler;

    @Resource
    private SocialUserDetailsService socialUserDetailsService;

    @Resource
    private UsersConnectionRepository usersConnectionRepository;

    @Override
    public void configure(HttpSecurity http) throws Exception {
        OpenIdAuthenticationFilter openIdAuthenticationFilter = new OpenIdAuthenticationFilter();
        openIdAuthenticationFilter.setAuthenticationManager(http.getSharedObject(AuthenticationManager.class));
        openIdAuthenticationFilter.setAuthenticationSuccessHandler(successHandler);
        openIdAuthenticationFilter.setAuthenticationFailureHandler(failHandler);

        OpenIdAuthenticationProvider provider = new OpenIdAuthenticationProvider();
        provider.setSocialUserDetailsService(socialUserDetailsService);
        provider.setUsersConnectionRepository(usersConnectionRepository);

        http.authenticationProvider(provider)
                .addFilterAfter(openIdAuthenticationFilter, UsernamePasswordAuthenticationFilter.class);
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;加入websecurityconfigAdapter配置&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;public class AppWebSecurityConfig extends WebSecurityConfigurerAdapter {

    @Resource
    private AppSuccessHandler appSuccessHandler;

    @Resource
    private OpenIdAuthenticationSecurityConfig openIdAuthenticationSecurityConfig;

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http.csrf().disable()
                .apply(openIdAuthenticationSecurityConfig).and()
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;3 测试&lt;/h3&gt;
&lt;p&gt;&lt;img src=&quot;openid%E6%8D%A2%E5%8F%96token.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;h2&gt;二、社交登录嫁接到Security OAuth2--服务提供商SDK使用授权码模式时&lt;/h2&gt;
&lt;h3&gt;1 原理&lt;/h3&gt;
&lt;p&gt;上篇文章 —《【Spring Security OAuth开发APP认证框架】— 重构社交登陆(1) — 服务提供商SDK使用简化模式时》讲解了服务提供商(如QQ、微信等）的SDK使用简化模式时我们开发社交登陆的原理和过程。本篇来讲解一下 服务提供商的SDK使用授权码模式时，开发社交登陆的基本原理和过程。&lt;/p&gt;
&lt;p&gt;基本原理如下&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;%E9%87%8D%E6%9E%84%E7%A4%BE%E4%BA%A4%E7%99%BB%E5%BD%95%E5%8E%9F%E7%90%86.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;而我们之前实现的QQ、微信登陆的基本原理如下：&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;%E6%8E%88%E6%9D%83%E7%A0%81%E6%A8%A1%E5%BC%8F%E8%BF%90%E8%A1%8C%E6%B5%81%E7%A8%8B.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;两张图一进行对比，我们就可以很清楚的看到，在我们现有的基础上，我们要做的事主要有以下两件：&lt;/p&gt;
&lt;p&gt;开发一个APP，然后将的原来浏览器项目里的0、1、2、3这四步转移到APP项目里；
在APP接到授权码后将授权码发给我们开发的第三方应用，然后第三方应用拿着获取的授权码向服务商的认证服务器申请令牌&lt;/p&gt;
&lt;p&gt;这里0、1、2、3不具体开发了，代替做法为&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;（1）先在浏览器项目里走0、1、2、3步，然后在申请令牌之前打一个断点，获取到授权码
（2）紧接着再切到APP项目，拿着授权码去申请令牌
（3）接着会走APP项目的6、7两步
（4）然后会拿着获取到的用户信息构建Authentication对象
（5）构建成功会走APP的成功处理器，并获取到accessToken&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;其中（3）、（4）两步其实我们不用管，因为我们之前都已经开发完，第（5）步在APP项目里会有点问题，一会我们再说。接下来我们先完成（1）、（2）两步。&lt;/p&gt;
&lt;h3&gt;3 APP下指定springsocial的成功处理器使用APP的成功处理器&lt;/h3&gt;
&lt;p&gt;3.1 代码开发
spring-security的默认成功处理器为认证成功后重新调用促发请求认证的url，在这browser项目里是没问题的，但在app项目里我们是想让它认证成功后调用成功处理器返回一个token，因此可以将这两块逻辑分别处理：&lt;/p&gt;
&lt;p&gt;浏览器项目里仍然使用spring-security默认的成功处理器
app项目里调用app项目的成功处理器
具体实现如下：&lt;/p&gt;
&lt;p&gt;(1) 定义一个指定springsocial过滤器成功处理器的接口&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;/**
 * &amp;lt;p&amp;gt;DESC: 后置处理器接口&amp;lt;/p&amp;gt;
 * &amp;lt;p&amp;gt;DATE: 2019-11-26 16:21&amp;lt;/p&amp;gt;
 * &amp;lt;p&amp;gt;VERSION:2.0.0&amp;lt;/p&amp;gt;
 * &amp;lt;p&amp;gt;@AUTHOR: ZhengYong&amp;lt;/p&amp;gt;
 */
public interface SocialAuthenticationFilterPostProcessor {

    /**
     * 处理
     * @param socialAuthenticationFilter 社交登录过滤器
     */
    void processor(SocialAuthenticationFilter socialAuthenticationFilter);
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;（2）在APP项目里实现上面的接口，并指定springsocial的成功处理器为可以返回token的成功处理器&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;/**
 * &amp;lt;p&amp;gt;DESC: 后置处理器&amp;lt;/p&amp;gt;
 * &amp;lt;p&amp;gt;DATE: 2019-11-26 16:26&amp;lt;/p&amp;gt;
 * &amp;lt;p&amp;gt;VERSION:2.0.0&amp;lt;/p&amp;gt;
 * &amp;lt;p&amp;gt;@AUTHOR: ZhengYong&amp;lt;/p&amp;gt;
 */
@Component
public class AppSocialAuthenticationFilterProcessor implements SocialAuthenticationFilterPostProcessor {

    @Resource
    private AppSuccessHandler appSuccessHandler;

    @Override
    public void processor(SocialAuthenticationFilter socialAuthenticationFilter) {
        socialAuthenticationFilter.setAuthenticationSuccessHandler(appSuccessHandler);
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;(3)在配置文件里让其生效&lt;/p&gt;
&lt;p&gt;核心代码&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;//后置处理器，可配置
        if(socialAuthenticationFilterPostProcessor != null){
            socialAuthenticationFilterPostProcessor.processor(filter);
        }
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;全代码&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;/**
 * &amp;lt;p&amp;gt;DESC: 覆盖后置方法，修改拦截地址&amp;lt;/p&amp;gt;
 * &amp;lt;p&amp;gt;DATE: 2019-11-06 16:38&amp;lt;/p&amp;gt;
 * &amp;lt;p&amp;gt;VERSION:2.0.0&amp;lt;/p&amp;gt;
 * &amp;lt;p&amp;gt;@AUTHOR: ZhengYong&amp;lt;/p&amp;gt;
 */
@EqualsAndHashCode(callSuper = true)
@Data
public class MySpringSocialConfigurer extends SpringSocialConfigurer {

    private String url;
    private AuthenticationSuccessHandler handler;

    private SocialAuthenticationFilterPostProcessor socialAuthenticationFilterPostProcessor;

    public MySpringSocialConfigurer(String url, AuthenticationSuccessHandler redUrl) {
        this.url = url;
        this.handler = redUrl;
    }

    @Override
    protected &amp;lt;T&amp;gt; T postProcess(T object) {
        //在父类处理完SocialAuthenticationFilter之后的基础上，将其默认拦截url改成我们配置的值
        SocialAuthenticationFilter filter = (SocialAuthenticationFilter) super.postProcess(object);
        filter.setFilterProcessesUrl(url);
        filter.setAuthenticationSuccessHandler(handler);
        //后置处理器，可配置
        if(socialAuthenticationFilterPostProcessor != null){
            socialAuthenticationFilterPostProcessor.processor(filter);
        }
        return (T) filter;
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;social config中构建的时候传入&lt;/p&gt;
&lt;p&gt;核心代码&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;    @Autowired(required = false)
    private SocialAuthenticationFilterPostProcessor socialAuthenticationFilterPostProcessor;

    @Bean
    public SpringSocialConfigurer imoocSocialSecurityConfig() {
        // 默认配置类，进行组件的组装
        // 包括了过滤器SocialAuthenticationFilter 添加到security过滤链中
        QQProperties qq = properties.getSocial().getQq();
        MySpringSocialConfigurer config = new MySpringSocialConfigurer(qq.getFilterProcessesUrl(),qqSuccessHandler);
        config.setSocialAuthenticationFilterPostProcessor(socialAuthenticationFilterPostProcessor);
        return config;
    }
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;全代码&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;/**
 * &amp;lt;p&amp;gt;DESC: social 配置&amp;lt;/p&amp;gt;
 * &amp;lt;p&amp;gt;DATE: 2019-11-05 21:26&amp;lt;/p&amp;gt;
 * &amp;lt;p&amp;gt;VERSION:1.0.0&amp;lt;/p&amp;gt;
 * &amp;lt;p&amp;gt;@AUTHOR: ZhengYong&amp;lt;/p&amp;gt;
 */
@Slf4j
@Configuration
@EnableSocial
public class SocialConfig extends SocialConfigurerAdapter {

    @Resource
    private DataSource dataSource;


    @Resource
    private MySecurityProperties properties;

    @Autowired(required = false)
    private ConnectionSignUp connectionSignUp;

    @Resource
    private QQSuccessHandler qqSuccessHandler;

    @Autowired(required = false)
    private SocialAuthenticationFilterPostProcessor socialAuthenticationFilterPostProcessor;

    /**
     * 将我们 JdbcUsersConnectionRepository 配置好
     * @param connectionFactoryLocator locator
     * @return UsersConnectionRepository
     */
    @Override
    public UsersConnectionRepository getUsersConnectionRepository(ConnectionFactoryLocator connectionFactoryLocator) {
        JdbcUsersConnectionRepository repository = new JdbcUsersConnectionRepository(dataSource, connectionFactoryLocator, Encryptors.noOpText());
        repository.setTablePrefix(&quot;dftc_&quot;);
        if(connectionSignUp != null){
            repository.setConnectionSignUp(connectionSignUp);
        }
        return repository;
    }

    @Bean
    public SpringSocialConfigurer imoocSocialSecurityConfig() {
        // 默认配置类，进行组件的组装
        // 包括了过滤器SocialAuthenticationFilter 添加到security过滤链中
        QQProperties qq = properties.getSocial().getQq();
        MySpringSocialConfigurer config = new MySpringSocialConfigurer(qq.getFilterProcessesUrl(),qqSuccessHandler);
        config.setSocialAuthenticationFilterPostProcessor(socialAuthenticationFilterPostProcessor);
        config.signupUrl(properties.getBrowser().getSignUpUrl());
        return config;
    }

    /**
     * ProviderSignInUtils有两个作用：
     * （1）从session里获取封装了第三方用户信息的Connection对象
     * （2）将注册的用户信息与第三方用户信息（QQ信息）建立关系并将该关系插入到userconnection表里
     *
     * 需要的两个参数：
     * （1）ConnectionFactoryLocator 可以直接注册进来，用来定位ConnectionFactory
     * （2）UsersConnectionRepository----》getUsersConnectionRepository(connectionFactoryLocator)
     *     即我们配置的用来处理userconnection表的bean
     * @param connectionFactoryLocator  连接工厂加载器
     * @return  ProviderSignInUtils
     */
    @Bean
    public ProviderSignInUtils providerSignInUtils(ConnectionFactoryLocator connectionFactoryLocator) {
        return new ProviderSignInUtils(connectionFactoryLocator,
                getUsersConnectionRepository(connectionFactoryLocator)) {
        };
    }

}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;3.2 测试
重新走2中的流程，即先在browser项目里登陆获取授权码，然后停掉，再将app项目启动，然后拿着服务提供商回调的URL和我们自己的认证服务器需要携带的client-id和client-id（比如我配置的为nrsc，123456）调用我们自己的APP项目就可以获取到我们项目的认证服务器发放的accessToken了，如下图。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;断点打在：OAuth2AuthenticationService 中的 getAuthToken方法中的code换区token的位置&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;AccessGrant accessGrant = this.getConnectionFactory().getOAuthOperations().exchangeForAccess(code, returnToUrl, (MultiValueMap)null);
&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;获取到code，切换到APP模式，直接访问服务，走授权流程,成功后返回token&lt;/li&gt;
&lt;/ul&gt;
&lt;blockquote&gt;
&lt;p&gt;这里注意：这里传入clientId和secret，整个流程的request的header中都会有Authorization（clientID和secret编码后的值）。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;pre&gt;&lt;code&gt;client:secret@www.pinzhi365.com/qqLogin/callback.do?code=1995A7306F352814A4E2F4B21CF76451
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这说明当服务提供商提供的SDK使用授权码模式时，开发APP社交登陆的整个流程已经走通。&lt;/p&gt;
&lt;h2&gt;【Spring Security OAuth开发APP认证框架】--- APP认证框架下社交登陆过程中注册逻辑处理&lt;/h2&gt;
&lt;h3&gt;1、原理&lt;/h3&gt;
&lt;blockquote&gt;
&lt;p&gt;上两篇文章讲解了开发APP认证框架时社交登陆的重构，但是这种重构其实是在一个前提下的，即用户已经在我们的系统里存在本系统账号与其社交账号的绑定关系。&lt;/p&gt;
&lt;p&gt;但当这种关系不存在时，会发生什么呢？&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;在browser项目里的具体逻辑如下：&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;如果没找到社交账户与当前系统用户的绑定关系会有两种处理方式
处理方式（1）
—》先将从社交账户获得的用户信息封装的Connection对象存到session
—》然后将用户引导到一个注册或绑定用户的url（一般会对应注册、绑定页面），在注册或绑定页面可以从session里取出社交账户的头像，名称等信息展示给前端，并提醒用户注册或绑定账户
—》用户输入要注册或绑定的账户点击注册或绑定时，将输入的账户与session中的Connection对象进行绑定插入到本系统账号与社交账号绑定关系表—userconnection表
处理方式（2）
可以配置一个注册类(重写ConnectionSignUp中的execute方法即可)，直接给其默认注册一个本系统账户，并将两者的绑定关系插入userconnection表。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;在APP项目里处理方式（1）是有问题的，原因是处理方式（1）是基于session的，但是&lt;code&gt;APP的每次访问都是在一个新的session里&lt;/code&gt;。&lt;/p&gt;
&lt;p&gt;解决思路如下：&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;(1) 如果没找到社交账户与当前系统用户的绑定关系
(2) 先将从社交账户获得的用户信息封装的Connection对象存到session
(3) 将用户引导到一个注册或绑定用户的url — 后端服务，在该服务里先从session里取出Connection对象然后将其转存到redis或其他数据库，然后将社交登陆失败的原因返回给APP
(4) APP拿到（3）返回的信息，引导用户到注册或绑定页面
(5) 用户输入要注册或绑定的账户点击注册或绑定时，将输入的账户与redis中存储的相对应的Connection对象进行绑定并插入到本系统账号与社交账号绑定关系表—userconnection表&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h3&gt;2、具体实现&lt;/h3&gt;
&lt;h3&gt;2.1 (1)-(3)步代码开发&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;之前没找到社交账户与当前系统用户绑定关系时跳向注册逻辑的url配置如下：&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;    /**
     * 通过apply()该实例，可以将SocialAuthenticationFilter加入到spring-security的过滤器链
     */
    @Bean
    public SpringSocialConfigurer nrscSocialSecurityConfig() {
        // 默认配置类，进行组件的组装
        // 包括了过滤器SocialAuthenticationFilter 添加到security过滤链中
        String filterProcessesUrl = nrscSecurityProperties.getSocial().getFilterProcessesUrl();
        NrscSpringSocialConfigurer configurer = new NrscSpringSocialConfigurer(filterProcessesUrl);

        //指定SpringSocial/SpringSecurity跳向注册页面时的url为我们配置的url
        configurer.signupUrl(nrscSecurityProperties.getBrowser().getSignUpUrl());

        //设置springsocial的认证成功处理器 -- app下可以返回token，browser下使用spring-security默认的
        configurer.setSocialAuthenticationFilterPostProcessor(socialAuthenticationFilterPostProcessor);
        return configurer;
    }
&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;在browser项目下这个配置没有问题的，在不修改上面逻辑的情况下，可以在APP模块里实现BeanPostProcessor接口来修改该bean的signupUrl&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;package com.nrsc.security.app;
import com.nrsc.security.core.social.NrscSpringSocialConfigurer;
import org.apache.commons.lang3.StringUtils;
import org.springframework.beans.BeansException;
import org.springframework.beans.factory.config.BeanPostProcessor;
import org.springframework.stereotype.Component;
@Component
public class SpringSocialConfigurerPostProcessor implements BeanPostProcessor {

    /***
     * spring启动时所有的bean初始化之前都会调用该方法 --- 可以在bean初始化之前对bean做一些操作
     * @param bean
     * @param beanName
     * @return
     * @throws BeansException
     */
    @Override
    public Object postProcessBeforeInitialization(Object bean, String beanName) throws BeansException {
        return bean;
    }

    /***
     * spring启动时所有的bean初始化之后都会调用该方法 --- 可以对初始化好的bean做一些修改
     * @param bean
     * @param beanName
     * @return
     * @throws BeansException
     */
    @Override
    public Object postProcessAfterInitialization(Object bean, String beanName) throws BeansException {
        if(StringUtils.equals(beanName, &quot;nrscSocialSecurityConfig&quot;)){
            NrscSpringSocialConfigurer config = (NrscSpringSocialConfigurer)bean;
            config.signupUrl(&quot;/social/signUp&quot;);
            return config;
        }
        return bean;
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;对应解决思路中的第三步&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;package com.nrsc.security.app.controller;

import com.nrsc.security.core.utils.AppSignUpUtils;
import com.nrsc.security.core.pojo.SocialUserInfo;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.HttpStatus;
import org.springframework.social.connect.Connection;
import org.springframework.social.connect.web.ProviderSignInUtils;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.ResponseStatus;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.context.request.ServletWebRequest;

import javax.servlet.http.HttpServletRequest;

/**
 * @author : Sun Chuan
 * @date : 2019/10/22 22:51
 * Description：将社交账户从session转存到redis，并返回给APP一个
 * 社交登陆失败的原因+社交账户信息（便于APP前端显示当前社交账户的用户名、头像等信息）
 */
@RestController
public class AppSecurityController {

    @Autowired
    private ProviderSignInUtils providerSignInUtils;

    //将社交账户信息转存到redis和从redis取出社交账户信息的工具类
    @Autowired
    private AppSignUpUtils appSignUpUtils;

    @GetMapping(&quot;/social/signUp&quot;)
    @ResponseStatus(HttpStatus.UNAUTHORIZED)
    public SocialUserInfo getSocialUserInfo(HttpServletRequest request) {
        SocialUserInfo userInfo = new SocialUserInfo();
        //从session中取出已经认证的社交账户信息（Connection对象）
        //--&amp;gt;本接口的执行和微信、QQ的等服务提供商拿着授权码回调我们的项目在一个session里，因此这里可以从session里取出社交账户信息
        Connection&amp;lt;?&amp;gt; connection = providerSignInUtils.getConnectionFromSession(new ServletWebRequest(request));
        userInfo.setProviderId(connection.getKey().getProviderId());
        userInfo.setProviderUserId(connection.getKey().getProviderUserId());
        userInfo.setNickName(connection.getDisplayName());
        userInfo.setHeadImg(connection.getImageUrl());

        //从session里将社交账户信息取出并转存到redis
        appSignUpUtils.saveConnectionData(new ServletWebRequest(request), connection.createData());
        return userInfo;
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;上面类中AppSingUpUtils — 将社交账户信息转存到redis和从redis取出社交账户信息的工具类，代码如下：&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;package com.nrsc.security.app.utils;
import com.nrsc.security.app.exception.AppSecretException;
import org.apache.commons.lang3.StringUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.social.connect.Connection;
import org.springframework.social.connect.ConnectionData;
import org.springframework.social.connect.ConnectionFactoryLocator;
import org.springframework.social.connect.UsersConnectionRepository;
import org.springframework.stereotype.Component;
import org.springframework.web.context.request.WebRequest;
import java.util.concurrent.TimeUnit;
@Component
public class AppSingUpUtils {

    @Autowired
    private RedisTemplate&amp;lt;Object, Object&amp;gt; redisTemplate;

    @Autowired
    private UsersConnectionRepository usersConnectionRepository;

    @Autowired
    private ConnectionFactoryLocator connectionFactoryLocator;

    /***
     * 将社交账户信息存到redis
     * @param request
     * @param connectionData
     */
    public void saveConnectionData(WebRequest request, ConnectionData connectionData) {
        redisTemplate.opsForValue().set(getKey(request), connectionData, 10, TimeUnit.MINUTES);
    }

    /***
     * 从redis取出社交账户信息，并将其与本系统账户建立关联，并将关联关系存到数据库---userconnection表
     * @param request
     * @param userId
     */
    public void doPostSignUp(WebRequest request, String userId) {
        String key = getKey(request);
        if(!redisTemplate.hasKey(key)){
            throw new AppSecretException(&quot;无法找到缓存的用户社交账号信息&quot;);
        }
        ConnectionData connectionData = (ConnectionData) redisTemplate.opsForValue().get(key);
        Connection&amp;lt;?&amp;gt; connection = connectionFactoryLocator.getConnectionFactory(connectionData.getProviderId())
                .createConnection(connectionData);
        usersConnectionRepository.createConnectionRepository(userId).addConnection(connection);

        redisTemplate.delete(key);
    }

    /***
     * 根据请求头中的deviceId拼接一个存到redis中的key
     * @param request
     * @return
     */
    private String getKey(WebRequest request) {
        String deviceId = request.getHeader(&quot;deviceId&quot;);
        if (StringUtils.isBlank(deviceId)) {
            throw new AppSecretException(&quot;设备id参数不能为空&quot;);
        }
        return &quot;nrsc:security:social.connect.&quot; + deviceId;
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;2.2 测试&lt;/h3&gt;
&lt;blockquote&gt;
&lt;p&gt;测试内容：社交登陆 —&amp;gt; 指引用户进行绑定或注册 —》注册 整个流程&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;（1）修改userconnection表，去掉我微信账户与本系统账户的关联关系&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;%E7%95%99%E4%B8%8B%E7%94%A8%E6%88%B7%E4%BF%A1%E6%81%AF.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;（2）像上篇文章一样，先在browser项目里获取到微信带着授权码回调我们项目的请求
（3）切回到app项目，先重写一下注册逻辑 —》 该接口免授权&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;package com.nrsc.security.controller;
import com.nrsc.security.app.utils.AppSignUpUtils;
import com.nrsc.security.domain.NrscUser;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.annotation.AuthenticationPrincipal;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.social.connect.web.ProviderSignInUtils;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.context.request.ServletWebRequest;

import javax.servlet.http.HttpServletRequest;

@RestController
@RequestMapping(&quot;/user&quot;)
public class UserController {

    @Autowired
    private ProviderSignInUtils providerSignInUtils;

    @Autowired
    private AppSignUpUtils appSignUpUtils;
    
    @PostMapping(&quot;/register&quot;) 
    public void register(NrscUser user, HttpServletRequest request) {
        //注册用户相关逻辑-----》即向用户表里插入一条用户数据-----》这里不写了

        //不管是注册用户还是绑定用户，都会拿到一个用户唯一标识，如用户名。
        String userId = user.getUsername();
        //将用户userId和第三方用户信息建立关系并将其插入到userconnection表
        //providerSignInUtils.doPostSignUp(userId, new ServletWebRequest(request));
        
        //使用我们自己的utils将用户userId和第三方用户信息建立关系、将该关系插入到userconnection表
        //和删掉redis中保存的deviceId信息
        appSignUpUtils.doPostSignUp(new ServletWebRequest(request), userId);
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;（4）启动APP项目，拿着（2）中获得的请求访问app项目 —》 由于找不到本系统账户与我微信账户的绑定关系，会重定向到我们指定的signupUrl,但是&lt;code&gt;可能是&lt;/code&gt;我们的请求与微信回调我们的请求不是一个request对象的原因，会出现一个回调异常，如下图：&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;%E5%9B%9E%E8%B0%83%E5%BC%82%E5%B8%B8.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;(5)我们手动调一下/social/signUp请求，获取到社交账户信息----》这个信息会返回给APP，APP根据这个请求将用户引到到注册或绑定页面。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;%E8%8E%B7%E5%8F%96%E5%88%B0%E8%B4%A6%E6%88%B7%E4%BF%A1%E6%81%AF.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;(6)然后进行用户注册或绑定—》下图调用了上面说的注册接口：&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;%E8%B0%83%E7%94%A8%E6%B3%A8%E5%86%8C%E6%8E%A5%E5%8F%A3.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;在注册成功后，可以看到在数据库的userconnection表里新插入了一条yoyo与我微信的绑定关系：&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;上诉测试表明1中所说得处理方式（1）所面临的问题，在我们开发的APP认证框架里已经得到了解决。&lt;/strong&gt;&lt;/p&gt;
&lt;h2&gt;客户端信息+token信息 数据库(mysql/redis)存储&lt;/h2&gt;
&lt;h3&gt;1、将客户端信息保存到mysql( 推荐)&lt;/h3&gt;
&lt;h3&gt;1.1 建表+代码开发&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;建表&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;create table oauth_client_details (
  client_id VARCHAR(256) PRIMARY KEY,
  resource_ids VARCHAR(256),
  client_secret VARCHAR(256),
  scope VARCHAR(256),
  authorized_grant_types VARCHAR(256),
  web_server_redirect_uri VARCHAR(256),
  authorities VARCHAR(256),
  access_token_validity INTEGER,
  refresh_token_validity INTEGER,
  additional_information VARCHAR(4096),
  autoapprove VARCHAR(256)
);
&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;数据库里做一些简单的配置&lt;/li&gt;
&lt;li&gt;这样客户端的配置就由上面代码中的一坨变成了下面这样，且如果增加一个客户端或减少一个客户端并不用修改代码，直接删除或增加数据库里的一条数据就可以了&lt;/li&gt;
&lt;li&gt;内存中配置如下&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;/**
 * &amp;lt;p&amp;gt;DESC: 服务配置&amp;lt;/p&amp;gt;
 * &amp;lt;p&amp;gt;DATE: 2019-11-19 14:26&amp;lt;/p&amp;gt;
 * &amp;lt;p&amp;gt;VERSION:2.0.0&amp;lt;/p&amp;gt;
 * &amp;lt;p&amp;gt;@AUTHOR: ZhengYong&amp;lt;/p&amp;gt;
 */
@Configuration
@EnableAuthorizationServer
public class ServiceConfig extends AuthorizationServerConfigurerAdapter {


    @Override
    public void configure(ClientDetailsServiceConfigurer clients) throws Exception {
        clients.inMemory().withClient(&quot;client&quot;)
                .secret(&quot;secret&quot;)
                .scopes(&quot;app&quot;)
          	   //获取code后重定向地址
                .redirectUris(&quot;http://www.baidu.com&quot;)
                .authorizedGrantTypes(&quot;authorization_code&quot;);
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;修改为数据库保存&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;    /***
     * 第三方客户端相关的配置在这里进行配置 ,之前我们在yml配置文件里对客户端进行过简单的配置
     * 在这里进行配置会覆盖yml中的配置
     * @param clients
     * @throws Exception
     */
    @Override
    public void configure(ClientDetailsServiceConfigurer clients) throws Exception {
        //直接将dataSource给clients就行了 --- jdbc会直接去库里拿客户端信息 
        clients.jdbc(dataSource);
    }
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;2、token信息由存储在内存改为存到redis数据库&lt;/h3&gt;
&lt;h3&gt;2.1 代码实现&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;资源服务器配置类和具体解释看如下代码：&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;核心代码&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;        endpoints
                //指定使用的TokenStore，tokenStore用来存取token，默认使用InMemoryTokenStore
                //这里使用redis存储token
                .tokenStore(redisTokenStore)
        ;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;完整代码：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;/**
 * &amp;lt;p&amp;gt;DESC: 服务配置&amp;lt;/p&amp;gt;
 * &amp;lt;p&amp;gt;DATE: 2019-11-19 14:26&amp;lt;/p&amp;gt;
 * &amp;lt;p&amp;gt;VERSION:2.0.0&amp;lt;/p&amp;gt;
 * &amp;lt;p&amp;gt;@AUTHOR: ZhengYong&amp;lt;/p&amp;gt;
 */
@Configuration
@EnableAuthorizationServer
public class ServiceConfig extends AuthorizationServerConfigurerAdapter {


    private final DataSource dataSource;

    private final TokenStore redisTokenStore;


    @Autowired
    public ServiceConfig(DataSource dataSource, TokenStore redisTokenStore) {
        this.dataSource = dataSource;
        this.redisTokenStore = redisTokenStore;
    }


    @Override
    public void configure(ClientDetailsServiceConfigurer clients) throws Exception {
        clients.jdbc(dataSource);
    }

    @Override
    public void configure(AuthorizationServerEndpointsConfigurer endpoints) throws Exception {
        endpoints
                //指定使用的TokenStore，tokenStore用来存取token，默认使用InMemoryTokenStore
                //这里使用redis存储token
                .tokenStore(redisTokenStore)
        ;
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;token配置&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;/**
 * &amp;lt;p&amp;gt;DESC: token存储配置&amp;lt;/p&amp;gt;
 * &amp;lt;p&amp;gt;DATE: 2019-12-02 14:56&amp;lt;/p&amp;gt;
 * &amp;lt;p&amp;gt;VERSION:2.0.0&amp;lt;/p&amp;gt;
 * &amp;lt;p&amp;gt;@AUTHOR: ZhengYong&amp;lt;/p&amp;gt;
 */
@Configuration
public class TokenStoreConfig {

    private final RedisConnectionFactory redisConnectionFactory;

    /***
     * RedisTokenStore需要一个连接工厂，这里可以直接注入进来
     */
    @Autowired
    public TokenStoreConfig(RedisConnectionFactory redisConnectionFactory) {
        this.redisConnectionFactory = redisConnectionFactory;
    }

    /***
     * 将RedisTokenStore注入到spring容器
     * @return tokenStore
     */
    @Bean
    public TokenStore redisTokenStore(){
        return new RedisTokenStore(redisConnectionFactory);
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;3 将token保存到mysql&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;建表sql — 具体字段含义可以去看这篇文章《&lt;a href=&quot;https://blog.csdn.net/qq_34997906/article/details/89609297&quot;&gt;OAuth2相关数据表字段的详细说明&lt;/a&gt;》&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;create table oauth_client_token (
  token_id VARCHAR(256),
  token BLOB,
  authentication_id VARCHAR(256) PRIMARY KEY,
  user_name VARCHAR(256),
  client_id VARCHAR(256)
);

create table oauth_access_token (
  token_id VARCHAR(256),
  token BLOB,
  authentication_id VARCHAR(256) PRIMARY KEY,
  user_name VARCHAR(256),
  client_id VARCHAR(256),
  authentication BLOB,
  refresh_token VARCHAR(256)
);

create table oauth_refresh_token (
  token_id VARCHAR(256),
  token BLOB,
  authentication BLOB
);

create table oauth_code (
  code VARCHAR(256), authentication BLOB
);

create table oauth_approvals (
	userId VARCHAR(256),
	clientId VARCHAR(256),
	scope VARCHAR(256),
	status VARCHAR(10),
	expiresAt DATETIME,
	lastModifiedAt DATETIME
);
&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;配置JdbcTokenStore并利用JdbcTokenStore替换RedisTokenStore&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;    /***
     * 将jdbcTokenStore注入到spring容器
     * @return tokenStore
     */
    @Bean
    public TokenStore jdbcTokenStore(){
        return new JdbcTokenStore(dataSource);
    }
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;    @Override
    public void configure(AuthorizationServerEndpointsConfigurer endpoints) throws Exception {
        endpoints
                //指定使用的TokenStore，tokenStore用来存取token，默认使用InMemoryTokenStore
                //这里使用redis存储token
                .tokenStore(jdbcTokenStore)
        ;
    }
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;使用JWT替换默认令牌&lt;/h2&gt;
&lt;h3&gt;1 JWT特点&lt;/h3&gt;
&lt;blockquote&gt;
&lt;p&gt;（1）自包含 — ★ JWT里面包含信息
（2）可扩展 — 即JWT生成时会包含一些默认信息，我们可以再往JWT里加入一些额外的信息
（3）密签 — 在生成JWT时指定一个密钥，然后校验时再拿着这个密钥进行校验（看了源码后可以理解的更清楚）&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h3&gt;2 使用JWT替换默认令牌&lt;/h3&gt;
&lt;h3&gt;2.1 代码开发&lt;/h3&gt;
&lt;p&gt;主要包括：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;配置TokenStore&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;TokenStore只负责token的存取，存到redis用到的TokenStore的实现类为RedisTokenStore，存到mysql用到的实现类为JdbcTokenStore。。。生成的token为JWT时要使用JwtTokenStore —》其实JwtTokenStore对对存、取token的操作就是啥也不做，因为jwt的自包含特性，服务端根本没必要去存储token。— 有兴趣的可以追踪一下源码
&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;配置JwtAccessTokenConverter — 真正生产JWT的类&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;JwtAccessTokenConverter 其实是一个TokenEnhancer。 通过阅读源码可知:TokenEnhancer是对生成的Token进行后续处理的（或者说是对Token进行增强的） 其实JwtAccessTokenConverter就是将默认生成的token做进一步处理使其成为一个JWT。
&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;将JwtTokenStore和JwtAccessTokenConverter设置到token的生成类中&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;具体代码如下：
（1）配置TokenStore和JwtAccessTokenConverter&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;/**
 * &amp;lt;p&amp;gt;DESC: token存储配置&amp;lt;/p&amp;gt;
 * &amp;lt;p&amp;gt;DATE: 2019-12-02 14:56&amp;lt;/p&amp;gt;
 * &amp;lt;p&amp;gt;VERSION:2.0.0&amp;lt;/p&amp;gt;
 * &amp;lt;p&amp;gt;@AUTHOR: ZhengYong&amp;lt;/p&amp;gt;
 */
@Configuration
public class TokenStoreConfig {

    private final RedisConnectionFactory redisConnectionFactory;

    private final DataSource dataSource;

    /***
     * RedisTokenStore需要一个连接工厂，这里可以直接注入进来
     */
    @Autowired
    public TokenStoreConfig(RedisConnectionFactory redisConnectionFactory,DataSource dataSource) {
        this.redisConnectionFactory = redisConnectionFactory;
        this.dataSource = dataSource;
    }

    /***
     * 将RedisTokenStore注入到spring容器
     * @return tokenStore
     */
    @Bean
    @ConditionalOnProperty(prefix = &quot;my-security.oauth2&quot;, name = &quot;tokenStore&quot;, havingValue = &quot;redis&quot;)
    public TokenStore redisTokenStore() {
        return new RedisTokenStore(redisConnectionFactory);
    }

    /***
     * 将jdbcTokenStore注入到spring容器
     * @return tokenStore
     */
    @Bean
    @ConditionalOnProperty(prefix = &quot;my-security.oauth2&quot;, name = &quot;tokenStore&quot;, havingValue = &quot;jdbc&quot;)
    public TokenStore jdbcTokenStore() {
        return new JdbcTokenStore(dataSource);
    }

    /*** 
     * 当yml配置文件里配置了my-security.oauth2.tokenStore = jwt 或者 根本就没配置该属性时 ---&amp;gt; 下面的配置生效
     */
    @ConditionalOnProperty(prefix = &quot;my-security.oauth2&quot;, name = &quot;tokenStore&quot;, havingValue = &quot;jwt&quot;, matchIfMissing = true)
    @Configuration
    public static class JwtConfig {

        @Resource
        private MySecurityProperties properties;

        @Bean
        public TokenStore tokenStore() {
            return new JwtTokenStore(jwtTokenEnhancer());
        }

        @Bean
        public JwtAccessTokenConverter jwtTokenEnhancer() {
            JwtAccessTokenConverter converter = new JwtAccessTokenConverter();
            converter.setSigningKey(properties.getOauth2().getJwtSignKey());
            return converter;
        }
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;（2）将JwtTokenStore和JwtAccessTokenConverter设置到token的生成类中&lt;/p&gt;
&lt;p&gt;核心代码&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;    private final TokenStore tokenStore;

    private final JwtAccessTokenConverter jwtAccessTokenConverter;

        endpoints
                //指定使用的TokenStore，tokenStore用来存取token，默认使用InMemoryTokenStore
                //这里使用redis存储token
                .tokenStore(tokenStore)
        ;
        if(jwtAccessTokenConverter != null){
            endpoints.accessTokenConverter(jwtAccessTokenConverter);
        }
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;完整代码：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;/**
 * &amp;lt;p&amp;gt;DESC: 服务配置&amp;lt;/p&amp;gt;
 * &amp;lt;p&amp;gt;DATE: 2019-11-19 14:26&amp;lt;/p&amp;gt;
 * &amp;lt;p&amp;gt;VERSION:2.0.0&amp;lt;/p&amp;gt;
 * &amp;lt;p&amp;gt;@AUTHOR: ZhengYong&amp;lt;/p&amp;gt;
 */
@Configuration
@EnableAuthorizationServer
public class ServiceConfig extends AuthorizationServerConfigurerAdapter {


    private final DataSource dataSource;

    private final TokenStore redisTokenStore;

    private final TokenStore jdbcTokenStore;

    private final TokenStore tokenStore;

    private final JwtAccessTokenConverter jwtAccessTokenConverter;


    @Autowired(required = false)
    public ServiceConfig(DataSource dataSource, TokenStore redisTokenStore,TokenStore jdbcTokenStore,JwtAccessTokenConverter jwtAccessTokenConverter,TokenStore tokenStore) {
        this.dataSource = dataSource;
        this.redisTokenStore = redisTokenStore;
        this.jdbcTokenStore = jdbcTokenStore;
        this.jwtAccessTokenConverter = jwtAccessTokenConverter;
        this.tokenStore = tokenStore;
    }


    @Override
    public void configure(ClientDetailsServiceConfigurer clients) throws Exception {
        clients.jdbc(dataSource);
    }

    @Override
    public void configure(AuthorizationServerEndpointsConfigurer endpoints) throws Exception {
        endpoints
                //指定使用的TokenStore，tokenStore用来存取token，默认使用InMemoryTokenStore
                //这里使用redis存储token
                .tokenStore(tokenStore)
        ;
        if(jwtAccessTokenConverter != null){
            endpoints.accessTokenConverter(jwtAccessTokenConverter);
        }
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;2.2 测试&lt;/h3&gt;
&lt;p&gt;（1）获取token&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;%E8%8E%B7%E5%8F%96token.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;（2）拿着token请求用户信息&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;通过JWT解析到的Authentication对象 (我代码里的/user/me1和/user/me2是获取Authentication对象)如下。其中principal为一个字符串，但是之前无论用户名+密码模式、短息登陆还是社交登陆过程中生成的Authentication对象里的principal都是一个UserDetails对象。— 》 这是一个很多的区别。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;（3）正是由于通过JWT生成的Authentication对象里的principal为一个字符串而不是UserDetails对象的原因，请求下面这个接口将获取不到任何信息&lt;/p&gt;
&lt;pre&gt;&lt;code&gt; @GetMapping(&quot;/me3&quot;)
 public Object getCurrentUser3(@AuthenticationPrincipal UserDetails user) {
     //方式3，只获取User对象
     return user;
 }
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;（4）但是下面这样可以&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;    /***
     * JWT 情况下获取的principal是一个字符串
     * @param user
     * @return
     */
    @GetMapping(&quot;/me4&quot;)
    public Object getCurrentUser4(@AuthenticationPrincipal String user) {
        //方式3，只获取User对象
        return user;
    }
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;JWT生成源码解读 + JWT扩展&lt;/h2&gt;
&lt;h3&gt;1 JWT生成源码解读&lt;/h3&gt;
&lt;h4&gt;1.1 spring security oauth生成token核心源码&lt;/h4&gt;
&lt;p&gt;&lt;strong&gt;所在类DefaultTokenServices&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;private OAuth2AccessToken createAccessToken(OAuth2Authentication authentication, OAuth2RefreshToken refreshToken) {
	//利用UUID当成构造函数的参数生成一个令牌
	DefaultOAuth2AccessToken token = new DefaultOAuth2AccessToken(UUID.randomUUID().toString());
	//获取过期时间并设置到token里
	int validitySeconds = getAccessTokenValiditySeconds(authentication.getOAuth2Request());
	if (validitySeconds &amp;gt; 0) {
		token.setExpiration(new Date(System.currentTimeMillis() + (validitySeconds * 1000L)));
	}
	//设置刷新令牌
	token.setRefreshToken(refreshToken);
	//设置令牌的权限
	token.setScope(authentication.getOAuth2Request().getScope());
	//看是否配置了AccessTokenEnhancer的具体实现
	//如果配置了对生成的token进行增强 --- 设置Jwt时会讲
	return accessTokenEnhancer != null ? accessTokenEnhancer.enhance(token, authentication) : token;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;从这段代码可知spring security oauth默认生成的token其实是利用UUID进行构造生成的，那JWT到底是怎么生成的呢？其实就隐藏在那个return语句里，也就是说JWT的真正生成其实是在accessTokenEnhancer.enhance(token, authentication) 方法里。&lt;/li&gt;
&lt;li&gt;上面讲到我们向token生成类中配置了一个JwtAccessTokenConverter，而它其实就是一个TokenEnhancer（即上面代码中的accessTokenEnhancer ），其作用正是调用accessTokenEnhancer.enhance(token, authentication) 方法生成一个JWT。&lt;/li&gt;
&lt;/ul&gt;
&lt;h4&gt;1.2 JWT生成的核心源码&lt;/h4&gt;
&lt;p&gt;下面我们来看一下JWT生成的具体代码：
&lt;strong&gt;所在类JwtAccessTokenConverter&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;public OAuth2AccessToken enhance(OAuth2AccessToken accessToken, OAuth2Authentication authentication) {
	//构建jwt情况下真正返回的token对象
	DefaultOAuth2AccessToken result = new DefaultOAuth2AccessToken(accessToken);
	Map&amp;lt;String, Object&amp;gt; info = new LinkedHashMap&amp;lt;String, Object&amp;gt;(accessToken.getAdditionalInformation());
	//将默认生成的access_token取出，并设置到result
	String tokenId = result.getValue();
	if (!info.containsKey(TOKEN_ID)) {
		info.put(TOKEN_ID, tokenId);
	}
	else {
		tokenId = (String) info.get(TOKEN_ID);
	}
	result.setAdditionalInformation(info);
	//真正的生成jwt中的access_token ---》下面会继续讲解 
	result.setValue(encode(result, authentication));
	//下面这块代码是对refreshToken 的处理---不细讲了
	OAuth2RefreshToken refreshToken = result.getRefreshToken();
	if (refreshToken != null) {
		DefaultOAuth2AccessToken encodedRefreshToken = new DefaultOAuth2AccessToken(accessToken);
		encodedRefreshToken.setValue(refreshToken.getValue());
		// Refresh tokens do not expire unless explicitly of the right type
		encodedRefreshToken.setExpiration(null);
		try {
			Map&amp;lt;String, Object&amp;gt; claims = objectMapper
					.parseMap(JwtHelper.decode(refreshToken.getValue()).getClaims());
			if (claims.containsKey(TOKEN_ID)) {
				encodedRefreshToken.setValue(claims.get(TOKEN_ID).toString());
			}
		}
		catch (IllegalArgumentException e) {
		}
		Map&amp;lt;String, Object&amp;gt; refreshTokenInfo = new LinkedHashMap&amp;lt;String, Object&amp;gt;(
				accessToken.getAdditionalInformation());
		refreshTokenInfo.put(TOKEN_ID, encodedRefreshToken.getValue());
		refreshTokenInfo.put(ACCESS_TOKEN_ID, tokenId);
		encodedRefreshToken.setAdditionalInformation(refreshTokenInfo);
		//调用encode方法生成refresh-token
		DefaultOAuth2RefreshToken token = new DefaultOAuth2RefreshToken(
				encode(encodedRefreshToken, authentication));
		//如果配置了refresh-token的过期时间则重新生成一个带过期时间的refresh-token
		if (refreshToken instanceof ExpiringOAuth2RefreshToken) {
			Date expiration = ((ExpiringOAuth2RefreshToken) refreshToken).getExpiration();
			encodedRefreshToken.setExpiration(expiration);
			//真正设置refresh-token的过期时间
			token = new DefaultExpiringOAuth2RefreshToken(encode(encodedRefreshToken, authentication), expiration);
		}
		result.setRefreshToken(token);
	}
	return result;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h4&gt;1.3 JWT自包含+密签源码&lt;/h4&gt;
&lt;blockquote&gt;
&lt;p&gt;encode的具体代码 — 将密钥和一些用户信息、第三方信息、过期时间等放到一起进行编码生成一个字符串
★ 这就是为什么JWT会包含信息的原因 — 当然从这里也可以看出其实所谓的密签也没有什么神奇的。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;&lt;strong&gt;所在类JwtAccessTokenConverter&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;protected String encode(OAuth2AccessToken accessToken, OAuth2Authentication authentication) {
	String content;
	try {
		//将access-token的过期时间、正在授权的用户的用户名、第三方应用的client-id等转成一个json字符串
		content = objectMapper.formatMap(tokenConverter.convertAccessToken(accessToken, authentication));
	}
	catch (Exception e) {
		throw new IllegalStateException(&quot;Cannot convert access token to JSON&quot;, e);
	}
	//★ 利用我们设置的密签signer和content进行编码生成access-token（有兴趣的可以跟踪一下更详细的代码）
	String token = JwtHelper.encode(content, signer).getEncoded();
	return token;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;2 JWT扩展 — 往JWT里加入附加信息&lt;/h3&gt;
&lt;h4&gt;2.1 代码开发&lt;/h4&gt;
&lt;p&gt;上面讲了JWT的三个特性，通过读源码已经了解了自包含+密签的原理，接下来在讲一下如何对JWT进行扩展。
主要需要三步：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;自定义一个TokenEnhancer&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;/**
 * &amp;lt;p&amp;gt;DESC: jwt增强器&amp;lt;/p&amp;gt;
 * &amp;lt;p&amp;gt;DATE: 2019-12-03 17:04&amp;lt;/p&amp;gt;
 * &amp;lt;p&amp;gt;VERSION:2.0.0&amp;lt;/p&amp;gt;
 * &amp;lt;p&amp;gt;@AUTHOR: ZhengYong&amp;lt;/p&amp;gt;
 */
@Component
public class MyJwtTokenEnhancer implements TokenEnhancer{


    @Override
    public OAuth2AccessToken enhance(OAuth2AccessToken accessToken, OAuth2Authentication authentication) {
        HashMap&amp;lt;String, Object&amp;gt; hashMap = new HashMap&amp;lt;&amp;gt;();
        hashMap.put(&quot;chinese&quot;,&quot;xiaoming&quot;);
        ((DefaultOAuth2AccessToken)accessToken).setAdditionalInformation(hashMap);
        return accessToken;
    }


}
&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;将自定义的TokenEnhancer注入到spring容器&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt; /***
  * 将自定义的TokenEnhancer注入到spring容器 --- 》可以覆盖该bean，实现自己的需求
  * @return
  */
 @Bean
 @ConditionalOnBean(TokenEnhancer.class)
 public TokenEnhancer jwtTokenEnhancer() {
     return new NrscJwtTokenEnhancer();
 }
&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;将自定义的TokenEnhancer设置到token的生成类中&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;@Override
public void configure(AuthorizationServerEndpointsConfigurer endpoints) throws Exception {
    endpoints
            //指定使用的TokenStore，tokenStore用来存取token，默认使用InMemoryTokenStore
            .tokenStore(tokenStore)
            //下面的配置主要用来指定&quot;对正在进行授权的用户进行认证+校验&quot;的类
            //在实现了AuthorizationServerConfigurerAdapter适配器类后，必须指定下面两项
            .authenticationManager(authenticationManager)
            .userDetailsService(NRSCDetailsService);

    if (jwtAccessTokenConverter != null &amp;amp;&amp;amp; jwtTokenEnhancer != null) {
        //配置增强器链
        // 并利用增强器链将生成jwt的TokenEnhancer（jwtAccessTokenConverter）
        // 和我们扩展的TokenEnhancer设置到token的生成类中
        TokenEnhancerChain enhancerChain = new TokenEnhancerChain();
        List&amp;lt;TokenEnhancer&amp;gt; enhancers = new ArrayList&amp;lt;&amp;gt;();
      //注意调用顺序，一定是，先增强，然后再，转化（编码）
        enhancers.add(jwtTokenEnhancer);
        enhancers.add(jwtAccessTokenConverter);
        enhancerChain.setTokenEnhancers(enhancers);
        endpoints.tokenEnhancer(enhancerChain);
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;3 JWT扩展 — 后端从JWT里取出附加信息&lt;/h3&gt;
&lt;blockquote&gt;
&lt;p&gt;通过JWT解析获得的Authentication对象里并不会包含我们自定义的扩展信息
但我们可以借助工具类自己解析JWT获取到&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;主要步骤如下：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;加入依赖&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;&amp;lt;dependency&amp;gt;
       &amp;lt;groupId&amp;gt;io.jsonwebtoken&amp;lt;/groupId&amp;gt;
       &amp;lt;artifactId&amp;gt;jjwt&amp;lt;/artifactId&amp;gt;
       &amp;lt;version&amp;gt;0.9.1&amp;lt;/version&amp;gt;
&amp;lt;/dependency&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;从请求头中获取到JWT，并利用Jwts工具借助&lt;code&gt;密钥&lt;/code&gt;解析获得到扩展信息&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;    @GetMapping(&quot;/me2&quot;)
    public Object getCurrentUser2(Authentication authentication, HttpServletRequest request) throws UnsupportedEncodingException {
        //方式2---方式1的简写版
        //从请求头中获取到JWT
        String token = StringUtils.substringAfter(request.getHeader(&quot;Authorization&quot;), &quot;bearer &quot;);
        //借助密钥对JWT进行解析,注意由于JWT生成时编码格式用的UTF-8（看源码可以追踪到）
        //但Jwts工具用到的默认编码格式不是，所以要设置其编码格式为UTF-8
        Claims claims = Jwts.parser()
                .setSigningKey(mySecurityProperties.getOauth2().getJwtSignKey().getBytes(&quot;UTF-8&quot;))
                .parseClaimsJws(token).getBody();
        //取出扩展信息，并打印
        String company = (String) claims.get(&quot;chinese&quot;);

        System.err.println(company);
        return authentication;
    }
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;4、refresh - token的使用&lt;/h3&gt;
&lt;blockquote&gt;
&lt;p&gt;refresh-token一般设置的时间较长 —》比如说一个星期。它主要的用途在4里已经讲到，即在access-token过期后让客户端通过refresh-token无感知的获取到一个新的令牌，其请求路径和请求参数如下：&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;&lt;img src=&quot;refresh_token%E6%9C%BA%E5%88%B6.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;注意：项目中需要以下注意的点&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;访问该路径，报  Encoded password does not look like BCrypt&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;问题出在：&lt;/p&gt;
&lt;p&gt;刷新token去请求的时候，会调用配置的密码器去匹配，之前的client_secret 是没有加密的&lt;/p&gt;
&lt;p&gt;（1）需要使用密码器加密。&lt;/p&gt;
&lt;p&gt;（2）AppSuccessHandler处理器需要使用密码器匹配一下。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;            ClientDetails clientDetails = clientDetailsService.loadClientByClientId(clientId);
            if (clientDetails == null) {
                throw new UnapprovedClientAuthenticationException(&quot;clientid不存在&quot;);
            } else if (!passwordEncoder.matches(secret,clientDetails.getClientSecret())) {
                throw new UnapprovedClientAuthenticationException(&quot;secret不匹配&quot;);
            }
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;到这里获取刷新token成功。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;AuthenticationManager注入到spring容器&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;@Configuration
@EnableWebSecurity
@EnableGlobalMethodSecurity(prePostEnabled = true, securedEnabled = true, jsr250Enabled = true)
public class AppWebSecurityConfig extends WebSecurityConfigurerAdapter {

    @Resource
    private OpenIdAuthenticationSecurityConfig openIdAuthenticationSecurityConfig;

    @Resource
    private SpringSocialConfigurer socialConfigurer;

    /***
     * 真正将AuthenticationManager注入到spring容器
     * @return AuthenticationManager
     * @throws Exception 异常
     */
    @Bean
    @Override
    public AuthenticationManager authenticationManagerBean() throws Exception {
        return super.authenticationManagerBean();
    }
&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;配置endpoint&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;    @Override
    public void configure(AuthorizationServerEndpointsConfigurer endpoints) throws Exception {
        endpoints
                //指定使用的TokenStore，tokenStore用来存取token，默认使用InMemoryTokenStore
                //这里使用redis存储token
                .tokenStore(tokenStore)
                //如果要使用refresh_token需要,根据解析的token去查找用户
                .userDetailsService(userDetailsService)
                .authenticationManager(authenticationManager)
&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;默认jwt是默认添加 user_name到 token中&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;我们之前用的openid传入token中，所以它拿着openID调用userdetailservice获取用户，获取不到报错。&lt;/p&gt;
&lt;p&gt;骚操作：&lt;/p&gt;
&lt;p&gt;在app成功处理器这里，将openid获取的用户名,构建一个新的认证OpenIdAuthenticationToken。传入。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;            //通过openID去获取用户名字
            OpenIdAuthenticationToken admin = new OpenIdAuthenticationToken(&quot;admin&quot;, &quot;&quot;, authentication.getAuthorities());

            //new出一个OAuth2Authentication对象
            OAuth2Authentication oAuth2Authentication = new OAuth2Authentication(oAuth2Request, admin);
&lt;/code&gt;&lt;/pre&gt;
&lt;hr /&gt;
&lt;h2&gt;SpringCloud OAuth2&lt;/h2&gt;
&lt;h2&gt;oauth2&lt;/h2&gt;
&lt;h3&gt;什么是 oAuth&lt;/h3&gt;
&lt;p&gt;oAuth 协议为用户资源的授权提供了一个安全的、开放而又简易的标准。与以往的授权方式不同之处是 oAuth 的授权不会使第三方触及到用户的帐号信息（如用户名与密码），即第三方无需使用用户的用户名与密码就可以申请获得该用户资源的授权，因此 oAuth 是安全的。&lt;/p&gt;
&lt;h3&gt;什么是 Spring Security&lt;/h3&gt;
&lt;p&gt;Spring Security 是一个安全框架，前身是 Acegi Security，能够为 Spring 企业应用系统提供声明式的安全访问控制。Spring Security 基于 Servlet 过滤器、IoC 和 AOP，为 Web 请求和方法调用提供身份确认和授权处理，避免了代码耦合，减少了大量重复代码工作。&lt;/p&gt;
&lt;h3&gt;为什么需要 oAuth2&lt;/h3&gt;
&lt;h4&gt;应用场景&lt;/h4&gt;
&lt;p&gt;我们假设你有一个“云笔记”产品，并提供了“云笔记服务”和“云相册服务”，此时用户需要在不同的设备（PC、Android、iPhone、TV、Watch）上去访问这些“资源”（笔记，图片）&lt;/p&gt;
&lt;p&gt;那么用户如何才能访问属于自己的那部分资源呢？此时传统的做法就是提供自己的账号和密码给我们的“云笔记”，登录成功后就可以获取资源了。但这样的做法会有以下几个问题：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;“云笔记服务”和“云相册服务”会分别部署，难道我们要分别登录吗？&lt;/li&gt;
&lt;li&gt;如果有第三方应用程序想要接入我们的“云笔记”，难道需要用户提供账号和密码给第三方应用程序，让他记录后再访问我们的资源吗？&lt;/li&gt;
&lt;li&gt;用户如何限制第三方应用程序在我们“云笔记”的授权范围和使用期限？难道把所有资料都永久暴露给它吗？&lt;/li&gt;
&lt;li&gt;如果用户修改了密码收回了权限，那么所有第三方应用程序会全部失效。&lt;/li&gt;
&lt;li&gt;只要有一个接入的第三方应用程序遭到破解，那么用户的密码就会泄露，后果不堪设想。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;为了解决如上问题，oAuth 应用而生。&lt;/p&gt;
&lt;h4&gt;名词解释&lt;/h4&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;第三方应用程序（Third-party application）：&lt;/strong&gt; 又称之为客户端（client），比如上节中提到的设备（PC、Android、iPhone、TV、Watch），我们会在这些设备中安装我们自己研发的 APP。又比如我们的产品想要使用 QQ、微信等第三方登录。对我们的产品来说，QQ、微信登录是第三方登录系统。我们又需要第三方登录系统的资源（头像、昵称等）。对于 QQ、微信等系统我们又是第三方应用程序。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;HTTP 服务提供商（HTTP service）：&lt;/strong&gt; 我们的云笔记产品以及 QQ、微信等都可以称之为“服务提供商”。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;资源所有者（Resource Owner）：&lt;/strong&gt; 又称之为用户（user）。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;用户代理（User Agent）：&lt;/strong&gt; 比如浏览器，代替用户去访问这些资源。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;认证服务器（Authorization server）：&lt;/strong&gt; 即服务提供商专门用来处理认证的服务器，简单点说就是登录功能（验证用户的账号密码是否正确以及分配相应的权限）&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;资源服务器（Resource server）：&lt;/strong&gt; 即服务提供商存放用户生成的资源的服务器。它与认证服务器，可以是同一台服务器，也可以是不同的服务器。简单点说就是资源的访问入口，比如上节中提到的“云笔记服务”和“云相册服务”都可以称之为资源服务器。&lt;/li&gt;
&lt;/ul&gt;
&lt;h4&gt;交互过程&lt;/h4&gt;
&lt;p&gt;oAuth 在 &quot;客户端&quot; 与 &quot;服务提供商&quot; 之间，设置了一个授权层（authorization layer）。&quot;客户端&quot; 不能直接登录 &quot;服务提供商&quot;，只能登录授权层，以此将用户与客户端区分开来。&quot;客户端&quot; 登录授权层所用的令牌（token），与用户的密码不同。用户可以在登录的时候，指定授权层令牌的权限范围和有效期。&quot;客户端&quot; 登录授权层以后，&quot;服务提供商&quot; 根据令牌的权限范围和有效期，向 &quot;客户端&quot; 开放用户储存的资料。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;%E7%94%A8%E6%88%B7%E4%BA%A4%E4%BA%92.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;h2&gt;开放平台&lt;/h2&gt;
&lt;h3&gt;交互模型&lt;/h3&gt;
&lt;p&gt;交互模型涉及三方：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;资源拥有者：用户&lt;/li&gt;
&lt;li&gt;客户端：APP&lt;/li&gt;
&lt;li&gt;服务提供方：包含两个角色
&lt;ul&gt;
&lt;li&gt;认证服务器&lt;/li&gt;
&lt;li&gt;资源服务器&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;认证服务器&lt;/h3&gt;
&lt;p&gt;认证服务器负责对用户进行认证，并授权给客户端权限。认证很容易实现（验证账号密码即可），问题在于如何授权。比如我们使用第三方登录 &quot;有道云笔记&quot;，你可以看到如使用 QQ 登录的授权页面上有 &quot;有道云笔记将获得以下权限&quot; 的字样以及权限信息&lt;/p&gt;
&lt;p&gt;认证服务器需要知道请求授权的客户端的身份以及该客户端请求的权限。我们可以为每一个客户端预先分配一个 id，并给每个 id 对应一个名称以及权限信息。这些信息可以写在认证服务器上的配置文件里。然后，客户端每次打开授权页面的时候，把属于自己的 id 传过来，如：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;http://www.funtl.com/login?client_id=yourClientId
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;随着时间的推移和业务的增长，会发现，修改配置的工作消耗了太多的人力。有没有办法把这个过程自动化起来，把人工从这些繁琐的操作中解放出来？当开始考虑这一步，开放平台的成型也就是水到渠成的事情了。&lt;/p&gt;
&lt;h3&gt;oAuth2 开放平台&lt;/h3&gt;
&lt;p&gt;开放平台是由 oAuth2.0 协议衍生出来的一个产品。它的作用是让客户端自己去这上面进行注册、申请，通过之后系统自动分配 &lt;code&gt;client_id&lt;/code&gt; ，并完成配置的自动更新（通常是写进数据库）。&lt;/p&gt;
&lt;p&gt;客户端要完成申请，通常需要填写客户端程序的类型（Web、App 等）、企业介绍、执照、想要获取的权限等等信息。这些信息在得到服务提供方的人工审核通过后，开发平台就会自动分配一个 &lt;code&gt;client_id&lt;/code&gt; 给客户端了。&lt;/p&gt;
&lt;p&gt;到这里，已经实现了登录认证、授权页的信息展示。那么接下来，当用户成功进行授权之后，认证服务器需要把产生的 &lt;code&gt;access_token&lt;/code&gt; 发送给客户端，方案如下：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;让客户端在开放平台申请的时候，填写一个 URL，例如：&lt;a href=&quot;http://www.qfdmy.com/wp-content/themes/quanbaike/go.php?url=aHR0cDovL3d3dy5mdW50bC5jb20=&quot;&gt;http://www.funtl.com&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;每次当有用户授权成功之后，认证服务器将页面重定向到这个 URL（回调），并带上 &lt;code&gt;access_token&lt;/code&gt;，例如：&lt;a href=&quot;http://www.qfdmy.com/wp-content/themes/quanbaike/go.php?url=aHR0cDovL3d3dy5mdW50bC5jb20/YWNjZXNzX3Rva2VuPTEyMzQ1Njc4OQ==&quot;&gt;http://www.funtl.com?access_token=123456789&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;客户端接收到了这个 &lt;code&gt;access_token&lt;/code&gt;，而且认证服务器的授权动作已经完成，刚好可以把程序的控制权转交回客户端，由客户端决定接下来向用户展示什么内容&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;令牌的访问与刷新&lt;/h2&gt;
&lt;h3&gt;Access Token&lt;/h3&gt;
&lt;p&gt;Access Token 是客户端访问资源服务器的令牌。拥有这个令牌代表着得到用户的授权。然而，这个授权应该是 &lt;strong&gt;临时&lt;/strong&gt; 的，有一定有效期。这是因为，Access Token 在使用的过程中 &lt;strong&gt;可能会泄露&lt;/strong&gt;。给 Access Token 限定一个 &lt;strong&gt;较短的有效期&lt;/strong&gt; 可以降低因 Access Token 泄露而带来的风险。&lt;/p&gt;
&lt;p&gt;然而引入了有效期之后，客户端使用起来就不那么方便了。每当 Access Token 过期，客户端就必须重新向用户索要授权。这样用户可能每隔几天，甚至每天都需要进行授权操作。这是一件非常影响用户体验的事情。希望有一种方法，可以避免这种情况。&lt;/p&gt;
&lt;p&gt;于是 oAuth2.0 引入了 Refresh Token 机制&lt;/p&gt;
&lt;h3&gt;Refresh Token&lt;/h3&gt;
&lt;p&gt;Refresh Token 的作用是用来刷新 Access Token。认证服务器提供一个刷新接口，例如：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;http://www.funtl.com/refresh?refresh_token=&amp;amp;client_id=
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;传入 &lt;code&gt;refresh_token&lt;/code&gt; 和 &lt;code&gt;client_id&lt;/code&gt;，认证服务器验证通过后，返回一个新的 Access Token。为了安全，oAuth2.0 引入了两个措施：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;oAuth2.0 要求，Refresh Token &lt;strong&gt;一定是保存在客户端的服务器上&lt;/strong&gt; ，而绝不能存放在狭义的客户端（例如 App、PC 端软件）上。调用 &lt;code&gt;refresh&lt;/code&gt; 接口的时候，一定是从服务器到服务器的访问。&lt;/li&gt;
&lt;li&gt;oAuth2.0 引入了 &lt;code&gt;client_secret&lt;/code&gt; 机制。即每一个 &lt;code&gt;client_id&lt;/code&gt; 都对应一个 &lt;code&gt;client_secret&lt;/code&gt;。这个 &lt;code&gt;client_secret&lt;/code&gt; 会在客户端申请 &lt;code&gt;client_id&lt;/code&gt; 时，随 &lt;code&gt;client_id&lt;/code&gt; 一起分配给客户端。&lt;strong&gt;客户端必须把 client_secret 妥善保管在服务器上&lt;/strong&gt;，决不能泄露。刷新 Access Token 时，需要验证这个 &lt;code&gt;client_secret&lt;/code&gt;。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;实际上的刷新接口类似于：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;http://www.funtl.com/refresh?refresh_token=&amp;amp;client_id=&amp;amp;client_secret=
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;以上就是 Refresh Token 机制。Refresh Token 的有效期非常长，会在用户授权时，随 Access Token 一起重定向到回调 URL，传递给客户端。&lt;/p&gt;
&lt;h2&gt;客户端授权模式&lt;/h2&gt;
&lt;h3&gt;概述&lt;/h3&gt;
&lt;p&gt;客户端必须得到用户的授权（authorization grant），才能获得令牌（access token）。oAuth 2.0 定义了四种授权方式。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;implicit：简化模式，不推荐使用&lt;/li&gt;
&lt;li&gt;authorization code：授权码模式&lt;/li&gt;
&lt;li&gt;resource owner password credentials：密码模式&lt;/li&gt;
&lt;li&gt;client credentials：客户端模式&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;简化模式&lt;/h3&gt;
&lt;p&gt;简化模式适用于纯静态页面应用。所谓纯静态页面应用，也就是应用没有在服务器上执行代码的权限（通常是把代码托管在别人的服务器上），只有前端 JS 代码的控制权。&lt;/p&gt;
&lt;p&gt;这种场景下，应用是没有持久化存储的能力的。因此，按照 oAuth2.0 的规定，这种应用是拿不到 Refresh Token 的。其整个授权流程如下&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;%E7%AE%80%E5%8C%96%E6%A8%A1%E5%BC%8F.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;该模式下，&lt;code&gt;access_token&lt;/code&gt; 容易泄露且不可刷新&lt;/p&gt;
&lt;h3&gt;授权码模式&lt;/h3&gt;
&lt;p&gt;授权码模式适用于有自己的服务器的应用，它是一个一次性的临时凭证，用来换取 &lt;code&gt;access_token&lt;/code&gt; 和 &lt;code&gt;refresh_token&lt;/code&gt;。认证服务器提供了一个类似这样的接口：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;https://www.funtl.com/exchange?code=&amp;amp;client_id=&amp;amp;client_secret=
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;需要传入 &lt;code&gt;code&lt;/code&gt;、&lt;code&gt;client_id&lt;/code&gt; 以及 &lt;code&gt;client_secret&lt;/code&gt;。验证通过后，返回 &lt;code&gt;access_token&lt;/code&gt; 和 &lt;code&gt;refresh_token&lt;/code&gt;。一旦换取成功，&lt;code&gt;code&lt;/code&gt; 立即作废，不能再使用第二次。流程图如下：&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;%E6%8E%88%E6%9D%83%E7%A0%81%E6%A8%A1%E5%BC%8F.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;这个 code 的作用是保护 token 的安全性。上一节说到，简单模式下，token 是不安全的。这是因为在第 4 步当中直接把 token 返回给应用。而这一步容易被拦截、窃听。引入了 code 之后，即使攻击者能够窃取到 code，但是由于他无法获得应用保存在服务器的 &lt;code&gt;client_secret&lt;/code&gt;，因此也无法通过 code 换取 token。而第 5 步，为什么不容易被拦截、窃听呢？这是因为，首先，这是一个从服务器到服务器的访问，黑客比较难捕捉到；其次，这个请求通常要求是 https 的实现。即使能窃听到数据包也无法解析出内容。&lt;/p&gt;
&lt;p&gt;有了这个 code，token 的安全性大大提高。因此，oAuth2.0 鼓励使用这种方式进行授权，而简单模式则是在不得已情况下才会使用。&lt;/p&gt;
&lt;h3&gt;密码模式&lt;/h3&gt;
&lt;p&gt;密码模式中，用户向客户端提供自己的用户名和密码。客户端使用这些信息，向 &quot;服务商提供商&quot; 索要授权。在这种模式中，用户必须把自己的密码给客户端，但是客户端不得储存密码。这通常用在用户对客户端高度信任的情况下，比如客户端是操作系统的一部分。&lt;/p&gt;
&lt;p&gt;一个典型的例子是同一个企业内部的不同产品要使用本企业的 oAuth2.0 体系。在有些情况下，产品希望能够定制化授权页面。由于是同个企业，不需要向用户展示“xxx将获取以下权限”等字样并询问用户的授权意向，而只需进行用户的身份认证即可。这个时候，由具体的产品团队开发定制化的授权界面，接收用户输入账号密码，并直接传递给鉴权服务器进行授权即可。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;%E5%AF%86%E7%A0%81%E6%A8%A1%E5%BC%8F.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;有一点需要特别注意的是，在第 2 步中，认证服务器需要对客户端的身份进行验证，确保是受信任的客户端。&lt;/p&gt;
&lt;h3&gt;客户端模式&lt;/h3&gt;
&lt;p&gt;如果信任关系再进一步，或者调用者是一个后端的模块，没有用户界面的时候，可以使用客户端模式。鉴权服务器直接对客户端进行身份验证，验证通过后，返回 token。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;%E5%AE%A2%E6%88%B7%E7%AB%AF%E6%A8%A1%E5%BC%8F.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;h2&gt;创建项目工程&lt;/h2&gt;
&lt;p&gt;创建一个名为 &lt;code&gt;hello-spring-security-oauth2&lt;/code&gt; 工程项目，POM 如下&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&amp;lt;?xml version=&quot;1.0&quot; encoding=&quot;UTF-8&quot;?&amp;gt;
&amp;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;&amp;gt;
    &amp;lt;modelVersion&amp;gt;4.0.0&amp;lt;/modelVersion&amp;gt;
    &amp;lt;parent&amp;gt;
        &amp;lt;groupId&amp;gt;org.springframework.boot&amp;lt;/groupId&amp;gt;
        &amp;lt;artifactId&amp;gt;spring-boot-starter-parent&amp;lt;/artifactId&amp;gt;
        &amp;lt;version&amp;gt;2.1.6.RELEASE&amp;lt;/version&amp;gt;
        &amp;lt;relativePath/&amp;gt; &amp;lt;!-- lookup parent from repository --&amp;gt;
    &amp;lt;/parent&amp;gt;
    &amp;lt;groupId&amp;gt;com.funtl&amp;lt;/groupId&amp;gt;
    &amp;lt;artifactId&amp;gt;hello-spring-security-oauth2&amp;lt;/artifactId&amp;gt;
    &amp;lt;version&amp;gt;0.0.1-SNAPSHOT&amp;lt;/version&amp;gt;
    &amp;lt;packaging&amp;gt;pom&amp;lt;/packaging&amp;gt;
    &amp;lt;url&amp;gt;http://www.funtl.com&amp;lt;/url&amp;gt;
    &amp;lt;modules&amp;gt;
        &amp;lt;module&amp;gt;hello-spring-security-oauth2-dependencies&amp;lt;/module&amp;gt;
    &amp;lt;/modules&amp;gt;
    &amp;lt;properties&amp;gt;
        &amp;lt;java.version&amp;gt;1.8&amp;lt;/java.version&amp;gt;
        &amp;lt;maven.compiler.source&amp;gt;${java.version}&amp;lt;/maven.compiler.source&amp;gt;
        &amp;lt;maven.compiler.target&amp;gt;${java.version}&amp;lt;/maven.compiler.target&amp;gt;
        &amp;lt;project.build.sourceEncoding&amp;gt;UTF-8&amp;lt;/project.build.sourceEncoding&amp;gt;
        &amp;lt;project.reporting.outputEncoding&amp;gt;UTF-8&amp;lt;/project.reporting.outputEncoding&amp;gt;
    &amp;lt;/properties&amp;gt;
    &amp;lt;licenses&amp;gt;
        &amp;lt;license&amp;gt;
            &amp;lt;name&amp;gt;Apache 2.0&amp;lt;/name&amp;gt;
            &amp;lt;url&amp;gt;https://www.apache.org/licenses/LICENSE-2.0.txt&amp;lt;/url&amp;gt;
        &amp;lt;/license&amp;gt;
    &amp;lt;/licenses&amp;gt;
    &amp;lt;developers&amp;gt;
        &amp;lt;developer&amp;gt;
            &amp;lt;id&amp;gt;liwemin&amp;lt;/id&amp;gt;
            &amp;lt;name&amp;gt;Lusifer Lee&amp;lt;/name&amp;gt;
            &amp;lt;email&amp;gt;lee.lusifer@gmail.com&amp;lt;/email&amp;gt;
        &amp;lt;/developer&amp;gt;
    &amp;lt;/developers&amp;gt;
    &amp;lt;dependencyManagement&amp;gt;
        &amp;lt;dependencies&amp;gt;
            &amp;lt;dependency&amp;gt;
                &amp;lt;groupId&amp;gt;com.funtl&amp;lt;/groupId&amp;gt;
                &amp;lt;artifactId&amp;gt;hello-spring-security-oauth2-dependencies&amp;lt;/artifactId&amp;gt;
                &amp;lt;version&amp;gt;${project.version}&amp;lt;/version&amp;gt;
                &amp;lt;type&amp;gt;pom&amp;lt;/type&amp;gt;
                &amp;lt;scope&amp;gt;import&amp;lt;/scope&amp;gt;
            &amp;lt;/dependency&amp;gt;
        &amp;lt;/dependencies&amp;gt;
    &amp;lt;/dependencyManagement&amp;gt;
    &amp;lt;profiles&amp;gt;
        &amp;lt;profile&amp;gt;
            &amp;lt;id&amp;gt;default&amp;lt;/id&amp;gt;
            &amp;lt;activation&amp;gt;
                &amp;lt;activeByDefault&amp;gt;true&amp;lt;/activeByDefault&amp;gt;
            &amp;lt;/activation&amp;gt;
            &amp;lt;properties&amp;gt;
                &amp;lt;spring-javaformat.version&amp;gt;0.0.12&amp;lt;/spring-javaformat.version&amp;gt;
            &amp;lt;/properties&amp;gt;
            &amp;lt;build&amp;gt;
                &amp;lt;plugins&amp;gt;
                    &amp;lt;plugin&amp;gt;
                        &amp;lt;groupId&amp;gt;io.spring.javaformat&amp;lt;/groupId&amp;gt;
                        &amp;lt;artifactId&amp;gt;spring-javaformat-maven-plugin&amp;lt;/artifactId&amp;gt;
                        &amp;lt;version&amp;gt;${spring-javaformat.version}&amp;lt;/version&amp;gt;
                    &amp;lt;/plugin&amp;gt;
                    &amp;lt;plugin&amp;gt;
                        &amp;lt;groupId&amp;gt;org.apache.maven.plugins&amp;lt;/groupId&amp;gt;
                        &amp;lt;artifactId&amp;gt;maven-surefire-plugin&amp;lt;/artifactId&amp;gt;
                        &amp;lt;configuration&amp;gt;
                            &amp;lt;includes&amp;gt;
                                &amp;lt;include&amp;gt;**/*Tests.java&amp;lt;/include&amp;gt;
                            &amp;lt;/includes&amp;gt;
                            &amp;lt;excludes&amp;gt;
                                &amp;lt;exclude&amp;gt;**/Abstract*.java&amp;lt;/exclude&amp;gt;
                            &amp;lt;/excludes&amp;gt;
                            &amp;lt;systemPropertyVariables&amp;gt;
                                &amp;lt;java.security.egd&amp;gt;file:/dev/./urandom&amp;lt;/java.security.egd&amp;gt;
                                &amp;lt;java.awt.headless&amp;gt;true&amp;lt;/java.awt.headless&amp;gt;
                            &amp;lt;/systemPropertyVariables&amp;gt;
                        &amp;lt;/configuration&amp;gt;
                    &amp;lt;/plugin&amp;gt;
                    &amp;lt;plugin&amp;gt;
                        &amp;lt;groupId&amp;gt;org.apache.maven.plugins&amp;lt;/groupId&amp;gt;
                        &amp;lt;artifactId&amp;gt;maven-enforcer-plugin&amp;lt;/artifactId&amp;gt;
                        &amp;lt;executions&amp;gt;
                            &amp;lt;execution&amp;gt;
                                &amp;lt;id&amp;gt;enforce-rules&amp;lt;/id&amp;gt;
                                &amp;lt;goals&amp;gt;
                                    &amp;lt;goal&amp;gt;enforce&amp;lt;/goal&amp;gt;
                                &amp;lt;/goals&amp;gt;
                                &amp;lt;configuration&amp;gt;
                                    &amp;lt;rules&amp;gt;
                                        &amp;lt;bannedDependencies&amp;gt;
                                            &amp;lt;excludes&amp;gt;
                                                &amp;lt;exclude&amp;gt;commons-logging:*:*&amp;lt;/exclude&amp;gt;
                                            &amp;lt;/excludes&amp;gt;
                                            &amp;lt;searchTransitive&amp;gt;true&amp;lt;/searchTransitive&amp;gt;
                                        &amp;lt;/bannedDependencies&amp;gt;
                                    &amp;lt;/rules&amp;gt;
                                    &amp;lt;fail&amp;gt;true&amp;lt;/fail&amp;gt;
                                &amp;lt;/configuration&amp;gt;
                            &amp;lt;/execution&amp;gt;
                        &amp;lt;/executions&amp;gt;
                    &amp;lt;/plugin&amp;gt;
                    &amp;lt;plugin&amp;gt;
                        &amp;lt;groupId&amp;gt;org.apache.maven.plugins&amp;lt;/groupId&amp;gt;
                        &amp;lt;artifactId&amp;gt;maven-install-plugin&amp;lt;/artifactId&amp;gt;
                        &amp;lt;configuration&amp;gt;
                            &amp;lt;skip&amp;gt;true&amp;lt;/skip&amp;gt;
                        &amp;lt;/configuration&amp;gt;
                    &amp;lt;/plugin&amp;gt;
                    &amp;lt;plugin&amp;gt;
                        &amp;lt;groupId&amp;gt;org.apache.maven.plugins&amp;lt;/groupId&amp;gt;
                        &amp;lt;artifactId&amp;gt;maven-javadoc-plugin&amp;lt;/artifactId&amp;gt;
                        &amp;lt;configuration&amp;gt;
                            &amp;lt;skip&amp;gt;true&amp;lt;/skip&amp;gt;
                        &amp;lt;/configuration&amp;gt;
                        &amp;lt;inherited&amp;gt;true&amp;lt;/inherited&amp;gt;
                    &amp;lt;/plugin&amp;gt;
                &amp;lt;/plugins&amp;gt;
            &amp;lt;/build&amp;gt;
        &amp;lt;/profile&amp;gt;
    &amp;lt;/profiles&amp;gt;
    &amp;lt;repositories&amp;gt;
        &amp;lt;repository&amp;gt;
            &amp;lt;id&amp;gt;spring-milestone&amp;lt;/id&amp;gt;
            &amp;lt;name&amp;gt;Spring Milestone&amp;lt;/name&amp;gt;
            &amp;lt;url&amp;gt;https://repo.spring.io/milestone&amp;lt;/url&amp;gt;
            &amp;lt;snapshots&amp;gt;
                &amp;lt;enabled&amp;gt;false&amp;lt;/enabled&amp;gt;
            &amp;lt;/snapshots&amp;gt;
        &amp;lt;/repository&amp;gt;
        &amp;lt;repository&amp;gt;
            &amp;lt;id&amp;gt;spring-snapshot&amp;lt;/id&amp;gt;
            &amp;lt;name&amp;gt;Spring Snapshot&amp;lt;/name&amp;gt;
            &amp;lt;url&amp;gt;https://repo.spring.io/snapshot&amp;lt;/url&amp;gt;
            &amp;lt;snapshots&amp;gt;
                &amp;lt;enabled&amp;gt;true&amp;lt;/enabled&amp;gt;
            &amp;lt;/snapshots&amp;gt;
        &amp;lt;/repository&amp;gt;
    &amp;lt;/repositories&amp;gt;
    &amp;lt;pluginRepositories&amp;gt;
        &amp;lt;pluginRepository&amp;gt;
            &amp;lt;id&amp;gt;spring-milestone&amp;lt;/id&amp;gt;
            &amp;lt;name&amp;gt;Spring Milestone&amp;lt;/name&amp;gt;
            &amp;lt;url&amp;gt;https://repo.spring.io/milestone&amp;lt;/url&amp;gt;
            &amp;lt;snapshots&amp;gt;
                &amp;lt;enabled&amp;gt;false&amp;lt;/enabled&amp;gt;
            &amp;lt;/snapshots&amp;gt;
        &amp;lt;/pluginRepository&amp;gt;
        &amp;lt;pluginRepository&amp;gt;
            &amp;lt;id&amp;gt;spring-snapshot&amp;lt;/id&amp;gt;
            &amp;lt;name&amp;gt;Spring Snapshot&amp;lt;/name&amp;gt;
            &amp;lt;url&amp;gt;https://repo.spring.io/snapshot&amp;lt;/url&amp;gt;
            &amp;lt;snapshots&amp;gt;
                &amp;lt;enabled&amp;gt;true&amp;lt;/enabled&amp;gt;
            &amp;lt;/snapshots&amp;gt;
        &amp;lt;/pluginRepository&amp;gt;
    &amp;lt;/pluginRepositories&amp;gt;
&amp;lt;/project&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;依赖管理项目&lt;/h3&gt;
&lt;p&gt;创建一个名为 &lt;code&gt;hello-spring-security-oauth2-dependencies&lt;/code&gt; 项目，POM 如下：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&amp;lt;?xml version=&quot;1.0&quot; encoding=&quot;UTF-8&quot;?&amp;gt;
&amp;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;&amp;gt;
    &amp;lt;modelVersion&amp;gt;4.0.0&amp;lt;/modelVersion&amp;gt;
    &amp;lt;groupId&amp;gt;com.funtl&amp;lt;/groupId&amp;gt;
    &amp;lt;artifactId&amp;gt;hello-spring-security-oauth2-dependencies&amp;lt;/artifactId&amp;gt;
    &amp;lt;version&amp;gt;0.0.1-SNAPSHOT&amp;lt;/version&amp;gt;
    &amp;lt;packaging&amp;gt;pom&amp;lt;/packaging&amp;gt;
    &amp;lt;url&amp;gt;http://www.funtl.com&amp;lt;/url&amp;gt;
    &amp;lt;properties&amp;gt;
        &amp;lt;spring-cloud.version&amp;gt;Greenwich.RELEASE&amp;lt;/spring-cloud.version&amp;gt;
    &amp;lt;/properties&amp;gt;
    &amp;lt;licenses&amp;gt;
        &amp;lt;license&amp;gt;
            &amp;lt;name&amp;gt;Apache 2.0&amp;lt;/name&amp;gt;
            &amp;lt;url&amp;gt;https://www.apache.org/licenses/LICENSE-2.0.txt&amp;lt;/url&amp;gt;
        &amp;lt;/license&amp;gt;
    &amp;lt;/licenses&amp;gt;
    &amp;lt;developers&amp;gt;
        &amp;lt;developer&amp;gt;
            &amp;lt;id&amp;gt;liwemin&amp;lt;/id&amp;gt;
            &amp;lt;name&amp;gt;Lusifer Lee&amp;lt;/name&amp;gt;
            &amp;lt;email&amp;gt;lee.lusifer@gmail.com&amp;lt;/email&amp;gt;
        &amp;lt;/developer&amp;gt;
    &amp;lt;/developers&amp;gt;
    &amp;lt;dependencyManagement&amp;gt;
        &amp;lt;dependencies&amp;gt;
            &amp;lt;dependency&amp;gt;
                &amp;lt;groupId&amp;gt;org.springframework.cloud&amp;lt;/groupId&amp;gt;
                &amp;lt;artifactId&amp;gt;spring-cloud-dependencies&amp;lt;/artifactId&amp;gt;
                &amp;lt;version&amp;gt;${spring-cloud.version}&amp;lt;/version&amp;gt;
                &amp;lt;type&amp;gt;pom&amp;lt;/type&amp;gt;
                &amp;lt;scope&amp;gt;import&amp;lt;/scope&amp;gt;
            &amp;lt;/dependency&amp;gt;
        &amp;lt;/dependencies&amp;gt;
    &amp;lt;/dependencyManagement&amp;gt;
    &amp;lt;repositories&amp;gt;
        &amp;lt;repository&amp;gt;
            &amp;lt;id&amp;gt;spring-milestone&amp;lt;/id&amp;gt;
            &amp;lt;name&amp;gt;Spring Milestone&amp;lt;/name&amp;gt;
            &amp;lt;url&amp;gt;https://repo.spring.io/milestone&amp;lt;/url&amp;gt;
            &amp;lt;snapshots&amp;gt;
                &amp;lt;enabled&amp;gt;false&amp;lt;/enabled&amp;gt;
            &amp;lt;/snapshots&amp;gt;
        &amp;lt;/repository&amp;gt;
        &amp;lt;repository&amp;gt;
            &amp;lt;id&amp;gt;spring-snapshot&amp;lt;/id&amp;gt;
            &amp;lt;name&amp;gt;Spring Snapshot&amp;lt;/name&amp;gt;
            &amp;lt;url&amp;gt;https://repo.spring.io/snapshot&amp;lt;/url&amp;gt;
            &amp;lt;snapshots&amp;gt;
                &amp;lt;enabled&amp;gt;true&amp;lt;/enabled&amp;gt;
            &amp;lt;/snapshots&amp;gt;
        &amp;lt;/repository&amp;gt;
    &amp;lt;/repositories&amp;gt;
    &amp;lt;pluginRepositories&amp;gt;
        &amp;lt;pluginRepository&amp;gt;
            &amp;lt;id&amp;gt;spring-milestone&amp;lt;/id&amp;gt;
            &amp;lt;name&amp;gt;Spring Milestone&amp;lt;/name&amp;gt;
            &amp;lt;url&amp;gt;https://repo.spring.io/milestone&amp;lt;/url&amp;gt;
            &amp;lt;snapshots&amp;gt;
                &amp;lt;enabled&amp;gt;false&amp;lt;/enabled&amp;gt;
            &amp;lt;/snapshots&amp;gt;
        &amp;lt;/pluginRepository&amp;gt;
        &amp;lt;pluginRepository&amp;gt;
            &amp;lt;id&amp;gt;spring-snapshot&amp;lt;/id&amp;gt;
            &amp;lt;name&amp;gt;Spring Snapshot&amp;lt;/name&amp;gt;
            &amp;lt;url&amp;gt;https://repo.spring.io/snapshot&amp;lt;/url&amp;gt;
            &amp;lt;snapshots&amp;gt;
                &amp;lt;enabled&amp;gt;true&amp;lt;/enabled&amp;gt;
            &amp;lt;/snapshots&amp;gt;
        &amp;lt;/pluginRepository&amp;gt;
    &amp;lt;/pluginRepositories&amp;gt;
&amp;lt;/project&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;创建认证服务器&lt;/h2&gt;
&lt;h3&gt;POM&lt;/h3&gt;
&lt;p&gt;创建一个名为 &lt;code&gt;hello-spring-security-oauth2-server&lt;/code&gt; 项目，POM 如下：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&amp;lt;?xml version=&quot;1.0&quot; encoding=&quot;UTF-8&quot;?&amp;gt;
&amp;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;&amp;gt;
    &amp;lt;modelVersion&amp;gt;4.0.0&amp;lt;/modelVersion&amp;gt;
    &amp;lt;parent&amp;gt;
        &amp;lt;groupId&amp;gt;com.funtl&amp;lt;/groupId&amp;gt;
        &amp;lt;artifactId&amp;gt;hello-spring-security-oauth2&amp;lt;/artifactId&amp;gt;
        &amp;lt;version&amp;gt;0.0.1-SNAPSHOT&amp;lt;/version&amp;gt;
    &amp;lt;/parent&amp;gt;
    &amp;lt;artifactId&amp;gt;hello-spring-security-oauth2-server&amp;lt;/artifactId&amp;gt;
    &amp;lt;url&amp;gt;http://www.funtl.com&amp;lt;/url&amp;gt;
    &amp;lt;licenses&amp;gt;
        &amp;lt;license&amp;gt;
            &amp;lt;name&amp;gt;Apache 2.0&amp;lt;/name&amp;gt;
            &amp;lt;url&amp;gt;https://www.apache.org/licenses/LICENSE-2.0.txt&amp;lt;/url&amp;gt;
        &amp;lt;/license&amp;gt;
    &amp;lt;/licenses&amp;gt;
    &amp;lt;developers&amp;gt;
        &amp;lt;developer&amp;gt;
            &amp;lt;id&amp;gt;liwemin&amp;lt;/id&amp;gt;
            &amp;lt;name&amp;gt;Lusifer Lee&amp;lt;/name&amp;gt;
            &amp;lt;email&amp;gt;lee.lusifer@gmail.com&amp;lt;/email&amp;gt;
        &amp;lt;/developer&amp;gt;
    &amp;lt;/developers&amp;gt;
    &amp;lt;dependencies&amp;gt;
        &amp;lt;!-- Spring Boot --&amp;gt;
        &amp;lt;dependency&amp;gt;
            &amp;lt;groupId&amp;gt;org.springframework.boot&amp;lt;/groupId&amp;gt;
            &amp;lt;artifactId&amp;gt;spring-boot-starter-web&amp;lt;/artifactId&amp;gt;
        &amp;lt;/dependency&amp;gt;
        &amp;lt;dependency&amp;gt;
            &amp;lt;groupId&amp;gt;org.springframework.boot&amp;lt;/groupId&amp;gt;
            &amp;lt;artifactId&amp;gt;spring-boot-starter-test&amp;lt;/artifactId&amp;gt;
        &amp;lt;/dependency&amp;gt;
        &amp;lt;!-- Spring Security --&amp;gt;
        &amp;lt;dependency&amp;gt;
            &amp;lt;groupId&amp;gt;org.springframework.cloud&amp;lt;/groupId&amp;gt;
            &amp;lt;artifactId&amp;gt;spring-cloud-starter-oauth2&amp;lt;/artifactId&amp;gt;
        &amp;lt;/dependency&amp;gt;
    &amp;lt;/dependencies&amp;gt;
    &amp;lt;build&amp;gt;
        &amp;lt;plugins&amp;gt;
            &amp;lt;plugin&amp;gt;
                &amp;lt;groupId&amp;gt;org.springframework.boot&amp;lt;/groupId&amp;gt;
                &amp;lt;artifactId&amp;gt;spring-boot-maven-plugin&amp;lt;/artifactId&amp;gt;
                &amp;lt;configuration&amp;gt;
                    &amp;lt;mainClass&amp;gt;com.funtl.spring.security.oauth2.server.OAuth2ServerApplication&amp;lt;/mainClass&amp;gt;
                &amp;lt;/configuration&amp;gt;
            &amp;lt;/plugin&amp;gt;
        &amp;lt;/plugins&amp;gt;
    &amp;lt;/build&amp;gt;
&amp;lt;/project&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Application&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;package com.funtl.spring.security.oauth2.server;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
@SpringBootApplication
public class OAuth2ServerApplication {
    public static void main(String[] args) {
        SpringApplication.run(OAuth2ServerApplication.class, args);
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;基于内存存储令牌&lt;/h2&gt;
&lt;h3&gt;概述&lt;/h3&gt;
&lt;p&gt;本章节基于 &lt;strong&gt;内存存储令牌&lt;/strong&gt; 的模式用于演示最基本的操作，帮助大家快速理解 oAuth2 认证服务器中 &quot;认证&quot;、&quot;授权&quot;、&quot;访问令牌” 的基本概念&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;操作流程&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;%E6%93%8D%E4%BD%9C%E6%B5%81%E7%A8%8B.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;配置认证服务器
&lt;ul&gt;
&lt;li&gt;配置客户端信息：ClientDetailsServiceConfigurer
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;inMemory&lt;/code&gt;：内存配置&lt;/li&gt;
&lt;li&gt;&lt;code&gt;withClient&lt;/code&gt;：客户端标识&lt;/li&gt;
&lt;li&gt;&lt;code&gt;secret&lt;/code&gt;：客户端安全码&lt;/li&gt;
&lt;li&gt;&lt;code&gt;authorizedGrantTypes&lt;/code&gt;：客户端授权类型&lt;/li&gt;
&lt;li&gt;&lt;code&gt;scopes&lt;/code&gt;：客户端授权范围&lt;/li&gt;
&lt;li&gt;&lt;code&gt;redirectUris&lt;/code&gt;：注册回调地址&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;配置 Web 安全&lt;/li&gt;
&lt;li&gt;通过 GET 请求访问认证服务器获取授权码
&lt;ul&gt;
&lt;li&gt;端点：&lt;code&gt;/oauth/authorize&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;通过 POST 请求利用授权码访问认证服务器获取令牌
&lt;ul&gt;
&lt;li&gt;端点：&lt;code&gt;/oauth/token&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;默认的端点 URL&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;/oauth/authorize&lt;/code&gt;：授权端点&lt;/li&gt;
&lt;li&gt;&lt;code&gt;/oauth/token&lt;/code&gt;：令牌端点&lt;/li&gt;
&lt;li&gt;&lt;code&gt;/oauth/confirm_access&lt;/code&gt;：用户确认授权提交端点&lt;/li&gt;
&lt;li&gt;&lt;code&gt;/oauth/error&lt;/code&gt;：授权服务错误信息端点&lt;/li&gt;
&lt;li&gt;&lt;code&gt;/oauth/check_token&lt;/code&gt;：用于资源服务访问的令牌解析端点&lt;/li&gt;
&lt;li&gt;&lt;code&gt;/oauth/token_key&lt;/code&gt;：提供公有密匙的端点，如果你使用 JWT 令牌的话&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;服务器安全配置&lt;/h3&gt;
&lt;p&gt;创建一个类继承 &lt;code&gt;WebSecurityConfigurerAdapter&lt;/code&gt; 并添加相关注解：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;@Configuration&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;@EnableWebSecurity&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;@EnableGlobalMethodSecurity(prePostEnabled = true, securedEnabled = true, jsr250Enabled = true)&lt;/code&gt;：全局方法拦截&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;package com.funtl.spring.security.oauth2.server.configure;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;
import org.springframework.security.config.annotation.method.configuration.EnableGlobalMethodSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
@Configuration
@EnableWebSecurity
@EnableGlobalMethodSecurity(prePostEnabled = true, securedEnabled = true, jsr250Enabled = true)
public class WebSecurityConfiguration extends WebSecurityConfigurerAdapter {
    @Bean
    public BCryptPasswordEncoder passwordEncoder() {
        // 配置默认的加密方式
        return new BCryptPasswordEncoder();
    }
    @Override
    protected void configure(AuthenticationManagerBuilder auth) throws Exception {
        // 在内存中创建用户
        auth.inMemoryAuthentication()
                .withUser(&quot;user&quot;).password(passwordEncoder().encode(&quot;123456&quot;)).roles(&quot;USER&quot;)
                .and()
                .withUser(&quot;admin&quot;).password(passwordEncoder().encode(&quot;admin888&quot;)).roles(&quot;ADMIN&quot;);
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;配置认证服务器&lt;/h3&gt;
&lt;p&gt;创建一个类继承 &lt;code&gt;AuthorizationServerConfigurerAdapter&lt;/code&gt; 并添加相关注解：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;@Configuration&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;@EnableAuthorizationServer&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;package com.funtl.spring.security.oauth2.server.configure;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.oauth2.config.annotation.configurers.ClientDetailsServiceConfigurer;
import org.springframework.security.oauth2.config.annotation.web.configuration.AuthorizationServerConfigurerAdapter;
import org.springframework.security.oauth2.config.annotation.web.configuration.EnableAuthorizationServer;
@Configuration
@EnableAuthorizationServer
public class AuthorizationServerConfiguration extends AuthorizationServerConfigurerAdapter {
    @Autowired
    private BCryptPasswordEncoder passwordEncoder;
    @Override
    public void configure(ClientDetailsServiceConfigurer clients) throws Exception {
        // 配置客户端
        clients
                // 使用内存设置
                .inMemory()
                // client_id
                .withClient(&quot;client&quot;)
                // client_secret
                .secret(passwordEncoder.encode(&quot;secret&quot;))
                // 授权类型
                .authorizedGrantTypes(&quot;authorization_code&quot;)
                // 授权范围
                .scopes(&quot;app&quot;)
                // 注册回调地址
                .redirectUris(&quot;https://www.baidu.com&quot;);
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;application.yml&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;spring:
  application:
    name: oauth2-server
server:
  port: 8080
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;访问获取授权码&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;通过浏览器访问&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;http://localhost:8080/oauth/authorize?client_id=client&amp;amp;response_type=code
&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;第一次访问会跳转到登录页面&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;img src=&quot;%E7%99%BB%E5%BD%95%E9%A1%B5%E9%9D%A2.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;验证成功后会询问用户是否授权客户端&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;img src=&quot;%E9%BB%98%E8%AE%A4%E6%8E%88%E6%9D%83%E9%A1%B5%E9%9D%A2.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;选择授权后会跳转到百度，浏览器地址上还会包含一个授权码（&lt;code&gt;code=1JuO6V&lt;/code&gt;），浏览器地址栏会显示如下地址：&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;https://www.baidu.com/?code=1JuO6V
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;向服务器申请令牌&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;通过 CURL 或是 Postman 请求&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;curl -X POST -H &quot;Content-Type: application/x-www-form-urlencoded&quot; -d &apos;grant_type=authorization_code&amp;amp;code=1JuO6V&apos; &quot;http://client:secret@localhost:8080/oauth/token&quot;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;img src=&quot;postman%E8%AF%B7%E6%B1%82%E4%BB%A4%E7%89%8C.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;得到响应结果如下&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;{
    &quot;access_token&quot;: &quot;016d8d4a-dd6e-4493-b590-5f072923c413&quot;,
    &quot;token_type&quot;: &quot;bearer&quot;,
    &quot;expires_in&quot;: 43199,
    &quot;scope&quot;: &quot;app&quot;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;通过Java代码发请求获取token&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;配置好 注册回调地址到自建服务（接受code,并发起请求获取token）。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;踩坑&lt;/strong&gt;&lt;/p&gt;
&lt;blockquote&gt;
&lt;pre&gt;&lt;code&gt;             * 获取token也是需要认证的（Authorization），basic模式，传入编码后的 client_id和secret
             * key是Authorization，value中的Basic是固定的代表基本认证（Basic后面有一个空格），
             * 后面的字符串是认证信息比如client+secret字符串相加做base64(java.util中有)加密之后的加密串。
             * postman中请求可以直接写 http://client:secret@localhost:8090/oauth/token
             * postman会自动解析，并加上对应请求头（Authorization）
&lt;/code&gt;&lt;/pre&gt;
&lt;/blockquote&gt;
&lt;pre&gt;&lt;code&gt;@GetMapping(&quot;/code&quot;)
    public String getCode(@PathParam(&quot;code&quot;) String code) throws IOException {
        log.info(&quot;【获取到的授权码】 code={}&quot;,code);
        String url = &quot;http://localhost:8080/oauth/token&quot;;
        OkHttpClient client = new OkHttpClient();
        RequestBody body = new FormBody.Builder()
                .add(&quot;grant_type&quot;, &quot;authorization_code&quot;)
                .add(&quot;code&quot;, code)
                .build();
        String basic = &quot;client:secret&quot;;
        String encode = Base64.getEncoder().encodeToString(basic.getBytes());
        Request request = new Request.Builder()
                .url(url)
                .header(&quot;Content-Type&quot;,MediaType.APPLICATION_FORM_URLENCODED_VALUE)
                .addHeader(&quot;Connection&quot;,&quot;keep-alive&quot;)
                /**
                 * 获取token也是需要认证的（Authorization），basic模式，传入编码后的 client_id和secret
                 * key是Authorization，value中的Basic是固定的代表基本认证（Basic后面有一个空格），
                 * 后面的字符串是认证信息比如client+secret字符串相加做base64加密之后的加密串。
                 * postman中请求可以直接写 http://client:secret@localhost:8090/oauth/token
                 * postman会自动解析，并加上对应请求头（Authorization）
                 */
                .addHeader(&quot;Authorization&quot;,&quot;Basic &quot;+encode)
                .post(body)
                .build();
        Call call = client.newCall(request);
        try {
            Response response = call.execute();
            System.out.println(response.body().string());
        } catch (IOException e) {
            e.printStackTrace();
        }
        log.info(&quot;【body】={}&quot;,body);
        return &quot;成功&quot;;
    }

    @GetMapping(&quot;/test&quot;)
    public String test(){
        return &quot;test&quot;;
    }
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;基于 JDBC 存储令牌&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;初始化 oAuth2 相关表&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;在数据库中配置客户端&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;配置认证服务器&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;配置数据源：&lt;code&gt;DataSource&lt;/code&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;配置令牌存储方式：&lt;code&gt;TokenStore&lt;/code&gt; -&amp;gt; &lt;code&gt;JdbcTokenStore&lt;/code&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;配置客户端读取方式：&lt;code&gt;ClientDetailsService&lt;/code&gt; -&amp;gt; &lt;code&gt;JdbcClientDetailsService&lt;/code&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;配置服务端点信息：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;AuthorizationServerEndpointsConfigurer
&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;tokenStore&lt;/code&gt;：设置令牌存储方式&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;配置客户端信息：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;ClientDetailsServiceConfigurer
&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;withClientDetails&lt;/code&gt;：设置客户端配置读取方式&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;配置 Web 安全&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;配置密码加密方式：&lt;code&gt;BCryptPasswordEncoder&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;配置认证信息：&lt;code&gt;AuthenticationManagerBuilder&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;通过 GET 请求访问认证服务器获取授权码&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;端点：&lt;code&gt;/oauth/authorize&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;通过 POST 请求利用授权码访问认证服务器获取令牌&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;端点：&lt;code&gt;/oauth/token&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;默认的端点 URL&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;/oauth/authorize&lt;/code&gt;：授权端点&lt;/li&gt;
&lt;li&gt;&lt;code&gt;/oauth/token&lt;/code&gt;：令牌端点&lt;/li&gt;
&lt;li&gt;&lt;code&gt;/oauth/confirm_access&lt;/code&gt;：用户确认授权提交端点&lt;/li&gt;
&lt;li&gt;&lt;code&gt;/oauth/error&lt;/code&gt;：授权服务错误信息端点&lt;/li&gt;
&lt;li&gt;&lt;code&gt;/oauth/check_token&lt;/code&gt;：用于资源服务访问的令牌解析端点&lt;/li&gt;
&lt;li&gt;&lt;code&gt;/oauth/token_key&lt;/code&gt;：提供公有密匙的端点，如果你使用 JWT 令牌的话&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;初始化 oAuth2 相关表&lt;/h3&gt;
&lt;p&gt;使用官方提供的建表脚本初始化 oAuth2 相关表，地址如下：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;https://github.com/spring-projects/spring-security-oauth/blob/master/spring-security-oauth2/src/test/resources/schema.sql
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;由于我们使用的是 MySQL 数据库，默认建表语句中主键为 &lt;code&gt;VARCHAR(256)&lt;/code&gt;，这超过了最大的主键长度，请手动修改为 &lt;code&gt;128&lt;/code&gt;，并用 &lt;code&gt;BLOB&lt;/code&gt; 替换语句中的 &lt;code&gt;LONGVARBINARY&lt;/code&gt; 类型，修改后的建表脚本如下：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;CREATE TABLE `clientdetails` (
  `appId` varchar(128) NOT NULL,
  `resourceIds` varchar(256) DEFAULT NULL,
  `appSecret` varchar(256) DEFAULT NULL,
  `scope` varchar(256) DEFAULT NULL,
  `grantTypes` varchar(256) DEFAULT NULL,
  `redirectUrl` varchar(256) DEFAULT NULL,
  `authorities` varchar(256) DEFAULT NULL,
  `access_token_validity` int(11) DEFAULT NULL,
  `refresh_token_validity` int(11) DEFAULT NULL,
  `additionalInformation` varchar(4096) DEFAULT NULL,
  `autoApproveScopes` varchar(256) DEFAULT NULL,
  PRIMARY KEY (`appId`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;
CREATE TABLE `oauth_access_token` (
  `token_id` varchar(256) DEFAULT NULL,
  `token` blob,
  `authentication_id` varchar(128) NOT NULL,
  `user_name` varchar(256) DEFAULT NULL,
  `client_id` varchar(256) DEFAULT NULL,
  `authentication` blob,
  `refresh_token` varchar(256) DEFAULT NULL,
  PRIMARY KEY (`authentication_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;
CREATE TABLE `oauth_approvals` (
  `userId` varchar(256) DEFAULT NULL,
  `clientId` varchar(256) DEFAULT NULL,
  `scope` varchar(256) DEFAULT NULL,
  `status` varchar(10) DEFAULT NULL,
  `expiresAt` timestamp NULL DEFAULT NULL,
  `lastModifiedAt` timestamp NULL DEFAULT NULL
) ENGINE=InnoDB DEFAULT CHARSET=utf8;
CREATE TABLE `oauth_client_details` (
  `client_id` varchar(128) NOT NULL,
  `resource_ids` varchar(256) DEFAULT NULL,
  `client_secret` varchar(256) DEFAULT NULL,
  `scope` varchar(256) DEFAULT NULL,
  `authorized_grant_types` varchar(256) DEFAULT NULL,
  `web_server_redirect_uri` varchar(256) DEFAULT NULL,
  `authorities` varchar(256) DEFAULT NULL,
  `access_token_validity` int(11) DEFAULT NULL,
  `refresh_token_validity` int(11) DEFAULT NULL,
  `additional_information` varchar(4096) DEFAULT NULL,
  `autoapprove` varchar(256) DEFAULT NULL,
  PRIMARY KEY (`client_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;
CREATE TABLE `oauth_client_token` (
  `token_id` varchar(256) DEFAULT NULL,
  `token` blob,
  `authentication_id` varchar(128) NOT NULL,
  `user_name` varchar(256) DEFAULT NULL,
  `client_id` varchar(256) DEFAULT NULL,
  PRIMARY KEY (`authentication_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;
CREATE TABLE `oauth_code` (
  `code` varchar(256) DEFAULT NULL,
  `authentication` blob
) ENGINE=InnoDB DEFAULT CHARSET=utf8;
CREATE TABLE `oauth_refresh_token` (
  `token_id` varchar(256) DEFAULT NULL,
  `token` blob,
  `authentication` blob
) ENGINE=InnoDB DEFAULT CHARSET=utf8;
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;在数据库中配置客户端&lt;/h3&gt;
&lt;p&gt;在表 &lt;code&gt;oauth_client_details&lt;/code&gt; 中增加一条客户端配置记录，需要设置的字段如下：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;client_id&lt;/code&gt;：客户端标识&lt;/li&gt;
&lt;li&gt;&lt;code&gt;client_secret&lt;/code&gt;：客户端安全码，&lt;strong&gt;此处不能是明文，需要加密&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;scope&lt;/code&gt;：客户端授权范围&lt;/li&gt;
&lt;li&gt;&lt;code&gt;authorized_grant_types&lt;/code&gt;：客户端授权类型&lt;/li&gt;
&lt;li&gt;&lt;code&gt;web_server_redirect_uri&lt;/code&gt;：服务器回调地址&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;使用 &lt;code&gt;BCryptPasswordEncoder&lt;/code&gt; 为客户端安全码加密，代码如下：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;package com.funtl.spring.security.oauth2.server;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.test.context.junit4.SpringRunner;
@SpringBootTest
@RunWith(SpringRunner.class)
public class OAuth2ServerApplicationTests {
    @Test
    public void testBCryptPasswordEncoder() {
        System.out.println(new BCryptPasswordEncoder().encode(&quot;secret&quot;));
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;数据库配置客户端效果图如下，授权类型填入 &lt;code&gt;authorization_code&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;%E9%85%8D%E7%BD%AE%E6%95%B0%E6%8D%AE%E5%BA%93%E8%AE%A4%E8%AF%81.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;h3&gt;POM&lt;/h3&gt;
&lt;p&gt;由于使用了 JDBC 存储，我们需要增加相关依赖，数据库连接池使用 &lt;strong&gt;HikariCP&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&amp;lt;dependency&amp;gt;
    &amp;lt;groupId&amp;gt;com.zaxxer&amp;lt;/groupId&amp;gt;
    &amp;lt;artifactId&amp;gt;HikariCP&amp;lt;/artifactId&amp;gt;
    &amp;lt;version&amp;gt;${hikaricp.version}&amp;lt;/version&amp;gt;
&amp;lt;/dependency&amp;gt;
&amp;lt;dependency&amp;gt;
    &amp;lt;groupId&amp;gt;org.springframework.boot&amp;lt;/groupId&amp;gt;
    &amp;lt;artifactId&amp;gt;spring-boot-starter-jdbc&amp;lt;/artifactId&amp;gt;
    &amp;lt;exclusions&amp;gt;
        &amp;lt;!-- 排除 tomcat-jdbc 以使用 HikariCP --&amp;gt;
        &amp;lt;exclusion&amp;gt;
            &amp;lt;groupId&amp;gt;org.apache.tomcat&amp;lt;/groupId&amp;gt;
            &amp;lt;artifactId&amp;gt;tomcat-jdbc&amp;lt;/artifactId&amp;gt;
        &amp;lt;/exclusion&amp;gt;
    &amp;lt;/exclusions&amp;gt;
&amp;lt;/dependency&amp;gt;
&amp;lt;dependency&amp;gt;
    &amp;lt;groupId&amp;gt;mysql&amp;lt;/groupId&amp;gt;
    &amp;lt;artifactId&amp;gt;mysql-connector-java&amp;lt;/artifactId&amp;gt;
    &amp;lt;version&amp;gt;${mysql.version}&amp;lt;/version&amp;gt;
&amp;lt;/dependency&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;application.yml&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;spring:
  application:
    name: oauth2-server
  datasource:
    type: com.zaxxer.hikari.HikariDataSource
    driver-class-name: com.mysql.cj.jdbc.Driver
    jdbc-url: jdbc:mysql://192.168.141.130:3306/oauth2?useUnicode=true&amp;amp;characterEncoding=utf-8&amp;amp;useSSL=false
    username: root
    password: 123456
    hikari:
      minimum-idle: 5
      idle-timeout: 600000
      maximum-pool-size: 10
      auto-commit: true
      pool-name: MyHikariCP
      max-lifetime: 1800000
      connection-timeout: 30000
      connection-test-query: SELECT 1
server:
  port: 8080
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Application&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;package com.funtl.spring.security.oauth2.server;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
@SpringBootApplication
public class OAuth2ServerApplication {
    public static void main(String[] args) {
        SpringApplication.run(OAuth2ServerApplication.class, args);
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;配置认证服务器&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;package com.funtl.spring.security.oauth2.server.configure;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.boot.jdbc.DataSourceBuilder;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Primary;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.oauth2.config.annotation.configurers.ClientDetailsServiceConfigurer;
import org.springframework.security.oauth2.config.annotation.web.configuration.AuthorizationServerConfigurerAdapter;
import org.springframework.security.oauth2.config.annotation.web.configuration.EnableAuthorizationServer;
import org.springframework.security.oauth2.config.annotation.web.configurers.AuthorizationServerEndpointsConfigurer;
import org.springframework.security.oauth2.provider.ClientDetailsService;
import org.springframework.security.oauth2.provider.client.JdbcClientDetailsService;
import org.springframework.security.oauth2.provider.token.TokenStore;
import org.springframework.security.oauth2.provider.token.store.JdbcTokenStore;
import javax.sql.DataSource;
@Configuration
@EnableAuthorizationServer
public class AuthorizationServerConfiguration extends AuthorizationServerConfigurerAdapter {
    @Autowired
    private BCryptPasswordEncoder passwordEncoder;
    @Bean
    @Primary
    @ConfigurationProperties(prefix = &quot;spring.datasource&quot;)
    public DataSource dataSource() {
        // 配置数据源（注意，我使用的是 HikariCP 连接池），以上注解是指定数据源，否则会有冲突
        return DataSourceBuilder.create().build();
    }
    @Bean
    public TokenStore tokenStore() {
        // 基于 JDBC 实现，令牌保存到数据库
        return new JdbcTokenStore(dataSource());
    }
    @Bean
    public ClientDetailsService jdbcClientDetailsService() {
        // 基于 JDBC 实现，需要事先在数据库配置客户端信息
        return new JdbcClientDetailsService(dataSource());
    }
    @Override
    public void configure(AuthorizationServerEndpointsConfigurer endpoints) throws Exception {
        // 设置令牌存储模式
        endpoints.tokenStore(tokenStore());
    }
    @Override
    public void configure(ClientDetailsServiceConfigurer clients) throws Exception {
        // 客户端配置
        clients.withClientDetails(jdbcClientDetailsService());
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;访问获取授权码&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;通过浏览器访问&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;http://localhost:8080/oauth/authorize?client_id=client&amp;amp;response_type=code
&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;第一次访问会跳转到登录页面&lt;/li&gt;
&lt;li&gt;验证成功后会询问用户是否授权客户端&lt;/li&gt;
&lt;li&gt;选择授权后会跳转到百度，浏览器地址上还会包含一个授权码（&lt;code&gt;code=1JuO6V&lt;/code&gt;），浏览器地址栏会显示如下地址：&lt;/li&gt;
&lt;li&gt;https://www.baidu.com/?code=1JuO6V&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;RBAC 基于角色的访问控制&lt;/h2&gt;
&lt;h3&gt;概述&lt;/h3&gt;
&lt;p&gt;RBAC（Role-Based Access Control，基于角色的访问控制），就是用户通过角色与权限进行关联。简单地说，一个用户拥有若干角色，每一个角色拥有若干权限。这样，就构造成“用户-角色-权限”的授权模型。在这种模型中，用户与角色之间，角色与权限之间，一般是多对多的关系。（如下图）&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://tangtangit.com/_astro/RBC.BrcZtNOF_Z4TT6g.webp&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;h3&gt;目的&lt;/h3&gt;
&lt;p&gt;在我们的 oAuth2 系统中，我们需要对系统的所有资源进行权限控制，系统中的资源包括：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;静态资源（对象资源）：功能操作、数据列&lt;/li&gt;
&lt;li&gt;动态资源（数据资源）：数据&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;系统的目的就是对应用系统的所有对象资源和数据资源进行权限控制，比如：功能菜单、界面按钮、数据显示的列、各种行级数据进行权限的操控&lt;/p&gt;
&lt;h3&gt;对象关系&lt;/h3&gt;
&lt;h4&gt;权限&lt;/h4&gt;
&lt;p&gt;系统的所有权限信息。权限具有上下级关系，是一个树状的结构。如：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;系统管理
&lt;ul&gt;
&lt;li&gt;用户管理
&lt;ul&gt;
&lt;li&gt;查看用户&lt;/li&gt;
&lt;li&gt;新增用户&lt;/li&gt;
&lt;li&gt;修改用户&lt;/li&gt;
&lt;li&gt;删除用户&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h4&gt;用户&lt;/h4&gt;
&lt;p&gt;系统的具体操作者，可以归属于一个或多个角色，它与角色的关系是多对多的关系&lt;/p&gt;
&lt;h4&gt;角色&lt;/h4&gt;
&lt;p&gt;为了对许多拥有相似权限的用户进行分类管理，定义了角色的概念，例如系统管理员、管理员、用户、访客等角色。角色具有上下级关系，可以形成树状视图，父级角色的权限是自身及它的所有子角色的权限的综合。父级角色的用户、父级角色的组同理可推。&lt;/p&gt;
&lt;h4&gt;关系图&lt;/h4&gt;
&lt;p&gt;&lt;img src=&quot;rbac%E5%85%B3%E7%B3%BB%E5%9B%BE.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;h4&gt;模块图&lt;/h4&gt;
&lt;p&gt;&lt;img src=&quot;ruac%E6%A8%A1%E5%9D%97%E5%9B%BE.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;h3&gt;表结构&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;CREATE TABLE `tb_permission` (
  `id` bigint(20) NOT NULL AUTO_INCREMENT,
  `parent_id` bigint(20) DEFAULT NULL COMMENT &apos;父权限&apos;,
  `name` varchar(64) NOT NULL COMMENT &apos;权限名称&apos;,
  `enname` varchar(64) NOT NULL COMMENT &apos;权限英文名称&apos;,
  `url` varchar(255) NOT NULL COMMENT &apos;授权路径&apos;,
  `description` varchar(200) DEFAULT NULL COMMENT &apos;备注&apos;,
  `created` datetime NOT NULL,
  `updated` datetime NOT NULL,
  PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=37 DEFAULT CHARSET=utf8 COMMENT=&apos;权限表&apos;;
CREATE TABLE `tb_role` (
  `id` bigint(20) NOT NULL AUTO_INCREMENT,
  `parent_id` bigint(20) DEFAULT NULL COMMENT &apos;父角色&apos;,
  `name` varchar(64) NOT NULL COMMENT &apos;角色名称&apos;,
  `enname` varchar(64) NOT NULL COMMENT &apos;角色英文名称&apos;,
  `description` varchar(200) DEFAULT NULL COMMENT &apos;备注&apos;,
  `created` datetime NOT NULL,
  `updated` datetime NOT NULL,
  PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=37 DEFAULT CHARSET=utf8 COMMENT=&apos;角色表&apos;;
CREATE TABLE `tb_role_permission` (
  `id` bigint(20) NOT NULL AUTO_INCREMENT,
  `role_id` bigint(20) NOT NULL COMMENT &apos;角色 ID&apos;,
  `permission_id` bigint(20) NOT NULL COMMENT &apos;权限 ID&apos;,
  PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=37 DEFAULT CHARSET=utf8 COMMENT=&apos;角色权限表&apos;;
CREATE TABLE `tb_user` (
  `id` bigint(20) NOT NULL AUTO_INCREMENT,
  `username` varchar(50) NOT NULL COMMENT &apos;用户名&apos;,
  `password` varchar(64) NOT NULL COMMENT &apos;密码，加密存储&apos;,
  `phone` varchar(20) DEFAULT NULL COMMENT &apos;注册手机号&apos;,
  `email` varchar(50) DEFAULT NULL COMMENT &apos;注册邮箱&apos;,
  `created` datetime NOT NULL,
  `updated` datetime NOT NULL,
  PRIMARY KEY (`id`),
  UNIQUE KEY `username` (`username`) USING BTREE,
  UNIQUE KEY `phone` (`phone`) USING BTREE,
  UNIQUE KEY `email` (`email`) USING BTREE
) ENGINE=InnoDB AUTO_INCREMENT=37 DEFAULT CHARSET=utf8 COMMENT=&apos;用户表&apos;;
CREATE TABLE `tb_user_role` (
  `id` bigint(20) NOT NULL AUTO_INCREMENT,
  `user_id` bigint(20) NOT NULL COMMENT &apos;用户 ID&apos;,
  `role_id` bigint(20) NOT NULL COMMENT &apos;角色 ID&apos;,
  PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=37 DEFAULT CHARSET=utf8 COMMENT=&apos;用户角色表&apos;;
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;基于 RBAC 的自定义认证&lt;/h2&gt;
&lt;h3&gt;概述&lt;/h3&gt;
&lt;p&gt;在实际开发中，我们的用户信息都是存在数据库里的，本章节基于 &lt;strong&gt;RBAC 模型&lt;/strong&gt; 将用户的认证信息与数据库对接，实现真正的用户认证与授权&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;操作流程&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;继续 &lt;strong&gt;基于 JDBC 存储令牌&lt;/strong&gt; 章节的代码开发&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;初始化 RBAC 相关表&lt;/li&gt;
&lt;li&gt;在数据库中配置“用户”、“角色”、“权限”相关信息&lt;/li&gt;
&lt;li&gt;数据库操作使用 &lt;code&gt;tk.mybatis&lt;/code&gt; 框架，故需要增加相关依赖&lt;/li&gt;
&lt;li&gt;配置 Web 安全
&lt;ul&gt;
&lt;li&gt;配置使用自定义认证与授权&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;通过 GET 请求访问认证服务器获取授权码
&lt;ul&gt;
&lt;li&gt;端点：&lt;code&gt;/oauth/authorize&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;通过 POST 请求利用授权码访问认证服务器获取令牌
&lt;ul&gt;
&lt;li&gt;端点：&lt;code&gt;/oauth/token&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;默认的端点 URL&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;/oauth/authorize&lt;/code&gt;：授权端点&lt;/li&gt;
&lt;li&gt;&lt;code&gt;/oauth/token&lt;/code&gt;：令牌端点&lt;/li&gt;
&lt;li&gt;&lt;code&gt;/oauth/confirm_access&lt;/code&gt;：用户确认授权提交端点&lt;/li&gt;
&lt;li&gt;&lt;code&gt;/oauth/error&lt;/code&gt;：授权服务错误信息端点&lt;/li&gt;
&lt;li&gt;&lt;code&gt;/oauth/check_token&lt;/code&gt;：用于资源服务访问的令牌解析端点&lt;/li&gt;
&lt;li&gt;&lt;code&gt;/oauth/token_key&lt;/code&gt;：提供公有密匙的端点，如果你使用 JWT 令牌的话&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;初始化 RBAC 相关表&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;CREATE TABLE `tb_permission` (
  `id` bigint(20) NOT NULL AUTO_INCREMENT,
  `parent_id` bigint(20) DEFAULT NULL COMMENT &apos;父权限&apos;,
  `name` varchar(64) NOT NULL COMMENT &apos;权限名称&apos;,
  `enname` varchar(64) NOT NULL COMMENT &apos;权限英文名称&apos;,
  `url` varchar(255) NOT NULL COMMENT &apos;授权路径&apos;,
  `description` varchar(200) DEFAULT NULL COMMENT &apos;备注&apos;,
  `created` datetime NOT NULL,
  `updated` datetime NOT NULL,
  PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=44 DEFAULT CHARSET=utf8 COMMENT=&apos;权限表&apos;;
insert  into `tb_permission`(`id`,`parent_id`,`name`,`enname`,`url`,`description`,`created`,`updated`) values 
(37,0,&apos;系统管理&apos;,&apos;System&apos;,&apos;/&apos;,NULL,&apos;2019-04-04 23:22:54&apos;,&apos;2019-04-04 23:22:56&apos;),
(38,37,&apos;用户管理&apos;,&apos;SystemUser&apos;,&apos;/users/&apos;,NULL,&apos;2019-04-04 23:25:31&apos;,&apos;2019-04-04 23:25:33&apos;),
(39,38,&apos;查看用户&apos;,&apos;SystemUserView&apos;,&apos;&apos;,NULL,&apos;2019-04-04 15:30:30&apos;,&apos;2019-04-04 15:30:43&apos;),
(40,38,&apos;新增用户&apos;,&apos;SystemUserInsert&apos;,&apos;&apos;,NULL,&apos;2019-04-04 15:30:31&apos;,&apos;2019-04-04 15:30:44&apos;),
(41,38,&apos;编辑用户&apos;,&apos;SystemUserUpdate&apos;,&apos;&apos;,NULL,&apos;2019-04-04 15:30:32&apos;,&apos;2019-04-04 15:30:45&apos;),
(42,38,&apos;删除用户&apos;,&apos;SystemUserDelete&apos;,&apos;&apos;,NULL,&apos;2019-04-04 15:30:48&apos;,&apos;2019-04-04 15:30:45&apos;);
CREATE TABLE `tb_role` (
  `id` bigint(20) NOT NULL AUTO_INCREMENT,
  `parent_id` bigint(20) DEFAULT NULL COMMENT &apos;父角色&apos;,
  `name` varchar(64) NOT NULL COMMENT &apos;角色名称&apos;,
  `enname` varchar(64) NOT NULL COMMENT &apos;角色英文名称&apos;,
  `description` varchar(200) DEFAULT NULL COMMENT &apos;备注&apos;,
  `created` datetime NOT NULL,
  `updated` datetime NOT NULL,
  PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=38 DEFAULT CHARSET=utf8 COMMENT=&apos;角色表&apos;;
insert  into `tb_role`(`id`,`parent_id`,`name`,`enname`,`description`,`created`,`updated`) values 
(37,0,&apos;超级管理员&apos;,&apos;admin&apos;,NULL,&apos;2019-04-04 23:22:03&apos;,&apos;2019-04-04 23:22:05&apos;);
CREATE TABLE `tb_role_permission` (
  `id` bigint(20) NOT NULL AUTO_INCREMENT,
  `role_id` bigint(20) NOT NULL COMMENT &apos;角色 ID&apos;,
  `permission_id` bigint(20) NOT NULL COMMENT &apos;权限 ID&apos;,
  PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=43 DEFAULT CHARSET=utf8 COMMENT=&apos;角色权限表&apos;;
insert  into `tb_role_permission`(`id`,`role_id`,`permission_id`) values 
(37,37,37),
(38,37,38),
(39,37,39),
(40,37,40),
(41,37,41),
(42,37,42);
CREATE TABLE `tb_user` (
  `id` bigint(20) NOT NULL AUTO_INCREMENT,
  `username` varchar(50) NOT NULL COMMENT &apos;用户名&apos;,
  `password` varchar(64) NOT NULL COMMENT &apos;密码，加密存储&apos;,
  `phone` varchar(20) DEFAULT NULL COMMENT &apos;注册手机号&apos;,
  `email` varchar(50) DEFAULT NULL COMMENT &apos;注册邮箱&apos;,
  `created` datetime NOT NULL,
  `updated` datetime NOT NULL,
  PRIMARY KEY (`id`),
  UNIQUE KEY `username` (`username`) USING BTREE,
  UNIQUE KEY `phone` (`phone`) USING BTREE,
  UNIQUE KEY `email` (`email`) USING BTREE
) ENGINE=InnoDB AUTO_INCREMENT=38 DEFAULT CHARSET=utf8 COMMENT=&apos;用户表&apos;;
insert  into `tb_user`(`id`,`username`,`password`,`phone`,`email`,`created`,`updated`) values 
(37,&apos;admin&apos;,&apos;$2a$10$9ZhDOBp.sRKat4l14ygu/.LscxrMUcDAfeVOEPiYwbcRkoB09gCmi&apos;,&apos;15888888888&apos;,&apos;lee.lusifer@gmail.com&apos;,&apos;2019-04-04 23:21:27&apos;,&apos;2019-04-04 23:21:29&apos;);
CREATE TABLE `tb_user_role` (
  `id` bigint(20) NOT NULL AUTO_INCREMENT,
  `user_id` bigint(20) NOT NULL COMMENT &apos;用户 ID&apos;,
  `role_id` bigint(20) NOT NULL COMMENT &apos;角色 ID&apos;,
  PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=38 DEFAULT CHARSET=utf8 COMMENT=&apos;用户角色表&apos;;
insert  into `tb_user_role`(`id`,`user_id`,`role_id`) values 
(37,37,37);
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;由于使用了 &lt;code&gt;BCryptPasswordEncoder&lt;/code&gt; 的加密方式，故用户密码需要加密，代码如下：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;System.out.println(new BCryptPasswordEncoder().encode(&quot;123456&quot;));
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;POM&lt;/h3&gt;
&lt;p&gt;数据库操作采用 &lt;code&gt;tk.mybatis:mapper-spring-boot-starter:2.1.5&lt;/code&gt; 框架，需增加相关依赖，完整 POM 如下：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&amp;lt;?xml version=&quot;1.0&quot; encoding=&quot;UTF-8&quot;?&amp;gt;
&amp;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;&amp;gt;
    &amp;lt;modelVersion&amp;gt;4.0.0&amp;lt;/modelVersion&amp;gt;
    &amp;lt;parent&amp;gt;
        &amp;lt;groupId&amp;gt;com.funtl&amp;lt;/groupId&amp;gt;
        &amp;lt;artifactId&amp;gt;hello-spring-security-oauth2&amp;lt;/artifactId&amp;gt;
        &amp;lt;version&amp;gt;0.0.1-SNAPSHOT&amp;lt;/version&amp;gt;
    &amp;lt;/parent&amp;gt;
    &amp;lt;artifactId&amp;gt;hello-spring-security-oauth2-server&amp;lt;/artifactId&amp;gt;
    &amp;lt;url&amp;gt;http://www.funtl.com&amp;lt;/url&amp;gt;
    &amp;lt;licenses&amp;gt;
        &amp;lt;license&amp;gt;
            &amp;lt;name&amp;gt;Apache 2.0&amp;lt;/name&amp;gt;
            &amp;lt;url&amp;gt;https://www.apache.org/licenses/LICENSE-2.0.txt&amp;lt;/url&amp;gt;
        &amp;lt;/license&amp;gt;
    &amp;lt;/licenses&amp;gt;
    &amp;lt;developers&amp;gt;
        &amp;lt;developer&amp;gt;
            &amp;lt;id&amp;gt;liwemin&amp;lt;/id&amp;gt;
            &amp;lt;name&amp;gt;Lusifer Lee&amp;lt;/name&amp;gt;
            &amp;lt;email&amp;gt;lee.lusifer@gmail.com&amp;lt;/email&amp;gt;
        &amp;lt;/developer&amp;gt;
    &amp;lt;/developers&amp;gt;
    &amp;lt;dependencies&amp;gt;
        &amp;lt;!-- Spring Boot --&amp;gt;
        &amp;lt;dependency&amp;gt;
            &amp;lt;groupId&amp;gt;org.springframework.boot&amp;lt;/groupId&amp;gt;
            &amp;lt;artifactId&amp;gt;spring-boot-starter-web&amp;lt;/artifactId&amp;gt;
        &amp;lt;/dependency&amp;gt;
        &amp;lt;dependency&amp;gt;
            &amp;lt;groupId&amp;gt;org.springframework.boot&amp;lt;/groupId&amp;gt;
            &amp;lt;artifactId&amp;gt;spring-boot-starter-test&amp;lt;/artifactId&amp;gt;
        &amp;lt;/dependency&amp;gt;
        &amp;lt;!-- Spring Security --&amp;gt;
        &amp;lt;dependency&amp;gt;
            &amp;lt;groupId&amp;gt;org.springframework.cloud&amp;lt;/groupId&amp;gt;
            &amp;lt;artifactId&amp;gt;spring-cloud-starter-oauth2&amp;lt;/artifactId&amp;gt;
        &amp;lt;/dependency&amp;gt;
        &amp;lt;!-- CP --&amp;gt;
        &amp;lt;dependency&amp;gt;
            &amp;lt;groupId&amp;gt;com.zaxxer&amp;lt;/groupId&amp;gt;
            &amp;lt;artifactId&amp;gt;HikariCP&amp;lt;/artifactId&amp;gt;
            &amp;lt;version&amp;gt;${hikaricp.version}&amp;lt;/version&amp;gt;
        &amp;lt;/dependency&amp;gt;
        &amp;lt;dependency&amp;gt;
            &amp;lt;groupId&amp;gt;org.springframework.boot&amp;lt;/groupId&amp;gt;
            &amp;lt;artifactId&amp;gt;spring-boot-starter-jdbc&amp;lt;/artifactId&amp;gt;
            &amp;lt;exclusions&amp;gt;
                &amp;lt;!-- 排除 tomcat-jdbc 以使用 HikariCP --&amp;gt;
                &amp;lt;exclusion&amp;gt;
                    &amp;lt;groupId&amp;gt;org.apache.tomcat&amp;lt;/groupId&amp;gt;
                    &amp;lt;artifactId&amp;gt;tomcat-jdbc&amp;lt;/artifactId&amp;gt;
                &amp;lt;/exclusion&amp;gt;
            &amp;lt;/exclusions&amp;gt;
        &amp;lt;/dependency&amp;gt;
        &amp;lt;dependency&amp;gt;
            &amp;lt;groupId&amp;gt;mysql&amp;lt;/groupId&amp;gt;
            &amp;lt;artifactId&amp;gt;mysql-connector-java&amp;lt;/artifactId&amp;gt;
            &amp;lt;version&amp;gt;${mysql.version}&amp;lt;/version&amp;gt;
        &amp;lt;/dependency&amp;gt;
        &amp;lt;dependency&amp;gt;
            &amp;lt;groupId&amp;gt;tk.mybatis&amp;lt;/groupId&amp;gt;
            &amp;lt;artifactId&amp;gt;mapper-spring-boot-starter&amp;lt;/artifactId&amp;gt;
        &amp;lt;/dependency&amp;gt;
    &amp;lt;/dependencies&amp;gt;
    &amp;lt;build&amp;gt;
        &amp;lt;plugins&amp;gt;
            &amp;lt;plugin&amp;gt;
                &amp;lt;groupId&amp;gt;org.springframework.boot&amp;lt;/groupId&amp;gt;
                &amp;lt;artifactId&amp;gt;spring-boot-maven-plugin&amp;lt;/artifactId&amp;gt;
                &amp;lt;configuration&amp;gt;
                    &amp;lt;mainClass&amp;gt;com.funtl.spring.security.oauth2.server.OAuth2ServerApplication&amp;lt;/mainClass&amp;gt;
                &amp;lt;/configuration&amp;gt;
            &amp;lt;/plugin&amp;gt;
        &amp;lt;/plugins&amp;gt;
    &amp;lt;/build&amp;gt;
&amp;lt;/project&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;application.yml&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;spring:
  application:
    name: oauth2-server
  datasource:
    type: com.zaxxer.hikari.HikariDataSource
    driver-class-name: com.mysql.cj.jdbc.Driver
    jdbc-url: jdbc:mysql://192.168.141.130:3306/oauth2?useUnicode=true&amp;amp;characterEncoding=utf-8&amp;amp;useSSL=false
    username: root
    password: 123456
    hikari:
      minimum-idle: 5
      idle-timeout: 600000
      maximum-pool-size: 10
      auto-commit: true
      pool-name: MyHikariCP
      max-lifetime: 1800000
      connection-timeout: 30000
      connection-test-query: SELECT 1
server:
  port: 8080
mybatis:
  type-aliases-package: com.funtl.spring.security.oauth2.server.domain
  mapper-locations: classpath:mapper/*.xml
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Application&lt;/h3&gt;
&lt;p&gt;增加了 Mapper 的包扫描配置&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;package com.funtl.spring.security.oauth2.server;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import tk.mybatis.spring.annotation.MapperScan;
@SpringBootApplication
@MapperScan(basePackages = &quot;com.funtl.spring.security.oauth2.server.mapper&quot;)
public class OAuth2ServerApplication {
    public static void main(String[] args) {
        SpringApplication.run(OAuth2ServerApplication.class, args);
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;关键步骤&lt;/h3&gt;
&lt;p&gt;由于本次增加了 MyBatis 相关操作，代码增加较多，可以参考我 &lt;a href=&quot;http://www.qfdmy.com/wp-content/themes/quanbaike/go.php?url=aHR0cHM6Ly9naXRodWIuY29tL3RvcHNhbGUvc3ByaW5nLWJvb3Qtc2FtcGxlcy90cmVlL21hc3Rlci9zcHJpbmctc2VjdXJpdHktb2F1dGgy&quot;&gt;&lt;strong&gt;GitHub&lt;/strong&gt;&lt;/a&gt; 上的源码，下面仅列出关键步骤及代码&lt;/p&gt;
&lt;h4&gt;获取用户信息&lt;/h4&gt;
&lt;p&gt;目的是为了实现自定义认证授权时可以通过数据库查询用户信息，Spring Security oAuth2 要求使用 &lt;code&gt;username&lt;/code&gt; 的方式查询，提供相关用户信息后，认证工作由框架自行完成&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;package com.funtl.spring.security.oauth2.server.service.impl;
import com.funtl.spring.security.oauth2.server.domain.TbUser;
import com.funtl.spring.security.oauth2.server.mapper.TbUserMapper;
import com.funtl.spring.security.oauth2.server.service.TbUserService;
import org.springframework.stereotype.Service;
import tk.mybatis.mapper.entity.Example;
import javax.annotation.Resource;
@Service
public class TbUserServiceImpl implements TbUserService {
    @Resource
    private TbUserMapper tbUserMapper;
    @Override
    public TbUser getByUsername(String username) {
        Example example = new Example(TbUser.class);
        example.createCriteria().andEqualTo(&quot;username&quot;, username);
        return tbUserMapper.selectOneByExample(example);
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h4&gt;获取用户权限信息&lt;/h4&gt;
&lt;p&gt;认证成功后需要给用户授权，具体的权限已经存储在数据库里了&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;package com.funtl.spring.security.oauth2.server.mapper;
import com.funtl.spring.security.oauth2.server.domain.TbPermission;
import org.apache.ibatis.annotations.Param;
import tk.mybatis.mapper.MyMapper;
import java.util.List;
public interface TbPermissionMapper extends MyMapper&amp;lt;TbPermission&amp;gt; {
    List&amp;lt;TbPermission&amp;gt; selectByUserId(@Param(&quot;id&quot;) Long id);
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;userdetailservice实现类&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;@Service
public class UserDetailServiceImpl implements UserDetailsService {

    @Resource
    private TbUserMapper tbUserMapper;

    @Override
    public UserDetails loadUserByUsername(String s) throws UsernameNotFoundException {
        QueryWrapper&amp;lt;TbUser&amp;gt; wrapper = new QueryWrapper&amp;lt;&amp;gt;();
        wrapper.eq(&quot;username&quot;,s);
        TbUser tbUser = tbUserMapper.selectOne(wrapper);
        List&amp;lt;GrantedAuthority&amp;gt; list= Lists.newArrayList();

        if(tbUser != null){
            List&amp;lt;TbPermission&amp;gt; userPermission = tbUserMapper.getUserPermission(tbUser.getId());
            userPermission.forEach(tbPermission -&amp;gt; {
                SimpleGrantedAuthority simpleGrantedAuthority = new SimpleGrantedAuthority(tbPermission.getEnname());
                list.add(simpleGrantedAuthority);
            });
            return new User(tbUser.getUsername(),tbUser.getPassword(),list);
        }
        return null;
    }
}

&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;自定义的WebSecurityConfiguration&lt;/p&gt;
&lt;p&gt;注入userdetailservice实现类&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;    @Bean
    public UserDetailsService userDetailService(){
        return new UserDetailServiceImpl();
    }

    @Override
    protected void configure(AuthenticationManagerBuilder auth) throws Exception {
//        super.configure(auth);
//        auth.inMemoryAuthentication()
//                .withUser(&quot;user&quot;).password(passwordEncoder().encode(&quot;123456&quot;)).roles(&quot;USER&quot;)
//                .and()
//                .withUser(&quot;admin&quot;).password(passwordEncoder().encode(&quot;admin888&quot;)).roles(&quot;ADMIN&quot;);
        auth.userDetailsService(userDetailService());
    }
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;&amp;lt;?xml version=&quot;1.0&quot; encoding=&quot;UTF-8&quot;?&amp;gt;
&amp;lt;!DOCTYPE mapper PUBLIC &quot;-//mybatis.org//DTD Mapper 3.0//EN&quot; &quot;http://mybatis.org/dtd/mybatis-3-mapper.dtd&quot;&amp;gt;
&amp;lt;mapper namespace=&quot;com.funtl.spring.security.oauth2.server.mapper.TbPermissionMapper&quot;&amp;gt;
  &amp;lt;resultMap id=&quot;BaseResultMap&quot; type=&quot;com.funtl.spring.security.oauth2.server.domain.TbPermission&quot;&amp;gt;
    &amp;lt;!--@mbg.generated generated on Tue Jul 16 00:41:48 CST 2019.--&amp;gt;
    &amp;lt;id column=&quot;id&quot; jdbcType=&quot;BIGINT&quot; property=&quot;id&quot; /&amp;gt;
    &amp;lt;result column=&quot;parent_id&quot; jdbcType=&quot;BIGINT&quot; property=&quot;parentId&quot; /&amp;gt;
    &amp;lt;result column=&quot;name&quot; jdbcType=&quot;VARCHAR&quot; property=&quot;name&quot; /&amp;gt;
    &amp;lt;result column=&quot;enname&quot; jdbcType=&quot;VARCHAR&quot; property=&quot;enname&quot; /&amp;gt;
    &amp;lt;result column=&quot;url&quot; jdbcType=&quot;VARCHAR&quot; property=&quot;url&quot; /&amp;gt;
    &amp;lt;result column=&quot;description&quot; jdbcType=&quot;VARCHAR&quot; property=&quot;description&quot; /&amp;gt;
    &amp;lt;result column=&quot;created&quot; jdbcType=&quot;TIMESTAMP&quot; property=&quot;created&quot; /&amp;gt;
    &amp;lt;result column=&quot;updated&quot; jdbcType=&quot;TIMESTAMP&quot; property=&quot;updated&quot; /&amp;gt;
  &amp;lt;/resultMap&amp;gt;
  &amp;lt;sql id=&quot;Base_Column_List&quot;&amp;gt;
    &amp;lt;!--@mbg.generated generated on Tue Jul 16 00:41:48 CST 2019.--&amp;gt;
    id, parent_id, `name`, enname, url, description, created, updated
  &amp;lt;/sql&amp;gt;
  &amp;lt;select id=&quot;selectByUserId&quot; resultMap=&quot;BaseResultMap&quot;&amp;gt;
    SELECT
      p.*
    FROM
      tb_user AS u
      LEFT JOIN tb_user_role AS ur
        ON u.id = ur.user_id
      LEFT JOIN tb_role AS r
        ON r.id = ur.role_id
      LEFT JOIN tb_role_permission AS rp
        ON r.id = rp.role_id
      LEFT JOIN tb_permission AS p
        ON p.id = rp.permission_id
    WHERE u.id = #{id}
  &amp;lt;/select&amp;gt;
&amp;lt;/mapper&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;创建资源服务器&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;初始化资源服务器数据库&lt;/li&gt;
&lt;li&gt;POM 所需依赖同认证服务器&lt;/li&gt;
&lt;li&gt;配置资源服务器&lt;/li&gt;
&lt;li&gt;配置资源(Controller)&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;初始化资源服务器数据库&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;CREATE TABLE `tb_content` (
  `id` bigint(20) NOT NULL AUTO_INCREMENT,
  `category_id` bigint(20) NOT NULL COMMENT &apos;内容类目ID&apos;,
  `title` varchar(200) DEFAULT NULL COMMENT &apos;内容标题&apos;,
  `sub_title` varchar(100) DEFAULT NULL COMMENT &apos;子标题&apos;,
  `title_desc` varchar(500) DEFAULT NULL COMMENT &apos;标题描述&apos;,
  `url` varchar(500) DEFAULT NULL COMMENT &apos;链接&apos;,
  `pic` varchar(300) DEFAULT NULL COMMENT &apos;图片绝对路径&apos;,
  `pic2` varchar(300) DEFAULT NULL COMMENT &apos;图片2&apos;,
  `content` text COMMENT &apos;内容&apos;,
  `created` datetime DEFAULT NULL,
  `updated` datetime DEFAULT NULL,
  PRIMARY KEY (`id`),
  KEY `category_id` (`category_id`),
  KEY `updated` (`updated`)
) ENGINE=InnoDB AUTO_INCREMENT=42 DEFAULT CHARSET=utf8;
insert  into `tb_content`(`id`,`category_id`,`title`,`sub_title`,`title_desc`,`url`,`pic`,`pic2`,`content`,`created`,`updated`) values 
(28,89,&apos;标题&apos;,&apos;子标题&apos;,&apos;标题说明&apos;,&apos;http://www.jd.com&apos;,NULL,NULL,NULL,&apos;2019-04-07 00:56:09&apos;,&apos;2019-04-07 00:56:11&apos;),
(29,89,&apos;ad2&apos;,&apos;ad2&apos;,&apos;ad2&apos;,&apos;http://www.baidu.com&apos;,NULL,NULL,NULL,&apos;2019-04-07 00:56:13&apos;,&apos;2019-04-07 00:56:15&apos;),
(30,89,&apos;ad3&apos;,&apos;ad3&apos;,&apos;ad3&apos;,&apos;http://www.sina.com.cn&apos;,NULL,NULL,NULL,&apos;2019-04-07 00:56:17&apos;,&apos;2019-04-07 00:56:19&apos;),
(31,89,&apos;ad4&apos;,&apos;ad4&apos;,&apos;ad4&apos;,&apos;http://www.funtl.com&apos;,NULL,NULL,NULL,&apos;2019-04-07 00:56:22&apos;,&apos;2019-04-07 00:56:25&apos;);
CREATE TABLE `tb_content_category` (
  `id` bigint(20) NOT NULL AUTO_INCREMENT COMMENT &apos;类目ID&apos;,
  `parent_id` bigint(20) DEFAULT NULL COMMENT &apos;父类目ID=0时，代表的是一级的类目&apos;,
  `name` varchar(50) DEFAULT NULL COMMENT &apos;分类名称&apos;,
  `status` int(1) DEFAULT &apos;1&apos; COMMENT &apos;状态。可选值:1(正常),2(删除)&apos;,
  `sort_order` int(4) DEFAULT NULL COMMENT &apos;排列序号，表示同级类目的展现次序，如数值相等则按名称次序排列。取值范围:大于零的整数&apos;,
  `is_parent` tinyint(1) DEFAULT &apos;1&apos; COMMENT &apos;该类目是否为父类目，1为true，0为false&apos;,
  `created` datetime DEFAULT NULL COMMENT &apos;创建时间&apos;,
  `updated` datetime DEFAULT NULL COMMENT &apos;创建时间&apos;,
  PRIMARY KEY (`id`),
  KEY `parent_id` (`parent_id`,`status`) USING BTREE,
  KEY `sort_order` (`sort_order`)
) ENGINE=InnoDB AUTO_INCREMENT=98 DEFAULT CHARSET=utf8 COMMENT=&apos;内容分类&apos;;
insert  into `tb_content_category`(`id`,`parent_id`,`name`,`status`,`sort_order`,`is_parent`,`created`,`updated`) values 
(30,0,&apos;LeeShop&apos;,1,1,1,&apos;2015-04-03 16:51:38&apos;,&apos;2015-04-03 16:51:40&apos;),
(86,30,&apos;首页&apos;,1,1,1,&apos;2015-06-07 15:36:07&apos;,&apos;2015-06-07 15:36:07&apos;),
(87,30,&apos;列表页面&apos;,1,1,1,&apos;2015-06-07 15:36:16&apos;,&apos;2015-06-07 15:36:16&apos;),
(88,30,&apos;详细页面&apos;,1,1,1,&apos;2015-06-07 15:36:27&apos;,&apos;2015-06-07 15:36:27&apos;),
(89,86,&apos;大广告&apos;,1,1,0,&apos;2015-06-07 15:36:38&apos;,&apos;2015-06-07 15:36:38&apos;),
(90,86,&apos;小广告&apos;,1,1,0,&apos;2015-06-07 15:36:45&apos;,&apos;2015-06-07 15:36:45&apos;),
(91,86,&apos;商城快报&apos;,1,1,0,&apos;2015-06-07 15:36:55&apos;,&apos;2015-06-07 15:36:55&apos;),
(92,87,&apos;边栏广告&apos;,1,1,0,&apos;2015-06-07 15:37:07&apos;,&apos;2015-06-07 15:37:07&apos;),
(93,87,&apos;页头广告&apos;,1,1,0,&apos;2015-06-07 15:37:17&apos;,&apos;2015-06-07 15:37:17&apos;),
(94,87,&apos;页脚广告&apos;,1,1,0,&apos;2015-06-07 15:37:31&apos;,&apos;2015-06-07 15:37:31&apos;),
(95,88,&apos;边栏广告&apos;,1,1,0,&apos;2015-06-07 15:37:56&apos;,&apos;2015-06-07 15:37:56&apos;),
(96,86,&apos;中广告&apos;,1,1,1,&apos;2015-07-25 18:58:52&apos;,&apos;2015-07-25 18:58:52&apos;),
(97,96,&apos;中广告1&apos;,1,1,0,&apos;2015-07-25 18:59:43&apos;,&apos;2015-07-25 18:59:43&apos;);
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;POM&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;&amp;lt;?xml version=&quot;1.0&quot; encoding=&quot;UTF-8&quot;?&amp;gt;
&amp;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;&amp;gt;
    &amp;lt;modelVersion&amp;gt;4.0.0&amp;lt;/modelVersion&amp;gt;
    &amp;lt;parent&amp;gt;
        &amp;lt;groupId&amp;gt;com.funtl&amp;lt;/groupId&amp;gt;
        &amp;lt;artifactId&amp;gt;hello-spring-security-oauth2&amp;lt;/artifactId&amp;gt;
        &amp;lt;version&amp;gt;0.0.1-SNAPSHOT&amp;lt;/version&amp;gt;
    &amp;lt;/parent&amp;gt;
    &amp;lt;artifactId&amp;gt;hello-spring-security-oauth2-resource&amp;lt;/artifactId&amp;gt;
    &amp;lt;url&amp;gt;http://www.funtl.com&amp;lt;/url&amp;gt;
    &amp;lt;licenses&amp;gt;
        &amp;lt;license&amp;gt;
            &amp;lt;name&amp;gt;Apache 2.0&amp;lt;/name&amp;gt;
            &amp;lt;url&amp;gt;https://www.apache.org/licenses/LICENSE-2.0.txt&amp;lt;/url&amp;gt;
        &amp;lt;/license&amp;gt;
    &amp;lt;/licenses&amp;gt;
    &amp;lt;developers&amp;gt;
        &amp;lt;developer&amp;gt;
            &amp;lt;id&amp;gt;liwemin&amp;lt;/id&amp;gt;
            &amp;lt;name&amp;gt;Lusifer Lee&amp;lt;/name&amp;gt;
            &amp;lt;email&amp;gt;lee.lusifer@gmail.com&amp;lt;/email&amp;gt;
        &amp;lt;/developer&amp;gt;
    &amp;lt;/developers&amp;gt;
    &amp;lt;dependencies&amp;gt;
        &amp;lt;!-- Spring Boot --&amp;gt;
        &amp;lt;dependency&amp;gt;
            &amp;lt;groupId&amp;gt;org.springframework.boot&amp;lt;/groupId&amp;gt;
            &amp;lt;artifactId&amp;gt;spring-boot-starter-web&amp;lt;/artifactId&amp;gt;
        &amp;lt;/dependency&amp;gt;
        &amp;lt;dependency&amp;gt;
            &amp;lt;groupId&amp;gt;org.springframework.boot&amp;lt;/groupId&amp;gt;
            &amp;lt;artifactId&amp;gt;spring-boot-starter-test&amp;lt;/artifactId&amp;gt;
        &amp;lt;/dependency&amp;gt;
        &amp;lt;!-- Spring Security --&amp;gt;
        &amp;lt;dependency&amp;gt;
            &amp;lt;groupId&amp;gt;org.springframework.cloud&amp;lt;/groupId&amp;gt;
            &amp;lt;artifactId&amp;gt;spring-cloud-starter-oauth2&amp;lt;/artifactId&amp;gt;
        &amp;lt;/dependency&amp;gt;
        &amp;lt;!-- CP --&amp;gt;
        &amp;lt;dependency&amp;gt;
            &amp;lt;groupId&amp;gt;com.zaxxer&amp;lt;/groupId&amp;gt;
            &amp;lt;artifactId&amp;gt;HikariCP&amp;lt;/artifactId&amp;gt;
            &amp;lt;version&amp;gt;${hikaricp.version}&amp;lt;/version&amp;gt;
        &amp;lt;/dependency&amp;gt;
        &amp;lt;dependency&amp;gt;
            &amp;lt;groupId&amp;gt;org.springframework.boot&amp;lt;/groupId&amp;gt;
            &amp;lt;artifactId&amp;gt;spring-boot-starter-jdbc&amp;lt;/artifactId&amp;gt;
            &amp;lt;exclusions&amp;gt;
                &amp;lt;!-- 排除 tomcat-jdbc 以使用 HikariCP --&amp;gt;
                &amp;lt;exclusion&amp;gt;
                    &amp;lt;groupId&amp;gt;org.apache.tomcat&amp;lt;/groupId&amp;gt;
                    &amp;lt;artifactId&amp;gt;tomcat-jdbc&amp;lt;/artifactId&amp;gt;
                &amp;lt;/exclusion&amp;gt;
            &amp;lt;/exclusions&amp;gt;
        &amp;lt;/dependency&amp;gt;
        &amp;lt;dependency&amp;gt;
            &amp;lt;groupId&amp;gt;mysql&amp;lt;/groupId&amp;gt;
            &amp;lt;artifactId&amp;gt;mysql-connector-java&amp;lt;/artifactId&amp;gt;
            &amp;lt;version&amp;gt;${mysql.version}&amp;lt;/version&amp;gt;
        &amp;lt;/dependency&amp;gt;
        &amp;lt;dependency&amp;gt;
            &amp;lt;groupId&amp;gt;tk.mybatis&amp;lt;/groupId&amp;gt;
            &amp;lt;artifactId&amp;gt;mapper-spring-boot-starter&amp;lt;/artifactId&amp;gt;
        &amp;lt;/dependency&amp;gt;
        &amp;lt;dependency&amp;gt;
            &amp;lt;groupId&amp;gt;org.projectlombok&amp;lt;/groupId&amp;gt;
            &amp;lt;artifactId&amp;gt;lombok&amp;lt;/artifactId&amp;gt;
        &amp;lt;/dependency&amp;gt;
    &amp;lt;/dependencies&amp;gt;
    &amp;lt;build&amp;gt;
        &amp;lt;plugins&amp;gt;
            &amp;lt;plugin&amp;gt;
                &amp;lt;groupId&amp;gt;org.springframework.boot&amp;lt;/groupId&amp;gt;
                &amp;lt;artifactId&amp;gt;spring-boot-maven-plugin&amp;lt;/artifactId&amp;gt;
                &amp;lt;configuration&amp;gt;
                    &amp;lt;mainClass&amp;gt;com.funtl.spring.security.oauth2.resource.OAuth2ResourceApplication&amp;lt;/mainClass&amp;gt;
                &amp;lt;/configuration&amp;gt;
            &amp;lt;/plugin&amp;gt;
        &amp;lt;/plugins&amp;gt;
    &amp;lt;/build&amp;gt;
&amp;lt;/project&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;application.yml&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;spring:
  application:
    name: oauth2-resource
  datasource:
    type: com.zaxxer.hikari.HikariDataSource
    driver-class-name: com.mysql.cj.jdbc.Driver
    url: jdbc:mysql://192.168.141.130:3306/oauth2_resource?useUnicode=true&amp;amp;characterEncoding=utf-8&amp;amp;useSSL=false
    username: root
    password: 123456
    hikari:
      minimum-idle: 5
      idle-timeout: 600000
      maximum-pool-size: 10
      auto-commit: true
      pool-name: MyHikariCP
      max-lifetime: 1800000
      connection-timeout: 30000
      connection-test-query: SELECT 1
security:
  oauth2:
    client:
      client-id: client
      client-secret: secret
      access-token-uri: http://localhost:8080/oauth/token
      user-authorization-uri: http://localhost:8080/oauth/authorize
    resource:
      token-info-uri: http://localhost:8080/oauth/check_token
server:
  port: 8081
  servlet:
    context-path: /contents
mybatis:
  type-aliases-package: com.funtl.spring.security.oauth2.resource.domain
  mapper-locations: classpath:mapper/*.xml
logging:
  level:
    root: INFO
    org.springframework.web: INFO
    org.springframework.security: INFO
    org.springframework.security.oauth2: INFO
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Application&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;package com.funtl.spring.security.oauth2.resource;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import tk.mybatis.spring.annotation.MapperScan;
@SpringBootApplication
@MapperScan(basePackages = &quot;com.funtl.spring.security.oauth2.resource.mapper&quot;)
public class OAuth2ResourceApplication {
    public static void main(String[] args) {
        SpringApplication.run(OAuth2ResourceApplication.class, args);
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h4&gt;配置资源服务器&lt;/h4&gt;
&lt;p&gt;创建一个类继承 &lt;code&gt;ResourceServerConfigurerAdapter&lt;/code&gt; 并添加相关注解：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;@Configuration&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;@EnableResourceServer&lt;/code&gt;：资源服务器&lt;/li&gt;
&lt;li&gt;&lt;code&gt;@EnableGlobalMethodSecurity(prePostEnabled = true, securedEnabled = true, jsr250Enabled = true)&lt;/code&gt;：全局方法拦截&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;package com.funtl.spring.security.oauth2.resource.configure;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.method.configuration.EnableGlobalMethodSecurity;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.http.SessionCreationPolicy;
import org.springframework.security.oauth2.config.annotation.web.configuration.EnableResourceServer;
import org.springframework.security.oauth2.config.annotation.web.configuration.ResourceServerConfigurerAdapter;
@Configuration
@EnableResourceServer
@EnableGlobalMethodSecurity(prePostEnabled = true, securedEnabled = true, jsr250Enabled = true)
public class ResourceServerConfiguration extends ResourceServerConfigurerAdapter {
    @Override
    public void configure(HttpSecurity http) throws Exception {
        http
                .exceptionHandling()
                .and()
                // Session 创建策略
                // ALWAYS 总是创建 HttpSession
                // IF_REQUIRED Spring Security 只会在需要时创建一个 HttpSession
                // NEVER Spring Security 不会创建 HttpSession，但如果它已经存在，将可以使用 HttpSession
                // STATELESS Spring Security 永远不会创建 HttpSession，它不会使用 HttpSession 来获取 SecurityContext
                .sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS)
                .and()
                .authorizeRequests()
                // 以下为配置所需保护的资源路径及权限，需要与认证服务器配置的授权部分对应
                .antMatchers(&quot;/&quot;).hasAuthority(&quot;SystemContent&quot;)
                .antMatchers(&quot;/view/**&quot;).hasAuthority(&quot;SystemContentView&quot;)
                .antMatchers(&quot;/insert/**&quot;).hasAuthority(&quot;SystemContentInsert&quot;)
                .antMatchers(&quot;/update/**&quot;).hasAuthority(&quot;SystemContentUpdate&quot;)
                .antMatchers(&quot;/delete/**&quot;).hasAuthority(&quot;SystemContentDelete&quot;);
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;认证服务器配置忽略路径&lt;/p&gt;
&lt;p&gt;&lt;code&gt; web.ignoring().antMatchers(&quot;/oauth/check_token&quot;);&lt;/code&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;package com.funtl.spring.security.oauth2.server.config;

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;
import org.springframework.security.config.annotation.method.configuration.EnableGlobalMethodSecurity;
import org.springframework.security.config.annotation.web.builders.WebSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;

/**
 * &amp;lt;p&amp;gt;DESC: 服务器安全配置&amp;lt;/p&amp;gt;
 * &amp;lt;p&amp;gt;DATE: 2019-11-01 21:14&amp;lt;/p&amp;gt;
 * &amp;lt;p&amp;gt;VERSION:1.0.0&amp;lt;/p&amp;gt;
 * &amp;lt;p&amp;gt;@AUTHOR: ZhengYong&amp;lt;/p&amp;gt;
 */
@Configuration
@EnableWebSecurity
@EnableGlobalMethodSecurity(prePostEnabled = true, securedEnabled = true, jsr250Enabled = true)
public class WebSecurityConfiguration extends WebSecurityConfigurerAdapter {

    //设置密码
    @Bean
    public PasswordEncoder passwordEncoder(){
        return new BCryptPasswordEncoder();
    }

    @Override
    protected void configure(AuthenticationManagerBuilder auth) throws Exception {
//        super.configure(auth);
        auth.inMemoryAuthentication()
                .withUser(&quot;user&quot;).password(passwordEncoder().encode(&quot;123456&quot;)).roles(&quot;USER&quot;)
                .and()
                .withUser(&quot;admin&quot;).password(passwordEncoder().encode(&quot;admin888&quot;)).roles(&quot;ADMIN&quot;);
    }

    @Override
    public void configure(WebSecurity web) throws Exception {
        web.ignoring().antMatchers(&quot;/oauth/check_token&quot;);
    }
}

&lt;/code&gt;&lt;/pre&gt;
&lt;h4&gt;数据传输对象&lt;/h4&gt;
&lt;p&gt;创建一个名为 &lt;code&gt;ResponseResult&lt;/code&gt; 的通用数据传输对象&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;package com.funtl.spring.security.oauth2.resource.dto;
import lombok.Data;
import java.io.Serializable;
/**
 * 通用的返回对象
 *
 * @param &amp;lt;T&amp;gt;
 */
@Data
public class ResponseResult&amp;lt;T&amp;gt; implements Serializable {
    private static final long serialVersionUID = 3468352004150968551L;
    /**
     * 状态码
     */
    private Integer state;
    /**
     * 消息
     */
    private String message;
    /**
     * 返回对象
     */
    private T data;
    public ResponseResult() {
        super();
    }
    public ResponseResult(Integer state) {
        super();
        this.state = state;
    }
    public ResponseResult(Integer state, String message) {
        super();
        this.state = state;
        this.message = message;
    }
    public ResponseResult(Integer state, Throwable throwable) {
        super();
        this.state = state;
        this.message = throwable.getMessage();
    }
    public ResponseResult(Integer state, T data) {
        super();
        this.state = state;
        this.data = data;
    }
    public ResponseResult(Integer state, String message, T data) {
        super();
        this.state = state;
        this.message = message;
        this.data = data;
    }
    public Integer getState() {
        return state;
    }
    public void setState(Integer state) {
        this.state = state;
    }
    public String getMessage() {
        return message;
    }
    public void setMessage(String message) {
        this.message = message;
    }
    public T getData() {
        return data;
    }
    public void setData(T data) {
        this.data = data;
    }
    @Override
    public int hashCode() {
        final int prime = 31;
        int result = 1;
        result = prime * result + ((data == null) ? 0 : data.hashCode());
        result = prime * result + ((message == null) ? 0 : message.hashCode());
        result = prime * result + ((state == null) ? 0 : state.hashCode());
        return result;
    }
    @Override
    public boolean equals(Object obj) {
        if (this == obj) {
            return true;
        }
        if (obj == null) {
            return false;
        }
        if (getClass() != obj.getClass()) {
            return false;
        }
        ResponseResult&amp;lt;?&amp;gt; other = (ResponseResult&amp;lt;?&amp;gt;) obj;
        if (data == null) {
            if (other.data != null) {
                return false;
            }
        } else if (!data.equals(other.data)) {
            return false;
        }
        if (message == null) {
            if (other.message != null) {
                return false;
            }
        } else if (!message.equals(other.message)) {
            return false;
        }
        if (state == null) {
            if (other.state != null) {
                return false;
            }
        } else if (!state.equals(other.state)) {
            return false;
        }
        return true;
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h4&gt;Controller&lt;/h4&gt;
&lt;pre&gt;&lt;code&gt;package com.funtl.spring.security.oauth2.resource.controller;
import com.funtl.spring.security.oauth2.resource.domain.TbContent;
import com.funtl.spring.security.oauth2.resource.dto.ResponseResult;
import com.funtl.spring.security.oauth2.resource.service.TbContentService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.HttpStatus;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;
import java.util.List;
@RestController
public class TbContentController {
    @Autowired
    private TbContentService tbContentService;
    @GetMapping(value = &quot;/&quot;)
    public ResponseResult&amp;lt;List&amp;lt;TbContent&amp;gt;&amp;gt; list() {
        List&amp;lt;TbContent&amp;gt; tbContents = tbContentService.selectAll();
        return new ResponseResult&amp;lt;List&amp;lt;TbContent&amp;gt;&amp;gt;(HttpStatus.OK.value(), HttpStatus.OK.toString(), tbContents);
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;访问资源&lt;/h3&gt;
&lt;h4&gt;访问获取授权码&lt;/h4&gt;
&lt;ul&gt;
&lt;li&gt;通过浏览器访问&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;http://localhost:8080/oauth/authorize?client_id=client&amp;amp;response_type=code&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;第一次访问会跳转到登录页面&lt;/li&gt;
&lt;li&gt;验证成功后会询问用户是否授权客户端&lt;/li&gt;
&lt;li&gt;选择授权后会跳转到百度，浏览器地址上还会包含一个授权码（&lt;code&gt;code=1JuO6V&lt;/code&gt;），浏览器地址栏会显示如下地址：&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;https://www.baidu.com/?code=1JuO6V&lt;/p&gt;
&lt;h4&gt;向服务器申请令牌&lt;/h4&gt;
&lt;ul&gt;
&lt;li&gt;通过 CURL 或是 Postman 请求&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;curl -X POST -H &quot;Content-Type: application/x-www-form-urlencoded&quot; -d &apos;grant_type=authorization_code&amp;amp;code=1JuO6V&apos; &quot;http://client:secret@localhost:8080/oauth/token&quot;
&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;得到响应结果如下&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;{
    &quot;access_token&quot;: &quot;016d8d4a-dd6e-4493-b590-5f072923c413&quot;,
    &quot;token_type&quot;: &quot;bearer&quot;,
    &quot;expires_in&quot;: 43199,
    &quot;scope&quot;: &quot;app&quot;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h4&gt;携带令牌访问资源服务器&lt;/h4&gt;
&lt;p&gt;此处以获取全部资源为例，其它请求方式一样，可以参考我源码中的单元测试代码。可以使用以下方式请求：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;使用 Headers 方式：需要在请求头增加 &lt;code&gt;Authorization: Bearer yourAccessToken&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;直接请求带参数方式：&lt;code&gt;http://localhost:8081/contents?access_token=yourAccessToken&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;使用 Headers 方式，通过 CURL 或是 Postman 请求&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;curl --location --request GET &quot;http://localhost:8081/contents&quot; --header &quot;Content-Type: application/json&quot; --header &quot;Authorization: Bearer yourAccessToken&quot;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;img src=&quot;%E6%90%BA%E5%B8%A6%E4%BB%A4%E7%89%8C%E8%AE%BF%E9%97%AE.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;h2&gt;公司实际RBAC模型&lt;/h2&gt;
&lt;p&gt;&lt;img src=&quot;%E5%85%AC%E5%8F%B8%E5%AE%9E%E9%99%85RBAC%E5%AE%9E%E7%8E%B0.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;h3&gt;表结构：&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;CREATE TABLE `dftc_oss_admin` (
  `id` bigint(20) NOT NULL COMMENT &apos;ID&apos;,
  `user_name` varchar(50) NOT NULL COMMENT &apos;用户名&apos;,
  `password` varchar(128) NOT NULL COMMENT &apos;密码&apos;,
  `nickname` varchar(255) DEFAULT NULL COMMENT &apos;昵称&apos;,
  `avatar` varchar(255) DEFAULT NULL COMMENT &apos;头像&apos;,
  `tel` varchar(11) DEFAULT NULL COMMENT &apos;手机号&apos;,
  `email` varchar(50) DEFAULT NULL COMMENT &apos;邮箱&apos;,
  `birthday` date DEFAULT NULL COMMENT &apos;生日&apos;,
  `sex` tinyint(1) DEFAULT NULL COMMENT &apos;性别 0：女 1：男&apos;,
  `status` tinyint(1) NOT NULL DEFAULT &apos;0&apos; COMMENT &apos;是否锁定 0：否 1：是&apos;,
  `intro` varchar(255) DEFAULT NULL COMMENT &apos;简介&apos;,
  `memo` varchar(255) DEFAULT NULL COMMENT &apos;备注&apos;,
  `del_flag` tinyint(1) NOT NULL DEFAULT &apos;0&apos; COMMENT &apos;是否删除 0：未删除 1：已删除&apos;,
  `last_login_ip` varchar(255) DEFAULT NULL COMMENT &apos;最后一次登录ip&apos;,
  `last_login_time` datetime DEFAULT NULL COMMENT &apos;最后一次登录时间&apos;,
  `last_login_address` varchar(255) DEFAULT NULL COMMENT &apos;最后一次登录地址&apos;,
  `create_time` datetime NOT NULL COMMENT &apos;创建时间&apos;,
  `update_time` datetime NOT NULL COMMENT &apos;修改时间&apos;,
  PRIMARY KEY (`id`) USING BTREE,
  KEY `username` (`user_name`) USING BTREE,
  KEY `createtime` (`create_time`) USING BTREE
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 ROW_FORMAT=DYNAMIC;

CREATE TABLE `dftc_oss_role` (
  `id` bigint(22) NOT NULL,
  `role_name` varchar(64) NOT NULL,
  `role_number` varchar(32) NOT NULL,
  `detail` varchar(255) DEFAULT NULL,
  `status` tinyint(1) NOT NULL DEFAULT &apos;0&apos; COMMENT &apos;是否已经停止使用 （0：未停止 1：已停止）&apos;,
  `memo` varchar(255) DEFAULT NULL,
  `create_time` datetime NOT NULL,
  `update_time` datetime DEFAULT NULL,
  PRIMARY KEY (`id`) USING BTREE,
  UNIQUE KEY `role_number_index` (`role_number`) USING BTREE,
  UNIQUE KEY `role_name_index` (`role_name`) USING BTREE
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 ROW_FORMAT=DYNAMIC;

CREATE TABLE `dftc_oss_admin_role` (
  `id` bigint(22) NOT NULL,
  `admin_id` bigint(22) NOT NULL,
  `role_id` bigint(22) NOT NULL,
  `memo` varchar(255) DEFAULT NULL,
  `create_time` datetime NOT NULL,
  `update_time` datetime DEFAULT NULL,
  PRIMARY KEY (`id`) USING BTREE,
  UNIQUE KEY `admin_role_index` (`admin_id`,`role_id`) USING BTREE
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 ROW_FORMAT=DYNAMIC;

CREATE TABLE `dftc_oss_menu` (
  `id` bigint(22) NOT NULL,
  `menu_name` varchar(24) NOT NULL,
  `menu_url` varchar(300) DEFAULT NULL COMMENT &apos;资源访问地址&apos;,
  `parent_id` bigint(22) DEFAULT NULL,
  `icons` varchar(255) DEFAULT NULL,
  `status` tinyint(1) NOT NULL DEFAULT &apos;0&apos; COMMENT &apos; 菜单是否停用 0：未停用 1：停用&apos;,
  `sort` int(4) NOT NULL DEFAULT &apos;20&apos;,
  `sys_menu` tinyint(1) NOT NULL DEFAULT &apos;0&apos; COMMENT &apos;是否是系统级菜单（不可删除） 0：不是 1：是&apos;,
  `type` tinyint(1) NOT NULL COMMENT &apos;菜单类型1:顶级菜单 2:2级菜单  3：三级菜单 4：按钮&apos;,
  `memo` varchar(255) DEFAULT NULL,
  `create_time` datetime NOT NULL,
  `update_time` datetime DEFAULT NULL,
  PRIMARY KEY (`id`) USING BTREE,
  UNIQUE KEY `menu_name_index` (`menu_name`) USING BTREE
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 ROW_FORMAT=DYNAMIC;

CREATE TABLE `dftc_oss_role_permissions` (
  `id` bigint(22) NOT NULL,
  `role_id` bigint(22) NOT NULL,
  `menu_id` bigint(22) NOT NULL COMMENT &apos;菜单id&apos;,
  `memo` varchar(255) DEFAULT NULL,
  `create_time` datetime NOT NULL,
  `update_time` datetime DEFAULT NULL,
  PRIMARY KEY (`id`) USING BTREE,
  UNIQUE KEY `role_resource_index` (`role_id`,`menu_id`) USING BTREE,
  KEY `menu` (`menu_id`) USING BTREE
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 ROW_FORMAT=DYNAMIC;
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;读取文件yaml文件&lt;/h2&gt;
&lt;h3&gt;前置知识&lt;/h3&gt;
&lt;h4&gt;Java对YAML文件的操作&lt;/h4&gt;
&lt;h4&gt;1、SnakeYAML简介&lt;/h4&gt;
&lt;blockquote&gt;
&lt;p&gt;SnakeYAML是一个完整的YAML1.1规范Processor，支持UTF-8/UTF-16，支持Java对象的序列化/反序列化，支持所有YAML定义的类型。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h4&gt;2、SnakeYAML依赖添加&lt;/h4&gt;
&lt;pre&gt;&lt;code&gt;&amp;lt;dependency&amp;gt;
    &amp;lt;groupId&amp;gt;org.yaml&amp;lt;/groupId&amp;gt;
    &amp;lt;artifactId&amp;gt;snakeyaml&amp;lt;/artifactId&amp;gt;
    &amp;lt;version&amp;gt;1.21&amp;lt;/version&amp;gt;
&amp;lt;/dependency&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;h4&gt;3、SnakeYAML的使用方法&lt;/h4&gt;
&lt;p&gt;&lt;strong&gt;1. 建立Person类&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;import lombok.Data;//lombok为一种Java工具框架

@Data
public class Person {
    private String name;
    private Integer age;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;2、建立person.yml文件&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;## person.yml
!!com.demo.Person {age: 24, name: Adam}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;3、读取并解析YAML文件&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;1 &amp;lt;T&amp;gt; T load(InputStream input)&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;//获取YAML中的单个对象
@Test
public void testLoadYaml() throws FileNotFoundException {
    Yaml yaml = new Yaml();
    File ymlFile = new File(System.getProperty(&quot;user.dir&quot;) + &quot;/src/main/resources/person.yml&quot;);
    Person person = yaml.load(new FileInputStream(ymlFile));
    System.out.println(person);
}
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;//获取YAML中的单个对象
@Test
public void testLoadYaml2() throws FileNotFoundException {
    Yaml yaml = new Yaml();
    File ymlFile = new File(System.getProperty(&quot;user.dir&quot;) + &quot;/src/main/resources/person.yml&quot;);
    List&amp;lt;Person&amp;gt; personList = yaml.load(new FileInputStream(ymlFile));
    System.out.println(personList);
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;2 &amp;lt;T&amp;gt; T loadAs(InputStream input, Class&amp;lt;T&amp;gt; type)&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;//读取YAML文件并返回一个对应类的对象
@Test
public void testLoadYaml() throws FileNotFoundException {
    Yaml yaml = new Yaml();
    File ymlFile = new File(System.getProperty(&quot;user.dir&quot;) + &quot;/src/main/resources/person.yml&quot;);
    Person person = yaml.loadAs(new FileInputStream(ymlFile), Person.class);
    System.out.println(person);
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;3 Iterable&amp;lt;Object&amp;gt; loadAll(InputStream input)&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;//读取YAML文件并返回一个Iterable接口的子类
@Test
public void testLoadAllYaml() throws FileNotFoundException {
    Yaml yaml = new Yaml();
    File ymlFile = new File(System.getProperty(&quot;user.dir&quot;) + &quot;/src/main/resources/person.yml&quot;);
    Iterable&amp;lt;Object&amp;gt; people = yaml.loadAll(new FileInputStream(ymlFile));
    for (Object person : people) {
        System.out.println(person);
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;结果&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;[{name=John, age=20}, {name=Steven, age=30}, {name=Jenny, age=18}]
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;4 void dump(Object data, Writer output)&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;//将POJO写入YAML文件
@Test
public void testDumpYaml() throws IOException {
    Yaml yaml = new Yaml();
    File ymlFile = new File(System.getProperty(&quot;user.dir&quot;) + &quot;/src/main/resources/person.yml&quot;);
    Person person = new Person();
    person.setName(&quot;Adam&quot;);
    person.setAge(24);
    yaml.dump(person, new FileWriter(ymlFile));
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;结果：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;!!com.liheng.demo.Person {age: 24, name: Adam}
注：dump方法会将YAML文件中的数据覆盖
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;5 void dumpAll(Iterator&amp;lt;? extends Object&amp;gt; data, Writer output)&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;//通过Iterator迭代器批量写入YAML文件
@Test
public void testDumpAllYaml() throws IOException {
    Yaml yaml = new Yaml();
    File ymlFile = new File(System.getProperty(&quot;user.dir&quot;) + &quot;/src/main/resources/person.yml&quot;);
    Iterable&amp;lt;Object&amp;gt; people = yaml.loadAll(new FileInputStream(ymlFile));
    List&amp;lt;Person&amp;gt; personList = Lists.newArrayList();
    for (Object person : people) {
        LinkedHashMap map = (LinkedHashMap) person;
        Person p = new Person();
        p.setName((String) map.get(&quot;name&quot;));
        p.setAge((Integer) map.get(&quot;age&quot;));
        personList.add(p);
    }
    yaml.dumpAll(personList.iterator(), new FileWriter(ymlFile));
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;结果：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;!!com.liheng.demo.Person {age: 20, name: John}
--- !!com.liheng.demo.Person {age: 30, name: Steven}
--- !!com.liheng.demo.Person {age: 18, name: Jenny}
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;工作实际使用&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;/**
 * PrivilegeConfig
 *
 * @author wjc
 * @date 2019/3/14
 */
@Slf4j
@Configuration
public class PrivilegeConfig {

    @Bean(name = &quot;privMap&quot;)
    public Map&amp;lt;String, List&amp;lt;String&amp;gt;&amp;gt; privConfig() {
        InputStream is = null;
        try {
            Yaml yaml = new Yaml();
            is = this.getClass().getClassLoader().getResourceAsStream(&quot;privilege.yml&quot;);
            Iterator&amp;lt;Object&amp;gt; iter = yaml.loadAll(is).iterator();
            Map&amp;lt;String, List&amp;lt;String&amp;gt;&amp;gt; result = new HashMap&amp;lt;&amp;gt;(16);
            Map&amp;lt;String, List&amp;lt;String&amp;gt;&amp;gt; privMap;
            while (iter.hasNext()) {
                privMap = (Map&amp;lt;String, List&amp;lt;String&amp;gt;&amp;gt;) iter.next();
                result.putAll(privMap);
            }
            return result;
        } finally {
            if (is != null) {
                try {
                    is.close();
                } catch (IOException e) {
                    log.error(&quot;close input stream failed&quot;, e);
                }
            }
        }
    }
}  
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;privilege.yml&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;##    菜单管理
XTGL_CDGL: [&quot;/menu/list&quot;]
XTGL_CDGL_CDXZ: [&quot;/menu/add&quot;,&quot;/menu/nameCheck&quot;]
XTGL_CDGL_CDBJ: [&quot;/menu/modify&quot;,&quot;/menu/nameCheck&quot;,&quot;/menu/listById&quot;]
XTGL_CDGL_CDSC: [&quot;/menu/drop&quot;]
##    账号管理
XTGL_ZHGL: [&quot;/admin/list&quot;]
XTGL_ZHGL_ZHXZ: [&quot;/admin/add&quot;,&quot;/admin/nameCheck&quot;]
XTGL_ZHGL_ZHBJ: [&quot;/admin/modify&quot;,&quot;/admin/nameCheck&quot;,&quot;/admin/listById&quot;]
XTGL_ZHGL_ZHSC: [&quot;/admin/drop&quot;]
XTGL_ZHGL_ZHSD: [&quot;/admin/lock&quot;]
XTGL_ZHGL_ZHJS: [&quot;/admin/unlock&quot;]
##    权限管理
XTGL_QXGL: [&quot;/role/list&quot;]
XTGL_QXGL_QXXZ: [&quot;/role/add&quot;,&quot;/role/nameCheck&quot;]
XTGL_QXGL_QXBJ: [&quot;/role/edit&quot;,&quot;/role/nameCheck&quot;,&quot;/role/listById&quot;]
XTGL_QXGL_QXSC: [&quot;/role/del&quot;]
XTGL_QXGL_QXSZ: [&quot;/role/refreshRolePermission&quot;,&quot;/role/getMenuWithPermission&quot;]
##   app用户管理
APPGL_APPYH: [&quot;/clientUser/list&quot;,&quot;/clientUser/userInfo&quot;,&quot;/clientUser/getUserLogList&quot;]
##   app用户审核
APPGL_APPYH_APPSH: [&quot;/clientUser/doUserVerify&quot;]

##  企业认证
APPGL_QYRZ: [&quot;/resume/getUserLicenseByUserId&quot;,&quot;/resume/getLicenseList&quot;,&quot;/resume/getLicenseOssInfo&quot;]
##  认证审核
APPGL_QYRZ_RZSH: [&quot;/resume/passOssLicense&quot;,&quot;/resume/failOssLicense&quot;]
&lt;/code&gt;&lt;/pre&gt;
&lt;hr /&gt;
&lt;h2&gt;Security微服务实战&lt;/h2&gt;
&lt;h2&gt;security实战&lt;/h2&gt;
&lt;h2&gt;CSRF&lt;/h2&gt;
&lt;blockquote&gt;
&lt;p&gt;CSRF跨站点请求伪造(Cross—Site Request Forgery)，&lt;/p&gt;
&lt;p&gt;跟XSS攻击一样，存在巨大的危害性，你可以这样来理解：
​       攻击者盗用了你的身份，以你的名义发送恶意请求，对服务器来说这个请求是完全合法的，但是却完成了攻击者所期望的一个操作，比如以你的名义发送邮件、发消息，盗取你的账号，添加系统管理员，甚至于购买商品、虚拟货币转账等。 如下：其中Web A为存在CSRF漏洞的网站，Web B为攻击者构建的恶意网站，User C为Web A网站的合法用户。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;CSRF攻击攻击原理及过程如下：&lt;/p&gt;
&lt;p&gt;​       1. 用户C打开浏览器，访问受信任网站A，输入用户名和密码请求登录网站A；&lt;/p&gt;
&lt;p&gt;​       2.在用户信息通过验证后，网站A产生Cookie信息并返回给浏览器，此时用户登录网站A成功，可以正常发送请求到网站A；&lt;/p&gt;
&lt;p&gt;​       3. 用户未退出网站A之前，在同一浏览器中，打开一个TAB页访问网站B；&lt;/p&gt;
&lt;p&gt;​       4. 网站B接收到用户请求后，返回一些攻击性代码，并发出一个请求要求访问第三方站点A；&lt;/p&gt;
&lt;p&gt;​       5. 浏览器在接收到这些攻击性代码后，根据网站B的请求，在用户不知情的情况下携带Cookie信息，向网站A发出请求。网站A并不知道该请求其实是由B发起的，所以会根据用户C的Cookie信息以C的权限处理该请求，导致来自网站B的恶意代码被执行。&lt;/p&gt;
&lt;h2&gt;CORS&lt;/h2&gt;
&lt;blockquote&gt;
&lt;p&gt;全称&quot;跨域资源共享&quot;（Cross-origin resource sharing）&lt;/p&gt;
&lt;p&gt;CORS需要浏览器和服务器同时支持，才可以实现跨域请求，目前几乎所有浏览器都支持CORS，IE则不能低于IE10。CORS的整个过程都由浏览器自动完成，前端无需做任何设置，跟平时发送ajax请求并无差异。so，实现CORS的关键在于服务器，只要服务器实现CORS接口，就可以实现跨域通信。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2&gt;简易的oauth2认证中心&lt;/h2&gt;
&lt;h3&gt;1、认证中心&lt;/h3&gt;
&lt;h4&gt;1.1 搭建is-server-auth&lt;/h4&gt;
&lt;p&gt;pom.xml&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;这里注意要确定Java编译的版本，防止出现出错&lt;/p&gt;
&lt;p&gt;&amp;lt;configuration&amp;gt;
&amp;lt;source&amp;gt;1.8&amp;lt;/source&amp;gt;
&amp;lt;target&amp;gt;1.8&amp;lt;/target&amp;gt;
&amp;lt;/configuration&amp;gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;pre&gt;&lt;code&gt;&amp;lt;?xml version=&quot;1.0&quot; encoding=&quot;UTF-8&quot;?&amp;gt;
&amp;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;&amp;gt;
    &amp;lt;modelVersion&amp;gt;4.0.0&amp;lt;/modelVersion&amp;gt;

    &amp;lt;parent&amp;gt;
        &amp;lt;groupId&amp;gt;com.eim&amp;lt;/groupId&amp;gt;
        &amp;lt;artifactId&amp;gt;security_item&amp;lt;/artifactId&amp;gt;
        &amp;lt;version&amp;gt;1.0.0-SNAPSHOT&amp;lt;/version&amp;gt;
    &amp;lt;/parent&amp;gt;

    &amp;lt;artifactId&amp;gt;is-server-auth&amp;lt;/artifactId&amp;gt;
    &amp;lt;version&amp;gt;1.0.0-SNAPSHOT&amp;lt;/version&amp;gt;

    &amp;lt;dependencies&amp;gt;
        &amp;lt;dependency&amp;gt;
            &amp;lt;groupId&amp;gt;org.springframework.cloud&amp;lt;/groupId&amp;gt;
            &amp;lt;artifactId&amp;gt;spring-cloud-starter-oauth2&amp;lt;/artifactId&amp;gt;
        &amp;lt;/dependency&amp;gt;
        &amp;lt;dependency&amp;gt;
            &amp;lt;groupId&amp;gt;mysql&amp;lt;/groupId&amp;gt;
            &amp;lt;artifactId&amp;gt;mysql-connector-java&amp;lt;/artifactId&amp;gt;
        &amp;lt;/dependency&amp;gt;
        &amp;lt;dependency&amp;gt;
            &amp;lt;groupId&amp;gt;com.baomidou&amp;lt;/groupId&amp;gt;
            &amp;lt;artifactId&amp;gt;mybatis-plus-boot-starter&amp;lt;/artifactId&amp;gt;
            &amp;lt;version&amp;gt;3.2.0&amp;lt;/version&amp;gt;
        &amp;lt;/dependency&amp;gt;
    &amp;lt;/dependencies&amp;gt;

    &amp;lt;build&amp;gt;
        &amp;lt;plugins&amp;gt;
            &amp;lt;plugin&amp;gt;
                &amp;lt;groupId&amp;gt;org.apache.maven.plugins&amp;lt;/groupId&amp;gt;
                &amp;lt;artifactId&amp;gt;maven-compiler-plugin&amp;lt;/artifactId&amp;gt;
                &amp;lt;configuration&amp;gt;
                    &amp;lt;source&amp;gt;1.8&amp;lt;/source&amp;gt;
                    &amp;lt;target&amp;gt;1.8&amp;lt;/target&amp;gt;
                &amp;lt;/configuration&amp;gt;
            &amp;lt;/plugin&amp;gt;
        &amp;lt;/plugins&amp;gt;
    &amp;lt;/build&amp;gt;

&amp;lt;/project&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;application.yml&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;server:
  port: 10000
spring:
  datasource:
    url: jdbc:mysql://49.232.154.66:4406/my_security?useUnicode=true&amp;amp;characterEncoding=utf-8&amp;amp;useSSL=false
    username: root
    password: zy123456
mybatis-plus:
  global-config:
    db-config:
      id-type: id_worker
&lt;/code&gt;&lt;/pre&gt;
&lt;h4&gt;1.2存储客户端信息&lt;/h4&gt;
&lt;ul&gt;
&lt;li&gt;存在内充中&lt;/li&gt;
&lt;li&gt;存在MySQL数据库&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;这里我们推荐存在数据库，建表语句如下&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;CREATE TABLE `oauth_client_details` (
  `client_id` varchar(256) NOT NULL,
  `resource_ids` varchar(256) DEFAULT NULL,
  `client_secret` varchar(256) DEFAULT NULL,
  `scope` varchar(256) DEFAULT NULL,
  `authorized_grant_types` varchar(256) DEFAULT NULL,
  `web_server_redirect_uri` varchar(256) DEFAULT NULL,
  `authorities` varchar(256) DEFAULT NULL,
  `access_token_validity` int(11) DEFAULT NULL,
  `refresh_token_validity` int(11) DEFAULT NULL,
  `additional_information` varchar(4096) DEFAULT NULL,
  `autoapprove` varchar(256) DEFAULT NULL,
  PRIMARY KEY (`client_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
&lt;/code&gt;&lt;/pre&gt;
&lt;h4&gt;1.3服务器配置&lt;/h4&gt;
&lt;p&gt;继承AuthorizationServerConfigurerAdapter（认证服务配置适配器），做自定义配置,源码如下：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;public class AuthorizationServerConfigurerAdapter implements AuthorizationServerConfigurer {
    public AuthorizationServerConfigurerAdapter() {
    }

    //目前不知道是做什么的
    public void configure(AuthorizationServerSecurityConfigurer security) throws Exception {
    }

    //客户端配置
    public void configure(ClientDetailsServiceConfigurer clients) throws Exception {
    }
    
   //端点配置，也可以说是入口配置（如获取token等）
    public void configure(AuthorizationServerEndpointsConfigurer endpoints) throws Exception {
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;自定义配置：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;/**
 * &amp;lt;p&amp;gt;DESC: 服务器配置&amp;lt;/p&amp;gt;
 * &amp;lt;p&amp;gt;DATE: 2019-12-05 16:28&amp;lt;/p&amp;gt;
 * &amp;lt;p&amp;gt;VERSION:2.0.0&amp;lt;/p&amp;gt;
 * &amp;lt;p&amp;gt;@AUTHOR: ZhengYong&amp;lt;/p&amp;gt;
 */
@Configuration
@EnableAuthorizationServer
public class AuthServerConfig extends AuthorizationServerConfigurerAdapter {

    @Resource
    private DataSource dataSource;

    @Resource
    private AuthenticationManager authenticationManager;

    /**
     * 配置客户端到数据库
     * @param clients  客户端
     * @throws Exception 异常
     */
    @Override
    public void configure(ClientDetailsServiceConfigurer clients) throws Exception {
        clients.jdbc(dataSource);
    }


    /**
     * 配置端点
     * @param endpoints  端点
     * @throws Exception 异常
     */
    @Override
    public void configure(AuthorizationServerEndpointsConfigurer endpoints) throws Exception {
        endpoints.authenticationManager(authenticationManager);
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;服务器web安全配置(WebSecurityConfigurerAdapter)：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;构建一个自定义的AuthenticationManager&lt;/li&gt;
&lt;li&gt;配置忽略的路径&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;/**
 * &amp;lt;p&amp;gt;DESC: web安全配置&amp;lt;/p&amp;gt;
 * &amp;lt;p&amp;gt;DATE: 2019-12-05 16:34&amp;lt;/p&amp;gt;
 * &amp;lt;p&amp;gt;VERSION:2.0.0&amp;lt;/p&amp;gt;
 * &amp;lt;p&amp;gt;@AUTHOR: ZhengYong&amp;lt;/p&amp;gt;
 */
@Configuration
@EnableWebSecurity
public class AuthWebSecurityConfig extends WebSecurityConfigurerAdapter {

    /**
     * UserDetailsService 实现类
     * 根据用户名获取用户信息
     */
    @Resource
    private OrderUserDetailServiceImpl userDetailService;

    /**
     * 加密解密器
     * @return PasswordEncoder
     */
    @Bean
    public PasswordEncoder passwordEncoder() {
        return new BCryptPasswordEncoder();
    }

    /**
     * 重写AuthenticationManager构造器，这里构造好以后供服务端点配置中使用
     * 因为需要制定自己的 userDetailService ，和 加密解密器
     * @param auth  认证构造器
     * @throws Exception 异常
     */
    @Override
    protected void configure(AuthenticationManagerBuilder auth) throws Exception {
        auth.userDetailsService(userDetailService)
                .passwordEncoder(passwordEncoder());
    }

    /**
     * 将认证管理器注入到spring容器中，供服务端点配置中使用
     * @return AuthenticationManager
     * @throws Exception 异常
     */
    @Bean
    @Override
    public AuthenticationManager authenticationManagerBean() throws Exception {
        return super.authenticationManagerBean();
    }

    /**
     * 配置忽略的路径，/oauth/check_token 是校验token的路径，security默认没有放行
     * @param web  web
     * @throws Exception 异常
     */
    @Override
    public void configure(WebSecurity web) throws Exception {
        web.ignoring().antMatchers(&quot;/oauth/check_token&quot;);
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;2、资源服务（order）&lt;/h3&gt;
&lt;h4&gt;2.1构建order&lt;/h4&gt;
&lt;p&gt;pom.xml&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&amp;lt;?xml version=&quot;1.0&quot; encoding=&quot;UTF-8&quot;?&amp;gt;
&amp;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;&amp;gt;
    &amp;lt;modelVersion&amp;gt;4.0.0&amp;lt;/modelVersion&amp;gt;

    &amp;lt;parent&amp;gt;
        &amp;lt;groupId&amp;gt;com.eim&amp;lt;/groupId&amp;gt;
        &amp;lt;artifactId&amp;gt;security_item&amp;lt;/artifactId&amp;gt;
        &amp;lt;version&amp;gt;1.0.0-SNAPSHOT&amp;lt;/version&amp;gt;
    &amp;lt;/parent&amp;gt;

    &amp;lt;artifactId&amp;gt;is-order-api&amp;lt;/artifactId&amp;gt;
    &amp;lt;version&amp;gt;1.0.0-SNAPSHOT&amp;lt;/version&amp;gt;

    &amp;lt;dependencies&amp;gt;
        &amp;lt;dependency&amp;gt;
            &amp;lt;groupId&amp;gt;org.springframework.cloud&amp;lt;/groupId&amp;gt;
            &amp;lt;artifactId&amp;gt;spring-cloud-starter-oauth2&amp;lt;/artifactId&amp;gt;
        &amp;lt;/dependency&amp;gt;
        &amp;lt;dependency&amp;gt;
            &amp;lt;groupId&amp;gt;mysql&amp;lt;/groupId&amp;gt;
            &amp;lt;artifactId&amp;gt;mysql-connector-java&amp;lt;/artifactId&amp;gt;
        &amp;lt;/dependency&amp;gt;
        &amp;lt;dependency&amp;gt;
            &amp;lt;groupId&amp;gt;com.baomidou&amp;lt;/groupId&amp;gt;
            &amp;lt;artifactId&amp;gt;mybatis-plus-boot-starter&amp;lt;/artifactId&amp;gt;
            &amp;lt;version&amp;gt;3.2.0&amp;lt;/version&amp;gt;
        &amp;lt;/dependency&amp;gt;
    &amp;lt;/dependencies&amp;gt;

    &amp;lt;build&amp;gt;
        &amp;lt;plugins&amp;gt;
            &amp;lt;plugin&amp;gt;
                &amp;lt;groupId&amp;gt;org.apache.maven.plugins&amp;lt;/groupId&amp;gt;
                &amp;lt;artifactId&amp;gt;maven-compiler-plugin&amp;lt;/artifactId&amp;gt;
                &amp;lt;configuration&amp;gt;
                    &amp;lt;source&amp;gt;1.8&amp;lt;/source&amp;gt;
                    &amp;lt;target&amp;gt;1.8&amp;lt;/target&amp;gt;
                &amp;lt;/configuration&amp;gt;
            &amp;lt;/plugin&amp;gt;
        &amp;lt;/plugins&amp;gt;
    &amp;lt;/build&amp;gt;

&amp;lt;/project&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;application.yaml&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;server:
  port: 10010
spring:
  datasource:
    url: jdbc:mysql://49.232.154.66:4406/my_security?useUnicode=true&amp;amp;characterEncoding=utf-8&amp;amp;useSSL=false
    username: root
    password: zy123456
mybatis-plus:
  global-config:
    db-config:
      id-type: id_worker
&lt;/code&gt;&lt;/pre&gt;
&lt;h4&gt;2.2ResourceServerConfigurerAdapter&lt;/h4&gt;
&lt;p&gt;配置资源服务器ResourceServerConfigurerAdapter，源码如下&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;public class ResourceServerConfigurerAdapter implements ResourceServerConfigurer {
    public ResourceServerConfigurerAdapter() {
    }

    //配置资源服务器
    public void configure(ResourceServerSecurityConfigurer resources) throws Exception {
    }
    
    //web安全配置，默认所有请求都需要认证
    public void configure(HttpSecurity http) throws Exception {
        ((AuthorizedUrl)http.authorizeRequests().anyRequest()).authenticated();
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;配置token校验地址：&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;资源服务器，需要对传入的token进行校验。&lt;/p&gt;
&lt;p&gt;配置的方式有两种：&lt;/p&gt;
&lt;p&gt;1、在代码中配置&lt;/p&gt;
&lt;p&gt;2、在application.yml中配置&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h4&gt;2.3代码中配置资源服务器&lt;/h4&gt;
&lt;ul&gt;
&lt;li&gt;自定义资源服务配置&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;/**
 * &amp;lt;p&amp;gt;DESC: 订单资源配置&amp;lt;/p&amp;gt;
 * &amp;lt;p&amp;gt;DATE: 2019-12-05 15:48&amp;lt;/p&amp;gt;
 * &amp;lt;p&amp;gt;VERSION:2.0.0&amp;lt;/p&amp;gt;
 * &amp;lt;p&amp;gt;@AUTHOR: ZhengYong&amp;lt;/p&amp;gt;
 */
@Configuration
@EnableResourceServer
public class OrderResourceConfig extends ResourceServerConfigurerAdapter {

    //配置resourceid,注意application.yml中配置此项不生效。原因未知
    @Override
    public void configure(ResourceServerSecurityConfigurer resources) throws Exception {
        resources.resourceId(&quot;resourceid&quot;);
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;自定义资源服务器web安全配置&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;/**
 * &amp;lt;p&amp;gt;DESC: 订单资源web安全配置&amp;lt;/p&amp;gt;
 * &amp;lt;p&amp;gt;DATE: 2019-12-05 16:18&amp;lt;/p&amp;gt;
 * &amp;lt;p&amp;gt;VERSION:2.0.0&amp;lt;/p&amp;gt;
 * &amp;lt;p&amp;gt;@AUTHOR: ZhengYong&amp;lt;/p&amp;gt;
 */
@Configuration
@EnableWebSecurity
public class OrderResourcesWebSecurityConfig extends WebSecurityConfigurerAdapter {


    @Bean
    public PasswordEncoder passwordEncoder(){
        return new BCryptPasswordEncoder();
    }

    /**
     * ResourceServerTokenServices 资源服务tokenservices
     * @return ResourceServerTokenServices
     */
    @Bean
    public ResourceServerTokenServices resourceServerTokenServices(){
        RemoteTokenServices tokenServices = new RemoteTokenServices();
        tokenServices.setClientId(&quot;client&quot;);
        tokenServices.setClientSecret(&quot;secret&quot;);
        tokenServices.setCheckTokenEndpointUrl(&quot;http://localhost:10000/oauth/check_token&quot;);
        return tokenServices;
    }

    /**
     * 重写认证管理器注入到spring，因为需要注入自定义的ResourceServerTokenServices
     * @return AuthenticationManager
     * @throws Exception 异常
     */
    @Bean
    @Override
    public AuthenticationManager authenticationManagerBean() throws Exception {
        OAuth2AuthenticationManager manager = new OAuth2AuthenticationManager();
        manager.setTokenServices(resourceServerTokenServices());
        return manager;
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h4&gt;2.4配置文件中配置资源服务器&lt;/h4&gt;
&lt;p&gt;application.yml中配置校验token地址&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;security:
  oauth2:
    client:
      client-id: client
      client-secret: secret
      access-token-uri: http://localhost:10000/oauth/token
      user-authorization-uri: http://localhost:10000/oauth/authorize
      resource-ids: 
##        - 这里配置resourceid不生效，原因未知
        - resourceid
    resource:
      token-info-uri: http://localhost:10000/oauth/check_token
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;3、资源服务（price）&lt;/h3&gt;
&lt;blockquote&gt;
&lt;p&gt;和order配置一致，自行参考order&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h3&gt;4、测试&lt;/h3&gt;
&lt;p&gt;获取token&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;%E5%AF%86%E7%A0%81%E6%A8%A1%E5%BC%8F%E8%8E%B7%E5%8F%96token.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;携带token，访问资源服务器&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;%E6%90%BA%E5%B8%A6token%E8%AE%BF%E9%97%AE%E8%B5%84%E6%BA%90%E6%9C%8D%E5%8A%A1%E5%99%A8.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;h2&gt;控制请求的scope权限&lt;/h2&gt;
&lt;blockquote&gt;
&lt;p&gt;目的用scope控制资源服务器的资源权限&lt;/p&gt;
&lt;p&gt;用法：&lt;/p&gt;
&lt;p&gt;复写 config 参数是： HttpSecurity 的方法，加上我们的scope表达式&lt;/p&gt;
&lt;/blockquote&gt;
&lt;ul&gt;
&lt;li&gt;配置&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;/**
 * &amp;lt;p&amp;gt;DESC: 订单资源配置&amp;lt;/p&amp;gt;
 * &amp;lt;p&amp;gt;DATE: 2019-12-05 15:48&amp;lt;/p&amp;gt;
 * &amp;lt;p&amp;gt;VERSION:2.0.0&amp;lt;/p&amp;gt;
 * &amp;lt;p&amp;gt;@AUTHOR: ZhengYong&amp;lt;/p&amp;gt;
 */
@Configuration
@EnableResourceServer
public class OrderResourceConfig extends ResourceServerConfigurerAdapter {

    @Override
    public void configure(ResourceServerSecurityConfigurer resources) throws Exception {
        resources.resourceId(&quot;resourceid&quot;);
    }

    @Override
    public void configure(HttpSecurity http) throws Exception {
        http.authorizeRequests()
                .antMatchers(HttpMethod.GET).access(&quot;#oauth2.hasScope(&apos;read&apos;)&quot;)
                .antMatchers(HttpMethod.POST).access(&quot;#oauth2.hasScope(&apos;write&apos;)&quot;);
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;测试&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;1、在密码模式申请令牌的时候去，去指定申请scope的  read 权限。&lt;/p&gt;
&lt;p&gt;2、获取到的token 去访问资源服务器的post请求。&lt;/p&gt;
&lt;p&gt;3、返回结果，scope权限不足拒绝访问。&lt;/p&gt;
&lt;h2&gt;接口获取用户信息&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;@AuthenticationPrincipal String username  注解获取用户名&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;@Slf4j
@RestController
@RequestMapping(&quot;/orders&quot;)
public class OrderController {

    @PostMapping
    public OrderInfo create(@RequestBody OrderInfo info, @AuthenticationPrincipal String username, HttpServletRequest srequest) throws IOException {
        log.info(&quot;【用户名】={}&quot;,username);

        String header = srequest.getHeader(&quot;Authorization&quot;);
&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;加入用户名获取用户信息，重写转换器convert方法&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;1、自定义UserDetailsService&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;过于简单，这里就不多说了&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;2、&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;/**
 * &amp;lt;p&amp;gt;DESC: 订单资源web安全配置&amp;lt;/p&amp;gt;
 * &amp;lt;p&amp;gt;DATE: 2019-12-05 16:18&amp;lt;/p&amp;gt;
 * &amp;lt;p&amp;gt;VERSION:2.0.0&amp;lt;/p&amp;gt;
 * &amp;lt;p&amp;gt;@AUTHOR: ZhengYong&amp;lt;/p&amp;gt;
 */
@Configuration
@EnableWebSecurity
public class OrderResourcesWebSecurityConfig extends WebSecurityConfigurerAdapter {

    @Resource
    private UserDetailsService userDetailsService;

    @Bean
    public PasswordEncoder passwordEncoder(){
        return new BCryptPasswordEncoder();
    }

    /**
     * ResourceServerTokenServices 资源服务tokenservices
     * @return ResourceServerTokenServices
     */
    @Bean
    public ResourceServerTokenServices resourceServerTokenServices(){
        RemoteTokenServices tokenServices = new RemoteTokenServices();
        tokenServices.setClientId(&quot;client&quot;);
        tokenServices.setClientSecret(&quot;secret&quot;);
        tokenServices.setCheckTokenEndpointUrl(&quot;http://localhost:10000/oauth/check_token&quot;);
        //重置token转换器
        tokenServices.setAccessTokenConverter(accessTokenConverter());
        return tokenServices;
    }

    private AccessTokenConverter accessTokenConverter() {
        DefaultAccessTokenConverter tokenConverter = new DefaultAccessTokenConverter();
        DefaultUserAuthenticationConverter userConvert = new DefaultUserAuthenticationConverter();
        //设置用户名获取用户信息
        userConvert.setUserDetailsService(userDetailsService);
        //加入用户转换器
        tokenConverter.setUserTokenConverter(userConvert);
        return tokenConverter;
    }
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;控制器用法：&lt;/p&gt;
&lt;p&gt;@AuthenticationPrincipal + 用户类 变量&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;/**
 * &amp;lt;p&amp;gt;DESC: 订单控制器&amp;lt;/p&amp;gt;
 * &amp;lt;p&amp;gt;DATE: 2019-12-05 12:56&amp;lt;/p&amp;gt;
 * &amp;lt;p&amp;gt;VERSION:2.0.0&amp;lt;/p&amp;gt;
 * &amp;lt;p&amp;gt;@AUTHOR: ZhengYong&amp;lt;/p&amp;gt;
 */
@Slf4j
@RestController
@RequestMapping(&quot;/orders&quot;)
public class OrderController {

    @PostMapping
    public OrderInfo create(@RequestBody OrderInfo info, @AuthenticationPrincipal String username,
                            @AuthenticationPrincipal TbUser tbUser, HttpServletRequest srequest) throws IOException {
        log.info(&quot;【用户名】={}&quot;,username);
        log.info(&quot;【用户】={}&quot;,tbUser);
&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;获取单个属性，使用spring的表达式&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;    @PostMapping
    public OrderInfo create(@RequestBody OrderInfo info,
                            @AuthenticationPrincipal String username,
                            @AuthenticationPrincipal(expression = &quot;#this.id&quot;) Long id,
                            @AuthenticationPrincipal TbUser tbUser, HttpServletRequest srequest) throws IOException {
        log.info(&quot;【用户名】={}&quot;,username);
        log.info(&quot;【用户】={}&quot;,tbUser);
        log.info(&quot;【用户id】={}&quot;,id);
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;搭建zuul网关&lt;/h2&gt;
&lt;h3&gt;1、环境&lt;/h3&gt;
&lt;p&gt;pom.xml&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&amp;lt;?xml version=&quot;1.0&quot; encoding=&quot;UTF-8&quot;?&amp;gt;
&amp;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;&amp;gt;
    &amp;lt;modelVersion&amp;gt;4.0.0&amp;lt;/modelVersion&amp;gt;

    &amp;lt;parent&amp;gt;
        &amp;lt;groupId&amp;gt;com.eim&amp;lt;/groupId&amp;gt;
        &amp;lt;artifactId&amp;gt;security_item&amp;lt;/artifactId&amp;gt;
        &amp;lt;version&amp;gt;1.0.0-SNAPSHOT&amp;lt;/version&amp;gt;
    &amp;lt;/parent&amp;gt;

    &amp;lt;artifactId&amp;gt;is-server-gateway&amp;lt;/artifactId&amp;gt;
    &amp;lt;version&amp;gt;1.0.0-SNAPSHOT&amp;lt;/version&amp;gt;

    &amp;lt;dependencies&amp;gt;
        &amp;lt;dependency&amp;gt;
            &amp;lt;groupId&amp;gt;org.springframework.cloud&amp;lt;/groupId&amp;gt;
            &amp;lt;artifactId&amp;gt;spring-cloud-starter-netflix-zuul&amp;lt;/artifactId&amp;gt;
        &amp;lt;/dependency&amp;gt;
    &amp;lt;/dependencies&amp;gt;

    &amp;lt;build&amp;gt;
        &amp;lt;plugins&amp;gt;
            &amp;lt;plugin&amp;gt;
                &amp;lt;groupId&amp;gt;org.apache.maven.plugins&amp;lt;/groupId&amp;gt;
                &amp;lt;artifactId&amp;gt;maven-compiler-plugin&amp;lt;/artifactId&amp;gt;
                &amp;lt;configuration&amp;gt;
                    &amp;lt;source&amp;gt;1.8&amp;lt;/source&amp;gt;
                    &amp;lt;target&amp;gt;1.8&amp;lt;/target&amp;gt;
                &amp;lt;/configuration&amp;gt;
            &amp;lt;/plugin&amp;gt;
        &amp;lt;/plugins&amp;gt;
    &amp;lt;/build&amp;gt;

&amp;lt;/project&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;application.yml&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;hystrix和ribbon是控制超时时间，防止请求时间过长，造成 熔断。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;pre&gt;&lt;code&gt;server:
  port: 9070
zuul:
  prefix: /api
  routes:
    token:
      url: http://localhost:10000
    order:
      url: http://localhost:10010
##    另一种路由配置  
##    token:
##      path: /token/**
##      serviceId: ddd
  sensitive-headers:
hystrix:
  command:
    default:
      execution:
        isolation:
          thread:
            timeoutInMillisecond: 6000 # 熔断超时时长：6000ms
ribbon:
  ConnectTimeout: 2500 # 连接超时时间(ms)
  ReadTimeout: 20000 # 通信超时时间(ms)
  OkToRetryOnAllOperations: true # 是否对所有操作重试
  MaxAutoRetriesNextServer: 2 # 同一服务不同实例的重试次数
  MaxAutoRetries: 1 # 同一实例的重试次数
  okhttp:
    enabled: true
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;2、开启zuul网关&lt;/h3&gt;
&lt;blockquote&gt;
&lt;p&gt;@EnableZuulProxy&lt;/p&gt;
&lt;/blockquote&gt;
&lt;pre&gt;&lt;code&gt;/**
 * &amp;lt;p&amp;gt;DESC: 启动类&amp;lt;/p&amp;gt;
 * &amp;lt;p&amp;gt;DATE: 2019-12-08 21:52&amp;lt;/p&amp;gt;
 * &amp;lt;p&amp;gt;VERSION:1.0.0&amp;lt;/p&amp;gt;
 * &amp;lt;p&amp;gt;@AUTHOR: ZhengYong&amp;lt;/p&amp;gt;
 */
@EnableZuulProxy
@SpringBootApplication
public class GatewayApplication {
    public static void main(String[] args) {
        SpringApplication.run(GatewayApplication.class,args);
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;3、自定义过滤器&lt;/h3&gt;
&lt;blockquote&gt;
&lt;p&gt;继承ZuulFilter，就可以实现自定义过滤器&lt;/p&gt;
&lt;/blockquote&gt;
&lt;pre&gt;&lt;code&gt;/**
 * &amp;lt;p&amp;gt;DESC: 认证过滤器&amp;lt;/p&amp;gt;
 * &amp;lt;p&amp;gt;DATE: 2019-12-10 10:45&amp;lt;/p&amp;gt;
 * &amp;lt;p&amp;gt;VERSION:2.0.0&amp;lt;/p&amp;gt;
 * &amp;lt;p&amp;gt;@AUTHOR: ZhengYong&amp;lt;/p&amp;gt;
 */
@Slf4j
@Component
public class AuthenticationFilter extends ZuulFilter {

    /**
     * 过滤类型
     * FilterConstants 过滤器常量池
     * @return 字符串
     */
    @Override
    public String filterType() {
        return PRE_TYPE;
    }

    /**
     * 过滤器顺序
     * @return 数字
     */
    @Override
    public int filterOrder() {
        return PRE_DECORATION_FILTER_ORDER -4;
    }

    /**
     * 是否应该过滤
     * @return boolean
     */
    @Override
    public boolean shouldFilter() {
        return true;
    }

    /**
     * 运行逻辑
     * @return Object  返回 null,执行下一个过滤器（放行）。
     * @throws ZuulException zuul异常
     */
    @Override
    public Object run() throws ZuulException {
        log.info(&quot;【start authentication】&quot;);
        return null;
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;4、Zuul降级&lt;/h3&gt;
&lt;blockquote&gt;
&lt;p&gt;实现FallbackProvider接口。重写两个方法&lt;/p&gt;
&lt;/blockquote&gt;
&lt;pre&gt;&lt;code&gt;/**
 * &amp;lt;p&amp;gt;DESC: Zuul提供一种降级功能&amp;lt;/p&amp;gt;
 * &amp;lt;p&amp;gt;DATE: 2019-12-10 14:01&amp;lt;/p&amp;gt;
 * &amp;lt;p&amp;gt;VERSION:2.0.0&amp;lt;/p&amp;gt;
 * &amp;lt;p&amp;gt;@AUTHOR: ZhengYong&amp;lt;/p&amp;gt;
 */
@Slf4j
@Component
public class ZuulFallback implements FallbackProvider {

    /**
     * 返回值表示需要针对此微服务做回退处理（该名称一定要是注册进入 eureka 微服务中的那个 serviceId 名称）；
     * @return string
     */
    @Override
    public String getRoute() {
        /**
         * 只有zuul配置serviceId 的形式,回退才能生效
         *  eg:
         *     token:
         *       path: /token/**
         *       serviceId: ddd
         *
         * api服务id，如果需要所有调用都支持回退，则return &quot;*&quot;或return null
         */
        return null;
    }

    @Override
    public ClientHttpResponse fallbackResponse(String route, Throwable cause) {
        return new ClientHttpResponse(){

            /**
             * 设置返回头
             * @return HttpHeaders
             */
            @Override
            public HttpHeaders getHeaders() {
                log.warn(&quot;warning={}&quot;,&quot;进入zuulFallback，设置header&quot;);
                HttpHeaders httpHeaders = new HttpHeaders();
                httpHeaders.setContentType(MediaType.APPLICATION_JSON_UTF8);
                httpHeaders.setCacheControl(&quot;private&quot;);
                return httpHeaders;
            }

            /**
             * 设置返回内容
             * @return InputStream
             * @throws IOException IOException
             */
            @Override
            public InputStream getBody() throws IOException {
                log.warn(&quot;warning={}&quot;,&quot;进入zuulFallback，设置body&quot;);
                ObjectMapper mapper = new ObjectMapper();
                return new ByteArrayInputStream(mapper.writeValueAsBytes(&quot;服务暂不可用&quot;));
            }

            /**
             * 返回 HttpStatus 对象
             * @return HttpStatus
             * @throws IOException 异常
             */
            @Override
            public HttpStatus getStatusCode() throws IOException {
                return HttpStatus.SERVICE_UNAVAILABLE;
            }

            /**
             * 返回 httpstatus 状态码
             * @return 状态码
             * @throws IOException 异常
             */
            @Override
            public int getRawStatusCode() throws IOException {
                return HttpStatus.SERVICE_UNAVAILABLE.value();
            }

            /**
             * 返回状态文字
             * @return 文字
             * @throws IOException 异常
             */
            @Override
            public String getStatusText() throws IOException {
                return HttpStatus.SERVICE_UNAVAILABLE.getReasonPhrase();
            }

            @Override
            public void close() {
            }
        };
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;搭建eureka&lt;/h2&gt;
&lt;h3&gt;1、环境&lt;/h3&gt;
&lt;p&gt;pom.xml&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&amp;lt;?xml version=&quot;1.0&quot; encoding=&quot;UTF-8&quot;?&amp;gt;
&amp;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;&amp;gt;
    &amp;lt;parent&amp;gt;
        &amp;lt;artifactId&amp;gt;security_item&amp;lt;/artifactId&amp;gt;
        &amp;lt;groupId&amp;gt;com.eim&amp;lt;/groupId&amp;gt;
        &amp;lt;version&amp;gt;1.0.0-SNAPSHOT&amp;lt;/version&amp;gt;
    &amp;lt;/parent&amp;gt;
    &amp;lt;modelVersion&amp;gt;4.0.0&amp;lt;/modelVersion&amp;gt;

    &amp;lt;artifactId&amp;gt;is-server-eureka&amp;lt;/artifactId&amp;gt;

    &amp;lt;dependencies&amp;gt;
        &amp;lt;dependency&amp;gt;
            &amp;lt;groupId&amp;gt;org.springframework.cloud&amp;lt;/groupId&amp;gt;
            &amp;lt;artifactId&amp;gt;spring-cloud-starter-netflix-eureka-server&amp;lt;/artifactId&amp;gt;
        &amp;lt;/dependency&amp;gt;
    &amp;lt;/dependencies&amp;gt;

&amp;lt;/project&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;application.yml&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;server:
  port: 8080
spring:
  application:
    name: eureka # 应用名称
eureka:
  client:
    register-with-eureka: false
    fetch-registry: false
    service-url:
      defaultZone: http://127.0.0.1:8080/eureka
  instance:
    instance-id: ${spring.application.name}:${server.port}
    # 10秒即过期
    lease-expiration-duration-in-seconds: 10
    # 5秒一次心跳
    lease-renewal-interval-in-seconds: 5
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;2、开启eureka服务&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;/**
 * &amp;lt;p&amp;gt;DESC: 启动类&amp;lt;/p&amp;gt;
 * &amp;lt;p&amp;gt;DATE: 2019-12-11 16:24&amp;lt;/p&amp;gt;
 * &amp;lt;p&amp;gt;VERSION:2.0.0&amp;lt;/p&amp;gt;
 * &amp;lt;p&amp;gt;@AUTHOR: ZhengYong&amp;lt;/p&amp;gt;
 */
@EnableEurekaServer
@SpringBootApplication
public class EurekaApplication {

    public static void main(String[] args) {
        SpringApplication.run(EurekaApplication.class,args);
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;3、搭建安全的eureka&lt;/h3&gt;
&lt;h4&gt;3.1、eureka中加入security依赖&lt;/h4&gt;
&lt;pre&gt;&lt;code&gt;        &amp;lt;dependency&amp;gt;
            &amp;lt;groupId&amp;gt;org.springframework.cloud&amp;lt;/groupId&amp;gt;
            &amp;lt;artifactId&amp;gt;spring-cloud-starter-oauth2&amp;lt;/artifactId&amp;gt;
        &amp;lt;/dependency&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;或则&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;	   &amp;lt;!-- Security --&amp;gt;
       &amp;lt;dependency&amp;gt;
           &amp;lt;groupId&amp;gt;org.springframework.cloud&amp;lt;/groupId&amp;gt;
           &amp;lt;artifactId&amp;gt;spring-cloud-starter-security&amp;lt;/artifactId&amp;gt;
       &amp;lt;/dependency&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;h4&gt;3.2、配置Eureka Server的application.yml文件&lt;/h4&gt;
&lt;pre&gt;&lt;code&gt;#基于SpringBoot 1.X版本是如下配置：
security.basic.enabled: true
security.user.name=admin
security.user.password=12345
#基于SpringBoot 2.x版本将security属性移入到spring配置里，且basic.enabled属性无效
spring.security.user.name=admin
spring.security.user.password=12345
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;核心代码：  defaultZone: http://admin:123456@127.0.0.1:${server.port}/eureka&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;server:
  port: 8080
spring:
  application:
    name: eureka # 应用名称
  security:
    user:
      name: admin
      password: 123456
eureka:
  client:
    #是否注册自己的信息到EurekaServer，默认是true
    register-with-eureka: false
    #是否拉取其它服务的信息，默认是true
    fetch-registry: false
    # EurekaServer的地址，现在是自己的地址，如果是集群，需要加上其它Server的地址。
    service-url:
      defaultZone: http://admin:123456@127.0.0.1:${server.port}/eureka
  instance:
    instance-id: ${spring.application.name}:${server.port}
    # 10秒即过期
    lease-expiration-duration-in-seconds: 10
    # 5秒一次心跳
    lease-renewal-interval-in-seconds: 5
&lt;/code&gt;&lt;/pre&gt;
&lt;h4&gt;3.3、配置eureka安全配置&lt;/h4&gt;
&lt;blockquote&gt;
&lt;p&gt;1、需要httpBasic模式&lt;/p&gt;
&lt;p&gt;2、关闭csrf&lt;/p&gt;
&lt;/blockquote&gt;
&lt;pre&gt;&lt;code&gt;/**
 * &amp;lt;p&amp;gt;DESC: eureka安全配置&amp;lt;/p&amp;gt;
 * &amp;lt;p&amp;gt;DATE: 2019-12-23 14:12&amp;lt;/p&amp;gt;
 * &amp;lt;p&amp;gt;VERSION:2.0.0&amp;lt;/p&amp;gt;
 * &amp;lt;p&amp;gt;@AUTHOR: ZhengYong&amp;lt;/p&amp;gt;
 */
@Configuration
public class EurekaSecurityConfig extends WebSecurityConfigurerAdapter {

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http.csrf().disable().httpBasic().and().authorizeRequests().anyRequest().authenticated();
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;网关过滤器&lt;/h2&gt;
&lt;h3&gt;1、认证过滤器&lt;/h3&gt;
&lt;blockquote&gt;
&lt;p&gt;1、指定过滤顺序&lt;/p&gt;
&lt;p&gt;2、指定过滤类型&lt;/p&gt;
&lt;p&gt;3、指定是否过滤，还是放行&lt;/p&gt;
&lt;p&gt;4、如果过滤，执行run方法中的代码&lt;/p&gt;
&lt;/blockquote&gt;
&lt;pre&gt;&lt;code&gt;/**
 * &amp;lt;p&amp;gt;DESC: 认证过滤器&amp;lt;/p&amp;gt;
 * &amp;lt;p&amp;gt;DATE: 2019-12-10 10:45&amp;lt;/p&amp;gt;
 * &amp;lt;p&amp;gt;VERSION:2.0.0&amp;lt;/p&amp;gt;
 * &amp;lt;p&amp;gt;@AUTHOR: ZhengYong&amp;lt;/p&amp;gt;
 */
@Slf4j
@Component
public class AuthenticationFilter extends ZuulFilter {

    /**
     * 过滤类型S
     * FilterConstants 过滤器常量池
     * @return 字符串
     */
    @Override
    public String filterType() {
        return PRE_TYPE;
    }

    /**
     * 过滤器顺序
     * @return 数字
     */
    @Override
    public int filterOrder() {
        return PRE_DECORATION_FILTER_ORDER -4;
    }

    /**
     * 是否应该过滤
     * @return boolean
     */
    @Override
    public boolean shouldFilter() {
        return true;
    }

    /**
     * 运行逻辑
     * @return Object  返回 null,执行下一个过滤器（放行）。
     * @throws ZuulException zuul异常
     */
    @Override
    public Object run() throws ZuulException {
        log.info(&quot;【start authentication】&quot;);
        //获取 HttpServletRequest 请求对象
        RequestContext currentContext = RequestContext.getCurrentContext();
        HttpServletRequest request = currentContext.getRequest();
        String requestUrl = request.getRequestURI();
        //1、判断请求地址是否是token开头,token 开头请求，是认证服务器的请求，不用校验
        if(StringUtils.startsWith(requestUrl,&quot;/api/token&quot;)){
            return null;
        }

        //2、如果不是请求token，就是业务上的请求，需要从请求头中拿出Authorization token 信息
        String authorization = request.getHeader(&quot;Authorization&quot;);
        //如果没带请求头，继续往下走。不管token有没有，不能在这里阻塞。
        if(StringUtils.isBlank(authorization)){
            return null;
        }

        //3、bearer  是oauth认证token,不是oauth的token,也是直接放过
        if(!StringUtils.startsWithIgnoreCase(authorization,&quot;bearer &quot;)){
            return null;
        }

        //剩下的就是，业务接口，携带了 oauth认证token。做校验
        try {
            TokenInfo tokeninfo = getTokenInfo(authorization);
            request.setAttribute(&quot;tokenInfo&quot;,tokeninfo);
        }catch (Exception e){
            log.error(&quot;【get tokenInfo fail】&quot;);
            e.printStackTrace();
        }

        return null;
    }

    private TokenInfo getTokenInfo(String authorization) throws IOException {
        String token = StringUtils.substringAfter(authorization, &quot;bearer &quot;);
        String oauthUrl = &quot;http://localhost:10000/oauth/check_token?token=&quot;+token;

        OkHttpClient client = new OkHttpClient();
        Headers header = new Headers.Builder()
                .add(&quot;Content-Type&quot;, MediaType.APPLICATION_JSON_UTF8_VALUE)
                .build();
        Request request = new Request.Builder()
                .url(oauthUrl)
                .headers(header)
                .build();
        Call call = client.newCall(request);
        Response response = call.execute();
        int code = response.code();
        if(code != 200){
            throw new RuntimeException();
        }
      //这里调用的body的string()方法
        String bodyStr = response.body().string();
        log.info(&quot;【校验token】 code={},{}&quot;,code,bodyStr);
        //返回文本  {&quot;aud&quot;:[&quot;resourceid&quot;],&quot;user_name&quot;:&quot;admin&quot;,&quot;scope&quot;:[&quot;read&quot;,&quot;write&quot;],&quot;active&quot;:true,&quot;exp&quot;:1576071177,&quot;authorities&quot;:[&quot;admin&quot;],&quot;client_id&quot;:&quot;client&quot;}
        TokenInfo tokenInfo = JSON.parseObject(bodyStr, TokenInfo.class);
        log.info(&quot;【tokeninfo】={}&quot;,tokenInfo.toString());
        return tokenInfo;
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;TokenInfo&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;根据校验token返回的参数，组装成tokenInfo对象&lt;/p&gt;
&lt;/blockquote&gt;
&lt;pre&gt;&lt;code&gt;/**
 * &amp;lt;p&amp;gt;DESC: token信息&amp;lt;/p&amp;gt;
 * &amp;lt;p&amp;gt;DATE: 2019-12-11 14:33&amp;lt;/p&amp;gt;
 * &amp;lt;p&amp;gt;VERSION:2.0.0&amp;lt;/p&amp;gt;
 * &amp;lt;p&amp;gt;@AUTHOR: ZhengYong&amp;lt;/p&amp;gt;
 */
@Data
public class TokenInfo {

    /**
     * 是否可用
     */
    private boolean active;

    private String client_id;

    private String[] scope;

    private String user_name;

    /**
     * resourceid
     */
    private String[] aud;

    private Date exp;

    /**
     * 角色权限
     */
    private String[] authorities;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;2、网关审计日志过滤器&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;/**
 * &amp;lt;p&amp;gt;DESC: 审计日志过滤器&amp;lt;/p&amp;gt;
 * &amp;lt;p&amp;gt;DATE: 2019-12-13 13:39&amp;lt;/p&amp;gt;
 * &amp;lt;p&amp;gt;VERSION:2.0.0&amp;lt;/p&amp;gt;
 * &amp;lt;p&amp;gt;@AUTHOR: ZhengYong&amp;lt;/p&amp;gt;
 */
@Slf4j
@Component
public class AuditLogFilter extends ZuulFilter {
    @Override
    public String filterType() {
        return PRE_TYPE;
    }

    @Override
    public int filterOrder() {
        return PRE_DECORATION_FILTER_ORDER-3;
    }

    @Override
    public boolean shouldFilter() {
        return true;
    }

    @Override
    public Object run() throws ZuulException {
        log.info(&quot;【新增审计日志】&quot;);
        return null;
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;3、授权过滤器&lt;/h3&gt;
&lt;blockquote&gt;
&lt;p&gt;被誉为：最后的防线&lt;/p&gt;
&lt;/blockquote&gt;
&lt;pre&gt;&lt;code&gt;/**
 * &amp;lt;p&amp;gt;DESC: 授权过滤器&amp;lt;/p&amp;gt;
 * &amp;lt;p&amp;gt;DATE: 2019-12-13 13:43&amp;lt;/p&amp;gt;
 * &amp;lt;p&amp;gt;VERSION:2.0.0&amp;lt;/p&amp;gt;
 * &amp;lt;p&amp;gt;@AUTHOR: ZhengYong&amp;lt;/p&amp;gt;
 */
@Slf4j
@Component
public class AuthorizationFilter extends ZuulFilter {
    @Override
    public String filterType() {
        return PRE_TYPE;
    }

    @Override
    public int filterOrder() {
        return PRE_DECORATION_FILTER_ORDER - 2;
    }

    @Override
    public boolean shouldFilter() {
        return true;
    }

    @Override
    public Object run() throws ZuulException {
        RequestContext currentContext = RequestContext.getCurrentContext();
        HttpServletRequest request = currentContext.getRequest();

        //判断当前的请求是不是需要身份认证，或者是需不需要做权限的判断
        if(isNeedAuth(request)){
            TokenInfo tokenInfo = (TokenInfo) request.getAttribute(&quot;tokenInfo&quot;);
            //不为空且是 激活状态的
            if(tokenInfo != null &amp;amp;&amp;amp; tokenInfo.isActive()){
                //能拿到用户信息，下一步就要判断权限。如果没有权限那么就报403的错误
                if(!hasPermission(tokenInfo,request)){
                    //没有权限
                    handleError(403,currentContext);
                }
            }else{
                log.error(&quot;【audit log update fail 401】&quot;);
                //拒绝访问
                handleError(401,currentContext);
            }
        }
        return null;
    }

    private void handleError(int status, RequestContext currentContext) {
        currentContext.getResponse().setContentType(MediaType.APPLICATION_JSON_UTF8_VALUE);
        currentContext.setResponseStatusCode(status);
        currentContext.setResponseBody(&quot;{\&quot;msg\&quot;:\&quot;失败\&quot;}&quot;);
        //到此终止，不再往下走
        currentContext.setSendZuulResponse(false);
    }

    private boolean hasPermission(TokenInfo tokenInfo, HttpServletRequest request) {
        return RandomUtils.nextInt() % 2 == 0 ;
    }

    /**
     * 判断当前的请求是不是需要身份认证，或者是需不需要做权限的判断
     * @param request 请求
     * @return boolean
     */
    private boolean isNeedAuth(HttpServletRequest request) {
        //放行请求token的请求
        if(request.getRequestURI().startsWith(&quot;/api/token&quot;)){
            return false;
        }
        return true;
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;4、测试&lt;/h3&gt;
&lt;p&gt;（1）先去拿token&lt;/p&gt;
&lt;p&gt;（2）拿到token，通过网关，获取访问order服务，或则price服务&lt;/p&gt;
&lt;p&gt;测试成功：&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;说明我们现在写的业务逻辑时生效的，我们在网关这 把权限控制了。现在就可以把订单服务里面和安全相关的逻辑去掉。包括先关的代码
这些逻辑去掉后，我们之前说的那些问题。比如说安全的逻辑和业务逻辑耦合，安全逻辑一遍导致业务逻辑重新部署，包括随着业务接节点增加，你们安全信息也在这里面。业务节点节点增加。认证服务器压力增大等等这些问题。都被解决掉了&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h3&gt;5、去掉订单服务安全的相关代码&lt;/h3&gt;
&lt;p&gt;（1） 首先去掉Oauth2的依赖。&lt;/p&gt;
&lt;p&gt;（2） 所有和安全相关的代码都删掉&lt;/p&gt;
&lt;p&gt;（3）当前用户的信息使用这个注解来拿到的。现在这个注解依赖都被删掉了 那么如何拿到当前用户的信息&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;在网关授权往下走的之前 ，设置header。这里也可以传json对象&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;&lt;img src=&quot;%E8%AF%B7%E6%B1%82%E5%A4%B4%E4%B8%AD%E8%8E%B7%E5%8F%96%E6%95%B0%E6%8D%AE.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;存入用户名到请求头中&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;%E5%87%BA%E5%85%A5%E7%94%A8%E6%88%B7%E5%90%8D%E5%88%B0%E8%AF%B7%E6%B1%82%E5%A4%B4%E4%B8%AD.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;h3&gt;6、启动测试&lt;/h3&gt;
&lt;p&gt;启动oderApi和网关
还是调这个创建订单的服务，要多试几次，因为是50%的几率是200&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;上面是基本架构（目的为了让我们知道网关都具体干了什么），下面对服务进行改造，进一步减少代码&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2&gt;&lt;strong&gt;=&lt;strong&gt;&lt;strong&gt;=&lt;/strong&gt;&lt;/strong&gt;=&lt;strong&gt;&lt;strong&gt;=&lt;/strong&gt;&lt;/strong&gt;=&lt;strong&gt;&lt;strong&gt;=&lt;/strong&gt;&lt;/strong&gt;=&lt;strong&gt;&lt;strong&gt;=&lt;/strong&gt;&lt;/strong&gt;=&lt;strong&gt;&lt;strong&gt;=&lt;/strong&gt;&lt;/strong&gt;=&lt;strong&gt;&lt;strong&gt;=&lt;/strong&gt;&lt;/strong&gt;=&lt;strong&gt;&lt;strong&gt;=&lt;/strong&gt;&lt;/strong&gt;=&lt;strong&gt;&lt;strong&gt;=&lt;/strong&gt;&lt;/strong&gt;=&lt;strong&gt;&lt;strong&gt;=&lt;/strong&gt;&lt;/strong&gt;=****=&lt;/strong&gt;&lt;/h2&gt;
&lt;h2&gt;微服务之间的通讯安全&lt;/h2&gt;
&lt;p&gt;&lt;img src=&quot;%E5%BE%AE%E6%9C%8D%E5%8A%A1%E4%B9%8B%E9%97%B4%E9%80%9A%E8%AE%AF%E5%AE%89%E5%85%A8.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;h3&gt;问题一：&lt;/h3&gt;
&lt;p&gt;在网关上做限流还是有一些问题的。例如我的订单服务限流是100，库存服务限流也是100。但是我的订单服务会调用我的库存服务。那么在网关这，给订单转100个请求，库存转100个请求，最后订单又调了库存，库存会同时受到200个请求。这时候库存服务可能就挂掉了。
这是在网关这里做限流，可能会出现的一些问题。&lt;/p&gt;
&lt;h3&gt;第二个问题：&lt;/h3&gt;
&lt;p&gt;就是身份认证。
效率低，在网关上每一个请求都要去认证服务器验令牌。这样就会导致多一次网络请求的开销。同时我的认证服务器压力就会很大，而且还要保证高可用。而且一旦我的认证服务器坏了，没法验令牌了。所有的请求就都没法处理了。
不安全：在传递用户信息的时候，是在请求头里面加了个username，然后把用户名放进去了。 订单服务从请求头里就知道他是谁了。&lt;/p&gt;
&lt;p&gt;那么我再写一个其他服务，也调用订单服务，下一个订单，在请求头里面传一个张三。订单服务就认为我是张三。传个李四，订单服务就认为我是李四，这样做实际上是不安全的
你不能从头里面一个请求中明文的参数，来判断用户是谁。
第三个问题:传递麻烦，把信息放在请求里面 传给了订单服务，比如说 订单服务还要调用库存服务。那么订单服务调用库存服务的时候，也要在请求头里面再加上username，再去调，库存服务才能知道当前这个用户是谁。传递起来也是比较麻烦的。这是认证面临的一些问题。&lt;/p&gt;
&lt;h3&gt;问题三：&lt;/h3&gt;
&lt;p&gt;授权:和限流的问题类似。例如我在权限控制里面 控制这个人只能访问订单服务，不能访问库存服务。，但是订单服务内部又访问了库存服务。
我一访问订单服务，实际上又访问库存的服务了。权限实际上是越权了。没有限制住，
微服务之间去调用的时候，通过网关调用的时候，还有一个是雪崩。当一个服务出现问题的时候，把其他的服务都给带死了。
比如说我的库存服务。 因为某些原因响应变慢了。比如说是网络的问题，数据库压力大或者是其他的一些问题。我的库存服务变慢了。会导致我的订单服务也变慢。然后所有调用库存服务的服务都变慢。这些线程全都在这等待着，然后这些线程可能都是通过网关进来的。那么网关上也有一大堆线程在这等待着，最后导致所有的服务，你的网关上所有线程都被占住了。
可能订单服务的所有线程都被占住了，最后导致，大量的服务都不能用了。但是最根上只有一个库存服务出了问题。这就是我们说的服务的雪崩，&lt;/p&gt;
&lt;p&gt;刚才讲的这些问题就是我们第六章要解决的问题。&lt;/p&gt;
&lt;h2&gt;解决认证问题&lt;/h2&gt;
&lt;blockquote&gt;
&lt;p&gt;首先来解决认证的问题。&lt;/p&gt;
&lt;p&gt;1.效率低，每次认证都要去认证服务器调一次服务。
2.传递用户身份，在请求头里面，
3.服务之间传递请求头比较麻烦。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;jwt令牌。
spring提供了工具，帮你在微服务之间传递令牌。让你不用去写额外的代码&lt;/p&gt;
&lt;h3&gt;1、服务器端的改造&lt;/h3&gt;
&lt;p&gt;看一下认证服务器配置的这个类。这里有个tokenStore，就是令牌的存储器。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;可以指定为jdbc存储token&lt;/li&gt;
&lt;li&gt;也可以指定为jwtToken&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;img src=&quot;https://tangtangit.com/_astro/jdbcTokenStore.DSmLShoM_gb2Iy.webp&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;我们new  一个jwtTokenStore它 需要一个参数jwtTokenEnhancer&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://tangtangit.com/_astro/jwtToken.B7LDKmkn_Z14LBq.webp&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;需要set一个key就是签名的键。jwt本身不是加密的，谁都可以看到令牌里的信息，它用来保证安全的方式就这个签名。你需要用一个key对这个token进行签名。然后使用token的人用相同的key去验证签名，如果那个签名证明是这个key签出去的。那么就说明token里面的内容没被改过。没被改过，我就认为它是安全的。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;%E6%9E%84%E5%BB%BAtoken%E5%A2%9E%E5%BC%BA%E5%99%A8.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;接收token的人需要这个key来验证签名，所以我们要把这个key当做一个服务暴露出去。 这样使用这个token的人才能通过这个服务拿到 这个key来验证签名。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;%E6%9A%B4%E9%9C%B2tokenKey%E6%9C%8D%E5%8A%A1.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;h3&gt;2、运行认证服务器测试&lt;/h3&gt;
&lt;p&gt;看一下jwt到底是个什么样子，然后再往下走。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;%E6%8E%A2%E7%A9%B6jwtToken.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;只是防串改，并不信息保密。所以不建议往jwt里面放一些和自己业务 相关的信息。可能会导致你的信息泄露。&lt;/p&gt;
&lt;p&gt;这里我们用了简单的串做为key，生产的时候不要这么去做。因为一旦泄露，发出去的token就危险了。&lt;/p&gt;
&lt;h3&gt;3、key做安全措施&lt;/h3&gt;
&lt;p&gt;rsa工具类&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;可以指定路径和密码，构建出公钥和私钥&lt;/p&gt;
&lt;/blockquote&gt;
&lt;pre&gt;&lt;code&gt;public class RsaUtils {
    /**
     * 从文件中读取公钥
     *
     * @param filename 公钥保存路径，相对于classpath
     * @return 公钥对象
     * @throws Exception
     */
    public static PublicKey getPublicKey(String filename) throws Exception {
        byte[] bytes = readFile(filename);
        return getPublicKey(bytes);
    }

    /**
     * 从文件中读取密钥
     *
     * @param filename 私钥保存路径，相对于classpath
     * @return 私钥对象
     * @throws Exception
     */
    public static PrivateKey getPrivateKey(String filename) throws Exception {
        byte[] bytes = readFile(filename);
        return getPrivateKey(bytes);
    }

    /**
     * 获取公钥
     *
     * @param bytes 公钥的字节形式
     * @return
     * @throws Exception
     */
    public static PublicKey getPublicKey(byte[] bytes) throws Exception {
        X509EncodedKeySpec spec = new X509EncodedKeySpec(bytes);
        KeyFactory factory = KeyFactory.getInstance(&quot;RSA&quot;);
        return factory.generatePublic(spec);
    }

    /**
     * 获取密钥
     *
     * @param bytes 私钥的字节形式
     * @return
     * @throws Exception
     */
    public static PrivateKey getPrivateKey(byte[] bytes) throws Exception {
        PKCS8EncodedKeySpec spec = new PKCS8EncodedKeySpec(bytes);
        KeyFactory factory = KeyFactory.getInstance(&quot;RSA&quot;);
        return factory.generatePrivate(spec);
    }

    /**
     * 根据密文，生存rsa公钥和私钥,并写入指定文件
     *
     * @param publicKeyFilename  公钥文件路径
     * @param privateKeyFilename 私钥文件路径
     * @param secret             生成密钥的密文
     * @throws IOException
     * @throws NoSuchAlgorithmException
     */
    public static void generateKey(String publicKeyFilename, String privateKeyFilename, String secret) throws Exception {
        KeyPairGenerator keyPairGenerator = KeyPairGenerator.getInstance(&quot;RSA&quot;);
        SecureRandom secureRandom = new SecureRandom(secret.getBytes());
        keyPairGenerator.initialize(1024, secureRandom);
        KeyPair keyPair = keyPairGenerator.genKeyPair();
        // 获取公钥并写出
        byte[] publicKeyBytes = keyPair.getPublic().getEncoded();
        writeFile(publicKeyFilename, publicKeyBytes);
        // 获取私钥并写出
        byte[] privateKeyBytes = keyPair.getPrivate().getEncoded();
        writeFile(privateKeyFilename, privateKeyBytes);
    }

    private static byte[] readFile(String fileName) throws Exception {
        return Files.readAllBytes(new File(fileName).toPath());
    }

    private static void writeFile(String destPath, byte[] bytes) throws IOException {
        File dest = new File(destPath);
        if (!dest.exists()) {
            dest.createNewFile();
        }
        Files.write(dest.toPath(), bytes);
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;看工具类的使用&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;/**
 * &amp;lt;p&amp;gt;DESC: 做出公钥和私钥&amp;lt;/p&amp;gt;
 * &amp;lt;p&amp;gt;DATE: 2019-12-16 14:39&amp;lt;/p&amp;gt;
 * &amp;lt;p&amp;gt;VERSION:2.0.0&amp;lt;/p&amp;gt;
 * &amp;lt;p&amp;gt;@AUTHOR: ZhengYong&amp;lt;/p&amp;gt;
 */
public class DoRsaKey {

    public static final String pubPath = &quot;C:/zy_java/security/security_item/is-server-auth/src/main/resources/key/rsa.pub&quot;;
    public static final String priPath = &quot;C:/zy_java/security/security_item/is-server-auth/src/main/resources/key/rsa.pri&quot;;

    public static void main(String[] args) throws Exception {
        //生成公钥私钥
        //RsaUtils.generateKey(pubPath,priPath,&quot;123456&quot;);
        
        //获取公钥私钥
        URL resource = DoRsaKey.class.getClassLoader().getResource(&quot;key/rsa.pub&quot;);
        System.out.println(resource.getPath());
        PublicKey publicKey = RsaUtils.getPublicKey(resource.getPath());
        System.out.println(publicKey);
    }

}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;使用公钥和秘钥替换简单的字符串&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;    /**
     * token 增强器
     * @return token增强器
     */
    private JwtAccessTokenConverter jwtTokenEnhancer(){
        JwtAccessTokenConverter converter = new JwtAccessTokenConverter();
        try {
            String pubPath = AuthServerConfig.class.getClassLoader().getResource(&quot;key/rsa.pub&quot;).getPath();
            String priPath = AuthServerConfig.class.getClassLoader().getResource(&quot;key/rsa.pri&quot;).getPath();
            PublicKey publicKey = RsaUtils.getPublicKey(pubPath);
            PrivateKey privateKey = RsaUtils.getPrivateKey(priPath);
            //设置秘钥对
            converter.setKeyPair(new KeyPair(publicKey,privateKey));
        } catch (Exception e) {
            e.printStackTrace();
        }
        return converter;
    }
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;JWT改造之网关和服务改造&lt;/h2&gt;
&lt;p&gt;网关上认证去做哪些改造:&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;在网关上用jwt去解析用户信息，而不再发送校验令牌的请求了。&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;之前的时候网关上实际上写了很多的代码&lt;/p&gt;
&lt;p&gt;包括认证，发check_token去把token请求，换成用户信息。&lt;/p&gt;
&lt;p&gt;审计日志和授权。&lt;/p&gt;
&lt;p&gt;限流&lt;/p&gt;
&lt;p&gt;filter都删掉，Spring Security和Spring OAuth 已经把所有的都封装好了。除了限流和日志。&lt;strong&gt;审计、认证、授权都封装好了&lt;/strong&gt;。&lt;/p&gt;
&lt;h3&gt;1、用Spring Security实现功能&lt;/h3&gt;
&lt;p&gt;在gateway网关 pom.xml加入Starter-oauth2的依赖。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;        &amp;lt;dependency&amp;gt;
            &amp;lt;groupId&amp;gt;org.springframework.cloud&amp;lt;/groupId&amp;gt;
            &amp;lt;artifactId&amp;gt;spring-cloud-starter-oauth2&amp;lt;/artifactId&amp;gt;
        &amp;lt;/dependency&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;img src=&quot;%E7%BD%91%E5%85%B3%E6%96%B0%E5%A2%9Eoauth2%E4%BE%9D%E8%B5%96.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;本节课先讲认证相关的功能。我们现在不再发校验令牌的请求，而是直接从jwt里面读取用户的信息，jwt的信息会有一个签名，读取信息的时候要去验这个签名，验签名的时候需要有一个key，&lt;strong&gt;key我们需要在Gateway启动的时候，从认证服务器获取到这个key&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;%E6%94%B9%E9%80%A0%E7%BD%91%E5%85%B3%E5%8E%BB%E6%8E%89%E9%AA%8C%E8%AF%81token.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;key-uri首先要配置从哪里拿key。这里应该配置的是认证服务器的地址。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;security:
  oauth2:
    resource:
      jwt:
        key-uri: 
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;img src=&quot;key-uri%E9%85%8D%E7%BD%AE1.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;key-uri%E9%85%8D%E7%BD%AE2.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;TokenKeyEndpoint来处理，&lt;strong&gt;/oauth/token_key&lt;/strong&gt;这个请求。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;%E5%A4%84%E7%90%86token_key.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;它就会把证书里面存的密钥返回&lt;/p&gt;
&lt;p&gt;上节课配置tokenStore的时候&lt;/p&gt;
&lt;p&gt;下面配置配置了tokenKeyAccess。就是说访问这个tokenKeyUrl的时候必须要身份认证。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;tokenKeyUrl%E9%9C%80%E8%A6%81%E8%BA%AB%E4%BB%BD%E8%AE%A4%E8%AF%81.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;所以在这里我就要告诉他身份认证的信息。clinetId和clientSceret&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;security:
  oauth2:
    client:
      client-id: client
      client-secret: secret
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;2、配置资源服务器相关的配置&lt;/h3&gt;
&lt;p&gt;网关是作为资源服务器存在的，所以继承ResourceServerConfigAdapter&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;/**
 * &amp;lt;p&amp;gt;DESC: 网关资源服务器配置&amp;lt;/p&amp;gt;
 * &amp;lt;p&amp;gt;DATE: 2019-12-16 16:43&amp;lt;/p&amp;gt;
 * &amp;lt;p&amp;gt;VERSION:2.0.0&amp;lt;/p&amp;gt;
 * &amp;lt;p&amp;gt;@AUTHOR: ZhengYong&amp;lt;/p&amp;gt;
 */
@Configuration
@EnableResourceServer
public class GatewaySecurityConfig extends ResourceServerConfigurerAdapter {
    
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;我们自己写的认证的时候，我们要把所有/token的请求都放过去，因为/token开头的请求都是在申请令牌，你不能让申请令牌的这些请求也需要身份认证。&lt;/p&gt;
&lt;p&gt;默认情况下所有的请求都需要身份认证。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;	@Override
	public void configure(HttpSecurity http) throws Exception {
		http.authorizeRequests().anyRequest().authenticated();
	}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;我们在这里 加一句，申请令牌的所有请求都放行。不需要带令牌。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;@Configuration
@EnableResourceServer
public class GatewaySecurityConfig extends ResourceServerConfigurerAdapter {

    @Override
    public void configure(HttpSecurity http) throws Exception {
        http.authorizeRequests()
                .antMatchers(&quot;/api/token&quot;).permitAll()
                .anyRequest().authenticated();
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;3、order-api&lt;/h3&gt;
&lt;p&gt;我们在网关里面把filter都去掉了。已经不再传明文的username了。这个时候order-api也需要修改代码
因为oauth2的依赖。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;orderpai%E6%94%B9%E9%80%A0username.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;也需要响应的配置，令牌传递过来，它也需要从令牌里解析当前用户是谁。因为我们传的是一个令牌，而不是明文的一个东西了。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;orderapi%E8%8E%B7%E5%8F%96key.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;在启动类里面加注解@EnableResourceServer&lt;/p&gt;
&lt;p&gt;拿到用户名，通过@AuthenticationPrincipal注解&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;AuthenticationPrincipal%E8%8E%B7%E5%8F%96username.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;h3&gt;4、启动测试&lt;/h3&gt;
&lt;h4&gt;（1）启动网关&lt;/h4&gt;
&lt;p&gt;在没有启动认证服务器的时候，直接启动网关 会报错。链接被拒绝，&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;%E7%BD%91%E5%85%B3%E5%90%AF%E5%8A%A8%E8%BF%9E%E6%8E%A5%E8%A2%AB%E6%8B%92%E7%BB%9D.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;它在启动的时候会去找认证服务器，拿签名的key。认证服务器如果还没有启动，连不上，所以会报错。你拿不到这个key 就没法验jwt&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;%E6%8B%BF%E7%AD%BE%E5%90%8D%E7%9A%84key%E5%A4%B1%E8%B4%A5.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;h4&gt;(2) 所以首先启动认证服务器，OAuth2的认证服务器。&lt;/h4&gt;
&lt;p&gt;然后再启动网关&lt;/p&gt;
&lt;h4&gt;(3) 再次启动网关的时候又报了个404的错误。就是刚才那个拿key的路径没有。&lt;/h4&gt;
&lt;p&gt;&lt;img src=&quot;%E8%8E%B7%E5%8F%96key%E7%9A%84%E8%B7%AF%E5%BE%84404.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;这是因为我们在配置Oauth2认证服务器的时候，改tokenStore的时候，&lt;strong&gt;声明的是私有的JwtAccessTokenConverter&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;%E7%A7%81%E6%9C%89%E7%9A%84jwtTokenConverter.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;这个类要声明称公有的，而且还要是Spring的一个Bean。只有在有这个Bean的情况下&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;%E5%85%AC%E6%9C%89%E7%9A%84tokenConvert.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;tokenKeyEndpoint这个端点才会起作用。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://tangtangit.com/_astro/tokenEndPoint.CXnXgjYx_Z1jr1Ow.webp&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;这个请求才会被处理&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;%E5%A4%84%E7%90%86tokenkey.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;如果没有这个public的Bean。这个端点是不会暴露的，所以就会报404 的错误。&lt;/p&gt;
&lt;h4&gt;(4) 重启认证服务器&lt;/h4&gt;
&lt;p&gt;网关重启  网关就能成功的启动了。&lt;/p&gt;
&lt;p&gt;​    启动orderapi&lt;/p&gt;
&lt;h4&gt;（5）首先来获取令牌&lt;/h4&gt;
&lt;ul&gt;
&lt;li&gt;通过网关访问认证服务器，获取token.&lt;/li&gt;
&lt;li&gt;通过网关调用创建订单的服务&lt;/li&gt;
&lt;/ul&gt;
&lt;h4&gt;(6)  403错误。&lt;/h4&gt;
&lt;p&gt;无效的token，不包含resource id oauth2-resouce 这说明我们的resouceid的检查没过。&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;报错的oauth2-server是哪里的呢？？？我们在配置网关的时候，没有指定resource_id&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;默认情况下没有指定resourceId的情况，这个类里面会有一个默认的resourceId.&lt;strong&gt;进入到ResourceServerSerurityConfigurer&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;因为网关没有配置，所以默认的就是这个类里的resourceId，oauth2-resource&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;%E9%BB%98%E8%AE%A4%E7%9A%84resourceId.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;我们在发出去的令牌只能访问order-server&lt;/p&gt;
&lt;p&gt;所以这个令牌在访问网关的时候，就被拦下来了。令牌是对的。但是没有权限。&lt;/p&gt;
&lt;p&gt;令牌随便加个字母访问，错误的令牌就是401&lt;/p&gt;
&lt;h4&gt;(7)  解决方法&lt;/h4&gt;
&lt;ul&gt;
&lt;li&gt;在网关设置resourceid&lt;/li&gt;
&lt;li&gt;然后把网关的resourceId配置成gateway&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;/**
 * &amp;lt;p&amp;gt;DESC: 网关资源服务器配置&amp;lt;/p&amp;gt;
 * &amp;lt;p&amp;gt;DATE: 2019-12-16 16:43&amp;lt;/p&amp;gt;
 * &amp;lt;p&amp;gt;VERSION:2.0.0&amp;lt;/p&amp;gt;
 * &amp;lt;p&amp;gt;@AUTHOR: ZhengYong&amp;lt;/p&amp;gt;
 */
@Configuration
@EnableResourceServer
public class GatewaySecurityConfig extends ResourceServerConfigurerAdapter {

    @Override
    public void configure(HttpSecurity http) throws Exception {
        http.authorizeRequests()
                .antMatchers(&quot;/api/token/**&quot;).permitAll()
                .anyRequest().authenticated();
    }

    @Override
    public void configure(ResourceServerSecurityConfigurer resources) throws Exception {
        resources.resourceId(&quot;resourceid&quot;);
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;获取在数据库的客户端表中，将resourceid置为空，这样就可以访问任何resource了。&lt;/li&gt;
&lt;/ul&gt;
&lt;h4&gt;（8）重新测试&lt;/h4&gt;
&lt;p&gt;重新申请令牌。新令牌就是可以访问任何的资源服务器。&lt;/p&gt;
&lt;p&gt;创建订单的控制台，把用户名打印出来。&lt;/p&gt;
&lt;p&gt;发了jwt后，在网关的时候，没有去校验令牌，在网关解析了jwt，判断这个jwt是有效的，再往下，jwt给到订单服务的时候，订单服务从jwt解析出来这个令牌对应的用户是jojo&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;%E7%BD%91%E5%85%B3%E8%87%AA%E8%A1%8C%E6%A0%A1%E9%AA%8CjwtToken.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;h3&gt;5、令牌如何传递&lt;/h3&gt;
&lt;blockquote&gt;
&lt;p&gt;订单服务想去调库存服务的时候，令牌如何往下传。仍然要从请求里面拿到令牌，放到请求头里。但是spring security已经帮我封装了。不用自己去做。只需要用他的一个工具类就可以了。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h4&gt;1)、价格服务&lt;/h4&gt;
&lt;p&gt;价格服务也加上oauth2的依赖&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;        &amp;lt;dependency&amp;gt;
            &amp;lt;groupId&amp;gt;org.springframework.cloud&amp;lt;/groupId&amp;gt;
            &amp;lt;artifactId&amp;gt;spring-cloud-starter-oauth2&amp;lt;/artifactId&amp;gt;
        &amp;lt;/dependency&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;加上配置，配置从哪里拿tokenKey&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;security:
  oauth2:
    client:
      client-id: client
      client-secret: secret
    resource:
      jwt:
        key-uri: http://localhost:10000/oauth/token_key
&lt;/code&gt;&lt;/pre&gt;
&lt;h4&gt;2)、修改orderController&lt;/h4&gt;
&lt;p&gt;在请求上把令牌带上。不再用原来的restTemplate了 ，而是用SpringSecurity提供的OauthRestTemplate&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;OauthRestTemplate功能就是从你当前请求的上下文里&lt;/strong&gt;，拿到令牌，然后把令牌放到请求头里面。然后再出去。&lt;strong&gt;只要用OauthRestTemplate来发请求就会自动带上请求头。&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;在springboot的启动类声明oauth2RestTemplate&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;/**
 * &amp;lt;p&amp;gt;DESC: 启动类&amp;lt;/p&amp;gt;
 * &amp;lt;p&amp;gt;DATE: 2019-12-05 12:54&amp;lt;/p&amp;gt;
 * &amp;lt;p&amp;gt;VERSION:2.0.0&amp;lt;/p&amp;gt;
 * &amp;lt;p&amp;gt;@AUTHOR: ZhengYong&amp;lt;/p&amp;gt;
 */
@EnableDiscoveryClient
@SpringBootApplication
@EnableResourceServer
public class OrderApiApplication {
    @Bean
    public OAuth2RestTemplate oAuth2RestTemplate(OAuth2ProtectedResourceDetails resource, OAuth2ClientContext context){
        return new OAuth2RestTemplate(resource);
    }
    public static void main(String[] args) {
        SpringApplication.run(OrderApiApplication.class,args);
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;改造controller&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;@Slf4j
@RestController
@RequestMapping(&quot;/orders&quot;)
public class OrderController {

    @Resource
    private OAuth2RestTemplate oAuth2RestTemplate;

    @PostMapping
    public OrderInfo create(@RequestBody OrderInfo info,
                            @AuthenticationPrincipal String username,
                            //@AuthenticationPrincipal(expression = &quot;#this.id&quot;) Long id,
                            //@AuthenticationPrincipal TbUser tbUser,
                            //@RequestHeader String username,
                            HttpServletRequest srequest) throws IOException {
        log.info(&quot;【用户名】={}&quot;,username);
        //log.info(&quot;【用户】={}&quot;,tbUser);
        //log.info(&quot;【用户id】={}&quot;,id);

        //Object forObject = oAuth2RestTemplate.getForObject(&quot;http://localhost:10086/price/&quot; + info.getId(), Object.class);
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;网关权限控制改造&lt;/h2&gt;
&lt;h3&gt;(1)、控制权限&lt;/h3&gt;
&lt;p&gt;角色和权限实时变化怎么去做授权？
在网关上做复杂的权限控制。
我们之前在网关上有这么个配置，除了token的请求，都需要做身份认证。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;    @Override
    public void configure(HttpSecurity http) throws Exception {
        http.authorizeRequests()
                .antMatchers(&quot;/api/token/**&quot;).permitAll()
                .anyRequest().authenticated();
    }
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这里修改成access ，你给它指定一个访问的规则。
request就是当前的请求，authentication就是当前的用户。
调用一个服务的hasPerission方法，把当前请求和当前用户传进去。这个方法返回一个bool，true或false。如果是true你就能访问，false 就不能访问。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;    @Override
    public void configure(HttpSecurity http) throws Exception {
        http.authorizeRequests()
                .antMatchers(&quot;/api/token/**&quot;).permitAll()
                .anyRequest().access(&quot;#permissionService.hasPermission(request,authentication)&quot;);
    }
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;permissionService从哪里来？这个就要自己去写了。里面只有一个方法就是hasPermission。authentication里面就包含了用户的信息。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;/**
 * &amp;lt;p&amp;gt;DESC: 权限校验&amp;lt;/p&amp;gt;
 * &amp;lt;p&amp;gt;DATE: 2019-12-17 12:48&amp;lt;/p&amp;gt;
 * &amp;lt;p&amp;gt;VERSION:2.0.0&amp;lt;/p&amp;gt;
 * &amp;lt;p&amp;gt;@AUTHOR: ZhengYong&amp;lt;/p&amp;gt;
 */
public interface PermissionService {

    boolean hasPermission(HttpServletRequest request, Authentication authentication);
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;实现类。&lt;/p&gt;
&lt;p&gt;在这里调用你的远程服务，或者查询数据库，或者查redis，或者网关启动的时候，已经把权限信息缓存到内存里了。&lt;/p&gt;
&lt;p&gt;这里应该是真正调用业务逻辑的服务来判断，这里我们就不调用服务了，打印一些信息。
50%的几率有权限访问，50%的几率没权限访问，&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;/**
 * &amp;lt;p&amp;gt;DESC: 权限校验实现&amp;lt;/p&amp;gt;
 * &amp;lt;p&amp;gt;DATE: 2019-12-17 12:49&amp;lt;/p&amp;gt;
 * &amp;lt;p&amp;gt;VERSION:2.0.0&amp;lt;/p&amp;gt;
 * &amp;lt;p&amp;gt;@AUTHOR: ZhengYong&amp;lt;/p&amp;gt;
 */
@Slf4j
@Service
public class PermissionServiceImpl implements PermissionService {

    @Override
    public boolean hasPermission(HttpServletRequest request, Authentication authentication) {
        log.info(&quot;【请求地址】 = {}&quot;,request.getRequestURI());
        log.info(&quot;【authentication】 = {}&quot;,authentication.toString());
        return RandomUtils.nextInt() % 2 == 0;
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这个时候psermissonService实际上是不起作用的。现在网关不认这个permissionService，不知道这个permissionService到底是什么&lt;/p&gt;
&lt;p&gt;如果此时重启网关，并访问接口，会出现：解析表达式失败了。不知道该怎么解析。&lt;/p&gt;
&lt;p&gt;现在要告诉它怎么解析这个psermissionService，让他知道permissionService技术这个东西&lt;/p&gt;
&lt;h3&gt;(2)、写一个表达式的处理器&lt;/h3&gt;
&lt;p&gt;继承的是Oauth2的web安全表达式处理器。在这里处理器里是不知道我自己写的服务的&lt;/p&gt;
&lt;p&gt;Evaluation&lt;/p&gt;
&lt;p&gt;创建一个评估的上下文来告诉它 permissionService怎么解析。
首先执行以下super的方法，创建一个标准的评估上下文。&lt;/p&gt;
&lt;p&gt;在这个标准的评估上下文上，我们去设置一个变量。&lt;/p&gt;
&lt;p&gt;有了这个表达式处理器，在表达式里面写psermissionService这个字符串的时候，她就知道我应该是去调用这个注入进来的psermissionService这个类里面的方法了。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;/**
 * &amp;lt;p&amp;gt;DESC: 网关表达式处理器&amp;lt;/p&amp;gt;
 * &amp;lt;p&amp;gt;DATE: 2019-12-17 12:55&amp;lt;/p&amp;gt;
 * &amp;lt;p&amp;gt;VERSION:2.0.0&amp;lt;/p&amp;gt;
 * &amp;lt;p&amp;gt;@AUTHOR: ZhengYong&amp;lt;/p&amp;gt;
 */
@Component
public class GatewayWebSecurityExpressionHandler extends OAuth2WebSecurityExpressionHandler {

    @Resource
    private PermissionService permissionService;

    @Override
    protected StandardEvaluationContext createEvaluationContextInternal(Authentication authentication, FilterInvocation invocation) {
        StandardEvaluationContext context = super.createEvaluationContextInternal(authentication, invocation);
        context.setVariable(&quot;permissionService&quot;,permissionService);
        return context;
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;注入我们自己写的表达式处理器。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;/**
 * &amp;lt;p&amp;gt;DESC: 网关资源服务器配置&amp;lt;/p&amp;gt;
 * &amp;lt;p&amp;gt;DATE: 2019-12-16 16:43&amp;lt;/p&amp;gt;
 * &amp;lt;p&amp;gt;VERSION:2.0.0&amp;lt;/p&amp;gt;
 * &amp;lt;p&amp;gt;@AUTHOR: ZhengYong&amp;lt;/p&amp;gt;
 */
@Configuration
@EnableResourceServer
public class GatewaySecurityConfig extends ResourceServerConfigurerAdapter {

    @Resource
    private GatewayWebSecurityExpressionHandler gatewayWebSecurityExpressionHandler;

    @Override
    public void configure(HttpSecurity http) throws Exception {
        http.authorizeRequests()
                .antMatchers(&quot;/api/token/**&quot;).permitAll()
                .anyRequest().access(&quot;#permissionService.hasPermission(request,authentication)&quot;);
    }

    @Override
    public void configure(ResourceServerSecurityConfigurer resources) throws Exception {
        resources.resourceId(&quot;resourceid&quot;)
          //注入我们自己写的表达式处理器
          .expressionHandler(gatewayWebSecurityExpressionHandler);
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;(3)、重启服务测试&lt;/h3&gt;
&lt;h2&gt;整合feign&lt;/h2&gt;
&lt;h3&gt;1、改造订单服务&lt;/h3&gt;
&lt;p&gt;加入feign依赖&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;        &amp;lt;dependency&amp;gt;
            &amp;lt;groupId&amp;gt;org.springframework.cloud&amp;lt;/groupId&amp;gt;
            &amp;lt;artifactId&amp;gt;spring-cloud-starter-openfeign&amp;lt;/artifactId&amp;gt;
        &amp;lt;/dependency&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;2、开启feign功能&lt;/h3&gt;
&lt;p&gt;@EnableFeignClients&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;还可以指定feign接口位置&lt;/p&gt;
&lt;/blockquote&gt;
&lt;blockquote&gt;
&lt;pre&gt;&lt;code&gt;@EnableFeignClients({&quot;com.dftcmedia.tckk.microservice.common.feginapis&quot;,&quot;com.dftcmedia.tckk.vservice2.logiccommon.feign&quot;})
&lt;/code&gt;&lt;/pre&gt;
&lt;/blockquote&gt;
&lt;pre&gt;&lt;code&gt;/**
 * &amp;lt;p&amp;gt;DESC: 启动类&amp;lt;/p&amp;gt;
 * &amp;lt;p&amp;gt;DATE: 2019-12-05 12:54&amp;lt;/p&amp;gt;
 * &amp;lt;p&amp;gt;VERSION:2.0.0&amp;lt;/p&amp;gt;
 * &amp;lt;p&amp;gt;@AUTHOR: ZhengYong&amp;lt;/p&amp;gt;
 */
@EnableFeignClients
@EnableDiscoveryClient
@SpringBootApplication
@EnableResourceServer
public class OrderApiApplication {
    //@Bean
    //public OAuth2RestTemplate oAuth2RestTemplate(OAuth2ProtectedResourceDetails resource, OAuth2ClientContext context){
    //    return new OAuth2RestTemplate(resource);
    //}
    public static void main(String[] args) {
        SpringApplication.run(OrderApiApplication.class,args);
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;3、写price的消费接口&lt;/h3&gt;
&lt;p&gt;price : 对应的是服务的名字，跟网关上路由的服务名一样&lt;/p&gt;
&lt;p&gt;configuration： 配置feign的拦截器&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;/**
 * &amp;lt;p&amp;gt;DESC: &amp;lt;/p&amp;gt;
 * &amp;lt;p&amp;gt;DATE: 2019-12-17 14:03&amp;lt;/p&amp;gt;
 * &amp;lt;p&amp;gt;VERSION:2.0.0&amp;lt;/p&amp;gt;
 * &amp;lt;p&amp;gt;@AUTHOR: ZhengYong&amp;lt;/p&amp;gt;
 */
@FeignClient(value = &quot;price&quot;,configuration = {FeignConfig.class})
public interface PriceFeign {

    @GetMapping(&quot;/price/{id}&quot;)
    Object getPrice(@PathVariable(&quot;id&quot;) String id);

}
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;4、传递token&lt;/h3&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;em&gt;在进行认证鉴权的时候，不管是jwt不是security,当使用Feign时就会发现外部请求到A服务的时候，A服务是可以拿到token的，然而当服务使用Feign调用B服务时，token就会丢失，从而认证失败&lt;/em&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;通过实现&lt;code&gt;RequestInterceptor&lt;/code&gt;拦截器，在调用Feign服务的时候，在请求头中添加需要传递的token&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;@Configuration
public class FeignConfig implements RequestInterceptor {
    @Override
    public void apply(RequestTemplate requestTemplate) {
        ServletRequestAttributes attributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
        HttpServletRequest request = attributes.getRequest();
        requestTemplate.header(HttpHeaders.AUTHORIZATION, request.getHeader(HttpHeaders.AUTHORIZATION));
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;5、测试&lt;/h3&gt;
&lt;h2&gt;获取刷新token&lt;/h2&gt;
&lt;h3&gt;1、发送请求&lt;/h3&gt;
&lt;p&gt;&lt;img src=&quot;%E8%8E%B7%E5%8F%96%E5%88%B7%E6%96%B0token.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;h3&gt;2、认证服务器报错&lt;/h3&gt;
&lt;h4&gt;抛出异常Handling error: IllegalStateException, UserDetailsService is required.&lt;/h4&gt;
&lt;h3&gt;3、解决方法：&lt;/h3&gt;
&lt;p&gt;认证服务器中，在AuthorizationServerConfigurerAdapter 中配置UserDetailsService对象&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;@Configuration
@EnableAuthorizationServer
public class OAuth2AuthorizationServer extends AuthorizationServerConfigurerAdapter {

    @Autowired
    private MyUserDetailsService myUserDetailsService;

    @Override
    public void configure(AuthorizationServerEndpointsConfigurer endpoints) throws Exception {
        // 执行token刷新需要带上此参数
        endpoints.userDetailsService(myUserDetailsService);
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;jwt改造之日志及错误处理（1）&lt;/h2&gt;
&lt;p&gt;在代码里，我们没有认证或者授权的filter。认证和授权的工作现在基本上完全由Spring Security的过滤器接管了。&lt;/p&gt;
&lt;p&gt;本节就来看下 如何在Spring Security的过滤器链上加入我们自己的逻辑，因为现在这个过滤器链上只处理了认证和授权。我们还有其他的一些安全机制，比如说限流、日志。我们看下怎么把这些机制加到Spring的默认实现里面去，最后总结一下，到底都做了哪些事情，然后整个它的处理流程是什么样子的&lt;/p&gt;
&lt;h3&gt;1、日志过滤器&lt;/h3&gt;
&lt;p&gt;首先来写处理日志的过滤器。和我们之前的处理是类似的&lt;/p&gt;
&lt;p&gt;继承OncePerRequestFilter&lt;/p&gt;
&lt;p&gt;这里一定注意不要用@Component注解把这个Filter声明成Spring 的Bean原因下面再讲。&lt;/p&gt;
&lt;p&gt;日志的这个过滤器应该是在认证的过滤器前面，在授权的过滤器前面。所以在这个过滤器里面，我应该知道当前用户是谁了。如果你的认证成功的话。
前面认证的过滤器会把jwt的令牌转换成一个Authentication，然后把它放到SpringSecurityContext安全的上下文里面。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;/**
 * &amp;lt;p&amp;gt;DESC: 网关日志过滤器&amp;lt;/p&amp;gt;
 * &amp;lt;p&amp;gt;DATE: 2019-12-17 17:57&amp;lt;/p&amp;gt;
 * &amp;lt;p&amp;gt;VERSION:2.0.0&amp;lt;/p&amp;gt;
 * &amp;lt;p&amp;gt;@AUTHOR: ZhengYong&amp;lt;/p&amp;gt;
 */
public class GatewayAuditLogSecurityFilter extends OncePerRequestFilter {

    @Override
    protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {

    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;通过下面的代码就可以把它从安全上下文里面再拿出来。principal就是我们申请令牌的时候的用户名&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;    @Override
    protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
        //前面认证的过滤器会把jwt的令牌转换成一个Authentication，然后把它放到SpringSecurityContext安全的上下文里面。
        Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
        String username = (String) authentication.getPrincipal();
        log.info(&quot;【日志过滤器】 用户名={}&quot;,username);
    }
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;调用后面的过滤器处理完之后，还要再加一句日志更新，日志的成功还是失败，更新到数据库里面。这里我简单的用一个sysout来处理。把日志更新成处理成功。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;    @Override
    protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
        //前面认证的过滤器会把jwt的令牌转换成一个Authentication，然后把它放到SpringSecurityContext安全的上下文里面。
        Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
        String username = (String) authentication.getPrincipal();
        log.info(&quot;【日志过滤器】 用户名={}&quot;,username);

        log.info(&quot;【日志过滤器】 1。 add log for ={}&quot;,username);

        //调用下面的过滤器
        filterChain.doFilter(request,response);
        log.info(&quot;【日志过滤器】 3。 update log for success&quot;);
        
    }
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;2、过滤器加到SpringSecurity配置中&lt;/h3&gt;
&lt;p&gt;http的安全配置这里加一行。第一个addFilter一般不会去用.
addFilterAfter、addFilterAt、addFilterBefore 就是你自己写的过滤器加到SpringSecurity的过滤器链上，指定一个位置。SpringSecurity过滤器链的顺序是固定的。所以你只要把你的过滤器加到spring的某一个过滤器的前面before或 后面after，at就是直接加入到这个过滤器的位置上。替换掉原来的过滤器。&lt;/p&gt;
&lt;p&gt;我们加的是一个日志过滤器，要加载授权的前面。用addFilterBefore&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;/**
 * &amp;lt;p&amp;gt;DESC: 网关资源服务器配置&amp;lt;/p&amp;gt;
 * &amp;lt;p&amp;gt;DATE: 2019-12-16 16:43&amp;lt;/p&amp;gt;
 * &amp;lt;p&amp;gt;VERSION:2.0.0&amp;lt;/p&amp;gt;
 * &amp;lt;p&amp;gt;@AUTHOR: ZhengYong&amp;lt;/p&amp;gt;
 */
@Configuration
@EnableResourceServer
public class GatewaySecurityConfig extends ResourceServerConfigurerAdapter {

    @Resource
    private GatewayWebSecurityExpressionHandler gatewayWebSecurityExpressionHandler;

    @Override
    public void configure(HttpSecurity http) throws Exception {
        http
                .addFilterBefore(new GatewayAuditLogSecurityFilter(), ExceptionTranslationFilter.class)
                .authorizeRequests()
                .antMatchers(&quot;/api/token/**&quot;).permitAll()
                .anyRequest().access(&quot;#permissionService.hasPermission(request,authentication)&quot;);
    }
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;前面是自己定义的过滤器，后面那个过滤器ExceptionTranslationFilter是Spring 用来转换异常的一个过滤器。我们最后会在授权的过滤器里面抛出异常。要不然是401需要身份认证，要不然是403需要权限。就这两种异常。两个异常抛出来以后，都会由这个过滤器来处理&lt;/p&gt;
&lt;p&gt;刚才我们说&lt;strong&gt;不要把自己定义的GatewayAuditLogFilter声明称一个spring的Bean&lt;/strong&gt;。SprintBoot默认情况下(如果声明成了Spring的Bean) ，会把这个过滤器直接加到web程序的过滤器链上。因为这个类继承了OncePreRequestFilter
后面这里还有一个addFilter的操作。也就是说SpringBoot加了一次，自己这里写代码又加了一次。这个过滤器加入到过滤器的链上，加了两次。所以我们这里不声明称Spring成的Bean。这里只用代码加到链上一次。&lt;/p&gt;
&lt;h3&gt;3、启动测试&lt;/h3&gt;
&lt;p&gt;申请令牌&lt;/p&gt;
&lt;p&gt;复制令牌&lt;/p&gt;
&lt;p&gt;去调用创建订单的服务。&lt;/p&gt;
&lt;p&gt;看一下 网关上的日志。说明进入了日志的过滤器里面。jojo是jwt内解析的用户名， 这说明日志是在认证的过滤器后面的。所以才会解析出当前的用户。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;【日志过滤器】 用户名=admin
【日志过滤器】 1。 add log for =admin
【请求地址】 = /api/order/orders
【日志过滤器】 3。 update log for success
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;第二句是在验证权限的服务，这里打印出来的。这就说明了日志过滤器在认证的过滤器之后，权限的过滤器之前生效的&lt;/p&gt;
&lt;p&gt;在权限的过滤器判断完成后，最终还会回到日志的过滤器里面，把它的处理结果更新成处理成功。这个是我们想要的执行的顺序&lt;/p&gt;
&lt;h3&gt;4、异常处理&lt;/h3&gt;
&lt;p&gt;多发几次创建订单的请求，因为是50%的成功率&lt;/p&gt;
&lt;p&gt;403访问被拒绝&lt;/p&gt;
&lt;p&gt;后台日志：&lt;/p&gt;
&lt;p&gt;​        这里并没有体现出来，请求是被拒绝掉的、现在的处理代码并没有处理这种情况，仍然是回到了日志Filter里面，把我的日志更新成功了。&lt;/p&gt;
&lt;p&gt;这个就是访问被拒绝 ，它的处理器&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;%E8%AE%BF%E9%97%AE%E6%8B%92%E7%BB%9D%E5%A4%84%E7%90%86%E5%99%A8.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;默认的处理器就是返回下面这样的json&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&amp;lt;ForbiddenException&amp;gt;
    &amp;lt;error&amp;gt;access_denied&amp;lt;/error&amp;gt;
    &amp;lt;error_description&amp;gt;Access is denied&amp;lt;/error_description&amp;gt;
&amp;lt;/ForbiddenException&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;我们可以自己定义自己的访问拒绝的处理器。在这个处理器里面我们可以记录日志，也可以自定义返回去的这个错误。这里只是针对没有权限403这种情况的一个处理。
还有另外一种情况401，一会再说。
我们要写的就是AccessDeniedHandler这个接口的实现。&lt;/p&gt;
&lt;h3&gt;4.1、AccessDeniedHandler实现&lt;/h3&gt;
&lt;blockquote&gt;
&lt;p&gt;自定义错误处理handler&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;直接继承一个父类。&lt;strong&gt;OAuth2AccessDeniedHandler&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;OAuth2AccessDeniedHandler是个默认的实现，如果不在网关的这里配置accessDeniedHandler这个配置的话。&lt;/p&gt;
&lt;p&gt;默认用的就是这个 OAuth2AccessDeniedHandler。在这里处理器里面，它会把异常 转换成一个简单的json也就是我们看到页面返回的403的json&lt;/p&gt;
&lt;p&gt;声明称Spring的Bean。然后覆盖handler方法。&lt;/p&gt;
&lt;p&gt;在这里能拿到request、response、抛出的exception。&lt;/p&gt;
&lt;p&gt;可以通过操作response,写你想写的错误信息。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;/**
 * &amp;lt;p&amp;gt;DESC: 网关访问拒绝处理器&amp;lt;/p&amp;gt;
 * &amp;lt;p&amp;gt;DATE: 2019-12-18 14:02&amp;lt;/p&amp;gt;
 * &amp;lt;p&amp;gt;VERSION:2.0.0&amp;lt;/p&amp;gt;
 * &amp;lt;p&amp;gt;@AUTHOR: ZhengYong&amp;lt;/p&amp;gt;
 */
@Slf4j
@Component
public class GatewayAccessDeniedHandler extends OAuth2AccessDeniedHandler {

    @Override
    public void handle(HttpServletRequest request, HttpServletResponse response, AccessDeniedException authException) throws IOException, ServletException {
        log.info(&quot;【访问拒绝过滤器】update log to 403&quot;);
        super.handle(request, response, authException);
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;注入我们自己写的错误处理器&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;    @Override
    public void configure(ResourceServerSecurityConfigurer resources) throws Exception {
        resources
                .accessDeniedHandler(gatewayAccessDeniedHandler)
                .resourceId(&quot;resourceid&quot;)
                .expressionHandler(gatewayWebSecurityExpressionHandler);
    }
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;4.2、重启网关服务。测试&lt;/h3&gt;
&lt;p&gt;日志的过滤器里面又update了一次。更新为成功了。我们想要的是 如果报错了 就不再更新日志为成功&lt;/p&gt;
&lt;p&gt;最简单的做法是在request里面设置一个attributes。value值随便定义。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;    @Override
    public void handle(HttpServletRequest request, HttpServletResponse response, AccessDeniedException authException) throws IOException, ServletException {
        log.info(&quot;【访问拒绝过滤器】update log to 403&quot;);
        request.setAttribute(&quot;updated&quot;, true);
        super.handle(request, response, authException);
    }
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;        if(request.getAttribute(&quot;updated&quot;) == null){
            log.info(&quot;【日志过滤器】 3。 update log for success&quot;);
        }
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;第一次请求失败了 打印出了403，第二次更新成功&lt;/p&gt;
&lt;p&gt;现在只处理了403这种情况，还有一种情况就是401，就是当前用户需要做身份认证，你没有做身份认证。&lt;/p&gt;
&lt;h2&gt;jwt改造之日志及错误处理（2）&lt;/h2&gt;
&lt;h3&gt;1、401的处理&lt;/h3&gt;
&lt;p&gt;与403类似，也是在这里配置。EntryPoint入口点。&lt;/p&gt;
&lt;p&gt;这个方法里面，我们要实现的接口是AuthenticationEntryPoint&lt;/p&gt;
&lt;p&gt;不直接实现这个接口，而是继承一个父类。OAuth2AuthenticationEntryPoint&lt;/p&gt;
&lt;p&gt;上面如果不配置。那么默认的实现就是OAuth2AuthenticationEntryPoint。它的默认实现就是返回一个401的错误码。然后返回一个和403类似的json&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;%E9%85%8D%E7%BD%AEentryPoint.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;令牌随便加个1&lt;/p&gt;
&lt;p&gt;这样就抛出401 的错误。&lt;/p&gt;
&lt;p&gt;401的这段信息就是OAuth2AuthenticationEntryPoint来处理的&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&amp;lt;InvalidTokenException&amp;gt;
    &amp;lt;error&amp;gt;invalid_token&amp;lt;/error&amp;gt;
    &amp;lt;error_description&amp;gt;Cannot convert access token to JSON&amp;lt;/error_description&amp;gt;
&amp;lt;/InvalidTokenException&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;2、EntryPoint&lt;/h3&gt;
&lt;p&gt;加上@Component声明称Spring的Bean&lt;/p&gt;
&lt;p&gt;覆盖的方法commence&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;/**
 * &amp;lt;p&amp;gt;DESC: 网关配置401处理器&amp;lt;/p&amp;gt;
 * &amp;lt;p&amp;gt;DATE: 2019-12-18 14:36&amp;lt;/p&amp;gt;
 * &amp;lt;p&amp;gt;VERSION:2.0.0&amp;lt;/p&amp;gt;
 * &amp;lt;p&amp;gt;@AUTHOR: ZhengYong&amp;lt;/p&amp;gt;
 */
@Slf4j
@Component
public class GatewayAuthenticationEntryPoint extends OAuth2AuthenticationEntryPoint {

    @Override
    public void commence(HttpServletRequest request, HttpServletResponse response, AuthenticationException authException) throws IOException, ServletException {
        log.info(&quot;【入口点】2. update log to 401&quot;);
        request.setAttribute(&quot;updated&quot;, true);
        super.commence(request, response, authException);
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;注入到配置类&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;    @Resource
    private GatewayAuthenticationEntryPoint authenticationEntryPoint;

    @Override
    public void configure(HttpSecurity http) throws Exception {
        http
                .addFilterBefore(new GatewayAuditLogSecurityFilter(), ExceptionTranslationFilter.class)
                .authorizeRequests()
                .antMatchers(&quot;/api/token/**&quot;).permitAll()
                .anyRequest().access(&quot;#permissionService.hasPermission(request,authentication)&quot;);
    }

    @Override
    public void configure(ResourceServerSecurityConfigurer resources) throws Exception {
        resources
                .authenticationEntryPoint(authenticationEntryPoint)
                .accessDeniedHandler(gatewayAccessDeniedHandler)
                .resourceId(&quot;resourceid&quot;)
                .expressionHandler(gatewayWebSecurityExpressionHandler);
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;3、测试&lt;/h3&gt;
&lt;p&gt;首先是发过来的令牌是有问题的，在令牌的前面加了一个1&lt;/p&gt;
&lt;p&gt;返回的结果就是401&lt;/p&gt;
&lt;p&gt;我们在后台日志里可以看到 ，只有一行日志&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;【入口点】2. update log to 401
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这说明我的请求进入到认证的过滤器以后，因为令牌有问题所以  &lt;strong&gt;认证&lt;/strong&gt;的 那个过滤器 直接抛了异常。
异常抛给了AuthenticationEntryPoint。它来处理，就输出了一行日志。然后整个处理就结束掉了。然后返回了错误信息。&lt;/p&gt;
&lt;p&gt;在这种情况下，实际上你的请求就没有经过日志的Filter，也就是这个GatewayAuditLogFilter。而是直接从&lt;strong&gt;认证 的过滤器&lt;/strong&gt;，直接抛异常。然后就走掉了。 &lt;strong&gt;这是一种情况。&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;另外一种情况&lt;/strong&gt;：不传令牌，直接访问&lt;/p&gt;
&lt;p&gt;多发几次请求。多点击几次请求的按钮。返回的永远是401&lt;/p&gt;
&lt;p&gt;后台的日志里，出现了两种情况&lt;/p&gt;
&lt;p&gt;anonymousUser是一个匿名的用户。因为没有传任何的令牌，所以实际上是一个匿名的用户，因为没传令牌，认证的过滤器默认建了一个匿名的用户身份放在他的authentication里面了。仍然往下走，走过了日志的过滤器。也走到了权限的判断，但是用的身份是匿名用户的身份。50%的概率，这里判断成功了。 请求放过去就执行了update log to success&lt;/p&gt;
&lt;p&gt;那么401是哪里来的呢，是订单的服务，是需要身份认证的，返回了401，是在网关阶段判断出了没有权限。于是返回了401&lt;/p&gt;
&lt;p&gt;两种情况：&lt;/p&gt;
&lt;p&gt;1.请求匿名的被放过去，后面的order的服务返回401.&lt;/p&gt;
&lt;p&gt;2。是网关这里权限没过，返回了401&lt;/p&gt;
&lt;p&gt;1.令牌有问题，2。没传令牌。权限之类被拦住&lt;/p&gt;
&lt;h3&gt;4、分别处理两种情况&lt;/h3&gt;
&lt;p&gt;最开始是这么配置的，一定要身份认证通过后，才能访问我们的这些请求&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;    @Override
    public void configure(HttpSecurity http) throws Exception {
        http
                .addFilterBefore(new GatewayAuditLogSecurityFilter(), ExceptionTranslationFilter.class)
                .authorizeRequests()
                .antMatchers(&quot;/api/token/**&quot;).permitAll()
                .anyRequest().access(&quot;#permissionService.hasPermission(request,authentication)&quot;);
    }
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;经过改造我们用了permissionService&lt;/p&gt;
&lt;p&gt;permissonService里面没有做是不是登陆了的判断。而是直接是一个随机数。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;首先我们要把当前用户是不是登陆的判断加上。判断传进来的authentication就可以了&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;如果令牌是正确有效的，那么这里的authentication是一个oauth2的authentication，如果没带令牌，就是匿名的用户的情况，那么这里的authentication是一个匿名的authentication。所以这里只需要判断authentication的类型就看可以了。&lt;/p&gt;
&lt;p&gt;所以这里判断如果当前的authentication是AnonymousAuthenticationToken匿名的 那么就抛出异常 必填的AccessToken的异常，&lt;/p&gt;
&lt;p&gt;它的参数是当前服务的detials，这里直接传空就可以了&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;@Slf4j
@Service
public class PermissionServiceImpl implements PermissionService {

    @Override
    public boolean hasPermission(HttpServletRequest request, Authentication authentication) {
        log.info(&quot;【请求地址】 = {}&quot;,request.getRequestURI());
        log.info(&quot;【authentication】 = {}&quot;,authentication.toString());
        if(authentication instanceof AnonymousAuthenticationToken){
            throw new AccessTokenRequiredException(null);
        }
        return RandomUtils.nextInt() % 2 == 0;
        //return true;
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这个EntryPoint是用来处理401错误的。讲了401有两种情况。
1.是传的令牌有问题，无法解析，就直接从认证的过滤器里面抛异常，抛异常服务就断了，不往下走 了。&lt;/p&gt;
&lt;p&gt;2.是没传令牌.以为anoymous的身份往下走。最后到PermissionService里面抛出一个AccessTokenRequiredException&lt;/p&gt;
&lt;p&gt;所以这里的authException有两种情况。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;authException%E5%BC%82%E5%B8%B8.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;一种是这里抛出的AccessTokenRequiredException。如果是这个exception，我认为你是没传令牌 ，如果不是这个异常，我就认为你传的令牌有问题&lt;/p&gt;
&lt;p&gt;如果是这种异常，说明你没传令牌，而且你的请求是经过了日志服务器，日志服务器已经往数据库插入了一条记录了。所以这里就是更新log为401&lt;/p&gt;
&lt;p&gt;如果不是这个，说明你的令牌本身是有问题的，这个时候不会过日志的Filter。所以这里就是add一个401的log。令牌有问题。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;/**
 * &amp;lt;p&amp;gt;DESC: 网关配置401处理器&amp;lt;/p&amp;gt;
 * &amp;lt;p&amp;gt;DATE: 2019-12-18 14:36&amp;lt;/p&amp;gt;
 * &amp;lt;p&amp;gt;VERSION:2.0.0&amp;lt;/p&amp;gt;
 * &amp;lt;p&amp;gt;@AUTHOR: ZhengYong&amp;lt;/p&amp;gt;
 */
@Slf4j
@Component
public class GatewayAuthenticationEntryPoint extends OAuth2AuthenticationEntryPoint {

    @Override
    public void commence(HttpServletRequest request, HttpServletResponse response, AuthenticationException authException) throws IOException, ServletException {

        //匿名用户
        if(authException instanceof AccessTokenRequiredException){
            log.info(&quot;【entryPoint】 2。 update log for 401&quot;);
        }
        //token 自身有问题
        else{
            log.info(&quot;【entryPoint】 2。 add log for = {token无法解析} 401&quot;);
        }

        request.setAttribute(&quot;updated&quot;, true);

        super.commence(request, response, authException);
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;5、重启网关测试&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;错误令牌&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;【entryPoint】 2。 add log for = {token无法解析} 401
&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;无令牌&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;【日志过滤器】 用户名=anonymousUser
【日志过滤器】 1。 add log for =anonymousUser
【请求地址】 = /api/order/orders
【entryPoint】 2。 update log for 401
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;网关日志实现&lt;/h2&gt;
&lt;h3&gt;1、表&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;CREATE TABLE `dftc_sys_access_log` (
  `id` bigint(22) NOT NULL,
  `request_params` longtext,
  `request_ip` varchar(255) DEFAULT NULL COMMENT &apos;请求IP&apos;,
  `request_url` varchar(255) DEFAULT NULL COMMENT &apos;请求服务器的url资源&apos;,
  `request_method` varchar(20) DEFAULT NULL COMMENT &apos;请求方式&apos;,
  `agent_info` varchar(600) DEFAULT NULL COMMENT &apos; 请求的客户端代理信息&apos;,
  `response_code` smallint(6) DEFAULT NULL COMMENT &apos;返回的业务code&apos;,
  `response_msg` varchar(255) DEFAULT NULL COMMENT &apos;业务返回信息&apos;,
  `response_data` longtext COMMENT &apos;返回的业务数据&apos;,
  `consuming_time` bigint(22) DEFAULT NULL COMMENT &apos;本次请求消耗时间&apos;,
  `memo` varchar(255) DEFAULT NULL,
  `create_time` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP,
  `update_time` timestamp NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
  PRIMARY KEY (`id`) USING BTREE
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 ROW_FORMAT=DYNAMIC;
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;2、实体类&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;/**
 * &amp;lt;p&amp;gt;DESC:访问日志 &amp;lt;/p&amp;gt;
 * &amp;lt;p&amp;gt;DATE: 2019/2/12&amp;lt;/p&amp;gt;
 * &amp;lt;p&amp;gt;VERSION:1.0.0&amp;lt;/p&amp;gt;
 * &amp;lt;p&amp;gt;@AUTHOR: ZhengYong&amp;lt;/p&amp;gt;
 */
@Data
@TableName(&quot;dftc_sys_access_log&quot;)
public class AccessLogDO {
    public AccessLogDO(String requestParams, String requestIp, String requestUrl, String requestMethod, String agentInfo,LocalDateTime now) {
        if (StringUtils.isNotBlank(requestParams) &amp;amp;&amp;amp; requestParams.length() &amp;gt; 600) {
            this.requestParams = &quot;数据太长，未记录&quot;;
        }
        this.requestParams = requestParams;
        this.requestIp = requestIp;
        this.requestUrl = requestUrl;
        this.requestMethod = requestMethod;
        this.agentInfo = agentInfo;
        this.createTime = now;
    }

    private static final long serialVersionUID = -6031010158629417797L;

    private Long id;
    private LocalDateTime createTime;
    private LocalDateTime updateTime;
    /**
     * 请求参数
     */
    private String requestParams;

    private String userId;
    /**
     * 请求IP
     */
    private String requestIp;

    /**
     * 请求服务器的url资源
     */
    private String requestUrl;

    /**
     * 请求方式
     */
    private String requestMethod;

    /**
     * 请求的客户端代理信息
     */
    private String agentInfo;

    /**
     * 返回的业务code
     */
    private Integer responseCode;

    /**
     * 业务返回信息
     */
    private String responseMsg;

    /**
     * 返回的业务数据
     */
    private String responseData;

    /**
     * 本次请求消耗时间
     */
    private Long consumingTime;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;dao&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;/**
 * &amp;lt;p&amp;gt;DESC: 访问日志DAO&amp;lt;/p&amp;gt;
 * &amp;lt;p&amp;gt;DATE: 2019-12-18 15:51&amp;lt;/p&amp;gt;
 * &amp;lt;p&amp;gt;VERSION:2.0.0&amp;lt;/p&amp;gt;
 * &amp;lt;p&amp;gt;@AUTHOR: ZhengYong&amp;lt;/p&amp;gt;
 */
@Repository
@Mapper
public interface AccessLogDao extends BaseMapper&amp;lt;AccessLogDO&amp;gt; {
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;3、具体新增和修改业务&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;    /**
     * 新增日志
     * @param request  请求
     * @return AccessLogDO
     */
    private AccessLogDO insertLog(InputStreamHttpServletRequestWrapper request) throws IOException {
        String params = HttpUtil.getBodyByBufferReader(request);
        String ipAddr = HttpUtil.getIpAddr(request);
        String requestUrl = request.getRequestURI();
        String method = request.getMethod();
        String agent = request.getHeader(HttpHeaders.USER_AGENT);
        AccessLogDO accessLogDO = new AccessLogDO(params, ipAddr, requestUrl, method, agent,LocalDateTime.now());
        accessLogDao.insert(accessLogDO);
        //加入到请求域中
        request.setAttribute(&quot;accessLog&quot;,accessLogDO);
        return accessLogDO;
    }

    /**
     * 修改日志
     * @param request  请求
     * @param response 返回
     * @param body     返回体
     */
    private void updateLog(HttpServletRequest request,HttpServletResponse response,String body){
        AccessLogDO accessLog = (AccessLogDO) request.getAttribute(&quot;accessLog&quot;);
        accessLog.setResponseData(body);
        accessLog.setConsumingTime(Duration.between(accessLog.getCreateTime(),LocalDateTime.now()).getSeconds());
        accessLog.setResponseCode(response.getStatus());
        accessLogDao.updateById(accessLog);
    }
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;4、审计日志过滤器&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;/**
 * &amp;lt;p&amp;gt;DESC: 网关日志过滤器&amp;lt;/p&amp;gt;
 * &amp;lt;p&amp;gt;DATE: 2019-12-17 17:57&amp;lt;/p&amp;gt;
 * &amp;lt;p&amp;gt;VERSION:2.0.0&amp;lt;/p&amp;gt;
 * &amp;lt;p&amp;gt;@AUTHOR: ZhengYong&amp;lt;/p&amp;gt;
 */
@Slf4j
public class GatewayAuditLogSecurityFilter extends OncePerRequestFilter {

    private AccessLogDao accessLogDao;

    public GatewayAuditLogSecurityFilter(AccessLogDao accessLogDao) {
        this.accessLogDao = accessLogDao;
    }

    @Override
    protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
        //前面认证的过滤器会把jwt的令牌转换成一个Authentication，然后把它放到SpringSecurityContext安全的上下文里面。
        Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
        String username = (String) authentication.getPrincipal();
        log.info(&quot;【日志过滤器】 用户名={}&quot;,username);

        log.info(&quot;【日志过滤器】 1。 add log for ={}&quot;,username);
        //具体实现新增
        InputStreamHttpServletRequestWrapper servletRequest = new InputStreamHttpServletRequestWrapper(request);
        insertLog(servletRequest);

        //调用下面的过滤器
        filterChain.doFilter(servletRequest,response);

        if(request.getAttribute(&quot;updated&quot;) == null){
            log.info(&quot;【日志过滤器】 3。 update log for success&quot;);
            //具体实现修改
            updateLog(servletRequest,response,&quot;body&quot;);
        }
    }
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;5、问题&lt;/h3&gt;
&lt;blockquote&gt;
&lt;p&gt;由于 request中getReader()和getInputStream()只能调用一次,如果使用一次后继续使用就会抛出 stream.io 异常&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;解决方案：&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;在filter中request.getInputStream()取出请求后，在后面就会拿不到请求数据了，即一个InputStream对象在被读取完成后，将无法被再次读取。所以必须将读取出的数据进行缓存。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;在javax.servlet.http包下面有一个装饰器类&lt;code&gt;HttpServletRequestWrapper&lt;/code&gt;，利用这个装饰器类，我们可以重新包装一个HttpServletRequest对象。&lt;/p&gt;
&lt;p&gt;定义一个装饰器继承&lt;code&gt;HttpServletRequestWrapper&lt;/code&gt;,&lt;code&gt;streamBody&lt;/code&gt;字节变量用来保存读取的数据，以便于多次读取。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;public class InputStreamHttpServletRequestWrapper extends HttpServletRequestWrapper{


    private final byte[] streamBody;
    private static final int BUFFER_SIZE = 4096;

   
    public InputStreamHttpServletRequestWrapper(HttpServletRequest request) throws IOException {
        super(request);
        byte[] bytes = inputStream2Byte(request.getInputStream());
        if (bytes.length == 0 &amp;amp;&amp;amp; RequestMethod.POST.name().equals(request.getMethod())) {
            //从ParameterMap获取参数，并保存以便多次获取
            bytes = request.getParameterMap().entrySet().stream()
                    .map(entry -&amp;gt; {
                        String result;
                        String[] value = entry.getValue();
                        if (value != null &amp;amp;&amp;amp; value.length &amp;gt; 1) {
                            result = Arrays.stream(value).map(s -&amp;gt; entry.getKey() + &quot;=&quot; + s)
                                    .collect(Collectors.joining(&quot;&amp;amp;&quot;));
                        } else {
                            result = entry.getKey() + &quot;=&quot; + value[0];
                        }

                        return result;
                    }).collect(Collectors.joining(&quot;&amp;amp;&quot;)).getBytes();
        }

        streamBody = bytes;
    }

    private byte[] inputStream2Byte(InputStream inputStream) throws IOException {
        ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
        byte[] bytes = new byte[BUFFER_SIZE];
        int length;
        while ((length = inputStream.read(bytes, 0, BUFFER_SIZE)) != -1) {
            outputStream.write(bytes, 0, length);
        }

        return outputStream.toByteArray();
    }


    @Override
    public ServletInputStream getInputStream() throws IOException {
        ByteArrayInputStream inputStream = new ByteArrayInputStream(streamBody);

        return new ServletInputStream() {
            @Override
            public boolean isFinished() {
                return false;
            }

            @Override
            public boolean isReady() {
                return false;
            }

            @Override
            public void setReadListener(ReadListener listener) {

            }

            @Override
            public int read() throws IOException {
                return inputStream.read();
            }
        };
    }

    @Override
    public BufferedReader getReader() throws IOException {
        return new BufferedReader(new InputStreamReader(getInputStream()));
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote&gt;
&lt;p&gt;声明一个带有HttpServletRequest入参的构造器，从该参数对象的流中解析数据，如果没有则继续从parameterMap中获取，然后以key=value&amp;amp;key=value形式拼接。用streamBody接收。然后我们重写getInputStream方法，以后每次调用getInputStream方法，其实是重新利用streamBody重新new一个流，所以可以多次读取。&lt;/p&gt;
&lt;p&gt;有了装饰器后，我们就要装饰目标对象。我们都知道SpringMVC的一次请求会被一个个过滤器层层调用，也就是我们常说的责任链模式。利用&lt;code&gt;Filter&lt;/code&gt;我们就可以在某个特定的位置装饰HttpServletRequest对象。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h3&gt;6、httputil&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;/**
 * &amp;lt;p&amp;gt;DESC: http工具&amp;lt;/p&amp;gt;
 * &amp;lt;p&amp;gt;DATE: 2019/1/30&amp;lt;/p&amp;gt;
 * &amp;lt;p&amp;gt;VERSION:1.0.0&amp;lt;/p&amp;gt;
 * &amp;lt;p&amp;gt;@AUTHOR: ZhengYong&amp;lt;/p&amp;gt;
 */
public class HttpUtil {
    /**
     * 未知IP
     */
    private static final String UNKNOWN = &quot;unknown&quot;;

    private final static String ENCODE = &quot;UTF-8&quot;;

    /**
     * 获取请求的IP地址
     *
     * @param request request
     * @return ip地址
     */
    public static String getIpAddr(HttpServletRequest request) {
        String ip = request.getHeader(&quot;x-forwarded-for&quot;);
        if (ip ** null || ip.length() ** 0 || UNKNOWN.equalsIgnoreCase(ip)) {
            ip = request.getHeader(&quot;Proxy-Client-IP&quot;);
        }
        if (ip ** null || ip.length() ** 0 || UNKNOWN.equalsIgnoreCase(ip)) {
            ip = request.getHeader(&quot;WL-Proxy-Client-IP&quot;);
        }
        if (ip ** null || ip.length() ** 0 || UNKNOWN.equalsIgnoreCase(ip)) {
            ip = request.getRemoteAddr();
        }
        return &quot;0:0:0:0:0:0:0:1&quot;.equals(ip) ? &quot;127.0.0.1&quot; : ip;
    }

    /**
     * 获取域名和端口
     *
     * @param request request
     * @return 域名和端口
     */
    public static String getBasePath(HttpServletRequest request) {
        String path = request.getContextPath();
        return request.getScheme() + &quot;://&quot; + request.getServerName() + &quot;:&quot; + request.getServerPort() + path;
    }

    /**
     * 获取请求的全路径
     *
     * @param request request
     * @return 全路径url
     */
    public static String getFullPath(HttpServletRequest request) {
        String basePath = getBasePath(request);
        return basePath + request.getRequestURI();
    }

    /**
     * 获取请求Body
     *
     * @param request request
     * @return String
     */
    public static String getRequestBody(HttpServletRequest request) {
        StringBuilder sb = new StringBuilder();
        InputStream inputStream = null;
        BufferedReader reader = null;
        String character = request.getCharacterEncoding();
        try {
            inputStream = request.getInputStream();
            reader = new BufferedReader(new InputStreamReader(inputStream, character));
            String line = &quot;&quot;;
            while ((line = reader.readLine()) != null) {
                sb.append(line);
            }
        } catch (IOException e) {
            e.printStackTrace();
        } finally {
            if (inputStream != null) {
                try {
                    inputStream.close();
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
            if (reader != null) {
                try {
                    reader.close();
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
        }
        return sb.toString();
    }

    /**
     * 获取请求body 通过 BufferReader
     * @param request  请求
     * @return   body
     * @throws IOException 异常
     */
    public static String getBodyByBufferReader(HttpServletRequest request) throws IOException {
        StringBuilder sb = new StringBuilder();
        BufferedReader reader = request.getReader();
        try {
            String line = &quot;&quot;;
            while ((line = reader.readLine()) != null) {
                sb.append(line);
            }
        } catch (IOException e) {
            e.printStackTrace();
        } finally {
            if (reader != null) {
                try {
                    reader.close();
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
        }
        return sb.toString();
    }

    /**
     * URL 解码
     *
     * @return String
     */
    public static String getURLDecoderString(String str) {
        String result = &quot;&quot;;
        if (null == str) {
            return &quot;&quot;;
        }
        try {
            result = java.net.URLDecoder.decode(str, ENCODE);
        } catch (UnsupportedEncodingException e) {
            e.printStackTrace();
        }
        return result;
    }

    public static byte[] readBytes(ServletInputStream inputStream) throws IOException {
        ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
        byte[] bffuer = new byte[1024];
        int n = 0 ;
        while (-1 != (n = inputStream.read(bffuer))){
            outputStream.write(bffuer,0,n);
        }
        return outputStream.toByteArray();
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;jwt改造总结&lt;/h2&gt;
&lt;h3&gt;1、总览图&lt;/h3&gt;
&lt;p&gt;左边本质上都是过滤器，虽然最后一个不是叫做什么什么filter。右边都是自己写的一些组件。组件的作用是改变过滤器的行为。
在过滤器链上，绿色是我们自己写的，蓝色是Spring Security提供的，自己写的组件注入到Spring的过滤器里面，来改变或者是增强Spring 自己的过滤器里面的行为。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;jwt%E6%94%B9%E9%80%A0%E6%80%BB%E7%BB%93.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;1.限流
2.从jwt或者其他的这些令牌里面，把当前的用户 身份提取出来
3。审计日志
4.从名字看是异常的转换过滤器。本身并没有任何业务逻辑。他的作用就是catch后面这个Interceptor抛出的来的异常，
5.作用就是判断权限，我们写的permissonSerice最终就是在这里生效的。&lt;/p&gt;
&lt;h3&gt;2、过滤器链&lt;/h3&gt;
&lt;p&gt;&lt;img src=&quot;%E8%BF%87%E6%BB%A4%E5%99%A8%E9%93%BE%E9%A1%BA%E5%BA%8F.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;一个请求过来，他会按照这个顺序，经过所有的过滤器。当然还有一些其他的过滤器，就是SpringSecurity过滤器链上的其他过滤器。这些和我们核心讲 的没关系，我们就把它忽略了。但是跟我讲的安全相关的，就是核心的几个安全相关的过滤器都列出来了。按照这个顺序，请求会传过过滤器。&lt;/p&gt;
&lt;h3&gt;3、SecurityInterceptor&lt;/h3&gt;
&lt;p&gt;我们会把自己的权限的判断的逻辑放在PermissionService里面。然后把PermissionService给到一个表达式处理器WebSecurityExpressionHandler。
表达式处理器给到我们的SecurityInterceptor&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;%E8%87%AA%E5%AE%9A%E4%B9%89%E6%9D%83%E9%99%90%E8%A1%A8%E8%BE%BE%E5%BC%8F.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;最终我们写的表达式是交给WebSecurityExpressionHandler来处理，然后它又交给了PermissionService&lt;/p&gt;
&lt;p&gt;PermissionService里面我们又写了如果你没有带token，就是你当前是个匿名用户 我就会抛出异常，如果你是个可以认出来的合法用户，那么你有没有权限，也会有个逻辑来判断。如果你没有权限，这里就会抛出相应的错误。就是Exception&lt;/p&gt;
&lt;h3&gt;4、ExceptionTranslationFilter&lt;/h3&gt;
&lt;blockquote&gt;
&lt;p&gt;ExceptionTranslationFilter&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;抛出Exception会被ExceptionTranslationFilter捕获住，捕获住以后，会根据你抛出来的异常类型去调相应的处理器。在整个安全的里面一共就两种异常，一种是401一种是403&lt;/p&gt;
&lt;h3&gt;5、GatewayAuthenticationEntryPoint&lt;/h3&gt;
&lt;blockquote&gt;
&lt;p&gt;GatewayAuthenticationEntryPoint&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;401就交给了GatewayAuthenticationEntryPoint来处理。你的令牌有问题或者是你没传令牌。&lt;/p&gt;
&lt;h3&gt;6、GatewayAccessDeniedHandler&lt;/h3&gt;
&lt;blockquote&gt;
&lt;p&gt;GatewayAccessDeniedHandler&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;另一种是你的身份认证过了，但是当前这个请求你没权限，这是403交给GatewayAccessDeniedHandler来处理&lt;/p&gt;
&lt;p&gt;这两个组件都是注到ExceptionTranslationFilter来进行相应的处理的&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;%E5%A4%84%E7%90%86401-403.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;同时GatewayAuthenticationEntryPoint也会被注到 OAuth2ClientAuthenticationProcessingFilter。有种情况是你的令牌传的不对，根本解析不了。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;%E6%B3%A8%E5%86%8C%E5%88%B0%E4%BA%BA%E5%8F%A3%E7%82%B9.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;h2&gt;sentinel限流实战&lt;/h2&gt;
&lt;p&gt;&lt;img src=&quot;https://tangtangit.com/_astro/sentinel.BhloLRs0_2kTMuO.webp&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;简单来说就是干三件事，最终的结果就是保证你的服务可用，不会崩掉。保证服务高可用。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;限流&lt;/li&gt;
&lt;li&gt;熔断&lt;/li&gt;
&lt;li&gt;降级规则&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;1、流控&lt;/h3&gt;
&lt;p&gt;先从最简单的场景来入手。
1.引用一个依赖，
2，声明一个资源。
3.声明一个规则
注意依赖是加在你的微服务上的，每一个微服务都要加一个sentinel的依赖。&lt;/p&gt;
&lt;hr /&gt;
&lt;h2&gt;OAuth2.0开发问题汇总&lt;/h2&gt;
&lt;h2&gt;问题1：&lt;/h2&gt;
&lt;p&gt;&lt;strong&gt;Spring Security – There is no PasswordEncoder mapped for the id “null”&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;解决方法一：
问题解决：https://blog.csdn.net/Hello_World_QWP/article/details/81811462&lt;/p&gt;
&lt;p&gt;解决方法二：
因为5.x版本新增了多种密码加密方式，必须指定一种，比如这样解决&lt;/p&gt;
&lt;pre&gt;&lt;code&gt; @Bean

 public static NoOpPasswordEncoder passwordEncoder() {

   return (NoOpPasswordEncoder) NoOpPasswordEncoder.getInstance();

 }

&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;问题2：&lt;/h2&gt;
&lt;p&gt;&lt;strong&gt;调用接口/com-oauth/oauth/check_token失败&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;返回错误码：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;{
  &quot;timestamp&quot;: &quot;2019-07-10T02:57:43.818+0000&quot;,
  &quot;status&quot;: 403,
  &quot;error&quot;: &quot;Forbidden&quot;,
  &quot;message&quot;: &quot;Forbidden&quot;,
  &quot;path&quot;: &quot;/com-oauth/oauth/check_token&quot;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;解决方法一：
设置 security.tokenKeyAccess(“permitAll()”).checkTokenAccess(“permitAll()”);&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;@Configuration
@EnableAuthorizationServer
public class OAuth2AuthorizationServer extends AuthorizationServerConfigurerAdapter {

    @Override
    public void configure(AuthorizationServerSecurityConfigurer security) throws Exception {
        super.configure(security);
        security.tokenKeyAccess(&quot;permitAll()&quot;).checkTokenAccess(&quot;permitAll()&quot;);
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;解决犯法二：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;@Configuration
@EnableWebSecurity
public class AuthWebSecurityConfig extends WebSecurityConfigurerAdapter {
      /**
     * 配置忽略的路径，/oauth/check_token 是校验token的路径，security默认没有放行
     * @param web  web
     * @throws Exception 异常
     */
    @Override
    public void configure(WebSecurity web) throws Exception {
        web.ignoring().antMatchers(&quot;/oauth/check_token&quot;);
    }
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;问题3：&lt;/h2&gt;
&lt;p&gt;在使用密码模式时，抛出异常：o.s.s.o.provider.endpoint.TokenEndpoint : Handling error: UnsupportedGrantTypeException, Unsupported grant type: password&lt;/p&gt;
&lt;p&gt;解决方法：配置AuthenticationManager&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;@Configuration
@EnableAuthorizationServer
public class OAuth2AuthorizationServer extends AuthorizationServerConfigurerAdapter {


   // 用户认证
   @Autowired
   private AuthenticationManager authenticationManager;

   @Override
   public void configure(AuthorizationServerEndpointsConfigurer endpoints) throws Exception {
       super.configure(endpoints);
       // 密码模式必须有这个参数
       endpoints.authenticationManager(authenticationManager);
   }
}

&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;问题4&lt;/h2&gt;
&lt;p&gt;Field authenticationManager in cn.springcloud.book.OAuthConfiguration required a bean of type ‘org.springframework.security.authentication.AuthenticationManager’ that could not be found&lt;/p&gt;
&lt;p&gt;解决方法：
继承WebSecurityConfigurerAdapter 类，并重写方法authenticationManager()，使用 @Bean注解标记&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;@ComponentScan
@Configuration
public class MyWebSecurityConfigurerAdapter extends WebSecurityConfigurerAdapter {

    @Bean(name = BeanIds.AUTHENTICATION_MANAGER)
    @Override
    protected AuthenticationManager authenticationManager() throws Exception {
        return super.authenticationManager();
    }
}

&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;问题5：&lt;/h2&gt;
&lt;p&gt;&lt;strong&gt;在passwod模式下，执行刷新token时，抛出异常Handling error: IllegalStateException, UserDetailsService is required.&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;执行以下命令，抛出异常Handling error: IllegalStateException, UserDetailsService is required&lt;/p&gt;
&lt;pre&gt;&lt;code&gt; curl -i -X POST -u &apos;clientapp2:112233&apos;  http://10.216.33.211:10808/com-oauth/oauth/token -H &quot;accept: application/json&quot; -d &apos;grant_type=refresh_token&amp;amp;refresh_token=b610dfa9-2ee4-4214-bc57-f6b2937d4b27&apos;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;解决方法：
在AuthorizationServerConfigurerAdapter 中配置UserDetailsService对象&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;@Configuration
@EnableAuthorizationServer
public class OAuth2AuthorizationServer extends AuthorizationServerConfigurerAdapter {

    @Autowired
    private MyUserDetailsService myUserDetailsService;

    @Override
    public void configure(AuthorizationServerEndpointsConfigurer endpoints) throws Exception {
        // 执行token刷新需要带上此参数
        endpoints.userDetailsService(myUserDetailsService);
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;问题6：&lt;/h2&gt;
&lt;p&gt;不支持form表单提交&lt;/p&gt;
&lt;p&gt;执行命令&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;curl -X POST &quot;http://10.216.33.211:10808/com-oauth/oauth/token&quot; -d &quot;grant_type=client_credentials&amp;amp;scope=read_contacts&amp;amp;client_id=clientapp&amp;amp;client_secret=112233&quot;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;返回错误：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;{&quot;timestamp&quot;:&quot;2019-07-11T02:27:29.962+0000&quot;,&quot;status&quot;:401,&quot;error&quot;:&quot;Unauthorized&quot;,&quot;message&quot;:&quot;Unauthorized&quot;,&quot;path&quot;:&quot;/com-oauth/oauth/token&quot;} 
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;解决方法：
支持Form表达提交： security.allowFormAuthenticationForClients();&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;@Configuration
@EnableAuthorizationServer
public class OAuth2AuthorizationServer extends AuthorizationServerConfigurerAdapter {

    @Override
    public void configure(AuthorizationServerSecurityConfigurer security) throws Exception {
        super.configure(security);
        security.tokenKeyAccess(&quot;permitAll()&quot;).checkTokenAccess(&quot;permitAll()&quot;);
        //允许表单认证
        security.allowFormAuthenticationForClients();
    }

 
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;执行命令&lt;/p&gt;
&lt;pre&gt;&lt;code&gt; curl -X POST &quot;http://10.216.33.211:10808/com-oauth/oauth/token&quot; -d &quot;grant_type=client_credentials&amp;amp;scope=read_contacts&amp;amp;client_id=clientapp&amp;amp;client_secret=112233&quot;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;返回正常结果：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;{&quot;access_token&quot;:&quot;35ae4576-f7b3-480e-aeff-eee7ea2ce803&quot;,&quot;token_type&quot;:&quot;bearer&quot;,&quot;refresh_token&quot;:&quot;0493963a-22f5-4cff-8b50-3cc5da3577a6&quot;,&quot;expires_in&quot;:197,&quot;scope&quot;:&quot;read_contacts&quot;}
&lt;/code&gt;&lt;/pre&gt;
&lt;hr /&gt;
&lt;h2&gt;SpringSecurity整合版v2&lt;/h2&gt;
&lt;h2&gt;&lt;strong&gt;SpringSecurity&lt;/strong&gt; &lt;strong&gt;基本原理&lt;/strong&gt;&lt;/h2&gt;
&lt;p&gt;项目地址：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;git@gitee.com:eim-ZY/acl_parent.git
git@gitee.com:eim-ZY/securitydemo1.git
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;过滤器链&lt;/h3&gt;
&lt;p&gt;SpringSecurity 本质是一个过滤器链：&lt;/p&gt;
&lt;p&gt;从启动是可以获取到过滤器链：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;org.springframework.security.web.context.request.async.WebAsyncManagerIntegrationFil
ter
org.springframework.security.web.context.SecurityContextPersistenceFilter 
org.springframework.security.web.header.HeaderWriterFilter
org.springframework.security.web.csrf.CsrfFilter
org.springframework.security.web.authentication.logout.LogoutFilter 
org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter 
org.springframework.security.web.authentication.ui.DefaultLoginPageGeneratingFilter 
org.springframework.security.web.authentication.ui.DefaultLogoutPageGeneratingFilter
org.springframework.security.web.savedrequest.RequestCacheAwareFilter
org.springframework.security.web.servletapi.SecurityContextHolderAwareRequestFilter
org.springframework.security.web.authentication.AnonymousAuthenticationFilter 
org.springframework.security.web.session.SessionManagementFilter 
org.springframework.security.web.access.ExceptionTranslationFilter 
org.springframework.security.web.access.intercept.FilterSecurityInterceptor
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;代码底层流程：重点看三个过滤器：&lt;/p&gt;
&lt;p&gt;FilterSecurityInterceptor：是一个方法级的权限过滤器, 基本位于过滤链的最底部。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://tangtangit.com/_astro/image-20210316144237188.BhPWmZuI_Z1omxVP.webp&quot; alt=&quot;image-20210316144237188&quot; /&gt;&lt;/p&gt;
&lt;p&gt;super.beforeInvocation(fi) 表示查看之前的 filter 是否通过。&lt;/p&gt;
&lt;p&gt;fi.getChain().doFilter(fi.getRequest(), fi.getResponse());表示真正的调用后台的服务。&lt;/p&gt;
&lt;p&gt;ExceptionTranslationFilter：是个异常过滤器，用来处理在认证授权过程中抛出的异常&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://tangtangit.com/_astro/image-20210316144517521.BvjqMm-P_1slvP2.webp&quot; alt=&quot;image-20210316144517521&quot; /&gt;&lt;/p&gt;
&lt;p&gt;UsernamePasswordAuthenticationFilter ：对/login 的 POST 请求做拦截，校验表单中用户名，密码。&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;DelegationFilterProxy首先加载 ，执行dofilter方法，执行delegateToUse = this.initDelegate(wac);&lt;/p&gt;
&lt;p&gt;initDelegate方法中获取固定名字FilterChainProxy，收集所有过滤器并组装返回&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;&lt;img src=&quot;https://tangtangit.com/_astro/image-20210316150130018.DXxF4gJx_1qrquU.webp&quot; alt=&quot;image-20210316150130018&quot; /&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://tangtangit.com/_astro/image-20210316152005164.Cl6a-19B_Z1NUvz1.webp&quot; alt=&quot;image-20210316152005164&quot; /&gt;&lt;/p&gt;
&lt;h3&gt;&lt;strong&gt;UserDetailsService&lt;/strong&gt; &lt;strong&gt;接口讲解&lt;/strong&gt;&lt;/h3&gt;
&lt;p&gt;当什么也没有配置的时候，账号和密码是由 Spring Security 定义生成的。而在实际项目中&lt;/p&gt;
&lt;p&gt;账号和密码都是从数据库中查询出来的。 所以我们要通过自定义逻辑控制认证逻辑。&lt;/p&gt;
&lt;p&gt;如果需要自定义逻辑时，只需要实现 UserDetailsService 接口即可。接口定义如下：&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://tangtangit.com/_astro/image-20210316144716862.w33s9Ggu_ZO50Ks.webp&quot; alt=&quot;image-20210316144716862&quot; /&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;返回值&lt;/strong&gt; &lt;strong&gt;UserDetails&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;这个类是系统默认的用户“&lt;strong&gt;主体&lt;/strong&gt;&lt;/p&gt;
&lt;h2&gt;web权限方案&lt;/h2&gt;
&lt;blockquote&gt;
&lt;p&gt;(1)  认证&lt;/p&gt;
&lt;/blockquote&gt;
&lt;blockquote&gt;
&lt;p&gt;(2）授权&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h3&gt;1、设置登陆的用户名密码&lt;/h3&gt;
&lt;blockquote&gt;
&lt;p&gt;方式一：通过配置文件&lt;/p&gt;
&lt;p&gt;方式二：通过配置类&lt;/p&gt;
&lt;p&gt;方式三：自定义编写实现类&lt;/p&gt;
&lt;/blockquote&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;h3&gt;通过配置文件&lt;/h3&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;img src=&quot;https://tangtangit.com/_astro/image-20210316184802961.CovOSUvr_s0YFB.webp&quot; alt=&quot;image-20210316184802961&quot; /&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;h3&gt;通过配置类&lt;/h3&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;@Configuration
public class MySecurityConfig extends WebSecurityConfigurerAdapter {

    @Bean
    public PasswordEncoder bCryptPasswordEncoder(){
        return new BCryptPasswordEncoder();
    }

    @Override
    protected void configure(AuthenticationManagerBuilder auth) throws Exception {
        BCryptPasswordEncoder passwordEncoder = new BCryptPasswordEncoder();
        String password = passwordEncoder.encode(&quot;123&quot;);
        auth.inMemoryAuthentication().withUser(&quot;jack&quot;).password(password).roles();
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;h3&gt;自定义编写实现类&lt;/h3&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;blockquote&gt;
&lt;p&gt;第一步：创建配置类，设置使用哪一个userDetailService实现类&lt;/p&gt;
&lt;p&gt;第二步：编写实现类，返回User对象，User对象有用户名和操作权限&lt;/p&gt;
&lt;/blockquote&gt;
&lt;pre&gt;&lt;code&gt;@Configuration
public class SecurityConfigTest extends WebSecurityConfigurerAdapter {

    @Autowired
    private UserDetailsService userDetailsService;

    @Bean
    public PasswordEncoder bCryptPasswordEncoder(){
        return new BCryptPasswordEncoder();
    }

    @Override
    protected void configure(AuthenticationManagerBuilder auth) throws Exception {
        auth.userDetailsService(userDetailsService).passwordEncoder(bCryptPasswordEncoder());
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;@Service(&quot;userDetailsService&quot;)
public class MyUserDetailsServiceImpl implements UserDetailsService {

    @Override
    public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
        List&amp;lt;GrantedAuthority&amp;gt; authorities = AuthorityUtils.commaSeparatedStringToAuthorityList(&quot;role&quot;);
        return new User(&quot;jack&quot;,new BCryptPasswordEncoder().encode(&quot;123&quot;),authorities);
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;2、查询数据库完成用户认证&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;依赖&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;   &amp;lt;!--    mysql数据库     --&amp;gt;
        &amp;lt;dependency&amp;gt;
            &amp;lt;groupId&amp;gt;com.baomidou&amp;lt;/groupId&amp;gt;
            &amp;lt;artifactId&amp;gt;mybatis-plus-boot-starter&amp;lt;/artifactId&amp;gt;
            &amp;lt;version&amp;gt;3.4.2&amp;lt;/version&amp;gt;
        &amp;lt;/dependency&amp;gt;
        &amp;lt;dependency&amp;gt;
            &amp;lt;groupId&amp;gt;mysql&amp;lt;/groupId&amp;gt;
            &amp;lt;artifactId&amp;gt;mysql-connector-java&amp;lt;/artifactId&amp;gt;
        &amp;lt;/dependency&amp;gt;
        &amp;lt;dependency&amp;gt;
            &amp;lt;groupId&amp;gt;org.projectlombok&amp;lt;/groupId&amp;gt;
            &amp;lt;artifactId&amp;gt;lombok&amp;lt;/artifactId&amp;gt;
        &amp;lt;/dependency&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;数据库配置&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;spring:
  datasource:
    driver-class-name: com.mysql.cj.jdbc.Driver
    url: jdbc:mysql://192.168.31.70:3306/security?serverTimezone=GMT%2B8
    username: root
    password: 123456
&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;数据库表&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;create table users(
 id bigint primary key auto_increment,
username varchar(20) unique not null,
password varchar(100)
);
-- 密码 atguigu
insert into users values(1,&apos;张
san&apos;,&apos;$2a$10$2R/M6iU3mCZt3ByG7kwYTeeW0w7/UqdeXrb27zkBIizBvAven0/na&apos;);
-- 密码 atguigu
insert into users values(2,&apos;李
si&apos;,&apos;$2a$10$2R/M6iU3mCZt3ByG7kwYTeeW0w7/UqdeXrb27zkBIizBvAven0/na&apos;);
create table role(
id bigint primary key auto_increment,
name varchar(20)
);
insert into role values(1,&apos;管理员&apos;);
insert into role values(2,&apos;普通用户&apos;);
create table role_user(
uid bigint,
rid bigint
);
insert into role_user values(1,1);
insert into role_user values(2,2);
create table menu(
id bigint primary key auto_increment,
name varchar(20),
url varchar(100),
parentid bigint,
permission varchar(20)
);
insert into menu values(1,&apos;系统管理&apos;,&apos;&apos;,0,&apos;menu:system&apos;);
insert into menu values(2,&apos;用户管理&apos;,&apos;&apos;,0,&apos;menu:user&apos;);
create table role_menu(
mid bigint,
rid bigint
);
insert into role_menu values(1,1);
insert into role_menu values(2,1);
insert into role_menu values(2,2);
&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;实体类&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;@Data
@TableName(value = &quot;users&quot;)
public class MyUser {
    private Integer id;
    private String username;
    private String password;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;整合map&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;@Repository
public interface MyUserMapper extends BaseMapper&amp;lt;MyUser&amp;gt; {
}
&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;userdetailservice中调用mapper查询数据库&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;@Service(&quot;userDetailsService&quot;)
public class MyUserDetailsServiceImpl implements UserDetailsService {

    @Autowired
    private MyUserMapper myUserMapper;

    @Override
    public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
        //调用mapper查询数据库
        QueryWrapper&amp;lt;MyUser&amp;gt; queryWrapper = new QueryWrapper&amp;lt;&amp;gt;();
        queryWrapper.eq(&quot;username&quot;,username);
        MyUser myUser = myUserMapper.selectOne(queryWrapper);
        if(myUser == null){
            throw new UsernameNotFoundException(&quot;用户名不存在！&quot;);
        }

        List&amp;lt;GrantedAuthority&amp;gt; authorities = AuthorityUtils.commaSeparatedStringToAuthorityList(&quot;role&quot;);
        return new User(myUser.getUsername(),new BCryptPasswordEncoder().encode(myUser.getPassword()),authorities);
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;启动类添加mapperscan&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;@MapperScan(&quot;com.eim.mapper&quot;)
@SpringBootApplication
public class SecurityDem1Application {

    public static void main(String[] args) {
        SpringApplication.run(SecurityDem1Application.class, args);
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;3、自定义登陆页面，不登陆访问&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;配置类(SecurityConfigTest extends WebSecurityConfigurerAdapter)中需要实现相关配置&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;@Override
    protected void configure(HttpSecurity http) throws Exception {
        http.formLogin()
                .loginPage(&quot;/login.html&quot;)              //登陆页面设置
                .loginProcessingUrl(&quot;/user/login&quot;)     //登陆访问路径
                .defaultSuccessUrl(&quot;/test/index&quot;).permitAll()   //登陆成功跳转页面
                .and().authorizeRequests()
                    .antMatchers(&quot;/&quot;,&quot;/test/hello&quot;,&quot;/user/login&quot;).permitAll() //设置路径无需登陆即可认证
                .anyRequest().authenticated()   //其他请求必须认证
                .and().csrf().disable();        //关闭csrf
    }
&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;创建页面和controller&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;&amp;lt;!DOCTYPE html&amp;gt;
&amp;lt;html lang=&quot;en&quot;&amp;gt;
&amp;lt;head&amp;gt;
    &amp;lt;meta charset=&quot;UTF-8&quot;&amp;gt;
    &amp;lt;title&amp;gt;login&amp;lt;/title&amp;gt;
&amp;lt;/head&amp;gt;
&amp;lt;body&amp;gt;
    &amp;lt;form action=&quot;/user/login&quot; method=&quot;post&quot;&amp;gt;
        用户名：&amp;lt;input type=&quot;text&quot; name=&quot;username&quot;&amp;gt;
        &amp;lt;br/&amp;gt;
        密码： &amp;lt;input type=&quot;text&quot; name=&quot;password&quot;&amp;gt;
        &amp;lt;br/&amp;gt;
        &amp;lt;input type=&quot;submit&quot; value=&quot;登陆&quot;&amp;gt;
    &amp;lt;/form&amp;gt;
&amp;lt;/body&amp;gt;
&amp;lt;/html&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;    @GetMapping(&quot;index&quot;)
    public String index(){
        return &quot;hello index&quot;;
    }
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;4、基于角色或权限进行访问&lt;/h3&gt;
&lt;h4&gt;&lt;strong&gt;第一个：hasAuthority&lt;/strong&gt; &lt;strong&gt;方法&lt;/strong&gt;&lt;/h4&gt;
&lt;p&gt;如果当前的主体具有指定的权限，则返回 true,否则返回 false&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;修改配置类，设置当前访问地址有那些权限&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;img src=&quot;https://tangtangit.com/_astro/image-20210316204527715.DrhUV5g9_oPeT5.webp&quot; alt=&quot;image-20210316204527715&quot; /&gt;&lt;/p&gt;
&lt;p&gt;在userDetailService中，把返回user对象设置权限&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://tangtangit.com/_astro/image-20210316204717790.BR_M3JGF_XwUwo.webp&quot; alt=&quot;image-20210316204717790&quot; /&gt;&lt;/p&gt;
&lt;h4&gt;第二个：&lt;strong&gt;hasAnyAuthority&lt;/strong&gt; &lt;strong&gt;方法&lt;/strong&gt;&lt;/h4&gt;
&lt;p&gt;如果当前的主体有任何提供的角色（给定的作为一个逗号分隔的字符串列表）的话，返回true&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://tangtangit.com/_astro/image-20210316205121100.DHoD3N-X_Z1etsjG.webp&quot; alt=&quot;image-20210316205121100&quot; /&gt;&lt;/p&gt;
&lt;h4&gt;第三个： &lt;strong&gt;hasRole&lt;/strong&gt; &lt;strong&gt;方法&lt;/strong&gt;&lt;/h4&gt;
&lt;p&gt;如果用户具备给定角色就允许访问,否则出现 403。&lt;/p&gt;
&lt;p&gt;如果当前主体具有指定的角色，则返回 true。&lt;/p&gt;
&lt;p&gt;底层源码：&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://tangtangit.com/_astro/image-20210316205442628.x5dhy11z_tJ2Sz.webp&quot; alt=&quot;image-20210316205442628&quot; /&gt;&lt;/p&gt;
&lt;p&gt;配置类：&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://tangtangit.com/_astro/image-20210316205722586.BIWrxxtr_1YJuBc.webp&quot; alt=&quot;image-20210316205722586&quot; /&gt;&lt;/p&gt;
&lt;p&gt;给用户添加角色：&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://tangtangit.com/_astro/image-20210316205915494.DzVU7nk4_Z7Rbgd.webp&quot; alt=&quot;image-20210316205915494&quot; /&gt;&lt;/p&gt;
&lt;h4&gt;第四个：hasAnyRole&lt;/h4&gt;
&lt;p&gt;表示用户具备任何一个条件都可以访问。&lt;/p&gt;
&lt;h3&gt;5、自定义403无权限页面&lt;/h3&gt;
&lt;p&gt;修改配置类&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://tangtangit.com/_astro/image-20210316210350710.CV3IcGkn_Z9kPb4.webp&quot; alt=&quot;image-20210316210350710&quot; /&gt;&lt;/p&gt;
&lt;h2&gt;注解使用&lt;/h2&gt;
&lt;h3&gt;1、&lt;strong&gt;@Secured&lt;/strong&gt;&lt;/h3&gt;
&lt;p&gt;判断是否具有角色，另外需要注意的是这里匹配的字符串需要添加前缀“ROLE_“。&lt;/p&gt;
&lt;p&gt;使用注解先要开启注解功能！放在启动类或则配置类&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;@EnableGlobalMethodSecurity(securedEnabled=true)&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;@EnableGlobalMethodSecurity(securedEnabled=true)
public class SecurityDem1Application {
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;在控制器方法上添加注解&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;    @Secured({&quot;ROLE_admin&quot;,&quot;ROLE_manager&quot;})
    @GetMapping(&quot;update&quot;)
    public String update(){
        return &quot;hello update&quot;;
    }
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;用户添加角色&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://tangtangit.com/_astro/image-20210316211623732.DrfgVNPT_Z14l4OU.webp&quot; alt=&quot;image-20210316211623732&quot; /&gt;&lt;/p&gt;
&lt;h3&gt;2、&lt;strong&gt;@PreAuthorize&lt;/strong&gt;&lt;/h3&gt;
&lt;p&gt;先开启注解功能：&lt;/p&gt;
&lt;p&gt;@EnableGlobalMethodSecurity(prePostEnabled = &lt;strong&gt;true&lt;/strong&gt;)           启动类上&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://tangtangit.com/_astro/image-20210316211837000.CIqPpNCi_2Xj6p.webp&quot; alt=&quot;image-20210316211837000&quot; /&gt;&lt;/p&gt;
&lt;p&gt;@PreAuthorize：注解适合进入方法&lt;strong&gt;前&lt;/strong&gt;的权限验证， @PreAuthorize 可以将登录用户的 roles/permissions 参数传到方法中。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;    @PreAuthorize(&quot;hasAuthority(&apos;admin&apos;)&quot;)
    //@Secured({&quot;ROLE_admin&quot;,&quot;ROLE_manager&quot;})
    @GetMapping(&quot;update&quot;)
    public String update(){
        return &quot;hello update&quot;;
    }
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;3、&lt;strong&gt;@PostAuthorize&lt;/strong&gt;&lt;/h3&gt;
&lt;p&gt;先开启注解功能：@EnableGlobalMethodSecurity(prePostEnabled = &lt;strong&gt;true&lt;/strong&gt;)&lt;/p&gt;
&lt;p&gt;@PostAuthorize 注解使用并不多，在方法执行后再进行权限验证，适合验证带有返回值的权限&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;    @PostAuthorize(&quot;hasAuthority(&apos;admin&apos;)&quot;)
    //@PreAuthorize(&quot;hasAuthority(&apos;admin&apos;)&quot;)
    //@Secured({&quot;ROLE_admin&quot;,&quot;ROLE_manager&quot;})
    @GetMapping(&quot;update&quot;)
    public String update(){
        System.out.println(&quot;update....&quot;);
        return &quot;hello update&quot;;
    }
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;4、**@PostFilter*&lt;/h3&gt;
&lt;p&gt;@PostFilter ：权限验证之后对数据进行过滤 留下用户名是 admin1 的数据&lt;/p&gt;
&lt;p&gt;表达式中的 filterObject 引用的是方法返回值 List 中的某一个元素&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;@RequestMapping(&quot;getAll&quot;)
@PreAuthorize(&quot;hasRole(&apos;ROLE_管理员&apos;)&quot;)
@PostFilter(&quot;filterObject.username == &apos;admin1&apos;&quot;)
@ResponseBody
public List&amp;lt;UserInfo&amp;gt; getAllUser(){
 ArrayList&amp;lt;UserInfo&amp;gt; list = new ArrayList&amp;lt;&amp;gt;();
 list.add(new UserInfo(1l,&quot;admin1&quot;,&quot;6666&quot;));
 list.add(new UserInfo(2l,&quot;admin2&quot;,&quot;888&quot;));
return list;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;5、&lt;strong&gt;@PreFilter&lt;/strong&gt;&lt;/h3&gt;
&lt;p&gt;@PreFilter: 进入控制器之前对数据进行过滤&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;@RequestMapping(&quot;getTestPreFilter&quot;)
@PreAuthorize(&quot;hasRole(&apos;ROLE_管理员&apos;)&quot;)
@PreFilter(value = &quot;filterObject.id%2==0&quot;)
@ResponseBody
public List&amp;lt;UserInfo&amp;gt; getTestPreFilter(@RequestBody List&amp;lt;UserInfo&amp;gt; 
list){
 list.forEach(t-&amp;gt; {
 System.out.println(t.getId()+&quot;\t&quot;+t.getUsername());
 });
return list;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;用户注销&lt;/h2&gt;
&lt;p&gt;&lt;strong&gt;在配置类中添加退出映射地址&lt;/strong&gt;.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;http.logout().logoutUrl(&quot;/logout&quot;).logoutSuccessUrl(&quot;/index&quot;).permitAll();
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;测试&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://tangtangit.com/_astro/image-20210316213825581.DAEH16Vv_obewr.webp&quot; alt=&quot;image-20210316213825581&quot; /&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;在登录页面添加一个退出连接&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&amp;lt;body&amp;gt;
登录成功&amp;lt;br&amp;gt; &amp;lt;a href=&quot;/logout&quot;&amp;gt;退出&amp;lt;/a&amp;gt;
&amp;lt;/body
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;自动登陆&lt;/h2&gt;
&lt;h3&gt;原理&lt;/h3&gt;
&lt;blockquote&gt;
&lt;p&gt;1、cookie&lt;/p&gt;
&lt;p&gt;2、安全框架机制实现自动登陆&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;&lt;img src=&quot;https://tangtangit.com/_astro/image-20210316214650583.BTSqTThE_2egg1C.webp&quot; alt=&quot;image-20210316214650583&quot; /&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://tangtangit.com/_astro/image-20210316214740832.zI0jWo6z_LyVx5.webp&quot; alt=&quot;image-20210316214740832&quot; /&gt;&lt;/p&gt;
&lt;h3&gt;具体实现&lt;/h3&gt;
&lt;p&gt;1、创建数据库表&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;CREATE TABLE `persistent_logins` (
 `username` varchar(64) NOT NULL,
 `series` varchar(64) NOT NULL,
 `token` varchar(64) NOT NULL,
 `last_used` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE 
CURRENT_TIMESTAMP,
 PRIMARY KEY (`series`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;2、配置类，注入数据源，配置操作库对象&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;    //注入数据源
    @Autowired
    private DataSource dataSource;

    @Bean
    public PersistentTokenRepository persistentTokenRepository(){
        JdbcTokenRepositoryImpl jdbcTokenRepository = new JdbcTokenRepositoryImpl();
        jdbcTokenRepository.setDataSource(dataSource);
        //jdbcTokenRepository.setCreateTableOnStartup(true);//自动创建表
        return jdbcTokenRepository;
    }
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;3、配置记住我&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;                .and().rememberMe().tokenRepository(persistentTokenRepository())
                .tokenValiditySeconds(60)
                .userDetailsService(userDetailsService)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;img src=&quot;https://tangtangit.com/_astro/image-20210316215917111.pRvCyEHl_WnvX4.webp&quot; alt=&quot;image-20210316215917111&quot; /&gt;&lt;/p&gt;
&lt;p&gt;4、&lt;strong&gt;页面添加记住我复选框&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;记住我：&amp;lt;input type=&quot;checkbox&quot;name=&quot;remember-me&quot;title=&quot;记住密码&quot;/&amp;gt;&amp;lt;br/&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;此处：name 属性值必须位 remember-me.不能改为其他值&lt;/p&gt;
&lt;h2&gt;&lt;strong&gt;CSRF&lt;/strong&gt;&lt;/h2&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;跨站请求伪造&lt;/strong&gt;（英语：Cross-site request forgery），也被称为 &lt;strong&gt;one-click&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;attack&lt;/strong&gt; 或者 &lt;strong&gt;session riding&lt;/strong&gt;，通常缩写为 &lt;strong&gt;CSRF&lt;/strong&gt; 或者 &lt;strong&gt;XSRF&lt;/strong&gt;， 是一种挟制用户在当前已&lt;/p&gt;
&lt;p&gt;登录的 Web 应用程序上执行非本意的操作的攻击方法。跟跨网站脚本（XSS）相比，&lt;strong&gt;XSS&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;利用的是用户对指定网站的信任，CSRF 利用的是网站对用户网页浏览器的信任。&lt;/p&gt;
&lt;p&gt;跨站请求攻击，简单地说，是攻击者通过一些技术手段欺骗用户的浏览器去访问一个&lt;/p&gt;
&lt;p&gt;自己曾经认证过的网站并运行一些操作（如发邮件，发消息，甚至财产操作如转账和购买&lt;/p&gt;
&lt;p&gt;商品）。由于浏览器曾经认证过，所以被访问的网站会认为是真正的用户操作而去运行。&lt;/p&gt;
&lt;p&gt;这利用了 web 中用户身份验证的一个漏洞：&lt;strong&gt;简单的身份验证只能保证请求发自某个用户的&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;浏览器，却不能保证请求本身是用户自愿发出的&lt;/strong&gt;。&lt;/p&gt;
&lt;p&gt;从 Spring Security 4.0 开始，默认情况下会启用 CSRF 保护，以防止 CSRF 攻击应用&lt;/p&gt;
&lt;p&gt;程序，Spring Security CSRF 会针对 PATCH，POST，PUT 和 DELETE 方法进行防护。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;&lt;strong&gt;案例&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;在登录页面添加一个隐藏域&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&amp;lt;input 
type=&quot;hidden&quot;th:if=&quot;${_csrf}!=null&quot;th:value=&quot;${_csrf.token}&quot;name=&quot;_csrf
&quot;/&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;关闭安全配置的类中的 csrf&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;// http.csrf().disable();
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;微服务权限认证方案&lt;/h2&gt;
&lt;h3&gt;1、&lt;strong&gt;什么是微服务&lt;/strong&gt;&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;1、微服务由来&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;微服务最早由 Martin Fowler 与 James Lewis 于 2014 年共同提出，微服务架构风格是一种&lt;/p&gt;
&lt;p&gt;使用一套小服务来开发单个应用的方式途径，每个服务运行在自己的进程中，并使用轻量&lt;/p&gt;
&lt;p&gt;级机制通信，通常是 HTTP API，这些服务基于业务能力构建，并能够通过自动化部署机制&lt;/p&gt;
&lt;p&gt;来独立部署，这些服务使用不同的编程语言实现，以及不同数据存储技术，并保持最低限&lt;/p&gt;
&lt;p&gt;度的集中式管理。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;2、微服务优势&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;（1）微服务每个模块就相当于一个单独的项目，代码量明显减少，遇到问题也相对来说比&lt;/p&gt;
&lt;p&gt;较好解决。&lt;/p&gt;
&lt;p&gt;（2）微服务每个模块都可以使用不同的存储方式（比如有的用 redis，有的用 mysql&lt;/p&gt;
&lt;p&gt;等），数据库也是单个模块对应自己的数据库。&lt;/p&gt;
&lt;p&gt;（3）微服务每个模块都可以使用不同的开发技术，开发模式更灵活。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;3、微服务本质&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;（1）微服务，关键其实不仅仅是微服务本身，而是系统要提供一套基础的架构，这种架构&lt;/p&gt;
&lt;p&gt;使得微服务可以独立的部署、运行、升级，不仅如此，这个系统架构还让微服务与微服务&lt;/p&gt;
&lt;p&gt;之间在结构上“松耦合”，而在功能上则表现为一个统一的整体。这种所谓的“统一的整&lt;/p&gt;
&lt;p&gt;体”表现出来的是统一风格的界面，统一的权限管理，统一的安全策略，统一的上线过&lt;/p&gt;
&lt;p&gt;程，统一的日志和审计方法，统一的调度方式，统一的访问入口等等。&lt;/p&gt;
&lt;p&gt;（2）微服务的目的是有效的拆分应用，实现敏捷开发和部署。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://tangtangit.com/_astro/image-20210316221642026.WElaURZS_1Wr4zd.webp&quot; alt=&quot;image-20210316221642026&quot; /&gt;&lt;/p&gt;
&lt;h3&gt;2、&lt;strong&gt;微服务认证与授权实现思路&lt;/strong&gt;&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;1&lt;/strong&gt;**、认证授权过程分析**&lt;/p&gt;
&lt;p&gt;（1）如果是基于 Session，那么 Spring-security 会对 cookie 里的 sessionid 进行解析，找&lt;/p&gt;
&lt;p&gt;到服务器存储的 session 信息，然后判断当前用户是否符合请求的要求。&lt;/p&gt;
&lt;p&gt;（2）如果是 token，则是解析出 token，然后将当前请求加入到 Spring-security 管理的权限&lt;/p&gt;
&lt;p&gt;信息中去&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://tangtangit.com/_astro/image-20210316221143685.Dgu6Mebs_ZQ8tPl.webp&quot; alt=&quot;image-20210316221143685&quot; /&gt;&lt;/p&gt;
&lt;p&gt;如果系统的模块众多，每个模块都需要进行授权与认证，所以我们选择基于 token 的形式&lt;/p&gt;
&lt;p&gt;进行授权与认证，用户根据用户名密码认证成功，然后获取当前用户角色的一系列权限&lt;/p&gt;
&lt;p&gt;值，并以用户名为 key，权限列表为 value 的形式存入 redis 缓存中，根据用户名相关信息&lt;/p&gt;
&lt;p&gt;生成 token 返回，浏览器将 token 记录到 cookie 中，每次调用 api 接口都默认将 token 携带&lt;/p&gt;
&lt;p&gt;到 header 请求头中，Spring-security 解析 header 头获取 token 信息，解析 token 获取当前&lt;/p&gt;
&lt;p&gt;用户名，根据用户名就可以从 redis 中获取权限列表，这样 Spring-security 就能够判断当前&lt;/p&gt;
&lt;p&gt;请求是否有权限访问&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;2&lt;/strong&gt;**、权限管理数据模型**&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://tangtangit.com/_astro/image-20210316221355823.ppXWbbxk_JDGkp.webp&quot; alt=&quot;image-20210316221355823&quot; /&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://tangtangit.com/_astro/image-20210316222024967.DoqKDS8i_ZCAYFY.webp&quot; alt=&quot;image-20210316222024967&quot; /&gt;&lt;/p&gt;
&lt;p&gt;数据库表结构&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;## Host: localhost  (Version 5.7.19)
## Date: 2019-11-18 15:49:15
## Generator: MySQL-Front 6.1  (Build 1.26)


#
## Structure for table &quot;acl_permission&quot;
#

CREATE TABLE `acl_permission` (
  `id` char(19) NOT NULL DEFAULT &apos;&apos; COMMENT &apos;编号&apos;,
  `pid` char(19) NOT NULL DEFAULT &apos;&apos; COMMENT &apos;所属上级&apos;,
  `name` varchar(20) NOT NULL DEFAULT &apos;&apos; COMMENT &apos;名称&apos;,
  `type` tinyint(3) NOT NULL DEFAULT &apos;0&apos; COMMENT &apos;类型(1:菜单,2:按钮)&apos;,
  `permission_value` varchar(50) DEFAULT NULL COMMENT &apos;权限值&apos;,
  `path` varchar(100) DEFAULT NULL COMMENT &apos;访问路径&apos;,
  `component` varchar(100) DEFAULT NULL COMMENT &apos;组件路径&apos;,
  `icon` varchar(50) DEFAULT NULL COMMENT &apos;图标&apos;,
  `status` tinyint(4) DEFAULT NULL COMMENT &apos;状态(0:禁止,1:正常)&apos;,
  `is_deleted` tinyint(1) unsigned NOT NULL DEFAULT &apos;0&apos; COMMENT &apos;逻辑删除 1（true）已删除， 0（false）未删除&apos;,
  `gmt_create` datetime DEFAULT NULL COMMENT &apos;创建时间&apos;,
  `gmt_modified` datetime DEFAULT NULL COMMENT &apos;更新时间&apos;,
  PRIMARY KEY (`id`),
  KEY `idx_pid` (`pid`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT=&apos;权限&apos;;

#
## Data for table &quot;acl_permission&quot;
#

INSERT INTO `acl_permission` VALUES (&apos;1&apos;,&apos;0&apos;,&apos;全部数据&apos;,0,NULL,NULL,NULL,NULL,NULL,0,&apos;2019-11-15 17:13:06&apos;,&apos;2019-11-15 17:13:06&apos;),(&apos;1195268474480156673&apos;,&apos;1&apos;,&apos;权限管理&apos;,1,NULL,&apos;/acl&apos;,&apos;Layout&apos;,NULL,NULL,0,&apos;2019-11-15 17:13:06&apos;,&apos;2019-11-18 13:54:25&apos;),(&apos;1195268616021139457&apos;,&apos;1195268474480156673&apos;,&apos;用户管理&apos;,1,NULL,&apos;user/list&apos;,&apos;/acl/user/list&apos;,NULL,NULL,0,&apos;2019-11-15 17:13:40&apos;,&apos;2019-11-18 13:53:12&apos;),(&apos;1195268788138598401&apos;,&apos;1195268474480156673&apos;,&apos;角色管理&apos;,1,NULL,&apos;role/list&apos;,&apos;/acl/role/list&apos;,NULL,NULL,0,&apos;2019-11-15 17:14:21&apos;,&apos;2019-11-15 17:14:21&apos;),(&apos;1195268893830864898&apos;,&apos;1195268474480156673&apos;,&apos;菜单管理&apos;,1,NULL,&apos;menu/list&apos;,&apos;/acl/menu/list&apos;,NULL,NULL,0,&apos;2019-11-15 17:14:46&apos;,&apos;2019-11-15 17:14:46&apos;),(&apos;1195269143060602882&apos;,&apos;1195268616021139457&apos;,&apos;查看&apos;,2,&apos;user.list&apos;,&apos;&apos;,&apos;&apos;,NULL,NULL,0,&apos;2019-11-15 17:15:45&apos;,&apos;2019-11-17 21:57:16&apos;),(&apos;1195269295926206466&apos;,&apos;1195268616021139457&apos;,&apos;添加&apos;,2,&apos;user.add&apos;,&apos;user/add&apos;,&apos;/acl/user/form&apos;,NULL,NULL,0,&apos;2019-11-15 17:16:22&apos;,&apos;2019-11-15 17:16:22&apos;),(&apos;1195269473479483394&apos;,&apos;1195268616021139457&apos;,&apos;修改&apos;,2,&apos;user.update&apos;,&apos;user/update/:id&apos;,&apos;/acl/user/form&apos;,NULL,NULL,0,&apos;2019-11-15 17:17:04&apos;,&apos;2019-11-15 17:17:04&apos;),(&apos;1195269547269873666&apos;,&apos;1195268616021139457&apos;,&apos;删除&apos;,2,&apos;user.remove&apos;,&apos;&apos;,&apos;&apos;,NULL,NULL,0,&apos;2019-11-15 17:17:22&apos;,&apos;2019-11-15 17:17:22&apos;),(&apos;1195269821262782465&apos;,&apos;1195268788138598401&apos;,&apos;修改&apos;,2,&apos;role.update&apos;,&apos;role/update/:id&apos;,&apos;/acl/role/form&apos;,NULL,NULL,0,&apos;2019-11-15 17:18:27&apos;,&apos;2019-11-15 17:19:53&apos;),(&apos;1195269903542444034&apos;,&apos;1195268788138598401&apos;,&apos;查看&apos;,2,&apos;role.list&apos;,&apos;&apos;,&apos;&apos;,NULL,NULL,0,&apos;2019-11-15 17:18:47&apos;,&apos;2019-11-15 17:18:47&apos;),(&apos;1195270037005197313&apos;,&apos;1195268788138598401&apos;,&apos;添加&apos;,2,&apos;role.add&apos;,&apos;role/add&apos;,&apos;/acl/role/form&apos;,NULL,NULL,0,&apos;2019-11-15 17:19:19&apos;,&apos;2019-11-18 11:05:42&apos;),(&apos;1195270442602782721&apos;,&apos;1195268788138598401&apos;,&apos;删除&apos;,2,&apos;role.remove&apos;,&apos;&apos;,&apos;&apos;,NULL,NULL,0,&apos;2019-11-15 17:20:55&apos;,&apos;2019-11-15 17:20:55&apos;),(&apos;1195270621548568578&apos;,&apos;1195268788138598401&apos;,&apos;角色权限&apos;,2,&apos;role.acl&apos;,&apos;role/distribution/:id&apos;,&apos;/acl/role/roleForm&apos;,NULL,NULL,0,&apos;2019-11-15 17:21:38&apos;,&apos;2019-11-15 17:21:38&apos;),(&apos;1195270744097742849&apos;,&apos;1195268893830864898&apos;,&apos;查看&apos;,2,&apos;permission.list&apos;,&apos;&apos;,&apos;&apos;,NULL,NULL,0,&apos;2019-11-15 17:22:07&apos;,&apos;2019-11-15 17:22:07&apos;),(&apos;1195270810560684034&apos;,&apos;1195268893830864898&apos;,&apos;添加&apos;,2,&apos;permission.add&apos;,&apos;&apos;,&apos;&apos;,NULL,NULL,0,&apos;2019-11-15 17:22:23&apos;,&apos;2019-11-15 17:22:23&apos;),(&apos;1195270862100291586&apos;,&apos;1195268893830864898&apos;,&apos;修改&apos;,2,&apos;permission.update&apos;,&apos;&apos;,&apos;&apos;,NULL,NULL,0,&apos;2019-11-15 17:22:35&apos;,&apos;2019-11-15 17:22:35&apos;),(&apos;1195270887933009922&apos;,&apos;1195268893830864898&apos;,&apos;删除&apos;,2,&apos;permission.remove&apos;,&apos;&apos;,&apos;&apos;,NULL,NULL,0,&apos;2019-11-15 17:22:41&apos;,&apos;2019-11-15 17:22:41&apos;),(&apos;1195349439240048642&apos;,&apos;1&apos;,&apos;讲师管理&apos;,1,NULL,&apos;/edu/teacher&apos;,&apos;Layout&apos;,NULL,NULL,0,&apos;2019-11-15 22:34:49&apos;,&apos;2019-11-15 22:34:49&apos;),(&apos;1195349699995734017&apos;,&apos;1195349439240048642&apos;,&apos;讲师列表&apos;,1,NULL,&apos;list&apos;,&apos;/edu/teacher/list&apos;,NULL,NULL,0,&apos;2019-11-15 22:35:52&apos;,&apos;2019-11-15 22:35:52&apos;),(&apos;1195349810561781761&apos;,&apos;1195349439240048642&apos;,&apos;添加讲师&apos;,1,NULL,&apos;create&apos;,&apos;/edu/teacher/form&apos;,NULL,NULL,0,&apos;2019-11-15 22:36:18&apos;,&apos;2019-11-15 22:36:18&apos;),(&apos;1195349876252971010&apos;,&apos;1195349810561781761&apos;,&apos;添加&apos;,2,&apos;teacher.add&apos;,&apos;&apos;,&apos;&apos;,NULL,NULL,0,&apos;2019-11-15 22:36:34&apos;,&apos;2019-11-15 22:36:34&apos;),(&apos;1195349979797753857&apos;,&apos;1195349699995734017&apos;,&apos;查看&apos;,2,&apos;teacher.list&apos;,&apos;&apos;,&apos;&apos;,NULL,NULL,0,&apos;2019-11-15 22:36:58&apos;,&apos;2019-11-15 22:36:58&apos;),(&apos;1195350117270261762&apos;,&apos;1195349699995734017&apos;,&apos;修改&apos;,2,&apos;teacher.update&apos;,&apos;edit/:id&apos;,&apos;/edu/teacher/form&apos;,NULL,NULL,0,&apos;2019-11-15 22:37:31&apos;,&apos;2019-11-15 22:37:31&apos;),(&apos;1195350188359520258&apos;,&apos;1195349699995734017&apos;,&apos;删除&apos;,2,&apos;teacher.remove&apos;,&apos;&apos;,&apos;&apos;,NULL,NULL,0,&apos;2019-11-15 22:37:48&apos;,&apos;2019-11-15 22:37:48&apos;),(&apos;1195350299365969922&apos;,&apos;1&apos;,&apos;课程分类&apos;,1,NULL,&apos;/edu/subject&apos;,&apos;Layout&apos;,NULL,NULL,0,&apos;2019-11-15 22:38:15&apos;,&apos;2019-11-15 22:38:15&apos;),(&apos;1195350397751758850&apos;,&apos;1195350299365969922&apos;,&apos;课程分类列表&apos;,1,NULL,&apos;list&apos;,&apos;/edu/subject/list&apos;,NULL,NULL,0,&apos;2019-11-15 22:38:38&apos;,&apos;2019-11-15 22:38:38&apos;),(&apos;1195350500512206850&apos;,&apos;1195350299365969922&apos;,&apos;导入课程分类&apos;,1,NULL,&apos;import&apos;,&apos;/edu/subject/import&apos;,NULL,NULL,0,&apos;2019-11-15 22:39:03&apos;,&apos;2019-11-15 22:39:03&apos;),(&apos;1195350612172967938&apos;,&apos;1195350397751758850&apos;,&apos;查看&apos;,2,&apos;subject.list&apos;,&apos;&apos;,&apos;&apos;,NULL,NULL,0,&apos;2019-11-15 22:39:29&apos;,&apos;2019-11-15 22:39:29&apos;),(&apos;1195350687590748161&apos;,&apos;1195350500512206850&apos;,&apos;导入&apos;,2,&apos;subject.import&apos;,&apos;&apos;,&apos;&apos;,NULL,NULL,0,&apos;2019-11-15 22:39:47&apos;,&apos;2019-11-15 22:39:47&apos;),(&apos;1195350831744782337&apos;,&apos;1&apos;,&apos;课程管理&apos;,1,NULL,&apos;/edu/course&apos;,&apos;Layout&apos;,NULL,NULL,0,&apos;2019-11-15 22:40:21&apos;,&apos;2019-11-15 22:40:21&apos;),(&apos;1195350919074385921&apos;,&apos;1195350831744782337&apos;,&apos;课程列表&apos;,1,NULL,&apos;list&apos;,&apos;/edu/course/list&apos;,NULL,NULL,0,&apos;2019-11-15 22:40:42&apos;,&apos;2019-11-15 22:40:42&apos;),(&apos;1195351020463296513&apos;,&apos;1195350831744782337&apos;,&apos;发布课程&apos;,1,NULL,&apos;info&apos;,&apos;/edu/course/info&apos;,NULL,NULL,0,&apos;2019-11-15 22:41:06&apos;,&apos;2019-11-15 22:41:06&apos;),(&apos;1195351159672246274&apos;,&apos;1195350919074385921&apos;,&apos;完成发布&apos;,2,&apos;course.publish&apos;,&apos;publish/:id&apos;,&apos;/edu/course/publish&apos;,NULL,NULL,0,&apos;2019-11-15 22:41:40&apos;,&apos;2019-11-15 22:44:01&apos;),(&apos;1195351326706208770&apos;,&apos;1195350919074385921&apos;,&apos;编辑课程&apos;,2,&apos;course.update&apos;,&apos;info/:id&apos;,&apos;/edu/course/info&apos;,NULL,NULL,0,&apos;2019-11-15 22:42:19&apos;,&apos;2019-11-15 22:42:19&apos;),(&apos;1195351566221938690&apos;,&apos;1195350919074385921&apos;,&apos;编辑课程大纲&apos;,2,&apos;chapter.update&apos;,&apos;chapter/:id&apos;,&apos;/edu/course/chapter&apos;,NULL,NULL,0,&apos;2019-11-15 22:43:17&apos;,&apos;2019-11-15 22:43:17&apos;),(&apos;1195351862889254913&apos;,&apos;1&apos;,&apos;统计分析&apos;,1,NULL,&apos;/statistics/daily&apos;,&apos;Layout&apos;,NULL,NULL,0,&apos;2019-11-15 22:44:27&apos;,&apos;2019-11-15 22:44:27&apos;),(&apos;1195351968841568257&apos;,&apos;1195351862889254913&apos;,&apos;生成统计&apos;,1,NULL,&apos;create&apos;,&apos;/statistics/daily/create&apos;,NULL,NULL,0,&apos;2019-11-15 22:44:53&apos;,&apos;2019-11-15 22:44:53&apos;),(&apos;1195352054917074946&apos;,&apos;1195351862889254913&apos;,&apos;统计图表&apos;,1,NULL,&apos;chart&apos;,&apos;/statistics/daily/chart&apos;,NULL,NULL,0,&apos;2019-11-15 22:45:13&apos;,&apos;2019-11-15 22:45:13&apos;),(&apos;1195352127734386690&apos;,&apos;1195352054917074946&apos;,&apos;查看&apos;,2,&apos;daily.list&apos;,&apos;&apos;,&apos;&apos;,NULL,NULL,0,&apos;2019-11-15 22:45:30&apos;,&apos;2019-11-15 22:45:30&apos;),(&apos;1195352215768633346&apos;,&apos;1195351968841568257&apos;,&apos;生成&apos;,2,&apos;daily.add&apos;,&apos;&apos;,&apos;&apos;,NULL,NULL,0,&apos;2019-11-15 22:45:51&apos;,&apos;2019-11-15 22:45:51&apos;),(&apos;1195352547621965825&apos;,&apos;1&apos;,&apos;CMS管理&apos;,1,NULL,&apos;/cms&apos;,&apos;Layout&apos;,NULL,NULL,0,&apos;2019-11-15 22:47:11&apos;,&apos;2019-11-18 10:51:46&apos;),(&apos;1195352856645701633&apos;,&apos;1195353513549205505&apos;,&apos;查看&apos;,2,&apos;banner.list&apos;,&apos;&apos;,NULL,NULL,NULL,0,&apos;2019-11-15 22:48:24&apos;,&apos;2019-11-15 22:48:24&apos;),(&apos;1195352909401657346&apos;,&apos;1195353513549205505&apos;,&apos;添加&apos;,2,&apos;banner.add&apos;,&apos;banner/add&apos;,&apos;/cms/banner/form&apos;,NULL,NULL,0,&apos;2019-11-15 22:48:37&apos;,&apos;2019-11-18 10:52:10&apos;),(&apos;1195353051395624961&apos;,&apos;1195353513549205505&apos;,&apos;修改&apos;,2,&apos;banner.update&apos;,&apos;banner/update/:id&apos;,&apos;/cms/banner/form&apos;,NULL,NULL,0,&apos;2019-11-15 22:49:11&apos;,&apos;2019-11-18 10:52:05&apos;),(&apos;1195353513549205505&apos;,&apos;1195352547621965825&apos;,&apos;Bander列表&apos;,1,NULL,&apos;banner/list&apos;,&apos;/cms/banner/list&apos;,NULL,NULL,0,&apos;2019-11-15 22:51:01&apos;,&apos;2019-11-18 10:51:29&apos;),(&apos;1195353672110673921&apos;,&apos;1195353513549205505&apos;,&apos;删除&apos;,2,&apos;banner.remove&apos;,&apos;&apos;,&apos;&apos;,NULL,NULL,0,&apos;2019-11-15 22:51:39&apos;,&apos;2019-11-15 22:51:39&apos;),(&apos;1195354076890370050&apos;,&apos;1&apos;,&apos;订单管理&apos;,1,NULL,&apos;/order&apos;,&apos;Layout&apos;,NULL,NULL,0,&apos;2019-11-15 22:53:15&apos;,&apos;2019-11-15 22:53:15&apos;),(&apos;1195354153482555393&apos;,&apos;1195354076890370050&apos;,&apos;订单列表&apos;,1,NULL,&apos;list&apos;,&apos;/order/list&apos;,NULL,NULL,0,&apos;2019-11-15 22:53:33&apos;,&apos;2019-11-15 22:53:58&apos;),(&apos;1195354315093282817&apos;,&apos;1195354153482555393&apos;,&apos;查看&apos;,2,&apos;order.list&apos;,&apos;&apos;,&apos;&apos;,NULL,NULL,0,&apos;2019-11-15 22:54:12&apos;,&apos;2019-11-15 22:54:12&apos;),(&apos;1196301740985311234&apos;,&apos;1195268616021139457&apos;,&apos;分配角色&apos;,2,&apos;user.assgin&apos;,&apos;user/role/:id&apos;,&apos;/acl/user/roleForm&apos;,NULL,NULL,0,&apos;2019-11-18 13:38:56&apos;,&apos;2019-11-18 13:38:56&apos;);

#
## Structure for table &quot;acl_role&quot;
#

CREATE TABLE `acl_role` (
  `id` char(19) NOT NULL DEFAULT &apos;&apos; COMMENT &apos;角色id&apos;,
  `role_name` varchar(20) NOT NULL DEFAULT &apos;&apos; COMMENT &apos;角色名称&apos;,
  `role_code` varchar(20) DEFAULT NULL COMMENT &apos;角色编码&apos;,
  `remark` varchar(255) DEFAULT NULL COMMENT &apos;备注&apos;,
  `is_deleted` tinyint(1) unsigned NOT NULL DEFAULT &apos;0&apos; COMMENT &apos;逻辑删除 1（true）已删除， 0（false）未删除&apos;,
  `gmt_create` datetime NOT NULL COMMENT &apos;创建时间&apos;,
  `gmt_modified` datetime NOT NULL COMMENT &apos;更新时间&apos;,
  PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;

#
## Data for table &quot;acl_role&quot;
#

INSERT INTO `acl_role` VALUES (&apos;1&apos;,&apos;普通管理员&apos;,NULL,NULL,0,&apos;2019-11-11 13:09:32&apos;,&apos;2019-11-18 10:27:18&apos;),(&apos;1193757683205607426&apos;,&apos;课程管理员&apos;,NULL,NULL,0,&apos;2019-11-11 13:09:45&apos;,&apos;2019-11-18 10:25:44&apos;),(&apos;1196300996034977794&apos;,&apos;test&apos;,NULL,NULL,0,&apos;2019-11-18 13:35:58&apos;,&apos;2019-11-18 13:35:58&apos;);

#
## Structure for table &quot;acl_role_permission&quot;
#

CREATE TABLE `acl_role_permission` (
  `id` char(19) NOT NULL DEFAULT &apos;&apos;,
  `role_id` char(19) NOT NULL DEFAULT &apos;&apos;,
  `permission_id` char(19) NOT NULL DEFAULT &apos;&apos;,
  `is_deleted` tinyint(1) unsigned NOT NULL DEFAULT &apos;0&apos; COMMENT &apos;逻辑删除 1（true）已删除， 0（false）未删除&apos;,
  `gmt_create` datetime NOT NULL COMMENT &apos;创建时间&apos;,
  `gmt_modified` datetime NOT NULL COMMENT &apos;更新时间&apos;,
  PRIMARY KEY (`id`),
  KEY `idx_role_id` (`role_id`),
  KEY `idx_permission_id` (`permission_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8 COMMENT=&apos;角色权限&apos;;

#
## Data for table &quot;acl_role_permission&quot;
#

INSERT INTO `acl_role_permission` VALUES (&apos;1196301979754455041&apos;,&apos;1&apos;,&apos;1&apos;,1,&apos;2019-11-18 13:39:53&apos;,&apos;2019-11-18 13:39:53&apos;),(&apos;1196301979792203778&apos;,&apos;1&apos;,&apos;1195268474480156673&apos;,1,&apos;2019-11-18 13:39:53&apos;,&apos;2019-11-18 13:39:53&apos;),(&apos;1196301979821563906&apos;,&apos;1&apos;,&apos;1195268616021139457&apos;,1,&apos;2019-11-18 13:39:53&apos;,&apos;2019-11-18 13:39:53&apos;),(&apos;1196301979842535426&apos;,&apos;1&apos;,&apos;1195269143060602882&apos;,1,&apos;2019-11-18 13:39:53&apos;,&apos;2019-11-18 13:39:53&apos;),(&apos;1196301979855118338&apos;,&apos;1&apos;,&apos;1195269295926206466&apos;,1,&apos;2019-11-18 13:39:53&apos;,&apos;2019-11-18 13:39:53&apos;),(&apos;1196301979880284161&apos;,&apos;1&apos;,&apos;1195269473479483394&apos;,1,&apos;2019-11-18 13:39:53&apos;,&apos;2019-11-18 13:39:53&apos;),(&apos;1196301979913838593&apos;,&apos;1&apos;,&apos;1195269547269873666&apos;,1,&apos;2019-11-18 13:39:53&apos;,&apos;2019-11-18 13:39:53&apos;),(&apos;1196301979926421506&apos;,&apos;1&apos;,&apos;1196301740985311234&apos;,1,&apos;2019-11-18 13:39:53&apos;,&apos;2019-11-18 13:39:53&apos;),(&apos;1196301979951587330&apos;,&apos;1&apos;,&apos;1195268788138598401&apos;,1,&apos;2019-11-18 13:39:53&apos;,&apos;2019-11-18 13:39:53&apos;),(&apos;1196301980014501889&apos;,&apos;1&apos;,&apos;1195269821262782465&apos;,1,&apos;2019-11-18 13:39:53&apos;,&apos;2019-11-18 13:39:53&apos;),(&apos;1196301980035473410&apos;,&apos;1&apos;,&apos;1195269903542444034&apos;,1,&apos;2019-11-18 13:39:53&apos;,&apos;2019-11-18 13:39:53&apos;),(&apos;1196301980052250626&apos;,&apos;1&apos;,&apos;1195270037005197313&apos;,1,&apos;2019-11-18 13:39:53&apos;,&apos;2019-11-18 13:39:53&apos;),(&apos;1196301980077416450&apos;,&apos;1&apos;,&apos;1195270442602782721&apos;,1,&apos;2019-11-18 13:39:53&apos;,&apos;2019-11-18 13:39:53&apos;),(&apos;1196301980094193665&apos;,&apos;1&apos;,&apos;1195270621548568578&apos;,1,&apos;2019-11-18 13:39:53&apos;,&apos;2019-11-18 13:39:53&apos;),(&apos;1196301980119359489&apos;,&apos;1&apos;,&apos;1195268893830864898&apos;,1,&apos;2019-11-18 13:39:53&apos;,&apos;2019-11-18 13:39:53&apos;),(&apos;1196301980136136706&apos;,&apos;1&apos;,&apos;1195270744097742849&apos;,1,&apos;2019-11-18 13:39:53&apos;,&apos;2019-11-18 13:39:53&apos;),(&apos;1196301980249382913&apos;,&apos;1&apos;,&apos;1195270810560684034&apos;,1,&apos;2019-11-18 13:39:53&apos;,&apos;2019-11-18 13:39:53&apos;),(&apos;1196301980270354434&apos;,&apos;1&apos;,&apos;1195270862100291586&apos;,1,&apos;2019-11-18 13:39:53&apos;,&apos;2019-11-18 13:39:53&apos;),(&apos;1196301980287131649&apos;,&apos;1&apos;,&apos;1195270887933009922&apos;,1,&apos;2019-11-18 13:39:53&apos;,&apos;2019-11-18 13:39:53&apos;),(&apos;1196301980303908866&apos;,&apos;1&apos;,&apos;1195349439240048642&apos;,1,&apos;2019-11-18 13:39:53&apos;,&apos;2019-11-18 13:39:53&apos;),(&apos;1196301980320686082&apos;,&apos;1&apos;,&apos;1195349699995734017&apos;,1,&apos;2019-11-18 13:39:53&apos;,&apos;2019-11-18 13:39:53&apos;),(&apos;1196301980345851905&apos;,&apos;1&apos;,&apos;1195349979797753857&apos;,1,&apos;2019-11-18 13:39:53&apos;,&apos;2019-11-18 13:39:53&apos;),(&apos;1196301980362629121&apos;,&apos;1&apos;,&apos;1195350117270261762&apos;,1,&apos;2019-11-18 13:39:53&apos;,&apos;2019-11-18 13:39:53&apos;),(&apos;1196301980383600641&apos;,&apos;1&apos;,&apos;1195350188359520258&apos;,1,&apos;2019-11-18 13:39:53&apos;,&apos;2019-11-18 13:39:53&apos;),(&apos;1196301980408766465&apos;,&apos;1&apos;,&apos;1195349810561781761&apos;,1,&apos;2019-11-18 13:39:53&apos;,&apos;2019-11-18 13:39:53&apos;),(&apos;1196301980421349378&apos;,&apos;1&apos;,&apos;1195349876252971010&apos;,1,&apos;2019-11-18 13:39:53&apos;,&apos;2019-11-18 13:39:53&apos;),(&apos;1196301980438126593&apos;,&apos;1&apos;,&apos;1195350299365969922&apos;,1,&apos;2019-11-18 13:39:53&apos;,&apos;2019-11-18 13:39:53&apos;),(&apos;1196301980450709506&apos;,&apos;1&apos;,&apos;1195350397751758850&apos;,1,&apos;2019-11-18 13:39:53&apos;,&apos;2019-11-18 13:39:53&apos;),(&apos;1196301980501041153&apos;,&apos;1&apos;,&apos;1195350612172967938&apos;,1,&apos;2019-11-18 13:39:53&apos;,&apos;2019-11-18 13:39:53&apos;),(&apos;1196301980517818370&apos;,&apos;1&apos;,&apos;1195350500512206850&apos;,1,&apos;2019-11-18 13:39:53&apos;,&apos;2019-11-18 13:39:53&apos;),(&apos;1196301980538789889&apos;,&apos;1&apos;,&apos;1195350687590748161&apos;,1,&apos;2019-11-18 13:39:53&apos;,&apos;2019-11-18 13:39:53&apos;),(&apos;1196301980622675970&apos;,&apos;1&apos;,&apos;1195350831744782337&apos;,1,&apos;2019-11-18 13:39:53&apos;,&apos;2019-11-18 13:39:53&apos;),(&apos;1196301980639453186&apos;,&apos;1&apos;,&apos;1195350919074385921&apos;,1,&apos;2019-11-18 13:39:53&apos;,&apos;2019-11-18 13:39:53&apos;),(&apos;1196301980660424705&apos;,&apos;1&apos;,&apos;1195351159672246274&apos;,1,&apos;2019-11-18 13:39:53&apos;,&apos;2019-11-18 13:39:53&apos;),(&apos;1196301980677201922&apos;,&apos;1&apos;,&apos;1195351326706208770&apos;,1,&apos;2019-11-18 13:39:53&apos;,&apos;2019-11-18 13:39:53&apos;),(&apos;1196301980698173441&apos;,&apos;1&apos;,&apos;1195351566221938690&apos;,1,&apos;2019-11-18 13:39:53&apos;,&apos;2019-11-18 13:39:53&apos;),(&apos;1196301980714950658&apos;,&apos;1&apos;,&apos;1195351020463296513&apos;,1,&apos;2019-11-18 13:39:53&apos;,&apos;2019-11-18 13:39:53&apos;),(&apos;1196301980723339266&apos;,&apos;1&apos;,&apos;1195351862889254913&apos;,1,&apos;2019-11-18 13:39:53&apos;,&apos;2019-11-18 13:39:53&apos;),(&apos;1196301980744310786&apos;,&apos;1&apos;,&apos;1195351968841568257&apos;,1,&apos;2019-11-18 13:39:53&apos;,&apos;2019-11-18 13:39:53&apos;),(&apos;1196301980761088001&apos;,&apos;1&apos;,&apos;1195352215768633346&apos;,1,&apos;2019-11-18 13:39:53&apos;,&apos;2019-11-18 13:39:53&apos;),(&apos;1196301980777865217&apos;,&apos;1&apos;,&apos;1195352054917074946&apos;,1,&apos;2019-11-18 13:39:53&apos;,&apos;2019-11-18 13:39:53&apos;),(&apos;1196301980794642434&apos;,&apos;1&apos;,&apos;1195352127734386690&apos;,1,&apos;2019-11-18 13:39:53&apos;,&apos;2019-11-18 13:39:53&apos;),(&apos;1196301980811419650&apos;,&apos;1&apos;,&apos;1195352547621965825&apos;,1,&apos;2019-11-18 13:39:53&apos;,&apos;2019-11-18 13:39:53&apos;),(&apos;1196301980828196865&apos;,&apos;1&apos;,&apos;1195353513549205505&apos;,1,&apos;2019-11-18 13:39:53&apos;,&apos;2019-11-18 13:39:53&apos;),(&apos;1196301980844974082&apos;,&apos;1&apos;,&apos;1195352856645701633&apos;,1,&apos;2019-11-18 13:39:53&apos;,&apos;2019-11-18 13:39:53&apos;),(&apos;1196301980861751298&apos;,&apos;1&apos;,&apos;1195352909401657346&apos;,1,&apos;2019-11-18 13:39:53&apos;,&apos;2019-11-18 13:39:53&apos;),(&apos;1196301980886917122&apos;,&apos;1&apos;,&apos;1195353051395624961&apos;,1,&apos;2019-11-18 13:39:53&apos;,&apos;2019-11-18 13:39:53&apos;),(&apos;1196301980928860162&apos;,&apos;1&apos;,&apos;1195353672110673921&apos;,1,&apos;2019-11-18 13:39:53&apos;,&apos;2019-11-18 13:39:53&apos;),(&apos;1196301980954025986&apos;,&apos;1&apos;,&apos;1195354076890370050&apos;,1,&apos;2019-11-18 13:39:53&apos;,&apos;2019-11-18 13:39:53&apos;),(&apos;1196301980970803201&apos;,&apos;1&apos;,&apos;1195354153482555393&apos;,1,&apos;2019-11-18 13:39:53&apos;,&apos;2019-11-18 13:39:53&apos;),(&apos;1196301980987580418&apos;,&apos;1&apos;,&apos;1195354315093282817&apos;,1,&apos;2019-11-18 13:39:53&apos;,&apos;2019-11-18 13:39:53&apos;),(&apos;1196305293070077953&apos;,&apos;1&apos;,&apos;1&apos;,1,&apos;2019-11-18 13:53:03&apos;,&apos;2019-11-18 13:53:03&apos;),(&apos;1196305293099438081&apos;,&apos;1&apos;,&apos;1195268474480156673&apos;,1,&apos;2019-11-18 13:53:03&apos;,&apos;2019-11-18 13:53:03&apos;),(&apos;1196305293120409602&apos;,&apos;1&apos;,&apos;1195268616021139457&apos;,1,&apos;2019-11-18 13:53:03&apos;,&apos;2019-11-18 13:53:03&apos;),(&apos;1196305293153964034&apos;,&apos;1&apos;,&apos;1195269143060602882&apos;,1,&apos;2019-11-18 13:53:03&apos;,&apos;2019-11-18 13:53:03&apos;),(&apos;1196305293183324162&apos;,&apos;1&apos;,&apos;1195269295926206466&apos;,1,&apos;2019-11-18 13:53:03&apos;,&apos;2019-11-18 13:53:03&apos;),(&apos;1196305293212684290&apos;,&apos;1&apos;,&apos;1195269473479483394&apos;,1,&apos;2019-11-18 13:53:03&apos;,&apos;2019-11-18 13:53:03&apos;),(&apos;1196305293237850114&apos;,&apos;1&apos;,&apos;1195269547269873666&apos;,1,&apos;2019-11-18 13:53:03&apos;,&apos;2019-11-18 13:53:03&apos;),(&apos;1196305293271404545&apos;,&apos;1&apos;,&apos;1196301740985311234&apos;,1,&apos;2019-11-18 13:53:03&apos;,&apos;2019-11-18 13:53:03&apos;),(&apos;1196305293485314049&apos;,&apos;1&apos;,&apos;1195268788138598401&apos;,1,&apos;2019-11-18 13:53:03&apos;,&apos;2019-11-18 13:53:03&apos;),(&apos;1196305293506285569&apos;,&apos;1&apos;,&apos;1195269821262782465&apos;,1,&apos;2019-11-18 13:53:03&apos;,&apos;2019-11-18 13:53:03&apos;),(&apos;1196305293527257089&apos;,&apos;1&apos;,&apos;1195269903542444034&apos;,1,&apos;2019-11-18 13:53:03&apos;,&apos;2019-11-18 13:53:03&apos;),(&apos;1196305293552422914&apos;,&apos;1&apos;,&apos;1195270037005197313&apos;,1,&apos;2019-11-18 13:53:03&apos;,&apos;2019-11-18 13:53:03&apos;),(&apos;1196305293565005825&apos;,&apos;1&apos;,&apos;1195270442602782721&apos;,1,&apos;2019-11-18 13:53:03&apos;,&apos;2019-11-18 13:53:03&apos;),(&apos;1196305293594365954&apos;,&apos;1&apos;,&apos;1195270621548568578&apos;,1,&apos;2019-11-18 13:53:03&apos;,&apos;2019-11-18 13:53:03&apos;),(&apos;1196305293611143169&apos;,&apos;1&apos;,&apos;1195268893830864898&apos;,1,&apos;2019-11-18 13:53:03&apos;,&apos;2019-11-18 13:53:03&apos;),(&apos;1196305293627920385&apos;,&apos;1&apos;,&apos;1195270744097742849&apos;,1,&apos;2019-11-18 13:53:03&apos;,&apos;2019-11-18 13:53:03&apos;),(&apos;1196305293657280513&apos;,&apos;1&apos;,&apos;1195349439240048642&apos;,1,&apos;2019-11-18 13:53:03&apos;,&apos;2019-11-18 13:53:03&apos;),(&apos;1196305293674057729&apos;,&apos;1&apos;,&apos;1195349699995734017&apos;,1,&apos;2019-11-18 13:53:03&apos;,&apos;2019-11-18 13:53:03&apos;),(&apos;1196305293690834946&apos;,&apos;1&apos;,&apos;1195349979797753857&apos;,1,&apos;2019-11-18 13:53:03&apos;,&apos;2019-11-18 13:53:03&apos;),(&apos;1196305293716000770&apos;,&apos;1&apos;,&apos;1195350117270261762&apos;,1,&apos;2019-11-18 13:53:03&apos;,&apos;2019-11-18 13:53:03&apos;),(&apos;1196305293736972290&apos;,&apos;1&apos;,&apos;1195350188359520258&apos;,1,&apos;2019-11-18 13:53:03&apos;,&apos;2019-11-18 13:53:03&apos;),(&apos;1196305293749555202&apos;,&apos;1&apos;,&apos;1195349810561781761&apos;,1,&apos;2019-11-18 13:53:03&apos;,&apos;2019-11-18 13:53:03&apos;),(&apos;1196305293766332417&apos;,&apos;1&apos;,&apos;1195349876252971010&apos;,1,&apos;2019-11-18 13:53:03&apos;,&apos;2019-11-18 13:53:03&apos;),(&apos;1196305293795692546&apos;,&apos;1&apos;,&apos;1195350299365969922&apos;,1,&apos;2019-11-18 13:53:03&apos;,&apos;2019-11-18 13:53:03&apos;),(&apos;1196305293812469762&apos;,&apos;1&apos;,&apos;1195350397751758850&apos;,1,&apos;2019-11-18 13:53:03&apos;,&apos;2019-11-18 13:53:03&apos;),(&apos;1196305293837635586&apos;,&apos;1&apos;,&apos;1195350612172967938&apos;,1,&apos;2019-11-18 13:53:03&apos;,&apos;2019-11-18 13:53:03&apos;),(&apos;1196305293858607106&apos;,&apos;1&apos;,&apos;1195350500512206850&apos;,1,&apos;2019-11-18 13:53:03&apos;,&apos;2019-11-18 13:53:03&apos;),(&apos;1196305293875384322&apos;,&apos;1&apos;,&apos;1195350687590748161&apos;,1,&apos;2019-11-18 13:53:03&apos;,&apos;2019-11-18 13:53:03&apos;),(&apos;1196305293892161538&apos;,&apos;1&apos;,&apos;1195350831744782337&apos;,1,&apos;2019-11-18 13:53:03&apos;,&apos;2019-11-18 13:53:03&apos;),(&apos;1196305293950881794&apos;,&apos;1&apos;,&apos;1195350919074385921&apos;,1,&apos;2019-11-18 13:53:03&apos;,&apos;2019-11-18 13:53:03&apos;),(&apos;1196305293976047617&apos;,&apos;1&apos;,&apos;1195351159672246274&apos;,1,&apos;2019-11-18 13:53:03&apos;,&apos;2019-11-18 13:53:03&apos;),(&apos;1196305294127042561&apos;,&apos;1&apos;,&apos;1195351326706208770&apos;,1,&apos;2019-11-18 13:53:03&apos;,&apos;2019-11-18 13:53:03&apos;),(&apos;1196305294156402690&apos;,&apos;1&apos;,&apos;1195351566221938690&apos;,1,&apos;2019-11-18 13:53:03&apos;,&apos;2019-11-18 13:53:03&apos;),(&apos;1196305294177374209&apos;,&apos;1&apos;,&apos;1195351862889254913&apos;,1,&apos;2019-11-18 13:53:03&apos;,&apos;2019-11-18 13:53:03&apos;),(&apos;1196305294194151425&apos;,&apos;1&apos;,&apos;1195351968841568257&apos;,1,&apos;2019-11-18 13:53:03&apos;,&apos;2019-11-18 13:53:03&apos;),(&apos;1196305294223511554&apos;,&apos;1&apos;,&apos;1195352215768633346&apos;,1,&apos;2019-11-18 13:53:03&apos;,&apos;2019-11-18 13:53:03&apos;),(&apos;1196305294240288770&apos;,&apos;1&apos;,&apos;1195352054917074946&apos;,1,&apos;2019-11-18 13:53:03&apos;,&apos;2019-11-18 13:53:03&apos;),(&apos;1196305294248677377&apos;,&apos;1&apos;,&apos;1195352127734386690&apos;,1,&apos;2019-11-18 13:53:03&apos;,&apos;2019-11-18 13:53:03&apos;),(&apos;1196305294248677378&apos;,&apos;1&apos;,&apos;1195352547621965825&apos;,1,&apos;2019-11-18 13:53:03&apos;,&apos;2019-11-18 13:53:03&apos;),(&apos;1196305294319980546&apos;,&apos;1&apos;,&apos;1195353513549205505&apos;,1,&apos;2019-11-18 13:53:03&apos;,&apos;2019-11-18 13:53:03&apos;),(&apos;1196305294319980547&apos;,&apos;1&apos;,&apos;1195352856645701633&apos;,1,&apos;2019-11-18 13:53:03&apos;,&apos;2019-11-18 13:53:03&apos;),(&apos;1196305294319980548&apos;,&apos;1&apos;,&apos;1195352909401657346&apos;,1,&apos;2019-11-18 13:53:03&apos;,&apos;2019-11-18 13:53:03&apos;),(&apos;1196305294378700802&apos;,&apos;1&apos;,&apos;1195353051395624961&apos;,1,&apos;2019-11-18 13:53:03&apos;,&apos;2019-11-18 13:53:03&apos;),(&apos;1196305294378700803&apos;,&apos;1&apos;,&apos;1195353672110673921&apos;,1,&apos;2019-11-18 13:53:03&apos;,&apos;2019-11-18 13:53:03&apos;),(&apos;1196305294458392577&apos;,&apos;1&apos;,&apos;1195354076890370050&apos;,1,&apos;2019-11-18 13:53:03&apos;,&apos;2019-11-18 13:53:03&apos;),(&apos;1196305294483558402&apos;,&apos;1&apos;,&apos;1195354153482555393&apos;,1,&apos;2019-11-18 13:53:03&apos;,&apos;2019-11-18 13:53:03&apos;),(&apos;1196305294500335618&apos;,&apos;1&apos;,&apos;1195354315093282817&apos;,1,&apos;2019-11-18 13:53:03&apos;,&apos;2019-11-18 13:53:03&apos;),(&apos;1196305566656139266&apos;,&apos;1&apos;,&apos;1&apos;,1,&apos;2019-11-18 13:54:08&apos;,&apos;2019-11-18 13:54:08&apos;),(&apos;1196305566689693698&apos;,&apos;1&apos;,&apos;1195268474480156673&apos;,1,&apos;2019-11-18 13:54:08&apos;,&apos;2019-11-18 13:54:08&apos;),(&apos;1196305566706470913&apos;,&apos;1&apos;,&apos;1195268616021139457&apos;,1,&apos;2019-11-18 13:54:08&apos;,&apos;2019-11-18 13:54:08&apos;),(&apos;1196305566740025346&apos;,&apos;1&apos;,&apos;1195269143060602882&apos;,1,&apos;2019-11-18 13:54:08&apos;,&apos;2019-11-18 13:54:08&apos;),(&apos;1196305566756802561&apos;,&apos;1&apos;,&apos;1195269295926206466&apos;,1,&apos;2019-11-18 13:54:08&apos;,&apos;2019-11-18 13:54:08&apos;),(&apos;1196305566781968385&apos;,&apos;1&apos;,&apos;1195269473479483394&apos;,1,&apos;2019-11-18 13:54:08&apos;,&apos;2019-11-18 13:54:08&apos;),(&apos;1196305566811328514&apos;,&apos;1&apos;,&apos;1195269547269873666&apos;,1,&apos;2019-11-18 13:54:08&apos;,&apos;2019-11-18 13:54:08&apos;),(&apos;1196305566828105730&apos;,&apos;1&apos;,&apos;1196301740985311234&apos;,1,&apos;2019-11-18 13:54:08&apos;,&apos;2019-11-18 13:54:08&apos;),(&apos;1196305566853271554&apos;,&apos;1&apos;,&apos;1195268788138598401&apos;,1,&apos;2019-11-18 13:54:08&apos;,&apos;2019-11-18 13:54:08&apos;),(&apos;1196305566878437378&apos;,&apos;1&apos;,&apos;1195269821262782465&apos;,1,&apos;2019-11-18 13:54:08&apos;,&apos;2019-11-18 13:54:08&apos;),(&apos;1196305566895214593&apos;,&apos;1&apos;,&apos;1195269903542444034&apos;,1,&apos;2019-11-18 13:54:08&apos;,&apos;2019-11-18 13:54:08&apos;),(&apos;1196305566916186113&apos;,&apos;1&apos;,&apos;1195270037005197313&apos;,1,&apos;2019-11-18 13:54:08&apos;,&apos;2019-11-18 13:54:08&apos;),(&apos;1196305566949740546&apos;,&apos;1&apos;,&apos;1195270442602782721&apos;,1,&apos;2019-11-18 13:54:08&apos;,&apos;2019-11-18 13:54:08&apos;),(&apos;1196305566966517761&apos;,&apos;1&apos;,&apos;1195270621548568578&apos;,1,&apos;2019-11-18 13:54:08&apos;,&apos;2019-11-18 13:54:08&apos;),(&apos;1196305566991683585&apos;,&apos;1&apos;,&apos;1195268893830864898&apos;,1,&apos;2019-11-18 13:54:08&apos;,&apos;2019-11-18 13:54:08&apos;),(&apos;1196305567012655106&apos;,&apos;1&apos;,&apos;1195270744097742849&apos;,1,&apos;2019-11-18 13:54:08&apos;,&apos;2019-11-18 13:54:08&apos;),(&apos;1196305567029432322&apos;,&apos;1&apos;,&apos;1195270810560684034&apos;,1,&apos;2019-11-18 13:54:08&apos;,&apos;2019-11-18 13:54:08&apos;),(&apos;1196305567042015233&apos;,&apos;1&apos;,&apos;1195270862100291586&apos;,1,&apos;2019-11-18 13:54:08&apos;,&apos;2019-11-18 13:54:08&apos;),(&apos;1196305567100735490&apos;,&apos;1&apos;,&apos;1195270887933009922&apos;,1,&apos;2019-11-18 13:54:08&apos;,&apos;2019-11-18 13:54:08&apos;),(&apos;1196305567117512705&apos;,&apos;1&apos;,&apos;1195349439240048642&apos;,1,&apos;2019-11-18 13:54:08&apos;,&apos;2019-11-18 13:54:08&apos;),(&apos;1196305567142678530&apos;,&apos;1&apos;,&apos;1195349699995734017&apos;,1,&apos;2019-11-18 13:54:08&apos;,&apos;2019-11-18 13:54:08&apos;),(&apos;1196305567155261442&apos;,&apos;1&apos;,&apos;1195349979797753857&apos;,1,&apos;2019-11-18 13:54:08&apos;,&apos;2019-11-18 13:54:08&apos;),(&apos;1196305567172038658&apos;,&apos;1&apos;,&apos;1195350117270261762&apos;,1,&apos;2019-11-18 13:54:08&apos;,&apos;2019-11-18 13:54:08&apos;),(&apos;1196305567188815873&apos;,&apos;1&apos;,&apos;1195350188359520258&apos;,1,&apos;2019-11-18 13:54:08&apos;,&apos;2019-11-18 13:54:08&apos;),(&apos;1196305567218176001&apos;,&apos;1&apos;,&apos;1195349810561781761&apos;,1,&apos;2019-11-18 13:54:08&apos;,&apos;2019-11-18 13:54:08&apos;),(&apos;1196305567234953217&apos;,&apos;1&apos;,&apos;1195349876252971010&apos;,1,&apos;2019-11-18 13:54:08&apos;,&apos;2019-11-18 13:54:08&apos;),(&apos;1196305567251730434&apos;,&apos;1&apos;,&apos;1195350299365969922&apos;,1,&apos;2019-11-18 13:54:08&apos;,&apos;2019-11-18 13:54:08&apos;),(&apos;1196305567272701954&apos;,&apos;1&apos;,&apos;1195350397751758850&apos;,1,&apos;2019-11-18 13:54:08&apos;,&apos;2019-11-18 13:54:08&apos;),(&apos;1196305567289479170&apos;,&apos;1&apos;,&apos;1195350612172967938&apos;,1,&apos;2019-11-18 13:54:08&apos;,&apos;2019-11-18 13:54:08&apos;),(&apos;1196305567310450690&apos;,&apos;1&apos;,&apos;1195350500512206850&apos;,1,&apos;2019-11-18 13:54:08&apos;,&apos;2019-11-18 13:54:08&apos;),(&apos;1196305567327227905&apos;,&apos;1&apos;,&apos;1195350687590748161&apos;,1,&apos;2019-11-18 13:54:08&apos;,&apos;2019-11-18 13:54:08&apos;),(&apos;1196305567448862722&apos;,&apos;1&apos;,&apos;1195350831744782337&apos;,1,&apos;2019-11-18 13:54:08&apos;,&apos;2019-11-18 13:54:08&apos;),(&apos;1196305567478222850&apos;,&apos;1&apos;,&apos;1195350919074385921&apos;,1,&apos;2019-11-18 13:54:08&apos;,&apos;2019-11-18 13:54:08&apos;),(&apos;1196305567495000065&apos;,&apos;1&apos;,&apos;1195351159672246274&apos;,1,&apos;2019-11-18 13:54:08&apos;,&apos;2019-11-18 13:54:08&apos;),(&apos;1196305567520165889&apos;,&apos;1&apos;,&apos;1195351326706208770&apos;,1,&apos;2019-11-18 13:54:08&apos;,&apos;2019-11-18 13:54:08&apos;),(&apos;1196305567541137409&apos;,&apos;1&apos;,&apos;1195351566221938690&apos;,1,&apos;2019-11-18 13:54:08&apos;,&apos;2019-11-18 13:54:08&apos;),(&apos;1196305567570497538&apos;,&apos;1&apos;,&apos;1195351862889254913&apos;,1,&apos;2019-11-18 13:54:08&apos;,&apos;2019-11-18 13:54:08&apos;),(&apos;1196305567587274754&apos;,&apos;1&apos;,&apos;1195351968841568257&apos;,1,&apos;2019-11-18 13:54:08&apos;,&apos;2019-11-18 13:54:08&apos;),(&apos;1196305567604051970&apos;,&apos;1&apos;,&apos;1195352215768633346&apos;,1,&apos;2019-11-18 13:54:08&apos;,&apos;2019-11-18 13:54:08&apos;),(&apos;1196305567633412098&apos;,&apos;1&apos;,&apos;1195352054917074946&apos;,1,&apos;2019-11-18 13:54:08&apos;,&apos;2019-11-18 13:54:08&apos;),(&apos;1196305567683743745&apos;,&apos;1&apos;,&apos;1195352127734386690&apos;,1,&apos;2019-11-18 13:54:08&apos;,&apos;2019-11-18 13:54:08&apos;),(&apos;1196305567721492481&apos;,&apos;1&apos;,&apos;1195352547621965825&apos;,1,&apos;2019-11-18 13:54:08&apos;,&apos;2019-11-18 13:54:08&apos;),(&apos;1196305567742464002&apos;,&apos;1&apos;,&apos;1195353513549205505&apos;,1,&apos;2019-11-18 13:54:08&apos;,&apos;2019-11-18 13:54:08&apos;),(&apos;1196305567771824129&apos;,&apos;1&apos;,&apos;1195352856645701633&apos;,1,&apos;2019-11-18 13:54:08&apos;,&apos;2019-11-18 13:54:08&apos;),(&apos;1196305567792795650&apos;,&apos;1&apos;,&apos;1195352909401657346&apos;,1,&apos;2019-11-18 13:54:08&apos;,&apos;2019-11-18 13:54:08&apos;),(&apos;1196305567809572866&apos;,&apos;1&apos;,&apos;1195353051395624961&apos;,1,&apos;2019-11-18 13:54:08&apos;,&apos;2019-11-18 13:54:08&apos;),(&apos;1196305567843127298&apos;,&apos;1&apos;,&apos;1195353672110673921&apos;,1,&apos;2019-11-18 13:54:08&apos;,&apos;2019-11-18 13:54:08&apos;),(&apos;1196305567868293122&apos;,&apos;1&apos;,&apos;1195354076890370050&apos;,1,&apos;2019-11-18 13:54:08&apos;,&apos;2019-11-18 13:54:08&apos;),(&apos;1196305567885070338&apos;,&apos;1&apos;,&apos;1195354153482555393&apos;,1,&apos;2019-11-18 13:54:08&apos;,&apos;2019-11-18 13:54:08&apos;),(&apos;1196305567910236162&apos;,&apos;1&apos;,&apos;1195354315093282817&apos;,1,&apos;2019-11-18 13:54:08&apos;,&apos;2019-11-18 13:54:08&apos;),(&apos;1196312702601695234&apos;,&apos;1&apos;,&apos;1&apos;,0,&apos;2019-11-18 14:22:29&apos;,&apos;2019-11-18 14:22:29&apos;),(&apos;1196312702652026881&apos;,&apos;1&apos;,&apos;1195268474480156673&apos;,0,&apos;2019-11-18 14:22:29&apos;,&apos;2019-11-18 14:22:29&apos;),(&apos;1196312702668804098&apos;,&apos;1&apos;,&apos;1195268616021139457&apos;,0,&apos;2019-11-18 14:22:29&apos;,&apos;2019-11-18 14:22:29&apos;),(&apos;1196312702698164226&apos;,&apos;1&apos;,&apos;1195269143060602882&apos;,0,&apos;2019-11-18 14:22:29&apos;,&apos;2019-11-18 14:22:29&apos;),(&apos;1196312702723330049&apos;,&apos;1&apos;,&apos;1195269295926206466&apos;,0,&apos;2019-11-18 14:22:29&apos;,&apos;2019-11-18 14:22:29&apos;),(&apos;1196312702744301569&apos;,&apos;1&apos;,&apos;1195269473479483394&apos;,0,&apos;2019-11-18 14:22:29&apos;,&apos;2019-11-18 14:22:29&apos;),(&apos;1196312702765273089&apos;,&apos;1&apos;,&apos;1195269547269873666&apos;,0,&apos;2019-11-18 14:22:29&apos;,&apos;2019-11-18 14:22:29&apos;),(&apos;1196312702790438913&apos;,&apos;1&apos;,&apos;1196301740985311234&apos;,0,&apos;2019-11-18 14:22:29&apos;,&apos;2019-11-18 14:22:29&apos;),(&apos;1196312702945628161&apos;,&apos;1&apos;,&apos;1195268788138598401&apos;,0,&apos;2019-11-18 14:22:29&apos;,&apos;2019-11-18 14:22:29&apos;),(&apos;1196312702970793985&apos;,&apos;1&apos;,&apos;1195269821262782465&apos;,0,&apos;2019-11-18 14:22:29&apos;,&apos;2019-11-18 14:22:29&apos;),(&apos;1196312703000154114&apos;,&apos;1&apos;,&apos;1195269903542444034&apos;,0,&apos;2019-11-18 14:22:29&apos;,&apos;2019-11-18 14:22:29&apos;),(&apos;1196312703025319938&apos;,&apos;1&apos;,&apos;1195270037005197313&apos;,0,&apos;2019-11-18 14:22:29&apos;,&apos;2019-11-18 14:22:29&apos;),(&apos;1196312703046291458&apos;,&apos;1&apos;,&apos;1195270442602782721&apos;,0,&apos;2019-11-18 14:22:29&apos;,&apos;2019-11-18 14:22:29&apos;),(&apos;1196312703063068673&apos;,&apos;1&apos;,&apos;1195270621548568578&apos;,0,&apos;2019-11-18 14:22:29&apos;,&apos;2019-11-18 14:22:29&apos;),(&apos;1196312703084040193&apos;,&apos;1&apos;,&apos;1195268893830864898&apos;,0,&apos;2019-11-18 14:22:29&apos;,&apos;2019-11-18 14:22:29&apos;),(&apos;1196312703113400321&apos;,&apos;1&apos;,&apos;1195270744097742849&apos;,0,&apos;2019-11-18 14:22:29&apos;,&apos;2019-11-18 14:22:29&apos;),(&apos;1196312703134371842&apos;,&apos;1&apos;,&apos;1195270810560684034&apos;,0,&apos;2019-11-18 14:22:30&apos;,&apos;2019-11-18 14:22:30&apos;),(&apos;1196312703159537665&apos;,&apos;1&apos;,&apos;1195270862100291586&apos;,0,&apos;2019-11-18 14:22:30&apos;,&apos;2019-11-18 14:22:30&apos;),(&apos;1196312703184703490&apos;,&apos;1&apos;,&apos;1195270887933009922&apos;,0,&apos;2019-11-18 14:22:30&apos;,&apos;2019-11-18 14:22:30&apos;),(&apos;1196312703209869313&apos;,&apos;1&apos;,&apos;1195349439240048642&apos;,0,&apos;2019-11-18 14:22:30&apos;,&apos;2019-11-18 14:22:30&apos;),(&apos;1196312703230840834&apos;,&apos;1&apos;,&apos;1195349699995734017&apos;,0,&apos;2019-11-18 14:22:30&apos;,&apos;2019-11-18 14:22:30&apos;),(&apos;1196312703251812354&apos;,&apos;1&apos;,&apos;1195349979797753857&apos;,0,&apos;2019-11-18 14:22:30&apos;,&apos;2019-11-18 14:22:30&apos;),(&apos;1196312703272783873&apos;,&apos;1&apos;,&apos;1195350117270261762&apos;,0,&apos;2019-11-18 14:22:30&apos;,&apos;2019-11-18 14:22:30&apos;),(&apos;1196312703293755394&apos;,&apos;1&apos;,&apos;1195350188359520258&apos;,0,&apos;2019-11-18 14:22:30&apos;,&apos;2019-11-18 14:22:30&apos;),(&apos;1196312703327309826&apos;,&apos;1&apos;,&apos;1195349810561781761&apos;,0,&apos;2019-11-18 14:22:30&apos;,&apos;2019-11-18 14:22:30&apos;),(&apos;1196312703348281345&apos;,&apos;1&apos;,&apos;1195349876252971010&apos;,0,&apos;2019-11-18 14:22:30&apos;,&apos;2019-11-18 14:22:30&apos;),(&apos;1196312703365058561&apos;,&apos;1&apos;,&apos;1195350299365969922&apos;,0,&apos;2019-11-18 14:22:30&apos;,&apos;2019-11-18 14:22:30&apos;),(&apos;1196312703386030082&apos;,&apos;1&apos;,&apos;1195350397751758850&apos;,0,&apos;2019-11-18 14:22:30&apos;,&apos;2019-11-18 14:22:30&apos;),(&apos;1196312703440556034&apos;,&apos;1&apos;,&apos;1195350612172967938&apos;,0,&apos;2019-11-18 14:22:30&apos;,&apos;2019-11-18 14:22:30&apos;),(&apos;1196312703486693378&apos;,&apos;1&apos;,&apos;1195350500512206850&apos;,0,&apos;2019-11-18 14:22:30&apos;,&apos;2019-11-18 14:22:30&apos;),(&apos;1196312703511859202&apos;,&apos;1&apos;,&apos;1195350687590748161&apos;,0,&apos;2019-11-18 14:22:30&apos;,&apos;2019-11-18 14:22:30&apos;),(&apos;1196312703654465537&apos;,&apos;1&apos;,&apos;1195350831744782337&apos;,0,&apos;2019-11-18 14:22:30&apos;,&apos;2019-11-18 14:22:30&apos;),(&apos;1196312703683825665&apos;,&apos;1&apos;,&apos;1195350919074385921&apos;,0,&apos;2019-11-18 14:22:30&apos;,&apos;2019-11-18 14:22:30&apos;),(&apos;1196312703700602882&apos;,&apos;1&apos;,&apos;1195351159672246274&apos;,0,&apos;2019-11-18 14:22:30&apos;,&apos;2019-11-18 14:22:30&apos;),(&apos;1196312703717380098&apos;,&apos;1&apos;,&apos;1195351326706208770&apos;,0,&apos;2019-11-18 14:22:30&apos;,&apos;2019-11-18 14:22:30&apos;),(&apos;1196312703738351618&apos;,&apos;1&apos;,&apos;1195351566221938690&apos;,0,&apos;2019-11-18 14:22:30&apos;,&apos;2019-11-18 14:22:30&apos;),(&apos;1196312703759323137&apos;,&apos;1&apos;,&apos;1195351020463296513&apos;,0,&apos;2019-11-18 14:22:30&apos;,&apos;2019-11-18 14:22:30&apos;),(&apos;1196312703776100353&apos;,&apos;1&apos;,&apos;1195351862889254913&apos;,0,&apos;2019-11-18 14:22:30&apos;,&apos;2019-11-18 14:22:30&apos;),(&apos;1196312703792877570&apos;,&apos;1&apos;,&apos;1195351968841568257&apos;,0,&apos;2019-11-18 14:22:30&apos;,&apos;2019-11-18 14:22:30&apos;),(&apos;1196312703830626305&apos;,&apos;1&apos;,&apos;1195352215768633346&apos;,0,&apos;2019-11-18 14:22:30&apos;,&apos;2019-11-18 14:22:30&apos;),(&apos;1196312703843209217&apos;,&apos;1&apos;,&apos;1195352054917074946&apos;,0,&apos;2019-11-18 14:22:30&apos;,&apos;2019-11-18 14:22:30&apos;),(&apos;1196312703868375041&apos;,&apos;1&apos;,&apos;1195352127734386690&apos;,0,&apos;2019-11-18 14:22:30&apos;,&apos;2019-11-18 14:22:30&apos;),(&apos;1196312703889346561&apos;,&apos;1&apos;,&apos;1195352547621965825&apos;,0,&apos;2019-11-18 14:22:30&apos;,&apos;2019-11-18 14:22:30&apos;),(&apos;1196312703901929473&apos;,&apos;1&apos;,&apos;1195353513549205505&apos;,0,&apos;2019-11-18 14:22:30&apos;,&apos;2019-11-18 14:22:30&apos;),(&apos;1196312703918706689&apos;,&apos;1&apos;,&apos;1195352856645701633&apos;,0,&apos;2019-11-18 14:22:30&apos;,&apos;2019-11-18 14:22:30&apos;),(&apos;1196312703952261121&apos;,&apos;1&apos;,&apos;1195352909401657346&apos;,0,&apos;2019-11-18 14:22:30&apos;,&apos;2019-11-18 14:22:30&apos;),(&apos;1196312703973232642&apos;,&apos;1&apos;,&apos;1195353051395624961&apos;,0,&apos;2019-11-18 14:22:30&apos;,&apos;2019-11-18 14:22:30&apos;),(&apos;1196312703990009857&apos;,&apos;1&apos;,&apos;1195353672110673921&apos;,0,&apos;2019-11-18 14:22:30&apos;,&apos;2019-11-18 14:22:30&apos;),(&apos;1196312704048730114&apos;,&apos;1&apos;,&apos;1195354076890370050&apos;,0,&apos;2019-11-18 14:22:30&apos;,&apos;2019-11-18 14:22:30&apos;),(&apos;1196312704069701633&apos;,&apos;1&apos;,&apos;1195354153482555393&apos;,0,&apos;2019-11-18 14:22:30&apos;,&apos;2019-11-18 14:22:30&apos;),(&apos;1196312704094867457&apos;,&apos;1&apos;,&apos;1195354315093282817&apos;,0,&apos;2019-11-18 14:22:30&apos;,&apos;2019-11-18 14:22:30&apos;);

#
## Structure for table &quot;acl_user&quot;
#

CREATE TABLE `acl_user` (
  `id` char(19) NOT NULL COMMENT &apos;会员id&apos;,
  `username` varchar(20) NOT NULL DEFAULT &apos;&apos; COMMENT &apos;微信openid&apos;,
  `password` varchar(32) NOT NULL DEFAULT &apos;&apos; COMMENT &apos;密码&apos;,
  `nick_name` varchar(50) DEFAULT NULL COMMENT &apos;昵称&apos;,
  `salt` varchar(255) DEFAULT NULL COMMENT &apos;用户头像&apos;,
  `token` varchar(100) DEFAULT NULL COMMENT &apos;用户签名&apos;,
  `is_deleted` tinyint(1) unsigned NOT NULL DEFAULT &apos;0&apos; COMMENT &apos;逻辑删除 1（true）已删除， 0（false）未删除&apos;,
  `gmt_create` datetime NOT NULL COMMENT &apos;创建时间&apos;,
  `gmt_modified` datetime NOT NULL COMMENT &apos;更新时间&apos;,
  PRIMARY KEY (`id`),
  UNIQUE KEY `uk_username` (`username`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT=&apos;用户表&apos;;

#
## Data for table &quot;acl_user&quot;
#

INSERT INTO `acl_user` VALUES (&apos;1&apos;,&apos;admin&apos;,&apos;96e79218965eb72c92a549dd5a330112&apos;,&apos;admin&apos;,&apos;&apos;,NULL,0,&apos;2019-11-01 10:39:47&apos;,&apos;2019-11-01 10:39:47&apos;),(&apos;2&apos;,&apos;test&apos;,&apos;96e79218965eb72c92a549dd5a330112&apos;,&apos;test&apos;,NULL,NULL,0,&apos;2019-11-01 16:36:07&apos;,&apos;2019-11-01 16:40:08&apos;);

#
## Structure for table &quot;acl_user_role&quot;
#

CREATE TABLE `acl_user_role` (
  `id` char(19) NOT NULL DEFAULT &apos;&apos; COMMENT &apos;主键id&apos;,
  `role_id` char(19) NOT NULL DEFAULT &apos;0&apos; COMMENT &apos;角色id&apos;,
  `user_id` char(19) NOT NULL DEFAULT &apos;0&apos; COMMENT &apos;用户id&apos;,
  `is_deleted` tinyint(1) unsigned NOT NULL DEFAULT &apos;0&apos; COMMENT &apos;逻辑删除 1（true）已删除， 0（false）未删除&apos;,
  `gmt_create` datetime NOT NULL COMMENT &apos;创建时间&apos;,
  `gmt_modified` datetime NOT NULL COMMENT &apos;更新时间&apos;,
  PRIMARY KEY (`id`),
  KEY `idx_role_id` (`role_id`),
  KEY `idx_user_id` (`user_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;

#
## Data for table &quot;acl_user_role&quot;
#

INSERT INTO `acl_user_role` VALUES (&apos;1&apos;,&apos;1&apos;,&apos;2&apos;,0,&apos;2019-11-11 13:09:53&apos;,&apos;2019-11-11 13:09:53&apos;);

&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;3、使用架构&lt;/h3&gt;
&lt;p&gt;&lt;img src=&quot;https://tangtangit.com/_astro/image-20210316222333939.DYoeu_p7_Z4u4wO.webp&quot; alt=&quot;image-20210316222333939&quot; /&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://tangtangit.com/_astro/image-20210316222422750.Czh1HcWT_Z2inyCX.webp&quot; alt=&quot;image-20210316222422750&quot; /&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://tangtangit.com/_astro/image-20210316222858034.CzpZMsdz_Z1JgWit.webp&quot; alt=&quot;image-20210316222858034&quot; /&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://tangtangit.com/_astro/image-20210317053544723.H3ZQIunZ_ZMw1JE.webp&quot; alt=&quot;image-20210317053544723&quot; /&gt;&lt;/p&gt;
&lt;h3&gt;注意包扫描&lt;/h3&gt;
&lt;p&gt;&lt;img src=&quot;https://tangtangit.com/_astro/image-20210319080821520.BkQyiYkb_Z2i7zti.webp&quot; alt=&quot;image-20210319080821520&quot; /&gt;&lt;/p&gt;
&lt;h3&gt;4、编写代码&lt;/h3&gt;
&lt;p&gt;1、编写common&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;添加工具类&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;2、security整体规划&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://tangtangit.com/_astro/image-20210317080604726.DJ6yT_zg_237tzb.webp&quot; alt=&quot;image-20210317080604726&quot; /&gt;&lt;/p&gt;
&lt;h4&gt;第一步：&lt;/h4&gt;
&lt;p&gt;&lt;img src=&quot;https://tangtangit.com/_astro/image-20210317080835158.RWoIekR7_2aNtch.webp&quot; alt=&quot;image-20210317080835158&quot; /&gt;&lt;/p&gt;
&lt;h5&gt;1、密码处理&lt;/h5&gt;
&lt;pre&gt;&lt;code&gt;@Component
public class DefaultPasswordEncoder implements PasswordEncoder {

    public DefaultPasswordEncoder(int strength) {
    }

    public DefaultPasswordEncoder() {
        this(-1);
    }

    //进行MD5加密
    @Override
    public String encode(CharSequence rawPassword) {
        return MD5.encrypt(rawPassword.toString());
    }

    //进行密码比对
    @Override
    public boolean matches(CharSequence rawPassword, String encodedPassword) {
        return encodedPassword.equals(MD5.encrypt(rawPassword.toString()));
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h5&gt;2、token操作类&lt;/h5&gt;
&lt;pre&gt;&lt;code&gt;@Component
public class TokenManager {

    //token有效时常
    private long tokeExpiration = 24 * 60 * 60 *1000;

    //编码密钥
    private String tokenSignKey = &quot;123456&quot;;

    //1、根据用户名生产token
    public String creatToken(String userName) {
        String token = Jwts.builder().setSubject(userName)
                .setExpiration(new Date(System.currentTimeMillis() + tokeExpiration))
                .signWith(SignatureAlgorithm.HS512, tokenSignKey).compressWith(CompressionCodecs.GZIP).compact();
        return token;
    }

    //2、根据token字符串获取用户信息
    public String getUserInfoFromToken(String token){
        String userInfo = Jwts.parser().setSigningKey(tokenSignKey).parseClaimsJws(token).getBody().getSubject();
        return userInfo;
    }
    //3、删除token
    public String deleteToken(String token){
       return null; 
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h5&gt;3、退出处理器&lt;/h5&gt;
&lt;pre&gt;&lt;code&gt;public class TokenLogoutHandler implements LogoutHandler {

    private TokenManager tokenManager;

    private RedisTemplate redisTemplate;

    public TokenLogoutHandler(TokenManager tokenManager, RedisTemplate redisTemplate) {
        this.tokenManager = tokenManager;
        this.redisTemplate = redisTemplate;
    }

    @Override
    public void logout(HttpServletRequest request, HttpServletResponse response, Authentication authentication) {
        //1、从header中获取token
        //2、token不为空，移除token，从redis中删除
        String token = request.getHeader(&quot;token&quot;);
        if(token != null){
            //移除
            tokenManager.deleteToken(token);

            //获取用户信息
            String username = tokenManager.getUserInfoFromToken(token);
            redisTemplate.delete(username);
        }
        ResponseUtil.out(response, R.ok());
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h5&gt;4、未认证统一处理类&lt;/h5&gt;
&lt;pre&gt;&lt;code&gt;public class UnauthEntryPoint implements AuthenticationEntryPoint {
    @Override
    public void commence(HttpServletRequest httpServletRequest, HttpServletResponse response, AuthenticationException e) throws IOException, ServletException {
        ResponseUtil.out(response, R.error());
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h5&gt;5、处理无权限访问类&lt;/h5&gt;
&lt;pre&gt;&lt;code&gt;@ControllerAdvice
public class SecurityGlobeException {

    //处理访问拒绝异常
    @ResponseBody
    @ExceptionHandler(AccessDeniedException.class)
    public R handleAccessDeniedException(AccessDeniedException e){
        return R.error().message(&quot;403, 无权限访问该接口&quot;);
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h4&gt;第二步：认证过滤器&lt;/h4&gt;
&lt;p&gt;&lt;img src=&quot;https://tangtangit.com/_astro/image-20210317202117940.PxXj95cN_1XhnpR.webp&quot; alt=&quot;image-20210317202117940&quot; /&gt;&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;认证的过滤器&lt;/p&gt;
&lt;/blockquote&gt;
&lt;pre&gt;&lt;code&gt;public class TokenLoginFilter extends UsernamePasswordAuthenticationFilter {

    private TokenManager tokenManager;
    private RedisTemplate redisTemplate;
    private AuthenticationManager authenticationManager;

    public TokenLoginFilter(TokenManager tokenManager, RedisTemplate redisTemplate, AuthenticationManager authenticationManager) {
        this.tokenManager = tokenManager;
        this.redisTemplate = redisTemplate;
        this.authenticationManager = authenticationManager;
        this.setPostOnly(false);
        //设置登陆路径
        this.setRequiresAuthenticationRequestMatcher(new AntPathRequestMatcher(&quot;/admin/acl/login&quot;, &quot;POST&quot;));
    }

    //1、获取表单提交过来的用户名密码
    @Override
    public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response) throws AuthenticationException {
        //获取表单提交的数据
        try {
            User user = new ObjectMapper().readValue(request.getInputStream(), User.class);
            return authenticationManager.authenticate(new UsernamePasswordAuthenticationToken(user.getUsername(), user.getPassword(),
                    new ArrayList&amp;lt;&amp;gt;()));
        } catch (IOException e) {
            e.printStackTrace();
            throw new RuntimeException();
        }
    }

    //2、认证成功调用的方法
    @Override
    protected void successfulAuthentication(HttpServletRequest request, HttpServletResponse response, FilterChain chain, Authentication authResult) throws IOException, ServletException {
        //获取认证成功之后用户信息
        SecurityUser securityUser = (SecurityUser) authResult.getPrincipal();
        //根据用户名生产token
        String token = tokenManager.creatToken(securityUser.getCurrentUserInfo().getUsername());
        //调用redis存入用户信息，返回token
        redisTemplate.opsForValue().set(securityUser.getCurrentUserInfo().getUsername(),securityUser.getPermissionValueList());
        //返回token
        ResponseUtil.out(response, R.ok().data(&quot;token&quot;,token));
    }

    //3、认证失败调用的方法
    @Override
    protected void unsuccessfulAuthentication(HttpServletRequest request, HttpServletResponse response, AuthenticationException failed) throws IOException, ServletException {
        ResponseUtil.out(response, R.error());
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote&gt;
&lt;p&gt;用户的实体类&lt;/p&gt;
&lt;/blockquote&gt;
&lt;pre&gt;&lt;code&gt;@Data
@ApiModel(description = &quot;用户实体类&quot;)
public class User implements Serializable {
    private String username;
    private String password;
    private String nickName;
    private String salt;
    private String token;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote&gt;
&lt;p&gt;用户权限实体类&lt;/p&gt;
&lt;/blockquote&gt;
&lt;pre&gt;&lt;code&gt;@Data
@Slf4j
public class SecurityUser implements UserDetails {
    //当前登录用户
    private transient User currentUserInfo;
    //当前权限
    private List&amp;lt;String&amp;gt; permissionValueList;

    public SecurityUser() {
    }

    public SecurityUser(User user) {
        if (user != null) {
            this.currentUserInfo = user;
        }
    }

    //设置权限
    @Override
    public Collection&amp;lt;? extends GrantedAuthority&amp;gt; getAuthorities() {
        Collection&amp;lt;GrantedAuthority&amp;gt; authorities = new ArrayList&amp;lt;&amp;gt;();
        for (String permissionValue : permissionValueList) {
            if (StringUtils.isEmpty(permissionValue)){
                continue;
            }
            SimpleGrantedAuthority authority = new
                    SimpleGrantedAuthority(permissionValue);
            authorities.add(authority);
        }
        return authorities;
    }

    @Override
    public String getPassword() {
        return currentUserInfo.getPassword();
    }

    @Override
    public String getUsername() {
        return currentUserInfo.getUsername();
    }

    @Override
    public boolean isAccountNonExpired() {
        return true;
    }

    @Override
    public boolean isAccountNonLocked() {
        return true;
    }

    @Override
    public boolean isCredentialsNonExpired() {
        return true;
    }

    @Override
    public boolean isEnabled() {
        return true;
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h4&gt;第三步：授权过滤器&lt;/h4&gt;
&lt;pre&gt;&lt;code&gt;package com.eim.security.filter;

import com.eim.security.security.TokenManager;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.web.authentication.www.BasicAuthenticationFilter;

import javax.servlet.FilterChain;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.util.Collection;
import java.util.List;
import java.util.stream.Collectors;

public class TokenAuthFilter extends BasicAuthenticationFilter {

    private TokenManager tokenManager;
    private RedisTemplate redisTemplate;

    public TokenAuthFilter(AuthenticationManager authenticationManager) {
        super(authenticationManager);
        this.tokenManager = tokenManager;
        this.redisTemplate = redisTemplate;
    }

    @Override
    protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain chain) throws IOException, ServletException {
        //获取认证成功用户信息
        UsernamePasswordAuthenticationToken authRequest = getAuthentication(request);
        //如果有权限信息放入security的上下文中
        if (authRequest != null) {
            SecurityContextHolder.getContext().setAuthentication(authRequest);
        }
            chain.doFilter(request,response);
    }

    private UsernamePasswordAuthenticationToken getAuthentication(HttpServletRequest request) {
        //从header中获取token
        String token = request.getHeader(&quot;token&quot;);
        if (token != null) {
            //解析token获取用户信息
            String username = tokenManager.getUserInfoFromToken(token);

            //从redis中获取对应权限列表
            List&amp;lt;String&amp;gt; permissionList = (List&amp;lt;String&amp;gt;) redisTemplate.opsForValue().get(username);
            //拼装权限列表
            assert permissionList != null;
            Collection&amp;lt;GrantedAuthority&amp;gt; authorities = permissionList.stream().map(e -&amp;gt; {
                return new SimpleGrantedAuthority(e);
            }).collect(Collectors.toList());

            return new UsernamePasswordAuthenticationToken(username, token, authorities);
        }
        return null;
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h4&gt;第四步：核心配置类&lt;/h4&gt;
&lt;p&gt;&lt;img src=&quot;https://tangtangit.com/_astro/image-20210317211919354.BDs2JO3d_1aTH3v.webp&quot; alt=&quot;image-20210317211919354&quot; /&gt;&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;TokenWebSecurityConfig&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;关闭session管理&lt;/strong&gt;&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;设置无状态模式，防止回话保持再没有token的情况下任然能够访问资源&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;.and().sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS)
&lt;/code&gt;&lt;/pre&gt;
&lt;/blockquote&gt;
&lt;/blockquote&gt;
&lt;pre&gt;&lt;code&gt;package com.eim.security.config;

import com.eim.security.filter.TokenAuthFilter;
import com.eim.security.filter.TokenLoginFilter;
import com.eim.security.security.DefaultPasswordEncoder;
import com.eim.security.security.TokenLogoutHandler;
import com.eim.security.security.TokenManager;
import com.eim.security.security.UnauthEntryPoint;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;
import org.springframework.security.config.annotation.method.configuration.EnableGlobalMethodSecurity;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.builders.WebSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.core.userdetails.UserDetailsService;

@Configuration
@EnableWebSecurity
@EnableGlobalMethodSecurity(prePostEnabled = true)
public class TokenWebSecurityConfig extends WebSecurityConfigurerAdapter {

    private TokenManager tokenManager;
    private RedisTemplate redisTemplate;
    private DefaultPasswordEncoder defaultPasswordEncoder;
    private UserDetailsService userDetailsService;

    @Autowired
    public TokenWebSecurityConfig(TokenManager tokenManager, RedisTemplate redisTemplate, DefaultPasswordEncoder defaultPasswordEncoder, UserDetailsService userDetailsService) {
        this.tokenManager = tokenManager;
        this.redisTemplate = redisTemplate;
        this.defaultPasswordEncoder = defaultPasswordEncoder;
        this.userDetailsService = userDetailsService;
    }

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http.exceptionHandling()
                //无权限时处理类
                .authenticationEntryPoint(new UnauthEntryPoint())
                //关闭session管理,设置成无状态
                .and().sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS)
                .and().csrf().disable()
                //授权请求
                .authorizeRequests()
                //任何请求都需要已认证
                .anyRequest().authenticated()
                //设置推出路径
                .and().logout().logoutUrl(&quot;/admin/acl/index/logout&quot;)
                //推出处理器
                .addLogoutHandler(new TokenLogoutHandler(tokenManager,redisTemplate))
                //认证过滤器
                .and().addFilter(new TokenLoginFilter(tokenManager,redisTemplate,authenticationManager()))
                //授权过滤器
                .addFilter(new TokenAuthFilter(tokenManager,redisTemplate,authenticationManager()))
        ;

    }

    //调用userDetailService和密码处理

    @Override
    protected void configure(AuthenticationManagerBuilder auth) throws Exception {
        auth.userDetailsService(userDetailsService).passwordEncoder(defaultPasswordEncoder);
    }

    //不进行认证的路径,可以直接访问
    @Override
    public void configure(WebSecurity web) throws Exception {
        web.ignoring().antMatchers(&quot;/api/**&quot;);
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h4&gt;第五步：实际项目模块完成UserDetailServiceIpml&lt;/h4&gt;
&lt;pre&gt;&lt;code&gt;@Service
public class UserDetailServiceIpml implements UserDetailsService {

    @Autowired
    private UserService userService;

    @Autowired
    private PermissionService permissionService;

    @Override
    public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
        //根据用户名查询数据库
        User user = userService.selectByUsername(username);
        //判断
        if(user == null){
            throw new UsernameNotFoundException(&quot;用户名不存在&quot;);
        }
        com.eim.security.entity.User curUser = new com.eim.security.entity.User();
        BeanUtils.copyProperties(user,curUser);
        //查询用户权限信息
        List&amp;lt;String&amp;gt; permissionlist = permissionService.selectPermissionValueByUserId(user.getId());
        SecurityUser securityUser = new SecurityUser();
        securityUser.setCurrentUserInfo(curUser);
        securityUser.setPermissionValueList(permissionlist);
        return securityUser;
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;第6步：controller设置权限&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;数据库拥有 permission_value 字段，设置权限值&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;img src=&quot;https://tangtangit.com/_astro/image-20210401124210123.CQ1EQlFv_nhtgB.webp&quot; alt=&quot;image-20210401124210123&quot; /&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;controller设置权限&lt;/li&gt;
&lt;/ul&gt;
&lt;blockquote&gt;
&lt;p&gt;添加@PreAuthorize(&quot;hasAuthority(&apos;user.list&apos;)&quot;)&lt;/p&gt;
&lt;p&gt;剩下交给spring security处理&lt;/p&gt;
&lt;/blockquote&gt;
&lt;pre&gt;&lt;code&gt;@RestController
@RequestMapping(&quot;/admin/acl/role&quot;)
//@CrossOrigin
public class RoleController {

    @Autowired
    private RoleService roleService;

    @PreAuthorize(&quot;hasAuthority(&apos;user.list&apos;)&quot;)
    @ApiOperation(value = &quot;获取角色分页列表&quot;)
    @GetMapping(&quot;{page}/{limit}&quot;)
    public R index(
            @ApiParam(name = &quot;page&quot;, value = &quot;当前页码&quot;, required = true)
            @PathVariable Long page,

            @ApiParam(name = &quot;limit&quot;, value = &quot;每页记录数&quot;, required = true)
            @PathVariable Long limit,
            Role role) {
        Page&amp;lt;Role&amp;gt; pageParam = new Page&amp;lt;&amp;gt;(page, limit);
        QueryWrapper&amp;lt;Role&amp;gt; wrapper = new QueryWrapper&amp;lt;&amp;gt;();
        if(!StringUtils.isEmpty(role.getRoleName())) {
            wrapper.like(&quot;role_name&quot;,role.getRoleName());
        }
        roleService.page(pageParam,wrapper);
        return R.ok().data(&quot;items&quot;, pageParam.getRecords()).data(&quot;total&quot;, pageParam.getTotal());
    }
}    
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;网关跨域&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;package com.eim.gateway.config;

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.cors.CorsConfiguration;
import org.springframework.web.cors.reactive.CorsWebFilter;
import org.springframework.web.cors.reactive.UrlBasedCorsConfigurationSource;
import org.springframework.web.util.pattern.PathPatternParser;


@Configuration
public class CorsConfig {

    //解决跨域问题
    @Bean
    public CorsWebFilter corsWebFilter(){
        CorsConfiguration configuration = new CorsConfiguration();
        configuration.addAllowedHeader(&quot;*&quot;);
        configuration.addAllowedMethod(&quot;*&quot;);
        configuration.addAllowedOrigin(&quot;*&quot;);

        UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource(new PathPatternParser());
        source.registerCorsConfiguration(&quot;/**&quot;,configuration);
        return new CorsWebFilter(source);
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote&gt;
&lt;p&gt;网关配置&lt;/p&gt;
&lt;/blockquote&gt;
&lt;pre&gt;&lt;code&gt;#端口号
server.port=8222
#服务名称
spring.application.name=service-gateway
#nacos地址
spring.cloud.nacos.discovery.server-addr=192.168.31.70:8848
#使用服务发现路由
spring.cloud.gateway.discovery.locator.enabled=true

#配置路由规则
spring.cloud.gateway.routes[0].id=service-acl
#设置路由url  lb://注册服务名称
spring.cloud.gateway.routes[0].uri.=lb://service-acl
#具体路径规则
spring.cloud.gateway.routes[0].predicates = Path=/*/acl/**
&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote&gt;
&lt;p&gt;acl项目配置&lt;/p&gt;
&lt;/blockquote&gt;
&lt;pre&gt;&lt;code&gt;## 服务端口
server.port=8009
## 服务名
spring.application.name=service-acl

## mysql数据库连接
spring.datasource.driver-class-name=com.mysql.cj.jdbc.Driver
spring.datasource.url=jdbc:mysql://192.168.31.70:3307/guli?serverTimezone=GMT%2B8
spring.datasource.username=root
spring.datasource.password=123456

#返回json的全局时间格式
spring.jackson.date-format=yyyy-MM-dd HH:mm:ss
spring.jackson.time-zone=GMT+8

spring.redis.host=192.168.31.70
spring.redis.port=6379
spring.redis.database= 0
#spring.redis.password=123456
spring.redis.timeout=1800000

spring.redis.lettuce.pool.max-active=20
spring.redis.lettuce.pool.max-wait=-1
#最大阻塞等待时间(负数表示没限制)
spring.redis.lettuce.pool.max-idle=5
spring.redis.lettuce.pool.min-idle=0
#最小空闲

#配置mapper xml文件的路径
mybatis-plus.mapper-locations=classpath:com/eim/aclservice/mapper/xml/*.xml

## nacos服务地址
spring.cloud.nacos.discovery.server-addr=192.168.31.70:8848

#mybatis日志
mybatis-plus.configuration.log-impl=org.apache.ibatis.logging.stdout.StdOutImpl
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;security源码分析&lt;/h2&gt;
&lt;p&gt;&lt;strong&gt;Spring Security&lt;/strong&gt; 采取过滤链实现认证与授权，只有当前过滤器通过，才能进入下一个过滤器：&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://tangtangit.com/_astro/image-20210318193405746.C3W1v-uo_Z8FF0q.webp&quot; alt=&quot;image-20210318193405746&quot; /&gt;&lt;/p&gt;
&lt;p&gt;绿色部分是认证过滤器，需要我们自己配置，可以配置多个认证过滤器。认证过滤器可以使用 &lt;strong&gt;Spring Security&lt;/strong&gt; 提供的认证过滤器，也可以自定义过滤器（例如：短信验证）。认证过滤器要在 **configure(HttpSecurity http)**方法中配置，没有配置不生效。下面会重点介绍以下三个过滤器：&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;UsernamePasswordAuthenticationFilter&lt;/strong&gt; 过滤器：该过滤器会拦截前端提交的 POST 方式的登录表单请求，并进行身份认证。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;ExceptionTranslationFilter&lt;/strong&gt; 过滤器：该过滤器不需要我们配置，对于前端提交的请求会直接放行，捕获后续抛出的异常并进行处理（例如：权限访问限制）。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;FilterSecurityInterceptor&lt;/strong&gt; 过滤器：该过滤器是过滤器链的最后一个过滤器，根据资源权限配置来判断当前请求是否有权限访问对应的资源。如果访问受限会抛出相关异常，并由 &lt;strong&gt;ExceptionTranslationFilter&lt;/strong&gt; 过滤器进行捕获和处理&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h3&gt;&lt;strong&gt;SpringSecurity 认证流程&lt;/strong&gt;&lt;/h3&gt;
&lt;p&gt;认证流程是在 &lt;strong&gt;UsernamePasswordAuthenticationFilter&lt;/strong&gt; 过滤器中处理的，具体流程如下所示&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://tangtangit.com/_astro/image-20210318193615614.CfUYlF18_f0MAR.webp&quot; alt=&quot;image-20210318193615614&quot; /&gt;&lt;/p&gt;
&lt;h4&gt;&lt;strong&gt;UsernamePasswordAuthenticationFilter 源码&lt;/strong&gt;&lt;/h4&gt;
&lt;p&gt;当前端提交的是一个 POST 方式的登录表单请求，就会被该过滤器拦截，并进行身份认证。该过滤器的 doFilter() 方法实现在其抽象父类&lt;/p&gt;
&lt;h4&gt;&lt;strong&gt;AbstractAuthenticationProcessingFilter&lt;/strong&gt; &lt;strong&gt;中，&lt;/strong&gt;&lt;/h4&gt;
&lt;p&gt;&lt;strong&gt;查看相关源码：&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://tangtangit.com/_astro/image-20210318193715597.BKi4_4hV_2rXBKJ.webp&quot; alt=&quot;image-20210318193715597&quot; /&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://tangtangit.com/_astro/image-20210318193735997.pVjCaS8C_2dC09V.webp&quot; alt=&quot;image-20210318193735997&quot; /&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://tangtangit.com/_astro/image-20210318193751579.Ce2n3vAW_ZjewbQ.webp&quot; alt=&quot;image-20210318193751579&quot; /&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://tangtangit.com/_astro/image-20210318193803763.BtaGa0aP_Z2g6262.webp&quot; alt=&quot;image-20210318193803763&quot; /&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;&lt;em&gt;*&lt;/em&gt; 上述的 第二 过程调用了 UsernamePasswordAuthenticationFilter 的&lt;/strong&gt;&lt;/p&gt;
&lt;h4&gt;&lt;strong&gt;attemptAuthentication() 方法&lt;/strong&gt;&lt;/h4&gt;
&lt;p&gt;&lt;strong&gt;源码如下&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://tangtangit.com/_astro/image-20210318193840113.3_C4pR6o_Z1j3F9b.webp&quot; alt=&quot;image-20210318193840113&quot; /&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://tangtangit.com/_astro/image-20210318193850261.fPiXAd2Q_OXRMC.webp&quot; alt=&quot;image-20210318193850261&quot; /&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;上述的（3）过程创建的&lt;/strong&gt; &lt;strong&gt;UsernamePasswordAuthenticationToken&lt;/strong&gt; 是&lt;strong&gt;Authentication&lt;/strong&gt; 接口的实现类，该类有两个构造器，一个用于封装前端请求传入的未认**&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;证的用户信息，一个用于封装认证成功后的用户信息：&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://tangtangit.com/_astro/image-20210318194011803.Ni5Xe0TK_ebrvK.webp&quot; alt=&quot;image-20210318194011803&quot; /&gt;&lt;/p&gt;
&lt;h4&gt;&lt;em&gt;&lt;strong&gt;*&lt;/strong&gt;&lt;/em&gt; **Authentication **&lt;/h4&gt;
&lt;p&gt;** 接口的实现类用于存储用户认证信息，查看该接口具体定义：**&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://tangtangit.com/_astro/image-20210318194037138.DSzk1beP_16Ls5n.webp&quot; alt=&quot;image-20210318194037138&quot; /&gt;&lt;/p&gt;
&lt;h4&gt;&lt;strong&gt;ProviderManager&lt;/strong&gt; &lt;strong&gt;源码&lt;/strong&gt;&lt;/h4&gt;
&lt;p&gt;上述过程中，&lt;strong&gt;UsernamePasswordAuthenticationFilter&lt;/strong&gt; 过滤器的attemptAuthentication() 方法的**（&lt;strong&gt;&lt;strong&gt;5&lt;/strong&gt;&lt;/strong&gt;）过程**将未认证的 Authentication 对象传入ProviderManager 类的 authenticate() 方法进行身份认证。&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;ProviderManager 是 AuthenticationManager 接口的实现类，该接口是认证相关的核心接&lt;/p&gt;
&lt;p&gt;口，也是认证的入口。在实际开发中，我们可能有多种不同的认证方式，例如：用户名+&lt;/p&gt;
&lt;p&gt;密码、邮箱+密码、手机号+验证码等，而这些认证方式的入口始终只有一个，那就是&lt;/p&gt;
&lt;p&gt;AuthenticationManager。在该接口的常用实现类 ProviderManager 内部会维护一个&lt;/p&gt;
&lt;p&gt;**List&amp;lt;AuthenticationProvider&amp;gt;**列表，存放多种认证方式，实际上这是委托者模式&lt;/p&gt;
&lt;p&gt;（Delegate）的应用。每种认证方式对应着一个 AuthenticationProvider，&lt;/p&gt;
&lt;p&gt;AuthenticationManager 根据认证方式的不同（根据传入的 Authentication 类型判断）委托&lt;/p&gt;
&lt;p&gt;对应的 AuthenticationProvider 进行用户认证。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;&lt;img src=&quot;https://tangtangit.com/_astro/image-20210318194439562.i93fiLc8_Z2nWclM.webp&quot; alt=&quot;image-20210318194439562&quot; /&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://tangtangit.com/_astro/image-20210318194449518.DXw-aJoy_13V6h8.webp&quot; alt=&quot;image-20210318194449518&quot; /&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://tangtangit.com/_astro/image-20210318194502052.F3GjgMpX_1LnK2j.webp&quot; alt=&quot;image-20210318194502052&quot; /&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://tangtangit.com/_astro/image-20210318194515426.mbd9X7Ti_Z1aYCzL.webp&quot; alt=&quot;image-20210318194515426&quot; /&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;上述认证成功之后的（&lt;strong&gt;&lt;strong&gt;6&lt;/strong&gt;&lt;/strong&gt;）过程，调用&lt;/strong&gt; &lt;strong&gt;CredentialsContainer&lt;/strong&gt; &lt;strong&gt;接口定义的&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;eraseCredentials()&lt;/strong&gt; &lt;strong&gt;方法去除敏感信息。查看&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;UsernamePasswordAuthenticationToken&lt;/strong&gt; &lt;strong&gt;实现的&lt;/strong&gt; &lt;strong&gt;eraseCredentials()&lt;/strong&gt; &lt;strong&gt;方法，该方&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;法实现在其父类中&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://tangtangit.com/_astro/image-20210318194540626.CD3SEGkH_Z1yTEmh.webp&quot; alt=&quot;image-20210318194540626&quot; /&gt;&lt;/p&gt;
&lt;h4&gt;&lt;strong&gt;认证成功&lt;/strong&gt;**/**&lt;strong&gt;失败处理&lt;/strong&gt;&lt;/h4&gt;
&lt;blockquote&gt;
&lt;p&gt;上述过程就是认证流程的最核心部分，接下来重新回到&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;UsernamePasswordAuthenticationFilter&lt;/strong&gt; 过滤器的 doFilter() 方法，查看认证成功/失败的处理：&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;&lt;img src=&quot;https://tangtangit.com/_astro/image-20210318194617528.CFsWB1_P_Z1vNapY.webp&quot; alt=&quot;image-20210318194617528&quot; /&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://tangtangit.com/_astro/image-20210318194631836.CyNjrhMS_1rjiXl.webp&quot; alt=&quot;image-20210318194631836&quot; /&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://tangtangit.com/_astro/image-20210318194654035.D2TBbs2Y_qbsMi.webp&quot; alt=&quot;image-20210318194654035&quot; /&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://tangtangit.com/_astro/image-20210318194705443.DXZSuOra_Z1Ez0SX.webp&quot; alt=&quot;image-20210318194705443&quot; /&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://tangtangit.com/_astro/image-20210318194730044.DNDVEVlN_1mFovq.webp&quot; alt=&quot;image-20210318194730044&quot; /&gt;&lt;/p&gt;
&lt;h3&gt;&lt;strong&gt;SpringSecurity 权限访问流程&lt;/strong&gt;&lt;/h3&gt;
&lt;p&gt;上一个部分通过源码的方式介绍了认证流程，下面介绍权限访问流程，主要是对&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;ExceptionTranslationFilter&lt;/strong&gt; 过滤器和 &lt;strong&gt;FilterSecurityInterceptor&lt;/strong&gt; 过滤器进行介绍。&lt;/p&gt;
&lt;h4&gt;&lt;strong&gt;ExceptionTranslationFilter 过滤器&lt;/strong&gt;&lt;/h4&gt;
&lt;blockquote&gt;
&lt;p&gt;该过滤器是用于处理异常的，不需要我们配置，对于前端提交的请求会直接放行，捕获后&lt;/p&gt;
&lt;p&gt;续抛出的异常并进行处理（例如：权限访问限制）。具体源码如下：&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;&lt;img src=&quot;https://tangtangit.com/_astro/image-20210318194858825.BZktO6-D_Z246v22.webp&quot; alt=&quot;image-20210318194858825&quot; /&gt;&lt;/p&gt;
&lt;h4&gt;&lt;strong&gt;FilterSecurityInterceptor&lt;/strong&gt; &lt;strong&gt;过滤器&lt;/strong&gt;&lt;/h4&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;FilterSecurityInterceptor&lt;/strong&gt; 是过滤器链的最后一个过滤器，该过滤器是过滤器链&lt;/p&gt;
&lt;p&gt;的最后一个过滤器，根据资源权限配置来判断当前请求是否有权限访问对应的资源。如果&lt;/p&gt;
&lt;p&gt;访问受限会抛出相关异常，最终所抛出的异常会由前一个过滤器&lt;strong&gt;ExceptionTranslationFilter&lt;/strong&gt; 进行捕获和处理。具体源码如下：&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;&lt;img src=&quot;https://tangtangit.com/_astro/image-20210318194945592.PhRdVRZ8_1K3o5l.webp&quot; alt=&quot;image-20210318194945592&quot; /&gt;&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;需要注意，&lt;strong&gt;Spring Security&lt;/strong&gt; 的过滤器链是配置在 SpringMVC 的核心组件&lt;/p&gt;
&lt;p&gt;DispatcherServlet 运行之前。也就是说，请求通过 &lt;strong&gt;Spring Security&lt;/strong&gt; 的所有过滤器，&lt;/p&gt;
&lt;p&gt;不意味着能够正常访问资源，该请求还需要通过 SpringMVC 的拦截器链。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h3&gt;&lt;strong&gt;SpringSecurity 请求间共享认证信息&lt;/strong&gt;&lt;/h3&gt;
&lt;blockquote&gt;
&lt;p&gt;一般认证成功后的用户信息是通过 Session 在多个请求之间共享，那么 &lt;strong&gt;Spring&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Security&lt;/strong&gt; 中是如何实现将已认证的用户信息对象 Authentication 与 Session 绑定的进行&lt;/p&gt;
&lt;p&gt;具体分析&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;&lt;img src=&quot;https://tangtangit.com/_astro/image-20210318195100765.C88V-Lwp_EKujM.webp&quot; alt=&quot;image-20210318195100765&quot; /&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;在前面讲解认证成功的处理方法 successfulAuthentication() 时，有以下代码：&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;img src=&quot;https://tangtangit.com/_astro/image-20210318195123131.RtJ0vLV6_3wLDX.webp&quot; alt=&quot;image-20210318195123131&quot; /&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;查 看 SecurityContext 接 口 及 其 实 现 类 SecurityContextImpl ， 该 类 其 实 就 是 对Authentication 的封装：&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;查 看 SecurityContextHolder 类 ， 该 类 其 实 是 对 ThreadLocal 的 封 装 ， 存 储SecurityContext 对象：&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;img src=&quot;https://tangtangit.com/_astro/image-20210318195220359.Bcyy1fUu_Aa6HH.webp&quot; alt=&quot;image-20210318195220359&quot; /&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://tangtangit.com/_astro/image-20210318195228598.C3aUkwsD_1maTXx.webp&quot; alt=&quot;image-20210318195228598&quot; /&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://tangtangit.com/_astro/image-20210318195237012.DF_vXGGa_ZHAF4.webp&quot; alt=&quot;image-20210318195237012&quot; /&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://tangtangit.com/_astro/image-20210318195246784.B0HJzEq4_ZiaXVM.webp&quot; alt=&quot;image-20210318195246784&quot; /&gt;&lt;/p&gt;
&lt;h4&gt;&lt;strong&gt;SecurityContextPersistenceFilter 过滤器&lt;/strong&gt;&lt;/h4&gt;
&lt;blockquote&gt;
&lt;p&gt;前面提到过，在 &lt;strong&gt;UsernamePasswordAuthenticationFilter&lt;/strong&gt; 过滤器认证成功之&lt;/p&gt;
&lt;p&gt;后，会在认证成功的处理方法中将已认证的用户信息对象 Authentication 封装进&lt;/p&gt;
&lt;p&gt;SecurityContext，并存入 SecurityContextHolder。&lt;/p&gt;
&lt;p&gt;之后，响应会通过 &lt;strong&gt;SecurityContextPersistenceFilter&lt;/strong&gt; 过滤器，该过滤器的位置&lt;/p&gt;
&lt;p&gt;在所有过滤器的最前面，请求到来先进它，响应返回最后一个通过它，所以在该过滤器中&lt;/p&gt;
&lt;p&gt;处理已认证的用户信息对象 Authentication 与 Session 绑定。&lt;/p&gt;
&lt;p&gt;认证成功的响应通过 &lt;strong&gt;SecurityContextPersistenceFilter&lt;/strong&gt; 过滤器时，会从&lt;/p&gt;
&lt;p&gt;SecurityContextHolder 中取出封装了已认证用户信息对象 Authentication 的&lt;/p&gt;
&lt;p&gt;SecurityContext，放进 Session 中。当请求再次到来时，请求首先经过该过滤器，该过滤&lt;/p&gt;
&lt;p&gt;器会判断当前请求的 Session 是否存有 SecurityContext 对象，如果有则将该对象取出再次&lt;/p&gt;
&lt;p&gt;放入 SecurityContextHolder 中，之后该请求所在的线程获得认证用户信息，后续的资源访&lt;/p&gt;
&lt;p&gt;问不需要进行身份认证；当响应再次返回时，该过滤器同样从 SecurityContextHolder 取出&lt;/p&gt;
&lt;p&gt;SecurityContext 对象，放入 Session 中。具体源码如下：&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;&lt;img src=&quot;https://tangtangit.com/_astro/image-20210318195338434.Ev4jutth_Z1KVsb3.webp&quot; alt=&quot;image-20210318195338434&quot; /&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://tangtangit.com/_astro/image-20210318195353409.CWC85qy9_2vuf4S.webp&quot; alt=&quot;image-20210318195353409&quot; /&gt;&lt;/p&gt;
&lt;h2&gt;常见异常&lt;/h2&gt;
&lt;h3&gt;spring security中配置了AccessDeniedHandler没有生效问题&lt;/h3&gt;
&lt;p&gt;出现情况的配置：&lt;/p&gt;
&lt;p&gt;在接口中增加了注解：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;@PreAuthorize(&quot;hasRole(&apos;ROLE_AAA&apos;)&quot;)
@RequestMapping(&quot;/hello0&quot;)
public String hello(){
    return &quot;HELLO&quot;;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;在WebSecurityConfigurerAdapter配置了代码：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;http.exceptionHandling()
                .authenticationEntryPoint(this.securityAuthenticateFailureHandler)
                .accessDeniedHandler(this.securityAccessDeniedHandler);

http.cors().and()
                // 访问所有的URL都需要登录
                .anyRequest().authenticated();
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;访问接口:/hello0, 报无法访问错误，没有执行设置的accessDeniedHandler&lt;/p&gt;
&lt;p&gt;原因：@PreAuthorize 注解的异常，抛出AccessDeniedException异常，不会被accessDeniedHandler捕获，而是会被全局异常捕获。&lt;/p&gt;
&lt;p&gt;示例代码：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;@RestControllerAdvice
public class GlobalExceptionHandler {

    // 会执行该方法
    @ExceptionHandler(AccessDeniedException.class)
    public BaseResponse handleAccessDeniedException(AccessDeniedException e){
        return BaseResponse.failure(403, &quot;无权限访问该接口&quot;);
    }

}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;如果需要被accessDeniedHandler捕获处理，则需要代码这么写：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;http.cors().and()
                .authorizeRequests().antMatchers(&quot;/hello0&quot;).permitAll()

                注意hasRole ，如果出现异常，
                会调用设置的accessDeniedHandler方法，

                .antMatchers(&quot;/hello0&quot;).hasRole(&quot;AAA&quot;)

                .anyRequest().authenticated();
&lt;/code&gt;&lt;/pre&gt;
&lt;hr /&gt;
&lt;h2&gt;SpringSecurity整合版v3&lt;/h2&gt;
&lt;h2&gt;6、oauth2&lt;/h2&gt;
&lt;p&gt;OAauth2.0包括以下角色：&lt;/p&gt;
&lt;p&gt;1、客户端&lt;/p&gt;
&lt;p&gt;本身不存储资源，需要通过资源拥有者的授权去请求资源服务器的资源，比如：Android客户端、Web客户端（浏&lt;/p&gt;
&lt;p&gt;览器端）、微信客户端等。&lt;/p&gt;
&lt;p&gt;2、资源拥有者&lt;/p&gt;
&lt;p&gt;通常为用户，也可以是应用程序，即该资源的拥有者。&lt;/p&gt;
&lt;p&gt;3、授权服务器（也称认证服务器）用于服务提供商对资源拥有的身份进行认证、对访问资源进行授权，认证成功后会给客户端发放令牌&lt;/p&gt;
&lt;p&gt;（access_token），作为客户端访问资源服务器的凭据。本例为微信的认证服务器。&lt;/p&gt;
&lt;p&gt;4、资源服务器&lt;/p&gt;
&lt;p&gt;存储资源的服务器，本例子为微信存储的用户信息。&lt;/p&gt;
&lt;p&gt;现在还有一个问题，服务提供商能允许随便一个&lt;strong&gt;客户端&lt;/strong&gt;就接入到它的&lt;strong&gt;授权服务器&lt;/strong&gt;吗？答案是否定的，服务提供商会&lt;/p&gt;
&lt;p&gt;给准入的接入方一个身份，用于接入时的凭据:&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;client_id&lt;/strong&gt;：客户端标识 &lt;strong&gt;client_secret&lt;/strong&gt;：客户端秘钥&lt;/p&gt;
&lt;p&gt;因此，准确来说，&lt;strong&gt;授权服务器&lt;/strong&gt;对两种OAuth2.0中的两个角色进行认证授权，分别是&lt;strong&gt;资源拥有者&lt;/strong&gt;、&lt;strong&gt;客户端&lt;/strong&gt;。&lt;/p&gt;
&lt;h3&gt;&lt;strong&gt;6.2 Spring Cloud Security OAuth2&lt;/strong&gt;&lt;/h3&gt;
&lt;h4&gt;&lt;strong&gt;6.2.1&lt;/strong&gt; &lt;strong&gt;环境介绍&lt;/strong&gt;&lt;/h4&gt;
&lt;p&gt;Spring-Security-OAuth2是对OAuth2的一种实现，并且跟我们之前学习的Spring Security相辅相成，与Spring&lt;/p&gt;
&lt;p&gt;Cloud体系的集成也非常便利，接下来，我们需要对它进行学习，最终使用它来实现我们设计的分布式认证授权解&lt;/p&gt;
&lt;p&gt;决方案。&lt;/p&gt;
&lt;p&gt;OAuth2.0的服务提供方涵盖两个服务，即授权服务 (Authorization Server，也叫认证服务) 和资源服务 (Resource&lt;/p&gt;
&lt;p&gt;Server)，使用 Spring Security OAuth2 的时候你可以选择把它们在同一个应用程序中实现，也可以选择建立使用&lt;/p&gt;
&lt;p&gt;同一个授权服务的多个资源服务。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;授权服务&lt;/strong&gt; &lt;strong&gt;(Authorization Server&lt;/strong&gt;**）**应包含对接入端以及登入用户的合法性进行验证并颁发token等功能，对令牌&lt;/p&gt;
&lt;p&gt;的请求端点由 Spring MVC 控制器进行实现，下面是配置一个认证服务必须要实现的endpoints：&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;AuthorizationEndpoint&lt;/strong&gt; 服务于认证请求。默认 URL： /oauth/authorize 。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;TokenEndpoint&lt;/strong&gt; 服务于访问令牌的请求。默认 URL： /oauth/token 。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;资源服务&lt;/strong&gt; &lt;strong&gt;(Resource Server)&lt;/strong&gt;，应包含对资源的保护功能，对非法请求进行拦截，对请求中token进行解析鉴&lt;/p&gt;
&lt;p&gt;权等，下面的过滤器用于实现 OAuth 2.0 资源服务：&lt;/p&gt;
&lt;p&gt;OAuth2AuthenticationProcessingFilter用来对请求给出的身份令牌解析鉴权。&lt;/p&gt;
&lt;p&gt;本教程分别创建uaa授权服务（也可叫认证服务）和order订单资源服务。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://tangtangit.com/_astro/image-20210411124622331.Du-j7qBR_149rtc.webp&quot; alt=&quot;image-20210411124622331&quot; /&gt;&lt;/p&gt;
&lt;p&gt;认证流程如下：&lt;/p&gt;
&lt;p&gt;1、客户端请求UAA授权服务进行认证。&lt;/p&gt;
&lt;p&gt;2、认证通过后由UAA颁发令牌。&lt;/p&gt;
&lt;p&gt;3、客户端携带令牌Token请求资源服务。&lt;/p&gt;
&lt;p&gt;4、资源服务校验令牌的合法性，合法即返回资源信息。&lt;/p&gt;
&lt;h4&gt;&lt;strong&gt;6.2.2&lt;/strong&gt; &lt;strong&gt;环境搭建&lt;/strong&gt;&lt;/h4&gt;
&lt;h4&gt;&lt;strong&gt;6.2.2.1&lt;/strong&gt; &lt;strong&gt;父工程&lt;/strong&gt;&lt;/h4&gt;
&lt;p&gt;创建maven工程作为父工程，依赖如下：&lt;/p&gt;
&lt;blockquote&gt;
&lt;pre&gt;&lt;code&gt;            &amp;lt;!--    security oauth2    --&amp;gt;
            &amp;lt;!--      此版本不能过高，过高之后直接某些方法报错      --&amp;gt;
            &amp;lt;dependency&amp;gt;
                &amp;lt;groupId&amp;gt;org.springframework.security.oauth.boot&amp;lt;/groupId&amp;gt;
                &amp;lt;artifactId&amp;gt;spring-security-oauth2-autoconfigure&amp;lt;/artifactId&amp;gt;
                &amp;lt;version&amp;gt;2.1.3.RELEASE&amp;lt;/version&amp;gt;
            &amp;lt;/dependency&amp;gt;
            &amp;lt;!--    security oauth2    --&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;/blockquote&gt;
&lt;p&gt;完整&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&amp;lt;?xml version=&quot;1.0&quot; encoding=&quot;UTF-8&quot;?&amp;gt;
&amp;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;&amp;gt;
    &amp;lt;modelVersion&amp;gt;4.0.0&amp;lt;/modelVersion&amp;gt;
    &amp;lt;modules&amp;gt;
        &amp;lt;module&amp;gt;service&amp;lt;/module&amp;gt;
        &amp;lt;module&amp;gt;common&amp;lt;/module&amp;gt;
    &amp;lt;/modules&amp;gt;
    &amp;lt;parent&amp;gt;
        &amp;lt;groupId&amp;gt;org.springframework.boot&amp;lt;/groupId&amp;gt;
        &amp;lt;artifactId&amp;gt;spring-boot-starter-parent&amp;lt;/artifactId&amp;gt;
        &amp;lt;version&amp;gt;2.2.1.RELEASE&amp;lt;/version&amp;gt;
        &amp;lt;relativePath/&amp;gt; &amp;lt;!-- lookup parent from repository --&amp;gt;
    &amp;lt;/parent&amp;gt;
    &amp;lt;groupId&amp;gt;com.eim&amp;lt;/groupId&amp;gt;
    &amp;lt;artifactId&amp;gt;guli_parent&amp;lt;/artifactId&amp;gt;
    &amp;lt;packaging&amp;gt;pom&amp;lt;/packaging&amp;gt;
    &amp;lt;version&amp;gt;0.0.1-SNAPSHOT&amp;lt;/version&amp;gt;
    &amp;lt;name&amp;gt;guli_parent&amp;lt;/name&amp;gt;
    &amp;lt;description&amp;gt;父工程&amp;lt;/description&amp;gt;


    &amp;lt;properties&amp;gt;
        &amp;lt;java.version&amp;gt;1.8&amp;lt;/java.version&amp;gt;
        &amp;lt;guli.version&amp;gt;0.0.1-SNAPSHOT&amp;lt;/guli.version&amp;gt;
        &amp;lt;mybatis-plus.version&amp;gt;3.4.2&amp;lt;/mybatis-plus.version&amp;gt;
        &amp;lt;mybatis-plus-plugin.version&amp;gt;3.4.0&amp;lt;/mybatis-plus-plugin.version&amp;gt;
        &amp;lt;velocity.version&amp;gt;2.3&amp;lt;/velocity.version&amp;gt;
        &amp;lt;swagger.version&amp;gt;2.7.0&amp;lt;/swagger.version&amp;gt;
        &amp;lt;swagger-ui.version&amp;gt;3.0.2&amp;lt;/swagger-ui.version&amp;gt;
        &amp;lt;commons.lang.version&amp;gt;2.6&amp;lt;/commons.lang.version&amp;gt;
        &amp;lt;aliyun.oss.version&amp;gt;2.8.3&amp;lt;/aliyun.oss.version&amp;gt;
        &amp;lt;jodatime.version&amp;gt;2.10.1&amp;lt;/jodatime.version&amp;gt;
        &amp;lt;poi.version&amp;gt;3.17&amp;lt;/poi.version&amp;gt;
        &amp;lt;commons-fileupload.version&amp;gt;1.3.1&amp;lt;/commons-fileupload.version&amp;gt;
        &amp;lt;commons-io.version&amp;gt;2.6&amp;lt;/commons-io.version&amp;gt;
        &amp;lt;httpclient.version&amp;gt;4.5.1&amp;lt;/httpclient.version&amp;gt;
        &amp;lt;jwt.version&amp;gt;0.7.0&amp;lt;/jwt.version&amp;gt;
        &amp;lt;aliyun-java-sdk-core.version&amp;gt;4.3.3&amp;lt;/aliyun-java-sdk-core.version&amp;gt;
        &amp;lt;aliyun-sdk-oss.version&amp;gt;3.1.0&amp;lt;/aliyun-sdk-oss.version&amp;gt;
        &amp;lt;aliyun-java-sdk-vod.version&amp;gt;2.15.2&amp;lt;/aliyun-java-sdk-vod.version&amp;gt;
        &amp;lt;aliyun-java-vod-upload.version&amp;gt;1.4.11&amp;lt;/aliyun-java-vod-upload.version&amp;gt;
        &amp;lt;aliyun-sdk-vod-upload.version&amp;gt;1.4.11&amp;lt;/aliyun-sdk-vod-upload.version&amp;gt;
        &amp;lt;fastjson.version&amp;gt;1.2.28&amp;lt;/fastjson.version&amp;gt;
        &amp;lt;gson.version&amp;gt;2.8.2&amp;lt;/gson.version&amp;gt;
        &amp;lt;json.version&amp;gt;20170516&amp;lt;/json.version&amp;gt;
        &amp;lt;commons-dbutils.version&amp;gt;1.7&amp;lt;/commons-dbutils.version&amp;gt;
        &amp;lt;canal.client.version&amp;gt;1.1.0&amp;lt;/canal.client.version&amp;gt;
        &amp;lt;docker.image.prefix&amp;gt;zx&amp;lt;/docker.image.prefix&amp;gt;
        &amp;lt;spring-cloud.version&amp;gt;Hoxton.RELEASE&amp;lt;/spring-cloud.version&amp;gt;
        &amp;lt;spring-cloud-ali.version&amp;gt;2.2.0.RELEASE&amp;lt;/spring-cloud-ali.version&amp;gt;
    &amp;lt;/properties&amp;gt;

    &amp;lt;dependencyManagement&amp;gt;
        &amp;lt;dependencies&amp;gt;
            &amp;lt;!--    security oauth2    --&amp;gt;
            &amp;lt;!--      此版本不能过高，过高之后直接某些方法报错      --&amp;gt;
            &amp;lt;dependency&amp;gt;
                &amp;lt;groupId&amp;gt;org.springframework.security.oauth.boot&amp;lt;/groupId&amp;gt;
                &amp;lt;artifactId&amp;gt;spring-security-oauth2-autoconfigure&amp;lt;/artifactId&amp;gt;
                &amp;lt;version&amp;gt;2.1.3.RELEASE&amp;lt;/version&amp;gt;
            &amp;lt;/dependency&amp;gt;
            &amp;lt;!--    security oauth2    --&amp;gt;

            &amp;lt;!--Spring Cloud--&amp;gt;
            &amp;lt;dependency&amp;gt;
                &amp;lt;groupId&amp;gt;org.springframework.cloud&amp;lt;/groupId&amp;gt;
                &amp;lt;artifactId&amp;gt;spring-cloud-dependencies&amp;lt;/artifactId&amp;gt;
                &amp;lt;version&amp;gt;${spring-cloud.version}&amp;lt;/version&amp;gt;
                &amp;lt;exclusions&amp;gt;
                    &amp;lt;exclusion&amp;gt;
                        &amp;lt;groupId&amp;gt;org.springframework.cloud&amp;lt;/groupId&amp;gt;
                        &amp;lt;artifactId&amp;gt;spring-cloud-security-dependencies&amp;lt;/artifactId&amp;gt;
                    &amp;lt;/exclusion&amp;gt;
                &amp;lt;/exclusions&amp;gt;
                &amp;lt;type&amp;gt;pom&amp;lt;/type&amp;gt;
                &amp;lt;scope&amp;gt;import&amp;lt;/scope&amp;gt;
            &amp;lt;/dependency&amp;gt;

            &amp;lt;dependency&amp;gt;
                &amp;lt;groupId&amp;gt;com.alibaba.cloud&amp;lt;/groupId&amp;gt;
                &amp;lt;artifactId&amp;gt;spring-cloud-alibaba-dependencies&amp;lt;/artifactId&amp;gt;
                &amp;lt;version&amp;gt;${spring-cloud-ali.version}&amp;lt;/version&amp;gt;
                &amp;lt;type&amp;gt;pom&amp;lt;/type&amp;gt;
                &amp;lt;scope&amp;gt;import&amp;lt;/scope&amp;gt;
            &amp;lt;/dependency&amp;gt;
            &amp;lt;!--mybatis-plus 持久层--&amp;gt;
            &amp;lt;dependency&amp;gt;
                &amp;lt;groupId&amp;gt;com.baomidou&amp;lt;/groupId&amp;gt;
                &amp;lt;artifactId&amp;gt;mybatis-plus-boot-starter&amp;lt;/artifactId&amp;gt;
                &amp;lt;version&amp;gt;${mybatis-plus.version}&amp;lt;/version&amp;gt;
            &amp;lt;/dependency&amp;gt;

            &amp;lt;!-- velocity 模板引擎, Mybatis Plus 代码生成器需要 --&amp;gt;
            &amp;lt;dependency&amp;gt;
                &amp;lt;groupId&amp;gt;com.baomidou&amp;lt;/groupId&amp;gt;
                &amp;lt;artifactId&amp;gt;mybatis-plus-generator&amp;lt;/artifactId&amp;gt;
                &amp;lt;version&amp;gt;${mybatis-plus-plugin.version}&amp;lt;/version&amp;gt;
            &amp;lt;/dependency&amp;gt;
            &amp;lt;dependency&amp;gt;
                &amp;lt;groupId&amp;gt;org.apache.velocity&amp;lt;/groupId&amp;gt;
                &amp;lt;artifactId&amp;gt;velocity-engine-core&amp;lt;/artifactId&amp;gt;
                &amp;lt;version&amp;gt;${velocity.version}&amp;lt;/version&amp;gt;
            &amp;lt;/dependency&amp;gt;

            &amp;lt;!--swagger--&amp;gt;
            &amp;lt;dependency&amp;gt;
                &amp;lt;groupId&amp;gt;io.springfox&amp;lt;/groupId&amp;gt;
                &amp;lt;artifactId&amp;gt;springfox-swagger2&amp;lt;/artifactId&amp;gt;
                &amp;lt;version&amp;gt;${swagger.version}&amp;lt;/version&amp;gt;
            &amp;lt;/dependency&amp;gt;
            &amp;lt;!--swagger ui--&amp;gt;
            &amp;lt;dependency&amp;gt;
                &amp;lt;groupId&amp;gt;io.springfox&amp;lt;/groupId&amp;gt;
                &amp;lt;artifactId&amp;gt;springfox-swagger-ui&amp;lt;/artifactId&amp;gt;
                &amp;lt;version&amp;gt;${swagger.version}&amp;lt;/version&amp;gt;
            &amp;lt;/dependency&amp;gt;
            &amp;lt;!--增强swagger-bootstrap-ui--&amp;gt;
            &amp;lt;dependency&amp;gt;
                &amp;lt;groupId&amp;gt;com.github.xiaoymin&amp;lt;/groupId&amp;gt;
                &amp;lt;artifactId&amp;gt;knife4j-spring-ui&amp;lt;/artifactId&amp;gt;
                &amp;lt;version&amp;gt;${swagger-ui.version}&amp;lt;/version&amp;gt;
            &amp;lt;/dependency&amp;gt;

            &amp;lt;!--日期时间工具--&amp;gt;
            &amp;lt;dependency&amp;gt;
                &amp;lt;groupId&amp;gt;joda-time&amp;lt;/groupId&amp;gt;
                &amp;lt;artifactId&amp;gt;joda-time&amp;lt;/artifactId&amp;gt;
                &amp;lt;version&amp;gt;${jodatime.version}&amp;lt;/version&amp;gt;
            &amp;lt;/dependency&amp;gt;

            &amp;lt;dependency&amp;gt;
                &amp;lt;groupId&amp;gt;commons-lang&amp;lt;/groupId&amp;gt;
                &amp;lt;artifactId&amp;gt;commons-lang&amp;lt;/artifactId&amp;gt;
                &amp;lt;version&amp;gt;${commons.lang.version}&amp;lt;/version&amp;gt;
            &amp;lt;/dependency&amp;gt;

            &amp;lt;!--xls--&amp;gt;
            &amp;lt;dependency&amp;gt;
                &amp;lt;groupId&amp;gt;org.apache.poi&amp;lt;/groupId&amp;gt;
                &amp;lt;artifactId&amp;gt;poi&amp;lt;/artifactId&amp;gt;
                &amp;lt;version&amp;gt;${poi.version}&amp;lt;/version&amp;gt;
            &amp;lt;/dependency&amp;gt;
            &amp;lt;!--xlsx--&amp;gt;
            &amp;lt;dependency&amp;gt;
                &amp;lt;groupId&amp;gt;org.apache.poi&amp;lt;/groupId&amp;gt;
                &amp;lt;artifactId&amp;gt;poi-ooxml&amp;lt;/artifactId&amp;gt;
                &amp;lt;version&amp;gt;${poi.version}&amp;lt;/version&amp;gt;
            &amp;lt;/dependency&amp;gt;

            &amp;lt;!--文件上传--&amp;gt;
            &amp;lt;dependency&amp;gt;
                &amp;lt;groupId&amp;gt;commons-fileupload&amp;lt;/groupId&amp;gt;
                &amp;lt;artifactId&amp;gt;commons-fileupload&amp;lt;/artifactId&amp;gt;
                &amp;lt;version&amp;gt;${commons-fileupload.version}&amp;lt;/version&amp;gt;
            &amp;lt;/dependency&amp;gt;

            &amp;lt;!--commons-io--&amp;gt;
            &amp;lt;dependency&amp;gt;
                &amp;lt;groupId&amp;gt;commons-io&amp;lt;/groupId&amp;gt;
                &amp;lt;artifactId&amp;gt;commons-io&amp;lt;/artifactId&amp;gt;
                &amp;lt;version&amp;gt;${commons-io.version}&amp;lt;/version&amp;gt;
            &amp;lt;/dependency&amp;gt;

            &amp;lt;!--httpclient--&amp;gt;
            &amp;lt;dependency&amp;gt;
                &amp;lt;groupId&amp;gt;org.apache.httpcomponents&amp;lt;/groupId&amp;gt;
                &amp;lt;artifactId&amp;gt;httpclient&amp;lt;/artifactId&amp;gt;
                &amp;lt;version&amp;gt;${httpclient.version}&amp;lt;/version&amp;gt;
            &amp;lt;/dependency&amp;gt;

            &amp;lt;dependency&amp;gt;
                &amp;lt;groupId&amp;gt;com.google.code.gson&amp;lt;/groupId&amp;gt;
                &amp;lt;artifactId&amp;gt;gson&amp;lt;/artifactId&amp;gt;
                &amp;lt;version&amp;gt;${gson.version}&amp;lt;/version&amp;gt;
            &amp;lt;/dependency&amp;gt;

            &amp;lt;!-- JWT --&amp;gt;
            &amp;lt;dependency&amp;gt;
                &amp;lt;groupId&amp;gt;io.jsonwebtoken&amp;lt;/groupId&amp;gt;
                &amp;lt;artifactId&amp;gt;jjwt&amp;lt;/artifactId&amp;gt;
                &amp;lt;version&amp;gt;${jwt.version}&amp;lt;/version&amp;gt;
            &amp;lt;/dependency&amp;gt;

            &amp;lt;!--aliyun--&amp;gt;
            &amp;lt;dependency&amp;gt;
                &amp;lt;groupId&amp;gt;com.aliyun&amp;lt;/groupId&amp;gt;
                &amp;lt;artifactId&amp;gt;aliyun-java-sdk-core&amp;lt;/artifactId&amp;gt;
                &amp;lt;version&amp;gt;${aliyun-java-sdk-core.version}&amp;lt;/version&amp;gt;
            &amp;lt;/dependency&amp;gt;
            &amp;lt;dependency&amp;gt;
                &amp;lt;groupId&amp;gt;com.aliyun.oss&amp;lt;/groupId&amp;gt;
                &amp;lt;artifactId&amp;gt;aliyun-sdk-oss&amp;lt;/artifactId&amp;gt;
                &amp;lt;version&amp;gt;${aliyun-sdk-oss.version}&amp;lt;/version&amp;gt;
            &amp;lt;/dependency&amp;gt;
            &amp;lt;dependency&amp;gt;
                &amp;lt;groupId&amp;gt;com.aliyun&amp;lt;/groupId&amp;gt;
                &amp;lt;artifactId&amp;gt;aliyun-java-sdk-vod&amp;lt;/artifactId&amp;gt;
                &amp;lt;version&amp;gt;${aliyun-java-sdk-vod.version}&amp;lt;/version&amp;gt;
            &amp;lt;/dependency&amp;gt;
            &amp;lt;!--            &amp;lt;dependency&amp;gt;--&amp;gt;
            &amp;lt;!--                &amp;lt;groupId&amp;gt;com.aliyun&amp;lt;/groupId&amp;gt;--&amp;gt;
            &amp;lt;!--                &amp;lt;artifactId&amp;gt;aliyun-java-vod-upload&amp;lt;/artifactId&amp;gt;--&amp;gt;
            &amp;lt;!--                &amp;lt;version&amp;gt;${aliyun-java-vod-upload.version}&amp;lt;/version&amp;gt;--&amp;gt;
            &amp;lt;!--            &amp;lt;/dependency&amp;gt;--&amp;gt;
            &amp;lt;!--            &amp;lt;dependency&amp;gt;--&amp;gt;
            &amp;lt;!--                &amp;lt;groupId&amp;gt;com.aliyun&amp;lt;/groupId&amp;gt;--&amp;gt;
            &amp;lt;!--                &amp;lt;artifactId&amp;gt;aliyun-sdk-vod-upload&amp;lt;/artifactId&amp;gt;--&amp;gt;
            &amp;lt;!--                &amp;lt;version&amp;gt;${aliyun-sdk-vod-upload.version}&amp;lt;/version&amp;gt;--&amp;gt;
            &amp;lt;!--            &amp;lt;/dependency&amp;gt;--&amp;gt;
            &amp;lt;dependency&amp;gt;
                &amp;lt;groupId&amp;gt;com.alibaba&amp;lt;/groupId&amp;gt;
                &amp;lt;artifactId&amp;gt;fastjson&amp;lt;/artifactId&amp;gt;
                &amp;lt;version&amp;gt;${fastjson.version}&amp;lt;/version&amp;gt;
            &amp;lt;/dependency&amp;gt;
            &amp;lt;dependency&amp;gt;
                &amp;lt;groupId&amp;gt;org.json&amp;lt;/groupId&amp;gt;
                &amp;lt;artifactId&amp;gt;json&amp;lt;/artifactId&amp;gt;
                &amp;lt;version&amp;gt;${json.version}&amp;lt;/version&amp;gt;
            &amp;lt;/dependency&amp;gt;

            &amp;lt;dependency&amp;gt;
                &amp;lt;groupId&amp;gt;commons-dbutils&amp;lt;/groupId&amp;gt;
                &amp;lt;artifactId&amp;gt;commons-dbutils&amp;lt;/artifactId&amp;gt;
                &amp;lt;version&amp;gt;${commons-dbutils.version}&amp;lt;/version&amp;gt;
            &amp;lt;/dependency&amp;gt;

            &amp;lt;dependency&amp;gt;
                &amp;lt;groupId&amp;gt;com.alibaba.otter&amp;lt;/groupId&amp;gt;
                &amp;lt;artifactId&amp;gt;canal.client&amp;lt;/artifactId&amp;gt;
                &amp;lt;version&amp;gt;${canal.client.version}&amp;lt;/version&amp;gt;
            &amp;lt;/dependency&amp;gt;
        &amp;lt;/dependencies&amp;gt;
    &amp;lt;/dependencyManagement&amp;gt;


    &amp;lt;build&amp;gt;
        &amp;lt;plugins&amp;gt;
            &amp;lt;plugin&amp;gt;
                &amp;lt;artifactId&amp;gt;maven-compiler-plugin&amp;lt;/artifactId&amp;gt;
                &amp;lt;configuration&amp;gt;
                    &amp;lt;encoding&amp;gt;UTF-8&amp;lt;/encoding&amp;gt;
                    &amp;lt;source&amp;gt;8&amp;lt;/source&amp;gt;
                    &amp;lt;target&amp;gt;8&amp;lt;/target&amp;gt;
                &amp;lt;/configuration&amp;gt;
                &amp;lt;groupId&amp;gt;org.apache.maven.plugins&amp;lt;/groupId&amp;gt;
                &amp;lt;version&amp;gt;3.1&amp;lt;/version&amp;gt;
            &amp;lt;/plugin&amp;gt;
        &amp;lt;/plugins&amp;gt;
    &amp;lt;/build&amp;gt;

&amp;lt;/project&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;h4&gt;&lt;strong&gt;6.2.2.2&lt;/strong&gt; &lt;strong&gt;创建&lt;strong&gt;&lt;strong&gt;UAA&lt;/strong&gt;&lt;/strong&gt;授权服务工程&lt;/strong&gt;&lt;/h4&gt;
&lt;p&gt;1、创建distributed-security-uaa&lt;/p&gt;
&lt;p&gt;创建distributed-security-uaa作为授权服务工程，依赖如下：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&amp;lt;?xml version=&quot;1.0&quot; encoding=&quot;UTF-8&quot;?&amp;gt;
&amp;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;&amp;gt;
    &amp;lt;parent&amp;gt;
        &amp;lt;artifactId&amp;gt;guli_parent&amp;lt;/artifactId&amp;gt;
        &amp;lt;groupId&amp;gt;com.eim&amp;lt;/groupId&amp;gt;
        &amp;lt;version&amp;gt;0.0.1-SNAPSHOT&amp;lt;/version&amp;gt;
    &amp;lt;/parent&amp;gt;
    &amp;lt;modelVersion&amp;gt;4.0.0&amp;lt;/modelVersion&amp;gt;

    &amp;lt;artifactId&amp;gt;uaa&amp;lt;/artifactId&amp;gt;

    &amp;lt;properties&amp;gt;
        &amp;lt;maven.compiler.source&amp;gt;8&amp;lt;/maven.compiler.source&amp;gt;
        &amp;lt;maven.compiler.target&amp;gt;8&amp;lt;/maven.compiler.target&amp;gt;
    &amp;lt;/properties&amp;gt;

    &amp;lt;dependencies&amp;gt;
        &amp;lt;!--fegin组件--&amp;gt;
        &amp;lt;dependency&amp;gt;
            &amp;lt;groupId&amp;gt;org.springframework.cloud&amp;lt;/groupId&amp;gt;
            &amp;lt;artifactId&amp;gt;spring-cloud-starter-openfeign&amp;lt;/artifactId&amp;gt;
        &amp;lt;/dependency&amp;gt;
        &amp;lt;dependency&amp;gt;
            &amp;lt;groupId&amp;gt;com.alibaba.cloud&amp;lt;/groupId&amp;gt;
            &amp;lt;artifactId&amp;gt;spring-cloud-starter-alibaba-nacos-config&amp;lt;/artifactId&amp;gt;
        &amp;lt;/dependency&amp;gt;
        &amp;lt;dependency&amp;gt;
            &amp;lt;groupId&amp;gt;com.alibaba.cloud&amp;lt;/groupId&amp;gt;
            &amp;lt;artifactId&amp;gt;spring-cloud-starter-alibaba-nacos-discovery&amp;lt;/artifactId&amp;gt;
        &amp;lt;/dependency&amp;gt;

        &amp;lt;dependency&amp;gt;
            &amp;lt;groupId&amp;gt;com.alibaba&amp;lt;/groupId&amp;gt;
            &amp;lt;artifactId&amp;gt;fastjson&amp;lt;/artifactId&amp;gt;
        &amp;lt;/dependency&amp;gt;
        &amp;lt;dependency&amp;gt;
            &amp;lt;groupId&amp;gt;org.springframework.boot&amp;lt;/groupId&amp;gt;
            &amp;lt;artifactId&amp;gt;spring-boot-starter-web&amp;lt;/artifactId&amp;gt;
        &amp;lt;/dependency&amp;gt;
        &amp;lt;dependency&amp;gt;
            &amp;lt;groupId&amp;gt;org.springframework.cloud&amp;lt;/groupId&amp;gt;
            &amp;lt;artifactId&amp;gt;spring-cloud-starter-openfeign&amp;lt;/artifactId&amp;gt;
        &amp;lt;/dependency&amp;gt;
        &amp;lt;dependency&amp;gt;
            &amp;lt;groupId&amp;gt;org.projectlombok&amp;lt;/groupId&amp;gt;
            &amp;lt;artifactId&amp;gt;lombok&amp;lt;/artifactId&amp;gt;
        &amp;lt;/dependency&amp;gt;

        &amp;lt;dependency&amp;gt;
            &amp;lt;groupId&amp;gt;com.baomidou&amp;lt;/groupId&amp;gt;
            &amp;lt;artifactId&amp;gt;mybatis-plus-boot-starter&amp;lt;/artifactId&amp;gt;
        &amp;lt;/dependency&amp;gt;
        &amp;lt;dependency&amp;gt;
            &amp;lt;groupId&amp;gt;mysql&amp;lt;/groupId&amp;gt;
            &amp;lt;artifactId&amp;gt;mysql-connector-java&amp;lt;/artifactId&amp;gt;
        &amp;lt;/dependency&amp;gt;


        &amp;lt;!--安全框架--&amp;gt;
        &amp;lt;!--    jwt工具类    --&amp;gt;
        &amp;lt;dependency&amp;gt;
            &amp;lt;groupId&amp;gt;io.jsonwebtoken&amp;lt;/groupId&amp;gt;
            &amp;lt;artifactId&amp;gt;jjwt&amp;lt;/artifactId&amp;gt;
        &amp;lt;/dependency&amp;gt;
        &amp;lt;!--    security    --&amp;gt;
        &amp;lt;dependency&amp;gt;
            &amp;lt;groupId&amp;gt;org.springframework.cloud&amp;lt;/groupId&amp;gt;
            &amp;lt;artifactId&amp;gt;spring-cloud-starter-security&amp;lt;/artifactId&amp;gt;
        &amp;lt;/dependency&amp;gt;
        &amp;lt;!--    security oauth2    --&amp;gt;
        &amp;lt;dependency&amp;gt;
            &amp;lt;groupId&amp;gt;org.springframework.cloud&amp;lt;/groupId&amp;gt;
            &amp;lt;artifactId&amp;gt;spring-cloud-starter-oauth2&amp;lt;/artifactId&amp;gt;
        &amp;lt;/dependency&amp;gt;
        &amp;lt;!--    security-oauth2-autoconfigure    --&amp;gt;
        &amp;lt;dependency&amp;gt;
            &amp;lt;groupId&amp;gt;org.springframework.security.oauth.boot&amp;lt;/groupId&amp;gt;
            &amp;lt;artifactId&amp;gt;spring-security-oauth2-autoconfigure&amp;lt;/artifactId&amp;gt;
        &amp;lt;/dependency&amp;gt;


    &amp;lt;/dependencies&amp;gt;

&amp;lt;/project&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;2、启动类&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;@EnableDiscoveryClient
@SpringBootApplication
public class UaaApplication {
    public static void main(String[] args) {
        SpringApplication.run(UaaApplication.class,args);
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;3、配置文件&lt;/p&gt;
&lt;p&gt;在resources下创建application.properties&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;server.port:8003
spring.application.name=uaa
spring.main.allow-bean-definition-overriding=true
## 环境设置：dev、test、prod
spring.profiles.active=dev
spring.http.encoding.enabled=true
spring.http.encoding.charset=utf-8
spring.http.encoding.force=true
server.tomcat.remote_ip_header=x‐forwarded‐for
server.tomcat.protocol_header=x‐forwarded‐proto
## mysql数据库连接
spring.datasource.driver-class-name=com.mysql.cj.jdbc.Driver
spring.datasource.url=jdbc:mysql://8.135.57.242:3306/test?serverTimezone=GMT%2B8
spring.datasource.username=root
spring.datasource.password=eq123456
spring.datasource.hikari.max-lifetime=60000
#mybatis日志
mybatis-plus.configuration.log-impl=org.apache.ibatis.logging.stdout.StdOutImpl
mybatis-plus.mapper-locations=classpath*:com/eim/eduservice/mapper/xml/*.xml
mybatis-plus.global-config.db-config.logic-delete-field=isDeleted
spring.jackson.date-format=yyyy-MM-dd HH:mm:ss
spring.jackson.time-zone=GMT+8
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;bootstrap.properties&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;spring.application.name=uaa
spring.cloud.nacos.discovery.server-addr=8.135.57.242:8848
spring.cloud.nacos.config.server-addr=8.135.57.242:8848
&lt;/code&gt;&lt;/pre&gt;
&lt;h4&gt;&lt;strong&gt;6.2.2.3&lt;/strong&gt; &lt;strong&gt;创建&lt;strong&gt;&lt;strong&gt;service-edu&lt;/strong&gt;&lt;/strong&gt;资源服务工程&lt;/strong&gt;&lt;/h4&gt;
&lt;p&gt;本工程为service-edu订单服务工程，访问本工程的资源需要认证通过。&lt;/p&gt;
&lt;p&gt;本工程的目的主要是测试认证授权的功能，所以不涉及订单管理相关业务。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&amp;lt;?xml version=&quot;1.0&quot; encoding=&quot;UTF-8&quot;?&amp;gt;
&amp;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;&amp;gt;
    &amp;lt;parent&amp;gt;
        &amp;lt;artifactId&amp;gt;service&amp;lt;/artifactId&amp;gt;
        &amp;lt;groupId&amp;gt;com.eim&amp;lt;/groupId&amp;gt;
        &amp;lt;version&amp;gt;0.0.1-SNAPSHOT&amp;lt;/version&amp;gt;
    &amp;lt;/parent&amp;gt;
    &amp;lt;modelVersion&amp;gt;4.0.0&amp;lt;/modelVersion&amp;gt;

    &amp;lt;artifactId&amp;gt;service-edu&amp;lt;/artifactId&amp;gt;

    &amp;lt;properties&amp;gt;
        &amp;lt;maven.compiler.source&amp;gt;8&amp;lt;/maven.compiler.source&amp;gt;
        &amp;lt;maven.compiler.target&amp;gt;8&amp;lt;/maven.compiler.target&amp;gt;
    &amp;lt;/properties&amp;gt;

    &amp;lt;dependencies&amp;gt;
        &amp;lt;dependency&amp;gt;
            &amp;lt;groupId&amp;gt;com.eim&amp;lt;/groupId&amp;gt;
            &amp;lt;artifactId&amp;gt;common&amp;lt;/artifactId&amp;gt;
            &amp;lt;version&amp;gt;0.0.1-SNAPSHOT&amp;lt;/version&amp;gt;
        &amp;lt;/dependency&amp;gt;
        &amp;lt;dependency&amp;gt;
            &amp;lt;groupId&amp;gt;org.springframework.boot&amp;lt;/groupId&amp;gt;
            &amp;lt;artifactId&amp;gt;spring-boot-starter-web&amp;lt;/artifactId&amp;gt;
        &amp;lt;/dependency&amp;gt;
        &amp;lt;!--fegin组件--&amp;gt;
        &amp;lt;dependency&amp;gt;
            &amp;lt;groupId&amp;gt;org.springframework.cloud&amp;lt;/groupId&amp;gt;
            &amp;lt;artifactId&amp;gt;spring-cloud-starter-openfeign&amp;lt;/artifactId&amp;gt;
        &amp;lt;/dependency&amp;gt;
        &amp;lt;dependency&amp;gt;
            &amp;lt;groupId&amp;gt;com.alibaba.cloud&amp;lt;/groupId&amp;gt;
            &amp;lt;artifactId&amp;gt;spring-cloud-starter-alibaba-nacos-config&amp;lt;/artifactId&amp;gt;
        &amp;lt;/dependency&amp;gt;
        &amp;lt;dependency&amp;gt;
            &amp;lt;groupId&amp;gt;com.alibaba.cloud&amp;lt;/groupId&amp;gt;
            &amp;lt;artifactId&amp;gt;spring-cloud-starter-alibaba-nacos-discovery&amp;lt;/artifactId&amp;gt;
        &amp;lt;/dependency&amp;gt;
        &amp;lt;!--   oauth     --&amp;gt;
        &amp;lt;dependency&amp;gt;
            &amp;lt;groupId&amp;gt;org.springframework.cloud&amp;lt;/groupId&amp;gt;
            &amp;lt;artifactId&amp;gt;spring-cloud-starter-security&amp;lt;/artifactId&amp;gt;
        &amp;lt;/dependency&amp;gt;
        &amp;lt;dependency&amp;gt;
            &amp;lt;groupId&amp;gt;org.springframework.cloud&amp;lt;/groupId&amp;gt;
            &amp;lt;artifactId&amp;gt;spring-cloud-starter-oauth2&amp;lt;/artifactId&amp;gt;
        &amp;lt;/dependency&amp;gt;
        &amp;lt;dependency&amp;gt;
            &amp;lt;groupId&amp;gt;org.springframework.security.oauth.boot&amp;lt;/groupId&amp;gt;
            &amp;lt;artifactId&amp;gt;spring-security-oauth2-autoconfigure&amp;lt;/artifactId&amp;gt;
        &amp;lt;/dependency&amp;gt;
        &amp;lt;!--   oauth     --&amp;gt;
    &amp;lt;/dependencies&amp;gt;

    &amp;lt;build&amp;gt;
        &amp;lt;resources&amp;gt;
            &amp;lt;resource&amp;gt;
                &amp;lt;!-- xml放在java目录下--&amp;gt;
                &amp;lt;directory&amp;gt;src/main/java&amp;lt;/directory&amp;gt;
                &amp;lt;includes&amp;gt;
                    &amp;lt;include&amp;gt;**/*.xml&amp;lt;/include&amp;gt;
                &amp;lt;/includes&amp;gt;
            &amp;lt;/resource&amp;gt;
            &amp;lt;!--指定资源的位置（xml放在resources下，可以不用指定）--&amp;gt;
            &amp;lt;resource&amp;gt;
                &amp;lt;directory&amp;gt;src/main/resources&amp;lt;/directory&amp;gt;
            &amp;lt;/resource&amp;gt;
        &amp;lt;/resources&amp;gt;
    &amp;lt;/build&amp;gt;

&amp;lt;/project&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;3、配置文件&lt;/p&gt;
&lt;p&gt;在resources中创建application.properties&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;## 服务端口
server.port=8001
## 服务名
spring.application.name=service-edu

## 环境设置：dev、test、prod
spring.profiles.active=dev

## mysql数据库连接
spring.datasource.driver-class-name=com.mysql.cj.jdbc.Driver
spring.datasource.url=jdbc:mysql://8.135.57.242:3306/test?serverTimezone=GMT%2B8
spring.datasource.username=root
spring.datasource.password=eq123456

#mybatis日志
mybatis-plus.configuration.log-impl=org.apache.ibatis.logging.stdout.StdOutImpl
mybatis-plus.mapper-locations=classpath*:com/eim/eduservice/mapper/xml/*.xml
mybatis-plus.global-config.db-config.logic-delete-field=isDeleted

spring.jackson.date-format=yyyy-MM-dd HH:mm:ss
spring.jackson.time-zone=GMT+8

&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;bootstrap.properties&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;spring.application.name=uaa
spring.cloud.nacos.discovery.server-addr=8.135.57.242:8848
spring.cloud.nacos.config.server-addr=8.135.57.242:8848
&lt;/code&gt;&lt;/pre&gt;
&lt;h4&gt;**6.2.2.**&lt;strong&gt;授权服务器配置&lt;/strong&gt;&lt;/h4&gt;
&lt;h5&gt;&lt;strong&gt;6.2.2.1 EnableAuthorizationServer&lt;/strong&gt;&lt;/h5&gt;
&lt;p&gt;可以用 @EnableAuthorizationServer 注解并继承AuthorizationServerConfifigurerAdapter来配置OAuth2.0 授权&lt;/p&gt;
&lt;p&gt;服务器。&lt;/p&gt;
&lt;p&gt;在Confifig包下创建AuthorizationServer：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;@Configuration
@EnableAuthorizationServer 
public class AuthorizationServer extends AuthorizationServerConfigurerAdapter {
    //略...
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;AuthorizationServerConfifigurerAdapter要求配置以下几个类，这几个类是由Spring创建的独立的配置对象，它们&lt;/p&gt;
&lt;p&gt;会被Spring传入AuthorizationServerConfifigurer中进行配置。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;public class AuthorizationServerConfigurerAdapter implements AuthorizationServerConfigurer {
    public AuthorizationServerConfigurerAdapter() {
    }
    public void configure(AuthorizationServerSecurityConfigurer security) throws Exception {
    }
    public void configure(ClientDetailsServiceConfigurer clients) throws Exception {
    }
    public void configure(AuthorizationServerEndpointsConfigurer endpoints) throws Exception {
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;ClientDetailsServiceConfifigurer&lt;/strong&gt;：用来配置客户端详情服务（ClientDetailsService），客户端详情信息在 这里进行初始化，你能够把客户端详情信息写死在这里或者是通过数据库来存储调取详情信息。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;AuthorizationServerEndpointsConfifigurer&lt;/strong&gt;：用来配置令牌（token）的访问端点和令牌服务(token services)。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;AuthorizationServerSecurityConfifigurer&lt;/strong&gt;：用来配置令牌端点的安全约束.&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h5&gt;**6.2.2.1.**&lt;strong&gt;配置客户端详细信息&lt;/strong&gt;&lt;/h5&gt;
&lt;p&gt;ClientDetailsServiceConfifigurer 能够使用内存或者JDBC来实现客户端详情服务（ClientDetailsService），&lt;/p&gt;
&lt;p&gt;ClientDetailsService负责查找ClientDetails，而ClientDetails有几个重要的属性如下列表：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;clientId：（必须的）用来标识客户的Id。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;secret：（需要值得信任的客户端）客户端安全码，如果有的话。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;scope：用来限制客户端的访问范围，如果为空（默认）的话，那么客户端拥有全部的访问范围。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;authorizedGrantTypes：此客户端可以使用的授权类型，默认为空。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;authorities：此客户端可以使用的权限（基于Spring Security authorities）。&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;blockquote&gt;
&lt;p&gt;客户端详情（Client Details）能够在应用程序运行的时候进行更新，可以通过访问底层的存储服务（例如将客户&lt;/p&gt;
&lt;p&gt;端详情存储在一个关系数据库的表中，就可以使用 JdbcClientDetailsService）或者通过自己实现&lt;/p&gt;
&lt;p&gt;ClientRegistrationService接口（同时你也可以实现 ClientDetailsService 接口）来进行管理。&lt;/p&gt;
&lt;p&gt;我们暂时使用内存方式存储客户端详情信息，配置如下:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;    /**
     * 配置客户端
     *
     * @param clients
     * @throws Exception
     */
    @Override
    public void configure(ClientDetailsServiceConfigurer clients) throws Exception {

        // 使用in‐memory存储
        clients.inMemory()
                //client_id
                .withClient(&quot;c1&quot;)
                .secret(new BCryptPasswordEncoder().encode(&quot;secret&quot;))
                .resourceIds(&quot;res1&quot;)
                // 该client允许的授权类型
                //authorization_code,password,refresh_token,implicit,client_credentials
                .authorizedGrantTypes(&quot;authorization_code&quot;, &quot;password&quot;, &quot;client_credentials&quot;, &quot;implicit&quot;, &quot;refresh_token&quot;)
                // 允许的授权范围
                .scopes(&quot;all&quot;)
                //false跳转到授权页面
                .autoApprove(false)
                //加上验证回调地址
                .redirectUris(&quot;http://www.baidu.com&quot;);
    }
&lt;/code&gt;&lt;/pre&gt;
&lt;/blockquote&gt;
&lt;h5&gt;**6.2.2.2.**&lt;strong&gt;管理令牌&lt;/strong&gt;&lt;/h5&gt;
&lt;p&gt;AuthorizationServerTokenServices 接口定义了一些操作使得你可以对令牌进行一些必要的管理，令牌可以被用来&lt;/p&gt;
&lt;p&gt;加载身份信息，里面包含了这个令牌的相关权限。&lt;/p&gt;
&lt;p&gt;自己可以创建 AuthorizationServerTokenServices 这个接口的实现，则需要继承 DefaultTokenServices 这个类，&lt;/p&gt;
&lt;p&gt;里面包含了一些有用实现，你可以使用它来修改令牌的格式和令牌的存储。默认的，当它尝试创建一个令牌的时&lt;/p&gt;
&lt;p&gt;候，是使用随机值来进行填充的，除了持久化令牌是委托一个 TokenStore 接口来实现以外，这个类几乎帮你做了&lt;/p&gt;
&lt;p&gt;所有的事情。并且 TokenStore 这个接口有一个默认的实现，它就是 InMemoryTokenStore ，如其命名，所有的&lt;/p&gt;
&lt;p&gt;令牌是被保存在了内存中。除了使用这个类以外，你还可以使用一些其他的预定义实现，下面有几个版本，它们都&lt;/p&gt;
&lt;p&gt;实现了TokenStore接口：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;InMemoryTokenStore：这个版本的实现是被默认采用的，它可以完美的工作在单服务器上（即访问并发量&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;压力不大的情况下，并且它在失败的时候不会进行备份），大多数的项目都可以使用这个版本的实现来进行&lt;/p&gt;
&lt;p&gt;尝试，你可以在开发的时候使用它来进行管理，因为不会被保存到磁盘中，所以更易于调试。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;JdbcTokenStore：这是一个基于JDBC的实现版本，令牌会被保存进关系型数据库。使用这个版本的实现时，&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;你可以在不同的服务器之间共享令牌信息，使用这个版本的时候请注意把&quot;spring-jdbc&quot;这个依赖加入到你的&lt;/p&gt;
&lt;p&gt;classpath当中。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;JwtTokenStore：这个版本的全称是 JSON Web Token（JWT），它可以把令牌相关的数据进行编码（因此对&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;于后端服务来说，它不需要进行存储，这将是一个重大优势），但是它有一个缺点，那就是撤销一个已经授&lt;/p&gt;
&lt;p&gt;权令牌将会非常困难，所以它通常用来处理一个生命周期较短的令牌以及撤销刷新令牌（refresh_token）。&lt;/p&gt;
&lt;p&gt;另外一个缺点就是这个令牌占用的空间会比较大，如果你加入了比较多用户凭证信息。JwtTokenStore 不会保存&lt;/p&gt;
&lt;p&gt;任何数据，但是它在转换令牌值以及授权信息方面与 DefaultTokenServices 所扮演的角色是一样的。&lt;/p&gt;
&lt;h6&gt;1、定义TokenConfifig&lt;/h6&gt;
&lt;p&gt;在confifig包下定义TokenConfifig，我们暂时先使用InMemoryTokenStore，生成一个普通的令牌。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;@Configuration 
public class TokenConfig {
    @Bean
    public TokenStore tokenStore() {
        return new InMemoryTokenStore(); 
    } 
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h6&gt;2、定义AuthorizationServerTokenServices&lt;/h6&gt;
&lt;p&gt;在AuthorizationServer中定义AuthorizationServerTokenServices&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;    @Autowired
    private TokenStore tokenStore;

    @Autowired
    private ClientDetailsService clientDetailsService;

    /**
     * 认证服务的token服务
     *
     * @return
     */
    @Bean
    public AuthorizationServerTokenServices tokenService() {
        DefaultTokenServices service = new DefaultTokenServices();
        //默认客户端操作
        service.setClientDetailsService(clientDetailsService);
        service.setSupportRefreshToken(true);
        //设置tokenStore
        service.setTokenStore(tokenStore);
        // 令牌默认有效期2小时
        service.setAccessTokenValiditySeconds(7200);
        // 刷新令牌默认有效期3天
        service.setRefreshTokenValiditySeconds(259200);
        return service;
    }
&lt;/code&gt;&lt;/pre&gt;
&lt;h5&gt;**6.2.2.3.**&lt;strong&gt;令牌访问端点配置&lt;/strong&gt;&lt;/h5&gt;
&lt;p&gt;AuthorizationServerEndpointsConfifigurer 这个对象的实例可以完成令牌服务以及令牌endpoint配置。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;配置授权类型（&lt;strong&gt;&lt;strong&gt;Grant Types&lt;/strong&gt;&lt;/strong&gt;）&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;AuthorizationServerEndpointsConfifigurer 通过设定以下属性决定支持的&lt;strong&gt;授权类型（&lt;strong&gt;&lt;strong&gt;Grant Types&lt;/strong&gt;&lt;/strong&gt;）&lt;/strong&gt;：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;authenticationManager：认证管理器，当你选择了资源所有者密码（password）授权类型的时候，请设置 这个属性注入一个 AuthenticationManager 对象。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;userDetailsService：如果你设置了这个属性的话，那说明你有一个自己的 UserDetailsService 接口的实现， 或者你可以把这个东西设置到全局域上面去（例如 GlobalAuthenticationManagerConfifigurer 这个配置对 象），当你设置了这个之后，那么 &quot;refresh_token&quot; 即刷新令牌授权类型模式的流程中就会包含一个检查，用 来确保这个账号是否仍然有效，假如说你禁用了这个账户的话。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;authorizationCodeServices：这个属性是用来设置授权码服务的（即 AuthorizationCodeServices 的实例对&lt;/p&gt;
&lt;p&gt;象），主要用于 &quot;authorization_code&quot; 授权码类型模式。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;implicitGrantService：这个属性用于设置隐式授权模式，用来管理隐式授权模式的状态。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;tokenGranter：当你设置了这个东西（即 TokenGranter 接口实现），那么授权将会交由你来完全掌控，并 且会忽略掉上面的这几个属性，这个属性一般是用作拓展用途的，即标准的四种授权模式已经满足不了你的需求的时候，才会考虑使用这个。&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;**配置授权端点的&lt;strong&gt;&lt;strong&gt;URL&lt;/strong&gt;&lt;/strong&gt;（&lt;strong&gt;&lt;strong&gt;Endpoint URLs&lt;/strong&gt;&lt;/strong&gt;）：&lt;/p&gt;
&lt;p&gt;AuthorizationServerEndpointsConfifigurer 这个配置对象有一个叫做 pathMapping() 的方法用来配置端点URL链接，它有两个参数：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;第一个参数：String 类型的，这个端点URL的默认链接。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;第二个参数：String 类型的，你要进行替代的URL链接。.&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;以上的参数都将以 &quot;/&quot; 字符为开始的字符串，框架的默认URL链接如下列表，可以作为这个 pathMapping() 方法的 第一个参数：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;/oauth/authorize：授权端点。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;/oauth/token：令牌端点。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;/oauth/confifirm_access：用户确认授权提交端点。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;/oauth/error：授权服务错误信息端点。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;/oauth/check_token：用于资源服务访问的令牌解析端点。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;/oauth/token_key：提供公有密匙的端点，如果你使用JWT令牌的话&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;需要注意的是授权端点这个URL应该被Spring Security保护起来只供授权用户访问. 在AuthorizationServer配置令牌访问端点&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;@Autowired 
private AuthorizationCodeServices authorizationCodeServices;

@Autowired 
private AuthenticationManager authenticationManager;

@Override 
public void configure(AuthorizationServerEndpointsConfigurer endpoints) { 
    endpoints.authenticationManager(authenticationManager) 
        .authorizationCodeServices(authorizationCodeServices) 
        .tokenServices(tokenService()) 
        .allowedTokenEndpointRequestMethods(HttpMethod.POST); 
}

@Bean public AuthorizationCodeServices authorizationCodeServices() { 
    //设置授权码模式的授权码如何 存取，暂时采用内存方式 
    return new InMemoryAuthorizationCodeServices(); 
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h5&gt;**6.2.2.4.**&lt;strong&gt;令牌端点的安全约束&lt;/strong&gt;&lt;/h5&gt;
&lt;p&gt;&lt;strong&gt;AuthorizationServerSecurityConfifigurer&lt;/strong&gt;：用来配置令牌端点(Token Endpoint)的安全约束，在&lt;/p&gt;
&lt;p&gt;AuthorizationServer中配置如下&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;@Override 
public void configure(AuthorizationServerSecurityConfigurer security){ 
    security .tokenKeyAccess(&quot;permitAll()&quot;)  (1) 
        .checkTokenAccess(&quot;permitAll()&quot;)     (2) 
        .allowFormAuthenticationForClients() (3); 
}
&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote&gt;
&lt;p&gt;（1）tokenkey这个endpoint当使用JwtToken且使用非对称加密时，资源服务用于获取公钥而开放的，这里指这个 endpoint完全公开。&lt;/p&gt;
&lt;p&gt;（2）checkToken这个endpoint完全公开&lt;/p&gt;
&lt;p&gt;（3） 允许表单认证&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;&lt;strong&gt;授权服务配置总结&lt;/strong&gt;：授权服务配置分成三大块，可以关联记忆。 既然要完成认证，它首先得知道客户端信息从哪儿读取，因此要进行客户端详情配置。 既然要颁发token，那必须得定义token的相关endpoint，以及token如何存取，以及客户端支持哪些类型的 token。 既然暴露除了一些endpoint，那对这些endpoint可以定义一些安全上的约束等。&lt;/p&gt;
&lt;h5&gt;&lt;strong&gt;6.2.2.5 web****安全配置&lt;/strong&gt;&lt;/h5&gt;
&lt;p&gt;将Spring-Boot工程中的WebSecurityConfifig拷贝到UAA工程中。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;/**
 * @author Administrator
 * @version 1.0
 **/
@Configuration
@EnableGlobalMethodSecurity(securedEnabled = true,prePostEnabled = true)
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {

    //认证管理器
    @Bean
    @Override
    public AuthenticationManager authenticationManagerBean() throws Exception {
        return super.authenticationManagerBean();
    }
    //密码编码器
    @Bean
    public PasswordEncoder passwordEncoder() {
        return new BCryptPasswordEncoder();
    }

    //安全拦截机制（最重要）
    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http.csrf().disable()
                .authorizeRequests()
                .antMatchers(&quot;/r/r1&quot;).hasAnyAuthority(&quot;p1&quot;)
                .antMatchers(&quot;/login*&quot;).permitAll()
                .anyRequest().authenticated()
                .and()
                .formLogin()
        ;

    }

}
&lt;/code&gt;&lt;/pre&gt;
&lt;h4&gt;**6.2.3.**&lt;strong&gt;授权码模式&lt;/strong&gt;&lt;/h4&gt;
&lt;h5&gt;&lt;strong&gt;6.2.3.1&lt;/strong&gt; &lt;strong&gt;授权码模式介绍&lt;/strong&gt;&lt;/h5&gt;
&lt;p&gt;下图是授权码模式交互图：&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://tangtangit.com/_astro/image-20210411133730219.BSt5uK0__1UJhcx.webp&quot; alt=&quot;image-20210411133730219&quot; /&gt;&lt;/p&gt;
&lt;p&gt;（1）资源拥有者打开客户端，客户端要求资源拥有者给予授权，它将浏览器被重定向到授权服务器，重定向时会&lt;strong&gt;附加客户端的身份信息。如：&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;/uaa/oauth/authorize?client_id=c1&amp;amp;response_type=code&amp;amp;scope=all&amp;amp;redirect_uri=http://www.baidu.com
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;参数列表如下：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;client_id：客户端准入标识。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;response_type：授权码模式固定为code。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;scope：客户端权限。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;redirect_uri：跳转uri，当授权码申请成功后会跳转到此地址，并在后边带上code参数（授权码）。&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;（2）浏览器出现向授权服务器授权页面，之后将用户同意授权。**&lt;/p&gt;
&lt;p&gt;（3）授权服务器将授权码（&lt;strong&gt;&lt;strong&gt;AuthorizationCode&lt;/strong&gt;&lt;/strong&gt;）转经浏览器发送给&lt;strong&gt;&lt;strong&gt;client(&lt;strong&gt;&lt;strong&gt;通过&lt;/strong&gt;&lt;/strong&gt;redirect_uri)&lt;/strong&gt;&lt;/strong&gt;。**&lt;/p&gt;
&lt;p&gt;（4）客户端拿着授权码向授权服务器索要访问&lt;strong&gt;&lt;strong&gt;access_token&lt;/strong&gt;&lt;/strong&gt;，请求如下：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;/uaa/oauth/token? client_id=c1&amp;amp;client_secret=secret&amp;amp;grant_type=authorization_code&amp;amp;code=5PgfcD&amp;amp;redirect_uri=http://www.baidu.com
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;参数列表如下&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;client_id：客户端准入标识。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;client_secret：客户端秘钥。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;grant_type：授权类型，填写authorization_code，表示授权码模式&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;code：授权码，就是刚刚获取的授权码，注意：授权码只使用一次就无效了，需要重新申请。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;redirect_uri：申请授权码时的跳转url，一定和申请授权码时用的redirect_uri一致。&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;（5）授权服务器返回令牌****(access_token)**&lt;/p&gt;
&lt;p&gt;这种模式是四种模式中最安全的一种模式。一般用于client是Web服务器端应用或第三方的原生App调用资源服务&lt;/p&gt;
&lt;p&gt;的时候。因为在这种模式中access_token不会经过浏览器或移动端的App，而是直接从服务端去交换，这样就最大&lt;/p&gt;
&lt;p&gt;限度的减小了令牌泄漏的风险。&lt;/p&gt;
&lt;h5&gt;&lt;strong&gt;6.2.3.2&lt;/strong&gt; &lt;strong&gt;测试&lt;/strong&gt;&lt;/h5&gt;
&lt;p&gt;浏览器访问认证页面：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;http://localhost:53020/uaa/oauth/authorize? client_id=c1&amp;amp;response_type=code&amp;amp;scope=all&amp;amp;redirect_uri=http://www.baidu.com
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;然后输入模拟的账号和密码点登陆之后进入授权页面：&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://tangtangit.com/_astro/image-20210411134211702.BP7LsT3p_rnE9F.webp&quot; alt=&quot;image-20210411134211702&quot; /&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://tangtangit.com/_astro/image-20210411134244863.rPhLR7Nt_1uB67N.webp&quot; alt=&quot;image-20210411134244863&quot; /&gt;&lt;/p&gt;
&lt;p&gt;确认授权后，浏览器会重定向到指定路径（oauth_client_details表中的web_server_redirect_uri）并附加验证码?&lt;/p&gt;
&lt;p&gt;code=DB2mFj（每次不一样），最后使用该验证码获取token。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;POST 
http://localhost:53020/uaa/oauth/token
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;img src=&quot;https://tangtangit.com/_astro/image-20210411134331859.B5BGEvDE_1hMmdc.webp&quot; alt=&quot;image-20210411134331859&quot; /&gt;&lt;/p&gt;
&lt;h4&gt;**6.2.4.**&lt;strong&gt;简化模式&lt;/strong&gt;&lt;/h4&gt;
&lt;h6&gt;&lt;strong&gt;6.2.4.1&lt;/strong&gt; &lt;strong&gt;简化模式介绍&lt;/strong&gt;&lt;/h6&gt;
&lt;p&gt;下图是简化模式交互图：&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://tangtangit.com/_astro/image-20210411134431087.BG3aNznD_Z23eEUF.webp&quot; alt=&quot;image-20210411134431087&quot; /&gt;&lt;/p&gt;
&lt;p&gt;（1）资源拥有者打开客户端，客户端要求资源拥有者给予授权，它将浏览器被重定向到授权服务器，重定向时会**&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;附加客户端的身份信息。如：&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;/uaa/oauth/authorize?client_id=c1&amp;amp;response_type=token&amp;amp;scope=all&amp;amp;redirect_uri=http://www.baidu.com
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;参数描述同&lt;strong&gt;授权码模式&lt;/strong&gt; ，注意response_type=token，说明是简化模式。&lt;/p&gt;
&lt;p&gt;（2）浏览器出现向授权服务器授权页面，之后将用户同意授权。**&lt;/p&gt;
&lt;p&gt;（3）授权服务器将授权码将令牌（&lt;strong&gt;&lt;strong&gt;access_token&lt;/strong&gt;&lt;/strong&gt;）以&lt;strong&gt;&lt;strong&gt;Hash&lt;/strong&gt;&lt;/strong&gt;的形式存放在重定向&lt;strong&gt;&lt;strong&gt;uri&lt;/strong&gt;&lt;/strong&gt;的&lt;strong&gt;&lt;strong&gt;fargment&lt;/strong&gt;&lt;/strong&gt;中发送给浏览***器。&lt;/p&gt;
&lt;p&gt;注：fragment 主要是用来标识 URI 所标识资源里的某个资源，在 URI 的末尾通过 （#）作为 fragment 的开头， 其中 # 不属于 fragment 的值。如https://domain/index#L18这个 URI 中 L18 就是 fragment 的值。大家只需要 知道js通过响应浏览器地址栏变化的方式能获取到fragment 就行了。 一般来说，简化模式用于没有服务器端的第三方单页面应用，因为没有服务器端就无法接收授权码。&lt;/p&gt;
&lt;h6&gt;&lt;strong&gt;6.2.4.2&lt;/strong&gt; &lt;strong&gt;测试&lt;/strong&gt;&lt;/h6&gt;
&lt;p&gt;浏览器访问认证页面&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;http://localhost:53020/uaa/oauth/authorize? client_id=c1&amp;amp;response_type=token&amp;amp;scope=all&amp;amp;redirect_uri=http://www.baidu.com
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;img src=&quot;https://tangtangit.com/_astro/image-20210411134654153.TmOnRaTu_Z1drGcF.webp&quot; alt=&quot;image-20210411134654153&quot; /&gt;&lt;/p&gt;
&lt;p&gt;然后输入模拟的账号和密码点登陆之后进入授权页面：&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://tangtangit.com/_astro/image-20210411134707900.BjU7Vpdn_Z1Mm1On.webp&quot; alt=&quot;image-20210411134707900&quot; /&gt;&lt;/p&gt;
&lt;p&gt;确认授权后，浏览器会重定向到指定路径（oauth_client_details表中的web_server_redirect_uri）并以Hash的形&lt;/p&gt;
&lt;p&gt;式存放在重定向uri的fargment中,如：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;http://aa.bb.cc/receive#access_token=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ0ZW5hbn...
&lt;/code&gt;&lt;/pre&gt;
&lt;h4&gt;**6.2.5.**&lt;strong&gt;密码模式&lt;/strong&gt;&lt;/h4&gt;
&lt;h6&gt;&lt;strong&gt;6.2.5.1&lt;/strong&gt; &lt;strong&gt;授权码模式介绍&lt;/strong&gt;&lt;/h6&gt;
&lt;p&gt;下图是密码模式交互图：&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://tangtangit.com/_astro/image-20210411134802621.C9IEBqPr_2n42JO.webp&quot; alt=&quot;image-20210411134802621&quot; /&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;（&lt;strong&gt;&lt;strong&gt;1&lt;/strong&gt;&lt;/strong&gt;）资源拥有者将用户名、密码发送给客户端&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;（&lt;strong&gt;&lt;strong&gt;2&lt;/strong&gt;&lt;/strong&gt;）客户端拿着资源拥有者的用户名、密码向授权服务器请求令牌（&lt;strong&gt;&lt;strong&gt;access_token&lt;/strong&gt;&lt;/strong&gt;）&lt;/strong&gt;，请求如下：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;/uaa/oauth/token? client_id=c1&amp;amp;client_secret=secret&amp;amp;grant_type=password&amp;amp;username=shangsan&amp;amp;password=123
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;参数列表如下：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;client_id：客户端准入标识。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;client_secret：客户端秘钥。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;grant_type：授权类型，填写password表示密码模式&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;username：资源拥有者用户名。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;password：资源拥有者密码&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;（&lt;strong&gt;&lt;strong&gt;3&lt;/strong&gt;&lt;/strong&gt;）授权服务器将令牌（&lt;strong&gt;&lt;strong&gt;access_token&lt;/strong&gt;&lt;/strong&gt;）发送给****client&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;这种模式十分简单，但是却意味着直接将用户敏感信息泄漏给了client，因此这就说明这种模式只能用于client是我&lt;/p&gt;
&lt;p&gt;们自己开发的情况下。因此密码模式一般用于我们自己开发的，第一方原生App或第一方单页面应用。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;6.2.5.2&lt;/strong&gt; &lt;strong&gt;测试&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;POST http://localhost:53020/uaa/oauth/token
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;请求参数：&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://tangtangit.com/_astro/image-20210411134927267.B6BquYs7_pxe1V.webp&quot; alt=&quot;image-20210411134927267&quot; /&gt;&lt;/p&gt;
&lt;h4&gt;**6.2.6.**&lt;strong&gt;客户端模式&lt;/strong&gt;&lt;/h4&gt;
&lt;h6&gt;&lt;strong&gt;6.2.6.1&lt;/strong&gt; &lt;strong&gt;客户端模式介绍&lt;/strong&gt;&lt;/h6&gt;
&lt;p&gt;&lt;img src=&quot;https://tangtangit.com/_astro/image-20210411134943073.B9DkvKx9_Z1ndCFo.webp&quot; alt=&quot;image-20210411134943073&quot; /&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;（&lt;strong&gt;&lt;strong&gt;1&lt;/strong&gt;&lt;/strong&gt;）客户端向授权服务器发送自己的身份信息，并请求令牌（&lt;strong&gt;&lt;strong&gt;access_token&lt;/strong&gt;&lt;/strong&gt;）&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;（&lt;strong&gt;&lt;strong&gt;2&lt;/strong&gt;&lt;/strong&gt;）确认客户端身份无误后，将令牌（&lt;strong&gt;&lt;strong&gt;access_token&lt;/strong&gt;&lt;/strong&gt;）发送给****client&lt;/strong&gt;，请求如下：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;/uaa/oauth/token?client_id=c1&amp;amp;client_secret=secret&amp;amp;grant_type=client_credentials
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;参数列表如下：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;client_id：客户端准入标识。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;client_secret：客户端秘钥。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;grant_type：授权类型，填写client_credentials表示客户端模式&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;这种模式是最方便但最不安全的模式。因此这就要求我们对client完全的信任，而client本身也是安全的。因 此这种模式一般用来提供给我们完全信任的服务器端服务。比如，合作方系统对接，拉取一组用户信息。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;6.2.6.2&lt;/strong&gt; &lt;strong&gt;客户端模式介绍&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;POST http://localhost:53020/uaa/oauth/token
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;img src=&quot;https://tangtangit.com/_astro/image-20210411135102720.uCjzcGx8_2bpJhl.webp&quot; alt=&quot;image-20210411135102720&quot; /&gt;&lt;/p&gt;
&lt;h4&gt;**6.2.7.**&lt;strong&gt;资源服务测试&lt;/strong&gt;&lt;/h4&gt;
&lt;h5&gt;&lt;strong&gt;6.2.7.1&lt;/strong&gt; &lt;strong&gt;资源服务器配置&lt;/strong&gt;&lt;/h5&gt;
&lt;p&gt;@EnableResourceServer 注解到一个 @Confifiguration 配置类上，并且必须使用 ResourceServerConfifigurer 这个&lt;/p&gt;
&lt;p&gt;配置对象来进行配置（可以选择继承自 ResourceServerConfifigurerAdapter 然后覆写其中的方法，参数就是这个&lt;/p&gt;
&lt;p&gt;对象的实例），下面是一些可以配置的属性：&lt;/p&gt;
&lt;p&gt;ResourceServerSecurityConfifigurer中主要包括：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;tokenServices：ResourceServerTokenServices 类的实例，用来实现令牌服务。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;tokenStore：TokenStore类的实例，指定令牌如何访问，与tokenServices配置可选&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;resourceId：这个资源服务的ID，这个属性是可选的，但是推荐设置并在授权服务中进行验证。&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;其他的拓展属性例如 tokenExtractor 令牌提取器用来提取请求中的令牌。&lt;/p&gt;
&lt;p&gt;HttpSecurity配置这个与Spring Security类似：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;请求匹配器，用来设置需要进行保护的资源路径，默认的情况下是保护资源服务的全部路径。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;通过http.authorizeRequests()来设置受保护资源的访问规则&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;其他的自定义权限保护规则通过 HttpSecurity 来进行配置。&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;@EnableResourceServer 注解自动增加了一个类型为 OAuth2AuthenticationProcessingFilter 的过滤器链&lt;/p&gt;
&lt;p&gt;编写ResouceServerConfifig：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;@Configuration
@EnableResourceServer
@EnableGlobalMethodSecurity(prePostEnabled = true)
public class ResouceServerConfig extends ResourceServerConfigurerAdapter {

    public static final String RESOURCE_ID = &quot;res1&quot;;

    @Autowired
    private TokenStore tokenStore;

    //资源服务令牌解析服务
//    @Bean
//    public ResourceServerTokenServices tokenService() {
//        // 使用远程服务请求授权服务器校验token,必须指定校验token 的url、client_id，client_secret
//        RemoteTokenServices service=new RemoteTokenServices();
//        service.setCheckTokenEndpointUrl(&quot;http://localhost:8003/oauth/check_token&quot;);
//        service.setClientId(&quot;c1&quot;); service.setClientSecret(&quot;secret&quot;);
//        return service;
//    }

    @Override
    public void configure(ResourceServerSecurityConfigurer resources) throws Exception {
        resources
                .resourceId(RESOURCE_ID)
//                .tokenServices(tokenService())
                .tokenStore(tokenStore)
                .stateless(true);
    }

    @Override
    public void configure(HttpSecurity http) throws Exception {
        //可以从数据库查询
        String scop = &quot;all&quot;;
        http.authorizeRequests()
                .antMatchers(&quot;/**&quot;)
                .access(&quot;#oauth2.hasScope(&apos;&quot;+scop+&quot;&apos;)&quot;)
                .and()
                .csrf().disable()
                .sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS);
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h5&gt;&lt;strong&gt;6.2.7.2&lt;/strong&gt; &lt;strong&gt;验证****token&lt;/strong&gt;&lt;/h5&gt;
&lt;p&gt;ResourceServerTokenServices 是组成授权服务的另一半，如果你的授权服务和资源服务在同一个应用程序上的&lt;/p&gt;
&lt;p&gt;话，你可以使用 DefaultTokenServices ，这样的话，你就不用考虑关于实现所有必要的接口的一致性问题。如果&lt;/p&gt;
&lt;p&gt;你的资源服务器是分离开的，那么你就必须要确保能够有匹配授权服务提供的 ResourceServerTokenServices，它&lt;/p&gt;
&lt;p&gt;知道如何对令牌进行解码。令牌解析方法： 使用 DefaultTokenServices 在资源服务器本地配置令牌存储、解码、解析方式 使用&lt;/p&gt;
&lt;p&gt;RemoteTokenServices 资源服务器通过 HTTP 请求来解码令牌，每次都请求授权服务器端点 /oauth/check_token&lt;/p&gt;
&lt;p&gt;使用授权服务的 /oauth/check_token 端点你需要在授权服务将这个端点暴露出去，以便资源服务可以进行访问，&lt;/p&gt;
&lt;p&gt;这在咱们授权服务配置中已经提到了，下面是一个例子,在这个例子中，我们在授权服务中配置了&lt;/p&gt;
&lt;p&gt;/oauth/check_token 和 /oauth/token_key 这两个端点：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;    /**
     * 安全约束
     *
     * @param security security
     * @throws Exception 异常
     */
    @Override
    public void configure(AuthorizationServerSecurityConfigurer security) throws Exception {
        security.tokenKeyAccess(&quot;permitAll()&quot;)  //oauth/token_key 安全配置
                .checkTokenAccess(&quot;permitAll()&quot;)//oauth/check_token 安全配置
                .allowFormAuthenticationForClients();
    }
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;在资源 服务配置RemoteTokenServices ，在ResouceServerConfifig中配置：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;    @Bean
    public ResourceServerTokenServices tokenService() {
        // 使用远程服务请求授权服务器校验token,必须指定校验token 的url、client_id，client_secret
        RemoteTokenServices service=new RemoteTokenServices();
        service.setCheckTokenEndpointUrl(&quot;http://localhost:8003/oauth/check_token&quot;);
        service.setClientId(&quot;c1&quot;); service.setClientSecret(&quot;secret&quot;);
        return service;
    }
&lt;/code&gt;&lt;/pre&gt;
&lt;h5&gt;&lt;strong&gt;6.2.7.3****编写资源&lt;/strong&gt;&lt;/h5&gt;
&lt;p&gt;在controller包下编写OrderController，此controller表示订单资源的访问类：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;@RequestMapping(&quot;/eduservice&quot;)
@RestController
public class OrderController {

    @GetMapping(value = &quot;/r1&quot;)
    @PreAuthorize(&quot;hasAnyAuthority(&apos;p1&apos;)&quot;)
    public String r1(HttpServletRequest request) {
        Object principal = SecurityContextHolder.getContext().getAuthentication().getPrincipal();
        System.out.println(&quot;【principal】=&quot;+principal.toString());

        String str = request.getHeader(&quot;Authorization&quot;);
        System.out.println(&quot;【Authorization】=&quot;+str);
        return &quot;访问资源1&quot;;
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h5&gt;&lt;strong&gt;6.2.7.4&lt;/strong&gt; &lt;strong&gt;添加安全访问控制&lt;/strong&gt;&lt;/h5&gt;
&lt;blockquote&gt;
&lt;p&gt;@EnableGlobalMethodSecurity(securedEnabled = true,prePostEnabled = true)&lt;/p&gt;
&lt;p&gt;这个注解只能用一次，切记resourceServerConfig中不要加入&lt;/p&gt;
&lt;/blockquote&gt;
&lt;pre&gt;&lt;code&gt;@Configuration
@EnableGlobalMethodSecurity(securedEnabled = true,prePostEnabled = true)
public class WebSecurityConfig extends WebSecurityConfigurerAdapter { 
    //安全拦截机制（最重要）
    @Override 
    protected void configure(HttpSecurity http) throws Exception { 
        http.csrf().disable() .authorizeRequests()  
            .antMatchers(&quot;/r/r1&quot;).hasAuthority(&quot;p2&quot;) 
            .antMatchers(&quot;/r/r2&quot;).hasAuthority(&quot;p2&quot;) 
            .antMatchers(&quot;/r/**&quot;).authenticated()//所有/r/**的请求必须认证通过    
            .anyRequest().permitAll();//除了/r/**，其它的请求可以访问
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h5&gt;&lt;strong&gt;6.2.7.5&lt;/strong&gt; &lt;strong&gt;测试&lt;/strong&gt;&lt;/h5&gt;
&lt;p&gt;1、申请令牌&lt;/p&gt;
&lt;p&gt;这里我们使用密码方式&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://tangtangit.com/_astro/image-20210411141400224.CV72107f_1lwwWm.webp&quot; alt=&quot;image-20210411141400224&quot; /&gt;&lt;/p&gt;
&lt;p&gt;2、请求资源&lt;/p&gt;
&lt;p&gt;按照oauth2.0协议要求，请求资源需要携带token，如下：&lt;/p&gt;
&lt;p&gt;token的参数名称为：Authorization，值为：Bearer token值&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://tangtangit.com/_astro/image-20210411141416547.BCjupRjc_ZcFkH6.webp&quot; alt=&quot;image-20210411141416547&quot; /&gt;&lt;/p&gt;
&lt;p&gt;如果token错误，则授权失败，如下&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;{ 
&quot;error&quot;: &quot;invalid_token&quot;, 
&quot;error_description&quot;: &quot;f3360db3‐2d85‐41c9‐ac5e‐b9adba48e26c&quot; 
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;&lt;strong&gt;6.3 JWT****令牌&lt;/strong&gt;&lt;/h3&gt;
&lt;h5&gt;&lt;strong&gt;6.3.1 JWT****介绍&lt;/strong&gt;&lt;/h5&gt;
&lt;p&gt;通过上边的测试我们发现，当资源服务和授权服务不在一起时资源服务使用RemoteTokenServices 远程请求授权&lt;/p&gt;
&lt;p&gt;服务验证token，如果访问量较大将会影响系统的性能 。&lt;/p&gt;
&lt;p&gt;解决上边问题：&lt;/p&gt;
&lt;p&gt;令牌采用JWT格式即可解决上边的问题，用户认证通过会得到一个JWT令牌，JWT令牌中已经包括了用户相关的信&lt;/p&gt;
&lt;p&gt;息，客户端只需要携带JWT访问资源服务，资源服务根据事先约定的算法自行完成令牌校验，无需每次都请求认证&lt;/p&gt;
&lt;p&gt;服务完成授权。&lt;/p&gt;
&lt;p&gt;1、什么是JWT？&lt;/p&gt;
&lt;p&gt;JSON Web Token（JWT）是一个开放的行业标准（RFC 7519），它定义了一种简介的、自包含的协议格式，用于&lt;/p&gt;
&lt;p&gt;在通信双方传递json对象，传递的信息经过数字签名可以被验证和信任。JWT可以使用HMAC算法或使用RSA的公&lt;/p&gt;
&lt;p&gt;钥/私钥对来签名，防止被篡改。&lt;/p&gt;
&lt;p&gt;官网：https://jwt.io/&lt;/p&gt;
&lt;p&gt;标准：https://tools.ietf.org/html/rfc7519&lt;/p&gt;
&lt;p&gt;JWT令牌的优点：&lt;/p&gt;
&lt;p&gt;1）jwt基于json，非常方便解析。&lt;/p&gt;
&lt;p&gt;2）可以在令牌中自定义丰富的内容，易扩展。&lt;/p&gt;
&lt;p&gt;3）通过非对称加密算法及数字签名技术，JWT防止篡改，安全性高。&lt;/p&gt;
&lt;p&gt;4）资源服务使用JWT可不依赖认证服务即可完成授权。缺点：&lt;/p&gt;
&lt;p&gt;１）JWT令牌较长，占存储空间比较大。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;2&lt;/strong&gt;**、&lt;strong&gt;&lt;strong&gt;JWT&lt;/strong&gt;&lt;/strong&gt;令牌结构**&lt;/p&gt;
&lt;p&gt;通过学习JWT令牌结构为自定义jwt令牌打好基础。&lt;/p&gt;
&lt;p&gt;JWT令牌由三部分组成，每部分中间使用点（.）分隔，比如：xxxxx.yyyyy.zzzzz&lt;/p&gt;
&lt;p&gt;Header&lt;/p&gt;
&lt;p&gt;头部包括令牌的类型（即JWT）及使用的哈希算法（如HMAC SHA256或RSA）&lt;/p&gt;
&lt;p&gt;一个例子如下：&lt;/p&gt;
&lt;p&gt;下边是Header部分的内容&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;{ 

&quot;alg&quot;: &quot;HS256&quot;, 

&quot;typ&quot;: &quot;JWT&quot; 

} 
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;将上边的内容使用Base64Url编码，得到一个字符串就是JWT令牌的第一部分。&lt;/p&gt;
&lt;p&gt;Payload&lt;/p&gt;
&lt;p&gt;第二部分是负载，内容也是一个json对象，它是存放有效信息的地方，它可以存放jwt提供的现成字段，比&lt;/p&gt;
&lt;p&gt;如：iss（签发者）,exp（过期时间戳）, sub（面向的用户）等，也可自定义字段。&lt;/p&gt;
&lt;p&gt;此部分不建议存放敏感信息，因为此部分可以解码还原原始内容。&lt;/p&gt;
&lt;p&gt;最后将第二部分负载使用Base64Url编码，得到一个字符串就是JWT令牌的第二部分。&lt;/p&gt;
&lt;p&gt;一个例子：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;{ 

&quot;sub&quot;: &quot;1234567890&quot;, 

&quot;name&quot;: &quot;456&quot;, 

&quot;admin&quot;: true 

} 
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Signatre&lt;/p&gt;
&lt;p&gt;第三部分是签名，此部分用于防止jwt内容被篡改。&lt;/p&gt;
&lt;p&gt;这个部分使用base64url将前两部分进行编码，编码后使用点（.）连接组成字符串，最后使用header中声明&lt;/p&gt;
&lt;p&gt;签名算法进行签名。&lt;/p&gt;
&lt;p&gt;一个例子：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;HMACSHA256( 

base64UrlEncode(header) + &quot;.&quot; + 

base64UrlEncode(payload), 

secret) 
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;base64UrlEncode(header)：jwt令牌的第一部分。&lt;/p&gt;
&lt;p&gt;base64UrlEncode(payload)：jwt令牌的第二部分。&lt;/p&gt;
&lt;p&gt;secret：签名所使用的密钥&lt;/p&gt;
&lt;h5&gt;&lt;strong&gt;6.3.2&lt;/strong&gt; &lt;strong&gt;配置&lt;strong&gt;&lt;strong&gt;JWT&lt;/strong&gt;&lt;/strong&gt;令牌服务&lt;/strong&gt;&lt;/h5&gt;
&lt;p&gt;在uaa中配置jwt令牌服务，即可实现生成jwt格式的令牌。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;@Configuration
public class TokenConfig {

    private String SIGNING_KEY = &quot;uaa123&quot;;

    @Bean
    public TokenStore tokenStore() {
        return new JwtTokenStore(accessTokenConverter());
    }

    @Bean
    public JwtAccessTokenConverter accessTokenConverter() {
        JwtAccessTokenConverter converter = new JwtAccessTokenConverter();
        converter.setSigningKey(SIGNING_KEY);
        //对称秘钥，资源服务器使用该秘钥来验证
        return converter;
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;2、定义JWT令牌服务&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;    @Autowired
    private JwtAccessTokenConverter accessTokenConverter;

    /**
     * 认证服务的token服务
     *
     * @return
     */
    @Bean
    public AuthorizationServerTokenServices tokenService() {
        DefaultTokenServices service = new DefaultTokenServices();
        //默认客户端操作
        service.setClientDetailsService(clientDetailsService);
        service.setSupportRefreshToken(true);
        //设置tokenStore
        service.setTokenStore(tokenStore);

        //token增强
        TokenEnhancerChain tokenEnhancerChain = new TokenEnhancerChain();
        tokenEnhancerChain.setTokenEnhancers(Collections.singletonList(accessTokenConverter));
        service.setTokenEnhancer(tokenEnhancerChain);

        // 令牌默认有效期2小时
        service.setAccessTokenValiditySeconds(7200);
        // 刷新令牌默认有效期3天
        service.setRefreshTokenValiditySeconds(259200);
        return service;
    }
&lt;/code&gt;&lt;/pre&gt;
&lt;h5&gt;&lt;strong&gt;6.3.3&lt;/strong&gt; &lt;strong&gt;生成&lt;strong&gt;&lt;strong&gt;jwt&lt;/strong&gt;&lt;/strong&gt;令牌&lt;/strong&gt;&lt;/h5&gt;
&lt;p&gt;&lt;img src=&quot;https://tangtangit.com/_astro/image-20210411142036948.D0BaEt3z_1FL8bc.webp&quot; alt=&quot;image-20210411142036948&quot; /&gt;&lt;/p&gt;
&lt;h5&gt;&lt;strong&gt;6.3.4&lt;/strong&gt; &lt;strong&gt;校验&lt;strong&gt;&lt;strong&gt;jwt&lt;/strong&gt;&lt;/strong&gt;令牌&lt;/strong&gt;&lt;/h5&gt;
&lt;p&gt;资源服务需要和授权服务拥有一致的签字、令牌服务等：&lt;/p&gt;
&lt;p&gt;1、将授权服务中的TokenConfifig类拷贝到资源 服务中&lt;/p&gt;
&lt;p&gt;2、屏蔽资源 服务原来的令牌服务类&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;@Configuration
@EnableResourceServer
@EnableGlobalMethodSecurity(prePostEnabled = true)
public class ResouceServerConfig extends ResourceServerConfigurerAdapter {

    public static final String RESOURCE_ID = &quot;res1&quot;;

    @Autowired
    private TokenStore tokenStore;

    //资源服务令牌解析服务
//    @Bean
//    public ResourceServerTokenServices tokenService() {
//        // 使用远程服务请求授权服务器校验token,必须指定校验token 的url、client_id，client_secret
//        RemoteTokenServices service=new RemoteTokenServices();
//        service.setCheckTokenEndpointUrl(&quot;http://localhost:8003/oauth/check_token&quot;);
//        service.setClientId(&quot;c1&quot;); service.setClientSecret(&quot;secret&quot;);
//        return service;
//    }

    @Override
    public void configure(ResourceServerSecurityConfigurer resources) throws Exception {
        resources
                .resourceId(RESOURCE_ID)
//                .tokenServices(tokenService())
                .tokenStore(tokenStore)
                .stateless(true);
    }
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;3、测试&lt;/p&gt;
&lt;p&gt;1）申请jwt令牌&lt;/p&gt;
&lt;p&gt;2）使用令牌请求资源&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://tangtangit.com/_astro/image-20210411142157665.BcsOrtCW_CNuDu.webp&quot; alt=&quot;image-20210411142157665&quot; /&gt;&lt;/p&gt;
&lt;p&gt;小技巧：&lt;/p&gt;
&lt;p&gt;令牌申请成功可以使用/uaa/oauth/check_token校验令牌的有效性，并查询令牌的内容，例子如下：&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://tangtangit.com/_astro/image-20210411142213561.cv2ne-IZ_1jQXnt.webp&quot; alt=&quot;image-20210411142213561&quot; /&gt;&lt;/p&gt;
&lt;h3&gt;&lt;strong&gt;6.4&lt;/strong&gt; &lt;strong&gt;完善环境配置&lt;/strong&gt;&lt;/h3&gt;
&lt;p&gt;截止目前客户端信息和授权码仍然存储在内存中，生产环境中通过会存储在数据库中，下边完善环境的配置：&lt;/p&gt;
&lt;h5&gt;&lt;strong&gt;6.4.1&lt;/strong&gt; &lt;strong&gt;创建表&lt;/strong&gt;&lt;/h5&gt;
&lt;p&gt;在user_db中创建如下表：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;SET FOREIGN_KEY_CHECKS=0;

-- ----------------------------
-- Table structure for oauth_client_details
-- ----------------------------
DROP TABLE IF EXISTS `oauth_client_details`;
CREATE TABLE `oauth_client_details` (
  `client_id` varchar(255) NOT NULL,
  `resource_ids` varchar(255) DEFAULT NULL,
  `client_secret` varchar(255) DEFAULT NULL,
  `scope` varchar(255) DEFAULT NULL,
  `authorized_grant_types` varchar(255) DEFAULT NULL,
  `web_server_redirect_uri` varchar(255) DEFAULT NULL,
  `authorities` varchar(255) DEFAULT NULL,
  `access_token_validity` int(11) DEFAULT NULL,
  `refresh_token_validity` int(11) DEFAULT NULL,
  `additional_information` longtext,
  `create_time` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP,
  `archived` tinyint(4) DEFAULT NULL,
  `trusted` tinyint(4) DEFAULT NULL,
  `autoapprove` varchar(255) DEFAULT NULL,
  PRIMARY KEY (`client_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;

-- ----------------------------
-- Records of oauth_client_details
-- ----------------------------
INSERT INTO `oauth_client_details` VALUES (&apos;c1&apos;, &apos;res1&apos;, &apos;$2a$10$Dappn49nM8ZjkCO3ASwR/evnHQN3iPH4npP45js0d9Ug3Vm4ui.sq&apos;, &apos;ROLE_ADMIN,ROLE_USER,ROLE_API&apos;, &apos;authorization_code,password,client_credentials,implicit,refresh_token&apos;, &apos;https://www.baidu.com&apos;, null, &apos;7200&apos;, &apos;259200&apos;, null, &apos;2020-12-13 15:53:10&apos;, &apos;0&apos;, &apos;0&apos;, &apos;false&apos;);
INSERT INTO `oauth_client_details` VALUES (&apos;c2&apos;, &apos;res2&apos;, &apos;secret&apos;, &apos;ROLE_API&apos;, &apos;authorization_code,password,client_credentials,implicit,refresh_token&apos;, &apos;https://www.baidu.com&apos;, null, &apos;7200&apos;, &apos;259200&apos;, null, &apos;2020-12-13 15:53:10&apos;, &apos;0&apos;, &apos;0&apos;, &apos;false&apos;);

&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;oauth_code表，Spring Security OAuth2使用，用来存储授权码：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;-- ----------------------------
-- Table structure for oauth_code
-- ----------------------------
DROP TABLE IF EXISTS `oauth_code`;
CREATE TABLE `oauth_code` (
  `code` varchar(255) DEFAULT NULL,
  `create_time` timestamp NULL DEFAULT CURRENT_TIMESTAMP,
  `authentication` blob
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;

-- ----------------------------
-- Records of oauth_code
-- ----------------------------
&lt;/code&gt;&lt;/pre&gt;
&lt;h5&gt;&lt;strong&gt;6.4.2&lt;/strong&gt; &lt;strong&gt;配置授权服务&lt;/strong&gt;&lt;/h5&gt;
&lt;p&gt;&lt;strong&gt;（&lt;strong&gt;&lt;strong&gt;1&lt;/strong&gt;&lt;/strong&gt;）修改&lt;strong&gt;&lt;strong&gt;AuthorizationServer&lt;/strong&gt;&lt;/strong&gt;：&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;ClientDetailsService和AuthorizationCodeServices从数据库读取数据&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;package com.eim.uaa.config;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.http.HttpMethod;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.oauth2.config.annotation.configurers.ClientDetailsServiceConfigurer;
import org.springframework.security.oauth2.config.annotation.web.configuration.AuthorizationServerConfigurerAdapter;
import org.springframework.security.oauth2.config.annotation.web.configuration.EnableAuthorizationServer;
import org.springframework.security.oauth2.config.annotation.web.configurers.AuthorizationServerEndpointsConfigurer;
import org.springframework.security.oauth2.config.annotation.web.configurers.AuthorizationServerSecurityConfigurer;
import org.springframework.security.oauth2.provider.ClientDetailsService;
import org.springframework.security.oauth2.provider.client.JdbcClientDetailsService;
import org.springframework.security.oauth2.provider.code.AuthorizationCodeServices;
import org.springframework.security.oauth2.provider.code.JdbcAuthorizationCodeServices;
import org.springframework.security.oauth2.provider.token.AuthorizationServerTokenServices;
import org.springframework.security.oauth2.provider.token.DefaultTokenServices;
import org.springframework.security.oauth2.provider.token.TokenEnhancerChain;
import org.springframework.security.oauth2.provider.token.TokenStore;
import org.springframework.security.oauth2.provider.token.store.JwtAccessTokenConverter;

import javax.sql.DataSource;
import java.util.Collections;


/**
 * @author zhengyong
 */
@Configuration
@EnableAuthorizationServer
public class AuthorizationServer extends AuthorizationServerConfigurerAdapter {

    @Autowired
    private TokenStore tokenStore;

    @Autowired
    private ClientDetailsService clientDetailsService;

    @Autowired
    private AuthorizationCodeServices authorizationCodeServices;

    @Autowired
    private AuthenticationManager authenticationManager;

    @Autowired
    private JwtAccessTokenConverter accessTokenConverter;

    @Bean
    public PasswordEncoder passwordEncoder() {
        return new BCryptPasswordEncoder();
    }

    @Bean
    public ClientDetailsService clientDetailsService(DataSource dataSource) {
        ClientDetailsService clientDetailsService = new JdbcClientDetailsService(dataSource);
        ((JdbcClientDetailsService) clientDetailsService).setPasswordEncoder(passwordEncoder());
        return clientDetailsService;
    }

    /**
     * 认证服务的token服务
     *
     * @return
     */
    @Bean
    public AuthorizationServerTokenServices tokenService() {
        DefaultTokenServices service = new DefaultTokenServices();
        //默认客户端操作
        service.setClientDetailsService(clientDetailsService);
        service.setSupportRefreshToken(true);
        //设置tokenStore
        service.setTokenStore(tokenStore);

        TokenEnhancerChain tokenEnhancerChain = new TokenEnhancerChain();
        tokenEnhancerChain.setTokenEnhancers(Collections.singletonList(accessTokenConverter));
        service.setTokenEnhancer(tokenEnhancerChain);

        // 令牌默认有效期2小时
        service.setAccessTokenValiditySeconds(7200);
        // 刷新令牌默认有效期3天
        service.setRefreshTokenValiditySeconds(259200);
        return service;
    }

    //授权码
    @Bean
    public AuthorizationCodeServices authorizationCodeServices(DataSource dataSource) {
        //设置授权码模式的授权码如何 存取，暂时采用内存方式
//        return new InMemoryAuthorizationCodeServices();
        return  new JdbcAuthorizationCodeServices(dataSource);
    }


    /**
     * 安全约束
     *
     * @param security security
     * @throws Exception 异常
     */
    @Override
    public void configure(AuthorizationServerSecurityConfigurer security) throws Exception {
        security.tokenKeyAccess(&quot;permitAll()&quot;)
                .checkTokenAccess(&quot;permitAll()&quot;)
                .allowFormAuthenticationForClients();
    }

    /**
     * 配置令牌访问端点
     *
     * @param endpoints 端点
     */
    @Override
    public void configure(AuthorizationServerEndpointsConfigurer endpoints) {
        endpoints
                //认证管理器
                .authenticationManager(authenticationManager)
                .authorizationCodeServices(authorizationCodeServices)
                .tokenServices(tokenService())
                .allowedTokenEndpointRequestMethods(HttpMethod.POST);
    }

    /**
     * 配置客户端
     *
     * @param clients
     * @throws Exception
     */
    @Override
    public void configure(ClientDetailsServiceConfigurer clients) throws Exception {
         clients.withClientDetails(clientDetailsService);
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;6.4.3****测试&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;1、测试申请令牌&lt;/p&gt;
&lt;p&gt;使用密码模式申请令牌，客户端信息需要和数据库中的信息一致。&lt;/p&gt;
&lt;p&gt;2、测试授权码模式&lt;/p&gt;
&lt;p&gt;生成的授权存储到数据库中。&lt;/p&gt;
&lt;h2&gt;&lt;strong&gt;7.Spring Security****实现分布式系统授权&lt;/strong&gt;&lt;/h2&gt;
&lt;p&gt;&lt;strong&gt;7.1&lt;/strong&gt; &lt;strong&gt;需求分析&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;回顾技术方案如下：&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://tangtangit.com/_astro/image-20210411144558995.D7vRAWiR_evqyB.webp&quot; alt=&quot;image-20210411144558995&quot; /&gt;&lt;/p&gt;
&lt;p&gt;1、UAA认证服务负责认证授权。&lt;/p&gt;
&lt;p&gt;2、所有请求经过 网关到达微服务&lt;/p&gt;
&lt;p&gt;3、网关负责鉴权客户端以及请求转发&lt;/p&gt;
&lt;p&gt;4、网关将token解析后传给微服务，微服务进行授权。&lt;/p&gt;
&lt;h3&gt;**7.2.**&lt;strong&gt;注册中心&lt;/strong&gt;&lt;/h3&gt;
&lt;p&gt;所有微服务的请求都经过网关，网关从注册中心读取微服务的地址，将请求转发至微服务。&lt;/p&gt;
&lt;p&gt;本节完成注册中心的搭建，注册中心采用nacos&lt;/p&gt;
&lt;h3&gt;**7.3.**&lt;strong&gt;网关&lt;/strong&gt;&lt;/h3&gt;
&lt;p&gt;网关整合 OAuth2.0 有&lt;strong&gt;两种思路&lt;/strong&gt;，一种是认证服务器生成jwt令牌, 所有请求统一在网关层验证，判断权限等操作；&lt;/p&gt;
&lt;p&gt;另一种是由各资源服务处理，网关只做请求转发。&lt;/p&gt;
&lt;p&gt;我们选用第一种。我们把API网关作为OAuth2.0的资源服务器角色，实现接入客户端权限拦截、令牌解析并转发当&lt;/p&gt;
&lt;p&gt;前登录用户信息(jsonToken)给微服务，这样下游微服务就不需要关心令牌格式解析以及OAuth2.0相关机制了。&lt;/p&gt;
&lt;p&gt;API网关在认证授权体系里主要负责两件事：&lt;/p&gt;
&lt;p&gt;（1）作为OAuth2.0的&lt;strong&gt;资源服务器&lt;/strong&gt;角色，实现接入方权限拦截。&lt;/p&gt;
&lt;p&gt;（2）令牌解析并转发当前登录用户信息（明文token）给微服务&lt;/p&gt;
&lt;p&gt;微服务拿到明文token(明文token中包含登录用户的身份和权限信息)后也需要做两件事：&lt;/p&gt;
&lt;p&gt;（1）用户授权拦截（看当前用户是否有权访问该资源）&lt;/p&gt;
&lt;p&gt;（2）将用户信息存储进当前线程上下文（有利于后续业务逻辑随时获取当前用户信息）&lt;/p&gt;
&lt;h3&gt;&lt;strong&gt;7.4.&lt;strong&gt;&lt;strong&gt;转发明文&lt;/strong&gt;&lt;/strong&gt;token****给微服务&lt;/strong&gt;&lt;/h3&gt;
&lt;p&gt;通过gateway过滤器的方式实现，目的是让下游微服务能够很方便的获取到当前的登录用户信息（明文token）&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;（&lt;em&gt;&lt;em&gt;&lt;strong&gt;1&lt;/strong&gt;&lt;/em&gt;）实现&lt;/em&gt;&lt;em&gt;&lt;strong&gt;Zuul&lt;/strong&gt;&lt;/em&gt;&lt;em&gt;前置过滤器，完成当前登录用户信息提取，并放入转发微服务的&lt;/em&gt;&lt;em&gt;&lt;strong&gt;request&lt;/strong&gt;&lt;/em&gt;*中&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;JwtTokenFilter&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;package com.eim.gateway.filter;

import com.alibaba.fastjson.JSON;
import com.eim.gateway.common.EncryptUtil;
import io.jsonwebtoken.Claims;
import io.jsonwebtoken.Jwts;
import org.apache.commons.lang3.StringUtils;
import org.springframework.cloud.gateway.filter.GatewayFilterChain;
import org.springframework.cloud.gateway.filter.GlobalFilter;
import org.springframework.core.Ordered;
import org.springframework.http.HttpHeaders;
import org.springframework.http.server.reactive.ServerHttpRequest;
import org.springframework.http.server.reactive.ServerHttpRequestDecorator;
import org.springframework.stereotype.Component;
import org.springframework.util.CollectionUtils;
import org.springframework.web.server.ServerWebExchange;
import reactor.core.publisher.Mono;

import java.net.URI;
import java.nio.charset.StandardCharsets;
import java.util.Arrays;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;

@Component
public class JwtTokenFilter implements GlobalFilter, Ordered {

    /**
     * 跳过校验url数组
     */
    private final String[] skipAuthUrls = {&quot;/api/uaa/oauth/token&quot;};

    @Override
    public Mono&amp;lt;Void&amp;gt; filter(ServerWebExchange exchange, GatewayFilterChain chain) {

        String url = exchange.getRequest().getURI().getPath();
        System.out.println(&quot;【url】=&quot;+url);
        //跳过不需要验证的路径
        if (null != skipAuthUrls &amp;amp;&amp;amp; Arrays.asList(skipAuthUrls).contains(url)) {
            return chain.filter(exchange);
        }
        //获取token
        String token = exchange.getRequest().getHeaders().getFirst(&quot;Authorization&quot;);

        if (StringUtils.isBlank(token)) {
            //没有token
            throw new RuntimeException(&quot;请登陆&quot;);
        } else {
            String[] splitToken = token.split(&quot; &quot;);
            token = splitToken[1];

            System.out.println(&quot;【token】=&quot;+token);
            //解密token
            Claims jwt = getTokenBody(token);

            //判断scope权限
            checkScope(jwt.get(&quot;scope&quot;));

            //解构token
            String userName = (String)jwt.get(&quot;user_name&quot;);
            Integer exp = (Integer) jwt.get(&quot;exp&quot;);
            String clientId = (String) jwt.get(&quot;client_id&quot;);
            List&amp;lt;String&amp;gt; aud = (List&amp;lt;String&amp;gt;) jwt.get(&quot;aud&quot;);
            List&amp;lt;String&amp;gt; scope = (List&amp;lt;String&amp;gt;) jwt.get(&quot;scope&quot;);
            List&amp;lt;String&amp;gt; authorities = (List&amp;lt;String&amp;gt;) jwt.get(&quot;authorities&quot;);
            Map&amp;lt;String,Object&amp;gt; jsonToken = new HashMap&amp;lt;&amp;gt;();
            jsonToken.put(&quot;principal&quot;,userName);
            jsonToken.put(&quot;authorities&quot;,authorities);

            ServerHttpRequest oldRequest= exchange.getRequest();
            URI uri = oldRequest.getURI();

            ServerHttpRequest  newRequest = oldRequest.mutate().uri(uri).build();
            // 定义新的消息头
            HttpHeaders headers = new HttpHeaders();
            //放入以前所有请求头
            headers.putAll(exchange.getRequest().getHeaders());
            //headers.remove(&quot;Authorization&quot;);
            headers.set(&quot;json-token&quot;, EncryptUtil.encodeUTF8StringBase64(JSON.toJSONString(jsonToken)));
            System.out.println(&quot;【jwt.toString()】=&quot;+jwt.toString());


            newRequest = new ServerHttpRequestDecorator(newRequest) {
                @Override
                public HttpHeaders getHeaders() {
                    HttpHeaders httpHeaders = new HttpHeaders();
                    httpHeaders.putAll(headers);
                    return httpHeaders;
                }
            };

            return chain.filter(exchange.mutate().request(newRequest).build());
        }

    }

    /**
     * 判断scope
     * @param scope 范围
     */
    private void checkScope(Object scope) {
        List&amp;lt;String&amp;gt; scopeList = (List&amp;lt;String&amp;gt;)scope;
        String eduScope=&quot;all&quot;;
        List&amp;lt;String&amp;gt; result = scopeList.stream().filter(e -&amp;gt; {
            return e.equals(eduScope);
        }).collect(Collectors.toList());
        if(CollectionUtils.isEmpty(result)){
           throw new RuntimeException(&quot;没有合适的scope权限&quot;);
        }
    }

    private static Claims getTokenBody(String token){
        String signingKey = &quot;uaa123&quot;;
        return Jwts.parser()
                //不使用.getBytes(StandardCharsets.UTF_8)会报错
                .setSigningKey(signingKey.getBytes(StandardCharsets.UTF_8))
                .parseClaimsJws(token)
                .getBody();
    }


    @Override
    public int getOrder() {
        return -201;
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;&lt;strong&gt;7.5****微服务用户鉴权拦截&lt;/strong&gt;&lt;/h3&gt;
&lt;p&gt;当微服务收到明文token时，应该怎么鉴权拦截呢？自己实现一个fifilter？自己解析明文token，自己定义一套资源 访问策略？&lt;/p&gt;
&lt;p&gt;能不能适配Spring Security呢，是不是突然想起了前面我们实现的Spring Security基于token认证例子。咱们还拿 统一用户服务作为网关下游微服务，对它进行改造，增加&lt;strong&gt;微服务用户鉴权拦截&lt;/strong&gt;功能。&lt;/p&gt;
&lt;p&gt;**（&lt;strong&gt;&lt;strong&gt;1&lt;/strong&gt;）增加测试资源&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;OrderController增加以下endpoint&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;@RestController
public class OrderController {

    @GetMapping(value = &quot;/r1&quot;)
    @PreAuthorize(&quot;hasAuthority(&apos;p1&apos;)&quot;)//拥有p1权限方可访问此url
    public String r1(){
        //获取用户身份信息
        UserDTO  userDTO = (UserDTO) SecurityContextHolder.getContext().getAuthentication().getPrincipal();
        return userDTO.getFullname()+&quot;访问资源1&quot;;
    }

    @GetMapping(value = &quot;/r2&quot;)
    @PreAuthorize(&quot;hasAuthority(&apos;p2&apos;)&quot;)//拥有p1权限方可访问此url
    public String r2(){
        //获取用户身份信息
        UserDTO  userDTO = (UserDTO) SecurityContextHolder.getContext().getAuthentication().getPrincipal();
        return userDTO.getFullname()+&quot;访问资源2&quot;;
    }

}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;（&lt;strong&gt;&lt;strong&gt;2&lt;/strong&gt;&lt;/strong&gt;）&lt;strong&gt;&lt;strong&gt;Spring Security&lt;/strong&gt;&lt;/strong&gt;配置&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;开启方法保护，并增加Spring配置策略，除了/login方法不受保护(统一认证要调用)，其他资源全部需要认证才能访 问。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;    @Override
    public void configure(HttpSecurity http) throws Exception {
        //可以从数据库查询
        String scop = &quot;all&quot;;
        http.authorizeRequests()
                .antMatchers(&quot;/**&quot;)
                .access(&quot;#oauth2.hasScope(&apos;&quot;+scop+&quot;&apos;)&quot;)
                .and()
                .csrf().disable()
                .sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS);
    }
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;（3）定义fifilter拦截token，并形成Spring Security的Authentication对象&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;/**
 * 扩展获取用户信息
 */
@Component
public class Myfilter extends OncePerRequestFilter {

    @Override
    protected void doFilterInternal(HttpServletRequest httpServletRequest,
                                    HttpServletResponse httpServletResponse,
                                    FilterChain filterChain) throws ServletException, IOException {
        String token = httpServletRequest.getHeader(&quot;json-token&quot;);
        if (token != null) {
            //1.解析token
            String json = EncryptUtil.decodeUTF8StringBase64(token);
            JSONObject userJson = JSON.parseObject(json);

//            UserDTO user = new UserDTO();
//            user.setUsername(userJson.getString(&quot;principal&quot;));
            String userName = userJson.getString(&quot;principal&quot;);
            JSONArray authoritiesArray = userJson.getJSONArray(&quot;authorities&quot;);
            String[] authorities = authoritiesArray.toArray(new String[authoritiesArray.size()]);

            //2.新建并填充authentication
            UsernamePasswordAuthenticationToken authentication = new UsernamePasswordAuthenticationToken(
                    userName,
                    null,
                    AuthorityUtils.createAuthorityList(authorities));
            authentication.setDetails(new WebAuthenticationDetailsSource().buildDetails(httpServletRequest));
            //3.将authentication保存进安全上下文
            SecurityContextHolder.getContext().setAuthentication(authentication);
        }
        filterChain.doFilter(httpServletRequest, httpServletResponse);
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;经过上边的过虑 器，资源 服务中就可以方便到的获取用户的身份信息：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;SecurityContextHolder.getContext().getAuthentication().getPrincipal();
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;还是三个步骤：&lt;/p&gt;
&lt;p&gt;1.解析token&lt;/p&gt;
&lt;p&gt;2.新建并填充authentication&lt;/p&gt;
&lt;p&gt;3.将authentication保存进安全上下文&lt;/p&gt;
&lt;p&gt;剩下的事儿就交给Spring Security好了。&lt;/p&gt;
&lt;h3&gt;&lt;strong&gt;7.6&lt;/strong&gt; &lt;strong&gt;扩展用户信息&lt;/strong&gt;&lt;/h3&gt;
&lt;h4&gt;&lt;strong&gt;7.6.1&lt;/strong&gt; &lt;strong&gt;需求分析&lt;/strong&gt;&lt;/h4&gt;
&lt;p&gt;目前jwt令牌存储了用户的身份信息、权限信息，网关将token明文化转发给微服务使用，目前用户身份信息仅包括&lt;/p&gt;
&lt;p&gt;了用户的账号，微服务还需要用户的ID、手机号等重要信息。&lt;/p&gt;
&lt;p&gt;所以，本案例将提供扩展用户信息的思路和方法，满足微服务使用用户信息的需求。&lt;/p&gt;
&lt;p&gt;下边分析JWT令牌中扩展用户信息的方案：&lt;/p&gt;
&lt;p&gt;在认证阶段DaoAuthenticationProvider会调用UserDetailService查询用户的信息，这里是可以获取到齐全的用户&lt;/p&gt;
&lt;p&gt;信息的。由于JWT令牌中用户身份信息来源于UserDetails，UserDetails中仅定义了username为用户的身份信息，&lt;/p&gt;
&lt;p&gt;这里有两个思路：第一是可以扩展UserDetails，使之包括更多的自定义属性，第二也可以扩展username的内容&lt;/p&gt;
&lt;p&gt;，比如存入json数据内容作为username的内容。相比较而言，方案二比较简单还不用破坏UserDetails的结构，我&lt;/p&gt;
&lt;p&gt;们采用方案二。&lt;/p&gt;
&lt;h3&gt;&lt;strong&gt;7.6.2&lt;/strong&gt; &lt;strong&gt;修改****UserDetailService&lt;/strong&gt;&lt;/h3&gt;
&lt;p&gt;从数据库查询到user，将整体user转成json存入userDetails对象。&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;关键&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;        //根据账号去数据库查询...
        UserDto user = new UserDto();
        user.setUsername(&quot;zs&quot;);
        user.setId(&quot;2454654&quot;);
        user.setMobile(&quot;12365&quot;);
        //将userDto转成json
        String principal = JSON.toJSONString(user);
&lt;/code&gt;&lt;/pre&gt;
&lt;/blockquote&gt;
&lt;pre&gt;&lt;code&gt;@Service
public class SpringDataUserDetailsServiceImpl implements UserDetailsService {


    //根据 账号查询用户信息
    @Override
    public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {

        //根据账号去数据库查询...
        UserDto user = new UserDto();
        user.setUsername(&quot;zs&quot;);
        user.setId(&quot;2454654&quot;);
        user.setMobile(&quot;12365&quot;);
        //将userDto转成json
        String principal = JSON.toJSONString(user);
        
        List&amp;lt;String&amp;gt; permissions = new ArrayList&amp;lt;&amp;gt;();
        permissions.add(&quot;p1&quot;);
        //将permissions转成数组
        String[] permissionArray = new String[permissions.size()];
        permissions.toArray(permissionArray);

        return User.withUsername(principal)
                .password(new BCryptPasswordEncoder().encode(&quot;123&quot;))
                .authorities(permissionArray).build();
        //将来连接数据库根据账号查询用户信息
//        UserDto userDto = userDao.getUserByUsername(username);
//        if(userDto == null){
//            //如果用户查不到，返回null，由provider来抛出异常
//            return null;
//        }
//        //根据用户的id查询用户的权限
//        List&amp;lt;String&amp;gt; permissions = userDao.findPermissionsByUserId(userDto.getId());
//        //将permissions转成数组
//        String[] permissionArray = new String[permissions.size()];
//        permissions.toArray(permissionArray);
//        //将userDto转成json
//        String principal = JSON.toJSONString(userDto);
//        UserDetails userDetails = User.withUsername(principal).password(userDto.getPassword()).authorities(permissionArray).build();
//        return userDetails;
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;&lt;strong&gt;7.6.3&lt;/strong&gt; &lt;strong&gt;修改资源服务过虑器&lt;/strong&gt;&lt;/h3&gt;
&lt;p&gt;资源服务中的过虑 器负责 从header中解析json-token，从中即可拿网关放入的用户身份信息，部分关键代码如 下：&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;img src=&quot;https://tangtangit.com/_astro/image-20210411173844384.BWssfA_L_qwXH3.webp&quot; alt=&quot;image-20210411173844384&quot; /&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;pre&gt;&lt;code&gt;@Component
public class Myfilter extends OncePerRequestFilter {

    @Override
    protected void doFilterInternal(HttpServletRequest httpServletRequest,
                                    HttpServletResponse httpServletResponse,
                                    FilterChain filterChain) throws ServletException, IOException {
        String token = httpServletRequest.getHeader(&quot;json-token&quot;);
        if (token != null) {
            //1.解析token
            String json = EncryptUtil.decodeUTF8StringBase64(token);
            JSONObject jsonObject = JSON.parseObject(json);

            String principal = jsonObject.getString(&quot;principal&quot;);

            UserDto userDto = JSON.parseObject(principal, UserDto.class);
            System.out.println(&quot;[principal=userDto]&quot;+userDto);


            JSONArray authoritiesArray = jsonObject.getJSONArray(&quot;authorities&quot;);
            String[] authorities = authoritiesArray.toArray(new String[authoritiesArray.size()]);

            //2.新建并填充authentication
            UsernamePasswordAuthenticationToken authentication = new UsernamePasswordAuthenticationToken(
                    userDto,
                    null,
                    AuthorityUtils.createAuthorityList(authorities));
            authentication.setDetails(new WebAuthenticationDetailsSource().buildDetails(httpServletRequest));
            //3.将authentication保存进安全上下文
            SecurityContextHolder.getContext().setAuthentication(authentication);
        }
        filterChain.doFilter(httpServletRequest, httpServletResponse);
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;&lt;strong&gt;7.6.4&lt;/strong&gt; 测试使用&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;    @GetMapping(value = &quot;/r1&quot;)
    @PreAuthorize(&quot;hasAnyAuthority(&apos;p1&apos;)&quot;)
    public String r1(HttpServletRequest request) {
        UserDto userDto = (UserDto) SecurityContextHolder.getContext().getAuthentication().getPrincipal();
        System.out.println(&quot;【userDto】=&quot;+userDto.toString());

        String str = request.getHeader(&quot;Authorization&quot;);
        System.out.println(&quot;【Authorization】=&quot;+str);
        return &quot;访问资源1&quot;;
    }
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;8、回顾&lt;/h2&gt;
&lt;p&gt;重点回顾：&lt;/p&gt;
&lt;p&gt;什么是认证、授权、会话。&lt;/p&gt;
&lt;p&gt;Java Servlet为支持http会话做了哪些事儿。&lt;/p&gt;
&lt;p&gt;基于session认证机制的运作流程。&lt;/p&gt;
&lt;p&gt;基于token认证机制的运作流程。&lt;/p&gt;
&lt;p&gt;理解Spring Security的工作原理，Spring Security结构总览，认证流程和授权，中间涉及到哪些组件，这些组件分&lt;/p&gt;
&lt;p&gt;别处理什么，如何自定义这些组件满足个性需求。&lt;/p&gt;
&lt;p&gt;OAuth2.0认证的四种模式？它们的大体流程是什么？&lt;/p&gt;
&lt;p&gt;Spring cloud Security OAuth2包括哪些组件？职责？&lt;/p&gt;
&lt;p&gt;分布式系统认证需要解决的问题？&lt;/p&gt;
&lt;p&gt;掌握学习方法，掌握思考方式。 .&lt;/p&gt;
&lt;h2&gt;9、生成密码测试&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;public class Test01 {

    public static void main(String[] args) {
        String hashpw = BCrypt.hashpw(&quot;secret&quot;, BCrypt.gensalt());
        System.out.println(hashpw);
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;&lt;strong&gt;附录&lt;/strong&gt;&lt;/h2&gt;
&lt;p&gt;&lt;strong&gt;HttpSecurity&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;HttpSecurity配置列表：&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;openidLogin()&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;用于基于 OpenId 的验证&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;headers()&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;将安全标头添加到响应&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;cors()&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;配置跨域资源共享（ CORS ）&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;sessionManagement()&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;允许配置会话管理&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;portMapper()&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;允许配置一个PortMapper(HttpSecurity#(getSharedObject(class)))，其他提供SecurityConfifigurer的对象使用 PortMapper 从 HTTP 重定&lt;/p&gt;
&lt;p&gt;向到 HTTPS 或者从 HTTPS 重定向到 HTTP。默认情况下，Spring Security使用一个PortMapperImpl映射 HTTP 端口8080到 HTTPS 端口&lt;/p&gt;
&lt;p&gt;8443，HTTP 端口80到 HTTPS 端口443&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;jee()&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;配置基于容器的预认证。 在这种情况下，认证由Servlet容器管理&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;x509()&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;配置基于x509的认证&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;rememberMe&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;允许配置“记住我”的验证&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;authorizeRequests()&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;允许基于使用HttpServletRequest限制访问&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;requestCache()&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;允许配置请求缓存&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;exceptionHandling()&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;允许配置错误处理&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;securityContext()&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;在HttpServletRequests之间的SecurityContextHolder上设置SecurityContext的管理。 当使用WebSecurityConfifigurerAdapter时，这将&lt;/p&gt;
&lt;p&gt;自动应用&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;servletApi()&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;将HttpServletRequest方法与在其上找到的值集成到SecurityContext中。 当使用WebSecurityConfifigurerAdapter时，这将自动应用&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;csrf()&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;添加 CSRF 支持，使用WebSecurityConfifigurerAdapter时，默认启用&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;logout()&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;添加退出登录支持。当使用WebSecurityConfifigurerAdapter时，这将自动应用。默认情况是，访问URL”/ logout”，使HTTP Session无效来&lt;/p&gt;
&lt;p&gt;清除用户，清除已配置的任何#rememberMe()身份验证，清除SecurityContextHolder，然后重定向到”/login?success”&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;anonymous()&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;允许配置匿名用户的表示方法。 当与WebSecurityConfifigurerAdapter结合使用时，这将自动应用。 默认情况下，匿名用户将使用&lt;/p&gt;
&lt;p&gt;org.springframework.security.authentication.AnonymousAuthenticationToken表示，并包含角色 “ROLE_ANONYMOUS”&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;formLogin()&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;指定支持基于表单的身份验证。如果未指定FormLoginConfifigurer#loginPage(String)，则将生成默认登录页面&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;oauth2Login()&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;根据外部OAuth 2.0或OpenID Connect 1.0提供程序配置身份验证&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;requiresChannel()&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;配置通道安全。为了使该配置有用，必须提供至少一个到所需信道的映射&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;httpBasic()&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;配置 Http Basic 验证&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;addFilterAt()&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;在指定的Filter类的位置添加过滤器&lt;/p&gt;
</content:encoded></item><item><title>SpringMVC常见问题：参数解析、序列化、校验与跨域</title><link>https://tangtangit.com/posts/spring%E7%94%9F%E6%80%81-03-springmvc/</link><guid isPermaLink="true">https://tangtangit.com/posts/spring%E7%94%9F%E6%80%81-03-springmvc/</guid><description>解析SpringMVC中@Valid与@Validated校验差异、MVC序列化配置、RequestMapping参数解析机制以及跨域问题的解决方案。</description><pubDate>Tue, 10 Feb 2026 00:00:00 GMT</pubDate><content:encoded>&lt;h2&gt;@valid和@validated&lt;/h2&gt;
&lt;h2&gt;@Valid与@Validated注解&lt;/h2&gt;
&lt;blockquote&gt;
&lt;p&gt;@Valid：常见用在方法，类中字段上进行校验&lt;/p&gt;
&lt;/blockquote&gt;
&lt;blockquote&gt;
&lt;p&gt;@Validated：是spring提供的对@Valid的封装，常见用在方法上进行校验&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;定义的校验类型&lt;/p&gt;
&lt;p&gt;@Null       验证对象是否为null
@NotNull    验证对象是否不为null, 无法查检长度为0的字符串
@NotBlank 检查约束字符串是不是Null还有被Trim的长度是否大于0,只对字符串,且会去掉前后空格.
@NotEmpty 检查约束元素是否为NULL或者是EMPTY.
@CreditCardNumber信用卡验证
@Email  验证是否是邮件地址，如果为null,不进行验证，算通过验证。
@URL(protocol=,host=, port=,regexp=, flags=) ip地址校验&lt;/p&gt;
&lt;p&gt;Booelan检查
@AssertTrue     验证 Boolean 对象是否为 true  
@AssertFalse    验证 Boolean 对象是否为 false  &lt;/p&gt;
&lt;p&gt;长度检查
@Size(min=, max=)   验证对象（Array,Collection,Map,String）长度是否在给定的范围之内  
@Length(min=, max=) Validates that the annotated string is between min and max included.&lt;/p&gt;
&lt;p&gt;日期检查
@Past       验证 Date 和 Calendar 对象是否在当前时间之前  
@Future     验证 Date 和 Calendar 对象是否在当前时间之后  
@Pattern    验证 String 对象是否符合正则表达式的规则&lt;/p&gt;
&lt;p&gt;数值检查，建议使用在Stirng,Integer类型，不建议使用在int类型上，因为表单值为&quot;&quot;时无法转换为int，但可以转换为Stirng为&quot;&quot;,Integer为null
@Min            验证 Number 和 String 对象是否大等于指定的值  
@Max            验证 Number 和 String 对象是否小等于指定的值  
@DecimalMax 被标注的值必须不大于约束中指定的最大值. 这个约束的参数是一个通过BigDecimal定义的最大值的字符串表示.小数存在精度
@DecimalMin 被标注的值必须不小于约束中指定的最小值. 这个约束的参数是一个通过BigDecimal定义的最小值的字符串表示.小数存在精度
@Digits     验证 Number 和 String 的构成是否合法  
@Digits(integer=,fraction=) 验证字符串是否是符合指定格式的数字，interger指定整数精度，fraction指定小数精度。&lt;/p&gt;
&lt;p&gt;注意的几点：&lt;/p&gt;
&lt;p&gt;（1）如果一个bean中包含第二个bean，这时要检验第二个bean中某个字段，即嵌套校验，必须要在第一个bean对象中使用@Valid标注到表示第二个bean对象的字段上，然后再第二个bean对象里面的字段上加上校验类型&lt;/p&gt;
&lt;p&gt;（2）@Validated支持分组注解&lt;/p&gt;
&lt;p&gt;1、先定义一个空接口：GroupA&lt;/p&gt;
&lt;p&gt;2、对bean对象中校验类型，添加分组信息，这里我对version字段进行了分组校验信息添加&lt;/p&gt;
&lt;p&gt;3、方法入参中进行分组信息添加&lt;/p&gt;
&lt;p&gt;如上图所示，则shelveProject方法由于添加了分组信息会校验DeleteProjectRequest对象中的version字段是否为空，而offShelveProject方法没有添加分组信息，不会校验version是否为空。&lt;/p&gt;
&lt;p&gt;博客原文：https://blog.csdn.net/zhenwei1994/article/details/81460419&lt;/p&gt;
&lt;h2&gt;&lt;a href=&quot;https://www.cnblogs.com/xiaoqi/p/spring-valid.html&quot;&gt;Spring中的@Valid 和 @Validated注解你用对了吗&lt;/a&gt;&lt;/h2&gt;
&lt;h3&gt;1.概述&lt;a href=&quot;https://www.cnblogs.com/xiaoqi/p/spring-valid.html#3205863036&quot;&gt;#&lt;/a&gt;&lt;/h3&gt;
&lt;p&gt;本文我们将重点介绍Spring中 &lt;a href=&quot;https://docs.oracle.com/javaee/7/api/javax/validation/Valid.html&quot;&gt;@Valid&lt;/a&gt;和&lt;a href=&quot;https://docs.spring.io/spring/docs/current/javadoc-api/org/springframework/validation/annotation/Validated.html&quot;&gt;@Validated&lt;/a&gt;注解的区别 。&lt;/p&gt;
&lt;p&gt;验证用户输入是否正确是我们应用程序中的常见功能。Spring提供了&lt;code&gt;@Valid&lt;/code&gt;和@&lt;code&gt;Validated&lt;/code&gt;两个注解来实现验证功能，下面我们来详细介绍它们。&lt;/p&gt;
&lt;h3&gt;2. @Valid和@Validate注解&lt;a href=&quot;https://www.cnblogs.com/xiaoqi/p/spring-valid.html#2453788224&quot;&gt;#&lt;/a&gt;&lt;/h3&gt;
&lt;p&gt;在Spring中，我们使用&lt;code&gt;@Valid&lt;/code&gt; 注解进行方法级别验证，同时还能用它来标记成员属性以进行验证。&lt;/p&gt;
&lt;p&gt;但是，此注释不支持分组验证。&lt;code&gt;@Validated&lt;/code&gt;则支持分组验证。&lt;/p&gt;
&lt;h3&gt;3.例子&lt;a href=&quot;https://www.cnblogs.com/xiaoqi/p/spring-valid.html#2449908520&quot;&gt;#&lt;/a&gt;&lt;/h3&gt;
&lt;p&gt;让我们考虑一个使用Spring Boot开发的简单用户注册表单。首先，我们只有&lt;code&gt;名称&lt;/code&gt;和&lt;code&gt;密码&lt;/code&gt;属性：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;Copypublic class UserAccount {

    @NotNull
    @Size(min = 4, max = 15)
    private String password;

    @NotBlank
    private String name;

    // standard constructors / setters / getters / toString

}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;接下来，让我们看一下控制器。在这里，我们将使用带有&lt;code&gt;@Valid&lt;/code&gt;批注的&lt;code&gt;saveBasicInfo&lt;/code&gt;方法来验证用户输入：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;Copy@RequestMapping(value = &quot;/saveBasicInfo&quot;, method = RequestMethod.POST)
public String saveBasicInfo(
  @Valid @ModelAttribute(&quot;useraccount&quot;) UserAccount useraccount, 
  BindingResult result, 
  ModelMap model) {
    if (result.hasErrors()) {
        return &quot;error&quot;;
    }
    return &quot;success&quot;;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;现在让我们测试一下这个方法：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;Copy@Test
public void givenSaveBasicInfo_whenCorrectInput`thenSuccess() throws Exception {
    this.mockMvc.perform(MockMvcRequestBuilders.post(&quot;/saveBasicInfo&quot;)
      .accept(MediaType.TEXT_HTML)
      .param(&quot;name&quot;, &quot;test123&quot;)
      .param(&quot;password&quot;, &quot;pass&quot;))
      .andExpect(view().name(&quot;success&quot;))
      .andExpect(status().isOk())
      .andDo(print());
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;在确认测试成功运行之后，现在让我们扩展功能。下一步的逻辑步骤是将其转换为多步骤注册表格，就像大多数向导一样。第一步，&lt;code&gt;名称&lt;/code&gt;和&lt;code&gt;密码&lt;/code&gt;保持不变。在第二步中，我们将获取其他信息，例如&lt;code&gt;age&lt;/code&gt; 和 &lt;code&gt;phone&lt;/code&gt;。因此，我们将使用以下其他字段更新域对象：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;Copypublic class UserAccount {

    @NotNull
    @Size(min = 4, max = 15)
    private String password;

    @NotBlank
    private String name;

    @Min(value = 18, message = &quot;Age should not be less than 18&quot;)
    private int age;

    @NotBlank
    private String phone;

    // standard constructors / setters / getters / toString   

}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;但是，这一次，我们将注意到先前的测试失败。这是因为我们没有传递&lt;code&gt;年龄&lt;/code&gt;和&lt;code&gt;电话&lt;/code&gt;字段。&lt;/p&gt;
&lt;p&gt;为了支持此行为，我们引入支持分组验证的&lt;code&gt;@Validated&lt;/code&gt;批注。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;分组验证`,就是将字段分组，分别验证，比如我们将用户信息分为两组：`BasicInfo`和`AdvanceInfo
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;可以建立两个空接口：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;Copypublic interface BasicInfo {
}
Copypublic interface AdvanceInfo {
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;第一步将具有&lt;code&gt;BasicInfo&lt;/code&gt;接口，第二步 将具有&lt;code&gt;AdvanceInfo&lt;/code&gt; 。此外，我们将更新&lt;code&gt;UserAccount&lt;/code&gt;类以使用这些标记接口，如下所示：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;Copypublic class UserAccount {

    @NotNull(groups = BasicInfo.class)
    @Size(min = 4, max = 15, groups = BasicInfo.class)
    private String password;

    @NotBlank(groups = BasicInfo.class)
    private String name;

    @Min(value = 18, message = &quot;Age should not be less than 18&quot;, groups = AdvanceInfo.class)
    private int age;

    @NotBlank(groups = AdvanceInfo.class)
    private String phone;

    // standard constructors / setters / getters / toString   

}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;另外，我们现在将更新控制器以使用&lt;code&gt;@Validated&lt;/code&gt;注释而不是&lt;code&gt;@Valid&lt;/code&gt;：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;Copy@RequestMapping(value = &quot;/saveBasicInfoStep1&quot;, method = RequestMethod.POST)
public String saveBasicInfoStep1(
  @Validated(BasicInfo.class) 
  @ModelAttribute(&quot;useraccount&quot;) UserAccount useraccount, 
  BindingResult result, ModelMap model) {
    if (result.hasErrors()) {
        return &quot;error&quot;;
    }
    return &quot;success&quot;;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;更新后，再次执行测试，现在可以成功运行。现在，我们还要测试这个新方法：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;Copy@Test
public void givenSaveBasicInfoStep1`whenCorrectInput`thenSuccess() throws Exception {
    this.mockMvc.perform(MockMvcRequestBuilders.post(&quot;/saveBasicInfoStep1&quot;)
      .accept(MediaType.TEXT_HTML)
      .param(&quot;name&quot;, &quot;test123&quot;)
      .param(&quot;password&quot;, &quot;pass&quot;))
      .andExpect(view().name(&quot;success&quot;))
      .andExpect(status().isOk())
      .andDo(print());
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;也成功运行！&lt;/p&gt;
&lt;p&gt;接下来，让我们看看&lt;code&gt;@Valid&lt;/code&gt;对于触发嵌套属性验证是必不可少的。&lt;/p&gt;
&lt;h3&gt;4.使用&lt;code&gt;@Valid&lt;/code&gt;批注标记嵌套对象&lt;a href=&quot;https://www.cnblogs.com/xiaoqi/p/spring-valid.html#934461804&quot;&gt;#&lt;/a&gt;&lt;/h3&gt;
&lt;p&gt;@Valid 可以用于嵌套对象。例如，在我们当前的场景中，让我们创建一个 &lt;code&gt;UserAddress &lt;/code&gt;对象：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;Copypublic class UserAddress {

    @NotBlank
    private String countryCode;

    // standard constructors / setters / getters / toString
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;为了确保验证此嵌套对象，我们将使用&lt;code&gt;@Valid&lt;/code&gt;批注装饰属性：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;Copypublic class UserAccount {

    //...

    @Valid
    @NotNull(groups = AdvanceInfo.class)
    private UserAddress useraddress;

    // standard constructors / setters / getters / toString 
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;5. 总结&lt;a href=&quot;https://www.cnblogs.com/xiaoqi/p/spring-valid.html#1414765736&quot;&gt;#&lt;/a&gt;&lt;/h3&gt;
&lt;p&gt;&lt;code&gt;@Valid&lt;/code&gt;保证了整个对象的验证, 但是它是对整个对象进行验证，当仅需要部分验证的时候就会出现问题。&lt;/p&gt;
&lt;p&gt;这时候，可以使用&lt;code&gt;@Validated&lt;/code&gt; 进行分组验证。&lt;/p&gt;
&lt;h2&gt;实战：&lt;/h2&gt;
&lt;h3&gt;校验分组&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;/**
 * 新增数据 Group
 *
 * @author zy
 */
public interface AddGroup {
}

/**
 * 更新数据 Group
 *
 * @author zy
 */

public interface UpdateGroup {

}
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;校验工具类：&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;
package com.easypark.common.utils.validator;




import javax.validation.ConstraintViolation;
import javax.validation.Validation;
import javax.validation.Validator;
import java.util.Set;

/**
 * hibernate-validator校验工具类
 *
 * 参考文档：http://docs.jboss.org/hibernate/validator/5.4/reference/en-US/html_single/
 *
 * @author zy
 */
public class ValidatorUtils {
    private static final Validator VALIDATOR;

    static {
        VALIDATOR = Validation.buildDefaultValidatorFactory().getValidator();
    }

    /**
     * 校验对象
     * @param object        待校验对象
     * @param groups        待校验的组
     * @throws RuntimeException  校验不通过，则报RRException异常
     */
    public static void validateEntity(Object object, Class&amp;lt;?&amp;gt;... groups)
            throws RuntimeException {
        Set&amp;lt;ConstraintViolation&amp;lt;Object&amp;gt;&amp;gt; constraintViolations = VALIDATOR.validate(object, groups);
        if (!constraintViolations.isEmpty()) {
            StringBuilder msg = new StringBuilder();
            for(ConstraintViolation&amp;lt;Object&amp;gt; constraint:  constraintViolations){
                msg.append(constraint.getMessage()).append(&quot;&amp;lt;br&amp;gt;&quot;);
            }
            throw new RuntimeException(msg.toString());
        }
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;校验字段：&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;public class CommercialAddOrEditRequest {

    @ApiModelProperty(value = &quot;商户ID（新增的时候不用传值，修改的时候必传！）&quot;)
    @NotBlank(message = &quot;id不能为空&quot;,groups = UpdateGroup.class)
    private String id;

    @ApiModelProperty(value = &quot;商户名称&quot;)
    @NotBlank(message = &quot;商户名称不能为空&quot;,groups = {AddGroup.class,UpdateGroup.class})
    private String commercialName;

    @ApiModelProperty(value = &quot;联系人姓名&quot;)
    @NotBlank(message = &quot;联系人姓名不能为空&quot;,groups = {AddGroup.class,UpdateGroup.class})
    private String contactName;

    @ApiModelProperty(value = &quot;电话&quot;)
    @NotBlank(message = &quot;电话不能为空&quot;,groups = {AddGroup.class,UpdateGroup.class})
    private String phone;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;使用校验：&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt; @ApiOperation(value = &quot;商户新增或修改接口&quot;)
    @PostMapping(value = &quot;/addOrEdit&quot;)
    public JsonResultVO&amp;lt;Object&amp;gt; addOrEdit(@RequestBody CommercialAddOrEditRequest request) {
        if(StringUtils.isNotBlank(request.getId())){
            ValidatorUtils.validateEntity(request, UpdateGroup.class);
        }else {
            ValidatorUtils.validateEntity(request, AddGroup.class);
        }
        commercialService.saveOrUpdateCommercial(request);
        return ResponseHelper.createJsonResult();
    }
&lt;/code&gt;&lt;/pre&gt;
&lt;hr /&gt;
&lt;h2&gt;MVC序列化&lt;/h2&gt;
&lt;h2&gt;springmvc序列化&lt;/h2&gt;
&lt;blockquote&gt;
&lt;p&gt;使用SpringBoot为例，SpringBoot默认使用Jackson作为序列化工具。通过修改Jackson配置即可自定义序列化规则。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h3&gt;@Valid&lt;/h3&gt;
&lt;blockquote&gt;
&lt;p&gt;Spring MVC采用的校验是hibernate-validate&lt;/p&gt;
&lt;/blockquote&gt;
&lt;pre&gt;&lt;code&gt;@NotNull  值不能为空
@Null     值必须为空
@Pattern(regex=) 字符串必须匹配正则表达式
@Size(min=,max=)  集合的元素数量必须在min和max之间
@CreditNumber(ignoreNonDigitCharacters=) 字符串必须是信用卡号(按美国的标准验证)
@Email    字符串必须是Email地址
@Length(min=，max=) 检查字符串的长度
@NotBlank   字符串必须有字符
@Range(min=,max=) 数字必须大于min，小于等于max
@SafeHtml   字符串是安全的html
@URL   字符串是合法的URL
@AssertFalse   值必须是false
@AssertTrue    值必须是true
@DecimalMax(value,inclusive=) 值必须小于等于(inclusive=true)/小于(inclusive=false) value属性指定的值，可以注解在字符串类型的属性上
@DecimalMin(value,inclusive=) 值必须大于等于(inclusive=true)/大于(inclusive=false) value属性指定的值，可以注解在字符串类型的
属性上
@Digits(integer=,fraction=)  数字格式检查，integer指定整数部分的最大长度，fraction指定小数部分的最大长度
@Future  值必须是未来的日志
@Past  值必须是过去的日期
@Max(value=) 值必须小于等于value指定的值，不能注视在字符串类型的属性上
@Min(value=) 值必须大于等于value指定的值，不能注视在字符串类型的属性上
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Jackson 常用注解&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;@JsonNaming(SnakeCaseStrategy.class)&lt;/p&gt;
&lt;p&gt;指定Json字段名映射策略为蛇形大小写策略。缺省则直接使用Bean属性名&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;可用的命名映射策略还有：
KebabCaseStrategy: 肉串策略 - 单词小写，使用连字符&apos;-&apos;连接
SnakeCaseStrategy: 蛇形策略 - 单词小写，使用下划线&apos;_&apos;连接；即老版本中的LowerCaseWithUnderscoresStrategy
LowerCaseStrategy: 小写策略 - 简单的把所有字母全部转为小写，不添加连接符
UpperCamelCaseStrategy: 驼峰策略 - 单词首字母大写其它小写，不添加连接符；即老版本中的PascalCaseStrategy&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;@JsonIgnoreProperties({&quot;id&quot;, &quot;created&quot;, &quot;steps&quot;, &quot;copy&quot;, &quot;stepList&quot;})&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;​         类注解，指定序列化时忽略这些属性，可以用于覆盖超类中默认输出的属性&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;@JsonInclude(Include.NON_EMPTY)&lt;/p&gt;
&lt;p&gt;仅在属性不为空时序列化此字段，对于字符串，即null或空字符串&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;@JsonIgnore&lt;/p&gt;
&lt;p&gt;序列化时忽略此字段&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;@JsonProperty(value = &quot;user_name&quot;)&lt;/p&gt;
&lt;p&gt;指定序列化时的字段名，默认使用属性名&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;@JsonFormat(pattern = &quot;yyyy-MM-dd HH:mm:ss&quot;, timezone = &quot;GMT+8&quot;)&lt;/p&gt;
&lt;p&gt;指定Date类字段序列化时的格式&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;@JsonUnwrapped(prefix = &quot;user_&quot;)&lt;/p&gt;
&lt;p&gt;private User user;
把成员对象中的属性提升到其容器类，并添加给定的前缀，比如上例中: User类中有name和age两个属性，不使用此注解则序列化为：
... &quot;user&quot;: { &quot;name&quot;: &quot;xxx&quot;, &quot;age&quot;: 22 } ...
使用此注解则序列化为：
... &quot;user_name&quot;: &quot;xxx&quot;, &quot;user_age&quot;: 22, ...&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;@JsonIgnoreType&lt;/p&gt;
&lt;p&gt;类注解，序列化时忽略此类&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;@JsonIdentityInfo(generator = ObjectIdGenerators.PropertyGenerator.class,property = &quot;id&quot;)&lt;/p&gt;
&lt;p&gt;作用于类或属性上，被用来在序列化/反序列化时为该对象或字段添加一个对象识别码，通常是用来解决循环嵌套的问题&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;序列化器：&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;实现JsonSerializer&amp;lt;String&amp;gt;&lt;/li&gt;
&lt;li&gt;对应注解上加注解&lt;/li&gt;
&lt;li&gt;需求是：json 字符串不想直接转给前段，而是json对象返回。&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;@Data
public class Test01 {
    @JsonSerialize(using = JsonStringSerilizer.class)
    private String jsond = &quot;[{\&quot;url\&quot;:\&quot;https://dftc-temps.oss-cn-shenzhen.aliyuncs.com/eQEj1SYtXmOdTqNWZlETR4CpVJieMJQI.jpg\&quot;},{\&quot;url\&quot;:\&quot;https://dftc-temps.oss-cn-shenzhen.aliyuncs.com/d5ClnyPCHTuWhBqglVDtIllF2wViDteC.jpg\&quot;},{\&quot;url\&quot;:\&quot;https://dftc-temps.oss-cn-shenzhen.aliyuncs.com/ZGSQX6iAxZ0tZhenH0IIvDlagYloSZLn.jpg\&quot;},{\&quot;url\&quot;:\&quot;https://dftc-temps.oss-cn-shenzhen.aliyuncs.com/KFOqZenayBSNHykp546iEa5Fus6PmxMs.jpg\&quot;}]&quot;;

    private Integer age;
    private Date time;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;/**
 * &amp;lt;p&amp;gt;DESC: &amp;lt;/p&amp;gt;
 * &amp;lt;p&amp;gt;DATE: 2019/6/27&amp;lt;/p&amp;gt;
 * &amp;lt;p&amp;gt;VERSION:1.0.0&amp;lt;/p&amp;gt;
 * &amp;lt;p&amp;gt;@AUTHOR: ZhengYong&amp;lt;/p&amp;gt;
 */
public class JsonStringSerilizer extends JsonSerializer&amp;lt;String&amp;gt; {

    @Override
    public void serialize(String s, JsonGenerator jsonGenerator, SerializerProvider serializerProvider) throws IOException {
        Object o = JSONUtil.toList(new JSONArray(s),Object.class );
        jsonGenerator.writeObject(o);
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;反序列化&lt;/h3&gt;
&lt;h4&gt;前段数据提交：&lt;/h4&gt;
&lt;ul&gt;
&lt;li&gt;from-data表单提交&lt;/li&gt;
&lt;li&gt;application/json 提交&lt;/li&gt;
&lt;/ul&gt;
&lt;h4&gt;1、 from-data&lt;/h4&gt;
&lt;p&gt;springmvc默认转换类：Convert&lt;/p&gt;
&lt;p&gt;可以自定义实现Convert&amp;lt;in,out&amp;gt; 接口，并且注册到spring中，即可生效&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;package com.leyou.auth.service.config;

import org.apache.commons.lang3.StringUtils;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.convert.converter.Converter;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;

import java.text.ParseException;
import java.text.SimpleDateFormat;
import java.util.Date;

/**
 * &amp;lt;p&amp;gt;DESC: &amp;lt;/p&amp;gt;
 * &amp;lt;p&amp;gt;DATE: 2019/6/27&amp;lt;/p&amp;gt;
 * &amp;lt;p&amp;gt;VERSION:1.0.0&amp;lt;/p&amp;gt;
 * &amp;lt;p&amp;gt;@AUTHOR: ZhengYong&amp;lt;/p&amp;gt;
 */
@Configuration
public class MvcConfig implements WebMvcConfigurer {
    @Bean
    public Converter&amp;lt;String, Date&amp;gt; stringToDate(){
        return new Converter&amp;lt;String, Date&amp;gt;() {
            @Override
            public Date convert(String s) {
                if(StringUtils.isNotBlank(s)){
                    SimpleDateFormat simpleDateFormat = new SimpleDateFormat(&quot;yyyy-MM-dd HH:mm:ss&quot;);
                    try {
                        System.out.println(&quot;进入convert&quot;);
                        Date parse = simpleDateFormat.parse(s);
                        return parse;
                    } catch (ParseException e) {
                        e.printStackTrace();
                    }
                }
                return null;
            }
        };
    }
}

&lt;/code&gt;&lt;/pre&gt;
&lt;h4&gt;2、application/json&lt;/h4&gt;
&lt;p&gt;因为传入的时json,springmvc默认参数转换使用的是：facson反序列化器 JsonDeserializer&amp;lt;T&amp;gt;&lt;/p&gt;
&lt;p&gt;注意controller中参数 注解：&lt;strong&gt;@requestBody&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;@RequestMapping(&quot;hello&quot;)
@RestController
public class MvcController {

    @PostMapping(&quot;/hello&quot;)
    public ResponseEntity hello(@RequestBody Test01 test){
        Test01 test01 = new Test01();
        test01.setAge(test.getAge());
        test01.setTime(test.getTime());
        test01.setJsond(&quot;[{\&quot;url\&quot;:\&quot;https://dftc-temps.oss-cn-shenzhen.aliyuncs.com/eQEj1SYtXmOdTqNWZlETR4CpVJieMJQI.jpg\&quot;},{\&quot;url\&quot;:\&quot;https://dftc-temps.oss-cn-shenzhen.aliyuncs.com/d5ClnyPCHTuWhBqglVDtIllF2wViDteC.jpg\&quot;},{\&quot;url\&quot;:\&quot;https://dftc-temps.oss-cn-shenzhen.aliyuncs.com/ZGSQX6iAxZ0tZhenH0IIvDlagYloSZLn.jpg\&quot;},{\&quot;url\&quot;:\&quot;https://dftc-temps.oss-cn-shenzhen.aliyuncs.com/KFOqZenayBSNHykp546iEa5Fus6PmxMs.jpg\&quot;}]&quot;);
        return ResponseEntity.ok(test01);
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;第一步，实现并且，重写反序列化方法。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;package com.leyou.auth.service.serializer;

import com.fasterxml.jackson.core.JsonParser;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.DeserializationContext;
import com.fasterxml.jackson.databind.JsonDeserializer;

import java.io.IOException;
import java.text.DateFormat;
import java.text.ParseException;
import java.text.SimpleDateFormat;
import java.util.Date;

/**
 * &amp;lt;p&amp;gt;DESC: &amp;lt;/p&amp;gt;
 * &amp;lt;p&amp;gt;DATE: 2019/6/27&amp;lt;/p&amp;gt;
 * &amp;lt;p&amp;gt;VERSION:1.0.0&amp;lt;/p&amp;gt;
 * &amp;lt;p&amp;gt;@AUTHOR: ZhengYong&amp;lt;/p&amp;gt;
 */
public class JsonStringDesSerilizer extends JsonDeserializer&amp;lt;Date&amp;gt; {

    @Override
    public Date deserialize(JsonParser jsonParser, DeserializationContext deserializationContext) throws IOException, JsonProcessingException {
        String value = jsonParser.getValueAsString();
        DateFormat dateFormat = new SimpleDateFormat(&quot;yyyy-MM-dd HH:mm:ss&quot;);
        try {
            return dateFormat.parse(value);
        } catch (ParseException e) {
            System.out.println(&quot;反序列化&quot;);
            e.printStackTrace();
        }
        return null;
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;第二步，对应属性添加注解：@&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;@Data
public class Test01 {

    private String jsond = &quot;&quot;;
   
    private Integer age;

    @JsonDeserialize(using = JsonStringDesSerilizer.class)
    private Date time;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h4&gt;3,springboot 配置文件，配置时间序列化&lt;/h4&gt;
&lt;pre&gt;&lt;code&gt;spring:
  datasource:
    url: jdbc:mysql://192.192.192.58/dftc_dev?useUnicode=true&amp;amp;characterEncoding=utf-8&amp;amp;useSSL=false&amp;amp;serverTimezone=GMT%2B8
    driver-class-name: com.mysql.cj.jdbc.Driver
    username: root
    password: dftc@Mysql
  redis:
    sentinel:
      master: master7000
      nodes: 192.192.192.58:26380,192.192.192.58:26381,192.192.192.58:26382
    password: 123456
  jackson:
    date-format: yyyy-MM-dd HH:mm:ss
    time-zone: GMT+8
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;注意： 只能对Date 类型的时间 序列化和反序列化&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;  jackson:
    date-format: yyyy-MM-dd HH:mm:ss
    time-zone: GMT+8
&lt;/code&gt;&lt;/pre&gt;
&lt;h4&gt;4，详细讲解&lt;/h4&gt;
&lt;p&gt;全局配置  全局配置  全局配置  Jackson时间格式化&lt;/p&gt;
&lt;p&gt;mvc  Jackson 返回时间时，默认会返回时间戳或者是 2018-08-30T12:12:12CST格式的时间字符串&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;spring.jackson.date-format=yyyy-MM-dd HH:mm:ss
spring.jackson.time-zone=GMT+8
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;配置了以上参数 只能对Date 类型的时间格式化  对于 LocalDateTime ,Instant 并没有什么效果
只需要添加以下配置就可以解决
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;@Configuration
public class Java8TimeConfig {

    @Value(&quot;${spring.jackson.date-format}&quot;)
    private String formatValue;

    @Bean(name = &quot;format&quot;)
    DateTimeFormatter format() {
        return DateTimeFormatter.ofPattern(formatValue);
    }

    @Bean
    public ObjectMapper serializingObjectMapper(@Qualifier(&quot;format&quot;) DateTimeFormatter format) {
        JavaTimeModule javaTimeModule = new JavaTimeModule();
        javaTimeModule.addSerializer(LocalDateTime.class, new LocalDateTimeSerializer(format));
        javaTimeModule.addSerializer(Instant.class, new InstantCustomSerializer(format));
        javaTimeModule.addSerializer(Date.class, new DateSerializer(false, new SimpleDateFormat(&quot;yyyy-MM-dd HH:mm:ss&quot;)));
        ObjectMapper mapper = new ObjectMapper()
                .registerModule(new ParameterNamesModule())
                .disable(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS)
                .configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false)
                .registerModule(javaTimeModule);
        return mapper;
    }

    class InstantCustomSerializer extends JsonSerializer&amp;lt;Instant&amp;gt; {
        private DateTimeFormatter format;

        private InstantCustomSerializer(DateTimeFormatter formatter) {
            this.format = formatter;
        }

        @Override
        public void serialize(Instant instant, JsonGenerator jsonGenerator, SerializerProvider serializerProvider) throws IOException {
            if (instant == null) {
                return;
            }
            String jsonValue = format.format(instant.atZone(ZoneId.systemDefault()));
            jsonGenerator.writeString(jsonValue);
        }
    }

}
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;需要引入相关jar包 下面是pom文件


        &amp;lt;!--jackson 时间格式化--&amp;gt;
        &amp;lt;dependency&amp;gt;
            &amp;lt;groupId&amp;gt;com.fasterxml.jackson.module&amp;lt;/groupId&amp;gt;
            &amp;lt;artifactId&amp;gt;jackson-module-parameter-names&amp;lt;/artifactId&amp;gt;
        &amp;lt;/dependency&amp;gt;
        &amp;lt;dependency&amp;gt;
            &amp;lt;groupId&amp;gt;com.fasterxml.jackson.datatype&amp;lt;/groupId&amp;gt;
            &amp;lt;artifactId&amp;gt;jackson-datatype-jdk8&amp;lt;/artifactId&amp;gt;
        &amp;lt;/dependency&amp;gt;
        &amp;lt;dependency&amp;gt;
            &amp;lt;groupId&amp;gt;com.fasterxml.jackson.datatype&amp;lt;/groupId&amp;gt;
            &amp;lt;artifactId&amp;gt;jackson-datatype-jsr310&amp;lt;/artifactId&amp;gt;
        &amp;lt;/dependency&amp;gt;
        &amp;lt;dependency&amp;gt;
            &amp;lt;groupId&amp;gt;com.fasterxml.jackson.core&amp;lt;/groupId&amp;gt;
            &amp;lt;artifactId&amp;gt;jackson-core&amp;lt;/artifactId&amp;gt;
        &amp;lt;/dependency&amp;gt;
        &amp;lt;!--jackson 时间格式化--&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;hr /&gt;
&lt;h2&gt;RequestMapping参数解析&lt;/h2&gt;
&lt;p&gt;&lt;strong&gt;Http请求中Content-Type讲解以及在Spring MVC注解中produce和consumes配置详解&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;引言： 在Http请求中，我们每天都在使用Content-type来指定不同格式的请求信息，但是却很少有人去全面了解content-type中允许的值有多少，这里将讲解Content-Type的可用值，以及在spring MVC中如何使用它们来映射请求信息。&lt;/p&gt;
&lt;h2&gt;1、Content-Type&lt;/h2&gt;
&lt;p&gt;MediaType，即是Internet Media Type，互联网媒体类型；也叫做MIME类型，在Http协议消息头中，使用Content-Type来表示具体请求中的媒体类型信息。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;类型格式：type/subtype(;parameter)? type
主类型，任意的字符串，如text，如果是*号代表所有； 
subtype 子类型，任意的字符串，如html，如果是*号代表所有； 
parameter 可选，一些参数，如Accept请求头的q参数， Content-Type的 charset参数。 
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;例如： Content-Type: text/html;charset:utf-8;
 常见的媒体格式类型如下：&lt;/p&gt;
&lt;p&gt;    text/html ： HTML格式
    text/plain ：纯文本格式     
    text/xml ：  XML格式
    image/gif ：gif图片格式   
    image/jpeg ：jpg图片格式
    image/png：png图片格式
   以application开头的媒体格式类型：&lt;/p&gt;
&lt;p&gt;   application/xhtml+xml ：XHTML格式
   application/xml     ： XML数据格式
   application/atom+xml  ：Atom XML聚合格式   
   application/json    ： JSON数据格式
   application/pdf       ：pdf格式 
   application/msword  ： Word文档格式
   application/octet-stream ： 二进制流数据（如常见的文件下载）
   application/x-www-form-urlencoded ： &amp;lt;form encType=””&amp;gt;中默认的encType，form表单数据被编码为key/value格式发送到服务器（表单默认的提交数据的格式）
   另外一种常见的媒体格式是上传文件之时使用的：&lt;/p&gt;
&lt;h2&gt;    multipart/form-data ： 需要在表单中进行文件上传时，就需要使用该格式
     以上就是我们在日常的开发中，经常会用到的若干content-type的内容格式。&lt;/h2&gt;
&lt;h2&gt;2、Spring MVC中关于关于Content-Type类型信息的使用&lt;/h2&gt;
&lt;p&gt;    首先我们来看看RequestMapping中的Class定义：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;@Target({ElementType.METHOD, ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Mapping
public @interface RequestMapping {
      String[] value() default {};
      RequestMethod[] method() default {};
      String[] params() default {};
      String[] headers() default {};
      String[] consumes() default {};
      String[] produces() default {};
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;value:  指定请求的实际地址， 比如 /action/info之类。
method：  指定请求的method类型， GET、POST、PUT、DELETE等
consumes： 指定处理请求的提交内容类型（Content-Type），例如application/json, text/html;
produces:    指定返回的内容类型，仅当request请求头中的(Accept)类型中包含该指定类型才返回
params： 指定request中必须包含某些参数值是，才让该方法处理
headers： 指定request中必须包含某些指定的header值，才能让该方法处理请求&lt;/p&gt;
&lt;h2&gt;3、使用示例&lt;/h2&gt;
&lt;p&gt;  3.1 headers&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;  @RequestMapping(value = &quot;/test&quot;, method = RequestMethod.GET, headers=&quot;Referer=http://www.ifeng.com/&quot;)  
  public void testHeaders(@PathVariable String ownerId, @PathVariable String petId) {      
    // implementation omitted  
  } 
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;  这里的Headers里面可以匹配所有Header里面可以出现的信息，不局限在Referer信息。
  示例2&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;@RequestMapping(value = &quot;/response/ContentType&quot;, headers = &quot;Accept=application/json&quot;)  
public void response2(HttpServletResponse response) throws IOException {  
    //表示响应的内容区数据的媒体类型为json格式，且编码为utf-8(客户端应该以utf-8解码)  
    response.setContentType(&quot;application/json;charset=utf-8&quot;);  
    //写出响应体内容  
    String jsonData = &quot;{&quot;username&quot;:&quot;zhang&quot;, &quot;password&quot;:&quot;123&quot;}&quot;;  
    response.getWriter().write(jsonData);  
}  
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;服务器根据请求头“Accept=application/json”生产json数据。&lt;/h2&gt;
&lt;p&gt;当你有如下Accept头，将遵守如下规则进行应用：
①Accept：text/html,application/xml,application/json
      将按照如下顺序进行produces的匹配 ①text/html ②application/xml ③application/json
②Accept：application/xml;q=0.5,application/json;q=0.9,text/html
      将按照如下顺序进行produces的匹配 ①text/html ②application/json ③application/xml
      参数为媒体类型的质量因子，越大则优先权越高(从0到1)
③Accept：&lt;em&gt;/&lt;/em&gt;,text/*,text/html&lt;/p&gt;
&lt;p&gt;  将按照如下顺序进行produces的匹配 ①text/html ②text/* ③*/*&lt;/p&gt;
&lt;p&gt;即匹配规则为：最明确的优先匹配。&lt;/p&gt;
&lt;p&gt;Requests部分
Header	解释	示例
Accept	指定客户端能够接收的内容类型	Accept: text/plain, text/html
Accept-Charset	浏览器可以接受的字符编码集。	Accept-Charset: iso-8859-5
Accept-Encoding	指定浏览器可以支持的web服务器返回内容压缩编码类型。	Accept-Encoding: compress, gzip
Accept-Language	浏览器可接受的语言	Accept-Language: en,zh
Accept-Ranges	可以请求网页实体的一个或者多个子范围字段	Accept-Ranges: bytes
Authorization	HTTP授权的授权证书	Authorization: Basic QWxhZGRpbjpvcGVuIHNlc2FtZQ==
Cache-Control	指定请求和响应遵循的缓存机制	Cache-Control: no-cache
Connection	表示是否需要持久连接。（HTTP 1.1默认进行持久连接）	Connection: close
Cookie	HTTP请求发送时，会把保存在该请求域名下的所有cookie值一起发送给web服务器。	Cookie: $Version=1; Skin=new;
Content-Length	请求的内容长度	Content-Length: 348
Content-Type	请求的与实体对应的MIME信息	Content-Type: application/x-www-form-urlencoded
Date	请求发送的日期和时间	Date: Tue, 15 Nov 2010 08:12:31 GMT
Expect	请求的特定的服务器行为	Expect: 100-continue
From	发出请求的用户的Email	From: user@email.com
Host	指定请求的服务器的域名和端口号	Host: www.zcmhi.com
If-Match	只有请求内容与实体相匹配才有效	If-Match: “737060cd8c284d8af7ad3082f209582d”
If-Modified-Since	如果请求的部分在指定时间之后被修改则请求成功，未被修改则返回304代码	If-Modified-Since: Sat, 29 Oct 2010 19:43:31 GMT
If-None-Match	如果内容未改变返回304代码，参数为服务器先前发送的Etag，与服务器回应的Etag比较判断是否改变	If-None-Match: “737060cd8c284d8af7ad3082f209582d”
If-Range	如果实体未改变，服务器发送客户端丢失的部分，否则发送整个实体。参数也为Etag	If-Range: “737060cd8c284d8af7ad3082f209582d”
If-Unmodified-Since	只在实体在指定时间之后未被修改才请求成功	If-Unmodified-Since: Sat, 29 Oct 2010 19:43:31 GMT
Max-Forwards	限制信息通过代理和网关传送的时间	Max-Forwards: 10
Pragma	用来包含实现特定的指令	Pragma: no-cache
Proxy-Authorization	连接到代理的授权证书	Proxy-Authorization: Basic QWxhZGRpbjpvcGVuIHNlc2FtZQ==
Range	只请求实体的一部分，指定范围	Range: bytes=500-999
Referer	先前网页的地址，当前请求网页紧随其后,即来路	Referer: http://www.zcmhi.com/archives/71.html
TE	客户端愿意接受的传输编码，并通知服务器接受接受尾加头信息	TE: trailers,deflate;q=0.5
Upgrade	向服务器指定某种传输协议以便服务器进行转换（如果支持）	Upgrade: HTTP/2.0, SHTTP/1.3, IRC/6.9, RTA/x11
User-Agent	User-Agent的内容包含发出请求的用户信息	User-Agent: Mozilla/5.0 (Linux; X11)
Via	通知中间网关或代理服务器地址，通信协议	Via: 1.0 fred, 1.1 nowhere.com (Apache/1.1)
Warning	关于消息实体的警告信息	Warn: 199 Miscellaneous warning
Responses 部分 
Header	解释	示例
Accept-Ranges	表明服务器是否支持指定范围请求及哪种类型的分段请求	Accept-Ranges: bytes
Age	从原始服务器到代理缓存形成的估算时间（以秒计，非负）	Age: 12
Allow	对某网络资源的有效的请求行为，不允许则返回405	Allow: GET, HEAD
Cache-Control	告诉所有的缓存机制是否可以缓存及哪种类型	Cache-Control: no-cache
Content-Encoding	web服务器支持的返回内容压缩编码类型。	Content-Encoding: gzip
Content-Language	响应体的语言	Content-Language: en,zh
Content-Length	响应体的长度	Content-Length: 348
Content-Location	请求资源可替代的备用的另一地址	Content-Location: /index.htm
Content-MD5	返回资源的MD5校验值	Content-MD5: Q2hlY2sgSW50ZWdyaXR5IQ==
Content-Range	在整个返回体中本部分的字节位置	Content-Range: bytes 21010-47021/47022
Content-Type	返回内容的MIME类型	Content-Type: text/html; charset=utf-8
Date	原始服务器消息发出的时间	Date: Tue, 15 Nov 2010 08:12:31 GMT
ETag	请求变量的实体标签的当前值	ETag: “737060cd8c284d8af7ad3082f209582d”
Expires	响应过期的日期和时间	Expires: Thu, 01 Dec 2010 16:00:00 GMT
Last-Modified	请求资源的最后修改时间	Last-Modified: Tue, 15 Nov 2010 12:45:26 GMT
Location	用来重定向接收方到非请求URL的位置来完成请求或标识新的资源	Location: http://www.zcmhi.com/archives/94.html
Pragma	包括实现特定的指令，它可应用到响应链上的任何接收方	Pragma: no-cache
Proxy-Authenticate	它指出认证方案和可应用到代理的该URL上的参数	Proxy-Authenticate: Basic
refresh	应用于重定向或一个新的资源被创造，在5秒之后重定向（由网景提出，被大部分浏览器支持）&lt;/p&gt;
&lt;p&gt; &lt;/p&gt;
&lt;p&gt;Refresh: 5; url=
http://www.zcmhi.com/archives/94.html
Retry-After	如果实体暂时不可取，通知客户端在指定时间之后再次尝试	Retry-After: 120
Server	web服务器软件名称	Server: Apache/1.3.27 (Unix) (Red-Hat/Linux)
Set-Cookie	设置Http Cookie	Set-Cookie: UserID=JohnDoe; Max-Age=3600; Version=1
Trailer	指出头域在分块传输编码的尾部存在	Trailer: Max-Forwards
Transfer-Encoding	文件传输编码	Transfer-Encoding:chunked
Vary	告诉下游代理是使用缓存响应还是从原始服务器请求	Vary: *
Via	告知代理客户端响应是通过哪里发送的	Via: 1.0 fred, 1.1 nowhere.com (Apache/1.1)
Warning	警告实体可能存在的问题	Warning: 199 Miscellaneous warning
WWW-Authenticate	表明客户端请求实体应该使用的授权方案	WWW-Authenticate: Basic
3.2 params的示例&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;  @RequestMapping(value = &quot;/test/{userId}&quot;, method = RequestMethod.GET, params=&quot;myParam=myValue&quot;)  
  public void findUser(@PathVariable String userId) {      
    // implementation omitted  
  }  

&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;  仅处理请求中包含了名为“myParam”，值为“myValue”的请求，起到了一个过滤的作用。
3.3 consumes/produces&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;@Controller  
@RequestMapping(value = &quot;/users&quot;, method = RequestMethod.POST, consumes=&quot;application/json&quot;, produces=&quot;application/json&quot;)  
@ResponseBody
public List&amp;lt;User&amp;gt; addUser(@RequestBody User userl) {      
    // implementation omitted  
    return List&amp;lt;User&amp;gt; users;
}  
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;  方法仅处理request Content-Type为“application/json”类型的请求. produces标识==&amp;gt;处理request请求中Accept头中包含了&quot;application/json&quot;的请求，同时暗示了返回的内容类型为application/json;&lt;/p&gt;
&lt;h2&gt;4、总结&lt;/h2&gt;
&lt;h2&gt;  在本文中，首先介绍了Content-Type主要支持的格式内容，然后基于@RequestMapping标注的内容介绍了主要的使用方法，其中,headers, consumes,produces,都是使用Content-Type中使用的各种媒体格式内容，可以基于这个格式内容来进行访问的控制和过滤。&lt;/h2&gt;
&lt;hr /&gt;
&lt;h2&gt;跨域问题&lt;/h2&gt;
&lt;h2&gt;一、什么是跨域&lt;/h2&gt;
&lt;p&gt;跨域是指跨域名的访问，以下情况都属于跨域：&lt;/p&gt;
&lt;p&gt;跨域原因说明   	示例&lt;br /&gt;
域名不同     	www.jd.com 与 www.taobao.com&lt;br /&gt;
域名相同，端口不同	www.jd.com:8080 与 www.jd.com:8081
二级域名不同   	item.jd.com 与 miaosha.jd.com&lt;/p&gt;
&lt;p&gt;如果域名和端口都相同，但是请求路径不同，不属于跨域，如：&lt;/p&gt;
&lt;p&gt;www.jd.com/item&lt;/p&gt;
&lt;p&gt;www.jd.com/goods&lt;/p&gt;
&lt;p&gt;而我们刚才是从manage.leyou.com去访问api.leyou.com，这属于二级域名不同，跨域了。&lt;/p&gt;
&lt;h2&gt;二.为什么有跨域问题？&lt;/h2&gt;
&lt;p&gt;跨域不一定会有跨域问题。&lt;/p&gt;
&lt;p&gt;因为跨域问题是浏览器对于ajax请求的一种安全限制：一个页面发起的ajax请求，只能是于当前页同域名的路径，这能有效的阻止跨站攻击。&lt;/p&gt;
&lt;p&gt;因此：跨域问题 是针对ajax的一种限制。&lt;/p&gt;
&lt;p&gt;但是这却给我们的开发带来了不变，而且在实际生成环境中，肯定会有很多台服务器之间交互，地址和端口都可能不同，怎么办？&lt;/p&gt;
&lt;h2&gt;三.解决跨域问题的方案&lt;/h2&gt;
&lt;p&gt;目前比较常用的跨域解决方案有3种：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Jsonp
最早的解决方案，利用script标签可以跨域的原理实现。
限制：
&lt;ul&gt;
&lt;li&gt;需要服务的支持&lt;/li&gt;
&lt;li&gt;只能发起GET请求&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;nginx反向代理
思路是：利用nginx反向代理把跨域为不跨域，支持各种请求方式
缺点：需要在nginx进行额外配置，语义不清晰&lt;/li&gt;
&lt;li&gt;CORS
规范化的跨域请求解决方案，安全可靠。
优势：
&lt;ul&gt;
&lt;li&gt;在服务端进行控制是否允许跨域，可自定义规则&lt;/li&gt;
&lt;li&gt;支持各种请求方式
缺点：&lt;/li&gt;
&lt;li&gt;会产生额外的请求&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;我们这里会采用cors的跨域方案。&lt;/p&gt;
&lt;h2&gt;四.cors解决跨域&lt;/h2&gt;
&lt;h3&gt;1.什么是cors&lt;/h3&gt;
&lt;p&gt;CORS是一个W3C标准，全称是&quot;跨域资源共享&quot;（Cross-origin resource sharing）。&lt;/p&gt;
&lt;p&gt;它允许浏览器向跨源服务器，发出XMLHttpRequest请求，从而克服了AJAX只能同源使用的限制。&lt;/p&gt;
&lt;p&gt;CORS需要浏览器和服务器同时支持。目前，所有浏览器都支持该功能，IE浏览器不能低于IE10。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;浏览器端：
目前，所有浏览器都支持该功能（IE10以下不行）。整个CORS通信过程，都是浏览器自动完成，不需要用户参与。&lt;/li&gt;
&lt;li&gt;服务端：
CORS通信与AJAX没有任何差别，因此你不需要改变以前的业务逻辑。只不过，浏览器会在请求中携带一些头信息，我们需要以此判断是否运行其跨域，然后在响应头中加入一些信息即可。这一般通过过滤器完成即可。&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;2.原理有点复杂&lt;/h3&gt;
&lt;p&gt;浏览器会将ajax请求分为两类，其处理方案略有差异：简单请求、特殊请求。&lt;/p&gt;
&lt;p&gt;简单请求&lt;/p&gt;
&lt;p&gt;只要同时满足以下两大条件，就属于简单请求。：&lt;/p&gt;
&lt;p&gt;（1) 请求方法是以下三种方法之一：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;HEAD&lt;/li&gt;
&lt;li&gt;GET&lt;/li&gt;
&lt;li&gt;POST&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;（2）HTTP的头信息不超出以下几种字段：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Accept&lt;/li&gt;
&lt;li&gt;Accept-Language&lt;/li&gt;
&lt;li&gt;Content-Language&lt;/li&gt;
&lt;li&gt;Last-Event-ID&lt;/li&gt;
&lt;li&gt;Content-Type：只限于三个值application/x-www-form-urlencoded、multipart/form-data、text/plain&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;当浏览器发现发现的ajax请求是简单请求时，会在请求头中携带一个字段：Origin.&lt;/p&gt;
&lt;p&gt;Origin中会指出当前请求属于哪个域（协议+域名+端口）。服务会根据这个值决定是否允许其跨域。&lt;/p&gt;
&lt;p&gt;如果服务器允许跨域，需要在返回的响应头中携带下面信息：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;Access-Control-Allow-Origin: http://manage.leyou.com
Access-Control-Allow-Credentials: true
Content-Type: text/html; charset=utf-8
&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;Access-Control-Allow-Origin：可接受的域，是一个具体域名或者*，代表任意&lt;/li&gt;
&lt;li&gt;Access-Control-Allow-Credentials：是否允许携带cookie，默认情况下，cors不会携带cookie，除非这个值是true&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;注意：&lt;/p&gt;
&lt;p&gt;如果跨域请求要想操作cookie，需要满足3个条件：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;服务的响应头中需要携带Access-Control-Allow-Credentials并且为true。&lt;/li&gt;
&lt;li&gt;浏览器发起ajax需要指定withCredentials 为true&lt;/li&gt;
&lt;li&gt;响应头中的Access-Control-Allow-Origin一定不能为*，必须是指定的域名&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;特殊请求&lt;/p&gt;
&lt;p&gt;不符合简单请求的条件，会被浏览器判定为特殊请求,，例如请求方式为PUT。&lt;/p&gt;
&lt;p&gt;预检请求&lt;/p&gt;
&lt;p&gt;特殊请求会在正式通信之前，增加一次HTTP查询请求，称为&quot;预检&quot;请求（preflight）。&lt;/p&gt;
&lt;p&gt;浏览器先询问服务器，当前网页所在的域名是否在服务器的许可名单之中，以及可以使用哪些HTTP动词和头信息字段。只有得到肯定答复，浏览器才会发出正式的XMLHttpRequest请求，否则就报错。&lt;/p&gt;
&lt;p&gt;一个“预检”请求的样板：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;OPTIONS /cors HTTP/1.1
Origin: http://manage.leyou.com
Access-Control-Request-Method: PUT
Access-Control-Request-Headers: X-Custom-Header
Host: api.leyou.com
Accept-Language: en-US
Connection: keep-alive
User-Agent: Mozilla/5.0...
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;与简单请求相比，除了Origin以外，多了两个头：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Access-Control-Request-Method：接下来会用到的请求方式，比如PUT&lt;/li&gt;
&lt;li&gt;Access-Control-Request-Headers：会额外用到的头信息&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;预检请求的响应&lt;/p&gt;
&lt;p&gt;服务的收到预检请求，如果许可跨域，会发出响应：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;HTTP/1.1 200 OK
Date: Mon, 01 Dec 2008 01:15:39 GMT
Server: Apache/2.0.61 (Unix)
Access-Control-Allow-Origin: http://manage.leyou.com
Access-Control-Allow-Credentials: true
Access-Control-Allow-Methods: GET, POST, PUT
Access-Control-Allow-Headers: X-Custom-Header
Access-Control-Max-Age: 1728000
Content-Type: text/html; charset=utf-8
Content-Encoding: gzip
Content-Length: 0
Keep-Alive: timeout=2, max=100
Connection: Keep-Alive
Content-Type: text/plain
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;除了Access-Control-Allow-Origin和Access-Control-Allow-Credentials以外，这里又额外多出3个头：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Access-Control-Allow-Methods：允许访问的方式&lt;/li&gt;
&lt;li&gt;Access-Control-Allow-Headers：允许携带的头&lt;/li&gt;
&lt;li&gt;Access-Control-Max-Age：本次许可的有效时长，单位是秒，过期之前的ajax请求就无需再次进行预检了&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;如果浏览器得到上述响应，则认定为可以跨域，后续就跟简单请求的处理是一样的了。&lt;/p&gt;
&lt;h3&gt;3.实现非常简单&lt;/h3&gt;
&lt;p&gt;虽然原理比较复杂，但是前面说过：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;浏览器端都有浏览器自动完成，我们无需操心&lt;/li&gt;
&lt;li&gt;服务端可以通过拦截器统一实现，不必每次都去进行跨域判定的编写。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;事实上，SpringMVC已经帮我们写好了CORS的跨域过滤器：CorsFilter ,内部已经实现了刚才所讲的判定逻辑，我们直接用就好了。&lt;/p&gt;
&lt;p&gt;在ly-api-gateway中编写一个配置类，并且注册CorsFilter：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.cors.CorsConfiguration;
import org.springframework.web.cors.UrlBasedCorsConfigurationSource;
import org.springframework.web.filter.CorsFilter;

@Configuration
public class GlobalCorsConfig {
    @Bean
    public CorsFilter corsFilter() {
        //1.添加CORS配置信息
        CorsConfiguration config = new CorsConfiguration();
        //1) 允许的域,不要写*，否则cookie就无法使用了
        config.addAllowedOrigin(&quot;http://manage.leyou.com&quot;);
        //2) 是否发送Cookie信息
        config.setAllowCredentials(true);
        //3) 允许的请求方式
        config.addAllowedMethod(&quot;OPTIONS&quot;);
        config.addAllowedMethod(&quot;HEAD&quot;);
        config.addAllowedMethod(&quot;GET&quot;);
        config.addAllowedMethod(&quot;PUT&quot;);
        config.addAllowedMethod(&quot;POST&quot;);
        config.addAllowedMethod(&quot;DELETE&quot;);
        config.addAllowedMethod(&quot;PATCH&quot;);
        // 4）允许的头信息
        config.addAllowedHeader(&quot;*&quot;);
		// 5）设置最大生命周期
        config.setMaxAge(3600L);
        //2.添加映射路径，我们拦截一切请求
        UrlBasedCorsConfigurationSource configSource = new UrlBasedCorsConfigurationSource();
        configSource.registerCorsConfiguration(&quot;/**&quot;, config);

        //3.返回新的CorsFilter.
        return new CorsFilter(configSource);
    }
}
&lt;/code&gt;&lt;/pre&gt;
</content:encoded></item><item><title>SpringBoot核心原理与自动装配详解</title><link>https://tangtangit.com/posts/spring%E7%94%9F%E6%80%81-02-springboot/</link><guid isPermaLink="true">https://tangtangit.com/posts/spring%E7%94%9F%E6%80%81-02-springboot/</guid><description>全面讲解SpringBoot自动装配机制、启动流程、Tomcat嵌入原理、配置文件、条件注解等核心知识。</description><pubDate>Fri, 06 Feb 2026 00:00:00 GMT</pubDate><content:encoded>&lt;h2&gt;一、Spring Boot 入门&lt;/h2&gt;
&lt;h3&gt;1）、Spring Boot 简介&lt;/h3&gt;
&lt;blockquote&gt;
&lt;p&gt;简化Spring应用开发的一个框架；&lt;/p&gt;
&lt;p&gt;整个Spring技术栈的一个大整合；&lt;/p&gt;
&lt;p&gt;J2EE开发的一站式解决方案；&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h3&gt;2）、微服务&lt;/h3&gt;
&lt;p&gt;2014，martin fowler&lt;/p&gt;
&lt;p&gt;微服务：架构风格（服务微化）&lt;/p&gt;
&lt;p&gt;一个应用应该是一组小型服务；可以通过HTTP的方式进行互通；&lt;/p&gt;
&lt;p&gt;单体应用：ALL IN ONE&lt;/p&gt;
&lt;p&gt;微服务：每一个功能元素最终都是一个可独立替换和独立升级的软件单元；&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;https://martinfowler.com/articles/microservices.html#MicroservicesAndSoa&quot;&gt;详细参照微服务文档&lt;/a&gt;&lt;/p&gt;
&lt;h3&gt;3）、环境准备&lt;/h3&gt;
&lt;p&gt;环境约束&lt;/p&gt;
&lt;p&gt;–jdk1.8：Spring Boot 推荐jdk1.7及以上；java version &quot;1.8.0_112&quot;&lt;/p&gt;
&lt;p&gt;–maven3.x：maven 3.3以上版本；Apache Maven 3.3.9&lt;/p&gt;
&lt;p&gt;–IntelliJIDEA2017：IntelliJ IDEA 2017.2.2 x64、STS&lt;/p&gt;
&lt;p&gt;–SpringBoot 1.5.9.RELEASE：1.5.9；&lt;/p&gt;
&lt;p&gt;统一环境；&lt;/p&gt;
&lt;h3&gt;4、Spring Boot HelloWorld&lt;/h3&gt;
&lt;p&gt;一个功能：&lt;/p&gt;
&lt;p&gt;浏览器发送hello请求，服务器接受请求并处理，响应Hello World字符串；&lt;/p&gt;
&lt;h4&gt;1、创建一个maven工程；（jar）&lt;/h4&gt;
&lt;h4&gt;2、导入spring boot相关的依赖&lt;/h4&gt;
&lt;pre&gt;&lt;code&gt;    &amp;lt;parent&amp;gt;
        &amp;lt;groupId&amp;gt;org.springframework.boot&amp;lt;/groupId&amp;gt;
        &amp;lt;artifactId&amp;gt;spring-boot-starter-parent&amp;lt;/artifactId&amp;gt;
        &amp;lt;version&amp;gt;1.5.9.RELEASE&amp;lt;/version&amp;gt;
    &amp;lt;/parent&amp;gt;
    &amp;lt;dependencies&amp;gt;
        &amp;lt;dependency&amp;gt;
            &amp;lt;groupId&amp;gt;org.springframework.boot&amp;lt;/groupId&amp;gt;
            &amp;lt;artifactId&amp;gt;spring-boot-starter-web&amp;lt;/artifactId&amp;gt;
        &amp;lt;/dependency&amp;gt;
    &amp;lt;/dependencies&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;h4&gt;3、编写一个主程序；启动Spring Boot应用&lt;/h4&gt;
&lt;pre&gt;&lt;code&gt;
/**
 *  @SpringBootApplication 来标注一个主程序类，说明这是一个Spring Boot应用
 */
@SpringBootApplication
public class HelloWorldMainApplication {

    public static void main(String[] args) {

        // Spring应用启动起来
        SpringApplication.run(HelloWorldMainApplication.class,args);
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h4&gt;4、编写相关的Controller、Service&lt;/h4&gt;
&lt;pre&gt;&lt;code&gt;@Controller
public class HelloController {

    @ResponseBody
    @RequestMapping(&quot;/hello&quot;)
    public String hello(){
        return &quot;Hello World!&quot;;
    }
}

&lt;/code&gt;&lt;/pre&gt;
&lt;h4&gt;5、运行主程序测试&lt;/h4&gt;
&lt;h4&gt;6、简化部署&lt;/h4&gt;
&lt;pre&gt;&lt;code&gt; &amp;lt;!-- 这个插件，可以将应用打包成一个可执行的jar包；--&amp;gt;
    &amp;lt;build&amp;gt;
        &amp;lt;plugins&amp;gt;
            &amp;lt;plugin&amp;gt;
                &amp;lt;groupId&amp;gt;org.springframework.boot&amp;lt;/groupId&amp;gt;
                &amp;lt;artifactId&amp;gt;spring-boot-maven-plugin&amp;lt;/artifactId&amp;gt;
            &amp;lt;/plugin&amp;gt;
        &amp;lt;/plugins&amp;gt;
    &amp;lt;/build&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;将这个应用打成jar包，直接使用java -jar的命令进行执行；&lt;/p&gt;
&lt;h3&gt;5、Hello World探究&lt;/h3&gt;
&lt;h4&gt;1、POM文件&lt;/h4&gt;
&lt;h5&gt;1、父项目&lt;/h5&gt;
&lt;pre&gt;&lt;code&gt;&amp;lt;parent&amp;gt;
    &amp;lt;groupId&amp;gt;org.springframework.boot&amp;lt;/groupId&amp;gt;
    &amp;lt;artifactId&amp;gt;spring-boot-starter-parent&amp;lt;/artifactId&amp;gt;
    &amp;lt;version&amp;gt;1.5.9.RELEASE&amp;lt;/version&amp;gt;
&amp;lt;/parent&amp;gt;

他的父项目是
&amp;lt;parent&amp;gt;
  &amp;lt;groupId&amp;gt;org.springframework.boot&amp;lt;/groupId&amp;gt;
  &amp;lt;artifactId&amp;gt;spring-boot-dependencies&amp;lt;/artifactId&amp;gt;
  &amp;lt;version&amp;gt;1.5.9.RELEASE&amp;lt;/version&amp;gt;
  &amp;lt;relativePath&amp;gt;../../spring-boot-dependencies&amp;lt;/relativePath&amp;gt;
&amp;lt;/parent&amp;gt;
他来真正管理Spring Boot应用里面的所有依赖版本；

&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Spring Boot的版本仲裁中心；&lt;/p&gt;
&lt;p&gt;以后我们导入依赖默认是不需要写版本；（没有在dependencies里面管理的依赖自然需要声明版本号）&lt;/p&gt;
&lt;h5&gt;2、启动器&lt;/h5&gt;
&lt;pre&gt;&lt;code&gt;&amp;lt;dependency&amp;gt;
    &amp;lt;groupId&amp;gt;org.springframework.boot&amp;lt;/groupId&amp;gt;
    &amp;lt;artifactId&amp;gt;spring-boot-starter-web&amp;lt;/artifactId&amp;gt;
&amp;lt;/dependency&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;spring-boot-starter&lt;/strong&gt;-&lt;strong&gt;web&lt;/strong&gt;：&lt;/p&gt;
&lt;p&gt;​	spring-boot-starter：spring-boot场景启动器；帮我们导入了web模块正常运行所依赖的组件；&lt;/p&gt;
&lt;p&gt;Spring Boot将所有的功能场景都抽取出来，做成一个个的starters（启动器），只需要在项目里面引入这些starter相关场景的所有依赖都会导入进来。要用什么功能就导入什么场景的启动器&lt;/p&gt;
&lt;h4&gt;2、主程序类，主入口类&lt;/h4&gt;
&lt;pre&gt;&lt;code&gt;/**
 *  @SpringBootApplication 来标注一个主程序类，说明这是一个Spring Boot应用
 */
@SpringBootApplication
public class HelloWorldMainApplication {

    public static void main(String[] args) {

        // Spring应用启动起来
        SpringApplication.run(HelloWorldMainApplication.class,args);
    }
}

&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;@&lt;strong&gt;SpringBootApplication&lt;/strong&gt;:    Spring Boot应用标注在某个类上说明这个类是SpringBoot的主配置类，SpringBoot就应该运行这个类的main方法来启动SpringBoot应用；&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Inherited
@SpringBootConfiguration
@EnableAutoConfiguration
@ComponentScan(excludeFilters = {
      @Filter(type = FilterType.CUSTOM, classes = TypeExcludeFilter.class),
      @Filter(type = FilterType.CUSTOM, classes = AutoConfigurationExcludeFilter.class) })
public @interface SpringBootApplication {
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;@&lt;strong&gt;SpringBootConfiguration&lt;/strong&gt;:Spring Boot的配置类；&lt;/p&gt;
&lt;p&gt;​&lt;/p&gt;
&lt;h4&gt;@&lt;strong&gt;Configuration&lt;/strong&gt;:&lt;/h4&gt;
&lt;p&gt;配置类上来标注这个注解；&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;标注在某个类上，表示这是一个Spring Boot的配置类；&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;ul&gt;
&lt;li&gt;最佳实战&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;ul&gt;
&lt;li&gt;配置 类组件之间无依赖关系用Lite模式加速容器启动过程，减少判断&lt;/li&gt;
&lt;li&gt;配置类组件之间有依赖关系，方法会被调用得到之前单实例组件，用Full模式&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/blockquote&gt;
&lt;pre&gt;&lt;code&gt;#############################Configuration使用示例######################################################
/**
 * 1、配置类里面使用@Bean标注在方法上给容器注册组件，默认也是单实例的
 * 2、配置类本身也是组件
 * 3、proxyBeanMethods：代理bean的方法
 *      Full(proxyBeanMethods = true)、【保证每个@Bean方法被调用多少次返回的组件都是单实例的】
 *      Lite(proxyBeanMethods = false)【每个@Bean方法被调用多少次返回的组件都是新创建的】
 *      组件依赖必须使用Full模式默认。其他默认是否Lite模式
 */
@Configuration(proxyBeanMethods = false) //告诉SpringBoot这是一个配置类 == 配置文件
public class MyConfig {

    /**
     * Full:外部无论对配置类中的这个组件注册方法调用多少次获取的都是之前注册容器中的单实例对象
     * @return
     */
    @Bean //给容器中添加组件。以方法名作为组件的id。返回类型就是组件类型。返回的值，就是组件在容器中的实例
    public User user01(){
        User zhangsan = new User(&quot;zhangsan&quot;, 18);
        //user组件依赖了Pet组件
        zhangsan.setPet(tomcatPet());
        return zhangsan;
    }

    @Bean(&quot;tom&quot;)//自定义组件名
    public Pet tomcatPet(){
        return new Pet(&quot;tomcat&quot;);
    }
}


################################@Configuration测试代码如下########################################
@SpringBootConfiguration
@EnableAutoConfiguration
@ComponentScan(&quot;com.atguigu.boot&quot;)
public class MainApplication {

    public static void main(String[] args) {
        //1、返回我们IOC容器
        ConfigurableApplicationContext run = SpringApplication.run(MainApplication.class, args);

        //2、查看容器里面的组件
        String[] names = run.getBeanDefinitionNames();
        for (String name : names) {
            System.out.println(name);
        }

        //3、从容器中获取组件
        Pet tom01 = run.getBean(&quot;tom&quot;, Pet.class);

        Pet tom02 = run.getBean(&quot;tom&quot;, Pet.class);

        System.out.println(&quot;组件：&quot;+(tom01 == tom02));

        //4、com.atguigu.boot.config.MyConfig$$EnhancerBySpringCGLIB$$51f1e1ca@1654a892
        MyConfig bean = run.getBean(MyConfig.class);
        System.out.println(bean);

        //如果@Configuration(proxyBeanMethods = true)代理对象调用方法。SpringBoot总会检查这个组件是否在容器中有。
        //保持组件单实例
        User user = bean.user01();
        User user1 = bean.user01();
        System.out.println(user == user1);


        User user01 = run.getBean(&quot;user01&quot;, User.class);
        Pet tom = run.getBean(&quot;tom&quot;, Pet.class);

        System.out.println(&quot;用户的宠物：&quot;+(user01.getPet() == tom));

    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h4&gt;@Import&lt;/h4&gt;
&lt;pre&gt;&lt;code&gt; * 4、@Import({User.class, DBHelper.class})
 *      给容器中自动创建出这两个类型的组件、默认组件的名字就是全类名
 *
 *
 *
 */

@Import({User.class, DBHelper.class})
@Configuration(proxyBeanMethods = false) //告诉SpringBoot这是一个配置类 == 配置文件
public class MyConfig {
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;@Import 高级用法： https://www.bilibili.com/video/BV1gW411W7wy?p=8&lt;/p&gt;
&lt;h4&gt;@Conditional&lt;/h4&gt;
&lt;p&gt;条件装配：满足Conditional指定的条件，则进行组件注入&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;可加在方法上和类上&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;**=****=****=****=**=测试条件装配**=****=****=****=****=**=
@Configuration(proxyBeanMethods = false) //告诉SpringBoot这是一个配置类 == 配置文件
//@ConditionalOnBean(name = &quot;tom&quot;)
@ConditionalOnMissingBean(name = &quot;tom&quot;)
public class MyConfig {


    /**
     * Full:外部无论对配置类中的这个组件注册方法调用多少次获取的都是之前注册容器中的单实例对象
     * @return
     */

    @Bean //给容器中添加组件。以方法名作为组件的id。返回类型就是组件类型。返回的值，就是组件在容器中的实例
    public User user01(){
        User zhangsan = new User(&quot;zhangsan&quot;, 18);
        //user组件依赖了Pet组件
        zhangsan.setPet(tomcatPet());
        return zhangsan;
    }

    @Bean(&quot;tom22&quot;)
    public Pet tomcatPet(){
        return new Pet(&quot;tomcat&quot;);
    }
}

public static void main(String[] args) {
        //1、返回我们IOC容器
        ConfigurableApplicationContext run = SpringApplication.run(MainApplication.class, args);

        //2、查看容器里面的组件
        String[] names = run.getBeanDefinitionNames();
        for (String name : names) {
            System.out.println(name);
        }

        boolean tom = run.containsBean(&quot;tom&quot;);
        System.out.println(&quot;容器中Tom组件：&quot;+tom);

        boolean user01 = run.containsBean(&quot;user01&quot;);
        System.out.println(&quot;容器中user01组件：&quot;+user01);

        boolean tom22 = run.containsBean(&quot;tom22&quot;);
        System.out.println(&quot;容器中tom22组件：&quot;+tom22);

    }
&lt;/code&gt;&lt;/pre&gt;
&lt;h4&gt;@ImportResource&lt;/h4&gt;
&lt;blockquote&gt;
&lt;p&gt;原生配置文件引入&lt;/p&gt;
&lt;/blockquote&gt;
&lt;pre&gt;&lt;code&gt;**=****=****=****=****beans.xml****=****=****=****=**===
&amp;lt;?xml version=&quot;1.0&quot; encoding=&quot;UTF-8&quot;?&amp;gt;
&amp;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 https://www.springframework.org/schema/context/spring-context.xsd&quot;&amp;gt;

    &amp;lt;bean id=&quot;haha&quot; class=&quot;com.atguigu.boot.bean.User&quot;&amp;gt;
        &amp;lt;property name=&quot;name&quot; value=&quot;zhangsan&quot;&amp;gt;&amp;lt;/property&amp;gt;
        &amp;lt;property name=&quot;age&quot; value=&quot;18&quot;&amp;gt;&amp;lt;/property&amp;gt;
    &amp;lt;/bean&amp;gt;

    &amp;lt;bean id=&quot;hehe&quot; class=&quot;com.atguigu.boot.bean.Pet&quot;&amp;gt;
        &amp;lt;property name=&quot;name&quot; value=&quot;tomcat&quot;&amp;gt;&amp;lt;/property&amp;gt;
    &amp;lt;/bean&amp;gt;
&amp;lt;/beans&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;@ImportResource(&quot;classpath:beans.xml&quot;)
public class MyConfig {}

**=****=****=****=****测试****=****=****=**
        boolean haha = run.containsBean(&quot;haha&quot;);
        boolean hehe = run.containsBean(&quot;hehe&quot;);
        System.out.println(&quot;haha：&quot;+haha);//true
        System.out.println(&quot;hehe：&quot;+hehe);//true
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;​&lt;/p&gt;
&lt;p&gt;配置类 -----  配置文件；配置类也是容器中的一个组件；@Component&lt;/p&gt;
&lt;p&gt;@&lt;strong&gt;EnableAutoConfiguration&lt;/strong&gt;：开启自动配置功能；&lt;/p&gt;
&lt;p&gt;​		以前我们需要配置的东西，Spring Boot帮我们自动配置；@&lt;strong&gt;EnableAutoConfiguration&lt;/strong&gt;告诉SpringBoot开启自动配置功能；这样自动配置才能生效；&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;@AutoConfigurationPackage
@Import(EnableAutoConfigurationImportSelector.class)
public @interface EnableAutoConfiguration {
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;​      	@&lt;strong&gt;AutoConfigurationPackage&lt;/strong&gt;：自动配置包&lt;/p&gt;
&lt;p&gt;​		@&lt;strong&gt;Import&lt;/strong&gt;(AutoConfigurationPackages.Registrar.class)：&lt;/p&gt;
&lt;p&gt;​		Spring的底层注解@Import，给容器中导入一个组件；导入的组件由AutoConfigurationPackages.Registrar.class；&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;将主配置类（@SpringBootApplication标注的类）的所在包及下面所有子包里面的所有组件扫描到Spring容器；&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;​	@&lt;strong&gt;Import&lt;/strong&gt;(EnableAutoConfigurationImportSelector.class)；&lt;/p&gt;
&lt;p&gt;​		给容器中导入组件？&lt;/p&gt;
&lt;p&gt;​		&lt;strong&gt;EnableAutoConfigurationImportSelector&lt;/strong&gt;：导入哪些组件的选择器；&lt;/p&gt;
&lt;p&gt;​		将所有需要导入的组件以全类名的方式返回；这些组件就会被添加到容器中；&lt;/p&gt;
&lt;p&gt;​		会给容器中导入非常多的自动配置类（xxxAutoConfiguration）；就是给容器中导入这个场景需要的所有组件，并配置好这些组件；		&lt;img src=&quot;%E6%90%9C%E7%8B%97%E6%88%AA%E5%9B%BE20180129224104.png&quot; alt=&quot;自动配置类&quot; /&gt;&lt;/p&gt;
&lt;p&gt;有了自动配置类，免去了我们手动编写配置注入功能组件等的工作；&lt;/p&gt;
&lt;p&gt;​		SpringFactoriesLoader.loadFactoryNames(EnableAutoConfiguration.class,classLoader)；&lt;/p&gt;
&lt;p&gt;**Spring Boot在启动的时候从类路径下的META-INF/spring.factories中获取EnableAutoConfiguration指定的值，将这些值作为自动配置类导入到容器中，自动配置类就生效，帮我们进行自动配置工作；**以前我们需要自己配置的东西，自动配置类都帮我们；&lt;/p&gt;
&lt;p&gt;J2EE的整体整合解决方案和自动配置都在spring-boot-autoconfigure-1.5.9.RELEASE.jar；&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Spring注解版（谷粒学院）&lt;/strong&gt;&lt;/p&gt;
&lt;h3&gt;6、使用Spring Initializer快速创建Spring Boot项目&lt;/h3&gt;
&lt;h4&gt;1、IDEA：使用 Spring Initializer快速创建项目&lt;/h4&gt;
&lt;p&gt;IDE都支持使用Spring的项目创建向导快速创建一个Spring Boot项目；&lt;/p&gt;
&lt;p&gt;选择我们需要的模块；向导会联网创建Spring Boot项目；&lt;/p&gt;
&lt;p&gt;默认生成的Spring Boot项目；&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;主程序已经生成好了，我们只需要我们自己的逻辑&lt;/li&gt;
&lt;li&gt;resources文件夹中目录结构
&lt;ul&gt;
&lt;li&gt;static：保存所有的静态资源； js css  images；&lt;/li&gt;
&lt;li&gt;templates：保存所有的模板页面；（Spring Boot默认jar包使用嵌入式的Tomcat，默认不支持JSP页面）；可以使用模板引擎（freemarker、thymeleaf）；&lt;/li&gt;
&lt;li&gt;application.properties：Spring Boot应用的配置文件；可以修改一些默认设置；&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h4&gt;2、STS使用 Spring Starter Project快速创建项目&lt;/h4&gt;
&lt;hr /&gt;
&lt;h2&gt;二、配置文件&lt;/h2&gt;
&lt;h3&gt;1、配置文件&lt;/h3&gt;
&lt;p&gt;SpringBoot使用一个全局的配置文件，配置文件名是固定的；&lt;/p&gt;
&lt;p&gt;•application.properties&lt;/p&gt;
&lt;p&gt;•application.yml&lt;/p&gt;
&lt;p&gt;配置文件的作用：修改SpringBoot自动配置的默认值；SpringBoot在底层都给我们自动配置好；&lt;/p&gt;
&lt;p&gt;YAML（YAML Ain&apos;t Markup Language）&lt;/p&gt;
&lt;p&gt;​	YAML  A Markup Language：是一个标记语言&lt;/p&gt;
&lt;p&gt;​	YAML   isn&apos;t Markup Language：不是一个标记语言；&lt;/p&gt;
&lt;p&gt;标记语言：&lt;/p&gt;
&lt;p&gt;​	以前的配置文件；大多都使用的是  &lt;strong&gt;xxxx.xml&lt;/strong&gt;文件；&lt;/p&gt;
&lt;p&gt;​	YAML：&lt;strong&gt;以数据为中心&lt;/strong&gt;，比json、xml等更适合做配置文件；&lt;/p&gt;
&lt;p&gt;​	YAML：配置例子&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;server:
  port: 8081
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;​	XML：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&amp;lt;server&amp;gt;
	&amp;lt;port&amp;gt;8081&amp;lt;/port&amp;gt;
&amp;lt;/server&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;2、YAML语法：&lt;/h3&gt;
&lt;h4&gt;1、基本语法&lt;/h4&gt;
&lt;p&gt;k:(空格)v：表示一对键值对（空格必须有）；&lt;/p&gt;
&lt;p&gt;以&lt;strong&gt;空格&lt;/strong&gt;的缩进来控制层级关系；只要是左对齐的一列数据，都是同一个层级的&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;server:
    port: 8081
    path: /hello
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;属性和值也是大小写敏感；&lt;/p&gt;
&lt;h4&gt;2、值的写法&lt;/h4&gt;
&lt;h5&gt;字面量：普通的值（数字，字符串，布尔）&lt;/h5&gt;
&lt;p&gt;​	k: v：字面直接来写；&lt;/p&gt;
&lt;p&gt;​		字符串默认不用加上单引号或者双引号；&lt;/p&gt;
&lt;p&gt;​		&quot;&quot;：双引号；不会转义字符串里面的特殊字符；特殊字符会作为本身想表示的意思&lt;/p&gt;
&lt;p&gt;​				name:   &quot;zhangsan \n lisi&quot;：输出；zhangsan 换行  lisi&lt;/p&gt;
&lt;p&gt;​		&apos;&apos;：单引号；会转义特殊字符，特殊字符最终只是一个普通的字符串数据&lt;/p&gt;
&lt;p&gt;​				name:   ‘zhangsan \n lisi’：输出；zhangsan \n  lisi&lt;/p&gt;
&lt;h5&gt;对象、Map（属性和值）（键值对）：&lt;/h5&gt;
&lt;p&gt;​	k: v：在下一行来写对象的属性和值的关系；注意缩进&lt;/p&gt;
&lt;p&gt;​		对象还是k: v的方式&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;friends:
		lastName: zhangsan
		age: 20
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;行内写法：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;friends: {lastName: zhangsan,age: 18}
&lt;/code&gt;&lt;/pre&gt;
&lt;h5&gt;数组（List、Set）：&lt;/h5&gt;
&lt;p&gt;用- 值表示数组中的一个元素&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;pets:
 - cat
 - dog
 - pig
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;行内写法&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;pets: [cat,dog,pig]
&lt;/code&gt;&lt;/pre&gt;
&lt;h4&gt;示例&lt;/h4&gt;
&lt;pre&gt;&lt;code&gt;@Data
public class Person {
	
	private String userName;
	private Boolean boss;
	private Date birth;
	private Integer age;
	private Pet pet;
	private String[] interests;
	private List&amp;lt;String&amp;gt; animal;
	private Map&amp;lt;String, Object&amp;gt; score;
	private Set&amp;lt;Double&amp;gt; salarys;
	private Map&amp;lt;String, List&amp;lt;Pet&amp;gt;&amp;gt; allPets;
}

@Data
public class Pet {
	private String name;
	private Double weight;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;## yaml表示以上对象
person:
  userName: zhangsan
  boss: false
  birth: 2019/12/12 20:12:33
  age: 18
  pet: 
    name: tomcat
    weight: 23.4
  interests: [篮球,游泳]
  animal: 
    - jerry
    - mario
  score:
    english: 
      first: 30
      second: 40
      third: 50
    math: [131,140,148]
    chinese: {first: 128,second: 136}
  salarys: [3999,4999.98,5999.99]
  allPets:
    sick:
      - {name: tom}
      - {name: jerry,weight: 47}
    health: [{name: mario,weight: 47}]
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;3、配置文件值注入&lt;/h3&gt;
&lt;h4&gt;配置注解区别&lt;/h4&gt;
&lt;p&gt;@EnableConfigurationProperties与@ConfigurationProperties的关系&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;在spring-boot中，&lt;/p&gt;
&lt;p&gt;1、&lt;/p&gt;
&lt;p&gt;如果@ConfigurationProperties所注的类可以被springboot扫描并添加进容器中作为bean（比如使用@Component等注解，或者配置扫描该类所在包等手段），那么spring容器会自动使该类上的@ConfigurationProperties生效，创建一个该类的实例，然后把对应配置属性绑定进该实例，再把该实例作为bean添加进spring容器。&lt;/p&gt;
&lt;p&gt;2、&lt;/p&gt;
&lt;p&gt;如果该类只使用了@ConfigurationProperties注解，然后该类没有在扫描路径下或者没有使用@Component等注解，导致无法被扫描为bean，那么就必须在配置类上使用@EnableConfigurationProperties注解去指定这个类，这个时候就会让该类上的@ConfigurationProperties生效，然后作为bean添加进spring容器中&lt;/p&gt;
&lt;p&gt;3、&lt;/p&gt;
&lt;p&gt;在javaconfig中@ConfigurationProperties与@Bean一起用，也是把配置文件中的属性注入该@Bean对应的要添加到容器中的bean实例中。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h4&gt;方法一、&lt;/h4&gt;
&lt;p&gt;@EnableConfigurationProperties + @ConfigurationProperties&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;/**
 * 将配置文件中配置的每一个属性的值，映射到这个组件中
 * @ConfigurationProperties：告诉SpringBoot将本类中的所有属性和配置文件中相关的配置进行绑定；
 *      prefix = &quot;person&quot;：配置文件中哪个下面的所有属性进行一一映射
 *
 * 只有这个组件是容器中的组件，才能容器提供的@ConfigurationProperties功能；
 *
 */
@Component
@ConfigurationProperties(prefix = &quot;person&quot;)
public class Person {

    private String lastName;
    private Integer age;
    private Boolean boss;
    private Date birth;

    private Map&amp;lt;String,Object&amp;gt; maps;
    private List&amp;lt;Object&amp;gt; lists;
    private Dog dog;

&lt;/code&gt;&lt;/pre&gt;
&lt;h4&gt;方法二、&lt;/h4&gt;
&lt;p&gt;@EnableConfigurationProperties + @ConfigurationProperties&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;使用场景：使用第三方包的时候&lt;/p&gt;
&lt;/blockquote&gt;
&lt;pre&gt;&lt;code&gt;@Configuration
@EnableConfigurationProperties(Car.class)
//1、开启Car配置绑定功能
//2、把这个Car这个组件自动注册到容器中
public class MyConfig {
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h4&gt;方法三、&lt;/h4&gt;
&lt;p&gt;@PropertySource&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;加载指定的属性文件（*.properties）到 Spring 的 Environment 中。&lt;/p&gt;
&lt;p&gt;可以配合 @Value 和@ConfigurationProperties 使用。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;@PropertySource 和 @Value
组合使用，可以将自定义属性文件中的属性变量值注入到当前类的使用@Value注解的成员变量中。&lt;/li&gt;
&lt;li&gt;@PropertySource 和 @ConfigurationProperties
组合使用，可以将属性文件与一个Java类绑定，将属性文件中的变量值注入到该Java类的成员变量中&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;@Configuration
@PropertySource(&quot;classpath:wxpay.properties&quot;) //读取配置文件
@ConfigurationProperties(prefix=&quot;wxpay&quot;) //读取wxpay节点
@Data //使用set方法将wxpay节点中的值填充到当前类的属性中
public class WxPayConfig {

    // 商户号
    private String mchId;

    // 商户API证书序列号
    private String mchSerialNo;

    // 商户私钥文件
    private String privateKeyPath;

    // APIv3密钥
    private String apiV3Key;

    // APPID
    private String appid;

    // 微信服务器地址
    private String domain;

    // 接收结果通知地址
    private String notifyDomain;

}
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;# 微信支付相关参数
# 商户号
wxpay.mch-id=1558950191
# 商户API证书序列号
wxpay.mch-serial-no=34345964330B66427E0D3D28826C4993C77E631F
# 商户私钥文件
wxpay.private-key-path=apiclient_key.pem
# APIv3密钥
wxpay.api-v3-key=UDuLFDcmy5Eb6o0nTNZdu6ek4DDh4K8B
# APPID
wxpay.appid=wx74862e0dfcf69954
# 微信服务器地址
wxpay.domain=https://api.mch.weixin.qq.com
# 接收结果通知地址
wxpay.notify-domain=https://7d92-115-171-63-135.ngrok.io

&lt;/code&gt;&lt;/pre&gt;
&lt;/blockquote&gt;
&lt;h4&gt;配置提示&lt;/h4&gt;
&lt;blockquote&gt;
&lt;p&gt;我们可以导入配置文件处理器，以后编写配置就有提示了&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;     &amp;lt;dependency&amp;gt;
         &amp;lt;groupId&amp;gt;org.springframework.boot&amp;lt;/groupId&amp;gt;
         &amp;lt;artifactId&amp;gt;spring-boot-configuration-processor&amp;lt;/artifactId&amp;gt;
         &amp;lt;optional&amp;gt;true&amp;lt;/optional&amp;gt;
     &amp;lt;/dependency&amp;gt;


&amp;lt;build&amp;gt;
     &amp;lt;plugins&amp;gt;
         &amp;lt;plugin&amp;gt;
             &amp;lt;groupId&amp;gt;org.springframework.boot&amp;lt;/groupId&amp;gt;
             &amp;lt;artifactId&amp;gt;spring-boot-maven-plugin&amp;lt;/artifactId&amp;gt;
             //打包排除配置提示
             &amp;lt;configuration&amp;gt;
                 &amp;lt;excludes&amp;gt;
                     &amp;lt;exclude&amp;gt;
                         &amp;lt;groupId&amp;gt;org.springframework.boot&amp;lt;/groupId&amp;gt;
                         &amp;lt;artifactId&amp;gt;spring-boot-configuration-processor&amp;lt;/artifactId&amp;gt;
                     &amp;lt;/exclude&amp;gt;
                 &amp;lt;/excludes&amp;gt;
             &amp;lt;/configuration&amp;gt;
         &amp;lt;/plugin&amp;gt;
     &amp;lt;/plugins&amp;gt;
 &amp;lt;/build&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;让IDEA可以识别配置文件，将配置文件的图标展示成SpringBoot的图标，同时配置文件的内容可以高亮显示&lt;/p&gt;
&lt;p&gt;File -&amp;gt; Project Structure -&amp;gt; Modules -&amp;gt; 选择小叶子&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://tangtangit.com/_astro/image-20220111110856552.B0YxE4z6_Z1z1P5Q.webp&quot; alt=&quot;image-20220111110856552&quot; /&gt;&lt;/p&gt;
&lt;p&gt;点击（+） 图标&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://tangtangit.com/_astro/image-20220111110914827.DW3Tg2EK_2nFgUi.webp&quot; alt=&quot;image-20220111110914827&quot; /&gt;&lt;/p&gt;
&lt;p&gt;选中配置文件：&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://tangtangit.com/_astro/image-20220111110928582.CAPYFQqy_qDMeb.webp&quot; alt=&quot;image-20220111110928582&quot; /&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h4&gt;1、properties配置文件在idea中默认utf-8可能会乱码&lt;/h4&gt;
&lt;p&gt;调整&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;%E6%90%9C%E7%8B%97%E6%88%AA%E5%9B%BE20180130161620.png&quot; alt=&quot;idea配置乱码&quot; /&gt;&lt;/p&gt;
&lt;h4&gt;2、@Value获取值和@ConfigurationProperties获取值比较&lt;/h4&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;&lt;/th&gt;
&lt;th&gt;@ConfigurationProperties&lt;/th&gt;
&lt;th&gt;@Value&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;功能&lt;/td&gt;
&lt;td&gt;批量注入配置文件中的属性&lt;/td&gt;
&lt;td&gt;一个个指定&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;松散绑定（松散语法）&lt;/td&gt;
&lt;td&gt;支持&lt;/td&gt;
&lt;td&gt;不支持&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;SpEL&lt;/td&gt;
&lt;td&gt;不支持&lt;/td&gt;
&lt;td&gt;支持&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;JSR303数据校验&lt;/td&gt;
&lt;td&gt;支持&lt;/td&gt;
&lt;td&gt;不支持&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;复杂类型封装&lt;/td&gt;
&lt;td&gt;支持&lt;/td&gt;
&lt;td&gt;不支持&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;配置文件yml还是properties他们都能获取到值；&lt;/p&gt;
&lt;p&gt;如果说，我们只是在某个业务逻辑中需要获取一下配置文件中的某项值，使用@Value；&lt;/p&gt;
&lt;p&gt;如果说，我们专门编写了一个javaBean来和配置文件进行映射，我们就直接使用@ConfigurationProperties；&lt;/p&gt;
&lt;blockquote&gt;
&lt;pre&gt;&lt;code&gt;@Configuration
public class WxUtils {

    // 微信appid
    @Value(&quot;${wx.applet.APP_ID}&quot;)
    private  String APP_ID = &quot;wxcdf8a126242fc795&quot;;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;一、SpringBoot 中@Value 具体作用有那些？
使用场景：系统在开发和正式运营之间的环境不相同，每一次部署服务器时或多或少需要更改相关配置，所以比较麻烦。我们使用@Value这个注解，将环境变量写在配置文件中，让他根据运行的环境进行读取。就可以解决这个问题了&lt;/p&gt;
&lt;p&gt;二、@Value 的注意事项 （以下问题都会造成，无法注入的问题）
1.不能作用于静态变量（static）
2.不能作用于常量（final）
3.不能在非注册的类中使用（类需要被注册在spring上下文中，如用@Service,@RestController,@Component等；
4.使用这个类时，只能通过依赖注入的方式，用new的方式是不会自动注入这些配置的&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h4&gt;3、配置文件注入值数据校验&lt;/h4&gt;
&lt;pre&gt;&lt;code&gt;@Component
@ConfigurationProperties(prefix = &quot;person&quot;)
@Validated
public class Person {

    /**
     * &amp;lt;bean class=&quot;Person&quot;&amp;gt;
     *      &amp;lt;property name=&quot;lastName&quot; value=&quot;字面量/${key}从环境变量、配置文件中获取值/#{SpEL}&quot;&amp;gt;&amp;lt;/property&amp;gt;
     * &amp;lt;bean/&amp;gt;
     */

   //lastName必须是邮箱格式
    @Email
    //@Value(&quot;${person.last-name}&quot;)
    private String lastName;
    //@Value(&quot;#{11*2}&quot;)
    private Integer age;
    //@Value(&quot;true&quot;)
    private Boolean boss;

    private Date birth;
    private Map&amp;lt;String,Object&amp;gt; maps;
    private List&amp;lt;Object&amp;gt; lists;
    private Dog dog;
&lt;/code&gt;&lt;/pre&gt;
&lt;h5&gt;4、@PropertySource&amp;amp;@ImportResource&amp;amp;@Bean&lt;/h5&gt;
&lt;p&gt;@&lt;strong&gt;PropertySource&lt;/strong&gt;：加载指定的配置文件；&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;/**
 * 将配置文件中配置的每一个属性的值，映射到这个组件中
 * @ConfigurationProperties：告诉SpringBoot将本类中的所有属性和配置文件中相关的配置进行绑定；
 *      prefix = &quot;person&quot;：配置文件中哪个下面的所有属性进行一一映射
 *
 * 只有这个组件是容器中的组件，才能容器提供的@ConfigurationProperties功能；
 *  @ConfigurationProperties(prefix = &quot;person&quot;)默认从全局配置文件中获取值；
 *
 */
@PropertySource(value = {&quot;classpath:person.properties&quot;})
@Component
@ConfigurationProperties(prefix = &quot;person&quot;)
//@Validated
public class Person {

    /**
     * &amp;lt;bean class=&quot;Person&quot;&amp;gt;
     *      &amp;lt;property name=&quot;lastName&quot; value=&quot;字面量/${key}从环境变量、配置文件中获取值/#{SpEL}&quot;&amp;gt;&amp;lt;/property&amp;gt;
     * &amp;lt;bean/&amp;gt;
     */

   //lastName必须是邮箱格式
   // @Email
    //@Value(&quot;${person.last-name}&quot;)
    private String lastName;
    //@Value(&quot;#{11*2}&quot;)
    private Integer age;
    //@Value(&quot;true&quot;)
    private Boolean boss;

&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;@&lt;strong&gt;ImportResource&lt;/strong&gt;：导入Spring的配置文件，让配置文件里面的内容生效；&lt;/p&gt;
&lt;p&gt;Spring Boot里面没有Spring的配置文件，我们自己编写的配置文件，也不能自动识别；&lt;/p&gt;
&lt;p&gt;想让Spring的配置文件生效，加载进来；@&lt;strong&gt;ImportResource&lt;/strong&gt;标注在一个配置类上&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;@ImportResource(locations = {&quot;classpath:beans.xml&quot;})
导入Spring的配置文件让其生效
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;不来编写Spring的配置文件&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&amp;lt;?xml version=&quot;1.0&quot; encoding=&quot;UTF-8&quot;?&amp;gt;
&amp;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;&amp;gt;


    &amp;lt;bean id=&quot;helloService&quot; class=&quot;com.atguigu.springboot.service.HelloService&quot;&amp;gt;&amp;lt;/bean&amp;gt;
&amp;lt;/beans&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;SpringBoot推荐给容器中添加组件的方式；推荐使用全注解的方式&lt;/p&gt;
&lt;p&gt;1、配置类**@Configuration**------&amp;gt;Spring配置文件&lt;/p&gt;
&lt;p&gt;2、使用**@Bean**给容器中添加组件&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;/**
 * @Configuration：指明当前类是一个配置类；就是来替代之前的Spring配置文件
 *
 * 在配置文件中用&amp;lt;bean&amp;gt;&amp;lt;bean/&amp;gt;标签添加组件
 *
 */
@Configuration
public class MyAppConfig {

    //将方法的返回值添加到容器中；容器中这个组件默认的id就是方法名
    @Bean
    public HelloService helloService02(){
        System.out.println(&quot;配置类@Bean给容器中添加组件了...&quot;);
        return new HelloService();
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;3.2springboot读取配置文件内容&lt;/h3&gt;
&lt;h4&gt;1、Environment&lt;/h4&gt;
&lt;p&gt;Environment是用来读取应用程序运行时的环境变量的类，可以通过key-value的方式读取application.properties和系统环境变量，命令行输入参数，系统属性等，具体如下：&lt;/p&gt;
&lt;p&gt;在application.yml文件定义如下：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;属性配置类的
server:
  port: 8082
spring:
  main:
    banner-mode: console
    自定义
alipay:
  pay:
    appid: 123456
    notify: http://www.xxx.com
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;定义读取的类如下：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;package com.kuangstudy.web.properties;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.core.env.Environment;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;

import java.util.HashMap;
import java.util.Map;
import java.util.Properties;
import java.util.Set;

   @RestController
   public class ReadPropertiesEnvironment {

       @Autowired
       private Environment environment;

        @GetMapping(&quot;/read/file&quot;)
        public Map&amp;lt;String,Object&amp;gt; readInfo(){
            Map&amp;lt;String,Object&amp;gt; map = new HashMap&amp;lt;&amp;gt;();
            map.put(&quot;port&quot;,environment.getProperty(&quot;server.port&quot;));
            map.put(&quot;appid&quot;,environment.getProperty(&quot;alipay.pay.appid&quot;));
            map.put(&quot;notify&quot;,environment.getProperty(&quot;alipay.pay.notify&quot;));
            map.put(&quot;javaversion&quot;,environment.getProperty(&quot;java.version&quot;));
            map.put(&quot;javahome&quot;,environment.getProperty(&quot;JAVA_HOME&quot;));
            map.put(&quot;mavenhome&quot;,environment.getProperty(&quot;MAVEN_HOME&quot;)); 
            return  map;
        }

        public static void main(String[] args) {
            Properties properties = System.getProperties();
            Set&amp;lt;String&amp;gt; strings = properties.stringPropertyNames();
            for (String string : strings) {
                System.out.println(string+&quot;===&amp;gt;&quot;+properties.get(string));
            }

        }
	}

&lt;/code&gt;&lt;/pre&gt;
&lt;h4&gt;2、@Value&lt;/h4&gt;
&lt;blockquote&gt;
&lt;p&gt;一、SpringBoot 中@Value 具体作用有那些？
使用场景：系统在开发和正式运营之间的环境不相同，每一次部署服务器时或多或少需要更改相关配置，所以比较麻烦。我们使用@Value这个注解，将环境变量写在配置文件中，让他根据运行的环境进行读取。就可以解决这个问题了&lt;/p&gt;
&lt;p&gt;二、@Value 的注意事项 （以下问题都会造成，无法注入的问题）
1.不能作用于静态变量（static）
2.不能作用于常量（final）
3.不能在非注册的类中使用（类需要被注册在spring上下文中，如用@Service,@RestController,@Component等；
4.使用这个类时，只能通过依赖注入的方式，用new的方式是不会自动注入这些配置的&lt;/p&gt;
&lt;p&gt;结论：其实@Value底层就是Environment.java&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;使用@Value注解读取配置文件内容，具体如下：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;package com.kuangstudy.web.properties;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.core.env.Environment;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;

import java.util.HashMap;
import java.util.Map;
import java.util.Properties;
import java.util.Set;

/**
 * Description:
 * Author: yykk Administrator
 * Version: 1.0
 * Create Date Time: 2021/12/11 21:25.
 * Update Date Time:
 *
 * @see
 */
@RestController
public class ReadPropertiesValue {

    @Value(&quot;${server.port}&quot;)
    private Integer port;
    @Value(&quot;${alipay.pay.appid}&quot;)
    private String appid;
    @Value(&quot;${alipay.pay.notify}&quot;)
    private String notify;
    @Value(&quot;${java.version}&quot;)
    private String javaVersion;
    @Value(&quot;${JAVA_HOME}&quot;)
    private String javaHome;
    @Value(&quot;${MAVEN_HOME}&quot;)
    private String mavenHome;


    @GetMapping(&quot;/read/value&quot;)
    public Map&amp;lt;String, Object&amp;gt; readInfo() {
        Map&amp;lt;String, Object&amp;gt; map = new HashMap&amp;lt;&amp;gt;();
        map.put(&quot;port&quot;, port);
        map.put(&quot;appid&quot;, appid);
        map.put(&quot;notify&quot;, notify);
        map.put(&quot;javaversion&quot;, javaVersion);
        map.put(&quot;javahome&quot;, javaHome);
        map.put(&quot;mavenhome&quot;, mavenHome);
        return map;
    }

}
&lt;/code&gt;&lt;/pre&gt;
&lt;h4&gt;3、@ConfigurationProperties&lt;/h4&gt;
&lt;p&gt;@Configuration + @ConfigurationProperties&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;具有面向对象的特征&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;可以在代码中自动提示&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;维护起来很方便。在配置文件也可以很方便的控制和提示&lt;/p&gt;
&lt;p&gt;具体实现步骤：&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;第一步：在application.yml自定义支付属性&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;## 自定义属性
ksd:
  alipay:
    appid: 2021003100625328
    mkey: MIIEvwIBADANBgkqhkiG9w0BAQEFAASCBKkwggSlAgEAAoIBAQC9kGK4VMbYm
    ckey: MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAx5i5LhEtDZw6Q6mUxkC5f6sAvZm9OncAkRXwfoBdDeKk
    callback: https://www.txnh.net/api/alipay/returnUrl
    charset: utf-8
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;第二步：定义一个属性配置类如下：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;package com.kuangstudy.web.properties;

import lombok.Data;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.boot.context.properties.ConfigurationProperties;

/**
 * Description:
 * Author: yykk Administrator
 * Version: 1.0
 * Create Date Time: 2021/12/14 20:53.
 * Update Date Time:
 *
 * @see
 */
@Configuration
@ConfigurationProperties(prefix = &quot;ksd.alipay&quot;)//这个注解是用找到类
@Data
public class AlipayProperties {
    private String appid;
    private String mkey;
    private String ckey;
    private String callback;
    private String charset;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;@ConfigurationProperties(prefix = “ksd.alipay”)//这个注解的作用是：找到类&lt;/li&gt;
&lt;li&gt;prefix = “ksd.alipay” 前缀未来可以在全局配置文件中进行自动提示。&lt;/li&gt;
&lt;li&gt;@Configuration自动将该类注册到spring容器中&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;第二步：在需要的地方进行引入&lt;/p&gt;
&lt;h3&gt;4、配置文件占位符&lt;/h3&gt;
&lt;h4&gt;1、随机数&lt;/h4&gt;
&lt;pre&gt;&lt;code&gt;${random.value}、${random.int}、${random.long}
${random.int(10)}、${random.int[1024,65536]}
&lt;/code&gt;&lt;/pre&gt;
&lt;h4&gt;2、占位符获取之前配置的值，如果没有可以是用:指定默认值&lt;/h4&gt;
&lt;pre&gt;&lt;code&gt;person.last-name=张三${random.uuid}
person.age=${random.int}
person.birth=2017/12/15
person.boss=false
person.maps.k1=v1
person.maps.k2=14
person.lists=a,b,c
person.dog.name=${person.hello:hello}_dog
person.dog.age=15
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;5、Profile&lt;/h3&gt;
&lt;h4&gt;1、多Profile文件&lt;/h4&gt;
&lt;p&gt;我们在主配置文件编写的时候，文件名可以是   application-{profile}.properties/yml&lt;/p&gt;
&lt;p&gt;默认使用application.properties的配置；&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;当需要在springboot中引用其他的yml文件时，需要在application.yml里配置

spring:
    profiles:
        include: email,xmyb


值得注意的是：application-email.yml的前缀一定要和主在配置文件application.yml的名字一致
&lt;/code&gt;&lt;/pre&gt;
&lt;h4&gt;2、yml支持多文档块方式&lt;/h4&gt;
&lt;pre&gt;&lt;code&gt;
server:
  port: 8081
spring:
  profiles:
    active: prod

---
server:
  port: 8083
spring:
  profiles: dev


---

server:
  port: 8084
spring:
  profiles: prod  #指定属于哪个环境
&lt;/code&gt;&lt;/pre&gt;
&lt;h4&gt;3、激活指定profile&lt;/h4&gt;
&lt;p&gt;​	1、在配置文件中指定  spring.profiles.active=dev&lt;/p&gt;
&lt;p&gt;​	2、命令行：&lt;/p&gt;
&lt;p&gt;​		java -jar spring-boot-02-config-0.0.1-SNAPSHOT.jar --spring.profiles.active=dev；&lt;/p&gt;
&lt;p&gt;​		可以直接在测试的时候，配置传入命令行参数&lt;/p&gt;
&lt;p&gt;​	3、虚拟机参数；&lt;/p&gt;
&lt;p&gt;​		-Dspring.profiles.active=dev&lt;/p&gt;
&lt;h3&gt;6、配置文件加载位置&lt;/h3&gt;
&lt;p&gt;springboot 启动会扫描以下位置的application.properties或者application.yml文件作为Spring boot的默认配置文件&lt;/p&gt;
&lt;p&gt;–file:./config/&lt;/p&gt;
&lt;p&gt;–file:./&lt;/p&gt;
&lt;p&gt;–classpath:/config/&lt;/p&gt;
&lt;p&gt;–classpath:/&lt;/p&gt;
&lt;p&gt;优先级由高到底，高优先级的配置会覆盖低优先级的配置；&lt;/p&gt;
&lt;p&gt;SpringBoot会从这四个位置全部加载主配置文件；&lt;strong&gt;互补配置&lt;/strong&gt;；&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;我们还可以通过spring.config.location来改变默认的配置文件位置&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;项目打包好以后，我们可以使用命令行参数的形式，启动项目的时候来指定配置文件的新位置；指定配置文件和默认加载的这些配置文件共同起作用形成互补配置；&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;java -jar spring-boot-02-config-02-0.0.1-SNAPSHOT.jar --spring.config.location=G:/application.properties&lt;/p&gt;
&lt;h3&gt;7、外部配置加载顺序&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;&lt;strong&gt;SpringBoot也可以从以下位置加载配置； 优先级从高到低；高优先级的配置覆盖低优先级的配置，所有的配置会形成互补配置&lt;/strong&gt;&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;1.命令行参数&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;所有的配置都可以在命令行上进行指定&lt;/p&gt;
&lt;p&gt;java -jar spring-boot-02-config-02-0.0.1-SNAPSHOT.jar --server.port=8087  --server.context-path=/abc&lt;/p&gt;
&lt;p&gt;多个配置用空格分开； --配置项=值&lt;/p&gt;
&lt;p&gt;2.来自java:comp/env的JNDI属性&lt;/p&gt;
&lt;p&gt;3.Java系统属性（System.getProperties()）&lt;/p&gt;
&lt;p&gt;4.操作系统环境变量&lt;/p&gt;
&lt;p&gt;5.RandomValuePropertySource配置的random.*属性值&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;&lt;strong&gt;由jar包外向jar包内进行寻找；&lt;/strong&gt;&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;&lt;strong&gt;优先加载带profile&lt;/strong&gt;&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;6.jar包外部的application-{profile}.properties或application.yml(带spring.profile)配置文件&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;7.jar包内部的application-{profile}.properties或application.yml(带spring.profile)配置文件&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;&lt;strong&gt;再来加载不带profile&lt;/strong&gt;&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;8.jar包外部的application.properties或application.yml(不带spring.profile)配置文件&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;9.jar包内部的application.properties或application.yml(不带spring.profile)配置文件&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;10.@Configuration注解类上的@PropertySource&lt;/p&gt;
&lt;p&gt;11.通过SpringApplication.setDefaultProperties指定的默认属性&lt;/p&gt;
&lt;p&gt;所有支持的配置加载来源；&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;https://docs.spring.io/spring-boot/docs/1.5.9.RELEASE/reference/htmlsingle/#boot-features-external-config&quot;&gt;参考官方文档&lt;/a&gt;&lt;/p&gt;
&lt;h3&gt;8、自动配置原理&lt;/h3&gt;
&lt;p&gt;配置文件到底能写什么？怎么写？自动配置原理；&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;https://docs.spring.io/spring-boot/docs/1.5.9.RELEASE/reference/htmlsingle/#common-application-properties&quot;&gt;配置文件能配置的属性参照&lt;/a&gt;&lt;/p&gt;
&lt;h4&gt;1、&lt;strong&gt;自动配置原理：&lt;/strong&gt;&lt;/h4&gt;
&lt;h4&gt;1、@SpringBootConfiguration&lt;/h4&gt;
&lt;p&gt;@Configuration。代表当前是一个配置类&lt;/p&gt;
&lt;h4&gt;2、@ComponentScan&lt;/h4&gt;
&lt;p&gt;指定扫描哪些，Spring注解；&lt;/p&gt;
&lt;h4&gt;3、@EnableAutoConfiguration&lt;/h4&gt;
&lt;pre&gt;&lt;code&gt;@AutoConfigurationPackage
@Import(AutoConfigurationImportSelector.class)
public @interface EnableAutoConfiguration {}
&lt;/code&gt;&lt;/pre&gt;
&lt;h5&gt;1、@AutoConfigurationPackage&lt;/h5&gt;
&lt;p&gt;自动配置包？指定了默认的包规则&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;@Import(AutoConfigurationPackages.Registrar.class)  //给容器中导入一个组件
public @interface AutoConfigurationPackage {}

//利用Registrar给容器中导入一系列组件
//将指定的一个包下的所有组件导入进来？MainApplication 所在包下。
&lt;/code&gt;&lt;/pre&gt;
&lt;h5&gt;2、@Import(AutoConfigurationImportSelector.class)&lt;/h5&gt;
&lt;pre&gt;&lt;code&gt;1、利用getAutoConfigurationEntry(annotationMetadata);给容器中批量导入一些组件
2、调用List&amp;lt;String&amp;gt; configurations = getCandidateConfigurations(annotationMetadata, attributes)获取到所有需要导入到容器中的配置类
3、利用工厂加载 Map&amp;lt;String, List&amp;lt;String&amp;gt;&amp;gt; loadSpringFactories(@Nullable ClassLoader classLoader)；得到所有的组件
4、从META-INF/spring.factories位置来加载一个文件。
	默认扫描我们当前系统里面所有META-INF/spring.factories位置的文件
    spring-boot-autoconfigure-2.3.4.RELEASE.jar包里面也有META-INF/spring.factories
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;文件里面写死了spring-boot一启动就要给容器中加载的所有配置类
spring-boot-autoconfigure-2.3.4.RELEASE.jar/META-INF/spring.factories
## Auto Configure
org.springframework.boot.autoconfigure.EnableAutoConfiguration=\
org.springframework.boot.autoconfigure.admin.SpringApplicationAdminJmxAutoConfiguration,\
org.springframework.boot.autoconfigure.aop.AopAutoConfiguration,\
org.springframework.boot.autoconfigure.amqp.RabbitAutoConfiguration,\
org.springframework.boot.autoconfigure.batch.BatchAutoConfiguration,\
org.springframework.boot.autoconfigure.cache.CacheAutoConfiguration,\
org.springframework.boot.autoconfigure.cassandra.CassandraAutoConfiguration,\
org.springframework.boot.autoconfigure.context.ConfigurationPropertiesAutoConfiguration,\
org.springframework.boot.autoconfigure.context.LifecycleAutoConfiguration,\
org.springframework.boot.autoconfigure.context.MessageSourceAutoConfiguration,\
org.springframework.boot.autoconfigure.context.PropertyPlaceholderAutoConfiguration,\
org.springframework.boot.autoconfigure.couchbase.CouchbaseAutoConfiguration,\
org.springframework.boot.autoconfigure.dao.PersistenceExceptionTranslationAutoConfiguration,\
org.springframework.boot.autoconfigure.data.cassandra.CassandraDataAutoConfiguration,\
org.springframework.boot.autoconfigure.data.cassandra.CassandraReactiveDataAutoConfiguration,\
org.springframework.boot.autoconfigure.data.cassandra.CassandraReactiveRepositoriesAutoConfiguration,\
org.springframework.boot.autoconfigure.data.cassandra.CassandraRepositoriesAutoConfiguration,\
org.springframework.boot.autoconfigure.data.couchbase.CouchbaseDataAutoConfiguration,\
org.springframework.boot.autoconfigure.data.couchbase.CouchbaseReactiveDataAutoConfiguration,\
org.springframework.boot.autoconfigure.data.couchbase.CouchbaseReactiveRepositoriesAutoConfiguration,\
org.springframework.boot.autoconfigure.data.couchbase.CouchbaseRepositoriesAutoConfiguration,\
org.springframework.boot.autoconfigure.data.elasticsearch.ElasticsearchDataAutoConfiguration,\
org.springframework.boot.autoconfigure.data.elasticsearch.ElasticsearchRepositoriesAutoConfiguration,\
org.springframework.boot.autoconfigure.data.elasticsearch.ReactiveElasticsearchRepositoriesAutoConfiguration,\
org.springframework.boot.autoconfigure.data.elasticsearch.ReactiveElasticsearchRestClientAutoConfiguration,\
org.springframework.boot.autoconfigure.data.jdbc.JdbcRepositoriesAutoConfiguration,\
org.springframework.boot.autoconfigure.data.jpa.JpaRepositoriesAutoConfiguration,\
org.springframework.boot.autoconfigure.data.ldap.LdapRepositoriesAutoConfiguration,\
org.springframework.boot.autoconfigure.data.mongo.MongoDataAutoConfiguration,\
org.springframework.boot.autoconfigure.data.mongo.MongoReactiveDataAutoConfiguration,\
org.springframework.boot.autoconfigure.data.mongo.MongoReactiveRepositoriesAutoConfiguration,\
org.springframework.boot.autoconfigure.data.mongo.MongoRepositoriesAutoConfiguration,\
org.springframework.boot.autoconfigure.data.neo4j.Neo4jDataAutoConfiguration,\
org.springframework.boot.autoconfigure.data.neo4j.Neo4jRepositoriesAutoConfiguration,\
org.springframework.boot.autoconfigure.data.solr.SolrRepositoriesAutoConfiguration,\
org.springframework.boot.autoconfigure.data.r2dbc.R2dbcDataAutoConfiguration,\
org.springframework.boot.autoconfigure.data.r2dbc.R2dbcRepositoriesAutoConfiguration,\
org.springframework.boot.autoconfigure.data.r2dbc.R2dbcTransactionManagerAutoConfiguration,\
org.springframework.boot.autoconfigure.data.redis.RedisAutoConfiguration,\
org.springframework.boot.autoconfigure.data.redis.RedisReactiveAutoConfiguration,\
org.springframework.boot.autoconfigure.data.redis.RedisRepositoriesAutoConfiguration,\
org.springframework.boot.autoconfigure.data.rest.RepositoryRestMvcAutoConfiguration,\
org.springframework.boot.autoconfigure.data.web.SpringDataWebAutoConfiguration,\
org.springframework.boot.autoconfigure.elasticsearch.ElasticsearchRestClientAutoConfiguration,\
org.springframework.boot.autoconfigure.flyway.FlywayAutoConfiguration,\
org.springframework.boot.autoconfigure.freemarker.FreeMarkerAutoConfiguration,\
org.springframework.boot.autoconfigure.groovy.template.GroovyTemplateAutoConfiguration,\
org.springframework.boot.autoconfigure.gson.GsonAutoConfiguration,\
org.springframework.boot.autoconfigure.h2.H2ConsoleAutoConfiguration,\
org.springframework.boot.autoconfigure.hateoas.HypermediaAutoConfiguration,\
org.springframework.boot.autoconfigure.hazelcast.HazelcastAutoConfiguration,\
org.springframework.boot.autoconfigure.hazelcast.HazelcastJpaDependencyAutoConfiguration,\
org.springframework.boot.autoconfigure.http.HttpMessageConvertersAutoConfiguration,\
org.springframework.boot.autoconfigure.http.codec.CodecsAutoConfiguration,\
org.springframework.boot.autoconfigure.influx.InfluxDbAutoConfiguration,\
org.springframework.boot.autoconfigure.info.ProjectInfoAutoConfiguration,\
org.springframework.boot.autoconfigure.integration.IntegrationAutoConfiguration,\
org.springframework.boot.autoconfigure.jackson.JacksonAutoConfiguration,\
org.springframework.boot.autoconfigure.jdbc.DataSourceAutoConfiguration,\
org.springframework.boot.autoconfigure.jdbc.JdbcTemplateAutoConfiguration,\
org.springframework.boot.autoconfigure.jdbc.JndiDataSourceAutoConfiguration,\
org.springframework.boot.autoconfigure.jdbc.XADataSourceAutoConfiguration,\
org.springframework.boot.autoconfigure.jdbc.DataSourceTransactionManagerAutoConfiguration,\
org.springframework.boot.autoconfigure.jms.JmsAutoConfiguration,\
org.springframework.boot.autoconfigure.jmx.JmxAutoConfiguration,\
org.springframework.boot.autoconfigure.jms.JndiConnectionFactoryAutoConfiguration,\
org.springframework.boot.autoconfigure.jms.activemq.ActiveMQAutoConfiguration,\
org.springframework.boot.autoconfigure.jms.artemis.ArtemisAutoConfiguration,\
org.springframework.boot.autoconfigure.jersey.JerseyAutoConfiguration,\
org.springframework.boot.autoconfigure.jooq.JooqAutoConfiguration,\
org.springframework.boot.autoconfigure.jsonb.JsonbAutoConfiguration,\
org.springframework.boot.autoconfigure.kafka.KafkaAutoConfiguration,\
org.springframework.boot.autoconfigure.availability.ApplicationAvailabilityAutoConfiguration,\
org.springframework.boot.autoconfigure.ldap.embedded.EmbeddedLdapAutoConfiguration,\
org.springframework.boot.autoconfigure.ldap.LdapAutoConfiguration,\
org.springframework.boot.autoconfigure.liquibase.LiquibaseAutoConfiguration,\
org.springframework.boot.autoconfigure.mail.MailSenderAutoConfiguration,\
org.springframework.boot.autoconfigure.mail.MailSenderValidatorAutoConfiguration,\
org.springframework.boot.autoconfigure.mongo.embedded.EmbeddedMongoAutoConfiguration,\
org.springframework.boot.autoconfigure.mongo.MongoAutoConfiguration,\
org.springframework.boot.autoconfigure.mongo.MongoReactiveAutoConfiguration,\
org.springframework.boot.autoconfigure.mustache.MustacheAutoConfiguration,\
org.springframework.boot.autoconfigure.orm.jpa.HibernateJpaAutoConfiguration,\
org.springframework.boot.autoconfigure.quartz.QuartzAutoConfiguration,\
org.springframework.boot.autoconfigure.r2dbc.R2dbcAutoConfiguration,\
org.springframework.boot.autoconfigure.rsocket.RSocketMessagingAutoConfiguration,\
org.springframework.boot.autoconfigure.rsocket.RSocketRequesterAutoConfiguration,\
org.springframework.boot.autoconfigure.rsocket.RSocketServerAutoConfiguration,\
org.springframework.boot.autoconfigure.rsocket.RSocketStrategiesAutoConfiguration,\
org.springframework.boot.autoconfigure.security.servlet.SecurityAutoConfiguration,\
org.springframework.boot.autoconfigure.security.servlet.UserDetailsServiceAutoConfiguration,\
org.springframework.boot.autoconfigure.security.servlet.SecurityFilterAutoConfiguration,\
org.springframework.boot.autoconfigure.security.reactive.ReactiveSecurityAutoConfiguration,\
org.springframework.boot.autoconfigure.security.reactive.ReactiveUserDetailsServiceAutoConfiguration,\
org.springframework.boot.autoconfigure.security.rsocket.RSocketSecurityAutoConfiguration,\
org.springframework.boot.autoconfigure.security.saml2.Saml2RelyingPartyAutoConfiguration,\
org.springframework.boot.autoconfigure.sendgrid.SendGridAutoConfiguration,\
org.springframework.boot.autoconfigure.session.SessionAutoConfiguration,\
org.springframework.boot.autoconfigure.security.oauth2.client.servlet.OAuth2ClientAutoConfiguration,\
org.springframework.boot.autoconfigure.security.oauth2.client.reactive.ReactiveOAuth2ClientAutoConfiguration,\
org.springframework.boot.autoconfigure.security.oauth2.resource.servlet.OAuth2ResourceServerAutoConfiguration,\
org.springframework.boot.autoconfigure.security.oauth2.resource.reactive.ReactiveOAuth2ResourceServerAutoConfiguration,\
org.springframework.boot.autoconfigure.solr.SolrAutoConfiguration,\
org.springframework.boot.autoconfigure.task.TaskExecutionAutoConfiguration,\
org.springframework.boot.autoconfigure.task.TaskSchedulingAutoConfiguration,\
org.springframework.boot.autoconfigure.thymeleaf.ThymeleafAutoConfiguration,\
org.springframework.boot.autoconfigure.transaction.TransactionAutoConfiguration,\
org.springframework.boot.autoconfigure.transaction.jta.JtaAutoConfiguration,\
org.springframework.boot.autoconfigure.validation.ValidationAutoConfiguration,\
org.springframework.boot.autoconfigure.web.client.RestTemplateAutoConfiguration,\
org.springframework.boot.autoconfigure.web.embedded.EmbeddedWebServerFactoryCustomizerAutoConfiguration,\
org.springframework.boot.autoconfigure.web.reactive.HttpHandlerAutoConfiguration,\
org.springframework.boot.autoconfigure.web.reactive.ReactiveWebServerFactoryAutoConfiguration,\
org.springframework.boot.autoconfigure.web.reactive.WebFluxAutoConfiguration,\
org.springframework.boot.autoconfigure.web.reactive.error.ErrorWebFluxAutoConfiguration,\
org.springframework.boot.autoconfigure.web.reactive.function.client.ClientHttpConnectorAutoConfiguration,\
org.springframework.boot.autoconfigure.web.reactive.function.client.WebClientAutoConfiguration,\
org.springframework.boot.autoconfigure.web.servlet.DispatcherServletAutoConfiguration,\
org.springframework.boot.autoconfigure.web.servlet.ServletWebServerFactoryAutoConfiguration,\
org.springframework.boot.autoconfigure.web.servlet.error.ErrorMvcAutoConfiguration,\
org.springframework.boot.autoconfigure.web.servlet.HttpEncodingAutoConfiguration,\
org.springframework.boot.autoconfigure.web.servlet.MultipartAutoConfiguration,\
org.springframework.boot.autoconfigure.web.servlet.WebMvcAutoConfiguration,\
org.springframework.boot.autoconfigure.websocket.reactive.WebSocketReactiveAutoConfiguration,\
org.springframework.boot.autoconfigure.websocket.servlet.WebSocketServletAutoConfiguration,\
org.springframework.boot.autoconfigure.websocket.servlet.WebSocketMessagingAutoConfiguration,\
org.springframework.boot.autoconfigure.webservices.WebServicesAutoConfiguration,\
org.springframework.boot.autoconfigure.webservices.client.WebServiceTemplateAutoConfiguration
&lt;/code&gt;&lt;/pre&gt;
&lt;h4&gt;3.2、按需开启自动配置项&lt;/h4&gt;
&lt;pre&gt;&lt;code&gt;虽然我们127个场景的所有自动配置启动的时候默认全部加载。xxxxAutoConfiguration
按照条件装配规则（@Conditional），最终会按需配置。
&lt;/code&gt;&lt;/pre&gt;
&lt;h4&gt;3.3、修改默认配置&lt;/h4&gt;
&lt;pre&gt;&lt;code&gt;        @Bean
		@ConditionalOnBean(MultipartResolver.class)  //容器中有这个类型组件
		@ConditionalOnMissingBean(name = DispatcherServlet.MULTIPART_RESOLVER_BEAN_NAME) //容器中没有这个名字 multipartResolver 的组件
		public MultipartResolver multipartResolver(MultipartResolver resolver) {
            //给@Bean标注的方法传入了对象参数，这个参数的值就会从容器中找。
            //SpringMVC multipartResolver。防止有些用户配置的文件上传解析器不符合规范
			// Detect if the user has created a MultipartResolver but named it incorrectly
			return resolver;
		}
给容器中加入了文件上传解析器；

&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;SpringBoot默认会在底层配好所有的组件。但是如果用户自己配置了以用户的优先&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;@Bean
	@ConditionalOnMissingBean
	public CharacterEncodingFilter characterEncodingFilter() {
    }
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;总结：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;SpringBoot先加载所有的自动配置类  xxxxxAutoConfiguration&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;每个自动配置类按照条件进行生效，默认都会绑定配置文件指定的值。xxxxProperties里面拿。xxxProperties和配置文件进行了绑定&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;生效的配置类就会给容器中装配很多组件&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;只要容器中有这些组件，相当于这些功能就有了&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;定制化配置&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;ul&gt;
&lt;li&gt;用户直接自己@Bean替换底层的组件&lt;/li&gt;
&lt;li&gt;用户去看这个组件是获取的配置文件什么值就去修改。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;xxxxxAutoConfiguration ---&amp;gt; 组件  ---&amp;gt;&lt;/strong&gt; &lt;strong&gt;xxxxProperties里面拿值  ----&amp;gt; application.properties&lt;/strong&gt;&lt;/p&gt;
&lt;h4&gt;3.4、最佳实践&lt;/h4&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;引入场景依赖&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;ul&gt;
&lt;li&gt;https://docs.spring.io/spring-boot/docs/current/reference/html/using-spring-boot.html#using-boot-starter&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;查看自动配置了哪些（选做）&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;ul&gt;
&lt;li&gt;自己分析，引入场景对应的自动配置一般都生效了&lt;/li&gt;
&lt;li&gt;配置文件中debug=true开启自动配置报告。Negative（不生效）\Positive（生效）&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;是否需要修改&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;ul&gt;
&lt;li&gt;参照文档修改配置项&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;ul&gt;
&lt;li&gt;https://docs.spring.io/spring-boot/docs/current/reference/html/appendix-application-properties.html#common-application-properties&lt;/li&gt;
&lt;li&gt;自己分析。xxxxProperties绑定了配置文件的哪些。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;ul&gt;
&lt;li&gt;自定义加入或者替换组件&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;ul&gt;
&lt;li&gt;@Bean、@Component。。。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;ul&gt;
&lt;li&gt;自定义器  &lt;strong&gt;XXXXXCustomizer&lt;/strong&gt;；&lt;/li&gt;
&lt;li&gt;......&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;&lt;strong&gt;将 类路径下  META-IN/spring.factories 里面配置的所有EnableAutoConfiguration的值加入到了容器中；&lt;/strong&gt;&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;## Auto Configure
org.springframework.boot.autoconfigure.EnableAutoConfiguration=\
org.springframework.boot.autoconfigure.admin.SpringApplicationAdminJmxAutoConfiguration,\
org.springframework.boot.autoconfigure.aop.AopAutoConfiguration,\
org.springframework.boot.autoconfigure.amqp.RabbitAutoConfiguration,\
org.springframework.boot.autoconfigure.batch.BatchAutoConfiguration,\
org.springframework.boot.autoconfigure.cache.CacheAutoConfiguration,\
org.springframework.boot.autoconfigure.cassandra.CassandraAutoConfiguration,\
org.springframework.boot.autoconfigure.cloud.CloudAutoConfiguration,\
org.springframework.boot.autoconfigure.context.ConfigurationPropertiesAutoConfiguration,\
org.springframework.boot.autoconfigure.context.MessageSourceAutoConfiguration,\
org.springframework.boot.autoconfigure.context.PropertyPlaceholderAutoConfiguration,\
org.springframework.boot.autoconfigure.couchbase.CouchbaseAutoConfiguration,\
org.springframework.boot.autoconfigure.dao.PersistenceExceptionTranslationAutoConfiguration,\
org.springframework.boot.autoconfigure.data.cassandra.CassandraDataAutoConfiguration,\
org.springframework.boot.autoconfigure.data.cassandra.CassandraRepositoriesAutoConfiguration,\
org.springframework.boot.autoconfigure.data.couchbase.CouchbaseDataAutoConfiguration,\
org.springframework.boot.autoconfigure.data.couchbase.CouchbaseRepositoriesAutoConfiguration,\
org.springframework.boot.autoconfigure.data.elasticsearch.ElasticsearchAutoConfiguration,\
org.springframework.boot.autoconfigure.data.elasticsearch.ElasticsearchDataAutoConfiguration,\
org.springframework.boot.autoconfigure.data.elasticsearch.ElasticsearchRepositoriesAutoConfiguration,\
org.springframework.boot.autoconfigure.data.jpa.JpaRepositoriesAutoConfiguration,\
org.springframework.boot.autoconfigure.data.ldap.LdapDataAutoConfiguration,\
org.springframework.boot.autoconfigure.data.ldap.LdapRepositoriesAutoConfiguration,\
org.springframework.boot.autoconfigure.data.mongo.MongoDataAutoConfiguration,\
org.springframework.boot.autoconfigure.data.mongo.MongoRepositoriesAutoConfiguration,\
org.springframework.boot.autoconfigure.data.neo4j.Neo4jDataAutoConfiguration,\
org.springframework.boot.autoconfigure.data.neo4j.Neo4jRepositoriesAutoConfiguration,\
org.springframework.boot.autoconfigure.data.solr.SolrRepositoriesAutoConfiguration,\
org.springframework.boot.autoconfigure.data.redis.RedisAutoConfiguration,\
org.springframework.boot.autoconfigure.data.redis.RedisRepositoriesAutoConfiguration,\
org.springframework.boot.autoconfigure.data.rest.RepositoryRestMvcAutoConfiguration,\
org.springframework.boot.autoconfigure.data.web.SpringDataWebAutoConfiguration,\
org.springframework.boot.autoconfigure.elasticsearch.jest.JestAutoConfiguration,\
org.springframework.boot.autoconfigure.freemarker.FreeMarkerAutoConfiguration,\
org.springframework.boot.autoconfigure.gson.GsonAutoConfiguration,\
org.springframework.boot.autoconfigure.h2.H2ConsoleAutoConfiguration,\
org.springframework.boot.autoconfigure.hateoas.HypermediaAutoConfiguration,\
org.springframework.boot.autoconfigure.hazelcast.HazelcastAutoConfiguration,\
org.springframework.boot.autoconfigure.hazelcast.HazelcastJpaDependencyAutoConfiguration,\
org.springframework.boot.autoconfigure.info.ProjectInfoAutoConfiguration,\
org.springframework.boot.autoconfigure.integration.IntegrationAutoConfiguration,\
org.springframework.boot.autoconfigure.jackson.JacksonAutoConfiguration,\
org.springframework.boot.autoconfigure.jdbc.DataSourceAutoConfiguration,\
org.springframework.boot.autoconfigure.jdbc.JdbcTemplateAutoConfiguration,\
org.springframework.boot.autoconfigure.jdbc.JndiDataSourceAutoConfiguration,\
org.springframework.boot.autoconfigure.jdbc.XADataSourceAutoConfiguration,\
org.springframework.boot.autoconfigure.jdbc.DataSourceTransactionManagerAutoConfiguration,\
org.springframework.boot.autoconfigure.jms.JmsAutoConfiguration,\
org.springframework.boot.autoconfigure.jmx.JmxAutoConfiguration,\
org.springframework.boot.autoconfigure.jms.JndiConnectionFactoryAutoConfiguration,\
org.springframework.boot.autoconfigure.jms.activemq.ActiveMQAutoConfiguration,\
org.springframework.boot.autoconfigure.jms.artemis.ArtemisAutoConfiguration,\
org.springframework.boot.autoconfigure.flyway.FlywayAutoConfiguration,\
org.springframework.boot.autoconfigure.groovy.template.GroovyTemplateAutoConfiguration,\
org.springframework.boot.autoconfigure.jersey.JerseyAutoConfiguration,\
org.springframework.boot.autoconfigure.jooq.JooqAutoConfiguration,\
org.springframework.boot.autoconfigure.kafka.KafkaAutoConfiguration,\
org.springframework.boot.autoconfigure.ldap.embedded.EmbeddedLdapAutoConfiguration,\
org.springframework.boot.autoconfigure.ldap.LdapAutoConfiguration,\
org.springframework.boot.autoconfigure.liquibase.LiquibaseAutoConfiguration,\
org.springframework.boot.autoconfigure.mail.MailSenderAutoConfiguration,\
org.springframework.boot.autoconfigure.mail.MailSenderValidatorAutoConfiguration,\
org.springframework.boot.autoconfigure.mobile.DeviceResolverAutoConfiguration,\
org.springframework.boot.autoconfigure.mobile.DeviceDelegatingViewResolverAutoConfiguration,\
org.springframework.boot.autoconfigure.mobile.SitePreferenceAutoConfiguration,\
org.springframework.boot.autoconfigure.mongo.embedded.EmbeddedMongoAutoConfiguration,\
org.springframework.boot.autoconfigure.mongo.MongoAutoConfiguration,\
org.springframework.boot.autoconfigure.mustache.MustacheAutoConfiguration,\
org.springframework.boot.autoconfigure.orm.jpa.HibernateJpaAutoConfiguration,\
org.springframework.boot.autoconfigure.reactor.ReactorAutoConfiguration,\
org.springframework.boot.autoconfigure.security.SecurityAutoConfiguration,\
org.springframework.boot.autoconfigure.security.SecurityFilterAutoConfiguration,\
org.springframework.boot.autoconfigure.security.FallbackWebSecurityAutoConfiguration,\
org.springframework.boot.autoconfigure.security.oauth2.OAuth2AutoConfiguration,\
org.springframework.boot.autoconfigure.sendgrid.SendGridAutoConfiguration,\
org.springframework.boot.autoconfigure.session.SessionAutoConfiguration,\
org.springframework.boot.autoconfigure.social.SocialWebAutoConfiguration,\
org.springframework.boot.autoconfigure.social.FacebookAutoConfiguration,\
org.springframework.boot.autoconfigure.social.LinkedInAutoConfiguration,\
org.springframework.boot.autoconfigure.social.TwitterAutoConfiguration,\
org.springframework.boot.autoconfigure.solr.SolrAutoConfiguration,\
org.springframework.boot.autoconfigure.thymeleaf.ThymeleafAutoConfiguration,\
org.springframework.boot.autoconfigure.transaction.TransactionAutoConfiguration,\
org.springframework.boot.autoconfigure.transaction.jta.JtaAutoConfiguration,\
org.springframework.boot.autoconfigure.validation.ValidationAutoConfiguration,\
org.springframework.boot.autoconfigure.web.DispatcherServletAutoConfiguration,\
org.springframework.boot.autoconfigure.web.EmbeddedServletContainerAutoConfiguration,\
org.springframework.boot.autoconfigure.web.ErrorMvcAutoConfiguration,\
org.springframework.boot.autoconfigure.web.HttpEncodingAutoConfiguration,\
org.springframework.boot.autoconfigure.web.HttpMessageConvertersAutoConfiguration,\
org.springframework.boot.autoconfigure.web.MultipartAutoConfiguration,\
org.springframework.boot.autoconfigure.web.ServerPropertiesAutoConfiguration,\
org.springframework.boot.autoconfigure.web.WebClientAutoConfiguration,\
org.springframework.boot.autoconfigure.web.WebMvcAutoConfiguration,\
org.springframework.boot.autoconfigure.websocket.WebSocketAutoConfiguration,\
org.springframework.boot.autoconfigure.websocket.WebSocketMessagingAutoConfiguration,\
org.springframework.boot.autoconfigure.webservices.WebServicesAutoConfiguration
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;案例、以**HttpEncodingAutoConfiguration（Http编码自动配置）**为例解释自动配置原理；&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;@Configuration   //表示这是一个配置类，以前编写的配置文件一样，也可以给容器中添加组件
@EnableConfigurationProperties(HttpEncodingProperties.class)  //启动指定类的ConfigurationProperties功能；将配置文件中对应的值和HttpEncodingProperties绑定起来；并把HttpEncodingProperties加入到ioc容器中

@ConditionalOnWebApplication //Spring底层@Conditional注解（Spring注解版），根据不同的条件，如果满足指定的条件，整个配置类里面的配置就会生效；    判断当前应用是否是web应用，如果是，当前配置类生效

@ConditionalOnClass(CharacterEncodingFilter.class)  //判断当前项目有没有这个类CharacterEncodingFilter；SpringMVC中进行乱码解决的过滤器；

@ConditionalOnProperty(prefix = &quot;spring.http.encoding&quot;, value = &quot;enabled&quot;, matchIfMissing = true)  //判断配置文件中是否存在某个配置  spring.http.encoding.enabled；如果不存在，判断也是成立的
//即使我们配置文件中不配置pring.http.encoding.enabled=true，也是默认生效的；
public class HttpEncodingAutoConfiguration {
  
  	//他已经和SpringBoot的配置文件映射了
  	private final HttpEncodingProperties properties;
  
   //只有一个有参构造器的情况下，参数的值就会从容器中拿
  	public HttpEncodingAutoConfiguration(HttpEncodingProperties properties) {
		this.properties = properties;
	}
  
    @Bean   //给容器中添加一个组件，这个组件的某些值需要从properties中获取
	@ConditionalOnMissingBean(CharacterEncodingFilter.class) //判断容器没有这个组件？
	public CharacterEncodingFilter characterEncodingFilter() {
		CharacterEncodingFilter filter = new OrderedCharacterEncodingFilter();
		filter.setEncoding(this.properties.getCharset().name());
		filter.setForceRequestEncoding(this.properties.shouldForce(Type.REQUEST));
		filter.setForceResponseEncoding(this.properties.shouldForce(Type.RESPONSE));
		return filter;
	}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;根据当前不同的条件判断，决定这个配置类是否生效？&lt;/p&gt;
&lt;p&gt;一但这个配置类生效；这个配置类就会给容器中添加各种组件；这些组件的属性是从对应的properties类中获取的，这些类里面的每一个属性又是和配置文件绑定的&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;精髓：&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;​	&lt;strong&gt;1）、SpringBoot启动会加载大量的自动配置类&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;​	&lt;strong&gt;2）、我们看我们需要的功能有没有SpringBoot默认写好的自动配置类；&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;​	&lt;strong&gt;3）、我们再来看这个自动配置类中到底配置了哪些组件；（只要我们要用的组件有，我们就不需要再来配置了）&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;​	&lt;strong&gt;4）、给容器中自动配置类添加组件的时候，会从properties类中获取某些属性。我们就可以在配置文件中指定这些属性的值；&lt;/strong&gt;&lt;/p&gt;
&lt;h4&gt;@Conditional派生注解&lt;/h4&gt;
&lt;p&gt;（Spring注解版原生的@Conditional作用）&lt;/p&gt;
&lt;p&gt;作用：必须是@Conditional指定的条件成立，才给容器中添加组件，配置配里面的所有内容才生效；&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;@Conditional扩展注解&lt;/th&gt;
&lt;th&gt;作用（判断是否满足当前指定条件）&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;@ConditionalOnJava&lt;/td&gt;
&lt;td&gt;系统的java版本是否符合要求&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;@ConditionalOnBean&lt;/td&gt;
&lt;td&gt;容器中存在指定Bean；&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;@ConditionalOnMissingBean&lt;/td&gt;
&lt;td&gt;容器中不存在指定Bean；&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;@ConditionalOnExpression&lt;/td&gt;
&lt;td&gt;满足SpEL表达式指定&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;@ConditionalOnClass&lt;/td&gt;
&lt;td&gt;系统中有指定的类&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;@ConditionalOnMissingClass&lt;/td&gt;
&lt;td&gt;系统中没有指定的类&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;@ConditionalOnSingleCandidate&lt;/td&gt;
&lt;td&gt;容器中只有一个指定的Bean，或者这个Bean是首选Bean&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;@ConditionalOnProperty&lt;/td&gt;
&lt;td&gt;系统中指定的属性是否有指定的值&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;@ConditionalOnResource&lt;/td&gt;
&lt;td&gt;类路径下是否存在指定资源文件&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;@ConditionalOnWebApplication&lt;/td&gt;
&lt;td&gt;当前是web环境&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;@ConditionalOnNotWebApplication&lt;/td&gt;
&lt;td&gt;当前不是web环境&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;@ConditionalOnJndi&lt;/td&gt;
&lt;td&gt;JNDI存在指定项&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;&lt;strong&gt;自动配置类必须在一定的条件下才能生效；&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;我们怎么知道哪些自动配置类生效；&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;&lt;strong&gt;我们可以通过启用  debug=true属性；来让控制台打印自动配置报告&lt;/strong&gt;&lt;/strong&gt;，这样我们就可以很方便的知道哪些自动配置类生效；&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;**=****=****=****=****=**
AUTO-CONFIGURATION REPORT
**=****=****=****=****=**


Positive matches:（自动配置类启用的）
-----------------

   DispatcherServletAutoConfiguration matched:
      - @ConditionalOnClass found required class &apos;org.springframework.web.servlet.DispatcherServlet&apos;; @ConditionalOnMissingClass did not find unwanted class (OnClassCondition)
      - @ConditionalOnWebApplication (required) found StandardServletEnvironment (OnWebApplicationCondition)
        
    
Negative matches:（没有启动，没有匹配成功的自动配置类）
-----------------

   ActiveMQAutoConfiguration:
      Did not match:
         - @ConditionalOnClass did not find required classes &apos;javax.jms.ConnectionFactory&apos;, &apos;org.apache.activemq.ActiveMQConnectionFactory&apos; (OnClassCondition)

   AopAutoConfiguration:
      Did not match:
         - @ConditionalOnClass did not find required classes &apos;org.aspectj.lang.annotation.Aspect&apos;, &apos;org.aspectj.lang.reflect.Advice&apos; (OnClassCondition)
        
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;9、常见springboot 配置&lt;/h3&gt;
&lt;h4&gt;(1)、springboot之jackson&lt;/h4&gt;
&lt;pre&gt;&lt;code&gt;
spring:
  mvc:
	##时间格式序列化时，str--&amp;gt;date 指定（str）日期格式
    date-format: yyyy-MM-dd HH:mm:ss
  jackson:
    #日期格式化 输出
    date-format: yyyy-MM-dd HH:mm:ss
    serialization:
       #格式化输出 
      indent_output: true
      #忽略无法转换的对象
      fail_on_empty_beans: false
    #设置空如何序列化
    defaultPropertyInclusion: NON_EMPTY
    deserialization:
      #允许对象忽略json中不存在的属性
      fail_on_unknown_properties: false
    parser:
      #允许出现特殊字符和转义符
      allow_unquoted_control_chars: true
      #允许出现单引号
      allow_single_quotes: true
&lt;/code&gt;&lt;/pre&gt;
&lt;h4&gt;（2）redis&lt;/h4&gt;
&lt;pre&gt;&lt;code&gt;spring:
  redis:
##    host: 192.168.2.34
    host: 47.102.115.60
    password: 123456
    port: 60010
&lt;/code&gt;&lt;/pre&gt;
&lt;h4&gt;(3)mysql&lt;/h4&gt;
&lt;pre&gt;&lt;code&gt;spring: 
   datasource:
    url: jdbc:mysql://192.168.2.33:3306/dftc_ots?useUnicode=true&amp;amp;characterEncoding=utf-8&amp;amp;useSSL=false&amp;amp;serverTimezone=GMT%2B8
    driver-class-name: com.mysql.cj.jdbc.Driver
    username: root
    password: Tckk@2019_mysql
&lt;/code&gt;&lt;/pre&gt;
&lt;h4&gt;(4)mybatisPlus&lt;/h4&gt;
&lt;pre&gt;&lt;code&gt;mybatis-plus:
  configuration:
    cache-enabled: false
    map-underscore-to-camel-case: true
    log-impl: org.apache.ibatis.logging.stdout.StdOutImpl
  global-config:
    id-type: 2
    db-column-underline: true
    table-prefix: dftc_oss_
    refresh-mapper: true
  type-handlers-package: com.dftcmedia.tckk.microservice.ots.common.typehandler
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;三、日志&lt;/h2&gt;
&lt;h3&gt;1、日志框架&lt;/h3&gt;
&lt;p&gt;小张；开发一个大型系统；&lt;/p&gt;
&lt;p&gt;​		1、System.out.println(&quot;&quot;)；将关键数据打印在控制台；去掉？写在一个文件？&lt;/p&gt;
&lt;p&gt;​		2、框架来记录系统的一些运行时信息；日志框架 ；  zhanglogging.jar；&lt;/p&gt;
&lt;p&gt;​		3、高大上的几个功能？异步模式？自动归档？xxxx？  zhanglogging-good.jar？&lt;/p&gt;
&lt;p&gt;​		4、将以前框架卸下来？换上新的框架，重新修改之前相关的API；zhanglogging-prefect.jar；&lt;/p&gt;
&lt;p&gt;​		5、JDBC---数据库驱动；&lt;/p&gt;
&lt;p&gt;​			写了一个统一的接口层；日志门面（日志的一个抽象层）；logging-abstract.jar；&lt;/p&gt;
&lt;p&gt;​			给项目中导入具体的日志实现就行了；我们之前的日志框架都是实现的抽象层；&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;市面上的日志框架；&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;JUL、JCL、Jboss-logging、logback、log4j、log4j2、slf4j....&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;日志门面  （日志的抽象层）&lt;/th&gt;
&lt;th&gt;日志实现&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;s&gt;JCL（Jakarta  Commons Logging）&lt;/s&gt;    SLF4j（Simple  Logging Facade for Java）    &lt;strong&gt;&lt;s&gt;jboss-logging&lt;/s&gt;&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Log4j  JUL（java.util.logging）  Log4j2  &lt;strong&gt;Logback&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;左边选一个门面（抽象层）、右边来选一个实现；&lt;/p&gt;
&lt;p&gt;日志门面：  SLF4J；&lt;/p&gt;
&lt;p&gt;日志实现：Logback；&lt;/p&gt;
&lt;p&gt;SpringBoot：底层是Spring框架，Spring框架默认是用JCL；‘&lt;/p&gt;
&lt;p&gt;​	&lt;strong&gt;&lt;strong&gt;SpringBoot选用 SLF4j和logback；&lt;/strong&gt;&lt;/strong&gt;&lt;/p&gt;
&lt;h3&gt;2、SLF4j使用&lt;/h3&gt;
&lt;h4&gt;1、如何在系统中使用SLF4j   https://www.slf4j.org&lt;/h4&gt;
&lt;p&gt;以后开发的时候，日志记录方法的调用，不应该来直接调用日志的实现类，而是调用日志抽象层里面的方法；&lt;/p&gt;
&lt;p&gt;给系统里面导入slf4j的jar和  logback的实现jar&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

public class HelloWorld {
  public static void main(String[] args) {
    Logger logger = LoggerFactory.getLogger(HelloWorld.class);
    logger.info(&quot;Hello World&quot;);
  }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;图示；&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://tangtangit.com/_astro/concrete-bindings.CZPkbyQJ_ZrXr9F.webp&quot; alt=&quot;images/concrete-bindings.png&quot; /&gt;&lt;/p&gt;
&lt;p&gt;每一个日志的实现框架都有自己的配置文件。使用slf4j以后，&lt;strong&gt;配置文件还是做成日志实现框架自己本身的配置文件；&lt;/strong&gt;&lt;/p&gt;
&lt;h4&gt;2、遗留问题&lt;/h4&gt;
&lt;p&gt;a（slf4j+logback）: Spring（commons-logging）、Hibernate（jboss-logging）、MyBatis、xxxx&lt;/p&gt;
&lt;p&gt;统一日志记录，即使是别的框架和我一起统一使用slf4j进行输出？&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://tangtangit.com/_astro/legacy.G-iubMOj_1RmwDD.webp&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;如何让系统中所有的日志都统一到slf4j；&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;1、将系统中其他日志框架先排除出去；&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;2、用中间包来替换原有的日志框架；&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;3、我们导入slf4j其他的实现&lt;/strong&gt;&lt;/p&gt;
&lt;h3&gt;3、SpringBoot日志关系&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;		&amp;lt;dependency&amp;gt;
			&amp;lt;groupId&amp;gt;org.springframework.boot&amp;lt;/groupId&amp;gt;
			&amp;lt;artifactId&amp;gt;spring-boot-starter&amp;lt;/artifactId&amp;gt;
		&amp;lt;/dependency&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;SpringBoot使用它来做日志功能；&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;	&amp;lt;dependency&amp;gt;
			&amp;lt;groupId&amp;gt;org.springframework.boot&amp;lt;/groupId&amp;gt;
			&amp;lt;artifactId&amp;gt;spring-boot-starter-logging&amp;lt;/artifactId&amp;gt;
		&amp;lt;/dependency&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;底层依赖关系&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;%E6%90%9C%E7%8B%97%E6%88%AA%E5%9B%BE20180131220946.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;总结：&lt;/p&gt;
&lt;p&gt;​	1）、SpringBoot底层也是使用slf4j+logback的方式进行日志记录&lt;/p&gt;
&lt;p&gt;​	2）、SpringBoot也把其他的日志都替换成了slf4j；&lt;/p&gt;
&lt;p&gt;​	3）、中间替换包？&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;@SuppressWarnings(&quot;rawtypes&quot;)
public abstract class LogFactory {

    static String UNSUPPORTED_OPERATION_IN_JCL_OVER_SLF4J = &quot;http://www.slf4j.org/codes.html#unsupported_operation_in_jcl_over_slf4j&quot;;

    static LogFactory logFactory = new SLF4JLogFactory();
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;img src=&quot;%E6%90%9C%E7%8B%97%E6%88%AA%E5%9B%BE20180131221411.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;​	4）、如果我们要引入其他框架？一定要把这个框架的默认日志依赖移除掉？&lt;/p&gt;
&lt;p&gt;​			Spring框架用的是commons-logging；&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;		&amp;lt;dependency&amp;gt;
			&amp;lt;groupId&amp;gt;org.springframework&amp;lt;/groupId&amp;gt;
			&amp;lt;artifactId&amp;gt;spring-core&amp;lt;/artifactId&amp;gt;
			&amp;lt;exclusions&amp;gt;
				&amp;lt;exclusion&amp;gt;
					&amp;lt;groupId&amp;gt;commons-logging&amp;lt;/groupId&amp;gt;
					&amp;lt;artifactId&amp;gt;commons-logging&amp;lt;/artifactId&amp;gt;
				&amp;lt;/exclusion&amp;gt;
			&amp;lt;/exclusions&amp;gt;
		&amp;lt;/dependency&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;&lt;strong&gt;SpringBoot能自动适配所有的日志，而且底层使用slf4j+logback的方式记录日志，引入其他框架的时候，只需要把这个框架依赖的日志框架排除掉即可；&lt;/strong&gt;&lt;/strong&gt;&lt;/p&gt;
&lt;h3&gt;4、日志使用；&lt;/h3&gt;
&lt;h4&gt;1、默认配置&lt;/h4&gt;
&lt;p&gt;SpringBoot默认帮我们配置好了日志；&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;	//记录器
	Logger logger = LoggerFactory.getLogger(getClass());
	@Test
	public void contextLoads() {
		//System.out.println();

		//日志的级别；
		//由低到高   trace&amp;lt;debug&amp;lt;info&amp;lt;warn&amp;lt;error
		//可以调整输出的日志级别；日志就只会在这个级别以以后的高级别生效
		logger.trace(&quot;这是trace日志...&quot;);
		logger.debug(&quot;这是debug日志...&quot;);
		//SpringBoot默认给我们使用的是info级别的，没有指定级别的就用SpringBoot默认规定的级别；root级别
		logger.info(&quot;这是info日志...&quot;);
		logger.warn(&quot;这是warn日志...&quot;);
		logger.error(&quot;这是error日志...&quot;);


	}
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;    日志输出格式：
		%d表示日期时间，
		%thread表示线程名，
		%-5level：级别从左显示5个字符宽度
		%logger{50} 表示logger名字最长50个字符，否则按照句点分割。 
		%msg：日志消息，
		%n是换行符
    --&amp;gt;
    %d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger{50} - %msg%n
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;SpringBoot修改日志的默认配置&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;logging.level.com.atguigu=trace


#logging.path=
## 不指定路径在当前项目下生成springboot.log日志
## 可以指定完整的路径；
#logging.file=G:/springboot.log

## 在当前磁盘的根路径下创建spring文件夹和里面的log文件夹；使用 spring.log 作为默认文件
logging.path=/spring/log

##  在控制台输出的日志的格式
logging.pattern.console=%d{yyyy-MM-dd} [%thread] %-5level %logger{50} - %msg%n
## 指定文件中日志输出的格式
logging.pattern.file=%d{yyyy-MM-dd} **= [%thread] **= %-5level **= %logger{50} **== %msg%n
&lt;/code&gt;&lt;/pre&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;logging.file&lt;/th&gt;
&lt;th&gt;logging.path&lt;/th&gt;
&lt;th&gt;Example&lt;/th&gt;
&lt;th&gt;Description&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;(none)&lt;/td&gt;
&lt;td&gt;(none)&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;td&gt;只在控制台输出&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;指定文件名&lt;/td&gt;
&lt;td&gt;(none)&lt;/td&gt;
&lt;td&gt;my.log&lt;/td&gt;
&lt;td&gt;输出日志到my.log文件&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;(none)&lt;/td&gt;
&lt;td&gt;指定目录&lt;/td&gt;
&lt;td&gt;/var/log&lt;/td&gt;
&lt;td&gt;输出到指定目录的 spring.log 文件中&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;h4&gt;2、指定配置&lt;/h4&gt;
&lt;p&gt;给类路径下放上每个日志框架自己的配置文件即可；SpringBoot就不使用他默认配置的了&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Logging System&lt;/th&gt;
&lt;th&gt;Customization&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Logback&lt;/td&gt;
&lt;td&gt;&lt;code&gt;logback-spring.xml&lt;/code&gt;, &lt;code&gt;logback-spring.groovy&lt;/code&gt;, &lt;code&gt;logback.xml&lt;/code&gt; or &lt;code&gt;logback.groovy&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Log4j2&lt;/td&gt;
&lt;td&gt;&lt;code&gt;log4j2-spring.xml&lt;/code&gt; or &lt;code&gt;log4j2.xml&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;JDK (Java Util Logging)&lt;/td&gt;
&lt;td&gt;&lt;code&gt;logging.properties&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;logback.xml：直接就被日志框架识别了；&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;logback-spring.xml&lt;/strong&gt;：日志框架就不直接加载日志的配置项，由SpringBoot解析日志配置，可以使用SpringBoot的高级Profile功能&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&amp;lt;springProfile name=&quot;staging&quot;&amp;gt;
    &amp;lt;!-- configuration to be enabled when the &quot;staging&quot; profile is active --&amp;gt;
  	可以指定某段配置只在某个环境下生效
&amp;lt;/springProfile&amp;gt;

&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;如：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&amp;lt;appender name=&quot;stdout&quot; class=&quot;ch.qos.logback.core.ConsoleAppender&quot;&amp;gt;
        &amp;lt;!--
        日志输出格式：
			%d表示日期时间，
			%thread表示线程名，
			%-5level：级别从左显示5个字符宽度
			%logger{50} 表示logger名字最长50个字符，否则按照句点分割。 
			%msg：日志消息，
			%n是换行符
        --&amp;gt;
        &amp;lt;layout class=&quot;ch.qos.logback.classic.PatternLayout&quot;&amp;gt;
            &amp;lt;springProfile name=&quot;dev&quot;&amp;gt;
                &amp;lt;pattern&amp;gt;%d{yyyy-MM-dd HH:mm:ss.SSS} ----&amp;gt; [%thread] ---&amp;gt; %-5level %logger{50} - %msg%n&amp;lt;/pattern&amp;gt;
            &amp;lt;/springProfile&amp;gt;
            &amp;lt;springProfile name=&quot;!dev&quot;&amp;gt;
                &amp;lt;pattern&amp;gt;%d{yyyy-MM-dd HH:mm:ss.SSS} **== [%thread] **== %-5level %logger{50} - %msg%n&amp;lt;/pattern&amp;gt;
            &amp;lt;/springProfile&amp;gt;
        &amp;lt;/layout&amp;gt;
    &amp;lt;/appender&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;如果使用logback.xml作为日志配置文件，还要使用profile功能，会有以下错误&lt;/p&gt;
&lt;p&gt;&lt;code&gt;no applicable action for [springProfile]&lt;/code&gt;&lt;/p&gt;
&lt;h4&gt;3、logback-spring.xml，实际使用规则案例&lt;/h4&gt;
&lt;p&gt;参考网址：https://blog.csdn.net/inke88/article/details/75007649#%E8%87%AA%E5%AE%9A%E4%B9%89%E6%97%A5%E5%BF%97%E9%85%8D%E7%BD%AE&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&amp;lt;?xml version=&quot;1.0&quot; encoding=&quot;utf-8&quot; ?&amp;gt;
&amp;lt;!--
    日志的级别；
	由低到高   trace&amp;lt;debug&amp;lt;info&amp;lt;warn&amp;lt;error
--&amp;gt;
&amp;lt;configuration&amp;gt;

    &amp;lt;!--用来定义变量值的标签， 有两个属性，name和value；其中name的值是变量的名称，
    value的值时变量定义的值。通过定义的值会被插入到logger上下文中。定义变量后，可以使“${}”来使用变量--&amp;gt;
    &amp;lt;property name=&quot;logback.logdir&quot; value=&quot;C:/Users/Zy/Desktop/sell&quot;/&amp;gt;

    &amp;lt;!--控制台输出配置项--&amp;gt;
    &amp;lt;appender name=&quot;consoleLog&quot; class=&quot;ch.qos.logback.core.ConsoleAppender&quot;&amp;gt;
        &amp;lt;!--展示--&amp;gt;
        &amp;lt;layout class=&quot;ch.qos.logback.classic.PatternLayout&quot;&amp;gt;
            &amp;lt;pattern&amp;gt;
                %d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger{50} - %msg%n
            &amp;lt;/pattern&amp;gt;
        &amp;lt;/layout&amp;gt;
    &amp;lt;/appender&amp;gt;

    &amp;lt;!--log文件 每天输出日志文件--&amp;gt;
    &amp;lt;appender name=&quot;fileInfoLog&quot; class=&quot;ch.qos.logback.core.rolling.RollingFileAppender&quot;&amp;gt;
        &amp;lt;!--LevelFilter 这个类，等级过滤 --&amp;gt;
        &amp;lt;!--配置过滤规则 加上这filter表示，只捕获该ERROR 及其以上的异常--&amp;gt;
        &amp;lt;filter class=&quot;ch.qos.logback.classic.filter.LevelFilter&quot;&amp;gt;
            &amp;lt;level&amp;gt;ERROR&amp;lt;/level&amp;gt;
            &amp;lt;!--命中使用 中立规则（命中error时，使用其他appender）--&amp;gt;
            &amp;lt;onMatch&amp;gt;DENY&amp;lt;/onMatch&amp;gt;
            &amp;lt;!--没命中 接收（没有命中error时，当前appender进行接收处理）--&amp;gt;
            &amp;lt;onMismatch&amp;gt;ACCEPT&amp;lt;/onMismatch&amp;gt;
        &amp;lt;/filter&amp;gt;
        &amp;lt;!--日志输出格式--&amp;gt;
        &amp;lt;encoder&amp;gt;
            &amp;lt;charset&amp;gt;UTF-8&amp;lt;/charset&amp;gt;
            &amp;lt;pattern&amp;gt;
                %d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger{50} - %msg%n
            &amp;lt;/pattern&amp;gt;
        &amp;lt;/encoder&amp;gt;
        &amp;lt;!--滚动策略--&amp;gt;
        &amp;lt;rollingPolicy class=&quot;ch.qos.logback.core.rolling.TimeBasedRollingPolicy&quot;&amp;gt;
            &amp;lt;!--路径  在日志的文件名上 带时间 %d--&amp;gt;
            &amp;lt;fileNamePattern&amp;gt;${logback.logdir}/info.%d.log&amp;lt;/fileNamePattern&amp;gt;
            &amp;lt;!--只保留最近90天的日志--&amp;gt;
            &amp;lt;maxHistory&amp;gt;90&amp;lt;/maxHistory&amp;gt;
            &amp;lt;!--用来指定日志文件的上限大小，那么到了这个值，就会删除旧的日志--&amp;gt;
            &amp;lt;!--&amp;lt;totalSizeCap&amp;gt;1GB&amp;lt;/totalSizeCap&amp;gt;--&amp;gt;
        &amp;lt;/rollingPolicy&amp;gt;
    &amp;lt;/appender&amp;gt;

    &amp;lt;!--error文件日志路径--&amp;gt;
    &amp;lt;appender name=&quot;fileErrorLog&quot; class=&quot;ch.qos.logback.core.rolling.RollingFileAppender&quot;&amp;gt;
        &amp;lt;!--ThresholdFilter 这个类是根据范围过滤--&amp;gt;
        &amp;lt;!--加上这filter表示，只捕获该ERROR 及其以上的异常--&amp;gt;
        &amp;lt;filter class=&quot;ch.qos.logback.classic.filter.ThresholdFilter&quot;&amp;gt;
            &amp;lt;level&amp;gt;ERROR&amp;lt;/level&amp;gt;
        &amp;lt;/filter&amp;gt;
        &amp;lt;!--日志输出格式--&amp;gt;
        &amp;lt;encoder&amp;gt;
            &amp;lt;charset&amp;gt;UTF-8&amp;lt;/charset&amp;gt;
            &amp;lt;pattern&amp;gt;
                %d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger{50} - %msg%n
            &amp;lt;/pattern&amp;gt;
        &amp;lt;/encoder&amp;gt;
        &amp;lt;!--滚动策略--&amp;gt;
        &amp;lt;rollingPolicy class=&quot;ch.qos.logback.core.rolling.TimeBasedRollingPolicy&quot;&amp;gt;
            &amp;lt;!--文件路径--&amp;gt;
            &amp;lt;fileNamePattern&amp;gt;${logback.logdir}/error.%d.log&amp;lt;/fileNamePattern&amp;gt;
            &amp;lt;!--只保留最近90天的日志--&amp;gt;
            &amp;lt;maxHistory&amp;gt;90&amp;lt;/maxHistory&amp;gt;
            &amp;lt;!--用来指定日志文件的上限大小，那么到了这个值，就会删除旧的日志--&amp;gt;
            &amp;lt;!--&amp;lt;totalSizeCap&amp;gt;1GB&amp;lt;/totalSizeCap&amp;gt;--&amp;gt;
        &amp;lt;/rollingPolicy&amp;gt;
    &amp;lt;/appender&amp;gt;

    &amp;lt;!--日志级别--&amp;gt;
    &amp;lt;root level=&quot;info&quot;&amp;gt;
        &amp;lt;appender-ref ref=&quot;consoleLog&quot;/&amp;gt;
        &amp;lt;!--指定--&amp;gt;
        &amp;lt;appender-ref ref=&quot;fileInfoLog&quot;/&amp;gt;
        &amp;lt;appender-ref ref=&quot;fileErrorLog&quot;/&amp;gt;
    &amp;lt;/root&amp;gt;
&amp;lt;/configuration&amp;gt;

&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;5、切换日志框架&lt;/h3&gt;
&lt;p&gt;可以按照slf4j的日志适配图，进行相关的切换；&lt;/p&gt;
&lt;p&gt;slf4j+log4j的方式；&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&amp;lt;dependency&amp;gt;
  &amp;lt;groupId&amp;gt;org.springframework.boot&amp;lt;/groupId&amp;gt;
  &amp;lt;artifactId&amp;gt;spring-boot-starter-web&amp;lt;/artifactId&amp;gt;
  &amp;lt;exclusions&amp;gt;
    &amp;lt;exclusion&amp;gt;
      &amp;lt;artifactId&amp;gt;logback-classic&amp;lt;/artifactId&amp;gt;
      &amp;lt;groupId&amp;gt;ch.qos.logback&amp;lt;/groupId&amp;gt;
    &amp;lt;/exclusion&amp;gt;
    &amp;lt;exclusion&amp;gt;
      &amp;lt;artifactId&amp;gt;log4j-over-slf4j&amp;lt;/artifactId&amp;gt;
      &amp;lt;groupId&amp;gt;org.slf4j&amp;lt;/groupId&amp;gt;
    &amp;lt;/exclusion&amp;gt;
  &amp;lt;/exclusions&amp;gt;
&amp;lt;/dependency&amp;gt;

&amp;lt;dependency&amp;gt;
  &amp;lt;groupId&amp;gt;org.slf4j&amp;lt;/groupId&amp;gt;
  &amp;lt;artifactId&amp;gt;slf4j-log4j12&amp;lt;/artifactId&amp;gt;
&amp;lt;/dependency&amp;gt;

&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;切换为log4j2&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;   &amp;lt;dependency&amp;gt;
            &amp;lt;groupId&amp;gt;org.springframework.boot&amp;lt;/groupId&amp;gt;
            &amp;lt;artifactId&amp;gt;spring-boot-starter-web&amp;lt;/artifactId&amp;gt;
            &amp;lt;exclusions&amp;gt;
                &amp;lt;exclusion&amp;gt;
                    &amp;lt;artifactId&amp;gt;spring-boot-starter-logging&amp;lt;/artifactId&amp;gt;
                    &amp;lt;groupId&amp;gt;org.springframework.boot&amp;lt;/groupId&amp;gt;
                &amp;lt;/exclusion&amp;gt;
            &amp;lt;/exclusions&amp;gt;
        &amp;lt;/dependency&amp;gt;

&amp;lt;dependency&amp;gt;
  &amp;lt;groupId&amp;gt;org.springframework.boot&amp;lt;/groupId&amp;gt;
  &amp;lt;artifactId&amp;gt;spring-boot-starter-log4j2&amp;lt;/artifactId&amp;gt;
&amp;lt;/dependency&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;hr /&gt;
&lt;h3&gt;6、日志区别&lt;/h3&gt;
&lt;p&gt;&lt;code&gt;/var/log/${name}.log 2&amp;gt;&amp;amp;1 &amp;amp;&lt;/code&gt; 是一个shell命令，它将程序的输出重定向到位于&lt;code&gt;/var/log&lt;/code&gt;目录下，文件名由&lt;code&gt;${name}&lt;/code&gt;变量的值决定的日志文件中。&lt;code&gt;2&amp;gt;&amp;amp;1&lt;/code&gt;语法将标准错误流重定向到标准输出流，这意味着标准输出和标准错误都将被写入日志文件中。&lt;/p&gt;
&lt;p&gt;另一方面，&lt;code&gt;logback-spring.xml&lt;/code&gt;是在Spring应用程序中使用的Logback日志框架的配置文件。它提供了一种配置日志的各个方面的方法，例如日志格式、日志文件位置、日志级别等等。&lt;/p&gt;
&lt;p&gt;虽然&lt;code&gt;/var/log/${name}.log 2&amp;gt;&amp;amp;1 &amp;amp;&lt;/code&gt;和&lt;code&gt;logback-spring.xml&lt;/code&gt;都与日志记录相关，但它们具有不同的用途。前者是一个shell命令，用于将程序输出重定向到日志文件，而后者是一个配置文件，用于控制日志的生成和存储方式。&lt;/p&gt;
&lt;h3&gt;7、springboot 下mybatis-plus 如何打印sql日志和参数到日志文件&lt;/h3&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;a href=&quot;https://blog.csdn.net/qq_36407469/article/details/119003229&quot;&gt;(12条消息) springboot 下mybatis-plus 如何打印sql日志和参数到日志文件_hikarpool_Joker2018的博客-CSDN博客&lt;/a&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;总结：总共需要以下三个配置&lt;/p&gt;
&lt;p&gt;1:设置mybatisplus包下的日志级别为DEBUG；
2:设置项目mapper目录的日志级别为DEBUG；
3:设置mybatis-plus的日志输出方式为slf4j。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;logging.level.com.baomidou.mybatisplus=DEBUG
#项目mapper目录
logging.level.com.dragonsoft.demojar.mapper=DEBUG
mybatis-plus.configuration.log-impl=org.apache.ibatis.logging.slf4j.Slf4jImpl
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;8、记录输出全日志到文件夹案例&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;&amp;lt;?xml version=&quot;1.0&quot; encoding=&quot;UTF-8&quot;?&amp;gt;
&amp;lt;configuration debug=&quot;false&quot;&amp;gt;
    &amp;lt;!--日志存储路径--&amp;gt;
    &amp;lt;property name=&quot;log&quot; value=&quot;/app/logs&quot; /&amp;gt;
    &amp;lt;!-- 控制台输出 --&amp;gt;
    &amp;lt;appender name=&quot;console&quot; class=&quot;ch.qos.logback.core.ConsoleAppender&quot;&amp;gt;
        &amp;lt;encoder class=&quot;ch.qos.logback.classic.encoder.PatternLayoutEncoder&quot;&amp;gt;
            &amp;lt;!--输出格式化--&amp;gt;
            &amp;lt;pattern&amp;gt;[%X{TRACE_ID}]  %d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger{50} - %msg%n&amp;lt;/pattern&amp;gt;
        &amp;lt;/encoder&amp;gt;
    &amp;lt;/appender&amp;gt;
    &amp;lt;!-- 按天生成日志文件 --&amp;gt;
    &amp;lt;appender name=&quot;file&quot; class=&quot;ch.qos.logback.core.rolling.RollingFileAppender&quot;&amp;gt;
        &amp;lt;rollingPolicy class=&quot;ch.qos.logback.core.rolling.TimeBasedRollingPolicy&quot;&amp;gt;
            &amp;lt;!--日志文件名--&amp;gt;
            &amp;lt;FileNamePattern&amp;gt;${log}/%d{yyyy-MM-dd}.log&amp;lt;/FileNamePattern&amp;gt;
            &amp;lt;!--保留天数--&amp;gt;
            &amp;lt;MaxHistory&amp;gt;30&amp;lt;/MaxHistory&amp;gt;
        &amp;lt;/rollingPolicy&amp;gt;
        &amp;lt;encoder class=&quot;ch.qos.logback.classic.encoder.PatternLayoutEncoder&quot;&amp;gt;
            &amp;lt;pattern&amp;gt;[%X{TRACE_ID}]  %d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger{50} - %msg%n&amp;lt;/pattern&amp;gt;
        &amp;lt;/encoder&amp;gt;
        &amp;lt;!--日志文件最大的大小--&amp;gt;
        &amp;lt;triggeringPolicy class=&quot;ch.qos.logback.core.rolling.SizeBasedTriggeringPolicy&quot;&amp;gt;
            &amp;lt;MaxFileSize&amp;gt;10MB&amp;lt;/MaxFileSize&amp;gt;
        &amp;lt;/triggeringPolicy&amp;gt;
    &amp;lt;/appender&amp;gt;

    &amp;lt;!-- 日志输出级别 --&amp;gt;
    &amp;lt;root level=&quot;INFO&quot;&amp;gt;
        &amp;lt;appender-ref ref=&quot;console&quot; /&amp;gt;
        &amp;lt;appender-ref ref=&quot;file&quot; /&amp;gt;
    &amp;lt;/root&amp;gt;
&amp;lt;/configuration&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;四、Web开发&lt;/h2&gt;
&lt;h3&gt;1、简介&lt;/h3&gt;
&lt;p&gt;使用SpringBoot；&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;1）、创建SpringBoot应用，选中我们需要的模块；&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;2）、SpringBoot已经默认将这些场景配置好了，只需要在配置文件中指定少量配置就可以运行起来&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;3）、自己编写业务代码；&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;自动配置原理？&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;这个场景SpringBoot帮我们配置了什么？能不能修改？能修改哪些配置？能不能扩展？xxx&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;xxxxAutoConfiguration：帮我们给容器中自动配置组件；
xxxxProperties:配置类来封装配置文件的内容；

&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;2、SpringBoot对静态资源的映射规则；&lt;/h3&gt;
&lt;h4&gt;2.1、静态资源目录&lt;/h4&gt;
&lt;blockquote&gt;
&lt;p&gt;只要静态资源放在类路径下： called &lt;code&gt;/static&lt;/code&gt; (or &lt;code&gt;/public&lt;/code&gt; or &lt;code&gt;/resources&lt;/code&gt; or &lt;code&gt;/META-INF/resources&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;访问 ： 当前项目根路径/ + 静态资源名&lt;/p&gt;
&lt;p&gt;原理： 静态映射/**。&lt;/p&gt;
&lt;p&gt;请求进来，先去找Controller看能不能处理。不能处理的所有请求又都交给静态资源处理器。静态资源也找不到则响应404页面&lt;/p&gt;
&lt;p&gt;改变默认的静态资源路径&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;spring:
  mvc:
    改变前缀
    static-path-pattern: /res/**

  resources:
    改变静态资源路径
    static-locations: [classpath:/haha/]
&lt;/code&gt;&lt;/pre&gt;
&lt;/blockquote&gt;
&lt;h4&gt;2.2、静态资源访问前缀&lt;/h4&gt;
&lt;p&gt;默认无前缀&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;spring:
  mvc:
    static-path-pattern: /res/**
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;当前项目 + static-path-pattern + 静态资源名 = 静态资源文件夹下找&lt;/p&gt;
&lt;p&gt;2.3、欢迎页支持&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;静态资源路径下  index.html&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;ul&gt;
&lt;li&gt;可以配置静态资源路径&lt;/li&gt;
&lt;li&gt;但是不可以配置静态资源的访问前缀。否则导致 index.html不能被默认访问&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;spring:
##  mvc:
##    static-path-pattern: /res/**   这个会导致welcome page功能失效

  resources:
    static-locations: [classpath:/haha/]
&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;controller能处理/index&lt;/li&gt;
&lt;/ul&gt;
&lt;h4&gt;2.4、自定义 &lt;code&gt;Favicon&lt;/code&gt;&lt;/h4&gt;
&lt;p&gt;favicon.ico 放在静态资源目录下即可&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;spring:
##  mvc:
##    static-path-pattern: /res/**   这个会导致 Favicon 功能失效
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;@ConfigurationProperties(prefix = &quot;spring.resources&quot;, ignoreUnknownFields = false)
public class ResourceProperties implements ResourceLoaderAware {
  //可以设置和静态资源有关的参数，缓存时间等
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;	WebMvcAuotConfiguration：
		@Override
		public void addResourceHandlers(ResourceHandlerRegistry registry) {
			if (!this.resourceProperties.isAddMappings()) {
				logger.debug(&quot;Default resource handling disabled&quot;);
				return;
			}
			Integer cachePeriod = this.resourceProperties.getCachePeriod();
			if (!registry.hasMappingForPattern(&quot;/webjars/**&quot;)) {
				customizeResourceHandlerRegistration(
						registry.addResourceHandler(&quot;/webjars/**&quot;)
								.addResourceLocations(
										&quot;classpath:/META-INF/resources/webjars/&quot;)
						.setCachePeriod(cachePeriod));
			}
			String staticPathPattern = this.mvcProperties.getStaticPathPattern();
          	//静态资源文件夹映射
			if (!registry.hasMappingForPattern(staticPathPattern)) {
				customizeResourceHandlerRegistration(
						registry.addResourceHandler(staticPathPattern)
								.addResourceLocations(
										this.resourceProperties.getStaticLocations())
						.setCachePeriod(cachePeriod));
			}
		}

        //配置欢迎页映射
		@Bean
		public WelcomePageHandlerMapping welcomePageHandlerMapping(
				ResourceProperties resourceProperties) {
			return new WelcomePageHandlerMapping(resourceProperties.getWelcomePage(),
					this.mvcProperties.getStaticPathPattern());
		}

       //配置喜欢的图标
		@Configuration
		@ConditionalOnProperty(value = &quot;spring.mvc.favicon.enabled&quot;, matchIfMissing = true)
		public static class FaviconConfiguration {

			private final ResourceProperties resourceProperties;

			public FaviconConfiguration(ResourceProperties resourceProperties) {
				this.resourceProperties = resourceProperties;
			}

			@Bean
			public SimpleUrlHandlerMapping faviconHandlerMapping() {
				SimpleUrlHandlerMapping mapping = new SimpleUrlHandlerMapping();
				mapping.setOrder(Ordered.HIGHEST_PRECEDENCE + 1);
              	//所有  **/favicon.ico 
				mapping.setUrlMap(Collections.singletonMap(&quot;**/favicon.ico&quot;,
						faviconRequestHandler()));
				return mapping;
			}

			@Bean
			public ResourceHttpRequestHandler faviconRequestHandler() {
				ResourceHttpRequestHandler requestHandler = new ResourceHttpRequestHandler();
				requestHandler
						.setLocations(this.resourceProperties.getFaviconLocations());
				return requestHandler;
			}

		}

&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;3、模板引擎&lt;/h3&gt;
&lt;p&gt;JSP、Velocity、Freemarker、Thymeleaf&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://tangtangit.com/_astro/template-engine.ZzQLkzRd_Wrzea.webp&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;SpringBoot推荐的Thymeleaf；&lt;/p&gt;
&lt;p&gt;语法更简单，功能更强大；&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;spring.thymeleaf.encoding=UTF-8
#热部署静态文件
spring.thymeleaf.cache=false
#使用HTML5标准
spring.thymeleaf.mode=HTML5
&lt;/code&gt;&lt;/pre&gt;
&lt;h4&gt;1、引入thymeleaf；&lt;/h4&gt;
&lt;pre&gt;&lt;code&gt;		&amp;lt;dependency&amp;gt;
			&amp;lt;groupId&amp;gt;org.springframework.boot&amp;lt;/groupId&amp;gt;
			&amp;lt;artifactId&amp;gt;spring-boot-starter-thymeleaf&amp;lt;/artifactId&amp;gt;
		&amp;lt;/dependency&amp;gt;
切换thymeleaf版本
&amp;lt;properties&amp;gt;
		&amp;lt;thymeleaf.version&amp;gt;3.0.9.RELEASE&amp;lt;/thymeleaf.version&amp;gt;
		&amp;lt;!-- 布局功能的支持程序  thymeleaf3主程序  layout2以上版本 --&amp;gt;
		&amp;lt;!-- thymeleaf2   layout1--&amp;gt;
		&amp;lt;thymeleaf-layout-dialect.version&amp;gt;2.2.2&amp;lt;/thymeleaf-layout-dialect.version&amp;gt;
  &amp;lt;/properties&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;h4&gt;2、Thymeleaf使用&lt;/h4&gt;
&lt;pre&gt;&lt;code&gt;@ConfigurationProperties(prefix = &quot;spring.thymeleaf&quot;)
public class ThymeleafProperties {

	private static final Charset DEFAULT_ENCODING = Charset.forName(&quot;UTF-8&quot;);

	private static final MimeType DEFAULT_CONTENT_TYPE = MimeType.valueOf(&quot;text/html&quot;);

	public static final String DEFAULT_PREFIX = &quot;classpath:/templates/&quot;;

	public static final String DEFAULT_SUFFIX = &quot;.html&quot;;
  	//
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;只要我们把HTML页面放在classpath:/templates/，thymeleaf就能自动渲染；&lt;/p&gt;
&lt;p&gt;使用：&lt;/p&gt;
&lt;p&gt;1、导入thymeleaf的名称空间&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&amp;lt;html lang=&quot;en&quot; xmlns:th=&quot;http://www.thymeleaf.org&quot;&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;2、使用thymeleaf语法；&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&amp;lt;!DOCTYPE html&amp;gt;
&amp;lt;html lang=&quot;en&quot; xmlns:th=&quot;http://www.thymeleaf.org&quot;&amp;gt;
&amp;lt;head&amp;gt;
    &amp;lt;meta charset=&quot;UTF-8&quot;&amp;gt;
    &amp;lt;title&amp;gt;Title&amp;lt;/title&amp;gt;
&amp;lt;/head&amp;gt;
&amp;lt;body&amp;gt;
    &amp;lt;h1&amp;gt;成功！&amp;lt;/h1&amp;gt;
    &amp;lt;!--th:text 将div里面的文本内容设置为 --&amp;gt;
    &amp;lt;div th:text=&quot;${hello}&quot;&amp;gt;这是显示欢迎信息&amp;lt;/div&amp;gt;
&amp;lt;/body&amp;gt;
&amp;lt;/html&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;h4&gt;3、语法规则&lt;/h4&gt;
&lt;p&gt;1）、th:text；改变当前元素里面的文本内容；&lt;/p&gt;
&lt;p&gt;​	th：任意html属性；来替换原生属性的值&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://tangtangit.com/_astro/2018-02-04_123955.Ba28kHjo_Z25reU7.webp&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;2）、表达式？&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;Simple expressions:（表达式语法）
    Variable Expressions: ${...}：获取变量值；OGNL；
    		1）、获取对象的属性、调用方法
    		2）、使用内置的基本对象：
    			#ctx : the context object.
    			#vars: the context variables.
                #locale : the context locale.
                #request : (only in Web Contexts) the HttpServletRequest object.
                #response : (only in Web Contexts) the HttpServletResponse object.
                #session : (only in Web Contexts) the HttpSession object.
                #servletContext : (only in Web Contexts) the ServletContext object.
                
                ${session.foo}
            3）、内置的一些工具对象：
#execInfo : information about the template being processed.
#messages : methods for obtaining externalized messages inside variables expressions, in the same way as they would be obtained using #{…} syntax.
#uris : methods for escaping parts of URLs/URIs
#conversions : methods for executing the configured conversion service (if any).
#dates : methods for java.util.Date objects: formatting, component extraction, etc.
#calendars : analogous to #dates , but for java.util.Calendar objects.
#numbers : methods for formatting numeric objects.
#strings : methods for String objects: contains, startsWith, prepending/appending, etc.
#objects : methods for objects in general.
#bools : methods for boolean evaluation.
#arrays : methods for arrays.
#lists : methods for lists.
#sets : methods for sets.
#maps : methods for maps.
#aggregates : methods for creating aggregates on arrays or collections.
#ids : methods for dealing with id attributes that might be repeated (for example, as a result of an iteration).

    Selection Variable Expressions: *{...}：选择表达式：和${}在功能上是一样；
    	补充：配合 th:object=&quot;${session.user}：
   &amp;lt;div th:object=&quot;${session.user}&quot;&amp;gt;
    &amp;lt;p&amp;gt;Name: &amp;lt;span th:text=&quot;*{firstName}&quot;&amp;gt;Sebastian&amp;lt;/span&amp;gt;.&amp;lt;/p&amp;gt;
    &amp;lt;p&amp;gt;Surname: &amp;lt;span th:text=&quot;*{lastName}&quot;&amp;gt;Pepper&amp;lt;/span&amp;gt;.&amp;lt;/p&amp;gt;
    &amp;lt;p&amp;gt;Nationality: &amp;lt;span th:text=&quot;*{nationality}&quot;&amp;gt;Saturn&amp;lt;/span&amp;gt;.&amp;lt;/p&amp;gt;
    &amp;lt;/div&amp;gt;
    
    Message Expressions: #{...}：获取国际化内容
    Link URL Expressions: @{...}：定义URL；
    		@{/order/process(execId=${execId},execType=&apos;FAST&apos;)}
    Fragment Expressions: ~{...}：片段引用表达式
    		&amp;lt;div th:insert=&quot;~{commons :: main}&quot;&amp;gt;...&amp;lt;/div&amp;gt;
    		
Literals（字面量）
      Text literals: &apos;one text&apos; , &apos;Another one!&apos; ,…
      Number literals: 0 , 34 , 3.0 , 12.3 ,…
      Boolean literals: true , false
      Null literal: null
      Literal tokens: one , sometext , main ,…
Text operations:（文本操作）
    String concatenation: +
    Literal substitutions: |The name is ${name}|
Arithmetic operations:（数学运算）
    Binary operators: + , - , * , / , %
    Minus sign (unary operator): -
Boolean operations:（布尔运算）
    Binary operators: and , or
    Boolean negation (unary operator): ! , not
Comparisons and equality:（比较运算）
    Comparators: &amp;gt; , &amp;lt; , &amp;gt;= , &amp;lt;= ( gt , lt , ge , le )
    Equality operators: == , != ( eq , ne )
Conditional operators:条件运算（三元运算符）
    If-then: (if) ? (then)
    If-then-else: (if) ? (then) : (else)
    Default: (value) ?: (defaultvalue)
Special tokens:
    No-Operation: _ 
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;4、SpringMVC自动配置&lt;/h3&gt;
&lt;p&gt;https://docs.spring.io/spring-boot/docs/1.5.10.RELEASE/reference/htmlsingle/#boot-features-developing-web-applications&lt;/p&gt;
&lt;h4&gt;1. Spring MVC auto-configuration&lt;/h4&gt;
&lt;p&gt;Spring Boot 自动配置好了SpringMVC&lt;/p&gt;
&lt;p&gt;以下是SpringBoot对SpringMVC的默认配置:&lt;strong&gt;&lt;strong&gt;（WebMvcAutoConfiguration）&lt;/strong&gt;&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;Inclusion of &lt;code&gt;ContentNegotiatingViewResolver&lt;/code&gt; and &lt;code&gt;BeanNameViewResolver&lt;/code&gt; beans.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;自动配置了ViewResolver（视图解析器：根据方法的返回值得到视图对象（View），视图对象决定如何渲染（转发？重定向？））&lt;/li&gt;
&lt;li&gt;ContentNegotiatingViewResolver：组合所有的视图解析器的；&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;如何定制：我们可以自己给容器中添加一个视图解析器；自动的将其组合进来；&lt;/strong&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Support for serving static resources, including support for WebJars (see below).静态资源文件夹路径,webjars&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Static &lt;code&gt;index.html&lt;/code&gt; support. 静态首页访问&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Custom &lt;code&gt;Favicon&lt;/code&gt; support (see below).  favicon.ico&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;自动注册了 of &lt;code&gt;Converter&lt;/code&gt;, &lt;code&gt;GenericConverter&lt;/code&gt;, &lt;code&gt;Formatter&lt;/code&gt; beans.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Converter：转换器；  public String hello(User user)：类型转换使用Converter&lt;/li&gt;
&lt;li&gt;&lt;code&gt;Formatter&lt;/code&gt;  格式化器；  2017.12.17===Date；&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;		@Bean
		@ConditionalOnProperty(prefix = &quot;spring.mvc&quot;, name = &quot;date-format&quot;)//在文件中配置日期格式化的规则
		public Formatter&amp;lt;Date&amp;gt; dateFormatter() {
			return new DateFormatter(this.mvcProperties.getDateFormat());//日期格式化组件
		}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;​	&lt;strong&gt;自己添加的格式化器转换器，我们只需要放在容器中即可&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;Support for &lt;code&gt;HttpMessageConverters&lt;/code&gt; (see below).&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;HttpMessageConverter：SpringMVC用来转换Http请求和响应的；User---Json；&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;HttpMessageConverters&lt;/code&gt; 是从容器中确定；获取所有的HttpMessageConverter；&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;自己给容器中添加HttpMessageConverter，只需要将自己的组件注册容器中（@Bean,@Component）&lt;/strong&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Automatic registration of &lt;code&gt;MessageCodesResolver&lt;/code&gt; (see below).定义错误代码生成规则&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Automatic use of a &lt;code&gt;ConfigurableWebBindingInitializer&lt;/code&gt; bean (see below).&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;我们可以配置一个ConfigurableWebBindingInitializer来替换默认的；（添加到容器）&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;初始化WebDataBinder；
请求数据**=**JavaBean；
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;org.springframework.boot.autoconfigure.web：web的所有自动场景；&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;If you want to keep Spring Boot MVC features, and you just want to add additional &lt;a href=&quot;https://docs.spring.io/spring/docs/4.3.14.RELEASE/spring-framework-reference/htmlsingle#mvc&quot;&gt;MVC configuration&lt;/a&gt; (interceptors, formatters, view controllers etc.) you can add your own &lt;code&gt;@Configuration&lt;/code&gt; class of type &lt;code&gt;WebMvcConfigurerAdapter&lt;/code&gt;, but &lt;strong&gt;without&lt;/strong&gt; &lt;code&gt;@EnableWebMvc&lt;/code&gt;. If you wish to provide custom instances of &lt;code&gt;RequestMappingHandlerMapping&lt;/code&gt;, &lt;code&gt;RequestMappingHandlerAdapter&lt;/code&gt; or &lt;code&gt;ExceptionHandlerExceptionResolver&lt;/code&gt; you can declare a &lt;code&gt;WebMvcRegistrationsAdapter&lt;/code&gt; instance providing such components.&lt;/p&gt;
&lt;p&gt;If you want to take complete control of Spring MVC, you can add your own &lt;code&gt;@Configuration&lt;/code&gt; annotated with &lt;code&gt;@EnableWebMvc&lt;/code&gt;.&lt;/p&gt;
&lt;h4&gt;2、扩展SpringMVC&lt;/h4&gt;
&lt;pre&gt;&lt;code&gt;    &amp;lt;mvc:view-controller path=&quot;/hello&quot; view-name=&quot;success&quot;/&amp;gt;
    &amp;lt;mvc:interceptors&amp;gt;
        &amp;lt;mvc:interceptor&amp;gt;
            &amp;lt;mvc:mapping path=&quot;/hello&quot;/&amp;gt;
            &amp;lt;bean&amp;gt;&amp;lt;/bean&amp;gt;
        &amp;lt;/mvc:interceptor&amp;gt;
    &amp;lt;/mvc:interceptors&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;&lt;strong&gt;编写一个配置类（@Configuration），是WebMvcConfigurerAdapter类型；不能标注@EnableWebMvc&lt;/strong&gt;&lt;/strong&gt;;&lt;/p&gt;
&lt;p&gt;既保留了所有的自动配置，也能用我们扩展的配置；&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;//使用WebMvcConfigurerAdapter可以来扩展SpringMVC的功能
@Configuration
public class MyMvcConfig extends WebMvcConfigurerAdapter {

    @Override
    public void addViewControllers(ViewControllerRegistry registry) {
       // super.addViewControllers(registry);
        //浏览器发送 /atguigu 请求来到 success
        registry.addViewController(&quot;/atguigu&quot;).setViewName(&quot;success&quot;);
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;原理：&lt;/p&gt;
&lt;p&gt;​	1）、WebMvcAutoConfiguration是SpringMVC的自动配置类&lt;/p&gt;
&lt;p&gt;​	2）、在做其他自动配置时会导入；@Import(&lt;strong&gt;EnableWebMvcConfiguration&lt;/strong&gt;.class)&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;    @Configuration
	public static class EnableWebMvcConfiguration extends DelegatingWebMvcConfiguration {
      private final WebMvcConfigurerComposite configurers = new WebMvcConfigurerComposite();

	 //从容器中获取所有的WebMvcConfigurer
      @Autowired(required = false)
      public void setConfigurers(List&amp;lt;WebMvcConfigurer&amp;gt; configurers) {
          if (!CollectionUtils.isEmpty(configurers)) {
              this.configurers.addWebMvcConfigurers(configurers);
            	//一个参考实现；将所有的WebMvcConfigurer相关配置都来一起调用；  
            	@Override
             // public void addViewControllers(ViewControllerRegistry registry) {
              //    for (WebMvcConfigurer delegate : this.delegates) {
               //       delegate.addViewControllers(registry);
               //   }
              }
          }
	}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;​	3）、容器中所有的WebMvcConfigurer都会一起起作用；&lt;/p&gt;
&lt;p&gt;​	4）、我们的配置类也会被调用；&lt;/p&gt;
&lt;p&gt;​	效果：SpringMVC的自动配置和我们的扩展配置都会起作用；&lt;/p&gt;
&lt;p&gt;自定义mvc的convert,&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;convet ----set方法---反序列化jsondescSerilizer&lt;/p&gt;
&lt;/blockquote&gt;
&lt;pre&gt;&lt;code&gt;/**
 * &amp;lt;p&amp;gt;DESC: &amp;lt;/p&amp;gt;
 * &amp;lt;p&amp;gt;DATE: 2019/6/27&amp;lt;/p&amp;gt;
 * &amp;lt;p&amp;gt;VERSION:1.0.0&amp;lt;/p&amp;gt;
 * &amp;lt;p&amp;gt;@AUTHOR: ZhengYong&amp;lt;/p&amp;gt;
 */
@Configuration
public class MvcConfig implements WebMvcConfigurer {
    
    @Bean
    public Converter&amp;lt;String, Date&amp;gt; stringToDate(){
        return new Converter&amp;lt;String, Date&amp;gt;() {
            @Override
            public Date convert(String s) {
                if(StringUtils.isNotBlank(s)){
                    SimpleDateFormat simpleDateFormat = new SimpleDateFormat(&quot;yyyy-MM-dd HH:mm:ss&quot;);
                    try {
                        Date parse = simpleDateFormat.parse(s);
                        return parse;
                    } catch (ParseException e) {
                        e.printStackTrace();
                    }
                }
                return null;
            }
        };
    }

}
&lt;/code&gt;&lt;/pre&gt;
&lt;h4&gt;2.1、&lt;strong&gt;处理跨域问题：&lt;/strong&gt;&lt;/h4&gt;
&lt;pre&gt;&lt;code&gt;   @Override
    public void addCorsMappings(CorsRegistry registry) {
        registry.addMapping(&quot;/**&quot;)//设置允许跨域的路径
                .allowedOrigins(&quot;*&quot;)//设置允许跨域请求的域名
                .allowCredentials(true)//是否允许证书 不再默认开启
                .allowedMethods(&quot;GET&quot;, &quot;POST&quot;, &quot;PUT&quot;, &quot;DELETE&quot;)//设置允许的方法
                .maxAge(3600);//跨域允许时间
    }
&lt;/code&gt;&lt;/pre&gt;
&lt;h4&gt;2.2、PathVariable和requestParam&lt;/h4&gt;
&lt;p&gt;@PathVariable：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;这个作用
1、是用来绑定URI模版上的变量值；
2、是用来获得请求的url中的动态参数的；
3、是用于请求url模版映射到功能处理方法的参数上 如：//配置url和方法的一个关系@RequestMapping(&quot;item/{itemId}&quot;)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;@RequestParam :&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;获取请求参数的值，如：localhost:8080/novelsite/chapter/edit?bookId=12 这个就是获取
bookid的值
还可以写定义默认值
@RequestParam(value=&quot;id&quot;,required = false,defaultValue = &quot;1&quot;) Integer id
&lt;/code&gt;&lt;/pre&gt;
&lt;h4&gt;3、全面接管SpringMVC；&lt;/h4&gt;
&lt;p&gt;SpringBoot对SpringMVC的自动配置不需要了，所有都是我们自己配置；所有的SpringMVC的自动配置都失效了&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;我们需要在配置类中添加@EnableWebMvc即可；&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;//使用WebMvcConfigurerAdapter可以来扩展SpringMVC的功能
@EnableWebMvc
@Configuration
public class MyMvcConfig extends WebMvcConfigurerAdapter {

    @Override
    public void addViewControllers(ViewControllerRegistry registry) {
       // super.addViewControllers(registry);
        //浏览器发送 /atguigu 请求来到 success
        registry.addViewController(&quot;/atguigu&quot;).setViewName(&quot;success&quot;);
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;原理：&lt;/p&gt;
&lt;p&gt;为什么@EnableWebMvc自动配置就失效了；&lt;/p&gt;
&lt;p&gt;1）@EnableWebMvc的核心&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;@Import(DelegatingWebMvcConfiguration.class)
public @interface EnableWebMvc {
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;2）、&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;@Configuration
public class DelegatingWebMvcConfiguration extends WebMvcConfigurationSupport {
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;3）、&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;@Configuration
@ConditionalOnWebApplication
@ConditionalOnClass({ Servlet.class, DispatcherServlet.class,
		WebMvcConfigurerAdapter.class })
//容器中没有这个组件的时候，这个自动配置类才生效
@ConditionalOnMissingBean(WebMvcConfigurationSupport.class)
@AutoConfigureOrder(Ordered.HIGHEST_PRECEDENCE + 10)
@AutoConfigureAfter({ DispatcherServletAutoConfiguration.class,
		ValidationAutoConfiguration.class })
public class WebMvcAutoConfiguration {
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;4）、@EnableWebMvc将WebMvcConfigurationSupport组件导入进来；&lt;/p&gt;
&lt;p&gt;5）、导入的WebMvcConfigurationSupport只是SpringMVC最基本的功能；&lt;/p&gt;
&lt;h3&gt;5、如何修改SpringBoot的默认配置&lt;/h3&gt;
&lt;p&gt;模式：&lt;/p&gt;
&lt;p&gt;​	1）、SpringBoot在自动配置很多组件的时候，先看容器中有没有用户自己配置的（@Bean、@Component）如果有就用用户配置的，如果没有，才自动配置；如果有些组件可以有多个（ViewResolver）将用户配置的和自己默认的组合起来；&lt;/p&gt;
&lt;p&gt;​	2）、在SpringBoot中会有非常多的xxxConfigurer帮助我们进行扩展配置&lt;/p&gt;
&lt;p&gt;​	3）、在SpringBoot中会有很多的xxxCustomizer帮助我们进行定制配置&lt;/p&gt;
&lt;h3&gt;6、RestfulCRUD&lt;/h3&gt;
&lt;h4&gt;1）、默认访问首页&lt;/h4&gt;
&lt;pre&gt;&lt;code&gt;
//使用WebMvcConfigurerAdapter可以来扩展SpringMVC的功能
//@EnableWebMvc   不要接管SpringMVC
@Configuration
public class MyMvcConfig extends WebMvcConfigurerAdapter {

    @Override
    public void addViewControllers(ViewControllerRegistry registry) {
       // super.addViewControllers(registry);
        //浏览器发送 /atguigu 请求来到 success
        registry.addViewController(&quot;/atguigu&quot;).setViewName(&quot;success&quot;);
    }

    //所有的WebMvcConfigurerAdapter组件都会一起起作用
    @Bean //将组件注册在容器
    public WebMvcConfigurerAdapter webMvcConfigurerAdapter(){
        WebMvcConfigurerAdapter adapter = new WebMvcConfigurerAdapter() {
            @Override
            public void addViewControllers(ViewControllerRegistry registry) {
                registry.addViewController(&quot;/&quot;).setViewName(&quot;login&quot;);
                registry.addViewController(&quot;/index.html&quot;).setViewName(&quot;login&quot;);
            }
        };
        return adapter;
    }
}

&lt;/code&gt;&lt;/pre&gt;
&lt;h4&gt;2）、国际化&lt;/h4&gt;
&lt;p&gt;&lt;strong&gt;1）、编写国际化配置文件；&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;2）、使用ResourceBundleMessageSource管理国际化资源文件&lt;/p&gt;
&lt;p&gt;3）、在页面使用fmt:message取出国际化内容&lt;/p&gt;
&lt;p&gt;步骤：&lt;/p&gt;
&lt;p&gt;1）、编写国际化配置文件，抽取页面需要显示的国际化消息&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;%E6%90%9C%E7%8B%97%E6%88%AA%E5%9B%BE20180211130721.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;2）、SpringBoot自动配置好了管理国际化资源文件的组件；&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;@ConfigurationProperties(prefix = &quot;spring.messages&quot;)
public class MessageSourceAutoConfiguration {
    
    /**
	 * Comma-separated list of basenames (essentially a fully-qualified classpath
	 * location), each following the ResourceBundle convention with relaxed support for
	 * slash based locations. If it doesn&apos;t contain a package qualifier (such as
	 * &quot;org.mypackage&quot;), it will be resolved from the classpath root.
	 */
	private String basename = &quot;messages&quot;;  
    //我们的配置文件可以直接放在类路径下叫messages.properties；
    
    @Bean
	public MessageSource messageSource() {
		ResourceBundleMessageSource messageSource = new ResourceBundleMessageSource();
		if (StringUtils.hasText(this.basename)) {
            //设置国际化资源文件的基础名（去掉语言国家代码的）
			messageSource.setBasenames(StringUtils.commaDelimitedListToStringArray(
					StringUtils.trimAllWhitespace(this.basename)));
		}
		if (this.encoding != null) {
			messageSource.setDefaultEncoding(this.encoding.name());
		}
		messageSource.setFallbackToSystemLocale(this.fallbackToSystemLocale);
		messageSource.setCacheSeconds(this.cacheSeconds);
		messageSource.setAlwaysUseMessageFormat(this.alwaysUseMessageFormat);
		return messageSource;
	}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;3）、去页面获取国际化的值；&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;%E6%90%9C%E7%8B%97%E6%88%AA%E5%9B%BE20180211134506.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&amp;lt;!DOCTYPE html&amp;gt;
&amp;lt;html lang=&quot;en&quot;  xmlns:th=&quot;http://www.thymeleaf.org&quot;&amp;gt;
	&amp;lt;head&amp;gt;
		&amp;lt;meta http-equiv=&quot;Content-Type&quot; content=&quot;text/html; charset=UTF-8&quot;&amp;gt;
		&amp;lt;meta name=&quot;viewport&quot; content=&quot;width=device-width, initial-scale=1, shrink-to-fit=no&quot;&amp;gt;
		&amp;lt;meta name=&quot;description&quot; content=&quot;&quot;&amp;gt;
		&amp;lt;meta name=&quot;author&quot; content=&quot;&quot;&amp;gt;
		&amp;lt;title&amp;gt;Signin Template for Bootstrap&amp;lt;/title&amp;gt;
		&amp;lt;!-- Bootstrap core CSS --&amp;gt;
		&amp;lt;link href=&quot;asserts/css/bootstrap.min.css&quot; th:href=&quot;@{/webjars/bootstrap/4.0.0/css/bootstrap.css}&quot; rel=&quot;stylesheet&quot;&amp;gt;
		&amp;lt;!-- Custom styles for this template --&amp;gt;
		&amp;lt;link href=&quot;asserts/css/signin.css&quot; th:href=&quot;@{/asserts/css/signin.css}&quot; rel=&quot;stylesheet&quot;&amp;gt;
	&amp;lt;/head&amp;gt;

	&amp;lt;body class=&quot;text-center&quot;&amp;gt;
		&amp;lt;form class=&quot;form-signin&quot; action=&quot;dashboard.html&quot;&amp;gt;
			&amp;lt;img class=&quot;mb-4&quot; th:src=&quot;@{/asserts/img/bootstrap-solid.svg}&quot; src=&quot;asserts/img/bootstrap-solid.svg&quot; alt=&quot;&quot; width=&quot;72&quot; height=&quot;72&quot;&amp;gt;
			&amp;lt;h1 class=&quot;h3 mb-3 font-weight-normal&quot; th:text=&quot;#{login.tip}&quot;&amp;gt;Please sign in&amp;lt;/h1&amp;gt;
			&amp;lt;label class=&quot;sr-only&quot; th:text=&quot;#{login.username}&quot;&amp;gt;Username&amp;lt;/label&amp;gt;
			&amp;lt;input type=&quot;text&quot; class=&quot;form-control&quot; placeholder=&quot;Username&quot; th:placeholder=&quot;#{login.username}&quot; required=&quot;&quot; autofocus=&quot;&quot;&amp;gt;
			&amp;lt;label class=&quot;sr-only&quot; th:text=&quot;#{login.password}&quot;&amp;gt;Password&amp;lt;/label&amp;gt;
			&amp;lt;input type=&quot;password&quot; class=&quot;form-control&quot; placeholder=&quot;Password&quot; th:placeholder=&quot;#{login.password}&quot; required=&quot;&quot;&amp;gt;
			&amp;lt;div class=&quot;checkbox mb-3&quot;&amp;gt;
				&amp;lt;label&amp;gt;
          		&amp;lt;input type=&quot;checkbox&quot; value=&quot;remember-me&quot;/&amp;gt; [[#{login.remember}]]
        &amp;lt;/label&amp;gt;
			&amp;lt;/div&amp;gt;
			&amp;lt;button class=&quot;btn btn-lg btn-primary btn-block&quot; type=&quot;submit&quot; th:text=&quot;#{login.btn}&quot;&amp;gt;Sign in&amp;lt;/button&amp;gt;
			&amp;lt;p class=&quot;mt-5 mb-3 text-muted&quot;&amp;gt;© 2017-2018&amp;lt;/p&amp;gt;
			&amp;lt;a class=&quot;btn btn-sm&quot;&amp;gt;中文&amp;lt;/a&amp;gt;
			&amp;lt;a class=&quot;btn btn-sm&quot;&amp;gt;English&amp;lt;/a&amp;gt;
		&amp;lt;/form&amp;gt;

	&amp;lt;/body&amp;gt;

&amp;lt;/html&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;效果：根据浏览器语言设置的信息切换了国际化；&lt;/p&gt;
&lt;p&gt;原理：&lt;/p&gt;
&lt;p&gt;​	国际化Locale（区域信息对象）；LocaleResolver（获取区域信息对象）；&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;		@Bean
		@ConditionalOnMissingBean
		@ConditionalOnProperty(prefix = &quot;spring.mvc&quot;, name = &quot;locale&quot;)
		public LocaleResolver localeResolver() {
			if (this.mvcProperties
					.getLocaleResolver() == WebMvcProperties.LocaleResolver.FIXED) {
				return new FixedLocaleResolver(this.mvcProperties.getLocale());
			}
			AcceptHeaderLocaleResolver localeResolver = new AcceptHeaderLocaleResolver();
			localeResolver.setDefaultLocale(this.mvcProperties.getLocale());
			return localeResolver;
		}
默认的就是根据请求头带来的区域信息获取Locale进行国际化
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;4）、点击链接切换国际化&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;/**
 * 可以在连接上携带区域信息
 */
public class MyLocaleResolver implements LocaleResolver {
    
    @Override
    public Locale resolveLocale(HttpServletRequest request) {
        String l = request.getParameter(&quot;l&quot;);
        Locale locale = Locale.getDefault();
        if(!StringUtils.isEmpty(l)){
            String[] split = l.split(&quot;_&quot;);
            locale = new Locale(split[0],split[1]);
        }
        return locale;
    }

    @Override
    public void setLocale(HttpServletRequest request, HttpServletResponse response, Locale locale) {

    }
}


 @Bean
    public LocaleResolver localeResolver(){
        return new MyLocaleResolver();
    }
}


&lt;/code&gt;&lt;/pre&gt;
&lt;h4&gt;3）、登陆&lt;/h4&gt;
&lt;p&gt;开发期间模板引擎页面修改以后，要实时生效&lt;/p&gt;
&lt;p&gt;1）、禁用模板引擎的缓存&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;## 禁用缓存
spring.thymeleaf.cache=false 
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;2）、页面修改完成以后ctrl+f9：重新编译；&lt;/p&gt;
&lt;p&gt;登陆错误消息的显示&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&amp;lt;p style=&quot;color: red&quot; th:text=&quot;${msg}&quot; th:if=&quot;${not #strings.isEmpty(msg)}&quot;&amp;gt;&amp;lt;/p&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;h4&gt;4）、拦截器进行登陆检查&lt;/h4&gt;
&lt;p&gt;拦截器&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;
/**
 * 登陆检查，
 */
public class LoginHandlerInterceptor implements HandlerInterceptor {
    //目标方法执行之前
    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
        Object user = request.getSession().getAttribute(&quot;loginUser&quot;);
        if(user == null){
            //未登陆，返回登陆页面
            request.setAttribute(&quot;msg&quot;,&quot;没有权限请先登陆&quot;);
            request.getRequestDispatcher(&quot;/index.html&quot;).forward(request,response);
            return false;
        }else{
            //已登陆，放行请求
            return true;
        }

    }

    @Override
    public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) throws Exception {

    }

    @Override
    public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {

    }
}

&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;注册拦截器&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;  //所有的WebMvcConfigurerAdapter组件都会一起起作用
    @Bean //将组件注册在容器
    public WebMvcConfigurerAdapter webMvcConfigurerAdapter(){
        WebMvcConfigurerAdapter adapter = new WebMvcConfigurerAdapter() {
            @Override
            public void addViewControllers(ViewControllerRegistry registry) {
                registry.addViewController(&quot;/&quot;).setViewName(&quot;login&quot;);
                registry.addViewController(&quot;/index.html&quot;).setViewName(&quot;login&quot;);
                registry.addViewController(&quot;/main.html&quot;).setViewName(&quot;dashboard&quot;);
            }

            //注册拦截器
            @Override
            public void addInterceptors(InterceptorRegistry registry) {
                //super.addInterceptors(registry);
                //静态资源；  *.css , *.js
                //SpringBoot已经做好了静态资源映射
                registry.addInterceptor(new LoginHandlerInterceptor()).addPathPatterns(&quot;/**&quot;)
                        .excludePathPatterns(&quot;/index.html&quot;,&quot;/&quot;,&quot;/user/login&quot;);
            }
        };
        return adapter;
    }
&lt;/code&gt;&lt;/pre&gt;
&lt;h4&gt;5）、CRUD-员工列表&lt;/h4&gt;
&lt;p&gt;实验要求：&lt;/p&gt;
&lt;p&gt;1）、RestfulCRUD：CRUD满足Rest风格；&lt;/p&gt;
&lt;p&gt;URI：  /资源名称/资源标识       HTTP请求方式区分对资源CRUD操作&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;&lt;/th&gt;
&lt;th&gt;普通CRUD（uri来区分操作）&lt;/th&gt;
&lt;th&gt;RestfulCRUD&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;查询&lt;/td&gt;
&lt;td&gt;getEmp&lt;/td&gt;
&lt;td&gt;emp---GET&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;添加&lt;/td&gt;
&lt;td&gt;addEmp?xxx&lt;/td&gt;
&lt;td&gt;emp---POST&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;修改&lt;/td&gt;
&lt;td&gt;updateEmp?id=xxx&amp;amp;xxx=xx&lt;/td&gt;
&lt;td&gt;emp/{id}---PUT&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;删除&lt;/td&gt;
&lt;td&gt;deleteEmp?id=1&lt;/td&gt;
&lt;td&gt;emp/{id}---DELETE&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;2）、实验的请求架构;&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;实验功能&lt;/th&gt;
&lt;th&gt;请求URI&lt;/th&gt;
&lt;th&gt;请求方式&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;查询所有员工&lt;/td&gt;
&lt;td&gt;emps&lt;/td&gt;
&lt;td&gt;GET&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;查询某个员工(来到修改页面)&lt;/td&gt;
&lt;td&gt;emp/1&lt;/td&gt;
&lt;td&gt;GET&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;来到添加页面&lt;/td&gt;
&lt;td&gt;emp&lt;/td&gt;
&lt;td&gt;GET&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;添加员工&lt;/td&gt;
&lt;td&gt;emp&lt;/td&gt;
&lt;td&gt;POST&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;来到修改页面（查出员工进行信息回显）&lt;/td&gt;
&lt;td&gt;emp/1&lt;/td&gt;
&lt;td&gt;GET&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;修改员工&lt;/td&gt;
&lt;td&gt;emp&lt;/td&gt;
&lt;td&gt;PUT&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;删除员工&lt;/td&gt;
&lt;td&gt;emp/1&lt;/td&gt;
&lt;td&gt;DELETE&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;3）、员工列表：&lt;/p&gt;
&lt;h5&gt;thymeleaf公共页面元素抽取&lt;/h5&gt;
&lt;pre&gt;&lt;code&gt;1、抽取公共片段
&amp;lt;div th:fragment=&quot;copy&quot;&amp;gt;
&amp;amp;copy; 2011 The Good Thymes Virtual Grocery
&amp;lt;/div&amp;gt;

2、引入公共片段
&amp;lt;div th:insert=&quot;~{footer :: copy}&quot;&amp;gt;&amp;lt;/div&amp;gt;
~{templatename::selector}：模板名::选择器
~{templatename::fragmentname}:模板名::片段名

3、默认效果：
insert的公共片段在div标签中
如果使用th:insert等属性进行引入，可以不用写~{}：
行内写法可以加上：[[~{}]];[(~{})]；
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;三种引入公共片段的th属性：&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;th:insert&lt;/strong&gt;：将公共片段整个插入到声明引入的元素中&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;th:replace&lt;/strong&gt;：将声明引入的元素替换为公共片段&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;th:include&lt;/strong&gt;：将被引入的片段的内容包含进这个标签中&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&amp;lt;footer th:fragment=&quot;copy&quot;&amp;gt;
&amp;amp;copy; 2011 The Good Thymes Virtual Grocery
&amp;lt;/footer&amp;gt;

引入方式
&amp;lt;div th:insert=&quot;footer :: copy&quot;&amp;gt;&amp;lt;/div&amp;gt;
&amp;lt;div th:replace=&quot;footer :: copy&quot;&amp;gt;&amp;lt;/div&amp;gt;
&amp;lt;div th:include=&quot;footer :: copy&quot;&amp;gt;&amp;lt;/div&amp;gt;

效果
&amp;lt;div&amp;gt;
    &amp;lt;footer&amp;gt;
    &amp;amp;copy; 2011 The Good Thymes Virtual Grocery
    &amp;lt;/footer&amp;gt;
&amp;lt;/div&amp;gt;

&amp;lt;footer&amp;gt;
&amp;amp;copy; 2011 The Good Thymes Virtual Grocery
&amp;lt;/footer&amp;gt;

&amp;lt;div&amp;gt;
&amp;amp;copy; 2011 The Good Thymes Virtual Grocery
&amp;lt;/div&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;引入片段的时候传入参数：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;
&amp;lt;nav class=&quot;col-md-2 d-none d-md-block bg-light sidebar&quot; id=&quot;sidebar&quot;&amp;gt;
    &amp;lt;div class=&quot;sidebar-sticky&quot;&amp;gt;
        &amp;lt;ul class=&quot;nav flex-column&quot;&amp;gt;
            &amp;lt;li class=&quot;nav-item&quot;&amp;gt;
                &amp;lt;a class=&quot;nav-link active&quot;
                   th:class=&quot;${activeUri==&apos;main.html&apos;?&apos;nav-link active&apos;:&apos;nav-link&apos;}&quot;
                   href=&quot;#&quot; th:href=&quot;@{/main.html}&quot;&amp;gt;
                    &amp;lt;svg xmlns=&quot;http://www.w3.org/2000/svg&quot; width=&quot;24&quot; height=&quot;24&quot; viewBox=&quot;0 0 24 24&quot; fill=&quot;none&quot; stroke=&quot;currentColor&quot; stroke-width=&quot;2&quot; stroke-linecap=&quot;round&quot; stroke-linejoin=&quot;round&quot; class=&quot;feather feather-home&quot;&amp;gt;
                        &amp;lt;path d=&quot;M3 9l9-7 9 7v11a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2z&quot;&amp;gt;&amp;lt;/path&amp;gt;
                        &amp;lt;polyline points=&quot;9 22 9 12 15 12 15 22&quot;&amp;gt;&amp;lt;/polyline&amp;gt;
                    &amp;lt;/svg&amp;gt;
                    Dashboard &amp;lt;span class=&quot;sr-only&quot;&amp;gt;(current)&amp;lt;/span&amp;gt;
                &amp;lt;/a&amp;gt;
            &amp;lt;/li&amp;gt;

&amp;lt;!--引入侧边栏;传入参数--&amp;gt;
&amp;lt;div th:replace=&quot;commons/bar::#sidebar(activeUri=&apos;emps&apos;)&quot;&amp;gt;&amp;lt;/div&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;h4&gt;6）、CRUD-员工添加&lt;/h4&gt;
&lt;p&gt;添加页面&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&amp;lt;form&amp;gt;
    &amp;lt;div class=&quot;form-group&quot;&amp;gt;
        &amp;lt;label&amp;gt;LastName&amp;lt;/label&amp;gt;
        &amp;lt;input type=&quot;text&quot; class=&quot;form-control&quot; placeholder=&quot;zhangsan&quot;&amp;gt;
    &amp;lt;/div&amp;gt;
    &amp;lt;div class=&quot;form-group&quot;&amp;gt;
        &amp;lt;label&amp;gt;Email&amp;lt;/label&amp;gt;
        &amp;lt;input type=&quot;email&quot; class=&quot;form-control&quot; placeholder=&quot;zhangsan@atguigu.com&quot;&amp;gt;
    &amp;lt;/div&amp;gt;
    &amp;lt;div class=&quot;form-group&quot;&amp;gt;
        &amp;lt;label&amp;gt;Gender&amp;lt;/label&amp;gt;&amp;lt;br/&amp;gt;
        &amp;lt;div class=&quot;form-check form-check-inline&quot;&amp;gt;
            &amp;lt;input class=&quot;form-check-input&quot; type=&quot;radio&quot; name=&quot;gender&quot;  value=&quot;1&quot;&amp;gt;
            &amp;lt;label class=&quot;form-check-label&quot;&amp;gt;男&amp;lt;/label&amp;gt;
        &amp;lt;/div&amp;gt;
        &amp;lt;div class=&quot;form-check form-check-inline&quot;&amp;gt;
            &amp;lt;input class=&quot;form-check-input&quot; type=&quot;radio&quot; name=&quot;gender&quot;  value=&quot;0&quot;&amp;gt;
            &amp;lt;label class=&quot;form-check-label&quot;&amp;gt;女&amp;lt;/label&amp;gt;
        &amp;lt;/div&amp;gt;
    &amp;lt;/div&amp;gt;
    &amp;lt;div class=&quot;form-group&quot;&amp;gt;
        &amp;lt;label&amp;gt;department&amp;lt;/label&amp;gt;
        &amp;lt;select class=&quot;form-control&quot;&amp;gt;
            &amp;lt;option&amp;gt;1&amp;lt;/option&amp;gt;
            &amp;lt;option&amp;gt;2&amp;lt;/option&amp;gt;
            &amp;lt;option&amp;gt;3&amp;lt;/option&amp;gt;
            &amp;lt;option&amp;gt;4&amp;lt;/option&amp;gt;
            &amp;lt;option&amp;gt;5&amp;lt;/option&amp;gt;
        &amp;lt;/select&amp;gt;
    &amp;lt;/div&amp;gt;
    &amp;lt;div class=&quot;form-group&quot;&amp;gt;
        &amp;lt;label&amp;gt;Birth&amp;lt;/label&amp;gt;
        &amp;lt;input type=&quot;text&quot; class=&quot;form-control&quot; placeholder=&quot;zhangsan&quot;&amp;gt;
    &amp;lt;/div&amp;gt;
    &amp;lt;button type=&quot;submit&quot; class=&quot;btn btn-primary&quot;&amp;gt;添加&amp;lt;/button&amp;gt;
&amp;lt;/form&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;h4&gt;6.1、提交的数据格式不对：生日：日期；&lt;/h4&gt;
&lt;p&gt;2017-12-12；2017/12/12；2017.12.12；&lt;/p&gt;
&lt;p&gt;日期的格式化；SpringMVC将页面提交的值需要转换为指定的类型;&lt;/p&gt;
&lt;p&gt;2017-12-12---Date； 类型转换，格式化;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;spring:
	mvc:
		##时间格式序列化时，str--&amp;gt;date 指定（str）日期格式
    	date-format: yyyy-MM-dd HH:mm:ss
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;默认日期是按照/的方式；&lt;/p&gt;
&lt;h4&gt;7）、CRUD-员工修改&lt;/h4&gt;
&lt;p&gt;修改添加二合一表单&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&amp;lt;!--需要区分是员工修改还是添加；--&amp;gt;
&amp;lt;form th:action=&quot;@{/emp}&quot; method=&quot;post&quot;&amp;gt;
    &amp;lt;!--发送put请求修改员工数据--&amp;gt;
    &amp;lt;!--
1、SpringMVC中配置HiddenHttpMethodFilter;（SpringBoot自动配置好的）
2、页面创建一个post表单
3、创建一个input项，name=&quot;_method&quot;;值就是我们指定的请求方式
--&amp;gt;
    &amp;lt;input type=&quot;hidden&quot; name=&quot;_method&quot; value=&quot;put&quot; th:if=&quot;${emp!=null}&quot;/&amp;gt;
    &amp;lt;input type=&quot;hidden&quot; name=&quot;id&quot; th:if=&quot;${emp!=null}&quot; th:value=&quot;${emp.id}&quot;&amp;gt;
    &amp;lt;div class=&quot;form-group&quot;&amp;gt;
        &amp;lt;label&amp;gt;LastName&amp;lt;/label&amp;gt;
        &amp;lt;input name=&quot;lastName&quot; type=&quot;text&quot; class=&quot;form-control&quot; placeholder=&quot;zhangsan&quot; th:value=&quot;${emp!=null}?${emp.lastName}&quot;&amp;gt;
    &amp;lt;/div&amp;gt;
    &amp;lt;div class=&quot;form-group&quot;&amp;gt;
        &amp;lt;label&amp;gt;Email&amp;lt;/label&amp;gt;
        &amp;lt;input name=&quot;email&quot; type=&quot;email&quot; class=&quot;form-control&quot; placeholder=&quot;zhangsan@atguigu.com&quot; th:value=&quot;${emp!=null}?${emp.email}&quot;&amp;gt;
    &amp;lt;/div&amp;gt;
    &amp;lt;div class=&quot;form-group&quot;&amp;gt;
        &amp;lt;label&amp;gt;Gender&amp;lt;/label&amp;gt;&amp;lt;br/&amp;gt;
        &amp;lt;div class=&quot;form-check form-check-inline&quot;&amp;gt;
            &amp;lt;input class=&quot;form-check-input&quot; type=&quot;radio&quot; name=&quot;gender&quot; value=&quot;1&quot; th:checked=&quot;${emp!=null}?${emp.gender==1}&quot;&amp;gt;
            &amp;lt;label class=&quot;form-check-label&quot;&amp;gt;男&amp;lt;/label&amp;gt;
        &amp;lt;/div&amp;gt;
        &amp;lt;div class=&quot;form-check form-check-inline&quot;&amp;gt;
            &amp;lt;input class=&quot;form-check-input&quot; type=&quot;radio&quot; name=&quot;gender&quot; value=&quot;0&quot; th:checked=&quot;${emp!=null}?${emp.gender==0}&quot;&amp;gt;
            &amp;lt;label class=&quot;form-check-label&quot;&amp;gt;女&amp;lt;/label&amp;gt;
        &amp;lt;/div&amp;gt;
    &amp;lt;/div&amp;gt;
    &amp;lt;div class=&quot;form-group&quot;&amp;gt;
        &amp;lt;label&amp;gt;department&amp;lt;/label&amp;gt;
        &amp;lt;!--提交的是部门的id--&amp;gt;
        &amp;lt;select class=&quot;form-control&quot; name=&quot;department.id&quot;&amp;gt;
            &amp;lt;option th:selected=&quot;${emp!=null}?${dept.id == emp.department.id}&quot; th:value=&quot;${dept.id}&quot; th:each=&quot;dept:${depts}&quot; th:text=&quot;${dept.departmentName}&quot;&amp;gt;1&amp;lt;/option&amp;gt;
        &amp;lt;/select&amp;gt;
    &amp;lt;/div&amp;gt;
    &amp;lt;div class=&quot;form-group&quot;&amp;gt;
        &amp;lt;label&amp;gt;Birth&amp;lt;/label&amp;gt;
        &amp;lt;input name=&quot;birth&quot; type=&quot;text&quot; class=&quot;form-control&quot; placeholder=&quot;zhangsan&quot; th:value=&quot;${emp!=null}?${#dates.format(emp.birth, &apos;yyyy-MM-dd HH:mm&apos;)}&quot;&amp;gt;
    &amp;lt;/div&amp;gt;
    &amp;lt;button type=&quot;submit&quot; class=&quot;btn btn-primary&quot; th:text=&quot;${emp!=null}?&apos;修改&apos;:&apos;添加&apos;&quot;&amp;gt;添加&amp;lt;/button&amp;gt;
&amp;lt;/form&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;h4&gt;8）、CRUD-员工删除&lt;/h4&gt;
&lt;pre&gt;&lt;code&gt;&amp;lt;tr th:each=&quot;emp:${emps}&quot;&amp;gt;
    &amp;lt;td th:text=&quot;${emp.id}&quot;&amp;gt;&amp;lt;/td&amp;gt;
    &amp;lt;td&amp;gt;[[${emp.lastName}]]&amp;lt;/td&amp;gt;
    &amp;lt;td th:text=&quot;${emp.email}&quot;&amp;gt;&amp;lt;/td&amp;gt;
    &amp;lt;td th:text=&quot;${emp.gender}==0?&apos;女&apos;:&apos;男&apos;&quot;&amp;gt;&amp;lt;/td&amp;gt;
    &amp;lt;td th:text=&quot;${emp.department.departmentName}&quot;&amp;gt;&amp;lt;/td&amp;gt;
    &amp;lt;td th:text=&quot;${#dates.format(emp.birth, &apos;yyyy-MM-dd HH:mm&apos;)}&quot;&amp;gt;&amp;lt;/td&amp;gt;
    &amp;lt;td&amp;gt;
        &amp;lt;a class=&quot;btn btn-sm btn-primary&quot; th:href=&quot;@{/emp/}+${emp.id}&quot;&amp;gt;编辑&amp;lt;/a&amp;gt;
        &amp;lt;button th:attr=&quot;del_uri=@{/emp/}+${emp.id}&quot; class=&quot;btn btn-sm btn-danger deleteBtn&quot;&amp;gt;删除&amp;lt;/button&amp;gt;
    &amp;lt;/td&amp;gt;
&amp;lt;/tr&amp;gt;


&amp;lt;script&amp;gt;
    $(&quot;.deleteBtn&quot;).click(function(){
        //删除当前员工的
        $(&quot;#deleteEmpForm&quot;).attr(&quot;action&quot;,$(this).attr(&quot;del_uri&quot;)).submit();
        return false;
    });
&amp;lt;/script&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;7、错误处理机制&lt;/h3&gt;
&lt;h4&gt;1）、SpringBoot默认的错误处理机制&lt;/h4&gt;
&lt;p&gt;默认效果：&lt;/p&gt;
&lt;p&gt;​		1）、浏览器，返回一个默认的错误页面&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;%E6%90%9C%E7%8B%97%E6%88%AA%E5%9B%BE20180226173408.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;浏览器发送请求的请求头：&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;%E6%90%9C%E7%8B%97%E6%88%AA%E5%9B%BE20180226180347.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;​		2）、如果是其他客户端，默认响应一个json数据&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;%E6%90%9C%E7%8B%97%E6%88%AA%E5%9B%BE20180226173527.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;​		&lt;img src=&quot;%E6%90%9C%E7%8B%97%E6%88%AA%E5%9B%BE20180226180504.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;原理：&lt;/p&gt;
&lt;p&gt;​	可以参照ErrorMvcAutoConfiguration；错误处理的自动配置；&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;给容器中添加了以下组件
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;​	1、DefaultErrorAttributes：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;帮我们在页面共享信息；
@Override
	public Map&amp;lt;String, Object&amp;gt; getErrorAttributes(RequestAttributes requestAttributes,
			boolean includeStackTrace) {
		Map&amp;lt;String, Object&amp;gt; errorAttributes = new LinkedHashMap&amp;lt;String, Object&amp;gt;();
		errorAttributes.put(&quot;timestamp&quot;, new Date());
		addStatus(errorAttributes, requestAttributes);
		addErrorDetails(errorAttributes, requestAttributes, includeStackTrace);
		addPath(errorAttributes, requestAttributes);
		return errorAttributes;
	}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;​	2、BasicErrorController：处理默认/error请求&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;@Controller
@RequestMapping(&quot;${server.error.path:${error.path:/error}}&quot;)
public class BasicErrorController extends AbstractErrorController {
    
    @RequestMapping(produces = &quot;text/html&quot;)//产生html类型的数据；浏览器发送的请求来到这个方法处理
	public ModelAndView errorHtml(HttpServletRequest request,
			HttpServletResponse response) {
		HttpStatus status = getStatus(request);
		Map&amp;lt;String, Object&amp;gt; model = Collections.unmodifiableMap(getErrorAttributes(
				request, isIncludeStackTrace(request, MediaType.TEXT_HTML)));
		response.setStatus(status.value());
        
        //去哪个页面作为错误页面；包含页面地址和页面内容
		ModelAndView modelAndView = resolveErrorView(request, response, status, model);
		return (modelAndView == null ? new ModelAndView(&quot;error&quot;, model) : modelAndView);
	}

	@RequestMapping
	@ResponseBody    //产生json数据，其他客户端来到这个方法处理；
	public ResponseEntity&amp;lt;Map&amp;lt;String, Object&amp;gt;&amp;gt; error(HttpServletRequest request) {
		Map&amp;lt;String, Object&amp;gt; body = getErrorAttributes(request,
				isIncludeStackTrace(request, MediaType.ALL));
		HttpStatus status = getStatus(request);
		return new ResponseEntity&amp;lt;Map&amp;lt;String, Object&amp;gt;&amp;gt;(body, status);
	}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;​	3、ErrorPageCustomizer：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;	@Value(&quot;${error.path:/error}&quot;)
	private String path = &quot;/error&quot;;  系统出现错误以后来到error请求进行处理；（web.xml注册的错误页面规则）
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;​	4、DefaultErrorViewResolver：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;@Override
	public ModelAndView resolveErrorView(HttpServletRequest request, HttpStatus status,
			Map&amp;lt;String, Object&amp;gt; model) {
		ModelAndView modelAndView = resolve(String.valueOf(status), model);
		if (modelAndView == null &amp;amp;&amp;amp; SERIES_VIEWS.containsKey(status.series())) {
			modelAndView = resolve(SERIES_VIEWS.get(status.series()), model);
		}
		return modelAndView;
	}

	private ModelAndView resolve(String viewName, Map&amp;lt;String, Object&amp;gt; model) {
        //默认SpringBoot可以去找到一个页面？  error/404
		String errorViewName = &quot;error/&quot; + viewName;
        
        //模板引擎可以解析这个页面地址就用模板引擎解析
		TemplateAvailabilityProvider provider = this.templateAvailabilityProviders
				.getProvider(errorViewName, this.applicationContext);
		if (provider != null) {
            //模板引擎可用的情况下返回到errorViewName指定的视图地址
			return new ModelAndView(errorViewName, model);
		}
        //模板引擎不可用，就在静态资源文件夹下找errorViewName对应的页面   error/404.html
		return resolveResource(errorViewName, model);
	}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;​	步骤：&lt;/p&gt;
&lt;p&gt;​		一但系统出现4xx或者5xx之类的错误；ErrorPageCustomizer就会生效（定制错误的响应规则）；就会来到/error请求；就会被&lt;strong&gt;BasicErrorController&lt;/strong&gt;处理；&lt;/p&gt;
&lt;p&gt;​		1）响应页面；去哪个页面是由&lt;strong&gt;DefaultErrorViewResolver&lt;/strong&gt;解析得到的；&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;protected ModelAndView resolveErrorView(HttpServletRequest request,
      HttpServletResponse response, HttpStatus status, Map&amp;lt;String, Object&amp;gt; model) {
    //所有的ErrorViewResolver得到ModelAndView
   for (ErrorViewResolver resolver : this.errorViewResolvers) {
      ModelAndView modelAndView = resolver.resolveErrorView(request, status, model);
      if (modelAndView != null) {
         return modelAndView;
      }
   }
   return null;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h4&gt;2）、如果定制错误响应：&lt;/h4&gt;
&lt;h5&gt;&lt;strong&gt;1）、如何定制错误的页面；&lt;/strong&gt;&lt;/h5&gt;
&lt;p&gt;​			&lt;strong&gt;1）、有模板引擎的情况下；error/状态码;&lt;/strong&gt; 【将错误页面命名为  错误状态码.html 放在模板引擎文件夹里面的 error文件夹下】，发生此状态码的错误就会来到  对应的页面；&lt;/p&gt;
&lt;p&gt;​			我们可以使用4xx和5xx作为错误页面的文件名来匹配这种类型的所有错误，精确优先（优先寻找精确的状态码.html）；&lt;/p&gt;
&lt;p&gt;​			页面能获取的信息；&lt;/p&gt;
&lt;p&gt;​				timestamp：时间戳&lt;/p&gt;
&lt;p&gt;​				status：状态码&lt;/p&gt;
&lt;p&gt;​				error：错误提示&lt;/p&gt;
&lt;p&gt;​				exception：异常对象&lt;/p&gt;
&lt;p&gt;​				message：异常消息&lt;/p&gt;
&lt;p&gt;​				errors：JSR303数据校验的错误都在这里&lt;/p&gt;
&lt;p&gt;​			2）、没有模板引擎（模板引擎找不到这个错误页面），静态资源文件夹下找；&lt;/p&gt;
&lt;p&gt;​			3）、以上都没有错误页面，就是默认来到SpringBoot默认的错误提示页面；&lt;/p&gt;
&lt;h5&gt;2）、如何定制错误的json数据；&lt;/h5&gt;
&lt;p&gt;​		1）、自定义异常处理&amp;amp;返回定制json数据；&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;@ControllerAdvice
public class MyExceptionHandler {

    @ResponseBody
    @ExceptionHandler(UserNotExistException.class)
    public Map&amp;lt;String,Object&amp;gt; handleException(Exception e){
        Map&amp;lt;String,Object&amp;gt; map = new HashMap&amp;lt;&amp;gt;();
        map.put(&quot;code&quot;,&quot;user.notexist&quot;);
        map.put(&quot;message&quot;,e.getMessage());
        return map;
    }
}
//没有自适应效果...
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;​		2）、转发到/error进行自适应响应效果处理&lt;/p&gt;
&lt;pre&gt;&lt;code&gt; @ExceptionHandler(UserNotExistException.class)
    public String handleException(Exception e, HttpServletRequest request){
        Map&amp;lt;String,Object&amp;gt; map = new HashMap&amp;lt;&amp;gt;();
        //传入我们自己的错误状态码  4xx 5xx，否则就不会进入定制错误页面的解析流程
        /**
         * Integer statusCode = (Integer) request
         .getAttribute(&quot;javax.servlet.error.status_code&quot;);
         */
        request.setAttribute(&quot;javax.servlet.error.status_code&quot;,500);
        map.put(&quot;code&quot;,&quot;user.notexist&quot;);
        map.put(&quot;message&quot;,e.getMessage());
        //转发到/error
        return &quot;forward:/error&quot;;
    }
&lt;/code&gt;&lt;/pre&gt;
&lt;h5&gt;3）、将我们的定制数据携带出去；&lt;/h5&gt;
&lt;p&gt;出现错误以后，会来到/error请求，会被BasicErrorController处理，响应出去可以获取的数据是由getErrorAttributes得到的（是AbstractErrorController（ErrorController）规定的方法）；&lt;/p&gt;
&lt;p&gt;​	1、完全来编写一个ErrorController的实现类【或者是编写AbstractErrorController的子类】，放在容器中；&lt;/p&gt;
&lt;p&gt;​	2、页面上能用的数据，或者是json返回能用的数据都是通过errorAttributes.getErrorAttributes得到；&lt;/p&gt;
&lt;p&gt;​			容器中DefaultErrorAttributes.getErrorAttributes()；默认进行数据处理的；&lt;/p&gt;
&lt;p&gt;自定义ErrorAttributes&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;//给容器中加入我们自己定义的ErrorAttributes
@Component
public class MyErrorAttributes extends DefaultErrorAttributes {

    @Override
    public Map&amp;lt;String, Object&amp;gt; getErrorAttributes(RequestAttributes requestAttributes, boolean includeStackTrace) {
        Map&amp;lt;String, Object&amp;gt; map = super.getErrorAttributes(requestAttributes, includeStackTrace);
        map.put(&quot;company&quot;,&quot;atguigu&quot;);
        return map;
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;最终的效果：响应是自适应的，可以通过定制ErrorAttributes改变需要返回的内容，&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;%E6%90%9C%E7%8B%97%E6%88%AA%E5%9B%BE20180228135513.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;h3&gt;8、配置嵌入式Servlet容器&lt;/h3&gt;
&lt;p&gt;SpringBoot默认使用Tomcat作为嵌入式的Servlet容器；&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;%E6%90%9C%E7%8B%97%E6%88%AA%E5%9B%BE20180301142915.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;问题？&lt;/p&gt;
&lt;h4&gt;1）、如何定制和修改Servlet容器的相关配置；&lt;/h4&gt;
&lt;p&gt;1、修改和server有关的配置（ServerProperties【也是EmbeddedServletContainerCustomizer】）；&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;server.port=8081
server.context-path=/crud

server.tomcat.uri-encoding=UTF-8

//通用的Servlet容器设置
server.xxx
//Tomcat的设置
server.tomcat.xxx
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;2、编写一个&lt;strong&gt;EmbeddedServletContainerCustomizer&lt;/strong&gt;：嵌入式的Servlet容器的定制器；来修改Servlet容器的配置&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;@Bean  //一定要将这个定制器加入到容器中
public EmbeddedServletContainerCustomizer embeddedServletContainerCustomizer(){
    return new EmbeddedServletContainerCustomizer() {

        //定制嵌入式的Servlet容器相关的规则
        @Override
        public void customize(ConfigurableEmbeddedServletContainer container) {
            container.setPort(8083);
        }
    };
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h4&gt;2）、注册Servlet三大组件【Servlet、Filter、Listener】&lt;/h4&gt;
&lt;p&gt;由于SpringBoot默认是以jar包的方式启动嵌入式的Servlet容器来启动SpringBoot的web应用，没有web.xml文件。&lt;/p&gt;
&lt;p&gt;注册三大组件用以下方式&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;ServletRegistrationBean&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;//注册三大组件
@Bean
public ServletRegistrationBean myServlet(){
    ServletRegistrationBean registrationBean = new ServletRegistrationBean(new MyServlet(),&quot;/myServlet&quot;);
    return registrationBean;
}

&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;FilterRegistrationBean&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;@Bean
public FilterRegistrationBean myFilter(){
    FilterRegistrationBean registrationBean = new FilterRegistrationBean();
    registrationBean.setFilter(new MyFilter());
    registrationBean.setUrlPatterns(Arrays.asList(&quot;/hello&quot;,&quot;/myServlet&quot;));
    return registrationBean;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;ServletListenerRegistrationBean&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;@Bean
public ServletListenerRegistrationBean myListener(){
    ServletListenerRegistrationBean&amp;lt;MyListener&amp;gt; registrationBean = new ServletListenerRegistrationBean&amp;lt;&amp;gt;(new MyListener());
    return registrationBean;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;SpringBoot帮我们自动SpringMVC的时候，自动的注册SpringMVC的前端控制器；DIspatcherServlet；&lt;/p&gt;
&lt;p&gt;DispatcherServletAutoConfiguration中：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;@Bean(name = DEFAULT_DISPATCHER_SERVLET_REGISTRATION_BEAN_NAME)
@ConditionalOnBean(value = DispatcherServlet.class, name = DEFAULT_DISPATCHER_SERVLET_BEAN_NAME)
public ServletRegistrationBean dispatcherServletRegistration(
      DispatcherServlet dispatcherServlet) {
   ServletRegistrationBean registration = new ServletRegistrationBean(
         dispatcherServlet, this.serverProperties.getServletMapping());
    //默认拦截： /  所有请求；包静态资源，但是不拦截jsp请求；   /*会拦截jsp
    //可以通过server.servletPath来修改SpringMVC前端控制器默认拦截的请求路径
    
   registration.setName(DEFAULT_DISPATCHER_SERVLET_BEAN_NAME);
   registration.setLoadOnStartup(
         this.webMvcProperties.getServlet().getLoadOnStartup());
   if (this.multipartConfig != null) {
      registration.setMultipartConfig(this.multipartConfig);
   }
   return registration;
}

&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;2）、SpringBoot能不能支持其他的Servlet容器；&lt;/p&gt;
&lt;h4&gt;3）、替换为其他嵌入式Servlet容器&lt;/h4&gt;
&lt;p&gt;&lt;img src=&quot;%E6%90%9C%E7%8B%97%E6%88%AA%E5%9B%BE20180302114401.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;默认支持：&lt;/p&gt;
&lt;p&gt;Tomcat（默认使用）&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&amp;lt;dependency&amp;gt;
   &amp;lt;groupId&amp;gt;org.springframework.boot&amp;lt;/groupId&amp;gt;
   &amp;lt;artifactId&amp;gt;spring-boot-starter-web&amp;lt;/artifactId&amp;gt;
   引入web模块默认就是使用嵌入式的Tomcat作为Servlet容器；
&amp;lt;/dependency&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Jetty&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&amp;lt;!-- 引入web模块 --&amp;gt;
&amp;lt;dependency&amp;gt;
   &amp;lt;groupId&amp;gt;org.springframework.boot&amp;lt;/groupId&amp;gt;
   &amp;lt;artifactId&amp;gt;spring-boot-starter-web&amp;lt;/artifactId&amp;gt;
   &amp;lt;exclusions&amp;gt;
      &amp;lt;exclusion&amp;gt;
         &amp;lt;artifactId&amp;gt;spring-boot-starter-tomcat&amp;lt;/artifactId&amp;gt;
         &amp;lt;groupId&amp;gt;org.springframework.boot&amp;lt;/groupId&amp;gt;
      &amp;lt;/exclusion&amp;gt;
   &amp;lt;/exclusions&amp;gt;
&amp;lt;/dependency&amp;gt;

&amp;lt;!--引入其他的Servlet容器--&amp;gt;
&amp;lt;dependency&amp;gt;
   &amp;lt;artifactId&amp;gt;spring-boot-starter-jetty&amp;lt;/artifactId&amp;gt;
   &amp;lt;groupId&amp;gt;org.springframework.boot&amp;lt;/groupId&amp;gt;
&amp;lt;/dependency&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Undertow&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&amp;lt;!-- 引入web模块 --&amp;gt;
&amp;lt;dependency&amp;gt;
   &amp;lt;groupId&amp;gt;org.springframework.boot&amp;lt;/groupId&amp;gt;
   &amp;lt;artifactId&amp;gt;spring-boot-starter-web&amp;lt;/artifactId&amp;gt;
   &amp;lt;exclusions&amp;gt;
      &amp;lt;exclusion&amp;gt;
         &amp;lt;artifactId&amp;gt;spring-boot-starter-tomcat&amp;lt;/artifactId&amp;gt;
         &amp;lt;groupId&amp;gt;org.springframework.boot&amp;lt;/groupId&amp;gt;
      &amp;lt;/exclusion&amp;gt;
   &amp;lt;/exclusions&amp;gt;
&amp;lt;/dependency&amp;gt;

&amp;lt;!--引入其他的Servlet容器--&amp;gt;
&amp;lt;dependency&amp;gt;
   &amp;lt;artifactId&amp;gt;spring-boot-starter-undertow&amp;lt;/artifactId&amp;gt;
   &amp;lt;groupId&amp;gt;org.springframework.boot&amp;lt;/groupId&amp;gt;
&amp;lt;/dependency&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;h4&gt;4）、嵌入式Servlet容器自动配置原理；&lt;/h4&gt;
&lt;p&gt;EmbeddedServletContainerAutoConfiguration：嵌入式的Servlet容器自动配置？&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;@AutoConfigureOrder(Ordered.HIGHEST_PRECEDENCE)
@Configuration
@ConditionalOnWebApplication
@Import(BeanPostProcessorsRegistrar.class)
//导入BeanPostProcessorsRegistrar：Spring注解版；给容器中导入一些组件
//导入了EmbeddedServletContainerCustomizerBeanPostProcessor：
//后置处理器：bean初始化前后（创建完对象，还没赋值赋值）执行初始化工作
public class EmbeddedServletContainerAutoConfiguration {
    
    @Configuration
	@ConditionalOnClass({ Servlet.class, Tomcat.class })//判断当前是否引入了Tomcat依赖；
	@ConditionalOnMissingBean(value = EmbeddedServletContainerFactory.class, search = SearchStrategy.CURRENT)//判断当前容器没有用户自己定义EmbeddedServletContainerFactory：嵌入式的Servlet容器工厂；作用：创建嵌入式的Servlet容器
	public static class EmbeddedTomcat {

		@Bean
		public TomcatEmbeddedServletContainerFactory tomcatEmbeddedServletContainerFactory() {
			return new TomcatEmbeddedServletContainerFactory();
		}

	}
    
    /**
	 * Nested configuration if Jetty is being used.
	 */
	@Configuration
	@ConditionalOnClass({ Servlet.class, Server.class, Loader.class,
			WebAppContext.class })
	@ConditionalOnMissingBean(value = EmbeddedServletContainerFactory.class, search = SearchStrategy.CURRENT)
	public static class EmbeddedJetty {

		@Bean
		public JettyEmbeddedServletContainerFactory jettyEmbeddedServletContainerFactory() {
			return new JettyEmbeddedServletContainerFactory();
		}

	}

	/**
	 * Nested configuration if Undertow is being used.
	 */
	@Configuration
	@ConditionalOnClass({ Servlet.class, Undertow.class, SslClientAuthMode.class })
	@ConditionalOnMissingBean(value = EmbeddedServletContainerFactory.class, search = SearchStrategy.CURRENT)
	public static class EmbeddedUndertow {

		@Bean
		public UndertowEmbeddedServletContainerFactory undertowEmbeddedServletContainerFactory() {
			return new UndertowEmbeddedServletContainerFactory();
		}

	}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;1）、EmbeddedServletContainerFactory（嵌入式Servlet容器工厂）&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;public interface EmbeddedServletContainerFactory {

   //获取嵌入式的Servlet容器
   EmbeddedServletContainer getEmbeddedServletContainer(
         ServletContextInitializer... initializers);

}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;img src=&quot;%E6%90%9C%E7%8B%97%E6%88%AA%E5%9B%BE20180302144835.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;2）、EmbeddedServletContainer：（嵌入式的Servlet容器）&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;%E6%90%9C%E7%8B%97%E6%88%AA%E5%9B%BE20180302144910.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;3）、以&lt;strong&gt;TomcatEmbeddedServletContainerFactory&lt;/strong&gt;为例&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;@Override
public EmbeddedServletContainer getEmbeddedServletContainer(
      ServletContextInitializer... initializers) {
    //创建一个Tomcat
   Tomcat tomcat = new Tomcat();
    
    //配置Tomcat的基本环节
   File baseDir = (this.baseDirectory != null ? this.baseDirectory
         : createTempDir(&quot;tomcat&quot;));
   tomcat.setBaseDir(baseDir.getAbsolutePath());
   Connector connector = new Connector(this.protocol);
   tomcat.getService().addConnector(connector);
   customizeConnector(connector);
   tomcat.setConnector(connector);
   tomcat.getHost().setAutoDeploy(false);
   configureEngine(tomcat.getEngine());
   for (Connector additionalConnector : this.additionalTomcatConnectors) {
      tomcat.getService().addConnector(additionalConnector);
   }
   prepareContext(tomcat.getHost(), initializers);
    
    //将配置好的Tomcat传入进去，返回一个EmbeddedServletContainer；并且启动Tomcat服务器
   return getTomcatEmbeddedServletContainer(tomcat);
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;4）、我们对嵌入式容器的配置修改是怎么生效？&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;ServerProperties、EmbeddedServletContainerCustomizer
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;EmbeddedServletContainerCustomizer&lt;/strong&gt;：定制器帮我们修改了Servlet容器的配置？&lt;/p&gt;
&lt;p&gt;怎么修改的原理？&lt;/p&gt;
&lt;p&gt;5）、容器中导入了&lt;strong&gt;EmbeddedServletContainerCustomizerBeanPostProcessor&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;//初始化之前
@Override
public Object postProcessBeforeInitialization(Object bean, String beanName)
      throws BeansException {
    //如果当前初始化的是一个ConfigurableEmbeddedServletContainer类型的组件
   if (bean instanceof ConfigurableEmbeddedServletContainer) {
       //
      postProcessBeforeInitialization((ConfigurableEmbeddedServletContainer) bean);
   }
   return bean;
}

private void postProcessBeforeInitialization(
			ConfigurableEmbeddedServletContainer bean) {
    //获取所有的定制器，调用每一个定制器的customize方法来给Servlet容器进行属性赋值；
    for (EmbeddedServletContainerCustomizer customizer : getCustomizers()) {
        customizer.customize(bean);
    }
}

private Collection&amp;lt;EmbeddedServletContainerCustomizer&amp;gt; getCustomizers() {
    if (this.customizers == null) {
        // Look up does not include the parent context
        this.customizers = new ArrayList&amp;lt;EmbeddedServletContainerCustomizer&amp;gt;(
            this.beanFactory
            //从容器中获取所有这葛类型的组件：EmbeddedServletContainerCustomizer
            //定制Servlet容器，给容器中可以添加一个EmbeddedServletContainerCustomizer类型的组件
            .getBeansOfType(EmbeddedServletContainerCustomizer.class,
                            false, false)
            .values());
        Collections.sort(this.customizers, AnnotationAwareOrderComparator.INSTANCE);
        this.customizers = Collections.unmodifiableList(this.customizers);
    }
    return this.customizers;
}

ServerProperties也是定制器
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;步骤：&lt;/p&gt;
&lt;p&gt;1）、SpringBoot根据导入的依赖情况，给容器中添加相应的EmbeddedServletContainerFactory【TomcatEmbeddedServletContainerFactory】&lt;/p&gt;
&lt;p&gt;2）、容器中某个组件要创建对象就会惊动后置处理器；EmbeddedServletContainerCustomizerBeanPostProcessor；&lt;/p&gt;
&lt;p&gt;只要是嵌入式的Servlet容器工厂，后置处理器就工作；&lt;/p&gt;
&lt;p&gt;3）、后置处理器，从容器中获取所有的&lt;strong&gt;EmbeddedServletContainerCustomizer&lt;/strong&gt;，调用定制器的定制方法&lt;/p&gt;
&lt;p&gt;###5）、嵌入式Servlet容器启动原理；&lt;/p&gt;
&lt;p&gt;什么时候创建嵌入式的Servlet容器工厂？什么时候获取嵌入式的Servlet容器并启动Tomcat；&lt;/p&gt;
&lt;p&gt;获取嵌入式的Servlet容器工厂：&lt;/p&gt;
&lt;p&gt;1）、SpringBoot应用启动运行run方法&lt;/p&gt;
&lt;p&gt;2）、refreshContext(context);SpringBoot刷新IOC容器【创建IOC容器对象，并初始化容器，创建容器中的每一个组件】；如果是web应用创建&lt;strong&gt;AnnotationConfigEmbeddedWebApplicationContext&lt;/strong&gt;，否则：&lt;strong&gt;AnnotationConfigApplicationContext&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;3）、refresh(context);&lt;strong&gt;刷新刚才创建好的ioc容器；&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;public void refresh() throws BeansException, IllegalStateException {
   synchronized (this.startupShutdownMonitor) {
      // Prepare this context for refreshing.
      prepareRefresh();

      // Tell the subclass to refresh the internal bean factory.
      ConfigurableListableBeanFactory beanFactory = obtainFreshBeanFactory();

      // Prepare the bean factory for use in this context.
      prepareBeanFactory(beanFactory);

      try {
         // Allows post-processing of the bean factory in context subclasses.
         postProcessBeanFactory(beanFactory);

         // Invoke factory processors registered as beans in the context.
         invokeBeanFactoryPostProcessors(beanFactory);

         // Register bean processors that intercept bean creation.
         registerBeanPostProcessors(beanFactory);

         // Initialize message source for this context.
         initMessageSource();

         // Initialize event multicaster for this context.
         initApplicationEventMulticaster();

         // Initialize other special beans in specific context subclasses.
         onRefresh();

         // Check for listener beans and register them.
         registerListeners();

         // Instantiate all remaining (non-lazy-init) singletons.
         finishBeanFactoryInitialization(beanFactory);

         // Last step: publish corresponding event.
         finishRefresh();
      }

      catch (BeansException ex) {
         if (logger.isWarnEnabled()) {
            logger.warn(&quot;Exception encountered during context initialization - &quot; +
                  &quot;cancelling refresh attempt: &quot; + ex);
         }

         // Destroy already created singletons to avoid dangling resources.
         destroyBeans();

         // Reset &apos;active&apos; flag.
         cancelRefresh(ex);

         // Propagate exception to caller.
         throw ex;
      }

      finally {
         // Reset common introspection caches in Spring&apos;s core, since we
         // might not ever need metadata for singleton beans anymore...
         resetCommonCaches();
      }
   }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;4）、  onRefresh(); web的ioc容器重写了onRefresh方法&lt;/p&gt;
&lt;p&gt;5）、webioc容器会创建嵌入式的Servlet容器；&lt;strong&gt;createEmbeddedServletContainer&lt;/strong&gt;();&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;6）、获取嵌入式的Servlet容器工厂：&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;EmbeddedServletContainerFactory containerFactory = getEmbeddedServletContainerFactory();&lt;/p&gt;
&lt;p&gt;​	从ioc容器中获取EmbeddedServletContainerFactory 组件；&lt;strong&gt;TomcatEmbeddedServletContainerFactory&lt;/strong&gt;创建对象，后置处理器一看是这个对象，就获取所有的定制器来先定制Servlet容器的相关配置；&lt;/p&gt;
&lt;p&gt;7）、&lt;strong&gt;使用容器工厂获取嵌入式的Servlet容器&lt;/strong&gt;：this.embeddedServletContainer = containerFactory      .getEmbeddedServletContainer(getSelfInitializer());&lt;/p&gt;
&lt;p&gt;8）、嵌入式的Servlet容器创建对象并启动Servlet容器；&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;先启动嵌入式的Servlet容器，再将ioc容器中剩下没有创建出的对象获取出来；&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;&lt;strong&gt;IOC容器启动创建嵌入式的Servlet容器&lt;/strong&gt;&lt;/strong&gt;&lt;/p&gt;
&lt;h3&gt;9、使用外置的Servlet容器&lt;/h3&gt;
&lt;p&gt;嵌入式Servlet容器：应用打成可执行的jar&lt;/p&gt;
&lt;p&gt;​		优点：简单、便携；&lt;/p&gt;
&lt;p&gt;​		缺点：默认不支持JSP、优化定制比较复杂（使用定制器【ServerProperties、自定义EmbeddedServletContainerCustomizer】，自己编写嵌入式Servlet容器的创建工厂【EmbeddedServletContainerFactory】）；&lt;/p&gt;
&lt;p&gt;外置的Servlet容器：外面安装Tomcat---应用war包的方式打包；&lt;/p&gt;
&lt;h4&gt;步骤&lt;/h4&gt;
&lt;p&gt;1）、必须创建一个war项目；（利用idea创建好目录结构）&lt;/p&gt;
&lt;p&gt;2）、将嵌入式的Tomcat指定为provided；&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&amp;lt;dependency&amp;gt;
   &amp;lt;groupId&amp;gt;org.springframework.boot&amp;lt;/groupId&amp;gt;
   &amp;lt;artifactId&amp;gt;spring-boot-starter-tomcat&amp;lt;/artifactId&amp;gt;
   &amp;lt;scope&amp;gt;provided&amp;lt;/scope&amp;gt;
&amp;lt;/dependency&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;3）、必须编写一个&lt;strong&gt;SpringBootServletInitializer&lt;/strong&gt;的子类，并调用configure方法&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;public class ServletInitializer extends SpringBootServletInitializer {

   @Override
   protected SpringApplicationBuilder configure(SpringApplicationBuilder application) {
       //传入SpringBoot应用的主程序
      return application.sources(SpringBoot04WebJspApplication.class);
   }

}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;4）、启动服务器就可以使用；&lt;/p&gt;
&lt;h4&gt;原理&lt;/h4&gt;
&lt;p&gt;jar包：执行SpringBoot主类的main方法，启动ioc容器，创建嵌入式的Servlet容器；&lt;/p&gt;
&lt;p&gt;war包：启动服务器，&lt;strong&gt;服务器启动SpringBoot应用&lt;/strong&gt;【SpringBootServletInitializer】，启动ioc容器；&lt;/p&gt;
&lt;p&gt;servlet3.0（Spring注解版）：&lt;/p&gt;
&lt;p&gt;8.2.4 Shared libraries / runtimes pluggability：&lt;/p&gt;
&lt;p&gt;规则：&lt;/p&gt;
&lt;p&gt;​	1）、服务器启动（web应用启动）会创建当前web应用里面每一个jar包里面ServletContainerInitializer实例：&lt;/p&gt;
&lt;p&gt;​	2）、ServletContainerInitializer的实现放在jar包的META-INF/services文件夹下，有一个名为javax.servlet.ServletContainerInitializer的文件，内容就是ServletContainerInitializer的实现类的全类名&lt;/p&gt;
&lt;p&gt;​	3）、还可以使用@HandlesTypes，在应用启动的时候加载我们感兴趣的类；&lt;/p&gt;
&lt;p&gt;流程：&lt;/p&gt;
&lt;p&gt;1）、启动Tomcat&lt;/p&gt;
&lt;p&gt;2）、org\springframework\spring-web\4.3.14.RELEASE\spring-web-4.3.14.RELEASE.jar!\META-INF\services\javax.servlet.ServletContainerInitializer：&lt;/p&gt;
&lt;p&gt;Spring的web模块里面有这个文件：&lt;strong&gt;org.springframework.web.SpringServletContainerInitializer&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;3）、SpringServletContainerInitializer将@HandlesTypes(WebApplicationInitializer.class)标注的所有这个类型的类都传入到onStartup方法的Set&amp;lt;Class&amp;lt;?&amp;gt;&amp;gt;；为这些WebApplicationInitializer类型的类创建实例；&lt;/p&gt;
&lt;p&gt;4）、每一个WebApplicationInitializer都调用自己的onStartup；&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;%E6%90%9C%E7%8B%97%E6%88%AA%E5%9B%BE20180302221835.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;5）、相当于我们的SpringBootServletInitializer的类会被创建对象，并执行onStartup方法&lt;/p&gt;
&lt;p&gt;6）、SpringBootServletInitializer实例执行onStartup的时候会createRootApplicationContext；创建容器&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;protected WebApplicationContext createRootApplicationContext(
      ServletContext servletContext) {
    //1、创建SpringApplicationBuilder
   SpringApplicationBuilder builder = createSpringApplicationBuilder();
   StandardServletEnvironment environment = new StandardServletEnvironment();
   environment.initPropertySources(servletContext, null);
   builder.environment(environment);
   builder.main(getClass());
   ApplicationContext parent = getExistingRootWebApplicationContext(servletContext);
   if (parent != null) {
      this.logger.info(&quot;Root context already created (using as parent).&quot;);
      servletContext.setAttribute(
            WebApplicationContext.ROOT_WEB_APPLICATION_CONTEXT_ATTRIBUTE, null);
      builder.initializers(new ParentContextApplicationContextInitializer(parent));
   }
   builder.initializers(
         new ServletContextApplicationContextInitializer(servletContext));
   builder.contextClass(AnnotationConfigEmbeddedWebApplicationContext.class);
    
    //调用configure方法，子类重写了这个方法，将SpringBoot的主程序类传入了进来
   builder = configure(builder);
    
    //使用builder创建一个Spring应用
   SpringApplication application = builder.build();
   if (application.getSources().isEmpty() &amp;amp;&amp;amp; AnnotationUtils
         .findAnnotation(getClass(), Configuration.class) != null) {
      application.getSources().add(getClass());
   }
   Assert.state(!application.getSources().isEmpty(),
         &quot;No SpringApplication sources have been defined. Either override the &quot;
               + &quot;configure method or add an @Configuration annotation&quot;);
   // Ensure error pages are registered
   if (this.registerErrorPageFilter) {
      application.getSources().add(ErrorPageFilterConfiguration.class);
   }
    //启动Spring应用
   return run(application);
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;7）、Spring的应用就启动并且创建IOC容器&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;public ConfigurableApplicationContext run(String... args) {
   StopWatch stopWatch = new StopWatch();
   stopWatch.start();
   ConfigurableApplicationContext context = null;
   FailureAnalyzers analyzers = null;
   configureHeadlessProperty();
   SpringApplicationRunListeners listeners = getRunListeners(args);
   listeners.starting();
   try {
      ApplicationArguments applicationArguments = new DefaultApplicationArguments(
            args);
      ConfigurableEnvironment environment = prepareEnvironment(listeners,
            applicationArguments);
      Banner printedBanner = printBanner(environment);
      context = createApplicationContext();
      analyzers = new FailureAnalyzers(context);
      prepareContext(context, environment, listeners, applicationArguments,
            printedBanner);
       
       //刷新IOC容器
      refreshContext(context);
      afterRefresh(context, applicationArguments);
      listeners.finished(context, null);
      stopWatch.stop();
      if (this.logStartupInfo) {
         new StartupInfoLogger(this.mainApplicationClass)
               .logStarted(getApplicationLog(), stopWatch);
      }
      return context;
   }
   catch (Throwable ex) {
      handleRunFailure(context, listeners, analyzers, ex);
      throw new IllegalStateException(ex);
   }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;&lt;strong&gt;启动Servlet容器，再启动SpringBoot应用&lt;/strong&gt;&lt;/strong&gt;&lt;/p&gt;
&lt;h2&gt;&lt;strong&gt;五、单元测试&lt;/strong&gt;&lt;/h2&gt;
&lt;blockquote&gt;
&lt;p&gt;SpringBoot整合Junit以后。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;编写测试方法：@Test标注（注意需要使用junit5版本的注解）&lt;/li&gt;
&lt;li&gt;Junit类具有Spring的功能，@Autowired、比如 @Transactional 标注测试方法，测试完成后自动回滚&lt;/li&gt;
&lt;/ul&gt;
&lt;/blockquote&gt;
&lt;pre&gt;&lt;code&gt;&amp;lt;dependency&amp;gt;
  &amp;lt;groupId&amp;gt;org.springframework.boot&amp;lt;/groupId&amp;gt;
  &amp;lt;artifactId&amp;gt;spring-boot-starter-test&amp;lt;/artifactId&amp;gt;
  &amp;lt;scope&amp;gt;test&amp;lt;/scope&amp;gt;
&amp;lt;/dependency&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;@SpringBootTest
class Boot05WebAdminApplicationTests {


    @Test
    void contextLoads() {

    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;JUnit5常用注解&lt;/h3&gt;
&lt;p&gt;JUnit5的注解与JUnit4的注解有所变化&lt;/p&gt;
&lt;p&gt;https://junit.org/junit5/docs/current/user-guide/#writing-tests-annotations&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;**@Test :**表示方法是测试方法。但是与JUnit4的@Test不同，他的职责非常单一不能声明任何属性，拓展的测试将会由Jupiter提供额外测试&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;**@ParameterizedTest :**表示方法是参数化测试，下方会有详细介绍&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;**@RepeatedTest :**表示方法可重复执行，下方会有详细介绍&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;**@DisplayName :**为测试类或者测试方法设置展示名称&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;**@BeforeEach :**表示在每个单元测试之前执行&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;**@AfterEach :**表示在每个单元测试之后执行&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;**@BeforeAll :**表示在所有单元测试之前执行&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;**@AfterAll :**表示在所有单元测试之后执行&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;**@Tag :**表示单元测试类别，类似于JUnit4中的@Categories&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;**@Disabled :**表示测试类或测试方法不执行，类似于JUnit4中的@Ignore&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;**@Timeout :**表示测试方法运行如果超过了指定时间将会返回错误&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;**@ExtendWith :**为测试类或测试方法提供扩展类引用&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;六、SpringBoot与数据访问&lt;/h2&gt;
&lt;h3&gt;1、JDBC&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;&amp;lt;dependency&amp;gt;
			&amp;lt;groupId&amp;gt;org.springframework.boot&amp;lt;/groupId&amp;gt;
			&amp;lt;artifactId&amp;gt;spring-boot-starter-jdbc&amp;lt;/artifactId&amp;gt;
		&amp;lt;/dependency&amp;gt;
		&amp;lt;dependency&amp;gt;
			&amp;lt;groupId&amp;gt;mysql&amp;lt;/groupId&amp;gt;
			&amp;lt;artifactId&amp;gt;mysql-connector-java&amp;lt;/artifactId&amp;gt;
			&amp;lt;scope&amp;gt;runtime&amp;lt;/scope&amp;gt;
		&amp;lt;/dependency&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;spring:
  datasource:
    username: root
    password: 123456
    url: jdbc:mysql://192.168.15.22:3306/jdbc?characterEncoding=utf-8&amp;amp;useSSL=false
    driver-class-name: com.mysql.jdbc.Driver
    type: 配置数据源类型
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;效果：&lt;/p&gt;
&lt;p&gt;​	默认是用org.apache.tomcat.jdbc.pool.DataSource作为数据源；&lt;/p&gt;
&lt;p&gt;​	数据源的相关配置都在DataSourceProperties里面；&lt;/p&gt;
&lt;p&gt;自动配置原理：&lt;/p&gt;
&lt;p&gt;org.springframework.boot.autoconfigure.jdbc：&lt;/p&gt;
&lt;p&gt;1、参考DataSourceConfiguration，根据配置创建数据源，默认使用Tomcat连接池；可以使用spring.datasource.type指定自定义的数据源类型；&lt;/p&gt;
&lt;p&gt;2、SpringBoot默认可以支持；&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;org.apache.tomcat.jdbc.pool.DataSource、HikariDataSource、BasicDataSource、
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;3、自定义数据源类型&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;/**
 * Generic DataSource configuration.
 */
@ConditionalOnMissingBean(DataSource.class)
@ConditionalOnProperty(name = &quot;spring.datasource.type&quot;)
static class Generic {

   @Bean
   public DataSource dataSource(DataSourceProperties properties) {
       //使用DataSourceBuilder创建数据源，利用反射创建响应type的数据源，并且绑定相关属性
      return properties.initializeDataSourceBuilder().build();
   }

}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;4、&lt;strong&gt;DataSourceInitializer：ApplicationListener&lt;/strong&gt;；&lt;/p&gt;
&lt;p&gt;​	作用：&lt;/p&gt;
&lt;p&gt;​		1）、runSchemaScripts();运行建表语句；&lt;/p&gt;
&lt;p&gt;​		2）、runDataScripts();运行插入数据的sql语句；&lt;/p&gt;
&lt;p&gt;默认只需要将文件命名为：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;schema-*.sql、data-*.sql
默认规则：schema.sql，schema-all.sql；
可以使用   
	schema:
      - classpath:department.sql
      指定位置
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;5、操作数据库：自动配置了JdbcTemplate操作数据库&lt;/p&gt;
&lt;h3&gt;2、整合Druid数据源&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;引入依赖：
		&amp;lt;dependency&amp;gt;
			&amp;lt;groupId&amp;gt;com.alibaba&amp;lt;/groupId&amp;gt;
			&amp;lt;artifactId&amp;gt;druid&amp;lt;/artifactId&amp;gt;
			&amp;lt;version&amp;gt;1.0.26&amp;lt;/version&amp;gt;
		&amp;lt;/dependency&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;导入druid数据源
import javax.sql.DataSource;

@Configuration
public class DruidConfig {

    @ConfigurationProperties(prefix = &quot;spring.datasource&quot;)
    @Bean
    public DataSource druid(){
       return  new DruidDataSource();
    }

    //配置Druid的监控
    //1、配置一个管理后台的Servlet
    @Bean
    public ServletRegistrationBean statViewServlet(){
        ServletRegistrationBean bean = new ServletRegistrationBean(new StatViewServlet(), &quot;/druid/*&quot;);
        Map&amp;lt;String,String&amp;gt; initParams = new HashMap&amp;lt;&amp;gt;();

        initParams.put(&quot;loginUsername&quot;,&quot;admin&quot;);
        initParams.put(&quot;loginPassword&quot;,&quot;123456&quot;);
        initParams.put(&quot;allow&quot;,&quot;&quot;);//默认就是允许所有访问
        initParams.put(&quot;deny&quot;,&quot;192.168.15.21&quot;);

        bean.setInitParameters(initParams);
        return bean;
    }


    //2、配置一个web监控的filter
    @Bean
    public FilterRegistrationBean webStatFilter(){
        FilterRegistrationBean bean = new FilterRegistrationBean();
        bean.setFilter(new WebStatFilter());

        Map&amp;lt;String,String&amp;gt; initParams = new HashMap&amp;lt;&amp;gt;();
        initParams.put(&quot;exclusions&quot;,&quot;*.js,*.css,/druid/*&quot;);

        bean.setInitParameters(initParams);

        bean.setUrlPatterns(Arrays.asList(&quot;/*&quot;));

        return  bean;
    }
}

&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;druid配置&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;spring:
  datasource:
##   数据源基本配置
    username: root
    password: 123456
    driver-class-name: com.mysql.jdbc.Driver
    url: jdbc:mysql://localhost:3306/ssm_crud
    type: com.alibaba.druid.pool.DruidDataSource
##   数据源其他配置
    initialSize: 5
    minIdle: 5
    maxActive: 20
    maxWait: 60000
    timeBetweenEvictionRunsMillis: 60000
    minEvictableIdleTimeMillis: 300000
    validationQuery: SELECT 1 FROM DUAL
    testWhileIdle: true
    testOnBorrow: false
    testOnReturn: false
    poolPreparedStatements: true
##   配置监控统计拦截的filters，去掉后监控界面sql无法统计，&apos;wall&apos;用于防火墙  
    filters: stat,wall,log4j
    maxPoolPreparedStatementPerConnectionSize: 20
    useGlobalDataSourceStat: true  
    connectionProperties: druid.stat.mergeSql=true;druid.stat.slowSqlMillis=500
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;druid  properties配置&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;##   数据源其他配置
spring.datasource.initialSize=5
spring.datasource.minIdle=5
spring.datasource.maxActive=20
spring.datasource.maxWait=60000
spring.datasource.timeBetweenEvictionRunsMillis=60000
spring.datasource.minEvictableIdleTimeMillis=300000
spring.datasource.validationQuery=SELECT 1 FROM DUAL
spring.datasource.testWhileIdle=true
spring.datasource.testOnBorrow=false
spring.datasource.testOnReturn=false
spring.datasource.poolPreparedStatements=true
##   配置监控统计拦截的filters，去掉后监控界面sql无法统计，&apos;wall&apos;用于防火墙
spring.datasource.filters=stat,wall,log4j
spring.datasource.maxPoolPreparedStatementPerConnectionSize=20
spring.datasource.useGlobalDataSourceStat=rue  
spring.datasource.connectionProperties=druid.stat.mergeSql=true;druid.stat.slowSqlMillis=500
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;3、整合MyBatis&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;		&amp;lt;dependency&amp;gt;
			&amp;lt;groupId&amp;gt;org.mybatis.spring.boot&amp;lt;/groupId&amp;gt;
			&amp;lt;artifactId&amp;gt;mybatis-spring-boot-starter&amp;lt;/artifactId&amp;gt;
			&amp;lt;version&amp;gt;1.3.1&amp;lt;/version&amp;gt;
		&amp;lt;/dependency&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;img src=&quot;%E6%90%9C%E7%8B%97%E6%88%AA%E5%9B%BE20180305194443.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;步骤：&lt;/p&gt;
&lt;p&gt;​	1）、配置数据源相关属性（见上一节Druid）&lt;/p&gt;
&lt;p&gt;​	2）、给数据库建表&lt;/p&gt;
&lt;p&gt;​	3）、创建JavaBean&lt;/p&gt;
&lt;p&gt;​	4 )、开启驼峰命名法：mybatis.configuration.map-underscore-to-camel-case=true&lt;/p&gt;
&lt;h4&gt;4）、注解版&lt;/h4&gt;
&lt;pre&gt;&lt;code&gt;//指定这是一个操作数据库的mapper
@Mapper
public interface DepartmentMapper {

    @Select(&quot;select * from department where id=#{id}&quot;)
    public Department getDeptById(Integer id);

    @Delete(&quot;delete from department where id=#{id}&quot;)
    public int deleteDeptById(Integer id);

    @Options(useGeneratedKeys = true,keyProperty = &quot;id&quot;)
    @Insert(&quot;insert into department(departmentName) values(#{departmentName})&quot;)
    public int insertDept(Department department);

    @Update(&quot;update department set departmentName=#{departmentName} where id=#{id}&quot;)
    public int updateDept(Department department);
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;问题：&lt;/p&gt;
&lt;p&gt;自定义MyBatis的配置规则；给容器中添加一个ConfigurationCustomizer；&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;@org.springframework.context.annotation.Configuration
public class MyBatisConfig {

    @Bean
    public ConfigurationCustomizer configurationCustomizer(){
        return new ConfigurationCustomizer(){

            @Override
            public void customize(Configuration configuration) {
                configuration.setMapUnderscoreToCamelCase(true);
            }
        };
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;使用MapperScan批量扫描所有的Mapper接口；
@MapperScan(value = &quot;com.atguigu.springboot.mapper&quot;)
@SpringBootApplication
public class SpringBoot06DataMybatisApplication {

	public static void main(String[] args) {
		SpringApplication.run(SpringBoot06DataMybatisApplication.class, args);
	}
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h4&gt;5）、配置文件版&lt;/h4&gt;
&lt;pre&gt;&lt;code&gt;mybatis:
  config-location: classpath:mybatis/mybatis-config.xml 指定全局配置文件的位置
  mapper-locations: classpath:mybatis/mapper/*.xml  指定sql映射文件的位置
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;更多使用参照&lt;/p&gt;
&lt;p&gt;http://www.mybatis.org/spring-boot-starter/mybatis-spring-boot-autoconfigure/&lt;/p&gt;
&lt;h3&gt;4、整合SpringData JPA&lt;/h3&gt;
&lt;h4&gt;1）、SpringData简介&lt;/h4&gt;
&lt;p&gt;&lt;img src=&quot;%E6%90%9C%E7%8B%97%E6%88%AA%E5%9B%BE20180306105412.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;h4&gt;2）、整合SpringData JPA&lt;/h4&gt;
&lt;p&gt;jsp是什么？&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;JPA(Java Persistence API)是Sun官方提出的Java持久化规范。它为Java开发人员提供了一种对象/关联映射工具来管理Java应用中的关系数据。他的出现主要是为了简化现有的持久化开发工作和整合ORM技术，结束现在Hibernate，TopLink，JDO等ORM框架各自为营的局面。
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;springdatajpa是什么？&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;Spring Data JPA 是 Spring 基于 ORM 框架、JPA 规范的基础上封装的一套JPA应用框架，可使开发者用极简的代码即可实现对数据的访问和操作。它提供了包括增删改查等在内的常用功能，且易于扩展！学习并使用 Spring Data JPA 可以极大提高开发效率！
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;**JPA:ORM（**Object Relational Mapping）；&lt;/p&gt;
&lt;p&gt;1）、编写一个实体类（bean）和数据表进行映射，并且配置好映射关系；&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;//使用JPA注解配置映射关系
@Entity //告诉JPA这是一个实体类（和数据表映射的类）
@Table(name = &quot;tbl_user&quot;) //@Table来指定和哪个数据表对应;如果省略默认表名就是user；
public class User {

    @Id //这是一个主键
    @GeneratedValue(strategy = GenerationType.IDENTITY)//自增主键
    private Integer id;

    @Column(name = &quot;last_name&quot;,length = 50) //这是和数据表对应的一个列
    private String lastName;
    @Column //省略默认列名就是属性名
    private String email;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;2）、编写一个Dao接口来操作实体类对应的数据表（Repository）&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;//继承JpaRepository来完成对数据库的操作
public interface UserRepository extends JpaRepository&amp;lt;User,Integer&amp;gt; {
}

&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;3）、基本的配置JpaProperties&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;spring:  
 jpa:
    hibernate:
##     更新或者创建数据表结构
      ddl-auto: update
##    控制台显示SQL
    show-sql: true
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;七、启动配置原理&lt;/h2&gt;
&lt;p&gt;几个重要的事件回调机制&lt;/p&gt;
&lt;p&gt;配置在META-INF/spring.factories&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;ApplicationContextInitializer&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;SpringApplicationRunListener&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;只需要放在ioc容器中&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;ApplicationRunner&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;CommandLineRunner&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;启动流程：&lt;/p&gt;
&lt;h3&gt;&lt;strong&gt;1、创建SpringApplication对象&lt;/strong&gt;&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;initialize(sources);
private void initialize(Object[] sources) {
    //保存主配置类
    if (sources != null &amp;amp;&amp;amp; sources.length &amp;gt; 0) {
        this.sources.addAll(Arrays.asList(sources));
    }
    //判断当前是否一个web应用
    this.webEnvironment = deduceWebEnvironment();
    //从类路径下找到META-INF/spring.factories配置的所有ApplicationContextInitializer；然后保存起来
    setInitializers((Collection) getSpringFactoriesInstances(
        ApplicationContextInitializer.class));
    //从类路径下找到ETA-INF/spring.factories配置的所有ApplicationListener
    setListeners((Collection) getSpringFactoriesInstances(ApplicationListener.class));
    //从多个配置类中找到有main方法的主配置类
    this.mainApplicationClass = deduceMainApplicationClass();
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;img src=&quot;%E6%90%9C%E7%8B%97%E6%88%AA%E5%9B%BE20180306145727.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;%E6%90%9C%E7%8B%97%E6%88%AA%E5%9B%BE20180306145855.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;h3&gt;2、运行run方法&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;public ConfigurableApplicationContext run(String... args) {
   StopWatch stopWatch = new StopWatch();
   stopWatch.start();
   ConfigurableApplicationContext context = null;
   FailureAnalyzers analyzers = null;
   configureHeadlessProperty();
    
   //获取SpringApplicationRunListeners；从类路径下META-INF/spring.factories
   SpringApplicationRunListeners listeners = getRunListeners(args);
    //回调所有的获取SpringApplicationRunListener.starting()方法
   listeners.starting();
   try {
       //封装命令行参数
      ApplicationArguments applicationArguments = new DefaultApplicationArguments(
            args);
      //准备环境
      ConfigurableEnvironment environment = prepareEnvironment(listeners,
            applicationArguments);
       		//创建环境完成后回调SpringApplicationRunListener.environmentPrepared()；表示环境准备完成
       
      Banner printedBanner = printBanner(environment);
       
       //创建ApplicationContext；决定创建web的ioc还是普通的ioc
      context = createApplicationContext();
       
      analyzers = new FailureAnalyzers(context);
       //准备上下文环境;将environment保存到ioc中；而且applyInitializers()；
       //applyInitializers()：回调之前保存的所有的ApplicationContextInitializer的initialize方法
       //回调所有的SpringApplicationRunListener的contextPrepared()；
       //
      prepareContext(context, environment, listeners, applicationArguments,
            printedBanner);
       //prepareContext运行完成以后回调所有的SpringApplicationRunListener的contextLoaded（）；
       
       //s刷新容器；ioc容器初始化（如果是web应用还会创建嵌入式的Tomcat）；Spring注解版
       //扫描，创建，加载所有组件的地方；（配置类，组件，自动配置）
      refreshContext(context);
       //从ioc容器中获取所有的ApplicationRunner和CommandLineRunner进行回调
       //ApplicationRunner先回调，CommandLineRunner再回调
      afterRefresh(context, applicationArguments);
       //所有的SpringApplicationRunListener回调finished方法
      listeners.finished(context, null);
      stopWatch.stop();
      if (this.logStartupInfo) {
         new StartupInfoLogger(this.mainApplicationClass)
               .logStarted(getApplicationLog(), stopWatch);
      }
       //整个SpringBoot应用启动完成以后返回启动的ioc容器；
      return context;
   }
   catch (Throwable ex) {
      handleRunFailure(context, listeners, analyzers, ex);
      throw new IllegalStateException(ex);
   }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;3、事件监听机制&lt;/h3&gt;
&lt;p&gt;配置在META-INF/spring.factories&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;ApplicationContextInitializer&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;public class HelloApplicationContextInitializer implements ApplicationContextInitializer&amp;lt;ConfigurableApplicationContext&amp;gt; {
    @Override
    public void initialize(ConfigurableApplicationContext applicationContext) {
        System.out.println(&quot;ApplicationContextInitializer...initialize...&quot;+applicationContext);
    }
}

&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;SpringApplicationRunListener&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;public class HelloSpringApplicationRunListener implements SpringApplicationRunListener {

    //必须有的构造器
    public HelloSpringApplicationRunListener(SpringApplication application, String[] args){

    }

    @Override
    public void starting() {
        System.out.println(&quot;SpringApplicationRunListener...starting...&quot;);
    }

    @Override
    public void environmentPrepared(ConfigurableEnvironment environment) {
        Object o = environment.getSystemProperties().get(&quot;os.name&quot;);
        System.out.println(&quot;SpringApplicationRunListener...environmentPrepared..&quot;+o);
    }

    @Override
    public void contextPrepared(ConfigurableApplicationContext context) {
        System.out.println(&quot;SpringApplicationRunListener...contextPrepared...&quot;);
    }

    @Override
    public void contextLoaded(ConfigurableApplicationContext context) {
        System.out.println(&quot;SpringApplicationRunListener...contextLoaded...&quot;);
    }

    @Override
    public void finished(ConfigurableApplicationContext context, Throwable exception) {
        System.out.println(&quot;SpringApplicationRunListener...finished...&quot;);
    }
}

&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;配置（META-INF/spring.factories）&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;org.springframework.context.ApplicationContextInitializer=\
com.atguigu.springboot.listener.HelloApplicationContextInitializer

org.springframework.boot.SpringApplicationRunListener=\
com.atguigu.springboot.listener.HelloSpringApplicationRunListener
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;只需要放在ioc容器中&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;ApplicationRunner&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;@Component
public class HelloApplicationRunner implements ApplicationRunner {
    @Override
    public void run(ApplicationArguments args) throws Exception {
        System.out.println(&quot;ApplicationRunner...run....&quot;);
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;CommandLineRunner&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;@Component
public class HelloCommandLineRunner implements CommandLineRunner {
    @Override
    public void run(String... args) throws Exception {
        System.out.println(&quot;CommandLineRunner...run...&quot;+ Arrays.asList(args));
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;八、自定义starter&lt;/h2&gt;
&lt;h3&gt;说明&lt;/h3&gt;
&lt;h4&gt;1、SpringBoot starter机制&lt;/h4&gt;
&lt;blockquote&gt;
&lt;p&gt;SpringBoot中的starter是一种非常重要的机制，能够抛弃以前繁杂的配置，将其统一集成进starter，应用者只需要在maven中引入starter依赖，SpringBoot就能自动扫描到要加载的信息并启动相应的默认配置。starter让我们摆脱了各种依赖库的处理，需要配置各种信息的困扰。SpringBoot会自动通过classpath路径下的类发现需要的Bean，并注册进IOC容器。SpringBoot提供了针对日常企业应用研发各种场景的spring-boot-starter依赖模块。所有这些依赖模块都遵循着约定成俗的默认配置，并允许我们调整这些配置，即遵循“约定大于配置”的理念。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;blockquote&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;SpringBoot 在启动时会去依赖的starter包中寻找 resources/META-INF/spring.factories 文件，然后根据文件中配置的Jar包去扫描项目所依赖的Jar包，这类似于 Java 的 SPI 机制。&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;SPI 全称 Service Provider Interface，是Java提供的一套用来被第三方实现或者扩展的API，它可以用来启用框架扩展和替换组件。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;根据 spring.factories配置加载AutoConfigure类。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;根据 @Conditional注解的条件，进行自动配置并将Bean注入Spring Context 上下文当中。也可以使用@ImportAutoConfiguration({MyServiceAutoConfiguration.class}) 指定自动配置哪些类。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;日常使用的Spring官方的Starter一般采取spring-boot-starter-{name} 的命名方式，如 spring-boot-starter-web 。而非官方的Starter，官方建议 artifactId 命名应遵循{name}-spring-boot-starter 的格式。 例如：door-spring-boot-starter 。&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;/blockquote&gt;
&lt;h4&gt;2、为什么要自定义starter&lt;/h4&gt;
&lt;blockquote&gt;
&lt;p&gt;在我们的日常开发工作中，经常会有一些独立于业务之外的配置模块，我们经常将其放到一个特定的包下，然后如果另一个工程需要复用这块功能的时候，需要将代码硬拷贝到另一个工程，重新集成一遍，麻烦至极。如果我们将这些可独立于业务代码之外的功配置模块封装成一个个starter，复用的时候只需要将其在pom中引用依赖即可，SpringBoot为我们完成自动装配，简直不要太爽。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h4&gt;3、自定义starter的案例&lt;/h4&gt;
&lt;p&gt;以下案例由笔者工作中遇到的部分场景&lt;/p&gt;
&lt;p&gt;　　▲ 动态数据源。&lt;/p&gt;
&lt;p&gt;　　▲ 登录模块。 结合springmvc的interceptor来做，参数保存和请求时间计算&lt;/p&gt;
&lt;p&gt;　　▲ 基于AOP技术实现日志切面。&lt;/p&gt;
&lt;h4&gt;4、自定义starter的命名规则&lt;/h4&gt;
&lt;blockquote&gt;
&lt;p&gt;SpringBoot提供的starter以&lt;code&gt;spring-boot-starter-xxx&lt;/code&gt;的方式命名的。&lt;/p&gt;
&lt;p&gt;官方建议自定义的starter使用&lt;code&gt;xxx-spring-boot-starter&lt;/code&gt;命名规则。以区分SpringBoot生态提供的starter。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h4&gt;5、基础依赖&lt;/h4&gt;
&lt;pre&gt;&lt;code&gt;  &amp;lt;!--引入spring-boot-starter；所有starter的基本配置--&amp;gt;
  &amp;lt;dependency&amp;gt;
     &amp;lt;groupId&amp;gt;org.springframework.boot&amp;lt;/groupId&amp;gt;
     &amp;lt;artifactId&amp;gt;spring-boot-starter&amp;lt;/artifactId&amp;gt;
  &amp;lt;/dependency&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;h4&gt;module模块规范&lt;/h4&gt;
&lt;p&gt;官方对自定义starter module模块做了规范，模块主要包括Autoconfigure module、starter module。&lt;/p&gt;
&lt;p&gt;两者的主要作用描述如下所示：&lt;/p&gt;
&lt;p&gt;Autoconfigure module自动配置module包含一切启动所需要的库、配置是Starter。&lt;/p&gt;
&lt;p&gt;starter module 代码内容为空，主要用来管理依赖，引用autoconfigure模块。供其他人引用。&lt;/p&gt;
&lt;p&gt;注意：如果管理依赖和自动配置无需分开，可以将两者合并到一起，只用一个spring boot项目即可。&lt;/p&gt;
&lt;h4&gt;主要涉及到注解&lt;/h4&gt;
&lt;p&gt;@Configuration:本注解是一个Spring Framework自定义配置注解，相当于Spring XML 配置文件的替代方案。主要作用是配置管理各种所需要的spring bean。&lt;/p&gt;
&lt;p&gt;@EnableConfigurationProperties和@ConfigurationProperties将外置spring 配置文件application.yml或application.properties关联到相关的java 对象上；配置application.yml或application.properties,可以通过java 对象操作配置文件中的值。&lt;/p&gt;
&lt;p&gt;@ConditionalOnClass和@ConditionalOnMissingBean是两个条件化配置注解，只有在某个条件满足时，才可以去创建bean。&lt;/p&gt;
&lt;p&gt;@ConditionalOnClass主要用于类路径上，满足条件某个条件才创建响应的bean；@ConditionalOnMissingBean只有当bean不存在时才创建bean；&lt;/p&gt;
&lt;h4&gt;自定义配置类自动加载配置&lt;/h4&gt;
&lt;p&gt;将自己开发的配置类，在META-INF/spring.factories(spring-boot 2.7 以前版本)中进行指定，然后spring boot启动时会自动去加载此配置类。spring boot 根据文件中配置的org.springframework.boot.autoconfigure.EnableAutoConfiguration的key进行自定义配置类的查找。实例如下：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;org.springframework.boot.autoconfigure.EnableAutoConfiguration=\
com.mycorp.libx.autoconfigure.LibXAutoConfiguration,\
com.mycorp.libx.autoconfigure.LibXWebAutoConfiguration
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;spring boot 2.7即以后版本使用/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports进行自动配置类配置，文件内容为类名。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;com.mycorp.libx.autoconfigure.LibXAutoConfiguration,\
com.mycorp.libx.autoconfigure.LibXWebAutoConfiguration 
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;自定义starter(简易好用)&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;自动配置类要能加载
将需要启动就加载的自动配置类，配置在META-INF/spring.factories
org.springframework.boot.autoconfigure.EnableAutoConfiguration=\
org.springframework.boot.autoconfigure.admin.SpringApplicationAdminJmxAutoConfiguration,\
org.springframework.boot.autoconfigure.aop.AopAutoConfiguration,\
&lt;/code&gt;&lt;/pre&gt;
&lt;h4&gt;1、新建一个工程&lt;/h4&gt;
&lt;p&gt;&lt;img src=&quot;https://tangtangit.com/_astro/image-20220122111012048.BbK_D7-c_Z29raTG.webp&quot; alt=&quot;image-20220122111012048&quot; /&gt;&lt;/p&gt;
&lt;h4&gt;2、pom依赖&lt;/h4&gt;
&lt;pre&gt;&lt;code&gt;&amp;lt;?xml version=&quot;1.0&quot; encoding=&quot;UTF-8&quot;?&amp;gt;
&amp;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;&amp;gt;
    &amp;lt;modelVersion&amp;gt;4.0.0&amp;lt;/modelVersion&amp;gt;
    &amp;lt;groupId&amp;gt;com.zy&amp;lt;/groupId&amp;gt;
    &amp;lt;artifactId&amp;gt;oss-spring-boot-starter&amp;lt;/artifactId&amp;gt;
    &amp;lt;version&amp;gt;0.0.1-SNAPSHOT&amp;lt;/version&amp;gt;
    &amp;lt;name&amp;gt;oss-spring-boot-starter&amp;lt;/name&amp;gt;
    &amp;lt;description&amp;gt;Demo project for Spring Boot&amp;lt;/description&amp;gt;

    &amp;lt;properties&amp;gt;
        &amp;lt;java.version&amp;gt;1.8&amp;lt;/java.version&amp;gt;
        &amp;lt;project.build.sourceEncoding&amp;gt;UTF-8&amp;lt;/project.build.sourceEncoding&amp;gt;
        &amp;lt;project.reporting.outputEncoding&amp;gt;UTF-8&amp;lt;/project.reporting.outputEncoding&amp;gt;
        &amp;lt;spring-boot.version&amp;gt;2.3.7.RELEASE&amp;lt;/spring-boot.version&amp;gt;
    &amp;lt;/properties&amp;gt;

    &amp;lt;dependencies&amp;gt;
        &amp;lt;!--引入spring-boot-starter；所有starter的基本配置--&amp;gt;
        &amp;lt;dependency&amp;gt;
            &amp;lt;groupId&amp;gt;org.springframework.boot&amp;lt;/groupId&amp;gt;
            &amp;lt;artifactId&amp;gt;spring-boot-starter&amp;lt;/artifactId&amp;gt;
        &amp;lt;/dependency&amp;gt;

        &amp;lt;!--aop依赖--&amp;gt;
        &amp;lt;dependency&amp;gt;
            &amp;lt;groupId&amp;gt;org.springframework.boot&amp;lt;/groupId&amp;gt;
            &amp;lt;artifactId&amp;gt;spring-boot-starter-aop&amp;lt;/artifactId&amp;gt;
        &amp;lt;/dependency&amp;gt;

        &amp;lt;dependency&amp;gt;
            &amp;lt;groupId&amp;gt;org.projectlombok&amp;lt;/groupId&amp;gt;
            &amp;lt;artifactId&amp;gt;lombok&amp;lt;/artifactId&amp;gt;
            &amp;lt;optional&amp;gt;true&amp;lt;/optional&amp;gt;
        &amp;lt;/dependency&amp;gt;
        &amp;lt;dependency&amp;gt;
            &amp;lt;groupId&amp;gt;org.springframework.boot&amp;lt;/groupId&amp;gt;
            &amp;lt;artifactId&amp;gt;spring-boot-starter-test&amp;lt;/artifactId&amp;gt;
            &amp;lt;scope&amp;gt;test&amp;lt;/scope&amp;gt;
            &amp;lt;exclusions&amp;gt;
                &amp;lt;exclusion&amp;gt;
                    &amp;lt;groupId&amp;gt;org.junit.vintage&amp;lt;/groupId&amp;gt;
                    &amp;lt;artifactId&amp;gt;junit-vintage-engine&amp;lt;/artifactId&amp;gt;
                &amp;lt;/exclusion&amp;gt;
            &amp;lt;/exclusions&amp;gt;
        &amp;lt;/dependency&amp;gt;
    &amp;lt;/dependencies&amp;gt;

    &amp;lt;dependencyManagement&amp;gt;
        &amp;lt;dependencies&amp;gt;
            &amp;lt;dependency&amp;gt;
                &amp;lt;groupId&amp;gt;org.springframework.boot&amp;lt;/groupId&amp;gt;
                &amp;lt;artifactId&amp;gt;spring-boot-dependencies&amp;lt;/artifactId&amp;gt;
                &amp;lt;version&amp;gt;${spring-boot.version}&amp;lt;/version&amp;gt;
                &amp;lt;type&amp;gt;pom&amp;lt;/type&amp;gt;
                &amp;lt;scope&amp;gt;import&amp;lt;/scope&amp;gt;
            &amp;lt;/dependency&amp;gt;
        &amp;lt;/dependencies&amp;gt;
    &amp;lt;/dependencyManagement&amp;gt;

&amp;lt;/project&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;h4&gt;3、自定义注解&lt;/h4&gt;
&lt;pre&gt;&lt;code&gt;@Retention(RetentionPolicy.RUNTIME) //在runtime时运行
@Target({ElementType.METHOD,ElementType.TYPE})//作用在方法和类上
public @interface TestAnnotation {

    /**
     * 描述信息
     * @return 信息
     */
    String des();
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h4&gt;4、aop编写&lt;/h4&gt;
&lt;pre&gt;&lt;code&gt;package com.zy.ossspringbootstarter.aop;

import com.zy.ossspringbootstarter.annotation.TestAnnotation;
import lombok.extern.slf4j.Slf4j;
import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.Signature;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Before;
import org.aspectj.lang.annotation.Pointcut;
import org.aspectj.lang.reflect.MethodSignature;
import org.springframework.stereotype.Component;

@Slf4j
@Aspect
@Component
public class MyAop {


    //切入点
    @Pointcut(value = &quot;@annotation(com.zy.ossspringbootstarter.annotation.TestAnnotation)&quot;)
    public void pointCut(){};


    @Before(value = &quot;pointCut()&quot;)
    public static void testBefore(JoinPoint joinPoint){
        log.info(&quot;测试前置通知&quot;);
        //获取签名
        Signature signature = joinPoint.getSignature();
        //获取强转为方法签名
        MethodSignature methodSignature = (MethodSignature) signature;
        //获取方法上的注解
        TestAnnotation annotation = methodSignature.getMethod().getAnnotation(TestAnnotation.class);
        log.info(&quot;注解desc,{}&quot;,annotation.des());
    }

}
&lt;/code&gt;&lt;/pre&gt;
&lt;h4&gt;5、定义一个配置类&lt;/h4&gt;
&lt;pre&gt;&lt;code&gt;@Configuration
@ConditionalOnWebApplication  //web应用才生效
public class OssAutoConfiger {

    //可以注入bean等
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h4&gt;6、加载配置类&lt;/h4&gt;
&lt;p&gt;在resource文件下，新建META-INF文件夹，然后创建spring.factories文件&lt;/p&gt;
&lt;p&gt;在该文件中加入如下配置，该配置指定上步骤中定义的配置类为自动装配的配置。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;org.springframework.boot.autoconfigure.EnableAutoConfiguration=com.zy.ossspringbootstarter.OssAutoConfiger
&lt;/code&gt;&lt;/pre&gt;
&lt;h4&gt;7、安装到本地仓库&lt;/h4&gt;
&lt;p&gt;在工程中执行mvn clean install 一个自定义的starter新鲜出炉。会将jar包导入本地仓库&lt;/p&gt;
&lt;h4&gt;8、引用&lt;/h4&gt;
&lt;p&gt;在需要用的项目中引用&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;    &amp;lt;dependencies&amp;gt;
&amp;lt;!--        自定义starter--&amp;gt;
        &amp;lt;dependency&amp;gt;
            &amp;lt;groupId&amp;gt;com.zy&amp;lt;/groupId&amp;gt;
            &amp;lt;artifactId&amp;gt;oss-spring-boot-starter&amp;lt;/artifactId&amp;gt;
            &amp;lt;version&amp;gt;0.0.1-SNAPSHOT&amp;lt;/version&amp;gt;
        &amp;lt;/dependency&amp;gt;
    &amp;lt;/dependencies&amp;gt;   
&lt;/code&gt;&lt;/pre&gt;
&lt;h4&gt;9、测试&lt;/h4&gt;
&lt;p&gt;在一个controller上安装&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;    @TestAnnotation(des = &quot;看我&quot;)
    //获取代办事项列表
    @ApiOperation(&quot;获取代办事项列表&quot;)
    @ResponseBody
    @GetMapping(&quot;/getTodoList&quot;)
    public R getTodoList(
            @RequestParam Integer currPage,
            @RequestParam Integer pageSize
    ) {
        log.info(&quot;getTodoList&quot;);
        int start = (currPage - 1) * pageSize;
        int stop = (currPage * pageSize) - 1;
        User user = HttpContextUtils.getUser();
        List&amp;lt;Object&amp;gt; list = redisTemplate.opsForList().range(&quot;todo:&quot; + user.getId(), start, stop);
        return R.ok(list);
    }
&lt;/code&gt;&lt;/pre&gt;
&lt;h4&gt;10、结果&lt;/h4&gt;
&lt;p&gt;&lt;img src=&quot;https://tangtangit.com/_astro/image-20220122111945914.CklVUeZ9_28LCov.webp&quot; alt=&quot;image-20220122111945914&quot; /&gt;&lt;/p&gt;
&lt;h3&gt;自定义starter标准&lt;/h3&gt;
&lt;p&gt;本demo实现一下openai自动配置starter，主要通过集成openai api 自动配置openaiservice，获取openaiservice对象。具体实现如下：&lt;/p&gt;
&lt;h4&gt;新建多模块项目&lt;/h4&gt;
&lt;p&gt;新建一个openai-freedom-demo项目，里边包含两个模块，一个模块为openai-freedom-spring-boot-autoconfigure，主要用来自动配置openaiservice bean，一个为openai-freedom-spring-boot-starter，主要用来管理依赖。将openai-freedom-spring-boot-autoconfigure依赖添加到starter模块中。其他第三方引用你开发的jar时，只需引用starter模块即可，无需再引用autoconfigure模块。&lt;/p&gt;
&lt;p&gt;下边为目录结构截图：&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://tangtangit.com/_astro/image-20231203132205913.BkloLIEH_A6439.webp&quot; alt=&quot;image-20231203132205913&quot; /&gt;&lt;/p&gt;
&lt;h4&gt;父pom.xml内容&lt;/h4&gt;
&lt;pre&gt;&lt;code&gt;&amp;lt;?xml version=&quot;1.0&quot; encoding=&quot;UTF-8&quot;?&amp;gt;
&amp;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;&amp;gt;
   &amp;lt;modelVersion&amp;gt;4.0.0&amp;lt;/modelVersion&amp;gt;
   &amp;lt;groupId&amp;gt;com.freedom&amp;lt;/groupId&amp;gt;
   &amp;lt;artifactId&amp;gt;openai-freedom-demo&amp;lt;/artifactId&amp;gt;
   &amp;lt;version&amp;gt;0.0.1-SNAPSHOT&amp;lt;/version&amp;gt;

   &amp;lt;packaging&amp;gt;pom&amp;lt;/packaging&amp;gt;
   &amp;lt;name&amp;gt;openai-freedom-demo&amp;lt;/name&amp;gt;
   &amp;lt;description&amp;gt;openai-freedom-demo&amp;lt;/description&amp;gt;
   &amp;lt;modules&amp;gt;
       &amp;lt;module&amp;gt;openai-freedom-spring-boot-autoconfigure&amp;lt;/module&amp;gt;
       &amp;lt;module&amp;gt;openai-freedom-spring-boot-starter&amp;lt;/module&amp;gt;
   &amp;lt;/modules&amp;gt;
   &amp;lt;build&amp;gt;
       &amp;lt;plugins&amp;gt;
           &amp;lt;plugin&amp;gt;
               &amp;lt;groupId&amp;gt;org.springframework.boot&amp;lt;/groupId&amp;gt;
               &amp;lt;artifactId&amp;gt;spring-boot-maven-plugin&amp;lt;/artifactId&amp;gt;
               &amp;lt;version&amp;gt;3.1.3&amp;lt;/version&amp;gt;
           &amp;lt;/plugin&amp;gt;
       &amp;lt;/plugins&amp;gt;
   &amp;lt;/build&amp;gt;
&amp;lt;/project&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;h4&gt;autoconfigure pom.xml 内容&lt;/h4&gt;
&lt;pre&gt;&lt;code&gt;&amp;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;&amp;gt;
   &amp;lt;modelVersion&amp;gt;4.0.0&amp;lt;/modelVersion&amp;gt;
   &amp;lt;parent&amp;gt;
       &amp;lt;groupId&amp;gt;com.freedom&amp;lt;/groupId&amp;gt;
       &amp;lt;artifactId&amp;gt;openai-freedom-demo&amp;lt;/artifactId&amp;gt;
       &amp;lt;version&amp;gt;0.0.1-SNAPSHOT&amp;lt;/version&amp;gt;
   &amp;lt;/parent&amp;gt;

   &amp;lt;groupId&amp;gt;com.freedom&amp;lt;/groupId&amp;gt;
   &amp;lt;version&amp;gt;0.0.1-SNAPSHOT&amp;lt;/version&amp;gt;
   &amp;lt;build&amp;gt;
       &amp;lt;plugins&amp;gt;
           &amp;lt;plugin&amp;gt;
               &amp;lt;groupId&amp;gt;org.apache.maven.plugins&amp;lt;/groupId&amp;gt;
               &amp;lt;artifactId&amp;gt;maven-compiler-plugin&amp;lt;/artifactId&amp;gt;
               &amp;lt;configuration&amp;gt;
                   &amp;lt;source&amp;gt;8&amp;lt;/source&amp;gt;
                   &amp;lt;target&amp;gt;8&amp;lt;/target&amp;gt;
               &amp;lt;/configuration&amp;gt;
           &amp;lt;/plugin&amp;gt;
       &amp;lt;/plugins&amp;gt;
   &amp;lt;/build&amp;gt;
   &amp;lt;artifactId&amp;gt;openai-freedom-spring-boot-autoconfigure&amp;lt;/artifactId&amp;gt;

   &amp;lt;name&amp;gt;openai-freedom-spring-boot-autoconfigure&amp;lt;/name&amp;gt;
   &amp;lt;url&amp;gt;http://maven.apache.org&amp;lt;/url&amp;gt;

   &amp;lt;properties&amp;gt;
       &amp;lt;project.build.sourceEncoding&amp;gt;UTF-8&amp;lt;/project.build.sourceEncoding&amp;gt;
   &amp;lt;/properties&amp;gt;

   &amp;lt;dependencies&amp;gt;
       &amp;lt;dependency&amp;gt;
           &amp;lt;groupId&amp;gt;junit&amp;lt;/groupId&amp;gt;
           &amp;lt;artifactId&amp;gt;junit&amp;lt;/artifactId&amp;gt;
           &amp;lt;version&amp;gt;3.8.1&amp;lt;/version&amp;gt;
           &amp;lt;scope&amp;gt;test&amp;lt;/scope&amp;gt;
       &amp;lt;/dependency&amp;gt;
       &amp;lt;dependency&amp;gt;
           &amp;lt;groupId&amp;gt;org.springframework.boot&amp;lt;/groupId&amp;gt;
           &amp;lt;artifactId&amp;gt;spring-boot-autoconfigure&amp;lt;/artifactId&amp;gt;
           &amp;lt;version&amp;gt;3.1.2&amp;lt;/version&amp;gt;
       &amp;lt;/dependency&amp;gt;
       &amp;lt;dependency&amp;gt;
           &amp;lt;groupId&amp;gt;org.springframework.boot&amp;lt;/groupId&amp;gt;
           &amp;lt;artifactId&amp;gt;spring-boot-configuration-processor&amp;lt;/artifactId&amp;gt;
           &amp;lt;version&amp;gt;3.1.2&amp;lt;/version&amp;gt;
       &amp;lt;/dependency&amp;gt;
       &amp;lt;dependency&amp;gt;
           &amp;lt;groupId&amp;gt;org.projectlombok&amp;lt;/groupId&amp;gt;
           &amp;lt;artifactId&amp;gt;lombok&amp;lt;/artifactId&amp;gt;
           &amp;lt;version&amp;gt;1.18.28&amp;lt;/version&amp;gt;
           &amp;lt;scope&amp;gt;provided&amp;lt;/scope&amp;gt;
       &amp;lt;/dependency&amp;gt;
       &amp;lt;dependency&amp;gt;
           &amp;lt;groupId&amp;gt;com.theokanning.openai-gpt3-java&amp;lt;/groupId&amp;gt;
           &amp;lt;artifactId&amp;gt;client&amp;lt;/artifactId&amp;gt;
           &amp;lt;version&amp;gt;0.14.0&amp;lt;/version&amp;gt;
       &amp;lt;/dependency&amp;gt;
       &amp;lt;dependency&amp;gt;
           &amp;lt;groupId&amp;gt;com.theokanning.openai-gpt3-java&amp;lt;/groupId&amp;gt;
           &amp;lt;artifactId&amp;gt;api&amp;lt;/artifactId&amp;gt;
           &amp;lt;version&amp;gt;0.14.0&amp;lt;/version&amp;gt;
       &amp;lt;/dependency&amp;gt;
       &amp;lt;dependency&amp;gt;
           &amp;lt;groupId&amp;gt;com.theokanning.openai-gpt3-java&amp;lt;/groupId&amp;gt;
           &amp;lt;artifactId&amp;gt;service&amp;lt;/artifactId&amp;gt;
           &amp;lt;version&amp;gt;0.14.0&amp;lt;/version&amp;gt;
       &amp;lt;/dependency&amp;gt;
   &amp;lt;/dependencies&amp;gt;

&amp;lt;/project&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;h4&gt;starter pom.xml 配置内容&lt;/h4&gt;
&lt;pre&gt;&lt;code&gt;&amp;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;&amp;gt;
   &amp;lt;modelVersion&amp;gt;4.0.0&amp;lt;/modelVersion&amp;gt;
   &amp;lt;parent&amp;gt;
       &amp;lt;groupId&amp;gt;com.freedom&amp;lt;/groupId&amp;gt;
       &amp;lt;artifactId&amp;gt;openai-freedom-demo&amp;lt;/artifactId&amp;gt;
       &amp;lt;version&amp;gt;0.0.1-SNAPSHOT&amp;lt;/version&amp;gt;
   &amp;lt;/parent&amp;gt;
   &amp;lt;groupId&amp;gt;com.freedom&amp;lt;/groupId&amp;gt;
   &amp;lt;artifactId&amp;gt;openai-freedom-spring-boot-starter&amp;lt;/artifactId&amp;gt;
   &amp;lt;version&amp;gt;0.0.1-SNAPSHOT&amp;lt;/version&amp;gt;
   &amp;lt;name&amp;gt;openai-freedom-spring-boot-starter&amp;lt;/name&amp;gt;
   &amp;lt;url&amp;gt;http://maven.apache.org&amp;lt;/url&amp;gt;

   &amp;lt;properties&amp;gt;
       &amp;lt;project.build.sourceEncoding&amp;gt;UTF-8&amp;lt;/project.build.sourceEncoding&amp;gt;
   &amp;lt;/properties&amp;gt;

   &amp;lt;dependencies&amp;gt;
       &amp;lt;dependency&amp;gt;
           &amp;lt;groupId&amp;gt;com.freedom&amp;lt;/groupId&amp;gt;
           &amp;lt;artifactId&amp;gt;openai-freedom-spring-boot-autoconfigure&amp;lt;/artifactId&amp;gt;
           &amp;lt;version&amp;gt;0.0.1-SNAPSHOT&amp;lt;/version&amp;gt;
       &amp;lt;/dependency&amp;gt;
   &amp;lt;/dependencies&amp;gt;
&amp;lt;/project&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;h4&gt;配置openai自动配置类&lt;/h4&gt;
&lt;p&gt;OpenAiProperties类，主要用来将application.yml中的配置项与java 实体进行映射。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;/**
* OpenAi配置参数
*
* @author wangshihai
* @date 2023/07/23 18:07
**/
@ConfigurationProperties(prefix = &quot;openai&quot;)
@Data
public class OpenAiProperties {
   /**
    * openai token
    */
   private String token = &quot;&quot;;
   /**
    * connect timeout, default 1000ms
    */
   private int timeout = 1000;

   /**
    * connect openai http proxy host
    */
   private String host = &quot;127.0.0.1&quot;;
   /**
    * connect openai http proxy port
    */
   private int port = 1080;

   /**
    * if open,value is true; defalut value false
    */
   private boolean proxyEnable = false;
} 
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;OpenAiAutoconfigure类，用来对openaiservice对象进行配置，并初始化。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;/**
* OpenAI自动配置类
*
* @author wangshihai
* @date 2023/07/23 18:30
**/
@Configuration
@EnableConfigurationProperties(OpenAiProperties.class)
public class OpenAIAutoConfigure {

   @Bean
   @ConditionalOnMissingBean
   public OpenAiService getOpenAiService(OpenAiProperties openAiProperties) {
       System.out.println(&quot;init config&quot;);
       ObjectMapper mapper = defaultObjectMapper();
       OkHttpClient client = null;
       if (openAiProperties.isProxyEnable()) {
           Proxy proxy = new Proxy(Proxy.Type.HTTP, new InetSocketAddress(&quot;127.0.0.1&quot;, 1080));
           client = defaultClient(openAiProperties.getToken(), Duration.ofMinutes(openAiProperties.getTimeout())).newBuilder().proxy(proxy).build();
       } else {
           client = defaultClient(openAiProperties.getToken(), Duration.ofMinutes(openAiProperties.getTimeout())).newBuilder().build();
       }

       Retrofit retrofit = defaultRetrofit(client, mapper);
       OpenAiApi api = retrofit.create(OpenAiApi.class);
       return new OpenAiService(api);
   }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;spring api 自动配置加载OpenAiAutoconfigure&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;spring boot 2.7 版本之前,使用META-INF/spring.factories配置&lt;/p&gt;
&lt;p&gt;org.springframework.boot.autoconfigure.EnableAutoConfiguration=&lt;br /&gt;
com.freedom.OpenAIAutoConfigure&lt;/p&gt;
&lt;p&gt;spring boot 2.7 版本即之后,使用/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports配置&lt;/p&gt;
&lt;p&gt;com.freedom.OpenAIAutoConfigure&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;两种情况可以同时存在于你的starter中，这样你就可以兼容两个版本的自定义配置starter&lt;/p&gt;
&lt;p&gt;application.properties自动提示说明
如果想让自定义openai的配置参数在application.propeties中，输入前缀后能够自动提示，可以添加以下依赖解决。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&amp;lt;dependency&amp;gt;
   &amp;lt;groupId&amp;gt;org.springframework.boot&amp;lt;/groupId&amp;gt;
   &amp;lt;artifactId&amp;gt;spring-boot-configuration-processor&amp;lt;/artifactId&amp;gt;
   &amp;lt;version&amp;gt;3.1.2&amp;lt;/version&amp;gt;
&amp;lt;/dependency&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;application.properties 应用所有参数从那找到说明&lt;/p&gt;
&lt;p&gt;针对平时我们在配置其他人写的starter时，可以通过加载jar中找到对应的properties类看里边包含的属性有那些，每个值的含义是什么以及如何配置。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://tangtangit.com/_astro/image-20231203132406874.DhBUOw0k_Z19xuXR.webp&quot; alt=&quot;image-20231203132406874&quot; /&gt;&lt;/p&gt;
&lt;h4&gt;测试自定义openai starter&lt;/h4&gt;
&lt;p&gt;新建一个maven项目&lt;/p&gt;
&lt;p&gt;引入maven 坐标&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&amp;lt;?xml version=&quot;1.0&quot; encoding=&quot;UTF-8&quot;?&amp;gt;
&amp;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;&amp;gt;
   &amp;lt;modelVersion&amp;gt;4.0.0&amp;lt;/modelVersion&amp;gt;
   &amp;lt;parent&amp;gt;
       &amp;lt;groupId&amp;gt;org.springframework.boot&amp;lt;/groupId&amp;gt;
       &amp;lt;artifactId&amp;gt;spring-boot-starter-parent&amp;lt;/artifactId&amp;gt;
       &amp;lt;version&amp;gt;3.1.3&amp;lt;/version&amp;gt;
       &amp;lt;relativePath/&amp;gt; &amp;lt;!-- lookup parent from repository --&amp;gt;
   &amp;lt;/parent&amp;gt;
   &amp;lt;groupId&amp;gt;com.example&amp;lt;/groupId&amp;gt;
   &amp;lt;artifactId&amp;gt;demo-starter&amp;lt;/artifactId&amp;gt;
   &amp;lt;version&amp;gt;0.0.1-SNAPSHOT&amp;lt;/version&amp;gt;
   &amp;lt;name&amp;gt;demo-starter&amp;lt;/name&amp;gt;
   &amp;lt;description&amp;gt;demo-starter&amp;lt;/description&amp;gt;
   &amp;lt;properties&amp;gt;
       &amp;lt;java.version&amp;gt;17&amp;lt;/java.version&amp;gt;
   &amp;lt;/properties&amp;gt;
   &amp;lt;dependencies&amp;gt;
       &amp;lt;dependency&amp;gt;
           &amp;lt;groupId&amp;gt;org.springframework.boot&amp;lt;/groupId&amp;gt;
           &amp;lt;artifactId&amp;gt;spring-boot-starter-test&amp;lt;/artifactId&amp;gt;
           &amp;lt;scope&amp;gt;test&amp;lt;/scope&amp;gt;
       &amp;lt;/dependency&amp;gt;
       &amp;lt;dependency&amp;gt;
           &amp;lt;groupId&amp;gt;com.freedom&amp;lt;/groupId&amp;gt;
           &amp;lt;artifactId&amp;gt;openai-freedom-spring-boot-starter&amp;lt;/artifactId&amp;gt;
           &amp;lt;version&amp;gt;0.0.1-SNAPSHOT&amp;lt;/version&amp;gt;
       &amp;lt;/dependency&amp;gt;
   &amp;lt;/dependencies&amp;gt;

   &amp;lt;build&amp;gt;
       &amp;lt;plugins&amp;gt;
           &amp;lt;plugin&amp;gt;
               &amp;lt;groupId&amp;gt;org.springframework.boot&amp;lt;/groupId&amp;gt;
               &amp;lt;artifactId&amp;gt;spring-boot-maven-plugin&amp;lt;/artifactId&amp;gt;
           &amp;lt;/plugin&amp;gt;
       &amp;lt;/plugins&amp;gt;
   &amp;lt;/build&amp;gt;

&amp;lt;/project&amp;gt; 
&lt;/code&gt;&lt;/pre&gt;
&lt;h4&gt;编写测试类&lt;/h4&gt;
&lt;pre&gt;&lt;code&gt;@SpringBootApplication
public class DemoStarterApplication {
   @Autowired
   private OpenAiService aiService;
   public static void main(String[] args) {
       SpringApplication.run(DemoStarterApplication.class, args);
   }

   @Bean
   public void getMyBean() {
       final List&amp;lt;ChatMessage&amp;gt; messages = new ArrayList&amp;lt;&amp;gt;();
       final ChatMessage systemMessage;
       try {
           systemMessage = new ChatMessage(ChatMessageRole.SYSTEM.value(), URLDecoder.decode(&quot;你好&quot;, &quot;UTF-8&quot;), &quot;xiaopang&quot;);
       } catch (UnsupportedEncodingException e) {
           throw new RuntimeException(e);
       }

       messages.add(systemMessage);
       ChatCompletionRequest chatCompletionRequest = ChatCompletionRequest
               .builder()
               .model(&quot;gpt-3.5-turbo&quot;)
               .messages(messages)
               .n(1)
               .maxTokens(50)
               .logitBias(new HashMap&amp;lt;&amp;gt;())
               .build();
       ChatMessage responseMessage = aiService.createChatCompletion(chatCompletionRequest).getChoices().get(0).getMessage();
       System.out.println(responseMessage);
   } 
&lt;/code&gt;&lt;/pre&gt;
&lt;h4&gt;测试&lt;/h4&gt;
&lt;p&gt;&lt;img src=&quot;https://tangtangit.com/_astro/image-20231203133601701.TQv3ga9f_TchcH.webp&quot; alt=&quot;image-20231203133601701&quot; /&gt;&lt;/p&gt;
&lt;p&gt;能够输出打印即表示自动加载成功，对于openai 代理错误问题，可以通过配置代理自行解决。&lt;/p&gt;
&lt;p&gt;作者：step_freedom https://www.bilibili.com/read/cv26411566/ 出处：bilibili&lt;/p&gt;
&lt;h2&gt;更多SpringBoot整合示例&lt;/h2&gt;
&lt;p&gt;https://github.com/spring-projects/spring-boot/tree/master/spring-boot-samples&lt;/p&gt;
</content:encoded></item><item><title>Spring核心：注解原理与手写Spring框架</title><link>https://tangtangit.com/posts/spring%E7%94%9F%E6%80%81-01-spring/</link><guid isPermaLink="true">https://tangtangit.com/posts/spring%E7%94%9F%E6%80%81-01-spring/</guid><description>深入剖析Spring注解体系（@Component/@Autowired/@Configuration等）及其底层原理，并通过手写简易Spring框架理解IOC和AOP的实现机制。</description><pubDate>Mon, 02 Feb 2026 00:00:00 GMT</pubDate><content:encoded>&lt;h2&gt;spring注解加原理&lt;/h2&gt;
&lt;h2&gt;spring源码及其收获（查看源码讲解xmd）&lt;/h2&gt;
&lt;h3&gt;1、Spring容器中的Bean是何时创建的？为什么我们能从容器中获取到？&lt;/h3&gt;
&lt;p&gt;结论&lt;/p&gt;
&lt;blockquote&gt;
&lt;ul&gt;
&lt;li&gt;非懒加载的单实例bean会在容器创建的时候创建。容器内部会创建一个beanFactory，使用beanFactory的doGetBean方法来进行创建，并且在创建后会把bean放入一个单例bean的map集合（singletonObjects）中存储。key就是我们配置的bean的名称。&lt;/li&gt;
&lt;li&gt;所以我们调用容器的getBean方法来获取对象的时候，其实他也是调用了doGetBean方法。就会从对应的集合中获取到之前创建的对象。&lt;/li&gt;
&lt;/ul&gt;
&lt;/blockquote&gt;
&lt;h3&gt;2、BeanDefinition是什么？是什么时候创建的？&lt;/h3&gt;
&lt;p&gt;结论：&lt;/p&gt;
&lt;blockquote&gt;
&lt;ul&gt;
&lt;li&gt;在容器创建时会先去创建一个beanFactory，然后使用XmlBeanDefinitionReader去读取xml配置文件，把里面的标签进行解析，然后创建BeanDefinition对象来存放bean标签中的各个属性的值。所以BeanDefinition相当于就是保存了bean的定义信息的对象。&lt;/li&gt;
&lt;li&gt;BeanDefinition被创建后会被存入beanFactoty的beanDefinitionMap集合和beanDefinitionNames集合中&lt;/li&gt;
&lt;/ul&gt;
&lt;/blockquote&gt;
&lt;h3&gt;3、探究SpringBoot项目中的Spring容器是在什么时候创建和刷新的?&lt;/h3&gt;
&lt;blockquote&gt;
&lt;p&gt;在启动类中调用SpringApplication的run方法时会根据容器的类型创建不同的容器对象，并且调用容器的refresh方法&lt;/p&gt;
&lt;p&gt;createApplicationContext 方法创建的是系统自带的类&lt;/p&gt;
&lt;p&gt;refresh方法 会将用户的类创建，并存入容器&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h3&gt;4、@Import注解&lt;/h3&gt;
&lt;p&gt;作用：&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;如果我们把所有的配置都写在一个配置类中不便于我们进行管理。所以Spring也支持我们编写多个配置类，只要使用@Impory注解引入其他配置类即可。作用相当于&amp;lt;import&amp;gt;标签，可以跨包使用&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;探究@Import注解为什么能引入配置类：&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;ConfigurationClassParser的doProcessConfigurationClass方法在处理启动类的时候会先&lt;strong&gt;递归&lt;/strong&gt;扫描其上所有的@Import注解，获取其中的的value属性值添加到集合中。然后获取遍历这个集合判断他们是ImportSelector的子类还是ImportBeanDefinitionRegistrar的子类还是配置类。如果是前面两种情况都不满足就按照配置类来出来。会继续调用processConfigurationClass(该类用于解析@Configuration注解的)方法来解析这个导入进来的配置类。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h3&gt;5、ImportSelector&lt;/h3&gt;
&lt;p&gt;作用：&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;可以根据字符串数组（数组元素为类的全类名）来批量的加载指定的bean。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;探究@Import注解引入ImportSelect是如何生效的:&lt;/p&gt;
&lt;blockquote&gt;
&lt;ul&gt;
&lt;li&gt;ConfigurationClassParser的doProcessConfigurationClass方法在处理启动类的时候会先递归扫描其上所有的@Import注解，获取其中的的value属性值添加到集合中。然后获取遍历这个集合判断他们是ImportSelector的子类还是ImportBeanDefinitionRegistrar的子类还是配置类。如果ImportSelector则会去通过反射创建其对象，调用其方法获取字符串数组，封装成SourceClass对象。然后递归调用这些处理import的方法，对这些期望导入的类做同样的处理。&lt;/li&gt;
&lt;li&gt;如果导入的不是ImportSelector或者ImportBeanDefinitionRegistrar，也会被当做是配置类调用processConfigurationClass进行处理。这个方法中会把它们添加到一个配置类的集合中（configurationClasses）。后面会把配置类集合中的对象加载成BeanDefinition对象。&lt;/li&gt;
&lt;li&gt;可以使用ImportSelect导入一些配置类。因为在处理过程中会把他们当作配置类类进行解析处理。----作用之一&lt;/li&gt;
&lt;/ul&gt;
&lt;/blockquote&gt;
&lt;h3&gt;6、ImportBeanDefinitionRegistrar&lt;/h3&gt;
&lt;p&gt;应用场景：&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;如要要实现动态Bean的装载可以使用ImportBeanDefinitionRegistrar。尤其是如果想装载动态代理对象的时候。例如Mybatis的启动器就是使用了它实现了Mapper接口的代理对象装载的。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h3&gt;7、探究SpringBoot项目中我们没有设置组件扫描的包为什么它会默认扫描启动类所在的包&lt;/h3&gt;
&lt;p&gt;结论：&lt;/p&gt;
&lt;blockquote&gt;
&lt;ul&gt;
&lt;li&gt;在容器刷新时会调用BeanFactoryPostProcessor(Bean工厂后置处理器)进行处理。其中就有一个ConfigurationClassPostProcessor（配置类处理器）。在这个处理器中使用ConfigurationClassParser（配置类解析器）的parse方法去解析处理我们的配置类，其中就有对ComponentScan注解的解析处理。会去使用ComponentScanAnnotationParser的parse方法进行解析。解析时如果发现没有配置basePackage，它会去获取我们加了注解的这个类所在的包，作为我们的basepackage进行组件扫描。&lt;/li&gt;
&lt;li&gt;ConfigurationClassParser中除了对组件扫描会进行处理，还对@PropertySource，@Import，@ImportResource，@Bean等注解进行处理&lt;/li&gt;
&lt;/ul&gt;
&lt;/blockquote&gt;
&lt;h2&gt;《spring注解及其原理》&lt;/h2&gt;
&lt;p&gt;准备工作&lt;/p&gt;
&lt;p&gt;pom.xml&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&amp;lt;?xml version=&quot;1.0&quot; encoding=&quot;UTF-8&quot;?&amp;gt;
&amp;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;&amp;gt;
    &amp;lt;modelVersion&amp;gt;4.0.0&amp;lt;/modelVersion&amp;gt;

    &amp;lt;groupId&amp;gt;org.example&amp;lt;/groupId&amp;gt;
    &amp;lt;artifactId&amp;gt;spring_annotation&amp;lt;/artifactId&amp;gt;
    &amp;lt;version&amp;gt;1.0-SNAPSHOT&amp;lt;/version&amp;gt;
    
    &amp;lt;properties&amp;gt;
        &amp;lt;maven.compiler.source&amp;gt;8&amp;lt;/maven.compiler.source&amp;gt;
        &amp;lt;maven.compiler.target&amp;gt;8&amp;lt;/maven.compiler.target&amp;gt;
    &amp;lt;/properties&amp;gt;
    &amp;lt;dependencies&amp;gt;
        &amp;lt;!-- lombok --&amp;gt;
        &amp;lt;dependency&amp;gt;
            &amp;lt;groupId&amp;gt;org.projectlombok&amp;lt;/groupId&amp;gt;
            &amp;lt;artifactId&amp;gt;lombok&amp;lt;/artifactId&amp;gt;
            &amp;lt;version&amp;gt;1.18.16&amp;lt;/version&amp;gt;
            &amp;lt;scope&amp;gt;provided&amp;lt;/scope&amp;gt;
        &amp;lt;/dependency&amp;gt;
        &amp;lt;!--   用于单元测试     --&amp;gt;
        &amp;lt;dependency&amp;gt;
            &amp;lt;groupId&amp;gt;junit&amp;lt;/groupId&amp;gt;
            &amp;lt;artifactId&amp;gt;junit&amp;lt;/artifactId&amp;gt;
            &amp;lt;version&amp;gt;4.12&amp;lt;/version&amp;gt;
            &amp;lt;scope&amp;gt;test&amp;lt;/scope&amp;gt;
        &amp;lt;/dependency&amp;gt;
        &amp;lt;!-- spring --&amp;gt;
        &amp;lt;dependency&amp;gt;
            &amp;lt;groupId&amp;gt;org.springframework&amp;lt;/groupId&amp;gt;
            &amp;lt;artifactId&amp;gt;spring-context&amp;lt;/artifactId&amp;gt;
            &amp;lt;version&amp;gt;5.3.4&amp;lt;/version&amp;gt;
        &amp;lt;/dependency&amp;gt;
    &amp;lt;/dependencies&amp;gt;
&amp;lt;/project&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;容器组件&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;给容器中添加组件&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;1）、包扫描+组件标注注解（@controller/@service/@repository/@component）[自己写的]&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;2）、@Bean【导入的第三方包里面的组件】&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;3）、@Import[快速的给容器中导入一个组件]&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;pre&gt;&lt;code&gt; ​              @Import(要导入容器的组件)：容器就会自动注册这个组件，id默认是全类名
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;pre&gt;&lt;code&gt; ​              ImportSelector  返回需要注册到容器中的组件的全类名数组
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;pre&gt;&lt;code&gt; ​              ImportBeanDefinitionRegistrar：手动注册bean到容器中
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;pre&gt;&lt;code&gt; 4）、使用spring提供的FactoryBean(工厂bean)
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;@Configuration&lt;/h3&gt;
&lt;blockquote&gt;
&lt;p&gt;告诉spring这是一个配置类&lt;/p&gt;
&lt;/blockquote&gt;
&lt;pre&gt;&lt;code&gt;//配置类==配置文件
@Configuration
public class Mainconfig {

    //给容器注册一个bean；类型为返回值类型；ID是默认方法名作为ID
    @Bean
    public Person person01(){
        return new Person(&quot;lisi&quot;,20);
    }

}
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;@Bean&lt;/h3&gt;
&lt;blockquote&gt;
&lt;p&gt;给容器注册一个bean；类型为返回值类型；ID是默认方法名作为ID&lt;/p&gt;
&lt;p&gt;不用被扫描到，自己就会注入到ioc容器中去&lt;/p&gt;
&lt;p&gt;默认是  &lt;strong&gt;单实例的&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;@Bean(&quot;填入为bean指定名称&quot;)&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h3&gt;AnnotationConfigApplicationContext&lt;/h3&gt;
&lt;blockquote&gt;
&lt;p&gt;注解配置应用上下文&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;&lt;img src=&quot;https://tangtangit.com/_astro/image-20210320105655270.DeYr-CMi_EWXvA.webp&quot; alt=&quot;image-20210320105655270&quot; /&gt;&lt;/p&gt;
&lt;h3&gt;@ComponentScan&lt;/h3&gt;
&lt;blockquote&gt;
&lt;p&gt;扫描指定包中注解那些可以注入的bean&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;value  指定包名&lt;/p&gt;
&lt;p&gt;excludeFilters&lt;/p&gt;
&lt;p&gt;includeFilters&lt;/p&gt;
&lt;p&gt;useDefaultFilters&lt;/p&gt;
&lt;p&gt;FilterType.ANNOTATION ：按照注解&lt;/p&gt;
&lt;p&gt;FilterType.ASSIGNABLE_TYPE  ：按照指定类型
FilterType.REGEX  ：按照指定正则表达式
FilterType.CUSTOM  ：使用自定义规则&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;@ComponentScan(value = &quot;com.eim&quot;,
        //excludeFilters = @ComponentScan.Filter(type = FilterType.ANNOTATION,classes = {Controller.class})
        includeFilters = @ComponentScan.Filter(type = FilterType.ANNOTATION,classes = {Controller.class}),
        useDefaultFilters = false
)
//@ComponentScan value:指定要扫描的包
//@ComponentScan注解中有@Repeatable(ComponentScans.class)，表明可以重复使用
//excludeFilters = Filter[]: 指定扫描时按什么规则排除那些组件
//includeFilters = Filter[]: 指定扫描时按什么规则包含那些组件,生效需要：useDefaultFilters = false
//FilterType.ANNOTATION ：按照注解
//FilterType.ASSIGNABLE_TYPE  ：按照指定类型
//FilterType.REGEX  ：按照指定正则表达式
//FilterType.CUSTOM  ：使用自定义规则
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;img src=&quot;https://tangtangit.com/_astro/image-20210320113055798.BnQQF7TR_ZysTGm.webp&quot; alt=&quot;image-20210320113055798&quot; /&gt;&lt;/p&gt;
&lt;h4&gt;测试自定义规则&lt;/h4&gt;
&lt;p&gt;&lt;img src=&quot;https://tangtangit.com/_astro/image-20210320121250924.B2otmPZe_qMw84.webp&quot; alt=&quot;image-20210320121250924&quot; /&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;package com.eim.filter;

import org.springframework.core.io.Resource;
import org.springframework.core.type.AnnotationMetadata;
import org.springframework.core.type.ClassMetadata;
import org.springframework.core.type.classreading.MetadataReader;
import org.springframework.core.type.classreading.MetadataReaderFactory;
import org.springframework.core.type.filter.TypeFilter;

import java.io.IOException;

//自定义过滤规则，实现TypeFilter接口，
public class MyTypeFilter implements TypeFilter {

    /**
     * 实现match方法，返回true匹配成功，返回false匹配失败
     * metadataReader： 读取到当前正在扫描的信息
     * metadataReaderFactory： 可以获取到其他任何类信息的
     */
    @Override
    public boolean match(MetadataReader metadataReader, MetadataReaderFactory metadataReaderFactory) throws IOException {
        //获取当前类注解信息
        AnnotationMetadata annotationMetadata = metadataReader.getAnnotationMetadata();
        //获取当前正在扫描类的信息（类型，是否实现接口）
        ClassMetadata classMetadata = metadataReader.getClassMetadata();
        //获取当前类资源信息（类的路径）
        Resource resource = metadataReader.getResource();

        String className = classMetadata.getClassName();
        System.out.println(&quot;-------------&amp;gt;&quot;+className);
        if(className.contains(&quot;er&quot;)){
            return true;
        }

        return false;
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;@ComponentScan(value = &quot;com.eim&quot;,
        //excludeFilters = @ComponentScan.Filter(type = FilterType.ANNOTATION,classes = {Controller.class})
        includeFilters = {
                //@ComponentScan.Filter(type = FilterType.ANNOTATION,classes = {Controller.class}),
                //@ComponentScan.Filter(type = FilterType.ASSIGNABLE_TYPE,classes = {BaseService.class})
                @ComponentScan.Filter(type = FilterType.CUSTOM,classes = {MyTypeFilter.class})
        },
        useDefaultFilters = false
)
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;public class IOCTest {

    @Test
    public void test01() {
        AnnotationConfigApplicationContext applicationContext = new AnnotationConfigApplicationContext(Mainconfig.class);
        String[] names = applicationContext.getBeanDefinitionNames();
        for (String name : names) {
            System.out.println(name);
        }
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;@ComponentScans&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;//可放入多个@ComponentScan
@ComponentScans(value = {
        @ComponentScan(value = &quot;com.eim&quot;,
                //excludeFilters = @ComponentScan.Filter(type = FilterType.ANNOTATION,classes = {Controller.class})
                includeFilters = @ComponentScan.Filter(type = FilterType.ANNOTATION,classes = {Controller.class}),
                useDefaultFilters = false
        )
})
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;@Scope&lt;/h3&gt;
&lt;blockquote&gt;
&lt;p&gt;调整bean创建作用域&lt;/p&gt;
&lt;p&gt;可用值：&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;prototype:  多实例
singleton:  单实例(默认)&lt;/p&gt;
&lt;/blockquote&gt;
&lt;/blockquote&gt;
&lt;p&gt;singleton  ：默认单实例情况下，启动ioc容器是调用方法创建对象到ioc容器中，以后每次获取从容器（map.get）中拿&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;    @Bean(&quot;person2&quot;)
    public Person person(){
        System.out.println(&quot;------给容器添加person2-----&quot;);
        return new Person(&quot;zhangsan&quot;,25);
    }
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;prototype ：ioc启动并不会去调用方法创建对象，&lt;strong&gt;每次&lt;/strong&gt;获取时调用方法创建对象；&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;    @Scope(&quot;prototype&quot;)
    @Bean(&quot;person2&quot;)
    public Person person(){
        System.out.println(&quot;------给容器添加person2-----&quot;);
        return new Person(&quot;zhangsan&quot;,25);
    }
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;@Lazy 懒加载&lt;/h3&gt;
&lt;blockquote&gt;
&lt;p&gt;单实例： 默认容器启动是创建对象；&lt;/p&gt;
&lt;p&gt;懒加载：容器启动不创建对象。第一次使用（获取）bean创建对象，并初始化。（只会被创建一次）&lt;/p&gt;
&lt;/blockquote&gt;
&lt;pre&gt;&lt;code&gt;    @Lazy
    @Bean(&quot;person2&quot;)
    public Person person(){
        System.out.println(&quot;------给容器添加person2-----&quot;);
        return new Person(&quot;zhangsan&quot;,25);
    }
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;@Conditional&lt;/h3&gt;
&lt;blockquote&gt;
&lt;p&gt;按照一定条件进行判断，满足条件给容器中注册bean，可以作用在类和方法上&lt;/p&gt;
&lt;p&gt;@Conditional({实现Conditional接口的类})&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h4&gt;1、配置文件类&lt;/h4&gt;
&lt;pre&gt;&lt;code&gt;//配置类==配置文件
@Configuration
@ComponentScan(value = &quot;com.eim&quot;)
public class MainConditionalconfig {

    /**
     *
     * 如果是Windows  bill
     * 如果是linux  linux
     */
    @Conditional({WindowsConditional.class})
    @Bean(&quot;bill&quot;)
    public Person person1(){
        return new Person(&quot;bill&quot;,25);
    }

    @Conditional({LinuxConditional.class})
    @Bean(&quot;linux&quot;)
    public Person person2(){
        return new Person(&quot;linux&quot;,30);
    }

}
&lt;/code&gt;&lt;/pre&gt;
&lt;h4&gt;2、测试类&lt;/h4&gt;
&lt;pre&gt;&lt;code&gt;public class IOCTest {

    AnnotationConfigApplicationContext applicationContext
            = new AnnotationConfigApplicationContext(MainConditionalconfig.class);

    @Test
    public void test03(){
        ConfigurableEnvironment environment = applicationContext.getEnvironment();
        //动态获取环境变量的值
        String property = environment.getProperty(&quot;os.name&quot;);
        System.out.println(&quot;os.name:&quot;+property);
        //名字
        String[] names = applicationContext.getBeanNamesForType(Person.class);
        for (String name : names) {
            System.out.println(&quot;name=&quot;+name);
        }
        //person对象的map集合
        Map&amp;lt;String, Person&amp;gt; personMap = applicationContext.getBeansOfType(Person.class);
        System.out.println(personMap);
    }
}    
&lt;/code&gt;&lt;/pre&gt;
&lt;h4&gt;3、自定义conditional接口&lt;/h4&gt;
&lt;blockquote&gt;
&lt;p&gt;LinuxConditional  和一下代码一致，除了判断条件&lt;/p&gt;
&lt;/blockquote&gt;
&lt;pre&gt;&lt;code&gt;public class WindowsConditional implements Condition {

    /**
     * ConditionContext 判断能使用的上下文（环境）
     * AnnotatedTypeMetadata 注释信息
     *
     */
    @Override
    public boolean matches(ConditionContext context, AnnotatedTypeMetadata metadata) {
        //是否是windons系统
        //ioc容器的bean工厂
        ConfigurableListableBeanFactory beanFactory = context.getBeanFactory();
        //获取到运行时的信息，环境变量，虚拟机环境变量
        Environment environment = context.getEnvironment();
        //获取类加载器
        ClassLoader classLoader = context.getClassLoader();
        //获取bean定义的注册类：所有的bean注册在这类中，可以查询和注册bean
        BeanDefinitionRegistry registry = context.getRegistry();

        String property = environment.getProperty(&quot;os.name&quot;);
        if(property.contains(&quot;Windows&quot;)){
            return true;
        }
        return false;
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h4&gt;4、修改虚拟机参数&lt;/h4&gt;
&lt;p&gt;&lt;img src=&quot;https://tangtangit.com/_astro/image-20210321174521370.DyZ1QpID_Z26L5kW.webp&quot; alt=&quot;image-20210321174521370&quot; /&gt;&lt;/p&gt;
&lt;h3&gt;@Import&lt;/h3&gt;
&lt;blockquote&gt;
&lt;p&gt;@Import :导入组件，id默认时组件的类全名&lt;/p&gt;
&lt;p&gt;ImportSelector接口:返回需要导入组件的全类名数组&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h4&gt;1、自定义 ImportSelector接口&lt;/h4&gt;
&lt;pre&gt;&lt;code&gt;//自定义需要导入的组件
public class MyImportSelector implements ImportSelector {

    //返回值，就是导入到容器中的组件全类名
    //AnnotationMetadata:当前标注@Import注解的类的所有注解信息
    @Override
    public String[] selectImports(AnnotationMetadata importingClassMetadata) {

        return new String[]{&quot;com.eim.bean.Blue&quot;};
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h4&gt;2、配置&lt;/h4&gt;
&lt;pre&gt;&lt;code&gt;@Import({Color.class, MyImportSelector.class})
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;img src=&quot;https://tangtangit.com/_astro/image-20210321180721925.CqsWB4gP_ZFNCkW.webp&quot; alt=&quot;image-20210321180721925&quot; /&gt;&lt;/p&gt;
&lt;h4&gt;3、测试&lt;/h4&gt;
&lt;p&gt;&lt;img src=&quot;https://tangtangit.com/_astro/image-20210321180805313.BlrVS8Gw_Z1exhJu.webp&quot; alt=&quot;image-20210321180805313&quot; /&gt;&lt;/p&gt;
&lt;h3&gt;factoryBean&lt;/h3&gt;
&lt;blockquote&gt;
&lt;p&gt;1、工厂获取的是调用getObject创建的对象&lt;/p&gt;
&lt;p&gt;2、获取工厂bean本身，需要给id前加上&amp;amp;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;1、自定义&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;//自定义factoryBean
public class ColorFactoryBean implements FactoryBean {
    
    //返回的对象会被注册到ioc容器中
    @Override
    public Object getObject() throws Exception {
        System.out.println(&quot;创建color&quot;);
        return new Color(&quot;黄色&quot;);
    }

    @Override
    public Class&amp;lt;?&amp;gt; getObjectType() {
        return null;
    }

    /**
     * true  单例
     * false 每次获取时，调用getObject方法生成对象
     */
    @Override
    public boolean isSingleton() {
        return false;
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;2、配置生效&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;配置类中加入配置&lt;/p&gt;
&lt;/blockquote&gt;
&lt;pre&gt;&lt;code&gt;    @Bean
    public ColorFactoryBean colorFactoryBean(){
        return new ColorFactoryBean();
    }
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;3、测试&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;    @Test
    public void test04(){
        getAllBean(applicationContext);
        //工厂bean获取的是调用getObject创建的对象
        Object bean = applicationContext.getBean(&quot;colorFactoryBean&quot;);
        System.out.println(&quot;bean的类型：&quot;+bean.getClass());
        
        //获取工厂类本身name前加上&amp;amp;
        Object bean2 = applicationContext.getBean(&quot;&amp;amp;colorFactoryBean&quot;);
        System.out.println(&quot;bean2的类型：&quot;+bean2.getClass());
    }
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;生命周期&lt;/h3&gt;
&lt;p&gt;》 bean的创建 》初始化》销毁过程&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;容器管理bean的生命周期&lt;/p&gt;
&lt;p&gt;我们可以自定义初始化和销毁方法：容器在bean进行到当前生命周期时调用我们自定义的方法&lt;/p&gt;
&lt;p&gt;1）、指定初始化和销毁方法&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2&gt;Spring解决循环依赖，你真的懂了吗？&lt;/h2&gt;
&lt;h3&gt;&lt;strong&gt;什么是循环依赖&lt;/strong&gt;&lt;/h3&gt;
&lt;blockquote&gt;
&lt;p&gt;简单的说就是A依赖B，B依赖C，C依赖A这样就构成了循环依赖。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;&lt;img src=&quot;https://tangtangit.com/_astro/b7fd5266d01609240b215e97c00254fce7cd34c5.NSu1qg0e_YwEAW.webp&quot; alt=&quot;img&quot; /&gt;&lt;/p&gt;
&lt;p&gt;循环依赖分为&lt;strong&gt;构造器依赖&lt;/strong&gt;和&lt;strong&gt;属性依赖&lt;/strong&gt;，众所周知的是Spring能够解决&lt;strong&gt;属性的循环依赖&lt;/strong&gt;（set注入）。下文将从源码角度分析Spring是如何解决属性的循环依赖。&lt;strong&gt;思路&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;如何解决循环依赖，Spring主要的思路就是依据三级缓存，在实例化A时调用doGetBean，发现A依赖的B的实例，此时调用doGetBean去实例B，实例化的B的时候发现又依赖A，如果不解决这个循环依赖的话此时的doGetBean将会无限循环下去，导致内存溢出，程序奔溃。spring引用了一个早期对象，并且把这个&quot;早期引用&quot;并将其注入到容器中，让B先完成实例化，此时A就获取B的引用，完成实例化。&lt;strong&gt;三级缓存&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;Spring能够轻松的解决属性的循环依赖正式用到了三级缓存，在AbstractBeanFactory中有详细的注释。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://tangtangit.com/_astro/267f9e2f07082838a8dc0415ac9cc8074d08f1ee.BNWcYU3U_1u7JTk.webp&quot; alt=&quot;img&quot; /&gt;&lt;/p&gt;
&lt;p&gt;一级缓存：singletonObjects，存放&lt;strong&gt;完全实例化属性赋值完成的Bean&lt;/strong&gt;，直接可以使用。&lt;/p&gt;
&lt;p&gt;二级缓存：earlySingletonObjects，存放早期Bean的引用，&lt;strong&gt;尚未属性装配的Bean&lt;/strong&gt;。&lt;/p&gt;
&lt;p&gt;三级缓存：singletonFactories，三级缓存，存放&lt;strong&gt;实例化完成的Bean****工厂&lt;/strong&gt;。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;什么是循环依赖&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;简单的说就是A依赖B，B依赖C，C依赖A这样就构成了循环依赖。&lt;/p&gt;
&lt;h3&gt;&lt;strong&gt;开撸&lt;/strong&gt;&lt;/h3&gt;
&lt;p&gt;先上一张流程图看看Spring是如何解决循环依赖的&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://tangtangit.com/_astro/f603918fa0ec08fa0585781944eb5c6b54fbda96.CissmxAe_l3J5v.webp&quot; alt=&quot;img&quot; /&gt;&lt;/p&gt;
&lt;p&gt;上图标记蓝色的部分都是涉及到三级缓存的操作，下面我们一个一个方法解析【1】 getSingleton(beanName)：源码如下：&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://tangtangit.com/_astro/8601a18b87d6277fddb2583c3b3d7e36e924fc19.DORvRVtB_1eiPbL.webp&quot; alt=&quot;img&quot; /&gt;&lt;/p&gt;
&lt;p&gt;从源码可以得知，doGetBean最初是查询缓存，一二三级缓存全部查询，如果三级缓存存在则将Bean早期引用存放在二级缓存中并移除三级缓存。（升级为二级缓存）【2】addSingletonFactory：源码如下&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://tangtangit.com/_astro/622762d0f703918f7b98d7004238479158eec4bf.BhuNMiqc_2ht8pm.webp&quot; alt=&quot;img&quot; /&gt;&lt;/p&gt;
&lt;p&gt;从源码得知，Bean在实例化完成之后会直接将未装配的Bean工厂存放在&lt;strong&gt;三级缓存&lt;/strong&gt;中，并且&lt;strong&gt;移除二级缓存&lt;/strong&gt;【3】addSingleton：源码如下：&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://tangtangit.com/_astro/58ee3d6d55fbb2fb7356fdff5a4f41a24723dced.JRXN_E5V_1pgWaE.webp&quot; alt=&quot;img&quot; /&gt;&lt;/p&gt;
&lt;p&gt;总之一句话，Bean添加到一级缓存，移除二三级缓存。&lt;/p&gt;
&lt;h3&gt;&lt;strong&gt;扩展&lt;/strong&gt;&lt;/h3&gt;
&lt;h4&gt;【1】为什么Spring不能解决构造器的循环依赖？&lt;/h4&gt;
&lt;p&gt;从流程图应该不难看出来，在Bean调用构造器实例化之前，一二三级缓存并没有Bean的任何相关信息，在实例化之后才放入三级缓存中，因此当getBean的时候缓存并没有命中，这样就抛出了循环依赖的异常了。&lt;/p&gt;
&lt;h4&gt;【2】为什么多实例Bean不能解决循环依赖？&lt;/h4&gt;
&lt;p&gt;多实例Bean是每次创建都会调用doGetBean方法，根本没有使用一二三级缓存，肯定不能解决循环依赖。&lt;/p&gt;
&lt;h4&gt;【3】构造器循环依赖&lt;/h4&gt;
&lt;p&gt;知道了这个原理时候，肯定就知道为啥Spring不能解决“A的构造方法中依赖了B的实例对象，同时B的构造方法中依赖了A的实例对象”这类问题了！因为加入singletonFactories三级缓存的前提是执行了构造器，所以构造器的循环依赖没法解决。&lt;/p&gt;
&lt;h3&gt;&lt;strong&gt;总结&lt;/strong&gt;&lt;/h3&gt;
&lt;p&gt;根据以上的分析，大概清楚了Spring是如何解决循环依赖的。假设A依赖B，B依赖A（注意：这里是set属性依赖）分以下步骤执行：A依次执行&lt;strong&gt;doGetBean&lt;/strong&gt;、查询缓存、&lt;strong&gt;createBean&lt;/strong&gt;创建实例，实例化完成放入三级缓存singletonFactories中，接着执行&lt;strong&gt;populateBean&lt;/strong&gt;方法装配属性，但是发现有一个属性是B的对象。因此再次调用doGetBean方法创建B的实例，依次执行doGetBean、查询缓存、createBean创建实例，实例化完成之后放入三级缓存singletonFactories中，执行populateBean装配属性，但是此时发现有一个属性是A对象。因此再次调用doGetBean创建A的实例，但是执行到getSingleton查询缓存的时候，从三级缓存中查询到了A的实例(早期引用，未完成属性装配)，此时直接返回A，不用执行后续的流程创建A了，那么B就完成了属性装配，此时是一个完整的对象放入到一级缓存singletonObjects中。B创建完成了，则A自然完成了属性装配，也创建完成放入了一级缓存singletonObjects中。Spring三级缓存的应用完美的解决了循环依赖的问题，下面是循环依赖的解决流程图。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://tangtangit.com/_astro/3b292df5e0fe9925237cd9d720ad3fd98cb171d4.DSVPzkes_Z1ztN5X.webp&quot; alt=&quot;img&quot; /&gt;&lt;/p&gt;
&lt;hr /&gt;
&lt;h2&gt;手写spring&lt;/h2&gt;
&lt;h2&gt;《Spring 手撸专栏》&lt;/h2&gt;
&lt;p&gt;地址：&lt;a href=&quot;https://bugstack.cn/md/spring/develop-spring/2021-05-16-%E7%AC%AC1%E7%AB%A0%EF%BC%9A%E5%BC%80%E7%AF%87%E4%BB%8B%E7%BB%8D%EF%BC%8C%E6%89%8B%E5%86%99Spring%E8%83%BD%E7%BB%99%E4%BD%A0%E5%B8%A6%E6%9D%A5%E4%BB%80%E4%B9%88%EF%BC%9F.html&quot;&gt;第01章：开篇介绍，我要带你撸 Spring 啦！ | 小傅哥 bugstack 虫洞栈&lt;/a&gt;&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;本文只记录难点，难以理解的点&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2&gt;第 2 章：小试牛刀，实现一个简单的Bean容器&lt;/h2&gt;
&lt;h3&gt;目标&lt;/h3&gt;
&lt;p&gt;一个简单的 Spring Bean 容器实现，还需 Bean 的定义、注册、获取三个基本步骤，简化设计如下；&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://tangtangit.com/_astro/spring-2-01.X2lN_c0x_Z2uNKe2.webp&quot; alt=&quot;img&quot; /&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;定义：BeanDefinition，可能这是你在查阅 Spring 源码时经常看到的一个类，例如它会包括 singleton、prototype、BeanClassName 等。但目前我们初步实现会更加简单的处理，只定义一个 Object 类型用于存放对象。&lt;/li&gt;
&lt;li&gt;注册：这个过程就相当于我们把数据存放到 HashMap 中，只不过现在 HashMap 存放的是定义了的 Bean 的对象信息。&lt;/li&gt;
&lt;li&gt;获取：最后就是获取对象，Bean 的名字就是key，Spring 容器初始化好 Bean 以后，就可以直接获取了。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;img src=&quot;https://tangtangit.com/_astro/spring-2-02.CadG0ruc_ZPIN7C.webp&quot; alt=&quot;图 2-2&quot; /&gt;&lt;/p&gt;
&lt;p&gt;由于BeanDefinition中存放的就是对象，故HashMap 即为对象容器。（按照道理应该分为BeanDefinition集合和对象集合）&lt;/p&gt;
&lt;h3&gt;测试&lt;/h3&gt;
&lt;p&gt;&lt;img src=&quot;https://tangtangit.com/_astro/image-20231130191730046.DP305Tmd_vrqJS.webp&quot; alt=&quot;image-20231130191730046&quot; /&gt;&lt;/p&gt;
&lt;h3&gt;疑问&lt;/h3&gt;
&lt;p&gt;为什么需要BeanDefinition?&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;Spring Bean的总体创建过程如下，Spring会加载指定的java类将其变成BeanDefinition对象，然后根据BeanDefinition来创建Bean&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://tangtangit.com/_astro/17b2c6a72f2c8f5b19d2d3ac8025e40b.sDUvnfHT_ZJumlT.webp&quot; alt=&quot;img&quot; /&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2&gt;第 3 章：初显身手，运用设计模式，实现 Bean 的定义、注册、获取&lt;/h2&gt;
&lt;h3&gt;目标&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;这一次我们把 Bean 的创建交给容器，而不是我们在调用时候传递一个实例化好的 Bean 对象，&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;另外还需要考虑单例对象，在对象的二次获取时是可以从内存中获取对象的。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;此外不仅要实现功能还需要完善基础容器框架的类结构体，否则将来就很难扩容进去其他的功能了。&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;设计&lt;/h3&gt;
&lt;p&gt;我们需要将 Spring Bean 容器完善起来，&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;首先非常重要的一点是在 Bean 注册的时候只注册一个类信息，而不会直接把实例化信息注册到 Spring 容器中。那么就需要修改 BeanDefinition 中的属性 Object 为 Class，&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;接下来在需要做的就是在获取 Bean 对象时需要处理 Bean 对象的实例化操作以及判断当前单例对象在容器中是否已经缓存起来了。整体设计如图 3-1&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;img src=&quot;https://tangtangit.com/_astro/spring-3-01.ASrteclm_QE1Ms.webp&quot; alt=&quot;img&quot; /&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;首先我们需要定义 BeanFactory 这样一个 Bean 工厂，提供 Bean 的获取方法 &lt;code&gt;getBean(String name)&lt;/code&gt;，之后这个 Bean 工厂接口由抽象类 AbstractBeanFactory 实现。这样使用&lt;a href=&quot;https://bugstack.cn/itstack-demo-design/2020/07/07/%E9%87%8D%E5%AD%A6-Java-%E8%AE%BE%E8%AE%A1%E6%A8%A1%E5%BC%8F-%E5%AE%9E%E6%88%98%E6%A8%A1%E6%9D%BF%E6%A8%A1%E5%BC%8F.html&quot;&gt;模板模式 (opens new window)&lt;/a&gt;的设计方式，可以统一收口通用核心方法的调用逻辑和标准定义，也就很好的控制了后续的实现者不用关心调用逻辑，按照统一方式执行。那么类的继承者只需要关心具体方法的逻辑实现即可。&lt;/li&gt;
&lt;li&gt;那么在继承抽象类 AbstractBeanFactory 后的 AbstractAutowireCapableBeanFactory 就可以实现相应的抽象方法了，因为 AbstractAutowireCapableBeanFactory 本身也是一个抽象类，所以它只会实现属于自己的抽象方法，其他抽象方法由继承 AbstractAutowireCapableBeanFactory 的类实现。这里就体现了类实现过程中的各司其职，你只需要关心属于你的内容，不是你的内容，不要参与。&lt;em&gt;这一部分内容我们会在代码里有具体的体现&lt;/em&gt;&lt;/li&gt;
&lt;li&gt;另外这里还有块非常重要的知识点，就是关于单例 SingletonBeanRegistry 的接口定义实现，而 DefaultSingletonBeanRegistry 对接口实现后，会被抽象类 AbstractBeanFactory 继承。现在 AbstractBeanFactory 就是一个非常完整且强大的抽象类了，也能非常好的体现出它对模板模式的抽象定义。&lt;em&gt;接下来我们就带着这些设计层面的思考，去看代码的具体实现结果&lt;/em&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;难点在于复杂度提高&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;small-spring-step-02
└── src
    ├── main
    │   └── java
    │       └── cn.bugstack.springframework.beans
    │           ├── factory
    │           │   ├── config
    │           │   │   ├── BeanDefinition.java
    │           │   │   └── SingletonBeanRegistry.java
    │           │   ├── support
    │           │   │   ├── AbstractAutowireCapableBeanFactory.java
    │           │   │   ├── AbstractBeanFactory.java
    │           │   │   ├── BeanDefinitionRegistry.java
    │           │   │   ├── DefaultListableBeanFactory.java
    │           │   │   └── DefaultSingletonBeanRegistry.java
    │           │   └── BeanFactory.java
    │           └── BeansException.java
    └── test
        └── java
            └── cn.bugstack.springframework.test
                ├── bean
                │   └── UserService.java
                └── ApiTest.java
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;类图&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://tangtangit.com/_astro/spring-3-02.Dea2gkaz_Zo3rCu.webp&quot; alt=&quot;图 3-2&quot; /&gt;&lt;/p&gt;
&lt;p&gt;虽然这一章节关于 Spring Bean 容器的功能实现与 &lt;code&gt;Spring 源码&lt;/code&gt;中还有不少的差距，但以目前实现结果的类关系图来看，其实已经具备了一定的设计复杂性，这些复杂的类关系设计在各个接口定义和实现以及在抽象类继承中都有所体现，例如：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;BeanFactory 的定义由 AbstractBeanFactory 抽象类实现接口的 getBean 方法&lt;/li&gt;
&lt;li&gt;而 AbstractBeanFactory 又继承了实现了 SingletonBeanRegistry 的DefaultSingletonBeanRegistry 类。这样 AbstractBeanFactory 抽象类就具备了单例 Bean 的注册功能。&lt;/li&gt;
&lt;li&gt;AbstractBeanFactory 中又定义了两个抽象方法：getBeanDefinition(String beanName)、createBean(String beanName, BeanDefinition beanDefinition) ，而这两个抽象方法分别由 DefaultListableBeanFactory、AbstractAutowireCapableBeanFactory 实现。&lt;/li&gt;
&lt;li&gt;最终 DefaultListableBeanFactory 还会继承抽象类 AbstractAutowireCapableBeanFactory 也就可以调用抽象类中的 createBean 方法了。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;综上这一部分的类关系和实现过程还是会有一些复杂的，因为所有的实现都以职责划分、共性分离以及调用关系定义为标准搭建的类关系。&lt;em&gt;这部分内容的学习，可能会丰富你在复杂业务系统开发中的设计思路。&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://tangtangit.com/_astro/image-20231120134908675.B_ggrW_t_Zl7plC.webp&quot; alt=&quot;image-20231120134908675&quot; /&gt;&lt;/p&gt;
&lt;h3&gt;设计模式&lt;/h3&gt;
&lt;h4&gt;抽象类定义模板方法(AbstractBeanFactory)&lt;/h4&gt;
&lt;p&gt;&lt;strong&gt;cn.bugstack.springframework.beans.factory.support.AbstractBeanFactory&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;public abstract class AbstractBeanFactory extends DefaultSingletonBeanRegistry implements BeanFactory {

    @Override
    public Object getBean(String name) throws BeansException {
        Object bean = getSingleton(name);
        if (bean != null) {
            return bean;
        }

        BeanDefinition beanDefinition = getBeanDefinition(name);
        return createBean(name, beanDefinition);
    }

    protected abstract BeanDefinition getBeanDefinition(String beanName) throws BeansException;

    protected abstract Object createBean(String beanName, BeanDefinition beanDefinition) throws BeansException;

}
    
&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;AbstractBeanFactory 首先继承了 DefaultSingletonBeanRegistry，也就具备了使用单例注册类方法。&lt;/li&gt;
&lt;li&gt;接下来很重要的一点是关于接口 BeanFactory 的实现，在方法 getBean 的实现过程中可以看到，主要是对单例 Bean 对象的获取以及在获取不到时需要拿到 Bean 的定义做相应 Bean 实例化操作。那么 getBean 并没有自身的去实现这些方法，而是只定义了调用过程以及提供了抽象方法，由实现此抽象类的其他类做相应实现。&lt;/li&gt;
&lt;li&gt;后续继承抽象类 AbstractBeanFactory 的类有两个，包括：AbstractAutowireCapableBeanFactory、DefaultListableBeanFactory，这两个类分别做了相应的实现处理，接着往下看。&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;集中方法&lt;/h3&gt;
&lt;p&gt;重要：&lt;em&gt;接口定义了注册，抽象类定义了获取，都集中在 DefaultListableBeanFactory 中的 beanDefinitionMap 里&lt;/em&gt;&lt;/p&gt;
&lt;h3&gt;测试&lt;/h3&gt;
&lt;p&gt;&lt;img src=&quot;https://tangtangit.com/_astro/image-20231130191830088.n3dzJHhH_ZNj4C4.webp&quot; alt=&quot;image-20231130191830088&quot; /&gt;&lt;/p&gt;
&lt;h3&gt;疑问&lt;/h3&gt;
&lt;p&gt;为什么有两个容器（单例对象容器、对象定义容器）？&lt;/p&gt;
&lt;blockquote&gt;
&lt;pre&gt;&lt;code&gt;//对象定义容器
Map&amp;lt;String, BeanDefinition&amp;gt; beanDefinitionMap
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;此容器只存放对象定义，也就是传入的Class对象会被包装成BeanDefinition，然后放入DefaultListableBeanFactory 中的beanDefinitionMap集合中保存。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;//单例对象容器
HashMap&amp;lt;String, Object&amp;gt; singletonObjects
&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;单例对象容器存放所有对象，不管是不是单例。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;只要调用获取对象时判断单例对象容器中没有该对象，则根据bean定义创建对象，并加入单例容器中。&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/blockquote&gt;
&lt;h3&gt;解析&lt;/h3&gt;
&lt;p&gt;2、类和接口的简析&lt;/p&gt;
&lt;p&gt;①BeanDefinition：由存储bean对象，改为存储类信息，这是将实例化Bean交给Spring操作的第一步&lt;/p&gt;
&lt;p&gt;②SingletonBeanRegistry：获取单例对象的接口类&lt;/p&gt;
&lt;p&gt;③DefaultSingletonBeanRegistry：继承SingletonBeanRegistry，定义单例对象的存储容器以及存取方法&lt;/p&gt;
&lt;p&gt;④BeanFactory：定义了获取bean对象的接口&lt;/p&gt;
&lt;p&gt;⑤AbstractBeanFactory：继承DefaultSingletonBeanRegistry类，就可以存取（注册）单例对象；继承BeanFactory实现bean对象的获取方法，并定义了getBeanDefinition、createBean抽象方法，如果从单例对象容器中获取不到对象，就调用这俩抽象方法 ⑥AbstractAutowireCapableBeanFactory：继承AbstractBeanFactory，实现createBean&lt;/p&gt;
&lt;p&gt;⑦BeanDefinitionRegistry：定义注册BeanDefinition的接口&lt;/p&gt;
&lt;p&gt;⑧DefaultListableBeanFactory（核心类）：继承AbstractAutowireCapableBeanFactory，就有了createBean的能力，并实现了BeanDefinitionRegistry#registerBeanDefinition，并实现了AbstractBeanFactory#getBeanDefinition，使得拥有了beanDefinitionMap容器的存取功能，并且该类是核心类，具有对BeanDefinition容器、单例Bean容器这两个核心容器的操作权限 ⑨UserService：自定义的类，测试时作为bean的来源&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;%E5%88%9D%E6%AD%A5%E6%90%AD%E5%BB%BA%E5%AE%B9%E5%99%A8.png&quot; alt=&quot;初步搭建容器&quot; /&gt;&lt;/p&gt;
&lt;p&gt;3、调用流程&lt;/p&gt;
&lt;p&gt;①实例化DefaultListableBeanFactory&lt;/p&gt;
&lt;p&gt;②将UserService类作为构造器参数，实例化BeanDefinition对象，&lt;/p&gt;
&lt;p&gt;③第①步的工厂对象调用registerBeanDefinition方法，将第②步的beanDefinition对象注册到BeanDefinitionMap容器&lt;/p&gt;
&lt;p&gt;④利用工厂获取UserService的bean对象，先从单例对象容器中获取，获取不到就从BeanDefinition容器中获取UserService类并实例化（newInstance），并将实例化对象存入单例对象容器中，方便下次调用&lt;/p&gt;
&lt;p&gt;⑤第二次调用UserService的bean对象时，就可以直接从单例Bean对象容器中获取了&lt;/p&gt;
&lt;h3&gt;收获&lt;/h3&gt;
&lt;p&gt;相对简单bean容器&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;BeanDefinition中修改了Object到Class&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;BeanDefinition使用Map集合进行了存储&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;使用了设计模式中的 ——模板模式&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;巧妙的使用了接口，抽象类做到单一职责&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;第 4 章：崭露头角，基于Cglib实现含构造函数的类实例化策略&lt;/h2&gt;
&lt;h3&gt;目标&lt;/h3&gt;
&lt;p&gt;这一章节的目标主要是为了解决上一章节我们&lt;code&gt;埋下的坑&lt;/code&gt;，那是什么坑呢？其实就是一个关于 Bean 对象在含有构造函数进行实例化的坑。&lt;/p&gt;
&lt;p&gt;在上一章节我们扩充了 Bean 容器的功能，把实例化对象交给容器来统一处理，但在我们实例化对象的代码里并没有考虑对象类是否含构造函数，也就是说如果我们去实例化一个含有构造函数的对象那么就要抛异常了。&lt;/p&gt;
&lt;p&gt;怎么验证？其实就是把 UserService 添加一个含入参信息的构造函数就可以，如下：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;public class UserService {

    private String name;

    public UserService(String name) {
        this.name = name;
    }  

    // ...
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;报错如下：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;java.lang.InstantiationException: cn.bugstack.springframework.test.bean.UserService

	at java.lang.Class.newInstance(Class.java:427)
	at cn.bugstack.springframework.test.ApiTest.test_newInstance(ApiTest.java:51)
	...    
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;发生这一现象的主要原因就是因为 &lt;code&gt;beanDefinition.getBeanClass().newInstance();&lt;/code&gt; 实例化方式并没有考虑构造函数的入参，所以就这个坑就在这等着你了！&lt;strong&gt;那么我们的目标就很明显了，来把这个坑填平！&lt;/strong&gt;&lt;/p&gt;
&lt;h3&gt;三、设计&lt;/h3&gt;
&lt;p&gt;填平这个坑的技术设计主要考虑两部分，一个是串流程从哪合理的把构造函数的入参信息传递到实例化操作里，另外一个是怎么去实例化含有构造函数的对象。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://tangtangit.com/_astro/spring-4-01.lBhtxxVd_ZmndR8.webp&quot; alt=&quot;图 4-1&quot; /&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;参考 Spring Bean 容器源码的实现方式，在 BeanFactory 中添加 &lt;code&gt;Object getBean(String name, Object... args)&lt;/code&gt; 接口，这样就可以在获取 Bean 时把构造函数的入参信息传递进去了。&lt;/li&gt;
&lt;li&gt;另外一个核心的内容是使用什么方式来创建含有构造函数的 Bean 对象呢？这里有两种方式可以选择，一个是基于 Java 本身自带的方法 &lt;code&gt;DeclaredConstructor&lt;/code&gt;，另外一个是使用 Cglib 来动态创建 Bean 对象。&lt;em&gt;Cglib 是基于字节码框架 ASM 实现，所以你也可以直接通过 ASM 操作指令码来创建对象&lt;/em&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;四、实现&lt;/h3&gt;
&lt;h4&gt;1. 工程结构&lt;/h4&gt;
&lt;pre&gt;&lt;code&gt;small-spring-step-03
└── src
    ├── main
    │   └── java
    │       └── cn.bugstack.springframework.beans
    │           ├── factory
    │           │   ├── config
    │           │   │   ├── BeanDefinition.java
    │           │   │   └── SingletonBeanRegistry.java
    │           │   ├── support
    │           │   │   ├── AbstractAutowireCapableBeanFactory.java
    │           │   │   ├── AbstractBeanFactory.java
    │           │   │   ├── BeanDefinitionRegistry.java
    │           │   │   ├── CglibSubclassingInstantiationStrategy.java
    │           │   │   ├── DefaultListableBeanFactory.java
    │           │   │   ├── DefaultSingletonBeanRegistry.java
    │           │   │   ├── InstantiationStrategy.java
    │           │   │   └── SimpleInstantiationStrategy.java
    │           │   └── BeanFactory.java
    │           └── BeansException.java
    └── test
        └── java
            └── cn.bugstack.springframework.test
                ├── bean
                │   └── UserService.java
                └── ApiTest.java
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;工程源码&lt;/strong&gt;：&lt;code&gt;公众号「bugstack虫洞栈」，回复：Spring 专栏，获取完整源码&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;Spring Bean 容器类关系，如图 4-2&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://tangtangit.com/_astro/spring-4-02.BdzR9Mbq_Zm9VBs.webp&quot; alt=&quot;图 4-2&quot; /&gt;&lt;/p&gt;
&lt;p&gt;本章节&lt;code&gt;“填坑”&lt;/code&gt;主要是在现有工程中添加 InstantiationStrategy 实例化策略接口，以及补充相应的 getBean 入参信息，让外部调用时可以传递构造函数的入参并顺利实例化。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;%E6%9C%89%E5%8F%82%E6%9E%84%E9%80%A0%E5%AE%9E%E4%BE%8B%E5%8C%96%E5%AF%B9%E8%B1%A1.png&quot; alt=&quot;有参构造实例化对象&quot; /&gt;&lt;/p&gt;
&lt;h3&gt;测试&lt;/h3&gt;
&lt;p&gt;&lt;img src=&quot;https://tangtangit.com/_astro/image-20231130192031252.BJnuQ21V_2fBj4m.webp&quot; alt=&quot;image-20231130192031252&quot; /&gt;&lt;/p&gt;
&lt;h3&gt;知识点&lt;/h3&gt;
&lt;h4&gt;getDeclaredConstructor&lt;/h4&gt;
&lt;p&gt;&lt;code&gt;getDeclaredConstructor&lt;/code&gt; 是Java反射（Reflection）API中的方法之一，属于 &lt;code&gt;Class&lt;/code&gt; 类的一部分。它用于获取类中声明的构造方法（包括私有构造方法），而不管构造方法的访问修饰符是什么。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;public Constructor&amp;lt;?&amp;gt; getDeclaredConstructor(Class&amp;lt;?&amp;gt;... parameterTypes) throws NoSuchMethodException, SecurityException
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;其中，&lt;code&gt;parameterTypes&lt;/code&gt; 参数是一个可变参数，用于指定要获取的构造方法的参数类型列表。&lt;/p&gt;
&lt;p&gt;使用示例：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;import java.lang.reflect.Constructor;
public class Example {
    public static void main(String[] args) {
        try {
            Class&amp;lt;?&amp;gt; clazz = MyClass.class;
            
            // 获取声明的构造方法，这里假设 MyClass 有一个参数为 int 的构造方法
            Constructor&amp;lt;?&amp;gt; constructor = clazz.getDeclaredConstructor(int.class);
            
            // 使用构造方法创建对象
            Object obj = constructor.newInstance(42);
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
    
    static class MyClass {
        private int value;
        
        // 假设有一个参数为 int 的私有构造方法
        private MyClass(int value) {
            this.value = value;
        }
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;在上述示例中，&lt;code&gt;getDeclaredConstructor&lt;/code&gt; 方法被用于获取 &lt;code&gt;MyClass&lt;/code&gt; 类中声明的私有构造方法，然后通过 &lt;code&gt;newInstance&lt;/code&gt; 方法创建了一个对象。这种通过反射获取构造方法并创建对象的方式在某些情况下是非常有用的，比如在框架中动态创建对象实例。&lt;/p&gt;
&lt;h4&gt;CGLIB——Enhancer&lt;/h4&gt;
&lt;p&gt;示例：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;public class CglibSubclassingInstantiationStrategy implements InstantiationStrategy {
    @Override
    public Object instantiate(BeanDefinition beanDefinition, String beanName, Constructor constructor, Object[] args) throws NoSuchMethodException, InvocationTargetException, InstantiationException, IllegalAccessException {
        Enhancer enhancer = new Enhancer();
        enhancer.setSuperclass(beanDefinition.getBeanClass());
        enhancer.setCallback(new NoOp() {
            @Override
            public int hashCode() {
                return super.hashCode();
            }
        });
        if (null == constructor) return enhancer.create();
        return enhancer.create(constructor.getParameterTypes(), args);
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;解答：&lt;/p&gt;
&lt;p&gt;这段代码涉及到 CGLIB（Code Generation Library）库的使用，它是一个用于生成动态代理和代码增强的库。在 Spring 中，CGLIB 可以用于创建代理对象，特别是用于代理那些没有接口的类。&lt;/p&gt;
&lt;p&gt;以下是代码中使用到的 CGLIB 方法的解释：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;Enhancer&lt;/code&gt;: &lt;code&gt;Enhancer&lt;/code&gt; 是 CGLIB 中的一个类，用于生成代理类。通过 &lt;code&gt;Enhancer&lt;/code&gt; 可以创建一个类的子类，该子类可以覆盖父类的方法，实现方法的拦截和增强。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;Enhancer enhancer = new Enhancer();
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;setSuperclass&lt;/code&gt;: 通过 &lt;code&gt;setSuperclass&lt;/code&gt; 方法设置需要生成子类的父类。在这里，&lt;code&gt;beanDefinition.getBeanClass()&lt;/code&gt; 返回的是要被代理的类的 Class 对象。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;enhancer.setSuperclass(beanDefinition.getBeanClass());
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;setCallback&lt;/code&gt;: 通过 &lt;code&gt;setCallback&lt;/code&gt; 方法设置一个回调对象，这里使用了 &lt;code&gt;NoOp&lt;/code&gt; 类，表示不进行任何操作，只是简单地继承父类的方法。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;enhancer.setCallback(new NoOp() {
    @Override
    public int hashCode() {
        return super.hashCode();
    }
});
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;create&lt;/code&gt;: 调用 &lt;code&gt;create&lt;/code&gt; 方法生成代理类的实例。如果 &lt;code&gt;constructor&lt;/code&gt; 为 &lt;code&gt;null&lt;/code&gt;，则调用无参构造方法创建对象；否则，调用带参数的构造方法创建对象。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;if (null == constructor) return enhancer.create();
return enhancer.create(constructor.getParameterTypes(), args);
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;总体来说，这段代码的目的是使用 CGLIB 生成一个代理类的实例。代理类继承了要被代理的类，通过设置回调对象实现对父类方法的拦截和增强。在生成代理类实例时，可以选择使用无参构造方法或带参数的构造方法。这种方式允许在运行时对目标类的行为进行灵活的控制和修改。&lt;/p&gt;
&lt;h2&gt;第 5 章：一鸣惊人，为Bean对象注入属性和依赖Bean的功能实现&lt;/h2&gt;
&lt;h3&gt;二、目标&lt;/h3&gt;
&lt;p&gt;首先我们回顾下这几章节都完成了什么，包括：&lt;a href=&quot;https://bugstack.cn/spring/2021/05/20/%E7%AC%AC2%E7%AB%A0-%E5%B0%8F%E8%AF%95%E7%89%9B%E5%88%80-%E5%AE%9E%E7%8E%B0%E4%B8%80%E4%B8%AA%E7%AE%80%E5%8D%95%E7%9A%84Bean%E5%AE%B9%E5%99%A8.html&quot;&gt;实现一个容器 (opens new window)&lt;/a&gt;、&lt;a href=&quot;https://bugstack.cn/spring/2021/05/23/%E7%AC%AC3%E7%AB%A0-%E5%88%9D%E6%98%BE%E8%BA%AB%E6%89%8B-%E8%BF%90%E7%94%A8%E8%AE%BE%E8%AE%A1%E6%A8%A1%E5%BC%8F-%E5%AE%9E%E7%8E%B0-Bean-%E7%9A%84%E5%AE%9A%E4%B9%89-%E6%B3%A8%E5%86%8C-%E8%8E%B7%E5%8F%96.html&quot;&gt;定义和注册Bean (opens new window)&lt;/a&gt;、&lt;a href=&quot;https://bugstack.cn/spring/2021/05/23/%E7%AC%AC3%E7%AB%A0-%E5%88%9D%E6%98%BE%E8%BA%AB%E6%89%8B-%E8%BF%90%E7%94%A8%E8%AE%BE%E8%AE%A1%E6%A8%A1%E5%BC%8F-%E5%AE%9E%E7%8E%B0-Bean-%E7%9A%84%E5%AE%9A%E4%B9%89-%E6%B3%A8%E5%86%8C-%E8%8E%B7%E5%8F%96.html&quot;&gt;实例化Bean (opens new window)&lt;/a&gt;，&lt;a href=&quot;https://bugstack.cn/spring/2021/05/30/%E7%AC%AC4%E7%AB%A0-%E5%B4%AD%E9%9C%B2%E5%A4%B4%E8%A7%92-%E5%9F%BA%E4%BA%8ECglib%E5%AE%9E%E7%8E%B0%E5%90%AB%E6%9E%84%E9%80%A0%E5%87%BD%E6%95%B0%E7%9A%84%E7%B1%BB%E5%AE%9E%E4%BE%8B%E5%8C%96%E7%AD%96%E7%95%A5.html&quot;&gt;按照是否包含构造函数实现不同的实例化策略 (opens new window)&lt;/a&gt;，那么在创建对象实例化这我们还缺少什么？其实还缺少一个关于&lt;code&gt;类中是否有属性的问题&lt;/code&gt;，如果有类中包含属性那么在实例化的时候就需要把属性信息填充上，这样才是一个完整的对象创建。&lt;/p&gt;
&lt;p&gt;对于属性的填充不只是 int、Long、String，还包括还没有实例化的对象属性，都需要在 Bean 创建时进行填充操作。&lt;em&gt;不过这里我们暂时不会考虑 Bean 的循环依赖，否则会把整个功能实现撑大，这样新人学习时就把握不住了，待后续陆续先把核心功能实现后，再逐步完善&lt;/em&gt;&lt;/p&gt;
&lt;h3&gt;三、设计&lt;/h3&gt;
&lt;p&gt;鉴于属性填充是在 Bean 使用 &lt;code&gt;newInstance&lt;/code&gt; 或者 &lt;code&gt;Cglib&lt;/code&gt; 创建后，开始补全属性信息，那么就可以在类 &lt;code&gt;AbstractAutowireCapableBeanFactory&lt;/code&gt; 的 createBean 方法中添加补全属性方法。&lt;em&gt;这部分大家在实习的过程中也可以对照Spring源码学习，这里的实现也是Spring的简化版，后续对照学习会更加易于理解&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://tangtangit.com/_astro/spring-5-01.RZP1PQu__Z1SP7he.webp&quot; alt=&quot;img&quot; /&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;属性填充要在类实例化创建之后，也就是需要在 &lt;code&gt;AbstractAutowireCapableBeanFactory&lt;/code&gt; 的 createBean 方法中添加 &lt;code&gt;applyPropertyValues&lt;/code&gt; 操作。&lt;/li&gt;
&lt;li&gt;由于我们需要在创建Bean时候填充属性操作，那么就需要在 bean 定义 BeanDefinition 类中，添加 PropertyValues 信息。&lt;/li&gt;
&lt;li&gt;另外是填充属性信息还包括了 Bean 的对象类型，也就是需要再定义一个 BeanReference，里面其实就是一个简单的 Bean 名称，在具体的实例化操作时进行递归创建和填充，与 Spring 源码实现一样。&lt;em&gt;Spring 源码中 BeanReference 是一个接口&lt;/em&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;四、实现&lt;/h3&gt;
&lt;h4&gt;1. 工程结构&lt;/h4&gt;
&lt;pre&gt;&lt;code&gt;small-spring-step-04
└── src
    ├── main
    │   └── java
    │       └── cn.bugstack.springframework.beans
    │           ├── factory
    │           │   ├── config
    │           │   │   ├── BeanDefinition.java
    │           │   │   ├── BeanReference.java
    │           │   │   └── SingletonBeanRegistry.java
    │           │   ├── support
    │           │   │   ├── AbstractAutowireCapableBeanFactory.java
    │           │   │   ├── AbstractBeanFactory.java
    │           │   │   ├── BeanDefinitionRegistry.java
    │           │   │   ├── CglibSubclassingInstantiationStrategy.java
    │           │   │   ├── DefaultListableBeanFactory.java
    │           │   │   ├── DefaultSingletonBeanRegistry.java
    │           │   │   ├── InstantiationStrategy.java
    │           │   │   └── SimpleInstantiationStrategy.java
    │           │   └── BeanFactory.java
    │           ├── BeansException.java
    │           ├── PropertyValue.java
    │           └── PropertyValues.java
    └── test
        └── java
            └── cn.bugstack.springframework.test
                ├── bean
                │   ├── UserDao.java
                │   └── UserService.java
                └── ApiTest.java
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;工程源码&lt;/strong&gt;：&lt;code&gt;公众号「bugstack虫洞栈」，回复：Spring 专栏，获取完整源码&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;Spring Bean 容器类关系，如图 5-2&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://tangtangit.com/_astro/spring-5-02.DLLHnQ8U_Z12GAAk.webp&quot; alt=&quot;图 5-2&quot; /&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;本章节中需要新增加3个类，&lt;code&gt;BeanReference&lt;/code&gt;(类引用)、&lt;code&gt;PropertyValue&lt;/code&gt;(属性值)、&lt;code&gt;PropertyValues&lt;/code&gt;(属性集合)，分别用于类和其他类型属性填充操作。&lt;/li&gt;
&lt;li&gt;另外改动的类主要是 &lt;code&gt;AbstractAutowireCapableBeanFactory&lt;/code&gt;，在 createBean 中补全属性填充部分&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;img src=&quot;step05%E6%B3%A8%E5%85%A5%E5%B1%9E%E6%80%A7.png&quot; alt=&quot;step05注入属性&quot; /&gt;&lt;/p&gt;
&lt;h3&gt;测试&lt;/h3&gt;
&lt;p&gt;&lt;img src=&quot;https://tangtangit.com/_astro/image-20231130192229884.CeZ5vyUH_Z209fCR.webp&quot; alt=&quot;image-20231130192229884&quot; /&gt;&lt;/p&gt;
&lt;h3&gt;核心关键&lt;/h3&gt;
&lt;h4&gt;Bean 属性填充&lt;/h4&gt;
&lt;p&gt;&lt;strong&gt;cn.bugstack.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;public abstract class AbstractAutowireCapableBeanFactory extends AbstractBeanFactory {

    private InstantiationStrategy instantiationStrategy = new CglibSubclassingInstantiationStrategy();

    @Override
    protected Object createBean(String beanName, BeanDefinition beanDefinition, Object[] args) throws BeansException {
        Object bean = null;
        try {
            bean = createBeanInstance(beanDefinition, beanName, args);
            // 给 Bean 填充属性
            applyPropertyValues(beanName, bean, beanDefinition);
        } catch (Exception e) {
            throw new BeansException(&quot;Instantiation of bean failed&quot;, e);
        }

        addSingleton(beanName, bean);
        return bean;
    }

    protected Object createBeanInstance(BeanDefinition beanDefinition, String beanName, Object[] args) {
        Constructor constructorToUse = null;
        Class&amp;lt;?&amp;gt; beanClass = beanDefinition.getBeanClass();
        Constructor&amp;lt;?&amp;gt;[] declaredConstructors = beanClass.getDeclaredConstructors();
        for (Constructor ctor : declaredConstructors) {
            if (null != args &amp;amp;&amp;amp; ctor.getParameterTypes().length == args.length) {
                constructorToUse = ctor;
                break;
            }
        }
        return getInstantiationStrategy().instantiate(beanDefinition, beanName, constructorToUse, args);
    }

    /**
     * Bean 属性填充
     */
    protected void applyPropertyValues(String beanName, Object bean, BeanDefinition beanDefinition) {
        try {
            PropertyValues propertyValues = beanDefinition.getPropertyValues();
            for (PropertyValue propertyValue : propertyValues.getPropertyValues()) {

                String name = propertyValue.getName();
                Object value = propertyValue.getValue();

                if (value instanceof BeanReference) {
                    // A 依赖 B，获取 B 的实例化
                    BeanReference beanReference = (BeanReference) value;
                    value = getBean(beanReference.getBeanName());
                }
                // 属性填充
                BeanUtil.setFieldValue(bean, name, value);
            }
        } catch (Exception e) {
            throw new BeansException(&quot;Error setting property values：&quot; + beanName);
        }
    }

    public InstantiationStrategy getInstantiationStrategy() {
        return instantiationStrategy;
    }

    public void setInstantiationStrategy(InstantiationStrategy instantiationStrategy) {
        this.instantiationStrategy = instantiationStrategy;
    }

}
&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;这个类的内容稍微有点长，主要包括三个方法：createBean、createBeanInstance、applyPropertyValues，这里我们主要关注 createBean 的方法中调用的 applyPropertyValues 方法。&lt;/li&gt;
&lt;li&gt;在 applyPropertyValues 中，通过获取 &lt;code&gt;beanDefinition.getPropertyValues()&lt;/code&gt; 循环进行属性填充操作，&lt;strong&gt;如果遇到的是 BeanReference，那么就需要递归获取 Bean 实例，调用 getBean 方法。&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;当把依赖的 Bean 对象创建完成后，会递归回现在属性填充中。这里需要注意我们并没有去处理循环依赖的问题，这部分内容较大，后续补充。&lt;em&gt;BeanUtil.setFieldValue(bean, name, value) 是 hutool-all 工具类中的方法，你也可以自己实现&lt;/em&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;疑问&lt;/h3&gt;
&lt;p&gt;如何理解spring中BeanReference？&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;在 Spring 框架中，&lt;code&gt;BeanReference&lt;/code&gt; 通常用于表示一个对另一个 Bean 的引用。它是 Spring IoC 容器中处理依赖关系的一部分，用于描述一个 Bean 对另一个 Bean 的依赖。&lt;/p&gt;
&lt;p&gt;具体来说，&lt;code&gt;BeanReference&lt;/code&gt; 类一般包含了被引用 Bean 的名称（或者其他标识符），以及可能的一些其他属性。它的存在主要是为了在实际需要使用被引用 Bean 的地方，延迟实际的 Bean 实例化和获取，而只保留一个引用信息。这有助于解决循环依赖等问题。&lt;/p&gt;
&lt;p&gt;以下是一个简单的示例来理解 &lt;code&gt;BeanReference&lt;/code&gt; 的概念：&lt;/p&gt;
&lt;p&gt;假设有两个类 &lt;code&gt;ClassA&lt;/code&gt; 和 &lt;code&gt;ClassB&lt;/code&gt;，其中 &lt;code&gt;ClassA&lt;/code&gt; 需要引用 &lt;code&gt;ClassB&lt;/code&gt;：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;public class ClassA {
    private ClassB classB;

    public void setClassB(ClassB classB) {
        this.classB = classB;
    }

    // 其他业务逻辑...
}

public class ClassB {
    // 一些属性和方法...
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;在 Spring 配置文件中，可以使用 &lt;code&gt;BeanReference&lt;/code&gt; 来表示对 &lt;code&gt;ClassB&lt;/code&gt; 的引用：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&amp;lt;bean id=&quot;classB&quot; class=&quot;com.example.ClassB&quot;/&amp;gt;

&amp;lt;bean id=&quot;classA&quot; class=&quot;com.example.ClassA&quot;&amp;gt;
    &amp;lt;property name=&quot;classB&quot;&amp;gt;
        &amp;lt;ref bean=&quot;classB&quot;/&amp;gt;
    &amp;lt;/property&amp;gt;
&amp;lt;/bean&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;在这个示例中，&lt;code&gt;&amp;lt;ref bean=&quot;classB&quot;/&amp;gt;&lt;/code&gt; 使用了 &lt;code&gt;BeanReference&lt;/code&gt;，表示 &lt;code&gt;ClassA&lt;/code&gt; 对 &lt;code&gt;ClassB&lt;/code&gt; 的引用。当 Spring IoC 容器创建 &lt;code&gt;ClassA&lt;/code&gt; 实例时，它会将对 &lt;code&gt;ClassB&lt;/code&gt; 的引用注入到 &lt;code&gt;ClassA&lt;/code&gt; 的 &lt;code&gt;classB&lt;/code&gt; 属性中。&lt;/p&gt;
&lt;p&gt;在 Spring 的内部实现中，&lt;code&gt;BeanReference&lt;/code&gt; 对象存储了被引用 Bean 的名称，而不是直接存储对目标 Bean 的引用。这样可以延迟获取实际的 Bean 实例，从而解决循环依赖等问题。&lt;/p&gt;
&lt;p&gt;总之，&lt;code&gt;BeanReference&lt;/code&gt; 是 Spring 中用于描述 Bean 之间依赖关系的一种机制，它使得容器能够在需要的时候，按需获取被引用 Bean 的实例。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2&gt;第 6 章：气吞山河，设计与实现资源加载器，从Spring.xml解析和注册Bean对象&lt;/h2&gt;
&lt;h3&gt;二、目标&lt;/h3&gt;
&lt;p&gt;在完成 Spring 的框架雏形后，现在我们可以通过单元测试进行手动操作 Bean 对象的定义、注册和属性填充，以及最终获取对象调用方法。但这里会有一个问题，就是如果实际使用这个 Spring 框架，是不太可能让用户通过手动方式创建的，而是最好能通过配置文件的方式简化创建过程。需要完成如下操作：&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://tangtangit.com/_astro/spring-6-01.DmH53MSF_ZmdGCv.webp&quot; alt=&quot;img&quot; /&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;如图中我们需要把步骤：2、3、4整合到Spring框架中，通过 Spring 配置文件的方式将 Bean 对象实例化。&lt;/li&gt;
&lt;li&gt;接下来我们就需要在现有的 Spring 框架中，添加能解决 Spring 配置的读取、解析、注册Bean的操作。&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;三、设计&lt;/h3&gt;
&lt;p&gt;依照本章节的需求背景，我们需要在现有的 Spring 框架雏形中添加一个资源解析器，也就是能读取classpath、本地文件和云文件的配置内容。这些配置内容就是像使用 Spring 时配置的 Spring.xml 一样，里面会包括 Bean 对象的描述和属性信息。 在读取配置文件信息后，接下来就是对配置文件中的 Bean 描述信息解析后进行注册操作，把 Bean 对象注册到 Spring 容器中。整体设计结构如下图：&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://tangtangit.com/_astro/spring-6-02.6B8xqVeo_25qjXY.webp&quot; alt=&quot;img&quot; /&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;资源加载器属于相对独立的部分，它位于 Spring 框架核心包下的IO实现内容，主要用于处理Class、本地和云环境中的文件信息。&lt;/li&gt;
&lt;li&gt;当资源可以加载后，接下来就是解析和注册 Bean 到 Spring 中的操作，这部分实现需要和 DefaultListableBeanFactory 核心类结合起来，因为你所有的解析后的注册动作，都会把 Bean 定义信息放入到这个类中。&lt;/li&gt;
&lt;li&gt;那么在实现的时候就设计好接口的实现层级关系，包括我们需要定义出 Bean 定义的读取接口 &lt;code&gt;BeanDefinitionReader&lt;/code&gt; 以及做好对应的实现类，在实现类中完成对 Bean 对象的解析和注册。&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;四、实现&lt;/h3&gt;
&lt;h4&gt;1. 工程结构&lt;/h4&gt;
&lt;pre&gt;&lt;code&gt;small-spring-step-05
└── src
    ├── main
    │   └── java
    │       └── cn.bugstack.springframework  
    │           ├── beans
    │           │   ├── factory
    │           │   │   ├── config
    │           │   │   │   ├── AutowireCapableBeanFactory.java
    │           │   │   │   ├── BeanDefinition.java
    │           │   │   │   ├── BeanReference.java
    │           │   │   │   ├── ConfigurableBeanFactory.java
    │           │   │   │   └── SingletonBeanRegistry.java
    │           │   │   ├── support
    │           │   │   │   ├── AbstractAutowireCapableBeanFactory.java
    │           │   │   │   ├── AbstractBeanDefinitionReader.java
    │           │   │   │   ├── AbstractBeanFactory.java
    │           │   │   │   ├── BeanDefinitionReader.java
    │           │   │   │   ├── BeanDefinitionRegistry.java
    │           │   │   │   ├── CglibSubclassingInstantiationStrategy.java
    │           │   │   │   ├── DefaultListableBeanFactory.java
    │           │   │   │   ├── DefaultSingletonBeanRegistry.java
    │           │   │   │   ├── InstantiationStrategy.java
    │           │   │   │   └── SimpleInstantiationStrategy.java  
    │           │   │   ├── xml
    │           │   │   │   └── XmlBeanDefinitionReader.java
    │           │   │   ├── BeanFactory.java
    │           │   │   ├── ConfigurableListableBeanFactory.java
    │           │   │   ├── HierarchicalBeanFactory.java
    │           │   │   └── ListableBeanFactory.java
    │           │   ├── BeansException.java
    │           │   ├── PropertyValue.java
    │           │   └── PropertyValues.java 
    │           ├── core.io
    │           │   ├── ClassPathResource.java 
    │           │   ├── DefaultResourceLoader.java 
    │           │   ├── FileSystemResource.java 
    │           │   ├── Resource.java 
    │           │   ├── ResourceLoader.java 
    │           │   └── UrlResource.java
    │           └── utils
    │               └── ClassUtils.java
    └── test
        └── java
            └── cn.bugstack.springframework.test
                ├── bean
                │   ├── UserDao.java
                │   └── UserService.java
                └── ApiTest.java
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;工程源码&lt;/strong&gt;：&lt;code&gt;公众号「bugstack虫洞栈」，回复：Spring 专栏，获取完整源码&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;Spring Bean 容器资源加载和使用类关系，如图 6-3&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://tangtangit.com/_astro/spring-6-03.WCSSKg1w_Z1G8z4R.webp&quot; alt=&quot;图 6-3&quot; /&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;本章节为了能把 Bean 的定义、注册和初始化交给 Spring.xml 配置化处理，那么就需要实现两大块内容，分别是：资源加载器、xml资源处理类，实现过程主要以对接口 &lt;code&gt;Resource&lt;/code&gt;、&lt;code&gt;ResourceLoader&lt;/code&gt; 的实现，而另外 &lt;code&gt;BeanDefinitionReader&lt;/code&gt; 接口则是对资源的具体使用，将配置信息注册到 Spring 容器中去。&lt;/li&gt;
&lt;li&gt;在 Resource 的资源加载器的实现中包括了，ClassPath、系统文件、云配置文件，这三部分与 Spring 源码中的设计和实现保持一致，最终在 DefaultResourceLoader 中做具体的调用。&lt;/li&gt;
&lt;li&gt;接口：BeanDefinitionReader、抽象类：AbstractBeanDefinitionReader、实现类：XmlBeanDefinitionReader，这三部分内容主要是合理清晰的处理了资源读取后的注册 Bean 容器操作。&lt;em&gt;接口管定义，抽象类处理非接口功能外的注册Bean组件填充，最终实现类即可只关心具体的业务实现&lt;/em&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;另外本章节还参考 Spring 源码，做了相应接口的集成和实现的关系，虽然这些接口目前还并没有太大的作用，但随着框架的逐步完善，它们也会发挥作用。如图 6-4&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://tangtangit.com/_astro/spring-6-04.BwElY2Oj_HYXHN.webp&quot; alt=&quot;图 6-4&quot; /&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;BeanFactory，已经存在的 Bean 工厂接口用于获取 Bean 对象，这次新增加了按照类型获取 Bean 的方法：&lt;code&gt;&amp;lt;T&amp;gt; T getBean(String name, Class&amp;lt;T&amp;gt; requiredType)&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;ListableBeanFactory，是一个扩展 Bean 工厂接口的接口，新增加了 &lt;code&gt;getBeansOfType&lt;/code&gt;、&lt;code&gt;getBeanDefinitionNames()&lt;/code&gt; 方法，在 Spring 源码中还有其他扩展方法。&lt;/li&gt;
&lt;li&gt;HierarchicalBeanFactory，在 Spring 源码中它提供了可以获取父类 BeanFactory 方法，属于是一种扩展工厂的层次子接口。&lt;em&gt;Sub-interface implemented by bean factories that can be part of a hierarchy.&lt;/em&gt;&lt;/li&gt;
&lt;li&gt;AutowireCapableBeanFactory，是一个自动化处理Bean工厂配置的接口，目前案例工程中还没有做相应的实现，后续逐步完善。&lt;/li&gt;
&lt;li&gt;ConfigurableBeanFactory，可获取 BeanPostProcessor、BeanClassLoader等的一个配置化接口。&lt;/li&gt;
&lt;li&gt;ConfigurableListableBeanFactory，提供分析和修改Bean以及预先实例化的操作接口，不过目前只有一个 getBeanDefinition 方法。&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;解析&lt;/h3&gt;
&lt;p&gt;1、回顾与计划&lt;/p&gt;
&lt;p&gt;① 之前在测试类中将UserService、uId、UserDao组装为PropertyValue、PropertyValues、BeanDefinition并注入BeanDefinition容器后，再进行后续getBean操作的&lt;/p&gt;
&lt;p&gt;② 希望将上述步骤改为通过配置文件操作，与代码解耦，且便于维护&lt;/p&gt;
&lt;p&gt;③ Spring框架是通过解析classpath:spring.xml获取配置信息&lt;/p&gt;
&lt;p&gt;2、项目更新 - 资源&lt;/p&gt;
&lt;p&gt;① +Source：定义获取InputStream流的接口&lt;/p&gt;
&lt;p&gt;② +ClassPathResource：ClassUtils.getDefaultClassLoader().getResourceAsStream(path)&lt;/p&gt;
&lt;p&gt;③ +FileSystemResource：new FileInputStream(new File(path))&lt;/p&gt;
&lt;p&gt;④ +UrlResource：url.openConnection().getInputStream()&lt;/p&gt;
&lt;p&gt;3、项目更新 - 资源加载&lt;/p&gt;
&lt;p&gt;① +ResourceLoader：定义获取资源的接口&lt;/p&gt;
&lt;p&gt;② +DefaultResourceLoader：实现接口，按照location判断并执行获取Resource&lt;/p&gt;
&lt;p&gt;4、项目更新 - 资源解析&lt;/p&gt;
&lt;p&gt;① +BeanDefinitionReader：定义获取BeanDefinition注册器、资源加载器、加载BeanDefinition的接口&lt;/p&gt;
&lt;p&gt;② +AbstractBeanDefinitionReader：通过构造函数的形式，让调用方传入BeanDefinition注册器、资源加载器（目前资源加载器是通过new的方式实例化的），并对外提供BeanDefinition注册器、资源加载器&lt;/p&gt;
&lt;p&gt;③ +XmlBeanDefinitionReader：继承AbstractBeanDefinitionReader，调用资源加载器加载Resource，从Resource中获取InputStream，解析InputStream，将属性封装成PropertyValues，将Class封装成BeanDefinition并注入BeanDefinition容器&lt;/p&gt;
&lt;p&gt;5、调用流程&lt;/p&gt;
&lt;p&gt;① 自定义spring.xml配置UserService、uId、UserDao&lt;/p&gt;
&lt;p&gt;② 实例化DefaultListableBeanFactory，得到了向BeanDefinition容器注册数据的功能&lt;/p&gt;
&lt;p&gt;③ 实例化XmlBeanDefinitionReader，又得到了资源加载的功能&lt;/p&gt;
&lt;p&gt;④ 使用资源解析器解析classpath:spring.xml，得到Resource，再从Resource中获取InputStream&lt;/p&gt;
&lt;p&gt;⑤ 解析InputStream，将属性封装成PropertyValues，存入将Class、PropertyValue存入BeanDefinition，将BeanDefinition注入BeanDefinition容器&lt;/p&gt;
&lt;p&gt;⑥ 调用getBean方法，可以正常获取BeanDefinition并实例化Class&lt;/p&gt;
&lt;h3&gt;测试&lt;/h3&gt;
&lt;p&gt;&lt;img src=&quot;https://tangtangit.com/_astro/image-20231130192333780.DBQx88-W_cInCn.webp&quot; alt=&quot;image-20231130192333780&quot; /&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://tangtangit.com/_astro/image-20231130192315101.Dbi3CDnQ_Z1Q6wJQ.webp&quot; alt=&quot;image-20231130192315101&quot; /&gt;&lt;/p&gt;
&lt;h3&gt;资源类&lt;/h3&gt;
&lt;h4&gt;设计&lt;/h4&gt;
&lt;p&gt;&lt;img src=&quot;https://tangtangit.com/_astro/Resource.CLIRTkZ9_Z15YdqI.webp&quot; alt=&quot;Resource&quot; /&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;public interface Resource {

    InputStream getInputStream() throws IOException;

}
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;package org.example.core.io;

import cn.hutool.core.lang.Assert;

import java.io.IOException;
import java.io.InputStream;
import java.net.HttpURLConnection;
import java.net.URL;
import java.net.URLConnection;

public class UrlResource implements Resource{

    private final URL url;

    public UrlResource(URL url) {
        Assert.notNull(url,&quot;URL must not be null&quot;);
        this.url = url;
    }

    @Override
    public InputStream getInputStream() throws IOException {
        URLConnection con = this.url.openConnection();
        try {
            return con.getInputStream();
        }
        catch (IOException ex){
            if (con instanceof HttpURLConnection){
                ((HttpURLConnection) con).disconnect();
            }
            throw ex;
        }
    }

}
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;package org.example.core.io;

import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.nio.file.Files;

public class FileSystemResource implements Resource {

    private final File file;

    private final String path;

    public FileSystemResource(File file) {
        this.file = file;
        this.path = file.getPath();
    }

    public FileSystemResource(String path) {
        this.file = new File(path);
        this.path = path;
    }

    @Override
    public InputStream getInputStream() throws IOException {
        return Files.newInputStream(this.file.toPath());
    }

    public final String getPath() {
        return this.path;
    }

}
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;package org.example.core.io;


import cn.hutool.core.lang.Assert;
import org.example.util.ClassUtils;

import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.InputStream;

public class ClassPathResource implements Resource {

    private final String path;

    private ClassLoader classLoader;

    public ClassPathResource(String path) {
        this(path, (ClassLoader) null);
    }

    public ClassPathResource(String path, ClassLoader classLoader) {
        Assert.notNull(path, &quot;Path must not be null&quot;);
        this.path = path;
        this.classLoader = (classLoader != null ? classLoader : ClassUtils.getDefaultClassLoader());
    }

    @Override
    public InputStream getInputStream() throws IOException {
        InputStream is = classLoader.getResourceAsStream(path);
        if (is == null) {
            throw new FileNotFoundException(
                    this.path + &quot; cannot be opened because it does not exist&quot;);
        }
        return is;
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h4&gt;测试&lt;/h4&gt;
&lt;pre&gt;&lt;code&gt;package org.example.test;

import cn.hutool.core.io.IoUtil;
import org.example.core.io.DefaultResourceLoader;
import org.example.core.io.Resource;
import org.junit.Before;
import org.junit.Test;

import java.io.IOException;
import java.io.InputStream;


public class ResourceTest {
    private DefaultResourceLoader resourceLoader;

    @Before
    public void init() {
        resourceLoader = new DefaultResourceLoader();
    }

    @Test
    public void test_classpath() throws IOException {
        Resource resource = resourceLoader.getResource(&quot;classpath:important.properties&quot;);
        InputStream inputStream = resource.getInputStream();
        String content = IoUtil.readUtf8(inputStream);
        System.out.println(content);
    }

    @Test
    public void test_file() throws IOException {
        Resource resource = resourceLoader.getResource(&quot;src/test/java/resources/important.properties&quot;);
        InputStream inputStream = resource.getInputStream();
        String content = IoUtil.readUtf8(inputStream);
        System.out.println(content);
    }

    @Test
    public void test_url() throws IOException {
        // Resource resource = resourceLoader.getResource(&quot;https://github.com/fuzhengwei/small-spring/important.properties&quot;);
        Resource resource = resourceLoader.getResource(&quot;https://github.com/fuzhengwei/small-spring/blob/main/important.properties&quot;);
        InputStream inputStream = resource.getInputStream();
        String content = IoUtil.readUtf8(inputStream);
        System.out.println(content);
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;第 7 章：所向披靡，实现应用上下文，自动识别、资源加载、扩展机制&lt;/h2&gt;
&lt;h3&gt;正确理解应用上下文&lt;/h3&gt;
&lt;blockquote&gt;
&lt;p&gt;当我们谈到&quot;应用上下文&quot;时，你可以把它想象成一个大管家，管理着整个应用程序中的各种组件和资源。这个&quot;管家&quot;知道哪些类、对象、服务在应用中，它们之间的关系是什么，以及如何创建它们。&lt;/p&gt;
&lt;p&gt;可以把应用上下文比作一个大的容器，里面装着你整个应用程序所需的各种东西。当你需要某个组件（比如一个服务、一个对象），你可以向这个容器（应用上下文）发出请求，然后它会为你提供需要的东西。&lt;/p&gt;
&lt;p&gt;这个&quot;管家&quot;的作用就是帮你处理应用中各个部分的关系，让它们能够协同工作而不需要你亲自操心。这样一来，你的代码更加模块化、可维护，因为你不必亲自管理每个组件之间的关系和创建过程。&lt;/p&gt;
&lt;p&gt;在Spring框架中，应用上下文是一个重要的概念，它实现了控制反转和依赖注入，使得整个应用程序的组件能够更加灵活、可扩展。因此，理解应用上下文就是理解整个应用是如何被组织和管理的，以及如何实现了一些重要的设计原则，如松耦合和模块化。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h3&gt;二、目标&lt;/h3&gt;
&lt;p&gt;如果你在自己的实际工作中开发过基于 Spring 的技术组件，或者学习过关于 &lt;a href=&quot;https://juejin.cn/book/6940996508632219689&quot;&gt;SpringBoot 中间件设计和开发 (opens new window)&lt;/a&gt;等内容。那么你一定会继承或者实现了 Spring 对外暴露的类或接口，在接口的实现中获取了 BeanFactory 以及 Bean 对象的获取等内容，并对这些内容做一些操作，例如：修改 Bean 的信息，添加日志打印、处理数据库路由对数据源的切换、给 RPC 服务连接注册中心等。&lt;/p&gt;
&lt;p&gt;在对容器中 Bean 的实例化过程添加扩展机制的同时，还需要把目前关于 Spring.xml 初始化和加载策略进行优化，因为我们不太可能让面向 Spring 本身开发的 &lt;code&gt;DefaultListableBeanFactory&lt;/code&gt; 服务，直接给予用户使用。修改点如下：&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://tangtangit.com/_astro/spring-7-01.D3i4RUgc_ZCRkLd.webp&quot; alt=&quot;img&quot; /&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;DefaultListableBeanFactory、XmlBeanDefinitionReader，是我们在目前 Spring 框架中对于服务功能测试的使用方式，它能很好的体现出 Spring 是如何对 xml 加载以及注册Bean对象的操作过程，但这种方式是面向 Spring 本身的，还不具备一定的扩展性。&lt;/li&gt;
&lt;li&gt;就像我们现在需要提供出一个可以在 Bean 初始化过程中，完成对 Bean 对象的扩展时，就很难做到自动化处理。所以我们要把 Bean 对象扩展机制功能和对 Spring 框架上下文的包装融合起来，对外提供完整的服务。&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;三、设计&lt;/h3&gt;
&lt;p&gt;为了能满足于在 Bean 对象从注册到实例化的过程中执行用户的自定义操作，就需要在 Bean 的定义和初始化过程中插入接口类，这个接口再有外部去实现自己需要的服务。那么在结合对 Spring 框架上下文的处理能力，就可以满足我们的目标需求了。整体设计结构如下图：&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://tangtangit.com/_astro/spring-7-02.CNqSYrTL_Z1j9gDH.webp&quot; alt=&quot;img&quot; /&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;满足于对 Bean 对象扩展的两个接口，其实也是 Spring 框架中非常具有重量级的两个接口：&lt;code&gt;BeanFactoryPostProcessor&lt;/code&gt; 和 &lt;code&gt;BeanPostProcessor&lt;/code&gt;，也几乎是大家在使用 Spring 框架额外新增开发自己组建需求的两个必备接口。&lt;/li&gt;
&lt;li&gt;BeanFactoryPostProcessor（bean工场后置处理器），是由 Spring 框架组建提供的容器扩展机制，允许在 Bean 对象注册后但未实例化之前（记忆技巧：既然没有实例化那么就是属于工场的），对 Bean 的定义信息 &lt;code&gt;BeanDefinition&lt;/code&gt; 执行修改操作。&lt;/li&gt;
&lt;li&gt;BeanPostProcessor，也是 Spring 提供的扩展机制，不过 BeanPostProcessor 是在 Bean 对象实例化之后修改 Bean 对象（记忆技巧：实例化以后就是bean后置处理器），也可以替换 Bean 对象。这部分与后面要实现的 AOP 有着密切的关系。&lt;/li&gt;
&lt;li&gt;同时如果只是添加这两个接口，不做任何包装，那么对于使用者来说还是非常麻烦的。我们希望于开发 Spring 的上下文操作类，把相应的 XML 加载 、注册、实例化以及新增的修改和扩展都融合进去，让 Spring 可以自动扫描到我们的新增服务，便于用户使用。&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;四、实现&lt;/h3&gt;
&lt;h4&gt;1. 工程结构&lt;/h4&gt;
&lt;pre&gt;&lt;code&gt;small-spring-step-06
└── src
    ├── main
    │   └── java
    │       └── cn.bugstack.springframework
    │           ├── beans
    │           │   ├── factory
    │           │   │   ├── config
    │           │   │   │   ├── AutowireCapableBeanFactory.java
    │           │   │   │   ├── BeanDefinition.java
    │           │   │   │   ├── BeanFactoryPostProcessor.java
    │           │   │   │   ├── BeanPostProcessor.java
    │           │   │   │   ├── BeanReference.java
    │           │   │   │   ├── ConfigurableBeanFactory.java
    │           │   │   │   └── SingletonBeanRegistry.java
    │           │   │   ├── support
    │           │   │   │   ├── AbstractAutowireCapableBeanFactory.java
    │           │   │   │   ├── AbstractBeanDefinitionReader.java
    │           │   │   │   ├── AbstractBeanFactory.java
    │           │   │   │   ├── BeanDefinitionReader.java
    │           │   │   │   ├── BeanDefinitionRegistry.java
    │           │   │   │   ├── CglibSubclassingInstantiationStrategy.java
    │           │   │   │   ├── DefaultListableBeanFactory.java
    │           │   │   │   ├── DefaultSingletonBeanRegistry.java
    │           │   │   │   ├── InstantiationStrategy.java
    │           │   │   │   └── SimpleInstantiationStrategy.java  
    │           │   │   ├── support
    │           │   │   │   └── XmlBeanDefinitionReader.java
    │           │   │   ├── BeanFactory.java
    │           │   │   ├── ConfigurableListableBeanFactory.java
    │           │   │   ├── HierarchicalBeanFactory.java
    │           │   │   └── ListableBeanFactory.java
    │           │   ├── BeansException.java
    │           │   ├── PropertyValue.java
    │           │   └── PropertyValues.java 
    │           ├── context
    │           │   ├── support
    │           │   │   ├── AbstractApplicationContext.java 
    │           │   │   ├── AbstractRefreshableApplicationContext.java 
    │           │   │   ├── AbstractXmlApplicationContext.java 
    │           │   │   └── ClassPathXmlApplicationContext.java 
    │           │   ├── ApplicationContext.java 
    │           │   └── ConfigurableApplicationContext.java
    │           ├── core.io
    │           │   ├── ClassPathResource.java 
    │           │   ├── DefaultResourceLoader.java 
    │           │   ├── FileSystemResource.java 
    │           │   ├── Resource.java 
    │           │   ├── ResourceLoader.java 
    │           │   └── UrlResource.java
    │           └── utils
    │               └── ClassUtils.java
    └── test
        └── java
            └── cn.bugstack.springframework.test
                ├── bean
                │   ├── UserDao.java
                │   └── UserService.java  
                ├── common
                │   ├── MyBeanFactoryPostProcessor.java
                │   └── MyBeanPostProcessor.java
                └── ApiTest.java
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;工程源码&lt;/strong&gt;：&lt;code&gt;公众号「bugstack虫洞栈」，回复：Spring 专栏，获取完整源码&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;Spring 应用上下文和对Bean对象扩展机制的类关系，如图 7-3&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://tangtangit.com/_astro/spring-7-03.COlRnVNG_2kVb1c.webp&quot; alt=&quot;图 7-3&quot; /&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;在整个类图中主要体现出来的是关于 Spring 应用上下文以及对 Bean 对象扩展机制的实现。&lt;/li&gt;
&lt;li&gt;以继承了 ListableBeanFactory 接口的 ApplicationContext 接口开始，扩展出一系列应用上下文的抽象实现类，并最终完成 &lt;code&gt;ClassPathXmlApplicationContext&lt;/code&gt; 类的实现。而这个类就是最后交给用户使用的类。&lt;/li&gt;
&lt;li&gt;同时在实现应用上下文的过程中，通过定义接口：&lt;code&gt;BeanFactoryPostProcessor&lt;/code&gt;、&lt;code&gt;BeanPostProcessor&lt;/code&gt; 两个接口，把关于对 Bean 的扩展机制串联进去了。&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;收获&lt;/h3&gt;
&lt;h4&gt;refresh方法&lt;/h4&gt;
&lt;pre&gt;&lt;code&gt;    @Override
    public void refresh() throws BeansException {
        // 1. 创建 BeanFactory，并加载 BeanDefinition
        refreshBeanFactory();

        // 2. 获取 BeanFactory
        ConfigurableListableBeanFactory beanFactory = getBeanFactory();

        // 3. 在 Bean 实例化之前，执行 BeanFactoryPostProcessor (Invoke factory processors registered as beans in the context.)
        invokeBeanFactoryPostProcessors(beanFactory);

        // 4. BeanPostProcessor 需要提前于其他 Bean 对象实例化之前执行注册操作
        //因为bean后置处理器是用于修改实例化对象后的操作，所以必须得提前于实例化对象前进行注册。
        registerBeanPostProcessors(beanFactory);

        // 5. 提前实例化单例Bean对象
        beanFactory.preInstantiateSingletons();
    }
&lt;/code&gt;&lt;/pre&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;refreshBeanFactory()&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;创建 BeanFactory&lt;/li&gt;
&lt;li&gt;加载 BeanDefinition（在Bean 对象注册后但未实例化），也就是说加载bean的定义到spring的BeanDefinition集合中&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;beanFactory.preInstantiateSingletons();&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;提前实例化单例Bean对象，回去调用doGetBean中的createBean方法并实例化对象&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;故前文的两个扩展点：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;BeanFactoryPostProcessor&lt;/strong&gt;，是由 Spring 框架组建提供的容器扩展机制，允许在 &lt;strong&gt;Bean 对象注册后但未实例化之前&lt;/strong&gt;，对 Bean 的定义信息 &lt;code&gt;BeanDefinition&lt;/code&gt; 执行修改操作。&lt;strong&gt;发生在refreshBeanFactory()方法后。&lt;/strong&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;BeanPostProcessor&lt;/strong&gt;，也是 Spring 提供的扩展机制，不过 BeanPostProcessor 是在 &lt;strong&gt;Bean 对象实例化之后修改 Bean 对象&lt;/strong&gt;，也可以替换 Bean 对象。这部分与后面要实现的 AOP 有着密切的关系。&lt;strong&gt;发生在createBean中的实例化对象后。&lt;/strong&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h4&gt;维护了两个后置处理集合&lt;/h4&gt;
&lt;ul&gt;
&lt;li&gt;Map&amp;lt;String, BeanFactoryPostProcessor&amp;gt; beanFactoryPostProcessorMap&lt;/li&gt;
&lt;/ul&gt;
&lt;blockquote&gt;
&lt;p&gt;作用：在所有的 BeanDefinition 加载完成后，实例化 Bean 对象之前，提供修改 BeanDefinition 属性的机制。&lt;/p&gt;
&lt;p&gt;该集合由DefaultListableBeanFactory类，通过getBeansOfType组装而成。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;@Override
public &amp;lt;T&amp;gt; Map&amp;lt;String, T&amp;gt; getBeansOfType(Class&amp;lt;T&amp;gt; type) throws BeansException {
    Map&amp;lt;String, T&amp;gt; result = new HashMap&amp;lt;&amp;gt;();
    beanDefinitionMap.forEach((beanName, beanDefinition) -&amp;gt; {
        Class beanClass = beanDefinition.getBeanClass();
        if (type.isAssignableFrom(beanClass)) {
            result.put(beanName, (T) getBean(beanName));
        }
    });
    return result;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;调用时机在&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://tangtangit.com/_astro/image-20231120181329982.MldmZnM1_Z1FYcmh.webp&quot; alt=&quot;image-20231120181329982&quot; /&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;private void invokeBeanFactoryPostProcessors(ConfigurableListableBeanFactory beanFactory) {
    //组装
    Map&amp;lt;String, BeanFactoryPostProcessor&amp;gt; beanFactoryPostProcessorMap = beanFactory.getBeansOfType(BeanFactoryPostProcessor.class);
   	//遍历
    for (BeanFactoryPostProcessor beanFactoryPostProcessor : beanFactoryPostProcessorMap.values()) {
        //执行
        beanFactoryPostProcessor.postProcessBeanFactory(beanFactory);
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;/blockquote&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;beanPostProcessors&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;执行时机initializeBean，也就是对象实例化后&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://tangtangit.com/_astro/image-20231120181530646.CF78vIq6_Z272f9X.webp&quot; alt=&quot;image-20231120181530646&quot; /&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;private Object initializeBean(String beanName, Object bean, BeanDefinition beanDefinition) {
    // 1. 执行 BeanPostProcessor Before 处理
    //遍历beanPostProcessors 然后执行postProcessBeforeInitialization方法，简称before方法
    Object wrappedBean = applyBeanPostProcessorsBeforeInitialization(bean, beanName);

    // 执行 Bean 对象的初始化方法
    try {
        invokeInitMethods(beanName, wrappedBean, beanDefinition);
    } catch (Exception e) {
        throw new BeansException(&quot;Invocation of init method of bean[&quot; + beanName + &quot;] failed&quot;, e);
    }

    // 2. 执行 BeanPostProcessor After 处理
    //遍历beanPostProcessors 然后执行postProcessAfterInitialization，简称After方法
    wrappedBean = applyBeanPostProcessorsAfterInitialization(wrappedBean, beanName);
    return wrappedBean;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;/blockquote&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;第 8 章：龙行有风，向虚拟机注册钩子，实现Bean对象的初始化和销毁方法&lt;/h2&gt;
&lt;h3&gt;二、目标&lt;/h3&gt;
&lt;p&gt;当我们的类创建的 Bean 对象，交给 Spring 容器管理以后，这个类对象就可以被赋予更多的使用能力。就像我们在上一章节已经给类对象添加了修改注册Bean定义未实例化前的属性信息修改和初始化过程中的前置和后置处理，这些额外能力的实现，都可以让我们对现有工程中的类对象做相应的扩展处理。&lt;/p&gt;
&lt;p&gt;那么除此之外我们还希望可以在 Bean 初始化过程，执行一些操作。比如帮我们做一些数据的加载执行，链接注册中心暴露RPC接口以及在Web程序关闭时执行链接断开，内存销毁等操作。&lt;em&gt;如果说没有Spring我们也可以通过构造函数、静态方法以及手动调用的方式实现，但这样的处理方式终究不如把诸如此类的操作都交给 Spring 容器来管理更加合适。&lt;/em&gt; 因此你会看到到 spring.xml 中有如下操作：&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://tangtangit.com/_astro/spring-8-02.DfnV-gEo_2etruq.webp&quot; alt=&quot;img&quot; /&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;需要满足用户可以在 xml 中配置初始化和销毁的方法，也可以通过实现类的方式处理，比如我们在使用 Spring 时用到的 InitializingBean, DisposableBean 两个接口。 -其实还可以有一种是注解的方式处理初始化操作，不过目前还没有实现到注解的逻辑，后续再完善此类功能。&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;三、设计&lt;/h3&gt;
&lt;p&gt;可能面对像 Spring 这样庞大的框架，对外暴露的接口定义使用或者xml配置，完成的一系列扩展性操作，都让 Spring 框架看上去很神秘。其实对于这样在 Bean 容器初始化过程中额外添加的处理操作，无非就是预先执行了一个定义好的接口方法或者是反射调用类中xml中配置的方法，最终你只要按照接口定义实现，就会有 Spring 容器在处理的过程中进行调用而已。整体设计结构如下图：&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://tangtangit.com/_astro/spring-8-03.BDv8q452_282olK.webp&quot; alt=&quot;img&quot; /&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;在 spring.xml 配置中添加 &lt;code&gt;init-method、destroy-method&lt;/code&gt; 两个注解，在配置文件加载的过程中，把注解配置一并定义到 BeanDefinition 的属性当中。这样在 initializeBean 初始化操作的工程中，就可以通过反射的方式来调用配置在 Bean 定义属性当中的方法信息了。另外如果是接口实现的方式，那么直接可以通过 Bean 对象调用对应接口定义的方法即可，&lt;code&gt;((InitializingBean) bean).afterPropertiesSet()&lt;/code&gt;，两种方式达到的效果是一样的。&lt;/li&gt;
&lt;li&gt;除了在初始化做的操作外，&lt;code&gt;destroy-method&lt;/code&gt; 和 &lt;code&gt;DisposableBean&lt;/code&gt; 接口的定义，都会在 Bean 对象初始化完成阶段，执行注册销毁方法的信息到 DefaultSingletonBeanRegistry 类中的 disposableBeans 属性里，这是为了后续统一进行操作。&lt;em&gt;这里还有一段适配器的使用，因为反射调用和接口直接调用，是两种方式。所以需要使用适配器进行包装，下文代码讲解中参考 DisposableBeanAdapter 的具体实现&lt;/em&gt; -关于销毁方法需要在虚拟机执行关闭之前进行操作，所以这里需要用到一个注册钩子的操作，如：&lt;code&gt;Runtime.getRuntime().addShutdownHook(new Thread(() -&amp;gt; System.out.println(&quot;close！&quot;)));&lt;/code&gt; &lt;em&gt;这段代码你可以执行测试&lt;/em&gt;，另外你可以使用手动调用 ApplicationContext.close 方法关闭容器。&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;四、实现&lt;/h3&gt;
&lt;h4&gt;&lt;a href=&quot;https://bugstack.cn/md/spring/develop-spring/2021-06-23-%E7%AC%AC8%E7%AB%A0%EF%BC%9A%E9%BE%99%E8%A1%8C%E6%9C%89%E9%A3%8E%EF%BC%8C%E5%90%91%E8%99%9A%E6%8B%9F%E6%9C%BA%E6%B3%A8%E5%86%8C%E9%92%A9%E5%AD%90%EF%BC%8C%E5%AE%9E%E7%8E%B0Bean%E5%AF%B9%E8%B1%A1%E7%9A%84%E5%88%9D%E5%A7%8B%E5%8C%96%E5%92%8C%E9%94%80%E6%AF%81%E6%96%B9%E6%B3%95.html#_1-%E5%B7%A5%E7%A8%8B%E7%BB%93%E6%9E%84&quot;&gt;#&lt;/a&gt;1. 工程结构&lt;/h4&gt;
&lt;pre&gt;&lt;code&gt;small-spring-step-07
└── src
    ├── main
    │   └── java
    │       └── cn.bugstack.springframework
    │           ├── beans
    │           │   ├── factory
    │           │   │   ├── config
    │           │   │   │   ├── AutowireCapableBeanFactory.java
    │           │   │   │   ├── BeanDefinition.java
    │           │   │   │   ├── BeanFactoryPostProcessor.java
    │           │   │   │   ├── BeanPostProcessor.java
    │           │   │   │   ├── BeanReference.java
    │           │   │   │   ├── ConfigurableBeanFactory.java
    │           │   │   │   └── SingletonBeanRegistry.java
    │           │   │   ├── support
    │           │   │   │   ├── AbstractAutowireCapableBeanFactory.java
    │           │   │   │   ├── AbstractBeanDefinitionReader.java
    │           │   │   │   ├── AbstractBeanFactory.java
    │           │   │   │   ├── BeanDefinitionReader.java
    │           │   │   │   ├── BeanDefinitionRegistry.java
    │           │   │   │   ├── CglibSubclassingInstantiationStrategy.java
    │           │   │   │   ├── DefaultListableBeanFactory.java
    │           │   │   │   ├── DefaultSingletonBeanRegistry.java
    │           │   │   │   ├── DisposableBeanAdapter.java
    │           │   │   │   ├── InstantiationStrategy.java
    │           │   │   │   └── SimpleInstantiationStrategy.java  
    │           │   │   ├── support
    │           │   │   │   └── XmlBeanDefinitionReader.java
    │           │   │   ├── BeanFactory.java
    │           │   │   ├── ConfigurableListableBeanFactory.java
    │           │   │   ├── DisposableBean.java
    │           │   │   ├── HierarchicalBeanFactory.java
    │           │   │   ├── InitializingBean.java
    │           │   │   └── ListableBeanFactory.java
    │           │   ├── BeansException.java
    │           │   ├── PropertyValue.java
    │           │   └── PropertyValues.java 
    │           ├── context
    │           │   ├── support
    │           │   │   ├── AbstractApplicationContext.java 
    │           │   │   ├── AbstractRefreshableApplicationContext.java 
    │           │   │   ├── AbstractXmlApplicationContext.java 
    │           │   │   └── ClassPathXmlApplicationContext.java 
    │           │   ├── ApplicationContext.java 
    │           │   └── ConfigurableApplicationContext.java
    │           ├── core.io
    │           │   ├── ClassPathResource.java 
    │           │   ├── DefaultResourceLoader.java 
    │           │   ├── FileSystemResource.java 
    │           │   ├── Resource.java 
    │           │   ├── ResourceLoader.java 
    │           │   └── UrlResource.java
    │           └── utils
    │               └── ClassUtils.java
    └── test
        └── java
            └── cn.bugstack.springframework.test
                ├── bean
                │   ├── UserDao.java
                │   └── UserService.java
                └── ApiTest.java
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;工程源码&lt;/strong&gt;：&lt;code&gt;公众号「bugstack虫洞栈」，回复：Spring 专栏，获取完整源码&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;Spring 应用上下文和对Bean对象扩展机制的类关系，如图 8-4&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://tangtangit.com/_astro/spring-8-04.VBC_3YFn_TF5ta.webp&quot; alt=&quot;图 8-4&quot; /&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;以上整个类图结构描述出来的就是本次新增 Bean 实例化过程中的初始化方法和销毁方法。&lt;/li&gt;
&lt;li&gt;因为我们一共实现了两种方式的初始化和销毁方法，xml配置和定义接口，所以这里既有 InitializingBean、DisposableBean 也有需要 XmlBeanDefinitionReader 加载 spring.xml 配置信息到 BeanDefinition 中。&lt;/li&gt;
&lt;li&gt;另外接口 ConfigurableBeanFactory 定义了 destroySingletons 销毁方法，并由 AbstractBeanFactory 继承的父类 DefaultSingletonBeanRegistry 实现 ConfigurableBeanFactory 接口定义的 destroySingletons 方法。&lt;em&gt;这种方式的设计可能数程序员是没有用过的，都是用的谁实现接口谁完成实现类，而不是把实现接口的操作又交给继承的父类处理。所以这块还是蛮有意思的，是一种不错的隔离分层服务的设计方式&lt;/em&gt;&lt;/li&gt;
&lt;li&gt;最后就是关于向虚拟机注册钩子，保证在虚拟机关闭之前，执行销毁操作。&lt;code&gt;Runtime.getRuntime().addShutdownHook(new Thread(() -&amp;gt; System.out.println(&quot;close！&quot;)));&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;解析&lt;/h3&gt;
&lt;p&gt;1、学习目标：&lt;/p&gt;
&lt;p&gt;定义、执行初始化和销毁方法&lt;/p&gt;
&lt;p&gt;2、回顾和计划&lt;/p&gt;
&lt;p&gt;① 上一章实现了应用上下文，概括为：以容器基础功能为前提，扩展了容器的使用方式，使Spring容器能够对外提供完整服务&lt;/p&gt;
&lt;p&gt;② 上一章为Class类、Bean对象提供了低耦合的更新操作，即Class类的属性更新、Bean对象初始化过程中的前置和后置处理&lt;/p&gt;
&lt;p&gt;③ 本节还希望Bean对象初始化过程中能够完成一些例如 初始数据加载、链接注册中心暴露RPC接口、程序关闭时断开链接、销毁内存数据等操作&lt;/p&gt;
&lt;p&gt;④ 因此可以在spring.xml中配置或者通过实现类，实现外围功能的嵌入&lt;/p&gt;
&lt;p&gt;3、项目新增&lt;/p&gt;
&lt;p&gt;① +InitializingBean：定义接口方法afterPropertiesSet，实现类可以在方法中实现参数的初始化，比如接口暴露、数据库数据读取、配置文件加载等，该方法在Bean属性填充后调用&lt;/p&gt;
&lt;p&gt;② +DisposableBean：定义销毁方法&lt;/p&gt;
&lt;p&gt;③ +DisposableBeanAdapter：实现了②接口中的销毁方法，该销毁方法提供了两种逻辑 Ⅰ 调用实现了DisposableBean的类的销毁方法 Ⅱ （在spring.xml中定义destroy-method属性，资源解析使将其属性值存入beanDefinition）从beanDefinition中取出destroy方法，利用反射机制method.invoke执行&lt;/p&gt;
&lt;p&gt;4、项目更新&lt;/p&gt;
&lt;p&gt;① BeanDefinition：定义initMethodName、destroyMethodName，解析spring.xml中的init-method、destroy-method存至这两个属性&lt;/p&gt;
&lt;p&gt;② XmlBeanDefinitionReader：解析spring.xml，将init-method、destroy-method存入beanDefinition&lt;/p&gt;
&lt;p&gt;③ AbstractAutowireCapableBeanFactory：获取实例化bean之后，执行 Bean 对象的初始化方法，初始化方法可以是InitializingBean实现类中的初始化方法，也可以是spring.xml中init-method注册的方法&lt;/p&gt;
&lt;p&gt;④ ConfigurableBeanFactory：定义销毁单例容器disposableBeans的方法destroySingletons&lt;/p&gt;
&lt;p&gt;⑤ DefaultSingletonBeanRegistry：实现destroySingletons，遍历移除disposableBeans容器中的元素DisposableBean，每次移除后调用DisposableBean的destroy方法，方法在下面有描述&lt;/p&gt;
&lt;p&gt;⑥ ConfigurableApplicationContext：定义registerShutdownHook、close方法&lt;/p&gt;
&lt;p&gt;⑦ AbstractApplicationContext：实现⑥定义的接口，即定义Runtime.getRuntime().addShutdownHook方法，该方法会开启新线程，在JVM关闭前调用自定义的close方法，close方法会调用⑤中的销毁方法&lt;/p&gt;
&lt;p&gt;5、执行流程&lt;/p&gt;
&lt;p&gt;① spring.xml的UserDao中定义init-method=&quot;initDataMethod&quot;、destroy-method=&quot;destroyDataMethod&quot;&lt;/p&gt;
&lt;p&gt;② 让UserService实现InitializingBean,、DisposableBean接口，则需要实现了初始化和销毁方法，① ②相当于提供了两种初始化、销毁的方法&lt;/p&gt;
&lt;p&gt;③ 在资源解析时将init-method、destroy-method注入beanDefinition&lt;/p&gt;
&lt;p&gt;④ 实例化Class类后，初始化Bean对象时，调用①、②中定义的初始化方法&lt;/p&gt;
&lt;p&gt;⑤ 定义ShutdownHook，在JVM销毁前执行自定义的线程，完成①、②中定义的销毁方法&lt;/p&gt;
&lt;p&gt;6、一些想法&lt;/p&gt;
&lt;p&gt;① DefaultSingletonBeanRegistry就专门负责底层容器的定义，以及对外提供存取、销毁方法，极好地体现了职责分明&lt;/p&gt;
&lt;p&gt;② 只要实现了InitializingBean,、DisposableBean，就可自定义初始、销毁方法，使得方法的调用与定义解耦，极大提升了扩展性&lt;/p&gt;
&lt;p&gt;③ 本章是在bean初始化时的的invokeInitMethods方法中，调用上面定义的初始化方法，上一章是在invokeInitMethods方法的前后通过注入的BeanPostProcessor实现类执行初始化方法，感觉目的是一样的，只是实现方式有差异&lt;/p&gt;
&lt;h2&gt;第 9 章：虎行有雨，定义标记类型Aware接口，实现感知容器对象&lt;/h2&gt;
&lt;h3&gt;二、目标&lt;/h3&gt;
&lt;p&gt;目前已实现的 Spring 框架，在 Bean 操作上能提供出的能力，包括：Bean 对象的定义和注册，以及在操作 Bean 对象过程中执行的，BeanFactoryPostProcessor、BeanPostProcessor、InitializingBean、DisposableBean，以及在 XML 新增的一些配置处理，让我们可以 Bean 对象有更强的操作性。&lt;/p&gt;
&lt;p&gt;那么，如果我们想获得 Spring 框架提供的 BeanFactory、ApplicationContext、BeanClassLoader等这些能力做一些扩展框架的使用时该怎么操作呢。所以我们本章节希望在 Spring 框架中提供一种能感知容器操作的接口，如果谁实现了这样的一个接口，就可以获取接口入参中的各类能力。&lt;/p&gt;
&lt;h3&gt;三、设计&lt;/h3&gt;
&lt;p&gt;如果说我希望拿到 Spring 框架中一些提供的资源，那么首先需要考虑以一个什么方式去获取，之后你定义出来的获取方式，在 Spring 框架中该怎么去承接，实现了这两项内容，就可以扩展出你需要的一些属于 Spring 框架本身的能力了。&lt;/p&gt;
&lt;p&gt;在关于 Bean 对象实例化阶段我们操作过一些额外定义、属性、初始化和销毁的操作，其实我们如果像获取 Spring 一些如 BeanFactory、ApplicationContext 时，也可以通过此类方式进行实现。那么我们需要定义一个标记性的接口，这个接口不需要有方法，它只起到标记作用就可以，而具体的功能由继承此接口的其他功能性接口定义具体方法，最终这个接口就可以通过 &lt;code&gt;instanceof&lt;/code&gt; 进行判断和调用了。整体设计结构如下图：&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://tangtangit.com/_astro/spring-9-01.CgDnUF6v_Z89sx1.webp&quot; alt=&quot;img&quot; /&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;定义接口 Aware，在 Spring 框架中它是一种感知标记性接口，具体的子类定义和实现能感知容器中的相关对象。&lt;em&gt;也就是通过这个桥梁，向具体的实现类中提供容器服务&lt;/em&gt;&lt;/li&gt;
&lt;li&gt;继承 Aware 的接口包括：BeanFactoryAware、BeanClassLoaderAware、BeanNameAware和ApplicationContextAware，当然在 Spring 源码中还有一些其他关于注解的，不过目前我们还是用不到。&lt;/li&gt;
&lt;li&gt;在具体的接口实现过程中你可以看到，一部分(&lt;em&gt;BeanFactoryAware、BeanClassLoaderAware、BeanNameAware&lt;/em&gt;)在 factory 的 support 文件夹下，另外 ApplicationContextAware 是在 context 的 support 中，这是因为不同的内容获取需要在不同的包下提供。所以，在 AbstractApplicationContext 的具体实现中会用到向 beanFactory 添加 BeanPostProcessor 内容的 &lt;code&gt;ApplicationContextAwareProcessor&lt;/code&gt; 操作，最后由 AbstractAutowireCapableBeanFactory 创建 createBean 时处理相应的调用操作。&lt;em&gt;关于 applyBeanPostProcessorsBeforeInitialization 已经在前面章节中实现过，如果忘记可以往前翻翻&lt;/em&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;四、实现&lt;/h3&gt;
&lt;h4&gt;1. 工程结构&lt;/h4&gt;
&lt;pre&gt;&lt;code&gt;small-spring-step-08
└── src
    ├── main
    │   └── java
    │       └── cn.bugstack.springframework
    │           ├── beans
    │           │   ├── factory
    │           │   │   ├── config
    │           │   │   │   ├── AutowireCapableBeanFactory.java
    │           │   │   │   ├── BeanDefinition.java
    │           │   │   │   ├── BeanFactoryPostProcessor.java
    │           │   │   │   ├── BeanPostProcessor.java
    │           │   │   │   ├── BeanReference.java
    │           │   │   │   ├── ConfigurableBeanFactory.java
    │           │   │   │   └── SingletonBeanRegistry.java
    │           │   │   ├── support
    │           │   │   │   ├── AbstractAutowireCapableBeanFactory.java
    │           │   │   │   ├── AbstractBeanDefinitionReader.java
    │           │   │   │   ├── AbstractBeanFactory.java
    │           │   │   │   ├── BeanDefinitionReader.java
    │           │   │   │   ├── BeanDefinitionRegistry.java
    │           │   │   │   ├── CglibSubclassingInstantiationStrategy.java
    │           │   │   │   ├── DefaultListableBeanFactory.java
    │           │   │   │   ├── DefaultSingletonBeanRegistry.java
    │           │   │   │   ├── DisposableBeanAdapter.java
    │           │   │   │   ├── InstantiationStrategy.java
    │           │   │   │   └── SimpleInstantiationStrategy.java  
    │           │   │   ├── support
    │           │   │   │   └── XmlBeanDefinitionReader.java
    │           │   │   ├── Aware.java
    │           │   │   ├── BeanClassLoaderAware.java
    │           │   │   ├── BeanFactory.java
    │           │   │   ├── BeanFactoryAware.java
    │           │   │   ├── BeanNameAware.java
    │           │   │   ├── ConfigurableListableBeanFactory.java
    │           │   │   ├── DisposableBean.java
    │           │   │   ├── HierarchicalBeanFactory.java
    │           │   │   ├── InitializingBean.java
    │           │   │   └── ListableBeanFactory.java
    │           │   ├── BeansException.java
    │           │   ├── PropertyValue.java
    │           │   └── PropertyValues.java 
    │           ├── context
    │           │   ├── support
    │           │   │   ├── AbstractApplicationContext.java 
    │           │   │   ├── AbstractRefreshableApplicationContext.java 
    │           │   │   ├── AbstractXmlApplicationContext.java 
    │           │   │   ├── ApplicationContextAwareProcessor.java 
    │           │   │   └── ClassPathXmlApplicationContext.java 
    │           │   ├── ApplicationContext.java 
    │           │   ├── ApplicationContextAware.java 
    │           │   └── ConfigurableApplicationContext.java
    │           ├── core.io
    │           │   ├── ClassPathResource.java 
    │           │   ├── DefaultResourceLoader.java 
    │           │   ├── FileSystemResource.java 
    │           │   ├── Resource.java 
    │           │   ├── ResourceLoader.java 
    │           │   └── UrlResource.java
    │           └── utils
    │               └── ClassUtils.java
    └── test
        └── java
            └── cn.bugstack.springframework.test
                ├── bean
                │   ├── UserDao.java
                │   └── UserService.java
                └── ApiTest.java
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;工程源码&lt;/strong&gt;：&lt;code&gt;公众号「bugstack虫洞栈」，回复：Spring 专栏，获取完整源码&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;Spring 感知接口的设计和实现类关系，如图 9-2&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://tangtangit.com/_astro/spring-9-02.ClZPnREY_Z3OubS.webp&quot; alt=&quot;图 9-2&quot; /&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;以上整个类关系就是关于 Aware 感知的定义和对容器感知的实现。&lt;/li&gt;
&lt;li&gt;Aware 有四个继承的接口，其他这些接口的继承都是为了继承一个标记，有了标记的存在更方便类的操作和具体判断实现。&lt;/li&gt;
&lt;li&gt;另外由于 ApplicationContext 并不是在 AbstractAutowireCapableBeanFactory 中 createBean 方法下的内容，所以需要像容器中注册 &lt;code&gt;addBeanPostProcessor&lt;/code&gt; ，再由 createBean 统一调用 applyBeanPostProcessorsBeforeInitialization 时进行操作。&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;解析&lt;/h3&gt;
&lt;p&gt;1、学习目标：Aware感知容器对象&lt;/p&gt;
&lt;p&gt;2、回顾与计划&lt;/p&gt;
&lt;p&gt;① 上一章通过两种方式，定义、执行初始化方法和初始化方法&lt;/p&gt;
&lt;p&gt;② 目前Spring框架已经具备了Bean对象定义、实例化、初始化，通过spring.xml新增配置处理等操作，&lt;/p&gt;
&lt;p&gt;③ 本章希望能够获取Spring容器的一些能力，例如BeanFactory、ApplicationContext、BeanClassLoader，这样就可以扩展框架的使用 ④ 所以本章引入Aware及其子接口，来感知、获取Spring内部对象&lt;/p&gt;
&lt;p&gt;3、项目更新&lt;/p&gt;
&lt;p&gt;① +Aware：标记类接口（类似标签），实现该接口可以被Spring容器感知&lt;/p&gt;
&lt;p&gt;② +BeanClassLoaderAware：实现此接口，既能感知到所属的 ClassLoader&lt;/p&gt;
&lt;p&gt;③ +BeanFactoryAware：实现此类，即能感知到所属的BeanFactory&lt;/p&gt;
&lt;p&gt;④ +BeanNameAware：实现此接口，既能感知到所属的 BeanName&lt;/p&gt;
&lt;p&gt;⑤ +ApplicationContextAware：实现此接口，既能感知到所属的 ApplicationContext&lt;/p&gt;
&lt;p&gt;⑥ +ApplicationContextAwareProcessor：继承BeanPostProcessor，那么在Bean初始化时就会调用该类中定义的postProcessBeforeInitialization方法给Bean对象赋applicationContext&lt;/p&gt;
&lt;p&gt;⑦ AbstractApplicationContext：在refresh中调用beanFactory.addBeanPostProcessor(new ApplicationContextAwareProcessor(this))，将⑥新增的类实例化至beanPostProcessors容器&lt;/p&gt;
&lt;p&gt;⑧ AbstractAutowireCapableBeanFactory：在initializeBean方法中执行invokeAwareMethods，即判断本次实例化的Bean实现哪些Aware的子接口，调用setXXX将将Spring内部对象注入Bean中&lt;/p&gt;
&lt;p&gt;⑨ AbstractBeanFactory：定义、实例化ClassLoader，并对外提供获取方法getBeanClassLoader&lt;/p&gt;
&lt;p&gt;4、Aware感知原理&lt;/p&gt;
&lt;p&gt;① Aware 接口的作用是在业务代码中获得 Spring 框架内部对象&lt;/p&gt;
&lt;p&gt;② BeanClassLoaderAware、BeanFactoryAware、BeanNameAware的实现原理：Spring在Bean初始化过程中，执行invokeAwareMethods判断Bean实现的Aware子接口的类型，调用setXXX方法将Spring内部对象注入到Bean中&lt;/p&gt;
&lt;p&gt;③ 除了上面的Aware子接口，还有一部分Aware子接口如ApplicationContextAware等的实现原理：涉及到第7章定义的BeanPostProcessor后置处理器，其作用是在Bean实例化以及属性填充之后，在执行完invokeAwareMethods之后，Spring会获取当前Bean容器中注册的所有BeanPostProcessor实现类的对象（是在spring.xml中定义，上下文refresh时注入beanPostProcessors容器的），利用invoke反射执行其中的逻辑&lt;/p&gt;
&lt;p&gt;④ 负责Aware逻辑的BeanPostProcessor是ApplicationContextAwareProcessor（就可以感知上下文），该类是在上下文refresh时通过new ApplicationContextAwareProcessor(AbstractApplicationContext对象）的方式注入beanPostProcessors容器的&lt;/p&gt;
&lt;p&gt;⑤ ApplicationContextAwareProcessor的postProcessBeforeInitialization方法中调用setXXX将applicationContext注入Bean对象&lt;/p&gt;
&lt;p&gt;⑥ 第7章的BeanPostProcessor的实现类是定义在spring.xml的&amp;lt;bean/&amp;gt;标签中，refresh时通过registerBeanPostProcessors注入beanPostProcessors容器的，&lt;/p&gt;
&lt;p&gt;5、总结&lt;/p&gt;
&lt;p&gt;① 虽然Aware子类生效方式和位置不同，但本质都是在Bean初始化过程中，通过instance判断Bean实现的Aware子接口的类型，调用setXXX方法将Spring内部对象注入Bean中&lt;/p&gt;
&lt;p&gt;② 以下一些Aware子接口也是通过BeanPostProcessor感知容器对象：EnvironmentAware、EmbeddedValueResolverAware、ResourceLoaderAware、ApplicationEventPublisherAware、MessageSourceAware、ApplicationStartupAware&lt;/p&gt;
&lt;p&gt;③ 这种扩展模式将业务代码和框架功能解耦，本质上就是自定义Bean后置处理器，在框架类代码中可以用来暴露自己的内部成员&lt;/p&gt;
&lt;h3&gt;疑问&lt;/h3&gt;
&lt;h4&gt;究竟如何感知&lt;/h4&gt;
&lt;h5&gt;执行点&lt;/h5&gt;
&lt;p&gt;执行感知点：AbstractAutowireCapableBeanFactory类中&lt;/p&gt;
&lt;p&gt;此处只有三个BeanFactoryAware、BeanClassLoaderAware、BeanNameAware感知&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;    private Object initializeBean(String beanName, Object bean, BeanDefinition beanDefinition) {

        // invokeAwareMethods
        if (bean instanceof Aware) {
            if (bean instanceof BeanFactoryAware) {
                ((BeanFactoryAware) bean).setBeanFactory(this);
            }
            if (bean instanceof BeanClassLoaderAware){
                ((BeanClassLoaderAware) bean).setBeanClassLoader(getBeanClassLoader());
            }
            if (bean instanceof BeanNameAware) {
                ((BeanNameAware) bean).setBeanName(beanName);
            }
        }
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;ApplicationContextAware的执行点设置在BeanPostProcessor中，在createBean对象后遍历beanPostProcessors并执行before方法&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;由于该ApplicationContextAware比较特殊，故埋点的位置在AbstractApplicationContext类中refresh();&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://tangtangit.com/_astro/image-20231120183615493.57vneMzp_16H5Ip.webp&quot; alt=&quot;image-20231120183615493&quot; /&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;pre&gt;&lt;code&gt;public class ApplicationContextAwareProcessor implements BeanPostProcessor {

    private final ApplicationContext applicationContext;

    public ApplicationContextAwareProcessor(ApplicationContext applicationContext) {
        this.applicationContext = applicationContext;
    }

    @Override
    public Object postProcessBeforeInitialization(Object bean, String beanName) throws BeansException {
        if (bean instanceof ApplicationContextAware){
            ((ApplicationContextAware) bean).setApplicationContext(applicationContext);
        }
        return bean;
    }

    @Override
    public Object postProcessAfterInitialization(Object bean, String beanName) throws BeansException {
        return bean;
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h5&gt;埋点&lt;/h5&gt;
&lt;p&gt;UserService实现感知接口&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;public class UserService implements BeanNameAware, BeanClassLoaderAware, ApplicationContextAware, BeanFactoryAware {

    private ApplicationContext applicationContext;
    private BeanFactory beanFactory;

    private String uId;
    private String company;
    private String location;
    private UserDao userDao;

    //1工厂
    @Override
    public void setBeanFactory(BeanFactory beanFactory) throws BeansException {
        this.beanFactory = beanFactory;
    }
    //2上下文
    @Override
    public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
        this.applicationContext = applicationContext;
    }
    //3bean名称
    @Override
    public void setBeanName(String name) {
        System.out.println(&quot;Bean Name is：&quot; + name);
    }
    //4bean的类加载器
    @Override
    public void setBeanClassLoader(ClassLoader classLoader) {
        System.out.println(&quot;ClassLoader：&quot; + classLoader);
    }

    public String queryUserInfo() {
        return userDao.queryUserName(uId) + &quot;,&quot; + company + &quot;,&quot; + location;
    }

    public ApplicationContext getApplicationContext() {
        return applicationContext;
    }

    public BeanFactory getBeanFactory() {
        return beanFactory;
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;测试时调用&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;public class ApiTest {
    @Test
    public void test_xml() {
        // 1.初始化 BeanFactory
        ClassPathXmlApplicationContext applicationContext = new ClassPathXmlApplicationContext(&quot;classpath:spring.xml&quot;);
        applicationContext.registerShutdownHook();

        // 2. 获取Bean对象调用方法
        UserService userService = applicationContext.getBean(&quot;userService&quot;, UserService.class);
        String result = userService.queryUserInfo();
        System.out.println(&quot;测试结果：&quot; + result);

        System.out.println(&quot;ApplicationContextAware：&quot;+userService.getApplicationContext());
        System.out.println(&quot;BeanFactoryAware：&quot;+userService.getBeanFactory());

    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;第 10 章：横刀跃马，关于Bean对象作用域以及FactoryBean的实现和使用&lt;/h2&gt;
&lt;h3&gt;二、目标&lt;/h3&gt;
&lt;p&gt;交给 Spring 管理的 Bean 对象，一定就是我们用类创建出来的 Bean 吗？创建出来的 Bean 就永远是单例的吗，没有可能是原型模式吗？&lt;/p&gt;
&lt;p&gt;在集合 Spring 框架下，我们使用的 MyBatis 框架中，它的核心作用是可以满足用户不需要实现 Dao 接口类，就可以通过 xml 或者注解配置的方式完成对数据库执行 CRUD 操作，那么在实现这样的 ORM 框架中，是怎么把一个数据库操作的 Bean 对象交给 Spring 管理的呢。&lt;/p&gt;
&lt;p&gt;因为我们在使用 Spring、MyBatis 框架的时候都可以知道，并没有手动的去创建任何操作数据库的 Bean 对象，有的仅仅是一个接口定义，而这个接口定义竟然可以被注入到其他需要使用 Dao 的属性中去了，那么这一过程最核心待解决的问题，就是需要完成把复杂且以代理方式动态变化的对象，注册到 Spring 容器中。而为了满足这样的一个扩展组件开发的需求，就需要我们在现有手写的 Spring 框架中，添加这一能力。&lt;/p&gt;
&lt;h3&gt;三、方案&lt;/h3&gt;
&lt;p&gt;关于提供一个能让使用者定义复杂的 Bean 对象，功能点非常不错，意义也非常大，因为这样做了之后 Spring 的生态种子孵化箱就此提供了，谁家的框架都可以在此标准上完成自己服务的接入。&lt;/p&gt;
&lt;p&gt;但这样的功能逻辑设计上并不复杂，因为整个 Spring 框架在开发的过程中就已经提供了各项扩展能力的&lt;code&gt;接茬&lt;/code&gt;，你只需要在合适的位置提供一个接茬的处理接口调用和相应的功能逻辑实现即可，像这里的目标实现就是对外提供一个可以二次从 FactoryBean 的 getObject 方法中获取对象的功能即可，这样所有实现此接口的对象类，就可以扩充自己的对象功能了。&lt;em&gt;MyBatis 就是实现了一个 MapperFactoryBean 类，在 getObject 方法中提供 SqlSession 对执行 CRUD 方法的操作&lt;/em&gt; 整体设计结构如下图：&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://tangtangit.com/_astro/spring-10-01.D3xEL3JF_Zo0Yb0.webp&quot; alt=&quot;img&quot; /&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;整个的实现过程包括了两部分，一个解决单例还是原型对象，另外一个处理 FactoryBean 类型对象创建过程中关于获取具体调用对象的 &lt;code&gt;getObject&lt;/code&gt; 操作。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;SCOPE_SINGLETON&lt;/code&gt;、&lt;code&gt;SCOPE_PROTOTYPE&lt;/code&gt;，对象类型的创建获取方式，主要区分在于 &lt;code&gt;AbstractAutowireCapableBeanFactory#createBean&lt;/code&gt; 创建完成对象后是否放入到内存中，如果不放入则每次获取都会重新创建。&lt;/li&gt;
&lt;li&gt;createBean 执行对象创建、属性填充、依赖加载、前置后置处理、初始化等操作后，就要开始做执行判断整个对象是否是一个 FactoryBean 对象，如果是这样的对象，就需要再继续执行获取 FactoryBean 具体对象中的 &lt;code&gt;getObject&lt;/code&gt; 对象了。整个 getBean 过程中都会新增一个单例类型的判断&lt;code&gt;factory.isSingleton()&lt;/code&gt;，用于决定是否使用内存存放对象信息&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;解析&lt;/h3&gt;
&lt;p&gt;1、学习目标：&lt;/p&gt;
&lt;p&gt;对象作用域和BeanFactory&lt;/p&gt;
&lt;p&gt;2、本章目标&lt;/p&gt;
&lt;p&gt;① 实际业务中不仅需要单例Bean对象，也需要原型模式的Bean对象&lt;/p&gt;
&lt;p&gt;② 因此本章增加了对Bean对象作用域的定义、管理&lt;/p&gt;
&lt;p&gt;③ 交由Spring管理的Bean对象，不一定都是由类创建出来的&lt;/p&gt;
&lt;p&gt;③ 本章就是要把复杂且以代理方式动态变化的bean对象，注入Spring容器&lt;/p&gt;
&lt;p&gt;④ 对外提供一个可以二次从 FactoryBean 的 getObject方法中获取对象的功能即可&lt;/p&gt;
&lt;p&gt;3、工程结构调整&lt;/p&gt;
&lt;p&gt;① +FactoryBean&amp;lt;T&amp;gt;：接口中定义getObject方法，实现了该方法的类，可以自定义对象功能&lt;/p&gt;
&lt;p&gt;② +IUserDao：测试使用，定义了一个查询接口&lt;/p&gt;
&lt;p&gt;③ +ProxyBeanFactory：测试使用，实现FactoryBean中的getObject，模拟了 UserDao 的原有功能，类似于 MyBatis 框架中的代理操作&lt;/p&gt;
&lt;p&gt;④ BeanDefinition：新增scope属性，默认为singleton，定义布尔类型的singleton、prototype，解析spring.xml中的scope标签值如果是singleton，则属性singleton=true，prototype=false&lt;/p&gt;
&lt;p&gt;⑤ XmlBeanDefinitionReader：读取spring.xml中的scope属性注入beanDefinition&lt;/p&gt;
&lt;p&gt;⑥ AbstractAutowireCapableBeanFactory：创建Bean对象初始化结束后，根据beanDefinition中的singleton属性判断是否要将Bean对象存入单例Bean对象容器；创建Bean对象时，也无需将非singleton类型的Bean对象注入disposableBeans容器，因为Singleton对象由Spring容器销毁，Prototype对象由JVM销毁&lt;/p&gt;
&lt;p&gt;⑦ DefaultSingletonBeanRegistry：定义了一个NULL_OBJECT&lt;/p&gt;
&lt;p&gt;⑧ +FactoryBeanRegistrySupport：定义factoryBeanObjectCache用来存放单例类型的对象，避免重复创建，对外提供获取容器对象的方法。取Bean对象分两种情况，&lt;/p&gt;
&lt;p&gt;1）如果实现了FactoryBean接口的Bean对象是单例的，则从factoryBeanObjectCache单例缓存池中取，如果缓存池中没有，则调用getObject方法创建对象，并会在缓存中也保存一下，以备下次使用&lt;/p&gt;
&lt;p&gt;2）如果Bean不是单例对象，则直接调用getObject方法创建新对象&lt;/p&gt;
&lt;p&gt;⑨ AbstractBeanFactory：doGetBean方法的逻辑有较大调整，先从单例Bean对象容器中取Bean对象，如果取不到，则创建Bean对象，然后判断Bean对象是否是FactoryBean的实例或子类实例，如果不是，直接给用户返回Bean对象；如果是，则调用FactoryBeanRegistrySupport类中的方法，即获取单例Bean对象或创建新的Bean对象&lt;/p&gt;
&lt;p&gt;4、什么是对象作用域？&lt;/p&gt;
&lt;p&gt;① Spring中可以定义有状态的Bean对象作用域为singleton&lt;/p&gt;
&lt;p&gt;② 有状态会话bean：每个用户私有一个实例，在用户的生存期内，bean保持了用户的信息，即“有状态”；一旦用户灭亡（调用结束或实例结束），bean的生命期也告结束。即每个用户最初都会得到一个初始的bean。&lt;/p&gt;
&lt;p&gt;③ Spring中可以定义无状态的Bean对象作用域为prototype&lt;/p&gt;
&lt;p&gt;④ 无状态会话bean：bean一旦实例化就被加进会话池中，各个用户都可以共用。即使用户已经消亡，bean的生命期也不一定结束，它可能依然存在于会话池中，供其他用户调用。由于没有特定的用户，那么也就不能保持某一用户的状态，所以叫无状态bean。但无状态会话bean  并非没有状态，如果它有自己的属性（变量），那么这些变量就会受到所有调用它的用户的影响，这是在实际应用中必须注意的。&lt;/p&gt;
&lt;p&gt;5、什么是FactoryBean？&lt;/p&gt;
&lt;p&gt;① FactoryBean是接口，其在IOC容器的基础上给实例化Bean对象添加了简单工厂模式和装饰器模式，用户可以在getObject方法中自定义实例化Bean，为IOC容器中复杂Bean的实例化提供了更加灵活的方式&lt;/p&gt;
&lt;p&gt;② 一般情况下，Spring通过反射机制利用&amp;lt;bean&amp;gt;的class属性指定实例化Bean，但某些情况下，实例化Bean的过程比较复杂，如果按照原始方式，需要在&amp;lt;bean&amp;gt;中提供大量的配置信息，不够灵活，这时采用编码方式更简单，因此Spring提供了FactoryBean的工厂类接口，用户可以通过实现该接口，自定义实例化Bean的逻辑&lt;/p&gt;
&lt;p&gt;③ 当spring.xml的&amp;lt;bean&amp;gt;的class配置的是FactoryBean的实现类时，通过getBean()方法返回的不是FactoryBean本身，而是FactoryBean#getObject()方法所返回的对象，相当于FactoryBean#getObject()得到的Bean对象，替代了createBean()所得到的Bean对象&lt;/p&gt;
&lt;p&gt;④ 如果bean实例不是FactoryBean类型，则直接返回bean实例；如果bean实例是FactoryBean类型，而beanName又是以&amp;amp;开头【beanFactory.getBean(&quot;&amp;amp;user&quot;)】，直接返回bean实例；如果bean实例是FactoryBean类型，而beanName不是以&amp;amp;开头，则返回bean实例的getObject()方法获取的对象实例（一般getObject()方法中就是我们需要的实例对象的创建过程）&lt;/p&gt;
&lt;p&gt;6、参考 #&lt;a href=&quot;https://www.cnblogs.com/aspirant/p/9082858.html#&quot;&gt;BeanFactory 简介以及它 和FactoryBean的区别(阿里面试) - aspirant...&lt;/a&gt; #&lt;a href=&quot;https://www.cnblogs.com/youzhibing/p/10528821.html#&quot;&gt;Spring拓展接口之FactoryBean，我们来看看其源码实现 - 青石路 - 博客园&lt;/a&gt;&lt;/p&gt;
&lt;h3&gt;收获&lt;/h3&gt;
&lt;p&gt;AbstractBeanFactory类中doGetBean方法中：&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;img src=&quot;https://tangtangit.com/_astro/image-20231121151820451.CTd5iTRs_1onyqe.webp&quot; alt=&quot;image-20231121151820451&quot; /&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;此处新增加的功能主要是在 doGetBean 方法中，添加了调用 &lt;code&gt;(T) getObjectForBeanInstance(sharedInstance, name)&lt;/code&gt; 对获取 FactoryBean 的操作。&lt;/li&gt;
&lt;li&gt;在 getObjectForBeanInstance 方法中做具体的 instanceof 判断，另外还会从 FactoryBean 的缓存中获取对象，如果不存在则调用 FactoryBeanRegistrySupport#getObjectFromFactoryBean，执行具体的操作。&lt;/li&gt;
&lt;li&gt;getObjectFromFactoryBean方法中是由调用getObject方法的&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;    protected Object getObjectFromFactoryBean(FactoryBean factory, String beanName) {
        if (factory.isSingleton()) {
            Object object = this.factoryBeanObjectCache.get(beanName);
            if (object == null) {
                object = doGetObjectFromFactoryBean(factory, beanName);
                this.factoryBeanObjectCache.put(beanName, (object != null ? object : NULL_OBJECT));
            }
            return (object != NULL_OBJECT ? object : null);
        } else {
            return doGetObjectFromFactoryBean(factory, beanName);
        }
    }

    private Object doGetObjectFromFactoryBean(final FactoryBean factory, final String beanName){
        try {
            //此处调用getObject
            return factory.getObject();
        } catch (Exception e) {
            throw new BeansException(&quot;FactoryBean threw exception on object[&quot; + beanName + &quot;] creation&quot;, e);
        }
    }
&lt;/code&gt;&lt;/pre&gt;
&lt;/blockquote&gt;
&lt;pre&gt;&lt;code&gt;    protected &amp;lt;T&amp;gt; T doGetBean(final String name, final Object[] args) {
        Object sharedInstance = getSingleton(name);
        if (sharedInstance != null) {
            // 如果是 FactoryBean，则需要调用 FactoryBean#getObject
            return (T) getObjectForBeanInstance(sharedInstance, name);
        }

        BeanDefinition beanDefinition = getBeanDefinition(name);
        Object bean = createBean(name, beanDefinition, args);
        return (T) getObjectForBeanInstance(bean, name);
    }

    private Object getObjectForBeanInstance(Object beanInstance, String beanName) {
        if (!(beanInstance instanceof FactoryBean)) {
            return beanInstance;
        }

        Object object = getCachedObjectForFactoryBean(beanName);

        if (object == null) {
            FactoryBean&amp;lt;?&amp;gt; factoryBean = (FactoryBean&amp;lt;?&amp;gt;) beanInstance;
            object = getObjectFromFactoryBean(factoryBean, beanName);
        }

        return object;
    }
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;第 11 章：更上层楼，基于观察者实现，容器事件和事件监听器&lt;/h2&gt;
&lt;h3&gt;二、目标&lt;/h3&gt;
&lt;p&gt;在 Spring 中有一个 Event 事件功能，它可以提供事件的定义、发布以及监听事件来完成一些自定义的动作。比如你可以定义一个新用户注册的事件，当有用户执行注册完成后，在事件监听中给用户发送一些优惠券和短信提醒，这样的操作就可以把属于基本功能的注册和对应的策略服务分开，降低系统的耦合。以后在扩展注册服务，比如需要添加风控策略、添加实名认证、判断用户属性等都不会影响到依赖注册成功后执行的动作。&lt;/p&gt;
&lt;p&gt;那么在本章节我们需要以观察者模式的方式，设计和实现 Spring Event 的容器事件和事件监听器功能，最终可以让我们在现有实现的 Spring 框架中可以定义、监听和发布自己的事件信息。&lt;/p&gt;
&lt;h3&gt;三、方案&lt;/h3&gt;
&lt;p&gt;其实事件的设计本身就是一种观察者模式的实现，它所要解决的就是一个对象状态改变给其他对象通知的问题，而且要考虑到易用和低耦合，保证高度的协作。&lt;/p&gt;
&lt;p&gt;在功能实现上我们需要定义出事件类、事件监听、事件发布，而这些类的功能需要结合到 Spring 的 AbstractApplicationContext#refresh()，以便于处理事件初始化和注册事件监听器的操作。整体设计结构如下图：&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://tangtangit.com/_astro/spring-11-01.EkV8FcOU_F25hh.webp&quot; alt=&quot;img&quot; /&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;在整个功能实现过程中，仍然需要在面向用户的应用上下文 &lt;code&gt;AbstractApplicationContext&lt;/code&gt; 中添加相关事件内容，包括：初始化事件发布者、注册事件监听器、发布容器刷新完成事件。&lt;/li&gt;
&lt;li&gt;使用观察者模式定义事件类、监听类、发布类，同时还需要完成一个广播器的功能，接收到事件推送时进行分析处理符合监听事件接受者感兴趣的事件，也就是使用 isAssignableFrom 进行判断。&lt;/li&gt;
&lt;li&gt;isAssignableFrom 和 instanceof 相似，不过 isAssignableFrom 是用来判断子类和父类的关系的，或者接口的实现类和接口的关系的，默认所有的类的终极父类都是Object。如果A.isAssignableFrom(B)结果是true，证明B可以转换成为A,也就是A可以由B转换而来。&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;四、实现&lt;/h3&gt;
&lt;h4&gt;1. 工程结构&lt;/h4&gt;
&lt;pre&gt;&lt;code&gt;small-spring-step-10
└── src
    ├── main
    │   └── java
    │       └── cn.bugstack.springframework
    │           ├── beans
    │           │   ├── factory
    │           │   │   ├── config
    │           │   │   │   ├── AutowireCapableBeanFactory.java
    │           │   │   │   ├── BeanDefinition.java
    │           │   │   │   ├── BeanFactoryPostProcessor.java
    │           │   │   │   ├── BeanPostProcessor.java
    │           │   │   │   ├── BeanReference.java
    │           │   │   │   ├── ConfigurableBeanFactory.java
    │           │   │   │   └── SingletonBeanRegistry.java
    │           │   │   ├── support
    │           │   │   │   ├── AbstractAutowireCapableBeanFactory.java
    │           │   │   │   ├── AbstractBeanDefinitionReader.java
    │           │   │   │   ├── AbstractBeanFactory.java
    │           │   │   │   ├── BeanDefinitionReader.java
    │           │   │   │   ├── BeanDefinitionRegistry.java
    │           │   │   │   ├── CglibSubclassingInstantiationStrategy.java
    │           │   │   │   ├── DefaultListableBeanFactory.java
    │           │   │   │   ├── DefaultSingletonBeanRegistry.java
    │           │   │   │   ├── DisposableBeanAdapter.java
    │           │   │   │   ├── FactoryBeanRegistrySupport.java
    │           │   │   │   ├── InstantiationStrategy.java
    │           │   │   │   └── SimpleInstantiationStrategy.java  
    │           │   │   ├── support
    │           │   │   │   └── XmlBeanDefinitionReader.java
    │           │   │   ├── Aware.java
    │           │   │   ├── BeanClassLoaderAware.java
    │           │   │   ├── BeanFactory.java
    │           │   │   ├── BeanFactoryAware.java
    │           │   │   ├── BeanNameAware.java
    │           │   │   ├── ConfigurableListableBeanFactory.java
    │           │   │   ├── DisposableBean.java
    │           │   │   ├── FactoryBean.java
    │           │   │   ├── HierarchicalBeanFactory.java
    │           │   │   ├── InitializingBean.java
    │           │   │   └── ListableBeanFactory.java
    │           │   ├── BeansException.java
    │           │   ├── PropertyValue.java
    │           │   └── PropertyValues.java 
    │           ├── context  
    │           │   ├── event
    │           │   │   ├── AbstractApplicationEventMulticaster.java 
    │           │   │   ├── ApplicationContextEvent.java 
    │           │   │   ├── ApplicationEventMulticaster.java 
    │           │   │   ├── ContextClosedEvent.java 
    │           │   │   ├── ContextRefreshedEvent.java 
    │           │   │   └── SimpleApplicationEventMulticaster.java 
    │           │   ├── support
    │           │   │   ├── AbstractApplicationContext.java 
    │           │   │   ├── AbstractRefreshableApplicationContext.java 
    │           │   │   ├── AbstractXmlApplicationContext.java 
    │           │   │   ├── ApplicationContextAwareProcessor.java 
    │           │   │   └── ClassPathXmlApplicationContext.java 
    │           │   ├── ApplicationContext.java 
    │           │   ├── ApplicationContextAware.java 
    │           │   ├── ApplicationEvent.java 
    │           │   ├── ApplicationEventPublisher.java 
    │           │   ├── ApplicationListener.java 
    │           │   └── ConfigurableApplicationContext.java
    │           ├── core.io
    │           │   ├── ClassPathResource.java 
    │           │   ├── DefaultResourceLoader.java 
    │           │   ├── FileSystemResource.java 
    │           │   ├── Resource.java 
    │           │   ├── ResourceLoader.java 
    │           │   └── UrlResource.java
    │           └── utils
    │               └── ClassUtils.java
    └── test
        └── java
            └── cn.bugstack.springframework.test
                ├── event
                │   ├── ContextClosedEventListener.java
                │   ├── ContextRefreshedEventListener.java
                │   ├── CustomEvent.java
                │   └── CustomEventListener.java
                └── ApiTest.java
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;工程源码&lt;/strong&gt;：&lt;code&gt;公众号「bugstack虫洞栈」，回复：Spring 专栏，获取完整源码&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;容器事件和事件监听器实现类关系，如图 11-2&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://tangtangit.com/_astro/spring-11-02.BuEsahvp_EJWjk.webp&quot; alt=&quot;图 10-2&quot; /&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;以上整个类关系图以围绕实现 event 事件定义、发布、监听功能实现和把事件的相关内容使用 AbstractApplicationContext#refresh 进行注册和处理操作。&lt;/li&gt;
&lt;li&gt;在实现的过程中主要以扩展 spring context 包为主，事件的实现也是在这个包下进行扩展的，当然也可以看出来目前所有的实现内容，仍然是以IOC为主。&lt;/li&gt;
&lt;li&gt;ApplicationContext 容器继承事件发布功能接口 ApplicationEventPublisher，并在实现类中提供事件监听功能。&lt;/li&gt;
&lt;li&gt;ApplicationEventMulticaster 接口是注册监听器和发布事件的广播器，提供添加、移除和发布事件方法。&lt;/li&gt;
&lt;li&gt;最后是发布容器关闭事件，这个仍然需要扩展到 AbstractApplicationContext#close 方法中，由注册到虚拟机的钩子实现。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;1、学习目标：&lt;/p&gt;
&lt;p&gt;容器事件和事件监听器&lt;/p&gt;
&lt;p&gt;2、本章计划&lt;/p&gt;
&lt;p&gt;① 在Spring框架中有Event事件功能，通过事件定义、发布、监听，来完成自定义的动作&lt;/p&gt;
&lt;p&gt;② 例如Spring框架中的容器刷新事件，用于监听容器初始化时刷新是否成功&lt;/p&gt;
&lt;p&gt;③ 在业务系统中可以定义用户注册事件，当用户注册完成，通过事件监听给用户发送短信通知、优惠券等&lt;/p&gt;
&lt;p&gt;④ 又例如为注册服务添加实名认证（扩展注册服务），通过事件监听都不会影响到依赖注册成功后执行的动作&lt;/p&gt;
&lt;p&gt;⑤ 引入事件监听，可以将基本业务功能与对应的策略服务分开，降低代码耦合性&lt;/p&gt;
&lt;p&gt;3、项目扩展 - 事件&lt;/p&gt;
&lt;p&gt;① +ApplicationEvent：定义抽象类继承EventObject，实例化时调用super(source)，即实例化EventObject类，并将给成员变量source赋值，source就是上下文对象，包含beanFactory等资源&lt;/p&gt;
&lt;p&gt;② +ApplicationContextEvent：定义应用上下文事件类，继承ApplicationEvent，实例化时给父类传入source（上下文对象）&lt;/p&gt;
&lt;p&gt;③ +ContextClosedEvent：Spring框架自己实现的实现类，继承了ApplicationContextEvent，用于监听关闭动作，实例化时传入source（上下文对象，具备事件监听的功能）&lt;/p&gt;
&lt;p&gt;④ +ContextRefreshedEvent：Spring框架自己实现的实现类，继承了ApplicationContextEvent，用于监听容器刷新动作，实例化时传入source（上下文对象，具备事件监听的功能）&lt;/p&gt;
&lt;p&gt;⑤ +CustomEvent：自定义的事件，继承了ApplicationContextEvent，用于监听容器刷新动作，实例化时给自定义属性赋值，并传入source（上下文对象）&lt;/p&gt;
&lt;p&gt;4、项目扩展 - 事件监听器&lt;/p&gt;
&lt;p&gt;① +ApplicationListener&amp;lt;E extends ApplicationEvent&amp;gt;：继承java.util.EventListener接口，定义onApplicationEvent(E event)事件处理方法，每个事件监听器都要实现该接口&lt;/p&gt;
&lt;p&gt;② +CustomEventListener：实现ApplicationListener&amp;lt;CustomEvent&amp;gt;类，实现自定义的事件监听&lt;/p&gt;
&lt;p&gt;③ +ContextClosedEventListener：Spring框架的容器关闭事件监听器，监听动作在容器关闭时执行&lt;/p&gt;
&lt;p&gt;④ +ContextRefreshedEventListener：Spring框架的容器刷新事件监听器，监听动作在容器完成初始化刷新时执行&lt;/p&gt;
&lt;p&gt;5、项目扩展 - 事件监听&lt;/p&gt;
&lt;p&gt;① +ApplicationEventMulticaster：应用事件广播接口，&lt;/p&gt;
&lt;p&gt;1）定义新增、删除监听器方法&lt;/p&gt;
&lt;p&gt;2）定义事件广播方法multicastEvent(ApplicationEvent event)&lt;/p&gt;
&lt;p&gt;② +AbstractApplicationEventMulticaster：应用事件广播抽象类，实现ApplicationEventMulticaster接口&lt;/p&gt;
&lt;p&gt;1）定义applicationListeners应用监听器容器Map，并实现从容器中新增、删除监听器的方法&lt;/p&gt;
&lt;p&gt;2）getApplicationListeners：将监听器Map容器中的监听器存入list&lt;/p&gt;
&lt;p&gt;3）supportEvent：判断监听器（applicationListener）是否对事件（event）感兴趣&lt;/p&gt;
&lt;p&gt;③ +SimpleApplicationEventMulticaster：简单的应用实践广播实现类，继承AbstractApplicationEventMulticaster&lt;/p&gt;
&lt;p&gt;1）为父类提高beanFactory&lt;/p&gt;
&lt;p&gt;2）实现ApplicationEventMulticaster中的multicastEvent(event)方法，该方法会调用事件对应的监听器，执        行onApplicationEvent(event)事件监听方法&lt;/p&gt;
&lt;p&gt;④ ClassUtils：新增方法isCglibProxyClass(Class&amp;lt;?&amp;gt; clazz)判断Class的Bean对象是否是通过Cglib实例化的&lt;/p&gt;
&lt;p&gt;6、项目扩展 - 发布、监听事件&lt;/p&gt;
&lt;p&gt;① SingletonBeanRegistry：定义接口registerSingleton(beanName, singletonObject)&lt;/p&gt;
&lt;p&gt;② DefaultSingletonBeanRegistry：实现registerSingleton接口，其实就是将Bean对象注入singletonObjects单例Bean对象容器&lt;/p&gt;
&lt;p&gt;③ +ApplicationEventPublisher：定义事件发布接口，之后Spring内事件、自定义事件都通过该接口中的publishEvent(ApplicationEvent event)发布&lt;/p&gt;
&lt;p&gt;④ ApplicationContext：接口继承ApplicationEventPublisher，使得应用上下文具备事件发布的能力&lt;/p&gt;
&lt;p&gt;⑤ AbstractApplicationContext：在refresh()方法中完善事件发布、监听相关功能&lt;/p&gt;
&lt;p&gt;1）initApplicationEventMulticaster()：初始化事件发布者，即实例化SimpleApplicationEventMulticaster，            这样上下文就具备了SimpleApplicationEventMulticaster中定义的事件发布功能，并将实例化Bean对象applicationEventMulticaster 存入单例Bean对象容器singletonObjects，以供其他方法使用&lt;/p&gt;
&lt;p&gt;2）registerListeners()：获取所有实现了ApplicationListener接口的监听器类对象，例如CustomEventListener，调用applicationEventMulticaster.addApplicationListener(listener)将监听器Bean对象存入监听器池子中&lt;/p&gt;
&lt;p&gt;3）finishRefresh()：发布【容器刷新完成】事件，容器关闭事件已经其他自定义事件的发布也是调用这个方法，该方法的入参是对应的事件实例Bean对象，调用1）中applicationEventMulticaster.multicastEvent(event)，该方法是从监听器池子中查询监听当前事件的监听器，调用listener.onApplicationEvent(event)即可监听事件，具体细节可以参考配图&lt;/p&gt;
&lt;p&gt;4）在close()方法中发布容器关闭事件publishEvent(new ContextClosedEvent(this));&lt;/p&gt;
&lt;p&gt;7、项目扩展&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;spring.xml 将事件监听器作为bean配置在spring.xml中&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;8、一些记录&lt;/p&gt;
&lt;p&gt;① isAssignableFrom()方法与instanceof关键字的区别总结为以下两个点：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;isAssignableFrom()方法是从类继承的角度去判断，&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;instanceof关键字是从实例继承的角度去判断。 isAssignableFrom()方法是判断是否为某个类的父类，instanceof关键字是判断是否某个类的子类。&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;② 观察者模式 当一个行为发生时，传递信息给另一个用户，执行相应的处理行为，这两个行为之间没有耦合关系，而是通过监听实现。例如 MQ 的消息生成与消费、总线监听，都体现了观察者模式，可以看bugstack中的设计模式篇&lt;/p&gt;
&lt;h2&gt;第 12 章：炉火纯青，基于JDK和Cglib动态代理，实现AOP核心功能&lt;/h2&gt;
&lt;p&gt;1、学习目标：基于JDK、Cglib实现AOP切面&lt;/p&gt;
&lt;p&gt;2、本章计划&lt;/p&gt;
&lt;p&gt;① AOP（面向切面编程）是指通过预编译的方式和运行期间动态代理实现程序功能的统一维护&lt;/p&gt;
&lt;p&gt;② 使用AOP对业务逻辑的各部分隔离，使各模块间的业务逻辑耦合度降低，提高代码的复用性，提高开发效率&lt;/p&gt;
&lt;p&gt;③ AOP的核心技术是动态代理，即代理一个目标类，调用目标类的方法，并可以进行功能增强&lt;/p&gt;
&lt;p&gt;④ 可以定义拦截器，通过规则找到被代理的方法，执行这些方法，并添加日志打印、异常监控等增强功能&lt;/p&gt;
&lt;p&gt;⑤ 例如使用Spring AOP，只处理需要被拦截的方法，在拦截方法执行后，可以对方法做一些扩展&lt;/p&gt;
&lt;p&gt;⑥ 那就需要定义代理方法的Proxy，使用方法拦截器MethodInterceptor#invoke实现&lt;/p&gt;
&lt;p&gt;⑦ 使用 org.aspectj.weaver.tools.PointcutParser 处理拦截表达式&lt;/p&gt;
&lt;p&gt;3、项目改造&lt;/p&gt;
&lt;p&gt;① ClassFilter：定义match方法用于判断切点表达式中的类，与给定的接口或目标类是否匹配&lt;/p&gt;
&lt;p&gt;② MethodMatcher：定义match方法用于判断切点表达式中的方法，与目标类中的方法是否匹配&lt;/p&gt;
&lt;p&gt;③ Pointcut：定义切入点接口，用于获取ClassFilter、MethodMatcher这俩接口的实现类，但好像没用到&lt;/p&gt;
&lt;p&gt;④ AspectJExpressionPointcut：实现了①②③接口中定义的方法，类实例化时，通过 aspectjweaver包提供的表达式解析器得到pointcutExpression，并执行类、方法的匹配校验&lt;/p&gt;
&lt;p&gt;⑤ TargetSource：定义目标对象类，其中定义了Object属性用于保存目标对象，并提供获取目标对象、获取目标对象的Class类实现的接口集合&lt;/p&gt;
&lt;p&gt;⑥ MethodInterceptor：是由aopalliance包提供的方法拦截器接口，由用户自定义类实现其中的invoke方法，该方法中包含目标对象的方法调用以及功能增强&lt;/p&gt;
&lt;p&gt;⑦ AdvisedSupport：该类把目标对象（⑤）、用户自定义的拦截器（⑥的实现类）、方法匹配器（②）包装在一起，方便在Cglib和JDK的Proxy实现类中使用，类似于业务开发中对入参的包装&lt;/p&gt;
&lt;p&gt;⑧ ReflectiveMethodInvocation：定义的包装类，整合了目标对象、方法、入参&lt;/p&gt;
&lt;p&gt;⑨ UserServiceInterceptor：implement⑥，实现MethodInterceptor#invoke方法，就是自定义了增强方法，以供方法切入时调用，其中的MethodInvocation.proceed方法是调用代理对象中，调用的目标对象的方法，其实也是通过动态代理实现的，注意是先执行finally再执行return&lt;/p&gt;
&lt;p&gt;⑩ AopProxy ：定义一个标准接口，用于获取代理类，定义接口方便管理JDK、Cglib等代理实现类 ⑾ JdkDynamicAopProxy【图1】：&lt;/p&gt;
&lt;p&gt;实现AopProxy、InvocationHandler接口，将获取代理对象与执行代理方法分开&lt;/p&gt;
&lt;p&gt;1）实现AopProxy#getProxy方法：&lt;/p&gt;
&lt;p&gt;✳ 该方法用于对外提供代理对象，&lt;/p&gt;
&lt;p&gt;✳ 需要传入类加载器、目标对象的接口集合（通过⑦获取）、当前类（因为当前类重写了invoke方法）&lt;/p&gt;
&lt;p&gt;2）实现InvocationHandler#invoke方法：&lt;/p&gt;
&lt;p&gt;✳ 该方法用于调用目标对象的方法，并做一些功能增强&lt;/p&gt;
&lt;p&gt;✳ 本方法的功能增强体现在校验定义的切点方法与代理对象的方法是否匹配，匹配成功后调用自定义的方法拦   截器（⑨），输出一些监控信息&lt;/p&gt;
&lt;p&gt;⑿ Cglib2AopProxy【图2】：&lt;/p&gt;
&lt;p&gt;✳ 基于Cglib使用Enhancer代理的类，使用ASM字节码增强技术，在运行期间为Java类或接口生成代理对象，不需要像JDK动态代理的实现那样继承InvocationHandler&lt;/p&gt;
&lt;p&gt;✳ 创建Enhancer对象，注入目标对象、目标对象实现的接口集合、代理对象执行器创建代理对象&lt;/p&gt;
&lt;p&gt;✳ 用户扩展的拦截方法在DynamicAdvisedInterceptor#intercept中实现，跟JDK动态代理中的invoke方法类似，先匹配切点方法，再调用目标对象的方法&lt;/p&gt;
&lt;p&gt;⒀ ApiTest&lt;/p&gt;
&lt;p&gt;✳ 测试动态代理，先实例化目标对象，实例化代理信息包装类AdvisedSupport，将目标对象、自定义的方法拦截器（⑨）、切点表达式注入其中&lt;/p&gt;
&lt;p&gt;✳ 实例化Proxy，生成代理对象&lt;/p&gt;
&lt;p&gt;✳ 通过代理对象调用方法&lt;/p&gt;
&lt;p&gt;4、AOP与动态代理&lt;/p&gt;
&lt;p&gt;① IOC解决了类之间的紧耦合，动态代理解决了方法之间的紧耦合&lt;/p&gt;
&lt;p&gt;② 使用JDK动态代理，Proxy.newProxyInstance(ClassLoader loader,Class&amp;lt;?&amp;gt;[] interfaces,InvocationHandler h)创建代理类对象时，必须要传入目标对象实现的接口，在某些情况下目标类没有实现接口，因此就需要使用Cglib&lt;/p&gt;
&lt;p&gt;③ 在Spring中，当Bean实现接口时，使用JDK动态代理，当Bean没有实现接口，使用Cglib动态代理&lt;/p&gt;
&lt;p&gt;④ 可以在spring.xml配置中添加&amp;lt;aop:aspectj-autoproxy proxyt-target-class=”true”/&amp;gt;强制使用Cglib&lt;/p&gt;
&lt;p&gt;⑤ AOP三步走&lt;/p&gt;
&lt;p&gt;✳ 确定目标对象targetObject，即需要进行无侵入式改造的Bean对象&lt;/p&gt;
&lt;p&gt;✳ 编写Advice通知方法，即编写功能增强代码&lt;/p&gt;
&lt;p&gt;✳ 配置切入点和切面&lt;/p&gt;
&lt;h2&gt;我只能说坚持不下去了，spring太复杂了&lt;/h2&gt;
</content:encoded></item><item><title>Spring AOP原理与自定义注解详解</title><link>https://tangtangit.com/posts/java%E6%A0%B8%E5%BF%83-07-aop%E4%B8%8E%E6%B3%A8%E8%A7%A3/</link><guid isPermaLink="true">https://tangtangit.com/posts/java%E6%A0%B8%E5%BF%83-07-aop%E4%B8%8E%E6%B3%A8%E8%A7%A3/</guid><description>从静态代理、JDK动态代理到CGLIB代理，深入理解Spring AOP的实现原理与通知执行顺序；结合面试题分析Spring4/Boot2的AOP差异；系统讲解Java注解机制与自定义注解实践。</description><pubDate>Sat, 24 Jan 2026 00:00:00 GMT</pubDate><content:encoded>&lt;h2&gt;spring-aop原理&lt;/h2&gt;
&lt;p&gt;&lt;strong&gt;简介&lt;/strong&gt;：&lt;/p&gt;
&lt;p&gt;Aspect Oriented Programming  面向切面编程。解耦是程序员编码开发过程中一直追求的。AOP也是为了解耦所诞生。&lt;/p&gt;
&lt;p&gt;具体思想是：定义一个切面，在切面的纵向定义处理方法，处理完成之后，回到横向业务流。&lt;/p&gt;
&lt;p&gt;AOP 在Spring框架中被作为核心组成部分之一，的确Spring将AOP发挥到很强大的功能。最常见的就是事务控制。工作之余，对于使用的工具，不免需要了解其所以然。学习了一下，写了些程序帮助理解。&lt;/p&gt;
&lt;p&gt;AOP 主要是利用代理模式的技术来实现的。&lt;/p&gt;
&lt;h3&gt;&lt;strong&gt;1、静态代理：&lt;/strong&gt;&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;就是设计模式中的proxy模式&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;a.业务接口&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://tangtangit.com/_astro/psb.7NM39PBX_29QzY4.webp&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;b.业务实现&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;%E4%B8%9A%E5%8A%A1%E5%AE%9E%E7%8E%B0.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;c.代理对象&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;%E4%BB%A3%E7%90%86%E5%AF%B9%E8%B1%A1.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;d.测试类&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;%E6%B5%8B%E8%AF%95%E7%B1%BB.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;从这段代码可以看出来，代理模式其实就是AOP的雏形。 上端代码中talk(String msg, String singname)是一个切面。在代理类中的sing(singname)方法是个后置处理方法。&lt;/p&gt;
&lt;p&gt;这样就实现了，其他的辅助方法和业务方法的解耦。业务不需要专门去调用，而是走到talk方法，顺理成章的调用sing方法&lt;/p&gt;
&lt;p&gt;再从这段代码看：1、要实现代理方式，必须要定义接口。2、每个业务类，需要一个代理类。&lt;/p&gt;
&lt;h3&gt;2、动态代理：&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;jdk1.5中提供，利用反射。实现InvocationHandler接口。&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;a.代理类&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;%E5%8A%A8%E6%80%81%E4%BB%A3%E7%90%86%E7%B1%BB.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;b.测试类&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;%E5%8A%A8%E6%80%81%E4%BB%A3%E7%90%86%E6%B5%8B%E8%AF%95.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;输出结果会是：&lt;/p&gt;
&lt;p&gt;切面之前执行
people talk业务说法
切面之后执行&lt;/p&gt;
&lt;p&gt;说明只要在业务调用方法切面之前，是可以动态的加入需要处理的方法。&lt;/p&gt;
&lt;p&gt;从代码来看，如果再建立一个业务模块，也只需要一个代理类。ITalk iTalk = (ITalk) new DynamicProxy().bind(new PeopleTalk());  将业务接口和业务类绑定到动态代理类。&lt;/p&gt;
&lt;p&gt;但是这种方式：还是需要定义接口。&lt;/p&gt;
&lt;h3&gt;&lt;strong&gt;3、利用cglib&lt;/strong&gt;&lt;/h3&gt;
&lt;p&gt;a.业务类&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;cglib%E4%B8%9A%E5%8A%A1%E7%B1%BB.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;b.代理类&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;cglib%E4%BB%A3%E7%90%86%E7%B1%BB.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;c.测试类&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;cglib%E6%B5%8B%E8%AF%95.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;最后输出结果：&lt;/p&gt;
&lt;p&gt;事物开始
people talk业务方法
事物结束
事物开始
spreak chinese业务方法
事物结束&lt;/p&gt;
&lt;h2&gt;spring-boot-starter-aop依赖&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;        &amp;lt;dependency&amp;gt;
            &amp;lt;groupId&amp;gt;org.springframework.boot&amp;lt;/groupId&amp;gt;
            &amp;lt;artifactId&amp;gt;spring-boot-starter-aop&amp;lt;/artifactId&amp;gt;
        &amp;lt;/dependency&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote&gt;
&lt;p&gt;引入了AOP的场景启动器，我们点击去看看&lt;/p&gt;
&lt;p&gt;还是引入了spring-aop和aspectj的依赖，和我们Spring集成AOP是引入了相同的包，接着我们直接就可以创建Advisor了，如上AspectJTest这个类，但是我们并没有通过@EnableAspectJAutoProxy开启AOP呢？那是因为AOP的自动配置类帮我们开启了&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2&gt;Java AOP&lt;/h2&gt;
&lt;p&gt;apo专业术语图解：&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;apo%E4%B8%93%E4%B8%9A%E6%9C%AF%E8%AF%AD.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;通知方法为：要写入的方法&lt;/p&gt;
&lt;p&gt;连接点图中一共16个&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;&lt;strong&gt;开发模式@Aspect注解说明&lt;/strong&gt;&lt;/p&gt;
&lt;h3&gt;2 注解说明&lt;/h3&gt;
&lt;p&gt;2.1 @Aspect&lt;/p&gt;
&lt;p&gt;作用是把当前类标识为一个切面供容器读取&lt;/p&gt;
&lt;p&gt;2.2 @Before
标识一个前置增强方法，相当于BeforeAdvice的功能，相似功能的还有&lt;/p&gt;
&lt;p&gt;2.3 @AfterReturning&lt;/p&gt;
&lt;p&gt;后置增强，相当于AfterReturningAdvice，方法正常退出时执行&lt;/p&gt;
&lt;p&gt;2.4 @AfterThrowing&lt;/p&gt;
&lt;p&gt;异常抛出增强，相当于ThrowsAdvice&lt;/p&gt;
&lt;p&gt;2.5 @After&lt;/p&gt;
&lt;p&gt;final增强，不管是抛出异常或者正常退出都会执行&lt;/p&gt;
&lt;p&gt;2.6 @Around&lt;/p&gt;
&lt;p&gt;环绕增强，相当于MethodInterceptor，前面四种的合体：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;try{
  @before 前置通知
  method.invoke();
  @afterreturning 返回通知
}catch(e){
  @afterthrowing 异常通知
}finally{
  @after  后置通知
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;3 execution切点函数&lt;/h3&gt;
&lt;p&gt;execution函数用于匹配方法执行的连接点，语法为：&lt;/p&gt;
&lt;p&gt;execution(方法修饰符(可选)  返回类型  方法名  参数  异常模式(可选)) &lt;/p&gt;
&lt;p&gt; &lt;/p&gt;
&lt;p&gt;参数部分允许使用通配符：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt; 匹配任意字符，但只能匹配一个元素&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;.. 匹配任意字符，可以匹配任意多个元素，表示类时，必须和*联合使用&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt; 必须跟在类名后面，如Horseman+，表示类本身和继承或扩展指定类的所有类&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;示例中的* chop(..)解读为：&lt;/p&gt;
&lt;p&gt;方法修饰符  无&lt;/p&gt;
&lt;p&gt;返回类型      *匹配任意数量字符，表示返回类型不限&lt;/p&gt;
&lt;p&gt;方法名          chop表示匹配名称为chop的方法&lt;/p&gt;
&lt;p&gt;参数               (..)表示匹配任意数量和类型的输入参数&lt;/p&gt;
&lt;p&gt;异常模式       不限&lt;/p&gt;
&lt;p&gt; &lt;/p&gt;
&lt;p&gt;更多示例：&lt;/p&gt;
&lt;p&gt;void chop(String,int)&lt;/p&gt;
&lt;p&gt;匹配目标类任意修饰符方法、返回void、方法名chop、带有一个String和一个int型参数的方法&lt;/p&gt;
&lt;p&gt;public void chop(*)&lt;/p&gt;
&lt;p&gt;匹配目标类public修饰、返回void、方法名chop、带有一个任意类型参数的方法&lt;/p&gt;
&lt;p&gt;public String &lt;em&gt;o&lt;/em&gt;(..)&lt;/p&gt;
&lt;p&gt; 匹配目标类public修饰、返回String类型、方法名中带有一个o字符、带有任意数量任意类型参数的方法&lt;/p&gt;
&lt;p&gt;public void &lt;em&gt;o&lt;/em&gt;(String,..)&lt;/p&gt;
&lt;p&gt; 匹配目标类public修饰、返回void、方法名中带有一个o字符、带有任意数量任意类型参数，但第一个参数必须有且为String型的方法&lt;/p&gt;
&lt;p&gt;也可以指定类：&lt;/p&gt;
&lt;p&gt;public void examples.chap03.Horseman.*(..)&lt;/p&gt;
&lt;p&gt;匹配Horseman的public修饰、返回void、不限方法名、带有任意数量任意类型参数的方法&lt;/p&gt;
&lt;p&gt;public void examples.chap03.&lt;em&gt;man.&lt;/em&gt;(..)&lt;/p&gt;
&lt;p&gt;匹配以man结尾的类中public修饰、返回void、不限方法名、带有任意数量任意类型参数的方法&lt;/p&gt;
&lt;p&gt;指定包：&lt;/p&gt;
&lt;p&gt;public void examples.chap03.*.chop(..)&lt;/p&gt;
&lt;p&gt;匹配examples.chap03包下所有类中public修饰、返回void、方法名chop、带有任意数量任意类型参数的方法&lt;/p&gt;
&lt;p&gt;public void examples..*.chop(..)&lt;/p&gt;
&lt;p&gt;匹配examples.包下和所有子包中的类中public修饰、返回void、方法名chop、带有任意数量任意类型参数的方法
可以用这些表达式替换StorageAdvisor中的代码并观察效果&lt;/p&gt;
&lt;h3&gt;4 更多切点函数&lt;/h3&gt;
&lt;p&gt;除了execution()，Spring中还支持其他多个函数，这里列出名称和简单介绍，以方便根据需要进行更详细的查询&lt;/p&gt;
&lt;p&gt;4.1 @annotation()&lt;/p&gt;
&lt;p&gt;表示标注了指定注解的目标类方法&lt;/p&gt;
&lt;p&gt;例如 @annotation(org.springframework.transaction.annotation.Transactional) 表示标注了@Transactional的方法&lt;/p&gt;
&lt;p&gt;4.2 args()&lt;/p&gt;
&lt;p&gt;通过目标类方法的参数类型指定切点&lt;/p&gt;
&lt;p&gt;例如 args(String) 表示有且仅有一个String型参数的方法&lt;/p&gt;
&lt;p&gt;4.3 @args()&lt;/p&gt;
&lt;p&gt;通过目标类参数的对象类型是否标注了指定注解指定切点&lt;/p&gt;
&lt;p&gt;如 @args(org.springframework.stereotype.Service) 表示有且仅有一个标注了@Service的类参数的方法&lt;/p&gt;
&lt;p&gt;4.4 within()&lt;/p&gt;
&lt;p&gt;通过类名指定切点&lt;/p&gt;
&lt;p&gt;如 within(examples.chap03.Horseman) 表示Horseman的所有方法&lt;/p&gt;
&lt;p&gt;4.5 target()&lt;/p&gt;
&lt;p&gt;通过类名指定，同时包含所有子类&lt;/p&gt;
&lt;p&gt;如 target(examples.chap03.Horseman)  且Elephantman extends Horseman，则两个类的所有方法都匹配&lt;/p&gt;
&lt;p&gt;4.6 @within()&lt;/p&gt;
&lt;p&gt;匹配标注了指定注解的类及其所有子类&lt;/p&gt;
&lt;p&gt;如 @within(org.springframework.stereotype.Service) 给Horseman加上@Service标注，则Horseman和Elephantman 的所有方法都匹配&lt;/p&gt;
&lt;p&gt;4.7 @target()&lt;/p&gt;
&lt;p&gt;所有标注了指定注解的类&lt;/p&gt;
&lt;p&gt;如 @target(org.springframework.stereotype.Service) 表示所有标注了@Service的类的所有方法&lt;/p&gt;
&lt;p&gt;4.8 this()&lt;/p&gt;
&lt;p&gt;大部分时候和target()相同，区别是this是在运行时生成代理类后，才判断代理类与指定的对象类型是否匹配&lt;/p&gt;
&lt;h3&gt;5 逻辑运算符&lt;/h3&gt;
&lt;p&gt;表达式可由多个切点函数通过逻辑运算组成&lt;/p&gt;
&lt;p&gt;5.1 &amp;amp;&amp;amp;&lt;/p&gt;
&lt;p&gt;与操作，求交集，也可以写成and&lt;/p&gt;
&lt;p&gt;例如 execution(* chop(..)) &amp;amp;&amp;amp; target(Horseman)  表示Horseman及其子类的chop方法&lt;/p&gt;
&lt;p&gt;5.2 ||&lt;/p&gt;
&lt;p&gt;或操作，求并集，也可以写成or&lt;/p&gt;
&lt;p&gt;例如 execution(* chop(..)) || args(String)  表示名称为chop的方法或者有一个String型参数的方法&lt;/p&gt;
&lt;p&gt;5.3 !&lt;/p&gt;
&lt;p&gt;非操作，求反集，也可以写成not&lt;/p&gt;
&lt;h3&gt;6 实际使用：&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;package com.example.aop.aop.aspect;

import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.*;
import org.springframework.core.annotation.Order;
import org.springframework.stereotype.Component;

/**
 * DESC: *
 * Created by zhengyong on 2019/6/23.
 */
@Aspect
@Component
//多切面时，使用order注解来排序，执行规则是，先进后出。（面包反弹）
@Order(1)
public class MyAspect {

    /**
     * 抽取重复切入点
     * 要求：public void 方法 且未实现的方法
     */
    @Pointcut(&quot;execution(public Object com..service.MethodBusiness.*(int,int))&quot;)
    public void testPoint(){}
    
    /**
     * 抽取重复切入点 抓取注解
     * 要求：public void 方法 且未实现的方法
     */
    @Pointcut(value = &quot;@annotation(com.zy.ossspringbootstarter.annotation.TestAnnotation)&quot;)
    public void pointCut(){};


    /**
     * 告诉spring何时执行
     * try{
     *   @before 前置通知
     *   method.invoke();
     *   @afterreturning 返回通知
     * }catch(e){
     *   @afterthrowing 异常通知
     * }finally{
     *   @after 后置通知
     * }
     *
     * @Before         目标方法执行前
     * @AfterReturning 目标方法正常返回
     * @AfterThrowing  目标方法抛出异常后
     * @After          最后执行
     * @Around          环绕
     */
    //before表示何时执行，切入点表达式表示何时执行
    @Before(value = &quot;testPoint()&quot;)
    public static void testBefore() {
        System.out.println(&quot;切面，前置通知&quot;);
    }

    /**
     * 切入点表达式：
     * 固定格式： execution(访问权限符 返回值类型 方法类全名（参数列表）)
     * 通配符：
     *      * :
     *         1)匹配一个或者多个字符 execution(public Object com.example.aop.aop.service.Method*.*(int,int))
     *         2)匹配任意一个参数(可能会重载无法切入) execution(public Object com.example.aop.aop.service.Method*.*(int,*))
     *         3）只能匹配一层路径
     *         4）*不能代替访问权限符，可以省略
     *      ..:
     *         1）匹配任意个数任意参数（表达式开头不能已双点开始）
     *         execution(public Object com.example.aop.aop.service.MethodBusiness.*(..))
     *         2)匹配任意多层路径
     *         execution(public Object com..service.MethodBusiness.*(int,int))
     * 最精确的
     *  execution(public Object com.example.aop.aop.service.MethodBusiness.add(int,int))
     * 最模糊的
     *  execution(* *.*(..)) 千万别写
     *
     *  aferReturning可以拿到结果信息
     *  Object result 方法参数列表中写入接受结果的类型
     *  告诉spring 返回结果用什么参数来接收：returning = &quot;result&quot;
     */
    @AfterReturning(value = &quot;execution(public Object com.example.aop.aop.service.Method*.*(*,*))&quot;,returning = &quot;result&quot;)
    public static void testAfterReturning(JoinPoint joinPoint,Object result) {
        System.out.println(&quot;切面，返回通知,方法名：&quot;+joinPoint.getSignature().getName()+&quot;,结果是：&quot;+result);
    }

    /**
     * 通知执行顺序
     *  正常：@before @after（后置通知）  @aferReturnin(返回通知)
     *  异常：@before @after（后置通知）   @aferThrowing（异常通知）
     *
     * @AfterThrowing 可以获取异常信息
     */
    @AfterThrowing(value = &quot;execution(public Object com.example.aop.aop.service.MethodBusiness.*(int,int))&quot;,throwing =&quot;e&quot;)
    public static void testAfterThrowing(JoinPoint joinPoint,Exception e){
        System.out.println(&quot;切面，异常通知,异常是：&quot;+e);
    }

    /**
     * joinPoint获取 方法名和  方法参数
     */
    @After(value = &quot;execution(public Object com.example.aop.aop.service.MethodBusiness.*(..))&quot;)
    public static void testAfter(JoinPoint joinPoint){
        //获取签名---获取方法名
        String name = joinPoint.getSignature().getName();
        System.out.println(&quot;方法名：&quot;+name);
        Object[] args = joinPoint.getArgs();
        for (Object arg : args) {
            System.out.println(&quot;方法参数列表：&quot;+arg);
        }
        System.out.println(&quot;切面，后置通知&quot;);
    }

    /**
     * 环绕方法就是动态代理
     * 和普通方法的却别就是，会直接影响目标方法
    
     	环绕通知，只影响当前切面中的通知执行顺序（比较深入了解执行顺序）
     *
     * @param joinPoint
     * @return
     * @throws Throwable
     */
    @Around(value = &quot;execution(public Object com.example.aop.aop.service.MethodBusiness.*(..))&quot;)
    public static Object testAround(ProceedingJoinPoint joinPoint) throws Throwable{
        Object[] args = joinPoint.getArgs();
        Object proceed = null;
        try{
            //before
            //利用反射，执行目标方法，需要参数,就是mothod.invoke()
             proceed= joinPoint.proceed(args);
             //afterReturning
        }catch(Exception e){
            //afterThrowing
            //如果有异常，要抛出去（看需求）
        }finally {
            //after
        }
        //反射调用后，返回值一定也要返回出去
        return proceed;
    }
}

&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;多切面执行顺序图解：&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;面包反弹原理&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;%E5%A4%9A%E5%88%87%E9%9D%A2%E6%89%A7%E8%A1%8C%E9%A1%BA%E5%BA%8F.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;h3&gt;aop获取注解上的值&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;    @Before(value = &quot;@annotation(com.example.aop.aop.annotation.DoSomething)&quot;)
    public void before(JoinPoint joinPoint){
        //获取签名
        Signature signature = joinPoint.getSignature();
        //获取强转为方法签名
        MethodSignature methodSignature = (MethodSignature) signature;
        //获取方法上的注解
        DoSomething annotation = methodSignature.getMethod().getAnnotation(DoSomething.class);
        //获取注解上的参数的值
        if(annotation.isDoSomething()){
            System.out.println(&quot;【老铁你来了】&quot;);
        }else{
            System.out.println(&quot;【给老子滚蛋】&quot;);
        }

        //获取参数
        Object[] args = joinPoint.getArgs();
        for (Object arg : args) {
            System.out.println(&quot;参数是：&quot;+arg);
        }
      
      	String[] paramNames = ((CodeSignature) joinPoint.getSignature()).getParameterNames();
        for (String paramName : paramNames) {
            System.out.println(&quot;参数名是&quot;+paramName);
        }
    
      
        System.out.println(&quot;注解前置方法&quot;);
    }
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;/**
* (joinPoint.getArgs()[0])  获取某个参数
* (joinPoint.getArgs()[0]).getClass() 获取参数类型（反射）
*  (joinPoint.getArgs()[0]).getClass().getDeclaredField(amountName)  获取属性
*  不能访问类的成员。带有修饰词“private”的属性 eg  	aClass.getDeclaredField(bizOrderNoName).get(object)
*  报错。解决方法，通过get方法获取。
*  String bizOrderNo = (String) object.getClass().getDeclaredMethod(&quot;getBizOrderNo&quot;).invoke(object);
 */
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;在SpringAOP中如何获取请求的参数和参数名&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;    private static Map getFieldsName(ProceedingJoinPoint joinPoint) throws ClassNotFoundException, NoSuchMethodException {
        String classType = joinPoint.getTarget().getClass().getName();
        String methodName = joinPoint.getSignature().getName();
        // 参数值
        Object[] args = joinPoint.getArgs();
        Class&amp;lt;?&amp;gt;[] classes = new Class[args.length];
        for (int k = 0; k &amp;lt; args.length; k++) {
            if (!args[k].getClass().isPrimitive()) {
                // 获取的是封装类型而不是基础类型
                String result = args[k].getClass().getName();
                Class s = map.get(result);
                if(args[k].getClass().getName().equals(RequestFacade.class.getName())){
                    //处理HttpServletRequest请求参数，导致反射找不到方法
                    classes[k] = s == null ? HttpServletRequest.class: s;
                }else if(args[k].getClass().getName().equals(ResponseFacade.class.getName())){
                    classes[k] = s == null ? HttpServletResponse.class: s;
                }else {
                    classes[k] = s == null ? args[k].getClass() : s;
                }
            }
        }
        ParameterNameDiscoverer pnd = new DefaultParameterNameDiscoverer();
        // 获取指定的方法，第二个参数可以不传，但是为了防止有重载的现象，还是需要传入参数的类型
        Method method = Class.forName(classType).getMethod(methodName, classes);
        // 参数名
        String[] parameterNames = pnd.getParameterNames(method);
        // 通过map封装参数和参数值
        HashMap&amp;lt;String, Object&amp;gt; paramMap = new HashMap();
        for (int i = 0; i &amp;lt; parameterNames.length; i++) {
            paramMap.put(parameterNames[i], args[i]);
        }
        return paramMap;
    }

    private static HashMap&amp;lt;String, Class&amp;gt; map = new HashMap&amp;lt;String, Class&amp;gt;() {
        {
            put(&quot;java.lang.Integer&quot;, int.class);
            put(&quot;java.lang.Double&quot;, double.class);
            put(&quot;java.lang.Float&quot;, float.class);
            put(&quot;java.lang.Long&quot;, long.class);
            put(&quot;java.lang.Short&quot;, short.class);
            put(&quot;java.lang.Boolean&quot;, boolean.class);
            put(&quot;java.lang.Char&quot;, char.class);
        }
    };
&lt;/code&gt;&lt;/pre&gt;
&lt;hr /&gt;
&lt;h2&gt;Spring的AOP顺序&lt;/h2&gt;
&lt;p&gt;Spring的循环依赖&lt;/p&gt;
&lt;p&gt;AOP常用注解：&lt;/p&gt;
&lt;p&gt;@Before 前置通知：目标方法之前执行&lt;/p&gt;
&lt;p&gt;@After 后置通知：目标方法之后执行（始终执行)&lt;/p&gt;
&lt;p&gt;@AfterReturning 返回后通知：执行方法结束前执行(异常不执行)&lt;/p&gt;
&lt;p&gt;@AfterThrowing 异常通知：出现异常时候执行&lt;/p&gt;
&lt;p&gt;@Around 环绕通知：环绕目标方法执行&lt;/p&gt;
&lt;h3&gt;面试题&lt;/h3&gt;
&lt;p&gt;你肯定知道spring，那说说aop的全部通知顺序springboot或springboot2对aop的执行顺序影响？&lt;/p&gt;
&lt;p&gt;说说你使用AOP中碰到的坑?&lt;/p&gt;
&lt;h4&gt;28_spring4下的aop测试案例&lt;/h4&gt;
&lt;p&gt;新建Maven工程，pom.xml如下：&lt;/p&gt;
&lt;h5&gt;pom&lt;/h5&gt;
&lt;pre&gt;&lt;code&gt;&amp;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;&amp;gt;
	&amp;lt;modelVersion&amp;gt;4.0.0&amp;lt;/modelVersion&amp;gt;

	&amp;lt;parent&amp;gt;
	    &amp;lt;groupId&amp;gt;org.springframework.boot&amp;lt;/groupId&amp;gt;
	    &amp;lt;artifactId&amp;gt;spring-boot-starter-parent&amp;lt;/artifactId&amp;gt;
	    &amp;lt;version&amp;gt;1.5.9.RELEASE&amp;lt;/version&amp;gt;
	    &amp;lt;relativePath/&amp;gt; &amp;lt;!-- lookup parent from repository --&amp;gt;
	&amp;lt;/parent&amp;gt;

	&amp;lt;groupId&amp;gt;com.lun&amp;lt;/groupId&amp;gt;
	&amp;lt;artifactId&amp;gt;HelloSpringBoot&amp;lt;/artifactId&amp;gt;
	&amp;lt;version&amp;gt;1.0.0-SNAPSHOT&amp;lt;/version&amp;gt;
	&amp;lt;packaging&amp;gt;jar&amp;lt;/packaging&amp;gt;

	&amp;lt;name&amp;gt;HelloSpringBoot&amp;lt;/name&amp;gt;
	&amp;lt;url&amp;gt;http://maven.apache.org&amp;lt;/url&amp;gt;

	&amp;lt;properties&amp;gt;
		&amp;lt;project.build.sourceEncoding&amp;gt;UTF-8&amp;lt;/project.build.sourceEncoding&amp;gt;
	&amp;lt;/properties&amp;gt;

	&amp;lt;dependencies&amp;gt;
		&amp;lt;!-- &amp;lt;version&amp;gt;1.5.9.RELEASE&amp;lt;/version〉 ch/qos/Logback/core/joran/spi/JoranException解决方案--&amp;gt;
		&amp;lt;dependency&amp;gt;
		    &amp;lt;groupId&amp;gt;ch.qos.logback&amp;lt;/groupId&amp;gt;
		    &amp;lt;artifactId&amp;gt;logback-core&amp;lt;/artifactId&amp;gt;
		    &amp;lt;version&amp;gt;1.1.3&amp;lt;/version&amp;gt;
		&amp;lt;/dependency&amp;gt;
		
		&amp;lt;dependency&amp;gt;
		    &amp;lt;groupId&amp;gt;ch.qos.logback&amp;lt;/groupId&amp;gt;
		    &amp;lt;artifactId&amp;gt;logback-access&amp;lt;/artifactId&amp;gt;
		    &amp;lt;version&amp;gt;1.1.3&amp;lt;/version&amp;gt;
		&amp;lt;/dependency&amp;gt;
		
		&amp;lt;dependency&amp;gt;
		    &amp;lt;groupId&amp;gt;ch.qos.logback&amp;lt;/groupId&amp;gt;
		    &amp;lt;artifactId&amp;gt;logback-classic&amp;lt;/artifactId&amp;gt;
		    &amp;lt;version&amp;gt;1.1.3&amp;lt;/version&amp;gt;
		&amp;lt;/dependency&amp;gt;
		
		&amp;lt;!-- web+actuator --&amp;gt;
		&amp;lt;dependency&amp;gt;
			&amp;lt;groupId&amp;gt;org.springframework.boot&amp;lt;/groupId&amp;gt;
			&amp;lt;artifactId&amp;gt;spring-boot-starter-web&amp;lt;/artifactId&amp;gt;
		&amp;lt;/dependency&amp;gt;
		
		&amp;lt;dependency&amp;gt;
			&amp;lt;groupId&amp;gt;org.springframework.boot&amp;lt;/groupId&amp;gt;
			&amp;lt;artifactId&amp;gt;spring-boot-starter-actuator&amp;lt;/artifactId&amp;gt;
		&amp;lt;/dependency&amp;gt;
		
		&amp;lt;!-- SpringBoot与Redis整合依赖 --&amp;gt;
		&amp;lt;!-- 
		&amp;lt;dependency&amp;gt;
			&amp;lt;groupId&amp;gt;org.springframework.boot&amp;lt;/groupId&amp;gt;
			&amp;lt;artifactId&amp;gt;spring-boot-starter-data-redis&amp;lt;/artifactId&amp;gt;
		&amp;lt;/dependency&amp;gt;
		 --&amp;gt;
		
		&amp;lt;dependency&amp;gt;
			&amp;lt;groupId&amp;gt;org.apache.commons&amp;lt;/groupId&amp;gt;
			&amp;lt;artifactId&amp;gt;commons-pool2&amp;lt;/artifactId&amp;gt;
		&amp;lt;/dependency&amp;gt;
		
		&amp;lt;!-- jedis --&amp;gt;
		&amp;lt;dependency&amp;gt;
			&amp;lt;groupId&amp;gt;redis.clients&amp;lt;/groupId&amp;gt;
			&amp;lt;artifactId&amp;gt;jedis&amp;lt;/artifactId&amp;gt;
			&amp;lt;version&amp;gt;3.1.0&amp;lt;/version&amp;gt;
		&amp;lt;/dependency&amp;gt;

		&amp;lt;!-- Spring Boot AOP技术--&amp;gt;
		&amp;lt;dependency&amp;gt;
			&amp;lt;groupId&amp;gt;org.springframework.boot&amp;lt;/groupId&amp;gt;
			&amp;lt;artifactId&amp;gt;spring-boot-starter-aop&amp;lt;/artifactId&amp;gt;
		&amp;lt;/dependency&amp;gt;
		
		&amp;lt;!-- redisson --&amp;gt;
		&amp;lt;dependency&amp;gt;
			&amp;lt;groupId&amp;gt;org.redisson&amp;lt;/groupId&amp;gt;
			&amp;lt;artifactId&amp;gt;redisson&amp;lt;/artifactId&amp;gt;
			&amp;lt;version&amp;gt;3.13.4&amp;lt;/version&amp;gt;
		&amp;lt;/dependency&amp;gt;

		&amp;lt;!-- 一般通用基础配置 --&amp;gt;
		&amp;lt;dependency&amp;gt;
		    &amp;lt;groupId&amp;gt;org.springframework.boot&amp;lt;/groupId&amp;gt;
		    &amp;lt;artifactId&amp;gt;spring-boot-devtools&amp;lt;/artifactId&amp;gt;
		    &amp;lt;scope&amp;gt;runtime&amp;lt;/scope&amp;gt;
		    &amp;lt;optional&amp;gt;true&amp;lt;/optional&amp;gt;
		&amp;lt;/dependency&amp;gt;
		
		&amp;lt;dependency&amp;gt;
		    &amp;lt;groupId&amp;gt;org.projectlombok&amp;lt;/groupId&amp;gt;
		    &amp;lt;artifactId&amp;gt;lombok&amp;lt;/artifactId&amp;gt;
		    &amp;lt;optional&amp;gt;true&amp;lt;/optional&amp;gt;
		&amp;lt;/dependency&amp;gt;
		
		&amp;lt;dependency&amp;gt;
		    &amp;lt;groupId&amp;gt;org.springframework.boot&amp;lt;/groupId&amp;gt;
		    &amp;lt;artifactId&amp;gt;spring-boot-starter-test&amp;lt;/artifactId&amp;gt;&amp;lt;scope&amp;gt;test&amp;lt;/scope&amp;gt;
		    &amp;lt;exclusions&amp;gt;
		        &amp;lt;exclusion&amp;gt;
		            &amp;lt;groupId&amp;gt;org.junit.vintage&amp;lt;/groupId&amp;gt;
		            &amp;lt;artifactId&amp;gt;junit-vintage-engine&amp;lt;/artifactId&amp;gt;
		        &amp;lt;/exclusion&amp;gt;
		    &amp;lt;/exclusions&amp;gt;
		&amp;lt;/dependency&amp;gt;

	&amp;lt;/dependencies&amp;gt;
	
	&amp;lt;build&amp;gt;
		&amp;lt;plugins&amp;gt;
			&amp;lt;plugin&amp;gt;
				&amp;lt;groupId&amp;gt;org.springframework.boot&amp;lt;/groupId&amp;gt;
				&amp;lt;artifactId&amp;gt;spring-boot-maven-plugin&amp;lt;/artifactId&amp;gt;
			&amp;lt;/plugin&amp;gt;
		&amp;lt;/plugins&amp;gt;
	&amp;lt;/build&amp;gt;

&amp;lt;/project&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;h5&gt;启动类&lt;/h5&gt;
&lt;pre&gt;&lt;code&gt;import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;

@SpringBootApplication
public class MainApplication {
	public static void main(String[] args) {
		SpringApplication.run(MainApplication.class, args);
	}
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h5&gt;接口CalcService&lt;/h5&gt;
&lt;pre&gt;&lt;code&gt;public interface CalcService {	
	public int div(int x, int y);
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h5&gt;接口实现类CalcServiceImpl新加@Service&lt;/h5&gt;
&lt;pre&gt;&lt;code&gt;import org.springframework.stereotype.Service;

@Service
public class CalcServiceImpl implements CalcService {

	@Override
	public int div(int x, int y) {
		int result = x / y;
		System.out.println(&quot;===&amp;gt;CalcServiceImpl被调用，计算结果为：&quot; + result);
		return result;
	}
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;新建一个切面类MyAspect并为切面类新增两个注解：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;@Aspect 指定一个类为切面类&lt;/li&gt;
&lt;li&gt;@Component 纳入Spring容器管理&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.After;
import org.aspectj.lang.annotation.AfterReturning;
import org.aspectj.lang.annotation.AfterThrowing;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Before;
import org.springframework.stereotype.Component;

@Aspect
@Component
public class MyAspect {
    @Before(&quot;execution(public int com.lun.interview.service.CalcServiceImpl.*(..))&quot;)
    public void beforeNotify() {
        System.out.println(&quot;********@Before我是前置通知&quot;);
    }

    @After(&quot;execution(public int com.lun.interview.service.CalcServiceImpl.*(..))&quot;)
    public void afterNotify() {
        System.out.println(&quot;********@After我是后置通知&quot;);
    }

    @AfterReturning(&quot;execution(public int com.lun.interview.service.CalcServiceImpl.*(..))&quot;)
    public void afterReturningNotify() {
        System.out.println(&quot;********@AfterReturning我是返回后通知&quot;);
    }

    @AfterThrowing(&quot; execution(public int com.lun.interview.service.CalcServiceImpl.*(..))&quot;)
    public void afterThrowingNotify() {
        System.out.println(&quot;********@AfterThrowing我是异常通知&quot;);
    }

    @Around(&quot; execution(public int com.lun.interview.service.CalcServiceImpl.*(..))&quot;)
    public Object around(ProceedingJoinPoint proceedingJoinPoint) throws Throwable {
        Object retvalue = null;
        System.out.println(&quot;我是环绕通知之前AAA&quot;);
        retvalue = proceedingJoinPoint.proceed();
        System.out.println(&quot;我是环绕通知之后BBB&quot;);
        return retvalue ;
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h5&gt;测试类&lt;/h5&gt;
&lt;pre&gt;&lt;code&gt;import javax.annotation.Resource;

import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.boot.SpringBootVersion;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.core.SpringVersion;
import org.springframework.test.context.junit4.SpringRunner;

import com.lun.interview.service.CalcService;

@SpringBootTest
@RunWith(SpringRunner.class)
public class AopTest {

	@Resource
	private CalcService calcService;
	
	@Test
	public void testAop4() {
		System.out.println(String.format(&quot;Spring Verision : %s, Sring Boot Version : %s.&quot;, //
				SpringVersion.getVersion(), SpringBootVersion.getVersion()));
		
		calcService.div(10, 2);
	}
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h5&gt;spring4下的aop测试结果&lt;/h5&gt;
&lt;pre&gt;&lt;code&gt;Spring Verision : 4.3.13.RELEASE, Sring Boot Version : 1.5.9.RELEASE.

我是环绕通知之前AAA
********@Before我是前置通知
===&amp;gt;CalcServiceImpl被调用，计算结果为：5
我是环绕通知之后BBB
********@After我是后置通知
********@AfterReturning我是返回后通知
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;修改测试类，让其抛出算术异常类：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;@SpringBootTest
@RunWith(SpringRunner.class)
public class AopTest {

	@Resource
	private CalcService calcService;
	
	@Test
	public void testAop4() {
		System.out.println(String.format(&quot;Spring Verision : %s, Sring Boot Version : %s.&quot;, //
				SpringVersion.getVersion(), SpringBootVersion.getVersion()));
		
		//calcService.div(10, 2);
        calcService.div(10, 0);//将会抛异常
	}
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;结果&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;Spring Verision : 4.3.13.RELEASE, Sring Boot Version : 1.5.9.RELEASE.

我是环绕通知之前AAA
********@Before我是前置通知
********@After我是后置通知
********@AfterThrowing我是异常通知

java.lang.ArithmeticException: / by zero
	at com.lun.interview.service.CalcServiceImpl.div(CalcServiceImpl.java:10)
	at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
	at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62)
	at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
	at java.lang.reflect.Method.invoke(Method.java:498)
	...
&lt;/code&gt;&lt;/pre&gt;
&lt;h5&gt;&lt;strong&gt;小结&lt;/strong&gt;&lt;/h5&gt;
&lt;p&gt;AOP执行顺序：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;正常情况下：@Before前置通知-----&amp;gt;@After后置通知-----&amp;gt;@AfterRunning正常返回&lt;/li&gt;
&lt;li&gt;异常情况下：@Before前置通知-----&amp;gt;@After后置通知-----&amp;gt;@AfterThrowing方法异常&lt;/li&gt;
&lt;/ul&gt;
&lt;h4&gt;spring5下的aop测试&lt;/h4&gt;
&lt;h5&gt;修改POM&lt;/h5&gt;
&lt;pre&gt;&lt;code&gt;&amp;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;&amp;gt;
	&amp;lt;modelVersion&amp;gt;4.0.0&amp;lt;/modelVersion&amp;gt;

	&amp;lt;parent&amp;gt;
	    &amp;lt;groupId&amp;gt;org.springframework.boot&amp;lt;/groupId&amp;gt;
	    &amp;lt;artifactId&amp;gt;spring-boot-starter-parent&amp;lt;/artifactId&amp;gt;
	    &amp;lt;version&amp;gt;2.3.3.RELEASE&amp;lt;/version&amp;gt;
	    &amp;lt;!-- 1.5.9.RELEASE --&amp;gt;
	    &amp;lt;relativePath/&amp;gt; &amp;lt;!-- lookup parent from repository --&amp;gt;
	&amp;lt;/parent&amp;gt;

	&amp;lt;groupId&amp;gt;com.lun&amp;lt;/groupId&amp;gt;
	&amp;lt;artifactId&amp;gt;HelloSpringBoot&amp;lt;/artifactId&amp;gt;
	&amp;lt;version&amp;gt;1.0.0-SNAPSHOT&amp;lt;/version&amp;gt;
	&amp;lt;packaging&amp;gt;jar&amp;lt;/packaging&amp;gt;

	&amp;lt;name&amp;gt;HelloSpringBoot&amp;lt;/name&amp;gt;
	&amp;lt;url&amp;gt;http://maven.apache.org&amp;lt;/url&amp;gt;

	&amp;lt;properties&amp;gt;
		&amp;lt;project.build.sourceEncoding&amp;gt;UTF-8&amp;lt;/project.build.sourceEncoding&amp;gt;
	&amp;lt;/properties&amp;gt;

	&amp;lt;dependencies&amp;gt;
		&amp;lt;!-- web+actuator --&amp;gt;
		&amp;lt;dependency&amp;gt;
			&amp;lt;groupId&amp;gt;org.springframework.boot&amp;lt;/groupId&amp;gt;
			&amp;lt;artifactId&amp;gt;spring-boot-starter-web&amp;lt;/artifactId&amp;gt;
		&amp;lt;/dependency&amp;gt;
		
		&amp;lt;dependency&amp;gt;
			&amp;lt;groupId&amp;gt;org.springframework.boot&amp;lt;/groupId&amp;gt;
			&amp;lt;artifactId&amp;gt;spring-boot-starter-actuator&amp;lt;/artifactId&amp;gt;
		&amp;lt;/dependency&amp;gt;
		
		&amp;lt;!-- SpringBoot与Redis整合依赖 --&amp;gt;
		&amp;lt;!-- 
		&amp;lt;dependency&amp;gt;
			&amp;lt;groupId&amp;gt;org.springframework.boot&amp;lt;/groupId&amp;gt;
			&amp;lt;artifactId&amp;gt;spring-boot-starter-data-redis&amp;lt;/artifactId&amp;gt;
		&amp;lt;/dependency&amp;gt;
		 --&amp;gt;
		
		&amp;lt;dependency&amp;gt;
			&amp;lt;groupId&amp;gt;org.apache.commons&amp;lt;/groupId&amp;gt;
			&amp;lt;artifactId&amp;gt;commons-pool2&amp;lt;/artifactId&amp;gt;
		&amp;lt;/dependency&amp;gt;
		
		&amp;lt;!-- jedis --&amp;gt;
		&amp;lt;dependency&amp;gt;
			&amp;lt;groupId&amp;gt;redis.clients&amp;lt;/groupId&amp;gt;
			&amp;lt;artifactId&amp;gt;jedis&amp;lt;/artifactId&amp;gt;
			&amp;lt;version&amp;gt;3.1.0&amp;lt;/version&amp;gt;
		&amp;lt;/dependency&amp;gt;

		&amp;lt;!-- Spring Boot AOP技术--&amp;gt;
		&amp;lt;dependency&amp;gt;
			&amp;lt;groupId&amp;gt;org.springframework.boot&amp;lt;/groupId&amp;gt;
			&amp;lt;artifactId&amp;gt;spring-boot-starter-aop&amp;lt;/artifactId&amp;gt;
		&amp;lt;/dependency&amp;gt;
		
		&amp;lt;!-- redisson --&amp;gt;
		&amp;lt;dependency&amp;gt;
			&amp;lt;groupId&amp;gt;org.redisson&amp;lt;/groupId&amp;gt;
			&amp;lt;artifactId&amp;gt;redisson&amp;lt;/artifactId&amp;gt;
			&amp;lt;version&amp;gt;3.13.4&amp;lt;/version&amp;gt;
		&amp;lt;/dependency&amp;gt;

		&amp;lt;!-- 一般通用基础配置 --&amp;gt;
		&amp;lt;dependency&amp;gt;
		    &amp;lt;groupId&amp;gt;org.springframework.boot&amp;lt;/groupId&amp;gt;
		    &amp;lt;artifactId&amp;gt;spring-boot-devtools&amp;lt;/artifactId&amp;gt;
		    &amp;lt;scope&amp;gt;runtime&amp;lt;/scope&amp;gt;
		    &amp;lt;optional&amp;gt;true&amp;lt;/optional&amp;gt;
		&amp;lt;/dependency&amp;gt;
		
		&amp;lt;dependency&amp;gt;
		    &amp;lt;groupId&amp;gt;org.projectlombok&amp;lt;/groupId&amp;gt;
		    &amp;lt;artifactId&amp;gt;lombok&amp;lt;/artifactId&amp;gt;
		    &amp;lt;optional&amp;gt;true&amp;lt;/optional&amp;gt;
		&amp;lt;/dependency&amp;gt;
		
		&amp;lt;dependency&amp;gt;
		    &amp;lt;groupId&amp;gt;org.springframework.boot&amp;lt;/groupId&amp;gt;
		    &amp;lt;artifactId&amp;gt;spring-boot-starter-test&amp;lt;/artifactId&amp;gt;&amp;lt;scope&amp;gt;test&amp;lt;/scope&amp;gt;
		    &amp;lt;exclusions&amp;gt;
		        &amp;lt;exclusion&amp;gt;
		            &amp;lt;groupId&amp;gt;org.junit.vintage&amp;lt;/groupId&amp;gt;
		            &amp;lt;artifactId&amp;gt;junit-vintage-engine&amp;lt;/artifactId&amp;gt;
		        &amp;lt;/exclusion&amp;gt;
		    &amp;lt;/exclusions&amp;gt;
		&amp;lt;/dependency&amp;gt;

	&amp;lt;/dependencies&amp;gt;
	
	&amp;lt;build&amp;gt;
		&amp;lt;plugins&amp;gt;
			&amp;lt;plugin&amp;gt;
				&amp;lt;groupId&amp;gt;org.springframework.boot&amp;lt;/groupId&amp;gt;
				&amp;lt;artifactId&amp;gt;spring-boot-maven-plugin&amp;lt;/artifactId&amp;gt;
			&amp;lt;/plugin&amp;gt;
		&amp;lt;/plugins&amp;gt;
	&amp;lt;/build&amp;gt;

&amp;lt;/project&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;h5&gt;修改测试类&lt;/h5&gt;
&lt;pre&gt;&lt;code&gt;import javax.annotation.Resource;

import org.junit.jupiter.api.Test;
//import org.junit.Test;
//import org.junit.runner.RunWith;
import org.springframework.boot.SpringBootVersion;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.core.SpringVersion;
//import org.springframework.test.context.junit4.SpringRunner;

import com.lun.interview.service.CalcService;

@SpringBootTest
//@RunWith(SpringRunner.class)
public class AopTest {

	@Resource
	private CalcService calcService;
	
	@Test
	public void testAop5() {
		System.out.println(String.format(&quot;Spring Verision : %s, Sring Boot Version : %s.&quot;, //
				SpringVersion.getVersion(), SpringBootVersion.getVersion()));
		System.out.println();
		calcService.div(10, 2);
		//calcService.div(10, 0);
	}
	
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;输出结果&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;Spring Verision : 5.2.8.RELEASE, Sring Boot Version : 2.3.3.RELEASE.

我是环绕通知之前AAA
********@Before我是前置通知
===&amp;gt;CalcServiceImpl被调用，计算结果为：5
********@AfterReturning我是返回后通知
********@After我是后置通知
我是环绕通知之后BBB
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;修改测试类，让其抛出算术异常类：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;@SpringBootTest
public class AopTest {

	@Resource
	private CalcService calcService;

    ...
	
	@Test
	public void testAop5() {
		System.out.println(String.format(&quot;Spring Verision : %s, Sring Boot Version : %s.&quot;, //
				SpringVersion.getVersion(), SpringBootVersion.getVersion()));
		System.out.println();
		calcService.div(10, 2);
		//calcService.div(10, 0);
	}
	
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;输出结果&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;Spring Verision : 5.2.8.RELEASE, Sring Boot Version : 2.3.3.RELEASE.

我是环绕通知之前AAA
********@Before我是前置通知
********@AfterThrowing我是异常通知
********@After我是后置通知

java.lang.ArithmeticException: / by zero
	at com.lun.interview.service.CalcServiceImpl.div(CalcServiceImpl.java:10)
	at com.lun.interview.service.CalcServiceImpl$$FastClassBySpringCGLIB$$355acbc4.invoke(&amp;lt;generated&amp;gt;)
	at org.springframework.cglib.proxy.MethodProxy.invoke(MethodProxy.java:218)
	at org.springframework.aop.framework.CglibAopProxy$CglibMethodInvocation.invokeJoinpoint(CglibAopProxy.java:771)
&lt;/code&gt;&lt;/pre&gt;
&lt;h4&gt;总节&lt;/h4&gt;
&lt;p&gt;spring4 aop执行顺序:&lt;/p&gt;
&lt;blockquote&gt;
&lt;ul&gt;
&lt;li&gt;正常情况下：@Before前置通知-----&amp;gt;@After后置通知-----&amp;gt;&lt;strong&gt;@AfterRunning正常返回&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;异常情况下：@Before前置通知-----&amp;gt;@After后置通知-----&amp;gt;&lt;strong&gt;@AfterThrowing方法异常&lt;/strong&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/blockquote&gt;
&lt;p&gt;spring5 aop执行顺序(将After想象成finally):&lt;/p&gt;
&lt;blockquote&gt;
&lt;ul&gt;
&lt;li&gt;正常情况下：@Before前置通知-----&amp;gt;&lt;strong&gt;@AfterRunning正常返回&lt;/strong&gt;-----&amp;gt;@After后置通知&lt;/li&gt;
&lt;li&gt;异常情况下：@Before前置通知-----&amp;gt;&lt;strong&gt;@AfterThrowing方法异常&lt;/strong&gt;-----&amp;gt;@After后置通知&lt;/li&gt;
&lt;/ul&gt;
&lt;/blockquote&gt;
&lt;p&gt;&lt;img src=&quot;https://tangtangit.com/_astro/image-20240319103609456.DVDk6TP-_Z17xthr.webp&quot; alt=&quot;image-20240319103609456&quot; /&gt;&lt;/p&gt;
&lt;h2&gt;Spring的循环依赖&lt;/h2&gt;
&lt;p&gt;恶心的大厂面试题。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;你解释下spring中的三级缓存？&lt;/li&gt;
&lt;li&gt;三级缓存分别是什么？三个Map有什么异同？&lt;/li&gt;
&lt;li&gt;什么是循环依赖？请你谈谈？看过spring源码吗？&lt;/li&gt;
&lt;li&gt;如何检测是否存在循环依赖？实际开发中见过循环依赖的异常吗？&lt;/li&gt;
&lt;li&gt;多例的情况下，循环依赖问题为什么无法解决？&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;什么是循环依赖？&lt;/h3&gt;
&lt;p&gt;多个bean之间相互依赖，形成了一个闭环。比如：A依赖于B、B依赖于C、C依赖于A。&lt;/p&gt;
&lt;p&gt;通常来说，如果问Spring容器内部如何解决循环依赖，一定是指默认的单例Bean中，属性互相引用的场景。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://tangtangit.com/_astro/image-20240319104418423.BKnOs0xB_1gkTwY.webp&quot; alt=&quot;image-20240319104418423&quot; /&gt;&lt;/p&gt;
&lt;p&gt;两种注入方式对循环依赖的影响&lt;/p&gt;
&lt;p&gt;循环依赖官网说明&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;Circular dependencies&lt;/p&gt;
&lt;p&gt;If you use predominantly constructor injection, it is possible to create an unresolvable circular dependency scenario.&lt;/p&gt;
&lt;p&gt;For example: Class A requires an instance of class B through constructor injection, and class B requires an instance of class A through constructor injection. If you configure beans for classes A and B to be injected into each other, the Spring IoC container detects this circular reference at runtime, and throws a BeanCurrentlyInCreationException.&lt;/p&gt;
&lt;p&gt;One possible solution is to edit the source code of some classes to be configured by setters rather than constructors. Alternatively, avoid constructor injection and use setter injection only. In other words, although it is not recommended, you can configure circular dependencies with setter injection.&lt;/p&gt;
&lt;p&gt;Unlike the typical case (with no circular dependencies), a circular dependency between bean A and bean B forces one of the beans to be injected into the other prior to being fully initialized itself (a classic chicken-and-egg scenario).&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h4&gt;结论&lt;/h4&gt;
&lt;p&gt;我们AB循环依赖问题只要A的注入方式是setter且singleton ，就不会有循环依赖问题。&lt;/p&gt;
&lt;h3&gt;spring循环依赖纯java代码验证案例&lt;/h3&gt;
&lt;p&gt;Spring容器循环依赖报错演示BeanCurrentlylnCreationException&lt;/p&gt;
&lt;p&gt;循环依赖现象在spring容器中注入依赖的对象，有2种情况&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;构造器方式注入依赖（不可行）&lt;/li&gt;
&lt;li&gt;以set方式注入依赖（可行）&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;构造器方式注入依赖（不可行）&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;@Component
public class ServiceB{
    private ServiceA serviceA;
    
    public ServiceB(ServiceA serviceA){
        this.serviceA = serviceA;
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;@Component
public class ServiceA{
    private ServiceB serviceB;
    
    public ServiceA(ServiceB serviceB){
        this.serviceB = serviceB;
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;public class ClientConstructor{
    public static void main(String[] args){
        new ServiceA(new ServiceB(new ServiceA()));//这会抛出编译异常
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;以set方式注入依赖（可行）&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;@Component
public class ServiceBB{
    private ServiceAA serviceAA;
    
    public void setServiceAA(ServiceAA serviceAA){
        this.serviceAA = serviceAA;
        System.out.println(&quot;B里面设置了A&quot;);
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;@Component
public class ServiceAA{
    private ServiceBB serviceBB;
    
    public void setServiceBB(ServiceBB serviceBB){
        this.serviceBB = serviceBB;
        System.out.println(&quot;A里面设置了B&quot;);
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;public class ClientSet{
    public static void main(String[] args){
        //创建serviceAA
        ServiceAA a = new ServiceAA();
        //创建serviceBB
        ServiceBB b = new ServiceBB();
        //将serviceA入到serviceB中
        b.setServiceAA(a);
        //将serviceB法入到serviceA中
        a.setServiceBB(b);
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;输出结果：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;B里面设置了A
A里面设置了B
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;spring循环依赖bug演示&lt;/h3&gt;
&lt;p&gt;beans：A，B&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;public class A {

	private B b;

	public B getB() {
		return b;
	}

	public void setB(B b) {
		this.b = b;
        System.out.println(&quot;A call setB.&quot;);
	}
}
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;public class B {

	private A a;

	public A getA() {
		return a;
	}

	public void setA(A a) {
		this.a = a;
        System.out.println(&quot;B call setA.&quot;);
	}	
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;运行类&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;import org.springframework.context.ApplicationContext;
import org.springframework.context.support.ClassPathXmlApplicationContext;

public class ClientSpringContainer {

	public static void main(String[] args) {
		ApplicationContext context = new ClassPathXmlApplicationContext(&quot;beans.xml&quot;);
		A a = context.getBean(&quot;a&quot;, A.class);
		B b = context.getBean(&quot;b&quot;, B.class);
	}
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h4&gt;单例&lt;/h4&gt;
&lt;p&gt;默认的单例(Singleton)的场景是&lt;strong&gt;支持&lt;/strong&gt;循环依赖的，不报错&lt;/p&gt;
&lt;p&gt;beans.xml&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&amp;lt;?xml version=&quot;1.0&quot; encoding=&quot;UTF-8&quot; ?&amp;gt;
&amp;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;
	xmlns:context=&quot;http://www.springframework.org/schema/context&quot;
	xmlns:aop=&quot;http://www.springframework.org/schema/aop&quot; xmlns:tx=&quot;http://www.springframework.org/schema/tx&quot;
	xsi:schemaLocation=&quot;http://www.springframework.org/schema/beans 
       http://www.springframework.org/schema/beans/spring-beans-4.0.xsd
       http://www.springframework.org/schema/context 
       http://www.springframework.org/schema/context/spring-context-4.0.xsd
       http://www.springframework.org/schema/tx 
       http://www.springframework.org/schema/tx/spring-tx-4.0.xsd
       http://www.springframework.org/schema/aop
       http://www.springframework.org/schema/aop/spring-aop-4.0.xsd&quot;&amp;gt;
    
    &amp;lt;bean id=&quot;a&quot; class=&quot;com.lun.interview.circular.A&quot;&amp;gt;
    	&amp;lt;property name=&quot;b&quot; ref=&quot;b&quot;&amp;gt;&amp;lt;/property&amp;gt;
    &amp;lt;/bean&amp;gt;
    &amp;lt;bean id=&quot;b&quot; class=&quot;com.lun.interview.circular.B&quot;&amp;gt;
    	&amp;lt;property name=&quot;a&quot; ref=&quot;a&quot;&amp;gt;&amp;lt;/property&amp;gt;
    &amp;lt;/bean&amp;gt;
    
&amp;lt;/beans&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;输出结果&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;00:00:25.649 [main] DEBUG org.springframework.context.support.ClassPathXmlApplicationContext - Refreshing org.springframework.context.support.ClassPathXmlApplicationContext@6d86b085
00:00:25.828 [main] DEBUG org.springframework.beans.factory.xml.XmlBeanDefinitionReader - Loaded 2 bean definitions from class path resource [beans.xml]
00:00:25.859 [main] DEBUG org.springframework.beans.factory.support.DefaultListableBeanFactory - Creating shared instance of singleton bean &apos;a&apos;
00:00:25.875 [main] DEBUG org.springframework.beans.factory.support.DefaultListableBeanFactory - Creating shared instance of singleton bean &apos;b&apos;
B call setA.
A call setB.
&lt;/code&gt;&lt;/pre&gt;
&lt;h4&gt;多例&lt;/h4&gt;
&lt;p&gt;原型(Prototype)的场景是&lt;strong&gt;不支持&lt;/strong&gt;循环依赖的，会报错&lt;/p&gt;
&lt;p&gt;beans.xml&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&amp;lt;?xml version=&quot;1.0&quot; encoding=&quot;UTF-8&quot; ?&amp;gt;
&amp;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;
	xmlns:context=&quot;http://www.springframework.org/schema/context&quot;
	xmlns:aop=&quot;http://www.springframework.org/schema/aop&quot; xmlns:tx=&quot;http://www.springframework.org/schema/tx&quot;
	xsi:schemaLocation=&quot;http://www.springframework.org/schema/beans 
       http://www.springframework.org/schema/beans/spring-beans-4.0.xsd
       http://www.springframework.org/schema/context 
       http://www.springframework.org/schema/context/spring-context-4.0.xsd
       http://www.springframework.org/schema/tx 
       http://www.springframework.org/schema/tx/spring-tx-4.0.xsd
       http://www.springframework.org/schema/aop
       http://www.springframework.org/schema/aop/spring-aop-4.0.xsd&quot;&amp;gt;
    
    &amp;lt;bean id=&quot;a&quot; class=&quot;com.lun.interview.circular.A&quot; scope=&quot;prototype&quot;&amp;gt;
    	&amp;lt;property name=&quot;b&quot; ref=&quot;b&quot;&amp;gt;&amp;lt;/property&amp;gt;
    &amp;lt;/bean&amp;gt;
    &amp;lt;bean id=&quot;b&quot; class=&quot;com.lun.interview.circular.B&quot; scope=&quot;prototype&quot;&amp;gt;
    	&amp;lt;property name=&quot;a&quot; ref=&quot;a&quot;&amp;gt;&amp;lt;/property&amp;gt;
    &amp;lt;/bean&amp;gt;
    
&amp;lt;/beans&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;输出结果&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;00:01:39.904 [main] DEBUG org.springframework.context.support.ClassPathXmlApplicationContext - Refreshing org.springframework.context.support.ClassPathXmlApplicationContext@6d86b085
00:01:40.062 [main] DEBUG org.springframework.beans.factory.xml.XmlBeanDefinitionReader - Loaded 2 bean definitions from class path resource [beans.xml]
Exception in thread &quot;main&quot; org.springframework.beans.factory.BeanCreationException: Error creating bean with name &apos;a&apos; defined in class path resource [beans.xml]: Cannot resolve reference to bean &apos;b&apos; while setting bean property &apos;b&apos;; nested exception is org.springframework.beans.factory.BeanCreationException: Error creating bean with name &apos;b&apos; defined in class path resource [beans.xml]: Cannot resolve reference to bean &apos;a&apos; while setting bean property &apos;a&apos;; nested exception is org.springframework.beans.factory.BeanCurrentlyInCreationException: Error creating bean with name &apos;a&apos;: Requested bean is currently in creation: Is there an unresolvable circular reference?
	at org.springframework.beans.factory.support.BeanDefinitionValueResolver.resolveReference(BeanDefinitionValueResolver.java:342)
	at org.springframework.beans.factory.support.BeanDefinitionValueResolver.resolveValueIfNecessary(BeanDefinitionValueResolver.java:113)
	at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.applyPropertyValues(AbstractAutowireCapableBeanFactory.java:1697)
	at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.populateBean(AbstractAutowireCapableBeanFactory.java:1442)
	at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.doCreateBean(AbstractAutowireCapableBeanFactory.java:593)
	at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.createBean(AbstractAutowireCapableBeanFactory.java:516)
	at org.springframework.beans.factory.support.AbstractBeanFactory.doGetBean(AbstractBeanFactory.java:342)
	at org.springframework.beans.factory.support.AbstractBeanFactory.getBean(AbstractBeanFactory.java:207)
	at org.springframework.context.support.AbstractApplicationContext.getBean(AbstractApplicationContext.java:1115)
	at com.lun.interview.circular.ClientSpringContainer.main(ClientSpringContainer.java:10)
Caused by: org.springframework.beans.factory.BeanCreationException: Error creating bean with name &apos;b&apos; defined in class path resource [beans.xml]: Cannot resolve reference to bean &apos;a&apos; while setting bean property &apos;a&apos;; nested exception is org.springframework.beans.factory.BeanCurrentlyInCreationException: Error creating bean with name &apos;a&apos;: Requested bean is currently in creation: Is there an unresolvable circular reference?
	at org.springframework.beans.factory.support.BeanDefinitionValueResolver.resolveReference(BeanDefinitionValueResolver.java:342)
	at org.springframework.beans.factory.support.BeanDefinitionValueResolver.resolveValueIfNecessary(BeanDefinitionValueResolver.java:113)
	at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.applyPropertyValues(AbstractAutowireCapableBeanFactory.java:1697)
	at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.populateBean(AbstractAutowireCapableBeanFactory.java:1442)
	at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.doCreateBean(AbstractAutowireCapableBeanFactory.java:593)
	at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.createBean(AbstractAutowireCapableBeanFactory.java:516)
	at org.springframework.beans.factory.support.AbstractBeanFactory.doGetBean(AbstractBeanFactory.java:342)
	at org.springframework.beans.factory.support.AbstractBeanFactory.getBean(AbstractBeanFactory.java:202)
	at org.springframework.beans.factory.support.BeanDefinitionValueResolver.resolveReference(BeanDefinitionValueResolver.java:330)
	... 9 more
Caused by: org.springframework.beans.factory.BeanCurrentlyInCreationException: Error creating bean with name &apos;a&apos;: Requested bean is currently in creation: Is there an unresolvable circular reference?
	at org.springframework.beans.factory.support.AbstractBeanFactory.doGetBean(AbstractBeanFactory.java:268)
	at org.springframework.beans.factory.support.AbstractBeanFactory.getBean(AbstractBeanFactory.java:202)
	at org.springframework.beans.factory.support.BeanDefinitionValueResolver.resolveReference(BeanDefinitionValueResolver.java:330)
	... 17 more
&lt;/code&gt;&lt;/pre&gt;
&lt;h4&gt;重要结论&lt;/h4&gt;
&lt;p&gt;(spring内部通过3级缓存来解决循环依赖) - DefaultSingletonBeanRegistry&lt;/p&gt;
&lt;p&gt;只有单例的bean会通过三级缓存提前暴露来解决循环依赖的问题，而非单例的bean，每次从容器中获取都是一个新的对象，都会重新创建，所以非单例的bean是没有缓存的，不会将其放到三级缓存中。&lt;/p&gt;
&lt;p&gt;第一级缓存（也叫单例池）singletonObjects：存放已经经历了完整生命周期的Bean对象。&lt;/p&gt;
&lt;p&gt;第二级缓存：earlySingletonObjects，存放早期暴露出来的Bean对象，Bean的生命周期未结束（属性还未填充完。）&lt;/p&gt;
&lt;p&gt;第三级缓存：Map&amp;lt;String, ObjectFactory&amp;lt;?&amp;gt;&amp;gt; singletonFactories，存放可以生成Bean的工厂。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;package org.springframework.beans.factory.support;

...

public class DefaultSingletonBeanRegistry extends SimpleAliasRegistry implements SingletonBeanRegistry {

	...

	/** Cache of singleton objects: bean name to bean instance. */
	private final Map&amp;lt;String, Object&amp;gt; singletonObjects = new ConcurrentHashMap&amp;lt;&amp;gt;(256);

	/** Cache of singleton factories: bean name to ObjectFactory. */
	private final Map&amp;lt;String, ObjectFactory&amp;lt;?&amp;gt;&amp;gt; singletonFactories = new HashMap&amp;lt;&amp;gt;(16);

	/** Cache of early singleton objects: bean name to bean instance. */
	private final Map&amp;lt;String, Object&amp;gt; earlySingletonObjects = new HashMap&amp;lt;&amp;gt;(16);
 
    ...
    
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;spring循环依赖debug前置知识&lt;/h3&gt;
&lt;h4&gt;三层map&lt;/h4&gt;
&lt;p&gt;实例化 - 内存中申请一块内存空间，如同租赁好房子，自己的家当还未搬来。&lt;/p&gt;
&lt;p&gt;初始化属性填充 - 完成属性的各种赋值，如同装修，家具，家电进场。&lt;/p&gt;
&lt;p&gt;3个Map和四大方法，总体相关对象&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://tangtangit.com/_astro/image-20240319105038901.bs7Jv7zn_2jw6XW.webp&quot; alt=&quot;image-20240319105038901&quot; /&gt;&lt;/p&gt;
&lt;p&gt;第一层singletonObjects存放的是已经初始化好了的Bean。（成品）&lt;/p&gt;
&lt;p&gt;第二层earlySingletonObjects存放的是实例化了，但是未初始化的Bean。（半成品）&lt;/p&gt;
&lt;p&gt;第三层singletonFactories存放的是FactoryBean。当一个bean正在创建过程中，会被放入该缓存中。（准备生产）假如A类实现了FactoryBean,那么依赖注入的时候不是A类，而是A类产生的Bean。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;package org.springframework.beans.factory.support;

...

public class DefaultSingletonBeanRegistry extends SimpleAliasRegistry implements SingletonBeanRegistry {

	...

	/** 
	单例对象的缓存:bean名称—bean实例，即:所谓的单例池。
	表示已经经历了完整生命周期的Bean对象
	第一级缓存
	*/
	private final Map&amp;lt;String, Object&amp;gt; singletonObjects = new ConcurrentHashMap&amp;lt;&amp;gt;(256);

	/**
	早期的单例对象的高速缓存: bean名称—bean实例。
	表示 Bean的生命周期还没走完（Bean的属性还未填充）就把这个 Bean存入该缓存中也就是实例化但未初始化的 bean放入该缓存里
	第二级缓存
	*/
	private final Map&amp;lt;String, Object&amp;gt; earlySingletonObjects = new HashMap&amp;lt;&amp;gt;(16);

	/**
	单例工厂的高速缓存:bean名称—ObjectFactory
	表示存放生成 bean的工厂
	第三级缓存
	*/
	private final Map&amp;lt;String, ObjectFactory&amp;lt;?&amp;gt;&amp;gt; singletonFactories = new HashMap&amp;lt;&amp;gt;(16);
 
    ...
    
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h4&gt;A / B两对象在三级缓存中的迁移说明&lt;/h4&gt;
&lt;ol&gt;
&lt;li&gt;A创建过程中需要B，于是A将自己放到三级缓里面，去实例化B。&lt;/li&gt;
&lt;li&gt;B实例化的时候发现需要A，于是B先查一级缓存，没有，再查二级缓存，还是没有，再查三级缓存，找到了A然后把三级缓存里面的这个A放到二级缓存里面，并删除三级缓存里面的A。&lt;/li&gt;
&lt;li&gt;B顺利初始化完毕，将自己放到一级缓存里面（此时B里面的A依然是创建中状态)，然后回来接着创建A，此时B已经创建结束，直接从一级缓存里面拿到B，然后完成创建，并将A自己放到一级缓存里面。&lt;/li&gt;
&lt;/ol&gt;
&lt;pre&gt;&lt;code&gt;@FunctionalInterface
public interface ObjectFactory&amp;lt;T&amp;gt; {

	T getObject() throws BeansException;

}
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;spring循环依赖小总结&lt;/h3&gt;
&lt;p&gt;Spring创建 bean主要分为两个步骤，创建原始bean对象，接着去填充对象属性和初始化&lt;/p&gt;
&lt;p&gt;每次创建 bean之前，我们都会从缓存中查下有没有该bean，因为是单例，只能有一个&lt;/p&gt;
&lt;p&gt;当我们创建 beanA的原始对象后，并把它放到三级缓存中，接下来就该填充对象属性了，这时候发现依赖了beanB，接着就又去创建beanB，同样的流程，创建完beanB填充属性时又发现它依赖了beanA又是同样的流程，
不同的是：这时候可以在三级缓存中查到刚放进去的原始对象beanA.所以不需要继续创建，用它注入 beanB，完成 beanB的创建&lt;/p&gt;
&lt;p&gt;既然 beanB创建好了，所以 beanA就可以完成填充属性的步骤了，接着执行剩下的逻辑，闭环完成。&lt;/p&gt;
&lt;p&gt;Spring解决循环依赖依靠的是Bean的&quot;中间态&quot;这个概念，而这个中间态指的是已经实例化但还没初始化的状态—&amp;gt;半成债。实例化的过程又是通过构造器创建的，如果A还没创建好出来怎么可能提前曝光，所以构造器的循环依赖无法解决。”对&lt;/p&gt;
&lt;p&gt;Spring为了解决单例的循坏依赖问题，使用了三级缓存：&lt;/p&gt;
&lt;p&gt;其中一级缓存为单例池(singletonObjects)。&lt;/p&gt;
&lt;p&gt;二级缓存为提前曝光对象(earlySingletonObjects)。&lt;/p&gt;
&lt;p&gt;三级级存为提前曝光对象工厂(singletonFactories) 。&lt;/p&gt;
&lt;p&gt;假设A、B循环引用，实例化A的时候就将其放入三级缓存中，接着填充属性的时候，发现依赖了B，同样的流程也是实例化后放入三级缓存，接着去填充属性时又发现自己依赖A，这时候从缓存中查找到早期暴露的A，没有AOP代理的话，直接将A的原始对象注入B，完成B的初始化后，进行属性填充和初始化，这时候B完成后，就去完成剩下的A的步骤，如果有AOP代理，就进行AOP处理获取代理后的对象A，注入B，走剩下的流程。&lt;/p&gt;
&lt;h4&gt;Spring解决循环依赖过程：&lt;/h4&gt;
&lt;ol&gt;
&lt;li&gt;调用doGetBean()方法，想要获取beanA，于是调用getSingleton()方法从缓存中查找beanA&lt;/li&gt;
&lt;li&gt;在getSingleton()方法中，从一级缓存中查找，没有，返回null&lt;/li&gt;
&lt;li&gt;doGetBean()方法中获取到的beanA为null，于是走对应的处理逻辑，调用getSingleton()的重载方法（参数为ObjectFactory的)&lt;/li&gt;
&lt;li&gt;在getSingleton()方法中，先将beanA_name添加到一个集合中，用于标记该bean正在创建中。然后回调匿名内部类的creatBean方法&lt;/li&gt;
&lt;li&gt;进入AbstractAutowireCapableBeanFactory#ndoCreateBean，先反射调用构造器创建出beanA的实例，然后判断:是否为单例、是否允许提前暴露引用(对于单例一般为true)、是否正在创建中（即是否在第四步的集合中）。判断为true则将beanA添加到【三级缓存】中&lt;/li&gt;
&lt;li&gt;对beanA进行属性填充，此时检测到beanA依赖于beanB，于是开始查找beanB&lt;/li&gt;
&lt;li&gt;调用doGetBean()方法，和上面beanA的过程一样，到缓存中查找beanB，没有则创建，然后给beanB填充属性&lt;/li&gt;
&lt;li&gt;此时 beanB依赖于beanA，调用getSingleton()获取beanA，依次从一级、二级、三级缓存中找，此时从三级缓存中获取到beanA的创建工厂，通过创建工厂获取到singletonObject，此时这个singletonObject指向的就是上面在doCreateBean()方法中实例化的beanA&lt;/li&gt;
&lt;li&gt;这样beanB就获取到了beanA的依赖，于是beanB顺利完成实例化，并将beanA从三级缓存移动到二级缓存中&lt;/li&gt;
&lt;li&gt;随后beanA继续他的属性填充工作，此时也获取到了beanB，beanA也随之完成了创建，回到getsingleton()方法中继续向下执行，将beanA从二级缓存移动到一级缓存中&lt;/li&gt;
&lt;/ol&gt;
&lt;h3&gt;衍生问题&lt;/h3&gt;
&lt;h4&gt;问题 1:为什么构造器注入属性无法解决循环依赖问题?&lt;/h4&gt;
&lt;p&gt;由于 spring 中的 bean 的创建过程为先实例化，再初始化(在进行对象实例化的过程中不必赋值)将实例化好的对象暴露出去，供其他对象调用，然而使用构造器注入，&lt;strong&gt;必须要使用构造器完成对象的初始化的操作&lt;/strong&gt;，就会陷入死循环的状态&lt;/p&gt;
&lt;h4&gt;问题 2:一级缓存能不能解决循环依赖问题? 不能&lt;/h4&gt;
&lt;p&gt;在三个级别的缓存中存储的对象是有区别的，一级缓存为完全实例化且初始化的对象，二级缓存实例化但未初始化对象，如果只有一级缓存，如果是&lt;strong&gt;并发&lt;/strong&gt;操作下，就有可能&lt;strong&gt;取到实例化但未初始化的对象&lt;/strong&gt;存入一级缓存，就会出现问题。&lt;/p&gt;
&lt;h4&gt;问题 3:二级缓存能不能解决循环依赖问题?&lt;/h4&gt;
&lt;p&gt;理论上二级缓存可以解决循环依赖问题，但是需要注意，为什么需要在三级缓存中存储匿名内部类(ObjectFactory)，原因在于&lt;strong&gt;需要创建代理对象&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;eg:现有 A 类，&lt;strong&gt;需要生成代理对象A&lt;/strong&gt;， 是否需要进行实例化(需要) 。&lt;/p&gt;
&lt;p&gt;在三级缓存中存放的是生成具体对象的一个匿名内部类，该类可能是代理类也可能是普通的对象，而使用三级缓存可以保证无论是否需要&lt;/p&gt;
&lt;p&gt;是代理对象，都可以保证使用的是同一个对象，而不会出现一会儿使用普通bean一会儿使用代理类。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;另一种回答：&lt;/strong&gt;&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;尽管二级缓存已经解决了大部分循环依赖的问题，但在某些特殊情况下，仍然需要使用三级缓存。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;当存在&lt;strong&gt;多个构造函数&lt;/strong&gt;时，二级缓存无法正确解决循环依赖的问题。这是因为在创建bean时，Spring无法确定应该使用哪个构造函数进行实例化。而三级缓存可以解决这个问题，通过在三级缓存中存储正在创建的bean对象，Spring可以在实例化时选择正确的构造函数。&lt;/li&gt;
&lt;li&gt;当存在&lt;strong&gt;循环依赖的链路过长&lt;/strong&gt;时，二级缓存可能无法正确解决循环依赖。在链路过长的情况下，二级缓存可能无法正确地维护依赖关系，导致循环依赖无法解决。而三级缓存通过在创建过程中存储&lt;strong&gt;bean对象的引用&lt;/strong&gt;，可以更好地处理链路过长的情况。&lt;/li&gt;
&lt;/ul&gt;
&lt;/blockquote&gt;
&lt;h4&gt;问题4：为什么多例不能解决循环依赖问题？&lt;/h4&gt;
&lt;p&gt;只有单例的bean会通过三级缓存提前暴露来解决循环依赖的问题，而非单例的bean，每次从容器中获取都是一个&lt;strong&gt;新的对象&lt;/strong&gt;，都会重新创建，所以&lt;strong&gt;非单例的bean是没有缓存的&lt;/strong&gt;，不会将其放到三级缓存中。&lt;/p&gt;
&lt;hr /&gt;
&lt;h2&gt;自定义注解&lt;/h2&gt;
&lt;h2&gt;一：什么是注解？&lt;/h2&gt;
&lt;p&gt;对于很多初次接触的开发者来说应该都有这个疑问？&lt;strong&gt;*Annontation&lt;/strong&gt;&lt;em&gt;是Java5开始引入的新特征，中文名称叫&lt;/em&gt;&lt;em&gt;注解&lt;/em&gt;*。它提供了一种安全的类似注释的机制，用来将任何的信息或元数据（metadata）与程序元素（类、方法、成员变量等）进行关联。为程序的元素（类、方法、成员变量）加上更直观更明了的说明，这些说明信息是与程序的业务逻辑无关，并且供指定的工具或框架使用。Annontation像一种修饰符一样，应用于包、类型、构造方法、方法、成员变量、参数及本地变量的声明语句中。
　　Java注解是附加在代码中的一些元信息，用于一些工具在编译、运行时进行解析和使用，起到说明、配置的功能。注解不会也不能影响代码的实际逻辑，仅仅起到辅助性的作用。包含在 java.lang.annotation 包中。&lt;/p&gt;
&lt;h2&gt;二：注解的用处：&lt;/h2&gt;
&lt;p&gt;1、生成文档。这是最常见的，也是java 最早提供的注解。常用的有@param @return 等
​      2、跟踪代码依赖性，实现替代配置文件功能。比如Dagger 2 依赖注入，未来java 开发，将大量注解配置，具有很大用处;
​      3、在编译时进行格式检查。如@override 放在方法前，如果你这个方法并不是覆盖了超类方法，则编译时就能检查出。&lt;/p&gt;
&lt;h2&gt;三：注解的原理：&lt;/h2&gt;
&lt;p&gt;注解本质是一个继承了Annotation 的特殊接口，其具体实现类是Java 运行时生成的动态代理类。而我们通过反射获取注解时，返回的是Java 运行时生成的动态代理对象$Proxy1。通过代理对象调用自定义注解（接口）的方法，会最终调用AnnotationInvocationHandler 的invoke 方法。该方法会从memberValues 这个Map 中索引出对应的值。而memberValues 的来源是Java 常量池。&lt;/p&gt;
&lt;h2&gt;四：元注解：&lt;/h2&gt;
&lt;p&gt;java.lang.annotation 提供了四种元注解，专门注解其他的注解（在自定义注解的时候，需要使用到元注解）：
@Documented – 注解是否将包含在JavaDoc中
@Retention – 什么时候使用该注解
@Target – 注解用于什么地方
@Inherited – 是否允许子类继承该注解&lt;/p&gt;
&lt;p&gt;1.）@Retention – 定义该注解的生命周期
●   RetentionPolicy.SOURCE : 在编译阶段丢弃。这些注解在编译结束之后就不再有任何意义，所以它们不会写入字节码。@Override, @SuppressWarnings都属于这类注解。
●   RetentionPolicy.CLASS : 在类加载的时候丢弃。在字节码文件的处理中有用。注解默认使用这种方式
●   RetentionPolicy.RUNTIME : 始终不会丢弃，运行期也保留该注解，因此可以使用反射机制读取该注解的信息。我们自定义的注解通常使用这种方式。&lt;/p&gt;
&lt;p&gt;2.）Target – 表示该注解用于什么地方。默认值为任何元素，表示该注解用于什么地方。可用的ElementType 参数包括
● ElementType.CONSTRUCTOR: 用于描述构造器
● ElementType.FIELD: 成员变量、对象、属性（包括enum实例）
● ElementType.LOCAL_VARIABLE: 用于描述局部变量
● ElementType.METHOD: 用于描述方法
● ElementType.PACKAGE: 用于描述包
● ElementType.PARAMETER: 用于描述参数
● ElementType.TYPE: 用于描述类、接口(包括注解类型) 或enum声明&lt;/p&gt;
&lt;p&gt;3.)@Documented – 一个简单的Annotations 标记注解，表示是否将注解信息添加在java 文档中。&lt;/p&gt;
&lt;p&gt;4.)@Inherited – 定义该注释和子类的关系
​     @Inherited 元注解是一个标记注解，@Inherited 阐述了某个被标注的类型是被继承的。如果一个使用了@Inherited 修饰的annotation 类型被用于一个class，则这个annotation 将被用于该class 的子类。&lt;/p&gt;
&lt;h2&gt;五：常见标准的Annotation：&lt;/h2&gt;
&lt;p&gt;1.）Override
​      java.lang.Override 是一个标记类型注解，它被用作标注方法。它说明了被标注的方法重载了父类的方法，起到了断言的作用。如果我们使用了这种注解在一个没有覆盖父类方法的方法时，java 编译器将以一个编译错误来警示。
2.）Deprecated
​     Deprecated 也是一种标记类型注解。当一个类型或者类型成员使用@Deprecated 修饰的话，编译器将不鼓励使用这个被标注的程序元素。所以使用这种修饰具有一定的“延续性”：如果我们在代码中通过继承或者覆盖的方式使用了这个过时的类型或者成员，虽然继承或者覆盖后的类型或者成员并不是被声明为@Deprecated，但编译器仍然要报警。
3.）SuppressWarnings
​     SuppressWarning 不是一个标记类型注解。它有一个类型为String[] 的成员，这个成员的值为被禁止的警告名。对于javac 编译器来讲，被-Xlint 选项有效的警告名也同样对@SuppressWarings 有效，同时编译器忽略掉无法识别的警告名。
　　@SuppressWarnings(&quot;unchecked&quot;)&lt;/p&gt;
&lt;h2&gt;六：自定义注解&lt;/h2&gt;
&lt;p&gt;自定义注解类编写的一些规则:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;1. Annotation 型定义为@interface, 所有的Annotation 会自动继承java.lang.Annotation这一接口,并且不能再去继承别的类或是接口.
2. 参数成员只能用public 或默认(default) 这两个访问权修饰
3. 参数成员只能用基本类型byte、short、char、int、long、float、double、boolean八种基本数据类型和String、Enum、Class、annotations等数据类型，以及这一些类型的数组.
4. 要获取类方法和字段的注解信息，必须通过Java的反射技术来获取 Annotation 对象，因为你除此之外没有别的获取注解对象的方法
5. 注解也可以没有定义成员,，不过这样注解就没啥用了
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;PS:自定义注解需要使用到元注解&lt;/p&gt;
&lt;h2&gt;七：我的第一个注解实例&lt;/h2&gt;
&lt;h3&gt;1、写一个自定义注解：&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;/**
 * 我的第一个注解
 */
//注解的使用范围
@Target({ElementType.METHOD,ElementType.TYPE})
//定义注解的生命周期
@Retention(RetentionPolicy.RUNTIME)
public @interface MyFirst {

    int code();

    String message();

    boolean isNo() default true;
    
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;枚举和类注解示范：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;import java.lang.annotation.Documented;
import java.lang.annotation.Retention;
import java.lang.annotation.Target;
import static java.lang.annotation.ElementType.FIELD;
import static java.lang.annotation.RetentionPolicy.RUNTIME;

/**
 * 水果颜色注解
 */
@Target(FIELD)
@Retention(RUNTIME)
@Documented
public @interface FruitColor {
    /**
     * 颜色枚举
     */
    public enum Color{ BLUE,RED,GREEN};
    
    /**
     * 颜色属性
     */
    Color fruitColor() default Color.GREEN;

}
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;2、测试自定义注解的使用(简陋)&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;package com.leyou.dos;

import com.leyou.annotation.MyFirst;
import lombok.Data;
import org.springframework.stereotype.Component;

import java.lang.reflect.Method;

/**
 * &amp;lt;p&amp;gt;DESC: 测试注解开发&amp;lt;/p&amp;gt;
 * &amp;lt;p&amp;gt;DATE: 2019/5/23&amp;lt;/p&amp;gt;
 * &amp;lt;p&amp;gt;VERSION:1.0.0&amp;lt;/p&amp;gt;
 * &amp;lt;p&amp;gt;@AUTHOR: ZhengYong&amp;lt;/p&amp;gt;
 */
@Component
@Data
@MyFirst(code = 90,message = &quot;haah&quot;)
public class TestAnnotation {

    private String name;

    private Integer age;

    @MyFirst(code = 0,message = &quot;你好呀&quot;)
    public static String getStr(){
        return &quot;abd&quot;;
    }

    public static void main(String[] args) {
        //获取加了注解的类对象
        Class&amp;lt;TestAnnotation&amp;gt; aClass = TestAnnotation.class;
        //获取自定义注解的类对象
        Class&amp;lt;MyFirst&amp;gt; myFirstClass = MyFirst.class;

        //获取方法列表
        Method[] methods = aClass.getMethods();
        
        for (Method f :methods) {
            System.out.println(f.getName());
            //判断是否存在自定义注解
            if(f.isAnnotationPresent(myFirstClass)){
                System.out.println(&quot;我逮到你了&quot;);
                //如果存在，获取方法上的 自定义注解对象
                MyFirst myFirst = (MyFirst)f.getAnnotation(myFirstClass);
                System.out.println(&quot;&quot;+myFirst.code()+myFirst.isNo()+myFirst.message());
            }
        }

    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;3、从spring上下文中测试自定义注解&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;package com.leyou.annotation;

import org.springframework.beans.BeansException;
import org.springframework.context.ApplicationContext;
import org.springframework.context.ApplicationContextAware;
import org.springframework.context.ApplicationListener;
import org.springframework.context.event.ContextRefreshedEvent;
import org.springframework.core.annotation.AnnotationUtils;
import org.springframework.stereotype.Component;

import java.lang.reflect.Method;
import java.util.Map;
import java.util.Set;

/**
 * &amp;lt;p&amp;gt;DESC: &amp;lt;/p&amp;gt;
 * &amp;lt;p&amp;gt;DATE: 2019/5/24&amp;lt;/p&amp;gt;
 * &amp;lt;p&amp;gt;VERSION:1.0.0&amp;lt;/p&amp;gt;
 * &amp;lt;p&amp;gt;@AUTHOR: ZhengYong&amp;lt;/p&amp;gt;
 */
@Component
public class AnnotationContext implements ApplicationContextAware, ApplicationListener&amp;lt;ContextRefreshedEvent&amp;gt;{


    private ApplicationContext applicationContext;

    /**
     * 获取spring 上下文对象
     * ApplicationContextAware 接口实现的方法
     * @param applicationContext
     * @throws BeansException
     */
    @Override
    public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
        this.applicationContext = applicationContext;
    }

    /**
     * 监听刷新事件
     * ApplicationListener 接口实现的方法
     * @param contextRefreshedEvent
     */
    @Override
    public void onApplicationEvent(ContextRefreshedEvent contextRefreshedEvent) {
        //自定义注解的类对象
        Class&amp;lt;MyFirst&amp;gt; myFirstClass = MyFirst.class;

        //从容器中获取 加了自定义注解的bean对象
        Map&amp;lt;String, Object&amp;gt; annotation = applicationContext.getBeansWithAnnotation(myFirstClass);
        //遍历集合
        Set&amp;lt;Map.Entry&amp;lt;String, Object&amp;gt;&amp;gt; entrySet = annotation.entrySet();
        for(Map.Entry&amp;lt;String, Object&amp;gt; m :entrySet){
            //获取bean的名字
            m.getKey();
            //获取bean的类对象
            Class&amp;lt;? extends Object&amp;gt; aClass = m.getValue().getClass();
            System.out.println(&quot;bean名字：&quot;+m.getKey());
            System.out.println(&quot;类名：&quot;+aClass.getName());

            //获取注解实例
            MyFirst first = AnnotationUtils.findAnnotation(aClass, myFirstClass);

            //循环当前类的方法
            Method[] methods = aClass.getMethods();
            for(Method method :methods){
                boolean present = method.isAnnotationPresent(myFirstClass);
                if(present){
                MyFirst annotation1 = method.getAnnotation(myFirstClass);
                    System.out.println(&quot;方式一&quot;+annotation1.code()+annotation1.message());
                }
                //方式二
                MyFirst annotation2 = AnnotationUtils.findAnnotation(method, myFirstClass);
                if(annotation2 != null){
                    System.out.println(&quot;方式二&quot;+annotation2.code()+annotation2.message());
                }
            }
        }
        
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;4、使用aop自定义注解&lt;/h3&gt;
&lt;p&gt;1、记录日志&lt;/p&gt;
&lt;p&gt;2、拦截器记录参数&lt;/p&gt;
</content:encoded></item><item><title>JVM与JUC并发编程完全指南</title><link>https://tangtangit.com/posts/java%E6%A0%B8%E5%BF%83-06-jvm%E4%B8%8Ejuc/</link><guid isPermaLink="true">https://tangtangit.com/posts/java%E6%A0%B8%E5%BF%83-06-jvm%E4%B8%8Ejuc/</guid><description>系统讲解JUC并发工具包与JVM核心体系：Lock接口、线程通信、Condition、线程池、BlockingQueue、ReentrantReadWriteLock、类加载、堆结构、GC垃圾回收、参数调优。</description><pubDate>Wed, 21 Jan 2026 00:00:00 GMT</pubDate><content:encoded>&lt;h1&gt;1、JUC是什么&lt;/h1&gt;
&lt;h2&gt;java.util.concurrent在并发编程中使用的工具类&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;concurrent             并发类&lt;/li&gt;
&lt;li&gt;consurrent.atomic 并发原子类&lt;/li&gt;
&lt;li&gt;concurrent.locks    并发锁类&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;img src=&quot;https://tangtangit.com/_astro/image-20210616103514010.DDyE9AeR_2gI4Ix.webp&quot; alt=&quot;image-20210616103514010&quot; /&gt;&lt;/p&gt;
&lt;h2&gt;进程/线程回顾&lt;/h2&gt;
&lt;h3&gt;1进程/线程是什么？&lt;/h3&gt;
&lt;blockquote&gt;
&lt;p&gt;进程：进程（Process）是计算机中运行中的程序的实例。它是作为分配资源的基本单位。进程是线程的容器。&lt;/p&gt;
&lt;p&gt;线程：线程是进程中的执行单元，是程序执行流的最小单元。通常在一个进程中可以包含若干个线程，当然一个进程中至少有一个线程，不然没有存在的意义。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h3&gt;2进程/线程例子？&lt;/h3&gt;
&lt;blockquote&gt;
&lt;p&gt;使用QQ，查看进程一定有一个QQ.exe的进程，我可以用qq和A文字聊天，和B视频聊天，给C传文件，给D发一段语言，QQ支持录入信息的搜索。&lt;/p&gt;
&lt;p&gt;大四的时候写论文，用word写论文，同时用QQ音乐放音乐，同时用QQ聊天，多个进程。&lt;/p&gt;
&lt;p&gt;word如没有保存，停电关机，再通电后打开word可以恢复之前未保存的文档，word也会检查你的拼写，两个线程：容灾备份，语法检查&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h3&gt;3线程状态？&lt;/h3&gt;
&lt;blockquote&gt;
&lt;ul&gt;
&lt;li&gt;一般定义：新建-就绪-运行-阻塞-销毁&lt;/li&gt;
&lt;li&gt;Java定义：新建-就绪-运行-等待-超时等待-销毁&lt;/li&gt;
&lt;/ul&gt;
&lt;/blockquote&gt;
&lt;pre&gt;&lt;code&gt;Thread.State
 
 
 
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:
     * &amp;lt;ul&amp;gt;
     *   &amp;lt;li&amp;gt;{@link Object#wait() Object.wait} with no timeout&amp;lt;/li&amp;gt;
     *   &amp;lt;li&amp;gt;{@link #join() Thread.join} with no timeout&amp;lt;/li&amp;gt;
     *   &amp;lt;li&amp;gt;{@link LockSupport#park() LockSupport.park}&amp;lt;/li&amp;gt;
     * &amp;lt;/ul&amp;gt;
     *
     * &amp;lt;p&amp;gt;A thread in the waiting state is waiting for another thread to
     * perform a particular action.
     *
     * For example, a thread that has called &amp;lt;tt&amp;gt;Object.wait()&amp;lt;/tt&amp;gt;
     * on an object is waiting for another thread to call
     * &amp;lt;tt&amp;gt;Object.notify()&amp;lt;/tt&amp;gt; or &amp;lt;tt&amp;gt;Object.notifyAll()&amp;lt;/tt&amp;gt; on
     * that object. A thread that has called &amp;lt;tt&amp;gt;Thread.join()&amp;lt;/tt&amp;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:
     * &amp;lt;ul&amp;gt;
     *   &amp;lt;li&amp;gt;{@link #sleep Thread.sleep}&amp;lt;/li&amp;gt;
     *   &amp;lt;li&amp;gt;{@link Object#wait(long) Object.wait} with timeout&amp;lt;/li&amp;gt;
     *   &amp;lt;li&amp;gt;{@link #join(long) Thread.join} with timeout&amp;lt;/li&amp;gt;
     *   &amp;lt;li&amp;gt;{@link LockSupport#parkNanos LockSupport.parkNanos}&amp;lt;/li&amp;gt;
     *   &amp;lt;li&amp;gt;{@link LockSupport#parkUntil LockSupport.parkUntil}&amp;lt;/li&amp;gt;
     * &amp;lt;/ul&amp;gt;
     */
    TIMED_WAITING,（过时不候）

    /**
     * Thread state for a terminated thread.
     * The thread has completed execution.
     */
    TERMINATED;(终结)
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;4 wait/sleep的区别？&lt;/h3&gt;
&lt;blockquote&gt;
&lt;p&gt;wait/sleep
功能都是当前线程暂停，有什么区别？
wait放开手去睡，放开手里的锁
sleep握紧手去睡，醒了手里还有锁&lt;/p&gt;
&lt;p&gt;记忆：&lt;/p&gt;
&lt;p&gt;wait是重新排队，是要释放资源的锁。&lt;/p&gt;
&lt;p&gt;sleep是睡觉，抱着锁睡。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h3&gt;5什么是并发？什么是并行？&lt;/h3&gt;
&lt;blockquote&gt;
&lt;p&gt;并发：同一时刻多个线程在访问同一个资源，多个线程对一个点
例子：小米9今天上午10点，限量抢购
春运抢票
电商秒杀...
并行：多项工作一起执行，之后再汇总
例子：泡方便面，电水壶烧水，一边撕调料倒入桶中&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h1&gt;2、Lock接口&lt;/h1&gt;
&lt;h2&gt;复习Synchronized&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;对于普通同步方法，锁是当前实例对象。&lt;/li&gt;
&lt;li&gt;对于静态同步方法，锁是当前类的Class对象。&lt;/li&gt;
&lt;li&gt;对于同步方法块，锁是Synchonized括号里配置的对象&lt;/li&gt;
&lt;/ul&gt;
&lt;blockquote&gt;
&lt;p&gt;下面我将使用代码示例来说明这三种情况：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;普通同步方法，锁是当前实例对象：&lt;/strong&gt;&lt;/li&gt;
&lt;/ol&gt;
&lt;pre&gt;&lt;code&gt;public class Example {
    public synchronized void synchronizedMethod() {
        // 这里的锁是当前实例对象
        // 执行操作
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;静态同步方法，锁是当前类的 &lt;code&gt;Class&lt;/code&gt; 对象：&lt;/strong&gt;&lt;/li&gt;
&lt;/ol&gt;
&lt;pre&gt;&lt;code&gt;public class Example {
    public static synchronized void synchronizedStaticMethod() {
        // 这里的锁是当前类的 Class 对象
        // 执行操作
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;同步方法块，锁是 &lt;code&gt;Synchronized&lt;/code&gt; 括号里配置的对象：&lt;/strong&gt;&lt;/li&gt;
&lt;/ol&gt;
&lt;pre&gt;&lt;code&gt;public class Example {
    private final Object lockObject = new Object();

    public void synchronizedBlock() {
        synchronized (lockObject) {
            // 这里的锁是 Synchronized 括号里配置的对象，即 lockObject
            // 执行操作
        }
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这三个例子分别展示了普通同步方法、静态同步方法和同步方法块的情况。在普通同步方法中，锁是当前实例对象；在静态同步方法中，锁是当前类的 &lt;code&gt;Class&lt;/code&gt; 对象；在同步方法块中，锁是 &lt;code&gt;Synchronized&lt;/code&gt; 括号里配置的对象。&lt;/p&gt;
&lt;p&gt;这样的锁机制确保了在多线程环境中对共享资源的访问是同步的，避免了竞争条件和数据不一致的问题。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h3&gt;多线程编程模板上&lt;/h3&gt;
&lt;p&gt;1、线程 操作 资源类&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;相信Thread对我们来说并不陌生，学java的时候都是入门的东西，我们一般使用thread都是new一个线程，然后调用start方法启动，使用start方法才真正实现了多线程运行，因为这个时候不用等待我们的run方法执行完成就可以继续执行下面的代码，这才叫多线程嘛！因为thread线程有5种状态，&lt;/p&gt;
&lt;p&gt;创建-就绪-运行-阻塞-死亡这五种，&lt;/p&gt;
&lt;p&gt;那么我们的&lt;strong&gt;start方法呢就是就绪这一步&lt;/strong&gt;，因为这个时候我们的线程并没有立即的执行，而是得等待，等到我们的cpu有空闲的时候，才会执行线程里面的run方法，等run方法执行完了，线程就结束了。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;2、高内聚低耦合&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;耦合： 模块与模块之间的联系。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;内聚：一般指（东西聚集在一起）形成一个模块，例如方法，变量，对象，或者是功能模块。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;高内聚：尽可能的让一个模块内部的代码相关程度高，相互联系的紧密。模块内部的代码，相互之间的联系越强，内聚就越高， 模块的独立性就越好。 一个模块应该尽量去独立的完成一个功能！如果必须写另外的功能，建议拆分成多个模块，低内聚的代码，不好维护，代码也不够健壮。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;低耦合：尽可能的将每一个功能通过模块单独写出去 ，然后通过指定的接口来相互联系，模块与模块之间的关系越是紧密，独立性就越不好，改变一个模块可能会影响其他的模块。&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;实现步骤&lt;/h3&gt;
&lt;p&gt;1、创建资源类&lt;/p&gt;
&lt;p&gt;2、资源类里创建同步方法、同步代码块&lt;/p&gt;
&lt;h3&gt;例子卖票程序&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;//资源类
@Slf4j
class Ticket{
    private int number = 30;

    public synchronized void saleTicket(){
        if(number &amp;gt; 0){
            log.info(Thread.currentThread().getName()+&quot;卖出第：{}&quot;,number--,&quot;还剩下：{}&quot;,number);
        }
    }
}

/**
 * 题目： 三个售票员  卖出  30张票
 *
 *多线程编程的企业套路+模板
 * 1、在高内聚低耦合的前提下：  线程  操作(对外暴露的调用方法)  资源类
 */
public class SaleTicket {

    public static void main(String[] args) {
        Ticket ticket = new Ticket();

        new Thread(new Runnable() {
           @Override
           public void run() {
               for (int i = 0; i &amp;lt; 40; i++) {
                   ticket.saleTicket();
               }
           }
       },&quot;A&quot;).start();

        new Thread(new Runnable() {
           @Override
           public void run() {
               for (int i = 0; i &amp;lt; 40; i++) {
                   ticket.saleTicket();
               }
           }
       },&quot;B&quot;).start();

        new Thread(new Runnable() {
           @Override
           public void run() {
               for (int i = 0; i &amp;lt; 40; i++) {
                   ticket.saleTicket();
               }
           }
       },&quot;C&quot;).start();

    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Lock&lt;/h2&gt;
&lt;h3&gt;是什么&lt;/h3&gt;
&lt;p&gt;&lt;img src=&quot;https://tangtangit.com/_astro/image-20210616124754276.B8ndxiS1_ZyWnO9.webp&quot; alt=&quot;image-20210616124754276&quot; /&gt;&lt;/p&gt;
&lt;h3&gt;Lock接口的实现&lt;/h3&gt;
&lt;p&gt;ReentrantLock可重入锁&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://tangtangit.com/_astro/image-20210616124848114.1olT2Cq7_mJkBR.webp&quot; alt=&quot;image-20210616124848114&quot; /&gt;&lt;/p&gt;
&lt;h3&gt;如何使用&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;class X {
   private final ReentrantLock lock = new ReentrantLock();
   // ...
 
   public void m() {
     lock.lock();  // block until condition holds
     try {
       // ... method body
     } finally {
       lock.unlock()
     }
   }
 }
 
 
synchronized与Lock的区别 
 
两者区别：
1.首先synchronized是java内置关键字，在jvm层面，Lock是个java类；
2.synchronized无法判断是否获取锁的状态，Lock可以判断是否获取到锁；
3.synchronized会自动释放锁(a 线程执行完同步代码会释放锁 ；b 线程执行过程中发生异常会释放锁)，
    Lock需在finally中手工释放锁（unlock()方法释放锁），s否则容易造成线程死锁；
4.用synchronized关键字的两个线程1和线程2，如果当前线程1获得锁，线程2线程等待。
    如果线程1阻塞，线程2则会一直等待下去，而Lock锁就不一定会等待下去，如果尝试获取不到锁，线程可以不用一直等待就结束了；
5.synchronized的锁可重入、不可中断、非公平，而Lock锁可重入、可中断、可公平（两者皆可）
6.Lock锁适合大量同步的代码的同步问题，synchronized锁适合代码少量的同步问题。
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这句话描述了 Java 中的两种不同的锁机制：synchronized 和 Lock（java.util.concurrent 包中的锁），并总结了它们的特性。让我们逐一解释这些特性的含义：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;synchronized 的锁可重入、不可中断、非公平：&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;可重入性（Reentrancy）：&lt;/strong&gt; 当一个线程获得了某个对象的锁后，它可以再次请求该对象的锁，而无需释放之前的锁。这是允许的，因为 Java 中的 synchronized 锁是可重入的，也就是说，&lt;strong&gt;同一个线程可以多次获取同一个锁，而不会出现死锁。&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;不可中断性：&lt;/strong&gt; synchronized 锁是不可中断的，意味着在等待获取锁的过程中，其他线程无法中断正在等待的线程。只有当等待获取锁的线程成功获得锁之后，或者它自己放弃等待，其他线程才有机会获得锁。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;非公平性：&lt;/strong&gt; 默认情况下，synchronized 锁是非公平的，这意味着当多个线程竞争锁时，**不会按照线程等待的时间顺序来分配锁。**这可能导致某些线程一直无法获得锁，而其他线程则一直占用锁。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Lock 的锁可重入、可判断、可公平：&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;可重入性（Reentrancy）：&lt;/strong&gt; 类似于 synchronized，Lock 也支持可重入性，允许同一个线程多次获取同一个锁。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;可中断：&lt;/strong&gt;&lt;code&gt;Lock&lt;/code&gt;接口的实现类&lt;code&gt;ReentrantLock&lt;/code&gt;提供了一种可中断的锁特性，它允许线程在等待锁的过程中被中断，而不是一直等待下去。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;可公平性：&lt;/strong&gt; Lock 可以被配置为公平锁或非公平锁。在公平锁模式下，线程将按照请求锁的顺序获取锁。在非公平锁模式下，与 synchronized 类似，锁将根据某些策略分配给等待的线程，不一定按照请求的顺序。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;总之，这句话总结了 synchronized 和 Lock 锁的几个关键特性，包括可重入性、中断行为、公平性和不同线程竞争锁的策略。理解这些特性有助于你在编写多线程代码时选择适当的锁机制。&lt;/p&gt;
&lt;h3&gt;lock示例：&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;//资源类
@Slf4j
class Ticket{
    private int number = 30;

    private final Lock lock = new ReentrantLock();

    public void saleTicket(){
        lock.lock();
        try {
            if(number &amp;gt; 0){
                log.info(Thread.currentThread().getName()+&quot;卖出第：{}&quot;,number--,&quot;还剩下：{}&quot;,number);
            }
        }finally {
            lock.unlock();
        }

    }
}

/**
 * 题目： 三个售票员  卖出  30张票
 *
 *多线程编程的企业套路+模板
 * 1、在高内聚低耦合的前提下：  线程  操作(对外暴露的调用方法)  资源类
 */
public class SaleTicket {

    public static void main(String[] args) {
        Ticket ticket = new Ticket();

        new Thread(new Runnable() {
           @Override
           public void run() {
               for (int i = 0; i &amp;lt; 40; i++) {
                   ticket.saleTicket();
               }
           }
       },&quot;A&quot;).start();

        new Thread(new Runnable() {
           @Override
           public void run() {
               for (int i = 0; i &amp;lt; 40; i++) {
                   ticket.saleTicket();
               }
           }
       },&quot;B&quot;).start();

        new Thread(new Runnable() {
           @Override
           public void run() {
               for (int i = 0; i &amp;lt; 40; i++) {
                   ticket.saleTicket();
               }
           }
       },&quot;C&quot;).start();

    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;创建线程方式&lt;/h2&gt;
&lt;blockquote&gt;
&lt;ul&gt;
&lt;li&gt;继承Thread类&lt;/li&gt;
&lt;li&gt;实现Runnable接口&lt;/li&gt;
&lt;/ul&gt;
&lt;/blockquote&gt;
&lt;blockquote&gt;
&lt;ul&gt;
&lt;li&gt;使用Callable和Future&lt;/li&gt;
&lt;li&gt;使用线程池&lt;/li&gt;
&lt;/ul&gt;
&lt;/blockquote&gt;
&lt;h3&gt;继承Thread (不能这样写)&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;public class SaleTicket extends Thread
 
java是单继承，资源宝贵，要用接口方式
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;new Thread() (不能这样写)&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;Thread t1 = new Thread();
   t1.start();
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;第三种&lt;/h3&gt;
&lt;p&gt;Thread(Runnable target, String name)&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://tangtangit.com/_astro/image-20210616130626423.34v-Qw0g_1Aw2I.webp&quot; alt=&quot;image-20210616130626423&quot; /&gt;&lt;/p&gt;
&lt;h2&gt;实现runnable方法&lt;/h2&gt;
&lt;h3&gt;新建类实现runnable接口&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;class MyThread implements Runnable//新建类实现runnable接口
 
new Thread(new MyThread,...)
 
这种方法会新增类，有更好的方法
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;匿名内部类&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;new Thread(new Runnable() {
    @Override
    public void run() {
 
    }
   }, &quot;your thread name&quot;).start();
 
 这种方法不需要创建新的类，可以new接口
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;lambda表达式&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;new Thread(() -&amp;gt; {
 
 }, &quot;your thread name&quot;).start();
 
  这种方法代码更简洁精炼
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;//资源类
@Slf4j
class Ticket{
    private int number = 30;

    private final Lock lock = new ReentrantLock();

    public void saleTicket(){
        lock.lock();
        try {
            if(number &amp;gt; 0){
                log.info(Thread.currentThread().getName()+&quot;卖出第：{}&quot;,number--,&quot;还剩下：{}&quot;,number);
            }
        }finally {
            lock.unlock();
        }

    }
}

/**
 * 题目： 三个售票员  卖出  30张票
 *
 *多线程编程的企业套路+模板
 * 1、在高内聚低耦合的前提下：  线程  操作(对外暴露的调用方法)  资源类
 */
public class SaleTicket {

    public static void main(String[] args) {
        Ticket ticket = new Ticket();

        new Thread(() -&amp;gt;{
            for (int i = 0; i &amp;lt;40; i++) {
                ticket.saleTicket();
            }
        },&quot;A&quot;).start();
        new Thread(() -&amp;gt;{
            for (int i = 0; i &amp;lt;40; i++) {
                ticket.saleTicket();
            }
        },&quot;B&quot;).start();

    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h1&gt;3、Java8之lambda表达式复习&lt;/h1&gt;
&lt;h2&gt;lambda表达式&lt;/h2&gt;
&lt;h3&gt;什么是Lambda&lt;/h3&gt;
&lt;blockquote&gt;
&lt;p&gt;Lambda 是一个匿名函数，我们可以把 Lambda
表达式理解为是一段可以传递的代码（将代码
像数据一样进行传递）。可以写出更简洁、更
灵活的代码。作为一种更紧凑的代码风格，使
Java的语言表达能力得到了提升。&lt;/p&gt;
&lt;p&gt;Lambda 表达式在Java 语言中引入了一个新的语法元
素和操作符。这个操作符为 “-&amp;gt;” ， 该操作符被称
为 Lambda 操作符或剪头操作符。它将 Lambda 分为
两个部分：
左侧：指定了 Lambda 表达式需要的所有参数
右侧：指定了 Lambda 体，即 Lambda 表达式要执行
的功能&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h3&gt;要求&lt;/h3&gt;
&lt;blockquote&gt;
&lt;p&gt;接口只有一个方法&lt;/p&gt;
&lt;p&gt;lambda表达式，如果一个接口只有一个方法，我可以把方法名省略
Foo foo = () -&amp;gt; {System.out.println(&quot;****hello lambda&quot;);};&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h3&gt;写法&lt;/h3&gt;
&lt;blockquote&gt;
&lt;p&gt;拷贝小括号（），写死右箭头-&amp;gt;，落地大括号{...}&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h3&gt;函数式接口&lt;/h3&gt;
&lt;blockquote&gt;
&lt;p&gt;lambda表达式，必须是函数式接口，必须只有一个方法
如果接口只有一个方法java默认它为函数式接口。
为了正确使用Lambda表达式，需要给接口加个注解：@FunctionalInterface
如有两个方法，立刻报错&lt;/p&gt;
&lt;p&gt;Runnable接口为什么可以用lambda表达式？&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2&gt;接口里是否能有实现方法？&lt;/h2&gt;
&lt;h3&gt;default方法&lt;/h3&gt;
&lt;blockquote&gt;
&lt;p&gt;接口里在java8后允许有接口的实现，default方法默认实现
default int div(int x,int y) {
return x/y;
}
接口里default方法可以有几个？  多个&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h3&gt;静态方法实现&lt;/h3&gt;
&lt;blockquote&gt;
&lt;p&gt;静态方法实现：接口新增
public static int sub(int x,int y){
return x-y;
}
可以有几个？  多个
注意静态的叫类方法，能用foo去调吗？要改成Foo&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2&gt;代码&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;package com.atguigu.thread;

@FunctionalInterface
interface Foo{
 
// public void sayHello() ;
// public void say886() ;
 
 public int add(int x,int y);
 
 default int div(int x,int y) {
   return x/y;
 }
 
 public static int sub(int x,int y) {
   return x-y;
 }
}
 
/**
 * 
 * @Description: Lambda Express-----&amp;gt; 函数式编程
 * 1 拷贝小括号(形参列表)，写死右箭头 -&amp;gt;，落地大括号 {方法实现}
 * 2 有且只有一个public方法@FunctionalInterface注解增强定义
 * 3 default方法默认实现
 * 4 静态方法实现
 */
public class LambdaDemo
{
 public static void main(String[] args)
 {
//  Foo foo = new Foo() {
//   @Override
//   public void sayHello() {
//     System.out.println(&quot;Hello!!&quot;);
//   }
//
//   @Override
//   public void say886() {
//     // TODO Auto-generated method stub
//     
//   }
//  };
//  foo.sayHello();
//  System.out.println(&quot;**=****=**==&quot;);
//  foo = ()-&amp;gt;{System.out.println(&quot;Hello!! lambda !!&quot;);};
//  foo.sayHello();
 
 Foo foo = (x,y)-&amp;gt;{
   System.out.println(&quot;Hello!! lambda !!&quot;);
   return x+y;
   };
   
   int result = foo.add(3,5);
   System.out.println(&quot;******result=&quot;+result);
   System.out.println(&quot;******result div=&quot;+foo.div(10, 2));
   System.out.println(&quot;******result sub=&quot;+Foo.sub(10, 2));
   
 }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h1&gt;4、线程间通信&lt;/h1&gt;
&lt;h2&gt;面试题：两个线程打印&lt;/h2&gt;
&lt;blockquote&gt;
&lt;p&gt;两个线程，一个线程打印1-52，另一个打印字母A-Z    打印顺序为12A34B...5152Z,
要求用线程间通信&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2&gt;例子：NotifyWaitDemo&lt;/h2&gt;
&lt;p&gt;线程间通信：&lt;/p&gt;
&lt;p&gt;1、生产者+消费者&lt;/p&gt;
&lt;p&gt;2、通知等待唤醒机制&lt;/p&gt;
&lt;h2&gt;多线程编程模板中&lt;/h2&gt;
&lt;p&gt;1、判断&lt;/p&gt;
&lt;p&gt;2、干活&lt;/p&gt;
&lt;p&gt;3、通知&lt;/p&gt;
&lt;h2&gt;synchronized实现&lt;/h2&gt;
&lt;h3&gt;代码&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;package com.atguigu.thread;
 
import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
 
 
 
class ShareDataOne//资源类
{
  private int number = 0;//初始值为零的一个变量
 
  //为0时加1，不为0时进入等待
  public synchronized void increment() throws InterruptedException 
  {
     //1判断
     if(number !=0 ) {
       this.wait();
     }
     //2干活
     ++number;
     System.out.println(Thread.currentThread().getName()+&quot;\t&quot;+number);
     //3通知
     this.notifyAll();
  }
  
  //为0时进入等待，不为0时减1
  public synchronized void decrement() throws InterruptedException 
  {
     // 1判断
     if (number == 0) {
       this.wait();
     }
     // 2干活
     --number;
     System.out.println(Thread.currentThread().getName() + &quot;\t&quot; + number);
     // 3通知
     this.notifyAll();
  }
}
 
/**
 * 
 * @Description:
 *现在两个线程，
 * 可以操作初始值为零的一个变量，
 * 实现一个线程对该变量加1，一个线程对该变量减1，
 * 交替，来10轮。 
 * @author xialei
 *
 *  * 笔记：Java里面如何进行工程级别的多线程编写
 * 1 多线程变成模板（套路）-----上
 *     1.1  线程    操作    资源类  
 *     1.2  高内聚  低耦合
 * 2 多线程变成模板（套路）-----下
 *     2.1  判断
 *     2.2  干活
 *     2.3  通知
 
 */
public class NotifyWaitDemoOne
{
  public static void main(String[] args)
  {
     ShareDataOne sd = new ShareDataOne();
     new Thread(() -&amp;gt; {
       for (int i = 1; i &amp;lt; 10; i++) {
          try {
            sd.increment();
          } catch (InterruptedException e) {
            // TODO Auto-generated catch block
            e.printStackTrace();
          }
       }
     }, &quot;A&quot;).start();
     new Thread(() -&amp;gt; {
       for (int i = 1; i &amp;lt; 10; i++) {
          try {
            sd.decrement();
          } catch (InterruptedException e) {
            // TODO Auto-generated catch block
            e.printStackTrace();
          }
       }
     }, &quot;B&quot;).start();
  }
}
/*
 * * 
 * 2 多线程变成模板（套路）-----下
 *     2.1  判断
 *     2.2  干活
 *     2.3  通知
 * 3 防止虚假唤醒用while
 * 虚假唤醒（Spurious Wakeup）是指在多线程编程中，有时候线程可能会在没有显式调用 notify 或 notifyAll 的情况下被唤醒，
 	即使条件并未满足。
 	为了防止虚假唤醒，应该使用 while 循环来进行条件的检查，而不是使用 if 语句。
 * 
 * */
class ShareDataOne {
    private int number = 0;

    private final Lock lock = new ReentrantLock();
    private final Condition condition = lock.newCondition();

    public void increment() throws InterruptedException {
        lock.lock();
        try {
            while (number != 0) { // 使用while循环判断条件
                condition.await();
            }
            ++number;
            System.out.println(Thread.currentThread().getName() + &quot;\t&quot; + number);
            condition.signalAll();
        } finally {
            lock.unlock();
        }
    }

    public void decrement() throws InterruptedException {
        lock.lock();
        try {
            while (number == 0) { // 使用while循环判断条件
                condition.await();
            }
            --number;
            System.out.println(Thread.currentThread().getName() + &quot;\t&quot; + number);
            condition.signalAll();
        } finally {
            lock.unlock();
        }
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Q&amp;amp;A&lt;/h2&gt;
&lt;p&gt;这段代码中的this.wait();怎么解释，我理解的wait是释放锁，线程结束&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;你对 &lt;code&gt;wait()&lt;/code&gt; 的理解有些不准确，我会为你解释一下。&lt;/p&gt;
&lt;p&gt;在 Java 中，&lt;code&gt;wait()&lt;/code&gt; 是 &lt;strong&gt;Object 类的一个方法&lt;/strong&gt;，&lt;strong&gt;用于让线程等待，同时释放对象的监视器（锁）&lt;/strong&gt;。当一个线程调用某个对象的 &lt;code&gt;wait()&lt;/code&gt; 方法时，&lt;strong&gt;它会进入对象的等待集（Waiting Set）中&lt;/strong&gt;，暂时放弃锁，允许其他线程获得该锁并继续执行。被放入等待集中的线程只有在其他线程调用相同对象的 &lt;code&gt;notify()&lt;/code&gt; 或 &lt;code&gt;notifyAll()&lt;/code&gt; 方法时，才有机会被唤醒。&lt;/p&gt;
&lt;p&gt;在你的代码中，&lt;code&gt;this.wait()&lt;/code&gt; 的作用是使当前线程进入等待状态，同时释放了对象 &lt;code&gt;this&lt;/code&gt; 的监视器。这样做的目的是为了在某些条件未满足的情况下，让线程等待，并允许其他线程执行。&lt;/p&gt;
&lt;p&gt;具体解释你代码中的 &lt;code&gt;this.wait()&lt;/code&gt;：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;当 &lt;code&gt;increment()&lt;/code&gt; 方法中的 &lt;code&gt;number&lt;/code&gt; 不等于 0 时，线程进入等待状态并释放锁，允许其他线程执行。&lt;/li&gt;
&lt;li&gt;当 &lt;code&gt;decrement()&lt;/code&gt; 方法中的 &lt;code&gt;number&lt;/code&gt; 等于 0 时，线程进入等待状态并释放锁，允许其他线程执行。&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;&lt;code&gt;wait()&lt;/code&gt; 并不是线程的结束，而是使线程暂时停止执行，直到被其他线程唤醒。线程会在某些条件满足时被唤醒，然后再次竞争锁，继续执行。通常在使用 &lt;code&gt;wait()&lt;/code&gt; 时，要结合条件判断来使用，以避免虚假唤醒。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;为什么会出现：操作系统或虚拟机可能会在没有通知的情况下唤醒其中一个线程。&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;在多线程环境中，操作系统和虚拟机可能会选择在没有明确的 &lt;code&gt;notify()&lt;/code&gt; 调用的情况下唤醒线程，这是为了提高整体系统的性能和响应性。&lt;/p&gt;
&lt;p&gt;这种行为涉及到操作系统和虚拟机内部的实现细节，具体原因如下：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;系统调度策略：&lt;/strong&gt; 操作系统负责调度线程以便它们在多核处理器上运行。系统可能会周期性地重新调度线程，以平衡负载或执行其他管理任务。在这个过程中，等待状态的线程可能被重新调度并唤醒。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;虚拟机内部机制：&lt;/strong&gt; 虚拟机为了&lt;strong&gt;提高性能&lt;/strong&gt;，可能会实现一些内部机制来更好地响应线程的状态变化。这可能导致在没有明确的 &lt;code&gt;notify()&lt;/code&gt; 调用的情况下唤醒线程。&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;为了应对这种情况，Java 中引入了等待状态的线程的经典解决方案，即在等待的时候使用 &lt;code&gt;while&lt;/code&gt; 循环而不是 &lt;code&gt;if&lt;/code&gt; 语句。这是为了防止虚假唤醒，即使在没有明确通知的情况下，线程也会在等待状态中醒来。使用 &lt;code&gt;while&lt;/code&gt; 循环会在每次唤醒后重新检查条件，确保在真正满足条件之前线程不会误以为条件已经满足。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;synchronized (lock) {
    while (!condition) {
        lock.wait();
    }
    // 执行操作
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这样的做法可以防止因虚假唤醒而导致的条件错误判断。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h3&gt;换成4个线程&lt;/h3&gt;
&lt;blockquote&gt;
&lt;p&gt;换成4个线程会导致错误，&lt;strong&gt;虚假唤醒&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;原因：在java多线程判断时，不能用if，程序出事出在了判断上面，
突然有一添加的线程进到if了，突然中断了交出控制权，
没有进行验证，而是直接走下去了，加了两次，甚至多次&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h3&gt;解决办法&lt;/h3&gt;
&lt;blockquote&gt;
&lt;p&gt;解决虚假唤醒：查看API，java.lang.Object&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://tangtangit.com/_astro/image-20210616154210661.BusFKHsR_ZPA9nk.webp&quot; alt=&quot;image-20210616154210661&quot; /&gt;&lt;/p&gt;
&lt;p&gt;中断和虚假唤醒是可能产生的，所以要用loop循环，if只判断一次，while是只要唤醒就要拉回来再判断一次。if换成while&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h4&gt;代码&lt;/h4&gt;
&lt;pre&gt;&lt;code&gt;package com.atguigu.thread;
 
import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
 
import org.omg.IOP.Codec;
 
 
class ShareData//资源类
{
  private int number = 0;//初始值为零的一个变量
 
  public synchronized void increment() throws InterruptedException 
  {
     //判断
     while(number!=0) {
       this.wait();
     }
     //干活
     ++number;
     System.out.println(Thread.currentThread().getName()+&quot; \t &quot;+number);
     //通知
     this.notifyAll();;
  }
  
  public synchronized void decrement() throws InterruptedException 
  {
     //判断
     while(number!=1) {
       this.wait();
     }
     //干活
     --number;
     System.out.println(Thread.currentThread().getName()+&quot; \t &quot;+number);
     //通知
     this.notifyAll();
  }
}
 
/**
 * 
 * @Description:
 *现在两个线程，
 * 可以操作初始值为零的一个变量，
 * 实现一个线程对该变量加1，一个线程对该变量减1，
 * 交替，来10轮。 
 * @author xialei
 *
 *  * 笔记：Java里面如何进行工程级别的多线程编写
 * 1 多线程变成模板（套路）-----上
 *     1.1  线程    操作    资源类  
 *     1.2  高内聚  低耦合
 * 2 多线程变成模板（套路）-----下
 *     2.1  判断
 *     2.2  干活
 *     2.3  通知
 
 */
public class NotifyWaitDemo
{
  public static void main(String[] args)
  {
     ShareData sd = new ShareData();
     new Thread(() -&amp;gt; {
 
       for (int i = 1; i &amp;lt;= 10; i++) {
          try {
            sd.increment();
          } catch (InterruptedException e) {
            e.printStackTrace();
          }
       }
     }, &quot;A&quot;).start();
     
     new Thread(() -&amp;gt; {
 
       for (int i = 1; i &amp;lt;= 10; i++) {
          try {
            sd.decrement();
          } catch (InterruptedException e) {
            e.printStackTrace();
          }
       }
     }, &quot;B&quot;).start();
     new Thread(() -&amp;gt; {
 
       for (int i = 1; i &amp;lt;= 10; i++) {
          try {
            sd.increment();
          } catch (InterruptedException e) {
            e.printStackTrace();
          }
       }
     }, &quot;C&quot;).start();
     new Thread(() -&amp;gt; {
 
       for (int i = 1; i &amp;lt;= 10; i++) {
          try {
            sd.decrement();
          } catch (InterruptedException e) {
            e.printStackTrace();
          }
       }
     }, &quot;D&quot;).start();
     
  }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h4&gt;原理图&lt;/h4&gt;
&lt;p&gt;&lt;img src=&quot;https://tangtangit.com/_astro/image-20210616154401776.BVJvQ7X5_ee8Qt.webp&quot; alt=&quot;image-20210616154401776&quot; /&gt;&lt;/p&gt;
&lt;h2&gt;多线程编程模板下&lt;/h2&gt;
&lt;h3&gt;1、注意多线程之间的虚假唤醒&lt;/h3&gt;
&lt;blockquote&gt;
&lt;ul&gt;
&lt;li&gt;虚假唤醒（Spurious Wakeup）是指在多线程编程中，有时候线程可能会&lt;strong&gt;在没有显式调用 notify 或 notifyAll 的情况下被唤醒，即使条件并未满足。&lt;/strong&gt;
为了防止虚假唤醒，应该使用 while 循环来进行条件的检查，而不是使用 if 语句。&lt;/li&gt;
&lt;/ul&gt;
&lt;/blockquote&gt;
&lt;h2&gt;java8新版实现&lt;/h2&gt;
&lt;h3&gt;synchronized和locks对标实现&lt;/h3&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;img src=&quot;https://tangtangit.com/_astro/image-20210616155048561.CBfmbMS-_2jFxqM.webp&quot; alt=&quot;image-20210616155048561&quot; /&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://tangtangit.com/_astro/image-20210616155057983.Bu0bF4vr_ZMTWS4.webp&quot; alt=&quot;image-20210616155057983&quot; /&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h3&gt;Condition&lt;/h3&gt;
&lt;p&gt;Condition：查看API，java.util.concurrent&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://tangtangit.com/_astro/image-20210616155145454.CRS2Qw8l_Z1PErWS.webp&quot; alt=&quot;image-20210616155145454&quot; /&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;class BoundedBuffer {
   final Lock lock = new ReentrantLock();
   final Condition notFull  = lock.newCondition(); 
   final Condition notEmpty = lock.newCondition(); 

   final Object[] items = new Object[100];
   int putptr, takeptr, count;

   public void put(Object x) throws InterruptedException {
     lock.lock();
     try {
       while (count == items.length)
         notFull.await();
       items[putptr] = x;
       if (++putptr == items.length) putptr = 0;
       ++count;
       notEmpty.signal();
     } finally {
       lock.unlock();
     }
   }
&lt;/code&gt;&lt;/pre&gt;
&lt;h4&gt;代码&lt;/h4&gt;
&lt;pre&gt;&lt;code&gt;package com.atguigu.thread;
 
import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
 
import org.omg.IOP.Codec;
 
 
class ShareData//资源类
{
  private int number = 0;//初始值为零的一个变量
 
  private Lock lock = new ReentrantLock();
  private Condition condition  = lock.newCondition(); 
   
  public  void increment() throws InterruptedException 
  {
     
      lock.lock();
         try {
          //判断
          while(number!=0) {
            condition.await();
          }
          //干活
          ++number;
          System.out.println(Thread.currentThread().getName()+&quot; \t &quot;+number);
          //通知
          condition.signalAll();
     } catch (Exception e) {
       e.printStackTrace();
     } finally {
       lock.unlock();
     }
     
  }
  
  
  public  void decrement() throws InterruptedException 
  {
      
      lock.lock();
         try {
          //判断
          while(number!=1) {
            condition.await();
          }
          //干活
          --number;
          System.out.println(Thread.currentThread().getName()+&quot; \t &quot;+number);
          //通知
          condition.signalAll();
     } catch (Exception e) {
       e.printStackTrace();
     } finally {
       lock.unlock();
     }
     
  }
  
  /*public synchronized void increment() throws InterruptedException 
  {
     //判断
     while(number!=0) {
       this.wait();
     }
     //干活
     ++number;
     System.out.println(Thread.currentThread().getName()+&quot; \t &quot;+number);
     //通知
     this.notifyAll();;
  }
  
  public synchronized void decrement() throws InterruptedException 
  {
     //判断
     while(number!=1) {
       this.wait();
     }
     //干活
     --number;
     System.out.println(Thread.currentThread().getName()+&quot; \t &quot;+number);
     //通知
     this.notifyAll();
  }*/
}
 
/**
 * 
 * @Description:
 *现在两个线程，
 * 可以操作初始值为零的一个变量，
 * 实现一个线程对该变量加1，一个线程对该变量减1，
 * 交替，来10轮。 
 *
 *  * 笔记：Java里面如何进行工程级别的多线程编写
 * 1 多线程变成模板（套路）-----上
 *     1.1  线程    操作    资源类  
 *     1.2  高内聚  低耦合
 * 2 多线程变成模板（套路）-----下
 *     2.1  判断
 *     2.2  干活
 *     2.3  通知
 
 */
public class NotifyWaitDemo
{
  public static void main(String[] args)
  {
     ShareData sd = new ShareData();
     new Thread(() -&amp;gt; {
 
       for (int i = 1; i &amp;lt;= 10; i++) {
          try {
            sd.increment();
          } catch (InterruptedException e) {
            e.printStackTrace();
          }
       }
     }, &quot;A&quot;).start();
     
     new Thread(() -&amp;gt; {
 
       for (int i = 1; i &amp;lt;= 10; i++) {
          try {
            sd.decrement();
          } catch (InterruptedException e) {
            e.printStackTrace();
          }
       }
     }, &quot;B&quot;).start();
     new Thread(() -&amp;gt; {
 
       for (int i = 1; i &amp;lt;= 10; i++) {
          try {
            sd.increment();
          } catch (InterruptedException e) {
            e.printStackTrace();
          }
       }
     }, &quot;C&quot;).start();
     new Thread(() -&amp;gt; {
 
       for (int i = 1; i &amp;lt;= 10; i++) {
          try {
            sd.decrement();
          } catch (InterruptedException e) {
            e.printStackTrace();
          }
       }
     }, &quot;D&quot;).start();
     
  }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h1&gt;5、线程间定制化调用通信Condition&lt;/h1&gt;
&lt;h2&gt;例子：ThreadOrderAccess&lt;/h2&gt;
&lt;h2&gt;线程-调用-资源类&lt;/h2&gt;
&lt;h2&gt;判断-干活-通知&lt;/h2&gt;
&lt;blockquote&gt;
&lt;p&gt;1、有顺序通知，需要有标识位&lt;/p&gt;
&lt;p&gt;2、有一个锁Lock，3把钥匙Condition&lt;/p&gt;
&lt;p&gt;3、判断标志位&lt;/p&gt;
&lt;p&gt;4、输出线程名+第几次+第几轮&lt;/p&gt;
&lt;p&gt;5、修改标志位，通知下一个&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2&gt;代码&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;package com.atguigu.thread;
 
import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
 
 
class ShareResource
{
  private int number = 1;//1:A 2:B 3:C 
  private Lock lock = new ReentrantLock();
  private Condition c1 = lock.newCondition();
  private Condition c2 = lock.newCondition();
  private Condition c3 = lock.newCondition();
 
  public void print5(int totalLoopNumber)
  {
     lock.lock();
     try 
     {
       //1 判断
       while(number != 1)
       {
          //A 就要停止
          c1.await();
       }
       //2 干活
       for (int i = 1; i &amp;lt;=5; i++) 
       {
          System.out.println(Thread.currentThread().getName()+&quot;\t&quot;+i+&quot;\t totalLoopNumber: &quot;+totalLoopNumber);
       }
       //3 通知
       number = 2;
       c2.signal();
     } catch (Exception e) {
       e.printStackTrace();
     } finally {
       lock.unlock();
     }
  }
  public void print10(int totalLoopNumber)
  {
     lock.lock();
     try 
     {
       //1 判断
       while(number != 2)
       {
          //A 就要停止
          c2.await();
       }
       //2 干活
       for (int i = 1; i &amp;lt;=10; i++) 
       {
          System.out.println(Thread.currentThread().getName()+&quot;\t&quot;+i+&quot;\t totalLoopNumber: &quot;+totalLoopNumber);
       }
       //3 通知
       number = 3;
       c3.signal();
     } catch (Exception e) {
       e.printStackTrace();
     } finally {
       lock.unlock();
     }
  }  
  
  public void print15(int totalLoopNumber)
  {
     lock.lock();
     try 
     {
       //1 判断
       while(number != 3)
       {
          //A 就要停止
          c3.await();
       }
       //2 干活
       for (int i = 1; i &amp;lt;=15; i++) 
       {
          System.out.println(Thread.currentThread().getName()+&quot;\t&quot;+i+&quot;\t totalLoopNumber: &quot;+totalLoopNumber);
       }
       //3 通知
       number = 1;
       c1.signal();
     } catch (Exception e) {
       e.printStackTrace();
     } finally {
       lock.unlock();
     }
  }  
}
 
 
/**
 * 
 * @Description: 
 * 多线程之间按顺序调用，实现A-&amp;gt;B-&amp;gt;C
 * 三个线程启动，要求如下：
 * 
 * AA打印5次，BB打印10次，CC打印15次
 * 接着
 * AA打印5次，BB打印10次，CC打印15次
 * ......来10轮  
 *
 */
public class ThreadOrderAccess
{
  public static void main(String[] args)
  {
     ShareResource sr = new ShareResource();
     
     new Thread(() -&amp;gt; {
       for (int i = 1; i &amp;lt;=10; i++) 
       {
          sr.print5(i);
       }
     }, &quot;AA&quot;).start();
     new Thread(() -&amp;gt; {
       for (int i = 1; i &amp;lt;=10; i++) 
       {
          sr.print10(i);
       }
     }, &quot;BB&quot;).start();
     new Thread(() -&amp;gt; {
       for (int i = 1; i &amp;lt;=10; i++) 
       {
          sr.print15(i);
       }
     }, &quot;CC&quot;).start();   
     
     
  }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h1&gt;6、多线程锁&lt;/h1&gt;
&lt;p&gt;例子：Lock_8&lt;/p&gt;
&lt;h2&gt;锁的8个问题&lt;/h2&gt;
&lt;h2&gt;8锁分析&lt;/h2&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;锁一、锁二&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;一个对象里面如果有多个synchronized方法，某一个时刻内，只要一个线程去调用其中的一个synchronized方法了，
其它的线程都只能等待，换句话说，某一个时刻内，只能有唯一一个线程去访问这些synchronized方法
锁的是当前对象this，被锁定后，其它的线程都不能进入到当前对象的其它的synchronized方法&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;锁三、锁四&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;加个普通方法后发现和同步锁无关
换成两个对象后，不是同一把锁了，情况立刻变化。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;锁五、锁六&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;synchronized实现同步的基础：Java中的每一个对象都可以作为锁。
具体表现为以下3种形式。
对于普通同步方法，锁是当前实例对象。
对于静态同步方法，锁是当前类的Class对象。
对于同步方法块，锁是Synchonized括号里配置的对象&lt;/p&gt;
&lt;p&gt;当一个线程试图访问同步代码块时，它首先必须得到锁，退出或抛出异常时必须释放锁。&lt;/p&gt;
&lt;p&gt;也就是说如果一个实例对象的非静态同步方法获取锁后，该实例对象的其他非静态同步方法必须等待获取锁的方法释放锁后才能获取锁，
可是别的实例对象的非静态同步方法因为跟该实例对象的非静态同步方法用的是不同的锁，
所以毋须等待该实例对象已获取锁的非静态同步方法释放锁就可以获取他们自己的锁。&lt;/p&gt;
&lt;p&gt;所有的静态同步方法用的也是同一把锁——类对象本身，
这两把锁是两个不同的对象，所以静态同步方法与非静态同步方法之间是不会有竞态条件的。
但是一旦一个静态同步方法获取锁后，其他的静态同步方法都必须等待该方法释放锁后才能获取锁，
而不管是同一个实例对象的静态同步方法之间，
还是不同的实例对象的静态同步方法之间，只要它们同一个类的实例对象！&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2&gt;代码&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;/**
 * @author Guanghao Wei
 * @create 2023-04-10 14:57
 */

class Phone {
    public synchronized void sendEmail() {
        try {
            TimeUnit.SECONDS.sleep(3);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println(&quot;------sendEmail&quot;);
    }

    public synchronized void sendSMS() {
        System.out.println(&quot;------sendSMS&quot;);
    }

    public void hello() {
        System.out.println(&quot;------hello&quot;);
    }
}

/**
 * 现象描述：
 * 1 标准访问ab两个线程，请问先打印邮件还是短信？ --------先邮件，后短信  共用一个对象锁
 * 2. sendEmail钟加入暂停3秒钟，请问先打印邮件还是短信？---------先邮件，后短信  共用一个对象锁
 * 3. 添加一个普通的hello方法，请问先打印普通方法还是邮件？ --------先hello，再邮件
 * 4. 有两部手机，请问先打印邮件还是短信？ ----先短信后邮件  资源没有争抢，不是同一个对象锁
 * 5. 有两个静态同步方法，一步手机， 请问先打印邮件还是短信？---------先邮件后短信  共用一个类锁
 * 6. 有两个静态同步方法，两部手机， 请问先打印邮件还是短信？ ----------先邮件后短信 共用一个类锁
 * 7. 有一个静态同步方法 一个普通同步方法，请问先打印邮件还是短信？ ---------先短信后邮件   一个用类锁一个用对象锁
 * 8. 有一个静态同步方法，一个普通同步方法，两部手机，请问先打印邮件还是短信？ -------先短信后邮件 一个类锁一个对象锁
 */

public class Lock8Demo {
    public static void main(String[] args) {
        Phone phone = new Phone();
        new Thread(() -&amp;gt; {
            phone.sendEmail();
        }, &quot;a&quot;).start();

        try {
            TimeUnit.MILLISECONDS.sleep(200);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }

        new Thread(() -&amp;gt; {
            phone.sendSMS();
        }, &quot;b&quot;).start();
    }

}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;结论：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;对于普通同步方法，锁的是当前实例对象，通常指this，所有的同步方法用的都是同一把锁---&amp;gt;实例对象本身&lt;/li&gt;
&lt;li&gt;对于静态同步方法，锁的时当前类的Class对象&lt;/li&gt;
&lt;li&gt;对于同步方法块，锁的时synchronized括号内的对象&lt;/li&gt;
&lt;/ul&gt;
&lt;h1&gt;7、NotSafeDemo&lt;/h1&gt;
&lt;h2&gt;需求&lt;/h2&gt;
&lt;p&gt;请举例说明集合类是不安全的&lt;/p&gt;
&lt;h2&gt;证明集合不安全&lt;/h2&gt;
&lt;h3&gt;例子：NotSafeDemo&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;public class NotSafeDemo {

    public static void main(String[] args) {
        ArrayList&amp;lt;String&amp;gt; list = new ArrayList&amp;lt;&amp;gt;();
        for (int i = 0; i &amp;lt;30; i++) {
            new Thread(()-&amp;gt;{
                list.add(UUID.randomUUID().toString().substring(0,8));
                System.out.println(list);
            },String.valueOf(i)).start();
        }
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;线程不安全错误&lt;/h3&gt;
&lt;blockquote&gt;
&lt;p&gt;java.util.ConcurrentModificationException&lt;/p&gt;
&lt;p&gt;ArrayList在迭代的时候如果同时对其进行修改就会
抛出java.util.ConcurrentModificationException异常
并发修改异常&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2&gt;Q&amp;amp;A&lt;/h2&gt;
&lt;p&gt;正在遍历的线程和添加元素的线程会同时操作同一个集合为什么就会报错？&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;当一个线程正在迭代（遍历）一个 &lt;code&gt;ArrayList&lt;/code&gt;，而另一个线程同时修改这个 &lt;code&gt;ArrayList&lt;/code&gt; 时，可能会出现以下情况：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;迭代器失效：&lt;/strong&gt; Java 的 &lt;code&gt;ArrayList&lt;/code&gt; 使用迭代器来遍历集合，当集合发生结构性改变（元素增加或删除）时，**迭代器的状态会失效，因为迭代器无法预知集合的结构变化。**这就是为什么会抛出 &lt;code&gt;ConcurrentModificationException&lt;/code&gt; 异常。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;数据不一致：&lt;/strong&gt; 当一个线程正在迭代 &lt;code&gt;ArrayList&lt;/code&gt; 的同时，另一个线程进行元素的增加、删除等操作，可能会导致数据不一致性，从而影响迭代过程中获取的元素或迭代结束的判定条件。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;img src=&quot;https://tangtangit.com/_astro/image-20230822115228132.h6bbxF2N_pHPew.webp&quot; alt=&quot;image-20230822115228132&quot; /&gt;&lt;/p&gt;
&lt;p&gt;修改的数量和预期修改的数量不一致，直接抛出异常。&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;/blockquote&gt;
&lt;p&gt;在arraylist源码中为什么会导致expectedModCount 和 modCount数量不一致，最后抛出并发修改异常？&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;在 &lt;code&gt;ArrayList&lt;/code&gt; 的源码中，&lt;code&gt;modCount&lt;/code&gt; 是一个用于记录对集合结构（元素的增加、删除等）进行修改的计数器。而 &lt;code&gt;expectedModCount&lt;/code&gt; 是用于迭代器的快速失败机制，用来检测在迭代过程中是否有其他线程对集合进行了修改。&lt;/p&gt;
&lt;p&gt;当你创建一个迭代器并开始遍历 &lt;code&gt;ArrayList&lt;/code&gt; 时，&lt;code&gt;expectedModCount&lt;/code&gt; 会被初始化为当前的 &lt;code&gt;modCount&lt;/code&gt; 值。在每次调用迭代器的 &lt;code&gt;next()&lt;/code&gt; 方法时，都会检查当前的 &lt;code&gt;modCount&lt;/code&gt; 是否与初始化时的 &lt;code&gt;expectedModCount&lt;/code&gt; 值相等，如果不相等，就会抛出 &lt;code&gt;ConcurrentModificationException&lt;/code&gt; 异常。&lt;/p&gt;
&lt;p&gt;这个机制旨在在迭代过程中，如果有其他线程对集合进行了结构性修改，就可以快速失败，避免不确定的结果和数据一致性问题。&lt;/p&gt;
&lt;p&gt;导致 &lt;code&gt;expectedModCount&lt;/code&gt; 和 &lt;code&gt;modCount&lt;/code&gt; 数量不一致并抛出并发修改异常的情况可能是因为：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;并发修改：&lt;/strong&gt; &lt;strong&gt;在你创建迭代器后，其他线程对 &lt;code&gt;ArrayList&lt;/code&gt; 进行了结构性修改，比如添加或删除元素，从而增加了 &lt;code&gt;modCount&lt;/code&gt; 的值，而 &lt;code&gt;expectedModCount&lt;/code&gt; 仍保持初始值。&lt;/strong&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;只读迭代器：&lt;/strong&gt; 如果使用 &lt;code&gt;Collections.unmodifiableList()&lt;/code&gt; 方法创建只读的迭代器，这时对 &lt;code&gt;ArrayList&lt;/code&gt; 的修改会导致 &lt;code&gt;modCount&lt;/code&gt; 值增加，而只读迭代器的 &lt;code&gt;expectedModCount&lt;/code&gt; 保持不变，从而引发异常。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;多线程修改：&lt;/strong&gt; 在多线程环境下，一个线程进行迭代，另一个线程同时修改 &lt;code&gt;ArrayList&lt;/code&gt; 的结构，导致 &lt;code&gt;modCount&lt;/code&gt; 值发生变化。&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;总之，&lt;code&gt;ArrayList&lt;/code&gt; 在迭代时使用 &lt;code&gt;modCount&lt;/code&gt; 和 &lt;code&gt;expectedModCount&lt;/code&gt; 机制来检测并发修改，以保证迭代过程中数据的一致性。当 &lt;code&gt;expectedModCount&lt;/code&gt; 和 &lt;code&gt;modCount&lt;/code&gt; 数量不一致时，就会抛出 &lt;code&gt;ConcurrentModificationException&lt;/code&gt; 异常。这是一种快速失败机制，帮助发现并发修改问题。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h3&gt;原理&lt;/h3&gt;
&lt;blockquote&gt;
&lt;pre&gt;&lt;code&gt;List&amp;lt;String&amp;gt; list = new ArrayList&amp;lt;&amp;gt;();
for (int i = 0; i &amp;lt;30 ; i++) {
      new Thread(()-&amp;gt;{
          list.add(UUID.randomUUID().toString().substring(0,8));
          System.out.println(list);
      },String.valueOf(i)).start();
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;看ArrayList的源码&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;        public void add(E e) {
            checkForComodification();

            try {
                int i = cursor;
                ArrayList.this.add(i, e);
                cursor = i + 1;
                lastRet = -1;
                expectedModCount = modCount;
            } catch (IndexOutOfBoundsException ex) {
                throw new ConcurrentModificationException();
            }
        }
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;checkForComodification&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;        final void checkForComodification() {
            if (modCount != expectedModCount)
                throw new ConcurrentModificationException();
        }
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;没有synchronized线程不安全&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2&gt;解决方案&lt;/h2&gt;
&lt;h3&gt;1、Vector&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;List&amp;lt;String&amp;gt; list = new Vector&amp;lt;&amp;gt;();

看Vector的源码
public synchronized boolean add(E e) {
    modCount++;
    ensureCapacityHelper(elementCount + 1);
    elementData[elementCount++] = e;
    return true;
}
有synchronized线程安全
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;2、Collections&lt;/h3&gt;
&lt;p&gt;List&amp;lt;String&amp;gt; list = Collections.synchronizedList(new ArrayList&amp;lt;&amp;gt;());
Collections提供了方法synchronizedList保证list是同步线程安全的&lt;/p&gt;
&lt;p&gt;那HashMap，HashSet是线程安全的吗？也不是
所以有同样的线程安全方法&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://tangtangit.com/_astro/image-20210617101336152.UW4k8wIE_Z24VJqc.webp&quot; alt=&quot;image-20210617101336152&quot; /&gt;&lt;/p&gt;
&lt;h3&gt;3、写时复制&lt;/h3&gt;
&lt;p&gt;&lt;img src=&quot;https://tangtangit.com/_astro/image-20210617101409018.BtUMymyF_ZeXgeI.webp&quot; alt=&quot;image-20210617101409018&quot; /&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;List&amp;lt;String&amp;gt; list = new CopyOnWriteArrayList&amp;lt;&amp;gt;();
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;写时复制&lt;/h2&gt;
&lt;h3&gt;不加锁性能提升出错误，加锁数据一致性能下降&lt;/h3&gt;
&lt;h3&gt;CopyOnWriteArrayList定义&lt;/h3&gt;
&lt;blockquote&gt;
&lt;p&gt;A thread-safe variant of ArrayList in which all mutative operations (add, set, and so on) are implemented by making a fresh copy of the underlying array.
CopyOnWriteArrayList是arraylist的一种线程安全变体，
其中所有可变操作（add、set等）都是通过生成底层数组的新副本来实现的。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h3&gt;举例：名单签到&lt;/h3&gt;
&lt;p&gt;&lt;img src=&quot;https://tangtangit.com/_astro/image-20210617103516026.B_pK2Tk-_Z1H0njG.webp&quot; alt=&quot;image-20210617103516026&quot; /&gt;&lt;/p&gt;
&lt;h3&gt;CopyOnWrite理论&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt; 
/**
 * Appends the specified element to the end of this list.
 *
 * @param e element to be appended to this list
 * @return {@code true} (as specified by {@link Collection#add})
 */
public boolean add(E e) {
    final ReentrantLock lock = this.lock;
    lock.lock();
    try {
        Object[] elements = getArray();
        int len = elements.length;
        Object[] newElements = Arrays.copyOf(elements, len + 1);
        newElements[len] = e;
        setArray(newElements);
        return true;
    } finally {
        lock.unlock();
    }
}
 
 
 
CopyOnWrite容器即写时复制的容器。往一个容器添加元素的时候，不直接往当前容器Object[]添加，
而是先将当前容器Object[]进行Copy，复制出一个新的容器Object[] newElements，然后向新的容器Object[] newElements里添加元素。
添加元素后，再将原容器的引用指向新的容器setArray(newElements)。
这样做的好处是可以对CopyOnWrite容器进行并发的读，而不需要加锁，因为当前容器不会添加任何元素。
所以CopyOnWrite容器也是一种读写分离的思想，****读和写不同的容器。****
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;扩展类比&lt;/h3&gt;
&lt;h4&gt;HashSet&lt;/h4&gt;
&lt;blockquote&gt;
&lt;p&gt;Set&amp;lt;String&amp;gt; set = new HashSet&amp;lt;&amp;gt;();//线程不安全&lt;/p&gt;
&lt;p&gt;Set&amp;lt;String&amp;gt; set = new &lt;strong&gt;CopyOnWriteArraySet&lt;/strong&gt;&amp;lt;&amp;gt;();//线程安全&lt;/p&gt;
&lt;p&gt;HashSet底层数据结构是什么？
HashMap  ?&lt;/p&gt;
&lt;p&gt;但HashSet的add是放一个键值，value永远是一个常量值，而HashMap是放K、V键值对&lt;/p&gt;
&lt;p&gt;public HashSet() {
map = new HashMap&amp;lt;&amp;gt;();
}&lt;/p&gt;
&lt;p&gt;private static final Object PRESENT = new Object();&lt;/p&gt;
&lt;p&gt;public boolean add(E e) {
return map.put(e, PRESENT)==null;
}&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h4&gt;HashMap&lt;/h4&gt;
&lt;blockquote&gt;
&lt;p&gt;Map&amp;lt;String,String&amp;gt; map = new HashMap&amp;lt;&amp;gt;();//线程不安全&lt;/p&gt;
&lt;p&gt;Map&amp;lt;String,String&amp;gt; map = new &lt;strong&gt;ConcurrentHashMap&lt;/strong&gt;&amp;lt;&amp;gt;();//线程安全&lt;/p&gt;
&lt;p&gt;默认容量16，负载因子0.75，阈值12&lt;/p&gt;
&lt;/blockquote&gt;
&lt;blockquote&gt;
&lt;p&gt;hashmap原理：&lt;/p&gt;
&lt;p&gt;底层是node节点的单向链表，node节点中包含有k v，当容量达到8会变成红黑树&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2&gt;集合比较：&lt;/h2&gt;
&lt;blockquote&gt;
&lt;p&gt;口诀：要有统统有，要无统统无&lt;/p&gt;
&lt;p&gt;Vector  有序 有重复&lt;/p&gt;
&lt;p&gt;arrylist   有序 有重复&lt;/p&gt;
&lt;p&gt;LinkedList  有序 有重复&lt;/p&gt;
&lt;p&gt;hashset  无序  无重复&lt;/p&gt;
&lt;p&gt;hashMap  无序  无重复&lt;/p&gt;
&lt;p&gt;TreeSet  有序  无重复&lt;/p&gt;
&lt;p&gt;TreeMap 有序   无重复&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2&gt;扩容工具类：&lt;/h2&gt;
&lt;p&gt;new HashMap   等价于  new HashMap(16,0.75)&lt;/p&gt;
&lt;p&gt;底层数组长度 16*0.75=12，当超过12时，会自动扩容，hashMap扩容是扩一倍，arrayList扩容为原来的一半&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;Arrays.copyOf()
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;代码&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;package com.atguigu.gmall.jucdemo;

import java.util.*;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.CopyOnWriteArrayList;
import java.util.concurrent.CopyOnWriteArraySet;

/**
 * 请举例说明集合类是不安全的
 */
public class NotSafeDemo {
    public static void main(String[] args) {

        Map&amp;lt;String,String&amp;gt; map = new ConcurrentHashMap&amp;lt;&amp;gt;();
        for (int i = 0; i &amp;lt;30 ; i++) {
            new Thread(()-&amp;gt;{
                map.put(Thread.currentThread().getName(),UUID.randomUUID().toString().substring(0,8));
                System.out.println(map);
            },String.valueOf(i)).start();
        }


    }

    private static void setNoSafe() {
        Set&amp;lt;String&amp;gt; set = new CopyOnWriteArraySet&amp;lt;&amp;gt;();
        for (int i = 0; i &amp;lt;30 ; i++) {
            new Thread(()-&amp;gt;{
                set.add(UUID.randomUUID().toString().substring(0,8));
                System.out.println(set);
            },String.valueOf(i)).start();
        }
    }

    private static void listNoSafe() {
        //        List&amp;lt;String&amp;gt; list = Arrays.asList(&quot;a&quot;,&quot;b&quot;,&quot;c&quot;);
        //        list.forEach(System.out::println);
        //写时复制
        List&amp;lt;String&amp;gt; list = new CopyOnWriteArrayList&amp;lt;&amp;gt;();
        // new CopyOnWriteArrayList&amp;lt;&amp;gt;();
        //Collections.synchronizedList(new ArrayList&amp;lt;&amp;gt;());
        //new Vector&amp;lt;&amp;gt;();//new ArrayList&amp;lt;&amp;gt;();

        for (int i = 0; i &amp;lt;30 ; i++) {
                    new Thread(()-&amp;gt;{
                        list.add(UUID.randomUUID().toString().substring(0,8));
                        System.out.println(list);
                    },String.valueOf(i)).start();
                }
    }


}


    /**
     * 写时复制
     CopyOnWrite容器即写时复制的容器。往一个容器添加元素的时候，不直接往当前容器Object[]添加，
     而是先将当前容器Object[]进行Copy，复制出一个新的容器Object[] newElements，然后向新的容器Object[] newElements里添加元素。
     添加元素后，再将原容器的引用指向新的容器setArray(newElements)。
     这样做的好处是可以对CopyOnWrite容器进行并发的读，而不需要加锁，因为当前容器不会添加任何元素。
     所以CopyOnWrite容器也是一种读写分离的思想，读和写不同的容器。

     *
     *
     *
     *

    public boolean add(E e) {
        final ReentrantLock lock = this.lock;
        lock.lock();
        try {
            Object[] elements = getArray();
            int len = elements.length;
            Object[] newElements = Arrays.copyOf(elements, len + 1);
            newElements[len] = e;
            setArray(newElements);
            return true;
        } finally {
            lock.unlock();
        }
    }
     */
&lt;/code&gt;&lt;/pre&gt;
&lt;h1&gt;8、Callable接口&lt;/h1&gt;
&lt;h2&gt;是什么&lt;/h2&gt;
&lt;p&gt;面试题：获得多线程的方法几种？&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;（1）继承thread类（2）runnable接口
如果只回答这两个你连被问到juc的机会都没有&lt;/p&gt;
&lt;p&gt;正确答案如下：
传统的是继承&lt;strong&gt;thread&lt;/strong&gt;类和实现&lt;strong&gt;runnable&lt;/strong&gt;接口，
java5以后又有实现&lt;strong&gt;callable&lt;/strong&gt;接口和&lt;strong&gt;java的线程池&lt;/strong&gt;获得&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;函数式接口&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://tangtangit.com/_astro/image-20210617113539751.DSi7a8uw_Z1vA78Y.webp&quot; alt=&quot;image-20210617113539751&quot; /&gt;&lt;/p&gt;
&lt;p&gt;这是一个函数式接口，因此可以用作lambda表达式或方法引用的赋值对象。&lt;/p&gt;
&lt;h2&gt;与runnable对比&lt;/h2&gt;
&lt;p&gt;实现方法对比&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;// 创建新类MyThread实现runnable接口
class MyThread implements Runnable{
     @Override
     public void run() {

     }
}
//新类MyThread2实现callable接口
class MyThread2 implements Callable&amp;lt;Integer&amp;gt;{
     @Override
     public Integer call() throws Exception {
      return 200;
     } 
}

 面试题:callable接口与runnable接口的区别？
 
 答：  （1）是否有返回值
       （2）是否抛异常
       （3）落地方法不一样，一个是run，一个是call
     
 由于callable是新增的方法，所以有返回值，可以抛出异常
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;怎么用&lt;/h2&gt;
&lt;p&gt;直接替换runnable是否可行？&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://tangtangit.com/_astro/image-20210617114251147.BzvFSC6s_244xQu.webp&quot; alt=&quot;image-20210617114251147&quot; /&gt;&lt;/p&gt;
&lt;p&gt;不可行，因为：thread类的构造方法根本没有Callable&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://tangtangit.com/_astro/image-20210617114305853.D9hLfwMX_Z1NaxC6.webp&quot; alt=&quot;image-20210617114305853&quot; /&gt;&lt;/p&gt;
&lt;p&gt;认识不同的人找中间人&lt;/p&gt;
&lt;p&gt;这像认识一个不认识的同学，我可以找中间人介绍。
中间人是什么？java多态，一个类可以实现多个接口！！&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://tangtangit.com/_astro/image-20210617114329332.DacG7A9h_ZgAJhM.webp&quot; alt=&quot;image-20210617114329332&quot; /&gt;&lt;/p&gt;
&lt;p&gt;FutureTask&amp;lt;Integer&amp;gt; ft = new FutureTask&amp;lt;Integer&amp;gt;(new MyThread());
new Thread(ft, &quot;AA&quot;).start();&lt;/p&gt;
&lt;p&gt;运行成功后如何获得返回值？&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://tangtangit.com/_astro/image-20210617114356561.BMDuf6od_Z2qgjrR.webp&quot; alt=&quot;image-20210617114356561&quot; /&gt;&lt;/p&gt;
&lt;p&gt;​	ft.get();&lt;/p&gt;
&lt;h2&gt;FutureTask&lt;/h2&gt;
&lt;h3&gt;是什么&lt;/h3&gt;
&lt;blockquote&gt;
&lt;p&gt;未来的任务，用它就干一件事，&lt;strong&gt;异步调用&lt;/strong&gt;
main方法就像一个冰糖葫芦，一个个方法由main串起来。
但解决不了一个问题：正常调用挂起堵塞问题&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://tangtangit.com/_astro/image-20210617115426817.C_32Vsji_17N96i.webp&quot; alt=&quot;image-20210617115426817&quot; /&gt;&lt;/p&gt;
&lt;p&gt;例子：
（1）老师上着课，口渴了，去买水不合适，讲课线程继续，我可以单起个线程找班长帮忙买水，
水买回来了放桌上，我需要的时候再去get。
（2）4个同学，A算1+20,B算21+30,C算31*到40,D算41+50，是不是C的计算量有点大啊，
FutureTask单起个线程给C计算，我先汇总ABD，最后等C计算完了再汇总C，拿到最终结果
（3）高考：会做的先做，不会的放在后面做&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h3&gt;原理&lt;/h3&gt;
&lt;blockquote&gt;
&lt;p&gt;在主线程中需要执行比较耗时的操作时，但又不想阻塞主线程时，可以把这些作业交给Future对象在后台完成，
当主线程将来需要时，就可以通过Future对象获得后台作业的计算结果或者执行状态。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;一般FutureTask多用于耗时的计算&lt;/strong&gt;，主线程可以在完成自己的任务后，再去获取结果。&lt;/p&gt;
&lt;p&gt;仅在计算完成时才能检索结果；&lt;strong&gt;如果计算尚未完成，则阻塞 get 方法&lt;/strong&gt;。一旦计算完成，
就不能再重新开始或取消计算。get方法而获取结果只有在计算完成时获取，否则会一直阻塞直到任务转入完成状态，
然后会返回结果或者抛出异常。&lt;/p&gt;
&lt;p&gt;同一个FutureTask&lt;strong&gt;只计算一次&lt;/strong&gt;
&lt;strong&gt;get方法放到最后&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;class MyThread implements Callable&amp;lt;Integer&amp;gt; {


    @Override
    public Integer call() throws Exception {
        System.out.println(Thread.currentThread().getName()+&quot;买水&quot;);
        TimeUnit.SECONDS.sleep(4);
        return 1024;
    }

}
public class CallableDemo {

    public static void main(String[] args) throws ExecutionException, InterruptedException {
        System.out.println(Thread.currentThread().getName()+&quot;准备上课&quot;);

        FutureTask&amp;lt;Integer&amp;gt; futureTask = new FutureTask&amp;lt;&amp;gt;(new MyThread());
        FutureTask&amp;lt;Integer&amp;gt; futureTaskB = new FutureTask&amp;lt;&amp;gt;(new MyThread());
        new Thread(futureTask,&quot;A线程&quot;).start();

        System.out.println(Thread.currentThread().getName()+&quot;直接上课&quot;);

        System.out.println(futureTask.get());
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;/blockquote&gt;
&lt;h3&gt;代码&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;import java.util.concurrent.Callable;
import java.util.concurrent.FutureTask;
import java.util.concurrent.TimeUnit;

class MyThread implements Runnable{

    @Override
    public void run() {

    }
}
class MyThread2 implements Callable&amp;lt;Integer&amp;gt;{

    @Override
    public Integer call() throws Exception {
        System.out.println(Thread.currentThread().getName()+&quot;come in callable&quot;);
        return 200;
    }
}


public class CallableDemo {


    public static void main(String[] args) throws Exception {

        //FutureTask&amp;lt;Integer&amp;gt; futureTask = new FutureTask(new MyThread2());
        FutureTask&amp;lt;Integer&amp;gt; futureTask = new FutureTask(()-&amp;gt;{
            System.out.println(Thread.currentThread().getName()+&quot;  come in callable&quot;);
            TimeUnit.SECONDS.sleep(4);
            return 1024;
        });
        FutureTask&amp;lt;Integer&amp;gt; futureTask2 = new FutureTask(()-&amp;gt;{
            System.out.println(Thread.currentThread().getName()+&quot;  come in callable&quot;);
            TimeUnit.SECONDS.sleep(4);
            return 2048;
        });

        new Thread(futureTask,&quot;zhang3&quot;).start();
        new Thread(futureTask2,&quot;li4&quot;).start();

        //System.out.println(futureTask.get());
        //System.out.println(futureTask2.get());
        //1、一般放在程序后面，直接获取结果
        //2、只会计算结果一次

        while(!futureTask.isDone()){
            System.out.println(&quot;***wait&quot;);
        }
        System.out.println(futureTask.get());
        System.out.println(Thread.currentThread().getName()+&quot; come over&quot;);
    }
}
 
/**
 * 
 * 
在主线程中需要执行比较耗时的操作时，但又不想阻塞主线程时，可以把这些作业交给Future对象在后台完成，
当主线程将来需要时，就可以通过Future对象获得后台作业的计算结果或者执行状态。
 
一般FutureTask多用于耗时的计算，主线程可以在完成自己的任务后，再去获取结果。
 
仅在计算完成时才能检索结果；如果计算尚未完成，则阻塞 get 方法。一旦计算完成，
就不能再重新开始或取消计算。get方法而获取结果只有在计算完成时获取，否则会一直阻塞直到任务转入完成状态，
然后会返回结果或者抛出异常。 
 
**只计算一次**
get方法放到最后
 */
&lt;/code&gt;&lt;/pre&gt;
&lt;h1&gt;9、JUC强大的辅助类讲解&lt;/h1&gt;
&lt;h2&gt;CountDownLatch(减法)&lt;/h2&gt;
&lt;blockquote&gt;
&lt;p&gt;减少计数   控制线程顺序&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h3&gt;例子：CountDownLatchDemo&lt;/h3&gt;
&lt;h3&gt;原理&lt;/h3&gt;
&lt;blockquote&gt;
&lt;ul&gt;
&lt;li&gt;CountDownLatch主要有两个方法，当一个或多个线程调用await方法时，这些线程会阻塞。&lt;/li&gt;
&lt;li&gt;其它线程调用countDown方法会将计数器减1(调用countDown方法的线程不会阻塞)，&lt;/li&gt;
&lt;li&gt;当计数器的值变为0时，因await方法阻塞的线程会被唤醒，继续执行。&lt;/li&gt;
&lt;/ul&gt;
&lt;/blockquote&gt;
&lt;h3&gt;代码&lt;/h3&gt;
&lt;blockquote&gt;
&lt;pre&gt;&lt;code&gt;package com.atguigu.thread;
 
import java.util.concurrent.CountDownLatch;
 
 
/**
 * 
 * @Description:
 *  *让一些线程阻塞直到另一些线程完成一系列操作后才被唤醒。
 * 
 * CountDownLatch主要有两个方法，当一个或多个线程调用await方法时，这些线程会阻塞。
 * 其它线程调用countDown方法会将计数器减1(调用countDown方法的线程不会阻塞)，
 * 当计数器的值变为0时，因await方法阻塞的线程会被唤醒，继续执行。
 * 
 * 解释：6个同学陆续离开教室后值班同学才可以关门。
 * 
 * main主线程必须要等前面6个线程完成全部工作后，自己才能开干 
 */
public class CountDownLatchDemo
{
   public static void main(String[] args) throws InterruptedException
   {
         CountDownLatch countDownLatch = new CountDownLatch(6);
       
       for (int i = 1; i &amp;lt;=6; i++) //6个上自习的同学，各自离开教室的时间不一致
       {
          new Thread(() -&amp;gt; {
              System.out.println(Thread.currentThread().getName()+&quot;\t 号同学离开教室&quot;);
              countDownLatch.countDown();
          }, String.valueOf(i)).start();
       }
       countDownLatch.await();
       System.out.println(Thread.currentThread().getName()+&quot;\t****** 班长关门走人，main线程是班长&quot;);
          
   }
 
}
&lt;/code&gt;&lt;/pre&gt;
&lt;/blockquote&gt;
&lt;p&gt;如果不使用countdownLatch的传统写法&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;import java.util.concurrent.atomic.AtomicInteger;
import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;

public class LockDemo {
    public static void main(String[] args) throws InterruptedException {
        Lock lock = new ReentrantLock();
        Condition condition = lock.newCondition();
        AtomicInteger num = new AtomicInteger(6);
        for (int i = 1; i &amp;lt;= num.get(); i++) {
            new Thread(() -&amp;gt; {
                lock.lock();
                try {
                    // 1 判断  这里判断在这代码中可以省略
                    while (num.get() == 0) {
                        // A 就要停止
                        condition.await();
                    }
                    System.out.println(Thread.currentThread().getName() + &quot;\t 号同学离开教室&quot;);
                    num.getAndDecrement();
                    condition.signal();//一定需要通知
                } catch (InterruptedException e) {
                    throw new RuntimeException(e);
                } finally {
                    lock.unlock();
                }
            }, String.valueOf(i)).start();
        }

        lock.lock();
        try {
            // 1 判断
            while (num.get() != 0) {
                // A 就要停止
                condition.await();
            }
            // 2 干活
            System.out.println(Thread.currentThread().getName() + &quot;\t****** 班长关门走人，main线程是班长&quot;);
            // 3 通知
            condition.signal();
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            lock.unlock();
        }
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;CyclicBarrier（加法）&lt;/h2&gt;
&lt;blockquote&gt;
&lt;p&gt;循环栅栏    做加法，集齐七颗龙珠召唤神龙&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h3&gt;例子：CyclicBarrierDemo&lt;/h3&gt;
&lt;h3&gt;原理&lt;/h3&gt;
&lt;blockquote&gt;
&lt;ul&gt;
&lt;li&gt;CyclicBarrier&lt;/li&gt;
&lt;li&gt;的字面意思是可循环（Cyclic）使用的屏障（Barrier）。它要做的事情是，&lt;/li&gt;
&lt;li&gt;让一组线程到达一个屏障（也可以叫同步点）时被阻塞，&lt;/li&gt;
&lt;li&gt;直到最后一个线程到达屏障时，屏障才会开门，所有&lt;/li&gt;
&lt;li&gt;被屏障拦截的线程才会继续干活。&lt;/li&gt;
&lt;li&gt;线程进入屏障通过CyclicBarrier的await()方法。&lt;/li&gt;
&lt;/ul&gt;
&lt;/blockquote&gt;
&lt;h3&gt;代码&lt;/h3&gt;
&lt;blockquote&gt;
&lt;pre&gt;&lt;code&gt;package com.atguigu.thread;
 
import java.util.concurrent.BrokenBarrierException;
import java.util.concurrent.CyclicBarrier;
 
/**
 * 
 *
 * CyclicBarrier
 * 的字面意思是可循环（Cyclic）使用的屏障（Barrier）。它要做的事情是，
 * 让一组线程到达一个屏障（也可以叫同步点）时被阻塞，
 * 直到最后一个线程到达屏障时，屏障才会开门，所有
 * 被屏障拦截的线程才会继续干活。
 * 线程进入屏障通过CyclicBarrier的await()方法。
 * 
 * 集齐7颗龙珠就可以召唤神龙
 */
public class CyclicBarrierDemo
{
  private static final int NUMBER = 7;
  
  public static void main(String[] args)
  {
     //CyclicBarrier(int parties, Runnable barrierAction) 
     
     CyclicBarrier cyclicBarrier = new CyclicBarrier(NUMBER, ()-&amp;gt;{System.out.println(&quot;*****集齐7颗龙珠就可以召唤神龙&quot;);}) ;
     
     for (int i = 1; i &amp;lt;= 7; i++) {
       new Thread(() -&amp;gt; {
          try {
            System.out.println(Thread.currentThread().getName()+&quot;\t 星龙珠被收集 &quot;);
            cyclicBarrier.await();
          } catch (InterruptedException | BrokenBarrierException e) {
            // TODO Auto-generated catch block
            e.printStackTrace();
          }
       
       }, String.valueOf(i)).start();
     }
     
 
  }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;/blockquote&gt;
&lt;h2&gt;Semaphore（控制多线程的并发数）&lt;/h2&gt;
&lt;blockquote&gt;
&lt;p&gt;信号灯   争车位（控制多线程的并发数）&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h3&gt;例子：SemaphoreDemo&lt;/h3&gt;
&lt;h3&gt;原理&lt;/h3&gt;
&lt;blockquote&gt;
&lt;p&gt;在信号量上我们定义两种操作：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;acquire（获取） 当一个线程调用acquire操作时，它要么通过成功获取信号量（信号量减1），&lt;/li&gt;
&lt;li&gt;
&lt;pre&gt;&lt;code&gt;  要么一直等下去，直到有线程释放信号量，或超时。
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;release（释放）实际上会将信号量的值加1，然后唤醒等待的线程。&lt;/li&gt;
&lt;li&gt;&lt;/li&gt;
&lt;li&gt;信号量主要用于两个目的，一个是用于多个共享资源的互斥使用，另一个用于并发线程数的控制。&lt;/li&gt;
&lt;/ul&gt;
&lt;/blockquote&gt;
&lt;h3&gt;代码&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;package com.atguigu.thread;
 
import java.util.Random;
import java.util.concurrent.Semaphore;
import java.util.concurrent.TimeUnit;
 
/**
 * 
 * @Description: TODO(这里用一句话描述这个类的作用)  
 * 
 * 在信号量上我们定义两种操作：
 * acquire（获取） 当一个线程调用acquire操作时，它要么通过成功获取信号量（信号量减1），
 *             要么一直等下去，直到有线程释放信号量，或超时。
 * release（释放）实际上会将信号量的值加1，然后唤醒等待的线程。
 * 
 * 信号量主要用于两个目的，一个是用于多个共享资源的互斥使用，另一个用于并发线程数的控制。
 */
public class SemaphoreDemo
{
  public static void main(String[] args)
  {
     Semaphore semaphore = new Semaphore(3);//模拟3个停车位
     
     for (int i = 1; i &amp;lt;=6; i++) //模拟6部汽车
     {
       new Thread(() -&amp;gt; {
          try 
          {
            semaphore.acquire();
            System.out.println(Thread.currentThread().getName()+&quot;\t 抢到了车位&quot;);
            TimeUnit.SECONDS.sleep(new Random().nextInt(5));
            System.out.println(Thread.currentThread().getName()+&quot;\t------- 离开&quot;);
          } catch (InterruptedException e) {
            e.printStackTrace();
          }finally {
            semaphore.release();
          }
       }, String.valueOf(i)).start();
     }
     
  }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h1&gt;10、ReentrantReadWriteLock&lt;/h1&gt;
&lt;blockquote&gt;
&lt;p&gt;读写锁&lt;/p&gt;
&lt;p&gt;口诀：读读可共享，写读写写要独占&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2&gt;例子：ReadWriteLockDemo&lt;/h2&gt;
&lt;h2&gt;类似案例&lt;/h2&gt;
&lt;h3&gt;红蜘蛛&lt;/h3&gt;
&lt;h3&gt;缓存&lt;/h3&gt;
&lt;h2&gt;问题例子&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;import java.util.HashMap;
import java.util.Map;
import java.util.concurrent.TimeUnit;

/*
多个线程同时读一个资源类没有任何问题，所以为了满足并发量，读取共享资源应该可以同时进行
但是
如果有一个线程想去写共享资源来，就不应该再有其他线程可以对该资源镜像读或写
小总结：
   读-读 能共存
   读-写 不能共存
   写-写 不能共存
*/
class MyCache{
    private volatile Map&amp;lt;String,Object&amp;gt; map = new HashMap&amp;lt;&amp;gt;();
    public void put(String key,Object value){
        System.out.println(Thread.currentThread().getName()+&quot;\t 正在写&quot;+key);
        //暂停一会儿线程
        try {TimeUnit.MILLISECONDS.sleep(300);} catch (InterruptedException e) {e.printStackTrace(); }
        map.put(key,value);
        System.out.println(Thread.currentThread().getName()+&quot;\t 写完了&quot;+key);
    }

    public Object get(String key){
        Object result = null;
        System.out.println(Thread.currentThread().getName()+&quot;\t 正在读&quot;+key);
        try {TimeUnit.MILLISECONDS.sleep(300);} catch (InterruptedException e) {e.printStackTrace(); }
        result = map.get(key);
        System.out.println(Thread.currentThread().getName()+&quot;\t 读完了&quot;+result);
        return result;
    }
}
public class ReadWriteLockDemo {


    public static void main(String[] args) {
        MyCache myCache = new MyCache();

        for (int i = 1; i &amp;lt;= 5; i++) {
            final int num = i;
            new Thread(()-&amp;gt;{
                myCache.put(num+&quot;&quot;,num+&quot;&quot;);
            },String.valueOf(i)).start();
        }
        for (int i = 1; i &amp;lt;= 5; i++) {
            final int num = i;
            new Thread(()-&amp;gt;{
                myCache.get(num+&quot;&quot;);
            },String.valueOf(i)).start();
        }

    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;代码&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;import java.util.HashMap;
import java.util.Map;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.locks.ReadWriteLock;
import java.util.concurrent.locks.ReentrantReadWriteLock;

class MyCache {
    private volatile Map&amp;lt;String, Object&amp;gt; map = new HashMap&amp;lt;&amp;gt;();
    private ReadWriteLock rwLock = new ReentrantReadWriteLock();

    public void put(String key, Object value) {
        rwLock.writeLock().lock();
        try {
            System.out.println(Thread.currentThread().getName() + &quot;\t 正在写&quot; + key);
            //暂停一会儿线程
            try {
                TimeUnit.MILLISECONDS.sleep(300);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            map.put(key, value);
            System.out.println(Thread.currentThread().getName() + &quot;\t 写完了&quot; + key);
            System.out.println();
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            rwLock.writeLock().unlock();
        }

    }

    public Object get(String key) {
        rwLock.readLock().lock();
        Object result = null;
        try {
            System.out.println(Thread.currentThread().getName() + &quot;\t 正在读&quot; + key);
            try {
                TimeUnit.MILLISECONDS.sleep(300);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            result = map.get(key);
            System.out.println(Thread.currentThread().getName() + &quot;\t 读完了&quot; + result);
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            rwLock.readLock().unlock();
        }
        return result;
    }
}

public class ReadWriteLockDemo {

    public static void main(String[] args) {
        MyCache myCache = new MyCache();

        for (int i = 1; i &amp;lt;= 5; i++) {
            final int num = i;
            new Thread(() -&amp;gt; {
                myCache.put(num + &quot;&quot;, num + &quot;&quot;);
            }, String.valueOf(i)).start();
        }
        for (int i = 1; i &amp;lt;= 5; i++) {
            final int num = i;
            new Thread(() -&amp;gt; {
                myCache.get(num + &quot;&quot;);
            }, String.valueOf(i)).start();
        }

    }

}
&lt;/code&gt;&lt;/pre&gt;
&lt;h1&gt;11、BlockingQueueDemo&lt;/h1&gt;
&lt;blockquote&gt;
&lt;p&gt;阻塞队列&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2&gt;例子：BlockingQueueDemo&lt;/h2&gt;
&lt;h2&gt;栈与队列&lt;/h2&gt;
&lt;p&gt;栈：先进后出，后进先出&lt;/p&gt;
&lt;p&gt;队列：先进先出&lt;/p&gt;
&lt;h2&gt;阻塞队列&lt;/h2&gt;
&lt;p&gt;阻塞：必须要阻塞/不得不阻塞
阻塞队列是一个队列，在数据结构中起的作用如下图：&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://tangtangit.com/_astro/image-20210617150833920.DMXPbioi_JFzF1.webp&quot; alt=&quot;image-20210617150833920&quot; /&gt;&lt;/p&gt;
&lt;p&gt;线程1往阻塞队列里添加元素，线程2从阻塞队列里移除元素&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;当队列是空的，从队列中获取元素的操作将会被阻塞&lt;/strong&gt;
&lt;strong&gt;当队列是满的，从队列中添加元素的操作将会被阻塞&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;试图从空的队列中获取元素的线程将会被阻塞，直到其他线程往空的队列插入新的元素&lt;/p&gt;
&lt;h2&gt;阻塞队列的用处&lt;/h2&gt;
&lt;blockquote&gt;
&lt;p&gt;在多线程领域：所谓阻塞，在某些情况下会挂起线程（即阻塞），一旦条件满足，被挂起的线程又会自动被唤起&lt;/p&gt;
&lt;p&gt;为什么需要BlockingQueue
好处是我们不需要关心什么时候需要阻塞线程，什么时候需要唤醒线程，因为这一切BlockingQueue都给你一手包办了&lt;/p&gt;
&lt;p&gt;在concurrent包发布以前，在多线程环境下，我们每个程序员都必须去自己控制这些细节，尤其还要兼顾效率和线程安全，而这会给我们的程序带来不小的复杂度。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2&gt;架构梳理、种类分析&lt;/h2&gt;
&lt;h3&gt;架构介绍&lt;/h3&gt;
&lt;p&gt;&lt;img src=&quot;https://tangtangit.com/_astro/image-20210617150953505.C1G4TqDJ_Z3bBs.webp&quot; alt=&quot;image-20210617150953505&quot; /&gt;&lt;/p&gt;
&lt;h3&gt;种类分析&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;ArrayBlockingQueue：由数组结构组成的有界阻塞队列。（重要）&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;LinkedBlockingQueue：由链表结构组成的有界（但大小默认值为integer.MAX_VALUE）阻塞队列。（重要）&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;PriorityBlockingQueue：支持优先级排序的无界阻塞队列。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;DelayQueue：使用优先级队列实现的延迟无界阻塞队列。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;SynchronousQueue：不存储元素的阻塞队列，也即单个元素的队列。(重要)&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;LinkedTransferQueue：由链表组成的无界阻塞队列。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;LinkedBlockingDeque：由链表组成的双向阻塞队列。&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;BlockingQueue核心方法&lt;/h2&gt;
&lt;p&gt;&lt;img src=&quot;https://tangtangit.com/_astro/image-20210617151713123.NtKYUznQ_1wE1Q0.webp&quot; alt=&quot;image-20210617151713123&quot; /&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;抛出异常&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;当阻塞队列满时，再往队列里add插入元素会抛IllegalStateException:Queue full
当阻塞队列空时，再往队列里remove移除元素会抛NoSuchElementException&lt;/p&gt;
&lt;/blockquote&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;特殊值&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;插入方法，成功ture失败false&lt;/p&gt;
&lt;p&gt;移除方法，成功返回出队列的元素，队列里没有就返回null&lt;/p&gt;
&lt;/blockquote&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;一直阻塞&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;当阻塞队列满时，生产者线程继续往队列里put元素，队列会一直阻塞生产者线程直到put数据or响应中断退出
当阻塞队列空时，消费者线程试图从队列里take元素，队列会一直阻塞消费者线程直到队列可用&lt;/p&gt;
&lt;/blockquote&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;超时退出&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;当阻塞队列满时，队列会阻塞生产者线程一定时间，超过限时后生产者线程会退出&lt;/p&gt;
&lt;/blockquote&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;代码&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.ArrayBlockingQueue;
import java.util.concurrent.BlockingQueue;
import java.util.concurrent.TimeUnit;

/**
 * 阻塞队列
 */
public class BlockingQueueDemo {

    public static void main(String[] args) throws InterruptedException {

//        List list = new ArrayList();

        BlockingQueue&amp;lt;String&amp;gt; blockingQueue = new ArrayBlockingQueue&amp;lt;&amp;gt;(3);
        //第一组
//        System.out.println(blockingQueue.add(&quot;a&quot;));
//        System.out.println(blockingQueue.add(&quot;b&quot;));
//        System.out.println(blockingQueue.add(&quot;c&quot;));
//        System.out.println(blockingQueue.element());

        //System.out.println(blockingQueue.add(&quot;x&quot;));
//        System.out.println(blockingQueue.remove());
//        System.out.println(blockingQueue.remove());
//        System.out.println(blockingQueue.remove());
//        System.out.println(blockingQueue.remove());
//    第二组
//        System.out.println(blockingQueue.offer(&quot;a&quot;));
//        System.out.println(blockingQueue.offer(&quot;b&quot;));
//        System.out.println(blockingQueue.offer(&quot;c&quot;));
//        System.out.println(blockingQueue.offer(&quot;x&quot;));
//        System.out.println(blockingQueue.poll());
//        System.out.println(blockingQueue.poll());
//        System.out.println(blockingQueue.poll());
//        System.out.println(blockingQueue.poll());
//    第三组        
//         blockingQueue.put(&quot;a&quot;);
//         blockingQueue.put(&quot;b&quot;);
//         blockingQueue.put(&quot;c&quot;);
//         //blockingQueue.put(&quot;x&quot;);
//        System.out.println(blockingQueue.take());
//        System.out.println(blockingQueue.take());
//        System.out.println(blockingQueue.take());
//        System.out.println(blockingQueue.take());
        
//    第四组        
        System.out.println(blockingQueue.offer(&quot;a&quot;));
        System.out.println(blockingQueue.offer(&quot;b&quot;));
        System.out.println(blockingQueue.offer(&quot;c&quot;));
        System.out.println(blockingQueue.offer(&quot;a&quot;,3L, TimeUnit.SECONDS));

    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h1&gt;12、ThreadPool线程池&lt;/h1&gt;
&lt;h2&gt;例子：MyThreadPoolDemo&lt;/h2&gt;
&lt;h2&gt;为什么用线程池&lt;/h2&gt;
&lt;blockquote&gt;
&lt;p&gt;例子：
10年前单核CPU电脑，假的多线程，像马戏团小丑玩多个球，CPU需要来回切换。
现在是多核电脑，多个线程各自跑在独立的CPU上，不用切换效率高。&lt;/p&gt;
&lt;p&gt;线程池的优势：
线程池做的工作只要是控制运行的线程数量，处理过程中将任务放入队列，然后在线程创建后启动这些任务，如果线程数量超过了最大数量，超出数量的线程排队等候，等其他线程执行完毕，再从队列中取出任务来执行。&lt;/p&gt;
&lt;p&gt;它的主要特点为：线程复用;控制最大并发数;管理线程。&lt;/p&gt;
&lt;p&gt;第一资源：降低资源消耗。通过重复利用已创建的线程降低线程创建和销毁造成的销耗。
第二速度：提高响应速度。当任务到达时，任务可以不需要等待线程创建就能立即执行。
第三管理：提高线程的可管理性。线程是稀缺资源，如果无限制的创建，不仅会销耗系统资源，还会降低系统的稳定性，使用线程池可以进行统一的分配，调优和监控。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2&gt;线程池如何使用&lt;/h2&gt;
&lt;h3&gt;架构说明&lt;/h3&gt;
&lt;blockquote&gt;
&lt;p&gt;Java中的线程池是通过Executor框架实现的，该框架中用到了Executor，Executors，ExecutorService，ThreadPoolExecutor这几个类&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://tangtangit.com/_astro/image-20210617162548001.DbDQHmfd_Z1UGE82.webp&quot; alt=&quot;image-20210617162548001&quot; /&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h3&gt;编码实现&lt;/h3&gt;
&lt;p&gt;Executors.newFixedThreadPool(int)&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;public static ExecutorService newFixedThreadPool(int nThreads) {
    return new ThreadPoolExecutor(nThreads, nThreads,
                                  0L, TimeUnit.MILLISECONDS,
                                  new LinkedBlockingQueue&amp;lt;Runnable&amp;gt;());
}
 
//newFixedThreadPool创建的线程池corePoolSize和maximumPoolSize值是相等的，它使用的是LinkedBlockingQueue
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Executors.newSingleThreadExecutor()&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;public static ExecutorService newSingleThreadExecutor() {
    return new FinalizableDelegatedExecutorService
        (new ThreadPoolExecutor(1, 1,
                                0L, TimeUnit.MILLISECONDS,
                                new LinkedBlockingQueue&amp;lt;Runnable&amp;gt;()));
}
 
//newSingleThreadExecutor 创建的线程池corePoolSize和maximumPoolSize值都是1，它使用的是LinkedBlockingQueue
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Executors.newCachedThreadPool()&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;执行很多短期异步任务，线程池根据需要创建新线程，
但在先前构建的线程可用时将重用它们。可扩容，遇强则强&lt;/p&gt;
&lt;/blockquote&gt;
&lt;pre&gt;&lt;code&gt;public static ExecutorService newCachedThreadPool() {
    return new ThreadPoolExecutor(0, Integer.MAX_VALUE,
                                  60L, TimeUnit.SECONDS,
                                  new SynchronousQueue&amp;lt;Runnable&amp;gt;());
}
//newCachedThreadPool创建的线程池将corePoolSize设置为0，将maximumPoolSize设置为Integer.MAX_VALUE，它使用的是SynchronousQueue，
//也就是说来了任务就创建线程运行，当线程空闲超过60秒，就销毁线程。
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;ThreadPoolExecutor底层原理&lt;/h3&gt;
&lt;p&gt;&lt;img src=&quot;https://tangtangit.com/_astro/image-20210617162712752.InkaghPo_1rpBGU.webp&quot; alt=&quot;image-20210617162712752&quot; /&gt;&lt;/p&gt;
&lt;h2&gt;线程池几个重要参数&lt;/h2&gt;
&lt;h3&gt;7大参数&lt;/h3&gt;
&lt;blockquote&gt;
&lt;pre&gt;&lt;code&gt;public ThreadPoolExecutor(int corePoolSize,
                          int maximumPoolSize,
                          long keepAliveTime,
                          TimeUnit unit,
                          BlockingQueue&amp;lt;Runnable&amp;gt; workQueue,
                          ThreadFactory threadFactory,
                          RejectedExecutionHandler handler) {
    if (corePoolSize &amp;lt; 0 ||
        maximumPoolSize &amp;lt;= 0 ||
        maximumPoolSize &amp;lt; corePoolSize ||
        keepAliveTime &amp;lt; 0)
        throw new IllegalArgumentException();
    if (workQueue ** null || threadFactory ** null || handler == null)
        throw new NullPointerException();
    this.corePoolSize = corePoolSize;
    this.maximumPoolSize = maximumPoolSize;
    this.workQueue = workQueue;
    this.keepAliveTime = unit.toNanos(keepAliveTime);
    this.threadFactory = threadFactory;
    this.handler = handler;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;1、corePoolSize：线程池中的常驻核心线程数&lt;/li&gt;
&lt;li&gt;2、maximumPoolSize：线程池中能够容纳同时
执行的最大线程数，此值必须大于等于1&lt;/li&gt;
&lt;li&gt;3、keepAliveTime：多余的空闲线程的存活时间
当前池中线程数量超过corePoolSize时，当空闲时间
达到keepAliveTime时，多余线程会被销毁直到
只剩下corePoolSize个线程为止&lt;/li&gt;
&lt;li&gt;4、unit：keepAliveTime的单位&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;​     3、4连在一起的&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;5、workQueue：任务队列，被提交但尚未被执行的任务&lt;/li&gt;
&lt;li&gt;6、threadFactory：表示生成线程池中工作线程的线程工厂，
用于创建线程，&lt;strong&gt;一般默认的即可&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;7、handler：&lt;strong&gt;拒绝策略&lt;/strong&gt;，表示当队列满了，并且工作线程大于
等于线程池的最大线程数（maximumPoolSize）时如何来拒绝
请求执行的runnable的策略&lt;/li&gt;
&lt;/ul&gt;
&lt;/blockquote&gt;
&lt;h2&gt;线程池底层工作原理&lt;/h2&gt;
&lt;p&gt;&lt;img src=&quot;%E5%9B%BE%E5%83%8F.bmp&quot; alt=&quot;图像&quot; /&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://tangtangit.com/_astro/image-20210617162439186.VgYwUAGB_Z1B5zXv.webp&quot; alt=&quot;image-20210617162439186&quot; /&gt;&lt;/p&gt;
&lt;blockquote&gt;
&lt;pre&gt;&lt;code&gt;                                以下重要：以下重要：以下重要：以下重要：以下重要：以下重要：

                                * 1、在创建了线程池后，开始等待请求。
                                * 2、当调用execute()方法添加一个请求任务时，线程池会做出如下判断：
                                2.1如果正在运行的线程数量小于corePoolSize，那么马上创建线程运行这个任务；
                                2.2如果正在运行的线程数量大于或等于corePoolSize，那么将这个任务放入队列；
                                2.3如果这个时候队列满了且正在运行的线程数量还小于maximumPoolSize，那么还是要创建非核心线程立刻运行这个任务；
                                2.4如果队列满了且正在运行的线程数量大于或等于maximumPoolSize，那么线程池会启动饱和拒绝策略来执行。
                                * 3、当一个线程完成任务时，它会从队列中取下一个任务来执行。
                                * 4、当一个线程无事可做超过一定的时间（keepAliveTime）时，线程会判断：
                                如果当前运行的线程数大于corePoolSize，那么这个线程就被停掉。
                                所以线程池的所有任务完成后，它最终会收缩到corePoolSize的大小。
&lt;/code&gt;&lt;/pre&gt;
&lt;/blockquote&gt;
&lt;blockquote&gt;
&lt;p&gt;任务队列和最大线程数之间存在密切的关系，它们共同决定了线程池的行为和性能。任务队列是用来存放待执行的任务的缓冲区，最大线程数则决定了线程池能够容纳的最大线程数量。下面详细说明它们之间的前后关系：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;任务队列与核心线程数：&lt;/strong&gt; 首先，任务队列对核心线程数的影响。当有新任务提交给线程池时，如果线程池中的线程数量还未达到核心线程数（&lt;code&gt;corePoolSize&lt;/code&gt;），新任务会直接创建一个新的核心线程来执行任务，不会放入任务队列。这有助于快速响应任务，避免任务等待。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;任务队列与最大线程数：&lt;/strong&gt; 当线程池中的线程数量已达到核心线程数，而新任务继续提交时，这时任务就会被放入任务队列。当任务队列已满时，根据最大线程数（&lt;code&gt;maximumPoolSize&lt;/code&gt;）的设置，线程池的行为会有所不同：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;如果线程池中的线程数还未达到最大线程数，那么会创建新的非核心线程来执行任务，直到达到最大线程数。&lt;/li&gt;
&lt;li&gt;如果线程池中的线程数已经达到最大线程数，这时新提交的任务就会被拒绝执行，根据所设置的拒绝策略来处理。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;任务队列和最大线程数的关系影响了线程池的并发度和资源利用率：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;如果任务队列容量足够大，即使最大线程数设置较小，线程池也可以在有限的线程数下缓冲一定数量的任务，避免过多的线程创建和销毁，提高资源利用率。&lt;/li&gt;
&lt;li&gt;如果任务队列容量较小，且最大线程数设置较大，线程池可能在任务队列满了之后继续创建新的线程，以处理更多的任务，但同时可能会增加上下文切换开销。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;在实际应用中，合理配置任务队列和最大线程数非常重要，需要根据应用的性质、系统资源和负载情况来调整，以达到最佳的性能和资源利用率。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2&gt;线程池用哪个？生产中如设置合理参数&lt;/h2&gt;
&lt;h3&gt;线程池的拒绝策略&lt;/h3&gt;
&lt;h4&gt;是什么&lt;/h4&gt;
&lt;blockquote&gt;
&lt;ul&gt;
&lt;li&gt;条件一：等待队列已经排满了，再也塞不下新任务了
同时，&lt;/li&gt;
&lt;li&gt;条件二：线程池中的max线程也达到了，无法继续为新任务服务。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;这个是时候我们就需要拒绝策略机制合理的处理这个问题。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h4&gt;JDK内置的拒绝策略&lt;/h4&gt;
&lt;blockquote&gt;
&lt;ul&gt;
&lt;li&gt;AbortPolicy(默认)&lt;strong&gt;（中止策略）&lt;/strong&gt;：直接抛出RejectedExecutionException异常阻止系统正常运行&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;在拒绝策略中，&lt;code&gt;AbortPolicy&lt;/code&gt; 是默认的策略。这种策略的实际效果是，如果任务队列已满，而线程池的线程数量也达到了最大线程数限制，那么任何新提交的任务都会被拒绝，不会执行。同时，线程池会抛出一个 &lt;code&gt;RejectedExecutionException&lt;/code&gt; 异常，以通知调用者有任务被拒绝执行。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;CallerRunsPolicy**（回退调用者策略）**：“调用者运行”一种调节机制，该策略既不会抛弃任务，也不
会抛出异常，而是将某些任务回退到调用者，从而降低新任务的流量。(回退)&lt;/li&gt;
&lt;li&gt;DiscardOldestPolicy**（丢弃最旧策略）**：抛弃队列中等待最久的任务，然后把当前任务加人队列中
尝试再次提交当前任务。&lt;/li&gt;
&lt;li&gt;DiscardPolicy**（丢弃策略）**：该策略默默地丢弃无法处理的任务，不予任何处理也不抛出异常。
如果允许任务丢失，这是最好的一种策略。&lt;/li&gt;
&lt;/ul&gt;
&lt;/blockquote&gt;
&lt;h4&gt;以上内置拒绝策略均实现了RejectedExecutionHandle接口&lt;/h4&gt;
&lt;h3&gt;在工作中单一的/固定数的/可变的三种创建线程池的方法哪个用的多？超级大坑&lt;/h3&gt;
&lt;blockquote&gt;
&lt;p&gt;答案是一个都不用，我们工作中只能使用自定义的&lt;/p&gt;
&lt;p&gt;Executors中JDK已经给你提供了，为什么不用？&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://tangtangit.com/_astro/image-20210617171050341.Zo50s--e_Z2nQNt1.webp&quot; alt=&quot;image-20210617171050341&quot; /&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h3&gt;在工作中如何使用线程池，是否自定义过线程池&lt;/h3&gt;
&lt;blockquote&gt;
&lt;p&gt;cpu密集型的计算工作&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;cup核数+1；设置到maximumPoolSize上&lt;/p&gt;
&lt;p&gt;Runtime.getRuntime().availableProcessors(); //Java获取系统cpu核数&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;io密集型的计算工作&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;麻烦&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;代码&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;
import java.util.Arrays;
import java.util.List;
import java.util.concurrent.*;

/**
 * 线程池
 * Arrays
 * Collections
 * Executors
 */
public class MyThreadPoolDemo {

    public static void main(String[] args) {
        ExecutorService threadPool = new ThreadPoolExecutor(
                2,
                5,
                2L,
                TimeUnit.SECONDS,
                new ArrayBlockingQueue&amp;lt;Runnable&amp;gt;(3),
                Executors.defaultThreadFactory(),
                //new ThreadPoolExecutor.AbortPolicy()
                //new ThreadPoolExecutor.CallerRunsPolicy()
                //new ThreadPoolExecutor.DiscardOldestPolicy()
                new ThreadPoolExecutor.DiscardOldestPolicy()
        );
        //10个顾客请求
        try {
            for (int i = 1; i &amp;lt;= 10; i++) {
                threadPool.execute(() -&amp;gt; {
                    System.out.println(Thread.currentThread().getName() + &quot;\t 办理业务&quot;);
                });
            }
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            threadPool.shutdown();
        }

    }

    private static void threadPool() {
        //List list = new ArrayList();
        //List list = Arrays.asList(&quot;a&quot;,&quot;b&quot;);
        //固定数的线程池，一池五线程

//       ExecutorService threadPool =  Executors.newFixedThreadPool(5); //一个银行网点，5个受理业务的窗口
//       ExecutorService threadPool =  Executors.newSingleThreadExecutor(); //一个银行网点，1个受理业务的窗口
        ExecutorService threadPool = Executors.newCachedThreadPool(); //一个银行网点，可扩展受理业务的窗口

        //10个顾客请求
        try {
            for (int i = 1; i &amp;lt;= 10; i++) {
                threadPool.execute(() -&amp;gt; {
                    System.out.println(Thread.currentThread().getName() + &quot;\t 办理业务&quot;);
                });
            }
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            threadPool.shutdown();
        }
    }
}
 

&lt;/code&gt;&lt;/pre&gt;
&lt;/blockquote&gt;
&lt;h2&gt;线程池实际使用&lt;/h2&gt;
&lt;blockquote&gt;
&lt;p&gt;1、当线程池小于corePoolSize时，新提交任务将创建一个新线程执行任务，即使此时线程池中存在空闲线程。
2、当线程池达到corePoolSize时，新提交任务将被放入workQueue中，等待线程池中任务调度执行
3、当workQueue已满，且maximumPoolSize&amp;gt;corePoolSize时，新提交任务会创建新线程执行任务
4、当提交任务数超过maximumPoolSize时，新提交任务由RejectedExecutionHandler处理
5、当线程池中超过corePoolSize线程，空闲时间达到keepAliveTime时，关闭空闲线程
6、当设置allowCoreThreadTimeOut(true)时，线程池中corePoolSize线程空闲时间达到keepAliveTime也将关闭&lt;/p&gt;
&lt;p&gt;可以看出，allowCoreThreadTimeOut这个方法就像其字面的意思一样，允许Core Thread超时后可以关闭。&lt;/p&gt;
&lt;p&gt;书上说了，要想使线程池没有任务时销毁所有的进程，需要启用allowCoreThreadTimeOut(true)同时将core size设置为0，而实际上，core size设置成任意一个正数值就可以，设置成0时，加不加allowCoreThreadTimeOut(true)都没有影响，因为这个方法是对core thread产生影响，但此时core thread为0，而且当新任务进来时，必须等到workQueue满时才会创建新线程，这也不是我们想要的结果。&lt;/p&gt;
&lt;p&gt;结论：allowCoreThreadTimeOut 保持默认设置&lt;/p&gt;
&lt;/blockquote&gt;
&lt;ul&gt;
&lt;li&gt;创建线程池对象&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor;

@Bean
public ThreadPoolTaskExecutor getThreadPool() {

    ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();

    //必须根据线上核心数来设置，例如2核，必须写2，否则报错
    //executor.setCorePoolSize(4); // 不推荐写法
    
 
    /**
    以下为推荐写法 
    *Runtime.getRuntime().availableProcessors()返回的是可用的计算资源，而不是CPU物理核心数，
    *对于支持超线程的CPU来说，单个物理处理器相当于拥有两个逻辑处理器，能够同时执行两个线程。
    */
    int processors = Runtime.getRuntime().availableProcessors();
    executor.setCorePoolSize(processors);

    //cup核数+1；设置到maximumPoolSize上，最大核心线程池的线程数必须大于核心线程池线程数，否则非法参数异常
    executor.setMaxPoolSize(processors+1);

    executor.setQueueCapacity(100);

    executor.setKeepAliveSeconds(60);

    executor.setThreadNamePrefix(&quot;Pool-A&quot;);

    executor.setRejectedExecutionHandler(new ThreadPoolExecutor.CallerRunsPolicy());

    executor.initialize();

    return executor;

}
&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;使用线程池&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor;

@Autowired
private ThreadPoolTaskExecutor executorService;

executorService.submit(new Runnable() {
    @Override
    public void run() {
        try {
            SendResult sendResult = sendMessage(topic, tag, finalTradePay.getPayId(), JSON.toJSONString(finalTradePay));
            log.info(JSON.toJSONString(sendResult));
            if (SendStatus.SEND_OK.equals(sendResult.getSendStatus())) {
                mqProducerTempMapper.deleteByPrimaryKey(mqProducerTemp.getId());
                System.out.println(&quot;删除消息表成功&quot;);
            }
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
});
&lt;/code&gt;&lt;/pre&gt;
&lt;h1&gt;13、Java8之流式计算复习&lt;/h1&gt;
&lt;h2&gt;例子：StreamDemo&lt;/h2&gt;
&lt;h2&gt;函数式接口&lt;/h2&gt;
&lt;h3&gt;java.util.function&lt;/h3&gt;
&lt;p&gt;&lt;img src=&quot;https://tangtangit.com/_astro/image-20210617155932383.CtsTWgPb_1d70Dl.webp&quot; alt=&quot;image-20210617155932383&quot; /&gt;&lt;/p&gt;
&lt;h3&gt;java内置核心四大函数式接口&lt;/h3&gt;
&lt;p&gt;&lt;img src=&quot;https://tangtangit.com/_astro/image-20210617155941135.ChUxEZ-W_ZKnxVK.webp&quot; alt=&quot;image-20210617155941135&quot; /&gt;&lt;/p&gt;
&lt;h3&gt;实例&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;//R apply(T t);函数型接口，一个参数，一个返回值
Function&amp;lt;String,Integer&amp;gt; function = t -&amp;gt;{return t.length();};
System.out.println(function.apply(&quot;abcd&quot;));

//boolean test(T t);断定型接口，一个参数，返回boolean
Predicate&amp;lt;String&amp;gt; predicate = t-&amp;gt;{return t.startsWith(&quot;a&quot;);};
System.out.println(predicate.test(&quot;a&quot;));

// void accept(T t);消费型接口，一个参数，没有返回值
Consumer&amp;lt;String&amp;gt; consumer = t-&amp;gt;{
    System.out.println(t);
};
consumer.accept(&quot;javaXXXX&quot;);

//T get(); 供给型接口，无参数，有返回值
Supplier&amp;lt;String&amp;gt; supplier =()-&amp;gt;{return UUID.randomUUID().toString();};
System.out.println(supplier.get());
 
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Stream流&lt;/h2&gt;
&lt;h3&gt;What&lt;/h3&gt;
&lt;blockquote&gt;
&lt;p&gt;流(Stream) 到底是什么呢？
是数据渠道，用于操作数据源（集合、数组等）所生成的元素序列。
“&lt;strong&gt;集合讲的是数据，流讲的是计算&lt;/strong&gt;！”&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h3&gt;Why&lt;/h3&gt;
&lt;p&gt;特点&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;Stream 自己不会存储元素&lt;/p&gt;
&lt;p&gt;Stream 不会改变源对象。相反，
他们会返回一个持有结果的新Stream。&lt;/p&gt;
&lt;p&gt;Stream 操作是延迟执行的。这意味着
他们会等到需要结果的时候才执行。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h3&gt;How&lt;/h3&gt;
&lt;p&gt;阶段&lt;/p&gt;
&lt;blockquote&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;创建一个Stream：一个数据源（数组、集合）&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;中间操作：一个中间操作，处理数据源数据&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;终止操作：一个终止操作，执行中间操作链，产生结果&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/blockquote&gt;
&lt;p&gt;源头=&amp;gt;中间流水线=&amp;gt;结果&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;package com.atguigu.juc.study;

import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;

import java.util.Arrays;
import java.util.List;
import java.util.UUID;
import java.util.function.*;

@Data
@NoArgsConstructor
@AllArgsConstructor
class User
{
    private Integer id;
    private String  userName;
    private int     age;
}

/**
 * @create 2019-02-26 22:24
 *
 * 题目：请按照给出数据，找出同时满足
 *      偶数ID且年龄大于24且用户名转为大写且用户名字母倒排序
 *      最后只输出一个用户名字
 */
public class StreamDemo
{
    public static void main(String[] args)
    {
        User u1 = new User(11,&quot;a&quot;,23);
        User u2 = new User(12,&quot;b&quot;,24);
        User u3 = new User(13,&quot;c&quot;,22);
        User u4 = new User(14,&quot;d&quot;,28);
        User u5 = new User(16,&quot;e&quot;,26);

        List&amp;lt;User&amp;gt; list = Arrays.asList(u1,u2,u3,u4,u5);

        list.stream().filter(p -&amp;gt; {
            return p.getId() % 2 == 0;
        }).filter(p -&amp;gt; {
            return p.getAge() &amp;gt; 24;
        }).map(f -&amp;gt; {
            return f.getUserName().toUpperCase();
        }).sorted((o1, o2) -&amp;gt; {
            return o2.compareTo(o1);
        }).limit(1).forEach(System.out::println);


        //    R apply(T t);
        Function&amp;lt;String,Integer&amp;gt; function = t -&amp;gt; {return t.length();};
        System.out.println(function.apply(&quot;abc&quot;));

        // boolean test(T t);
        Predicate&amp;lt;String&amp;gt; predicate = t -&amp;gt; {return t.startsWith(&quot;a&quot;);};
        System.out.println(predicate.test(&quot;a&quot;));

        //void accept(T t);
        Consumer&amp;lt;String&amp;gt; consumer = t -&amp;gt; {System.out.println(t);};
        consumer.accept(&quot;java1018&quot;);


        //    T get();
        Supplier&amp;lt;String&amp;gt; supplier =  () -&amp;gt; {return UUID.randomUUID().toString();};
        System.out.println(supplier.get());;

    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h1&gt;&lt;strong&gt;=&lt;strong&gt;&lt;strong&gt;=&lt;/strong&gt;&lt;/strong&gt;=&lt;strong&gt;&lt;strong&gt;=&lt;/strong&gt;&lt;/strong&gt;=&lt;strong&gt;&lt;strong&gt;=&lt;/strong&gt;&lt;/strong&gt;=&lt;strong&gt;&lt;strong&gt;=&lt;/strong&gt;&lt;/strong&gt;=&lt;strong&gt;&lt;strong&gt;=&lt;/strong&gt;&lt;/strong&gt;=&lt;strong&gt;&lt;strong&gt;=&lt;/strong&gt;&lt;/strong&gt;=&lt;strong&gt;&lt;strong&gt;=&lt;/strong&gt;&lt;/strong&gt;=&lt;strong&gt;&lt;strong&gt;=&lt;/strong&gt;&lt;/strong&gt;=&lt;strong&gt;&lt;strong&gt;=&lt;/strong&gt;&lt;/strong&gt;=&lt;strong&gt;&lt;strong&gt;=&lt;/strong&gt;&lt;/strong&gt;=&lt;strong&gt;&lt;strong&gt;=&lt;/strong&gt;&lt;/strong&gt;=&lt;strong&gt;&lt;strong&gt;=&lt;/strong&gt;&lt;/strong&gt;=&lt;strong&gt;&lt;strong&gt;=&lt;/strong&gt;&lt;/strong&gt;=&lt;strong&gt;&lt;strong&gt;=&lt;/strong&gt;&lt;/strong&gt;=&lt;strong&gt;&lt;strong&gt;=&lt;/strong&gt;&lt;/strong&gt;=&lt;strong&gt;&lt;strong&gt;=&lt;/strong&gt;&lt;/strong&gt;=&lt;strong&gt;&lt;strong&gt;=&lt;/strong&gt;&lt;/strong&gt;=&lt;strong&gt;&lt;strong&gt;=&lt;/strong&gt;&lt;/strong&gt;=&lt;strong&gt;&lt;strong&gt;=&lt;/strong&gt;&lt;/strong&gt;=&lt;strong&gt;&lt;strong&gt;=&lt;/strong&gt;&lt;/strong&gt;=&lt;strong&gt;&lt;strong&gt;=&lt;/strong&gt;&lt;/strong&gt;=&lt;strong&gt;&lt;strong&gt;=&lt;/strong&gt;&lt;/strong&gt;=&lt;strong&gt;&lt;strong&gt;=&lt;/strong&gt;&lt;/strong&gt;=&lt;strong&gt;&lt;strong&gt;=&lt;/strong&gt;&lt;/strong&gt;=&lt;strong&gt;&lt;strong&gt;=&lt;/strong&gt;&lt;/strong&gt;=&lt;strong&gt;&lt;strong&gt;=&lt;/strong&gt;&lt;/strong&gt;=&lt;strong&gt;&lt;strong&gt;=&lt;/strong&gt;&lt;/strong&gt;=&lt;strong&gt;&lt;strong&gt;=&lt;/strong&gt;&lt;/strong&gt;=&lt;strong&gt;&lt;strong&gt;=&lt;/strong&gt;&lt;/strong&gt;=&lt;strong&gt;&lt;strong&gt;=&lt;/strong&gt;&lt;/strong&gt;=&lt;strong&gt;&lt;strong&gt;=&lt;/strong&gt;&lt;/strong&gt;=&lt;strong&gt;&lt;strong&gt;=&lt;/strong&gt;&lt;/strong&gt;=&lt;strong&gt;&lt;strong&gt;=&lt;/strong&gt;&lt;/strong&gt;=&lt;strong&gt;&lt;strong&gt;=&lt;/strong&gt;&lt;/strong&gt;=&lt;strong&gt;&lt;strong&gt;=&lt;/strong&gt;&lt;/strong&gt;=&lt;strong&gt;&lt;strong&gt;=&lt;/strong&gt;&lt;/strong&gt;=&lt;strong&gt;&lt;strong&gt;=&lt;/strong&gt;&lt;/strong&gt;=&lt;strong&gt;&lt;strong&gt;=&lt;/strong&gt;&lt;/strong&gt;=&lt;strong&gt;&lt;strong&gt;=&lt;/strong&gt;&lt;/strong&gt;=&lt;strong&gt;&lt;strong&gt;=&lt;/strong&gt;&lt;/strong&gt;=****=&lt;/strong&gt;====&lt;/h1&gt;
&lt;h1&gt;变量理解&lt;/h1&gt;
&lt;p&gt;1、类中的声明位置分：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;局部变量 ---必须人工赋初始化值&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;成员变量(或属性)&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;类变量（静态的）---会有默认值&lt;/p&gt;
&lt;p&gt;实例变量（非静态的）---会有默认值&lt;/p&gt;
&lt;/blockquote&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;2、数据类型分：基本数据变量(8种) vs 引用类型变量(类、数组、接口、字符串)&lt;/p&gt;
&lt;p&gt;成员变量和局部变量都可以是基本数据类型或引用类型，它们的存储位置和生命周期不同。&lt;/p&gt;
&lt;h1&gt;JVM&lt;/h1&gt;
&lt;blockquote&gt;
&lt;p&gt;常见面试：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;请谈谈你对JVM的理解?java8的虚拟机有什么更新？&lt;/li&gt;
&lt;li&gt;什么是OOM？什么是StackOverflowError?有哪些方法分析？&lt;/li&gt;
&lt;li&gt;JVM的常用参数调优你知道哪些？&lt;/li&gt;
&lt;li&gt;谈谈JVM中，对类加载器你的认识？&lt;/li&gt;
&lt;/ul&gt;
&lt;/blockquote&gt;
&lt;h1&gt;1、JVM体系结构概述&lt;/h1&gt;
&lt;h2&gt;1.1、JVM位置&lt;/h2&gt;
&lt;blockquote&gt;
&lt;p&gt;JVM是运行在操作系统之上的，它与硬件没有直接的交互&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;&lt;img src=&quot;https://tangtangit.com/_astro/image-20210618091536765.DKMQX9SH_Z1jGEyc.webp&quot; alt=&quot;image-20210618091536765&quot; /&gt;&lt;/p&gt;
&lt;h2&gt;1.2、jvm体系结构概览&lt;/h2&gt;
&lt;blockquote&gt;
&lt;p&gt;亮色：线程共享、存在垃圾回收&lt;/p&gt;
&lt;p&gt;灰色：与之相反&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;&lt;img src=&quot;https://tangtangit.com/_astro/image-20210618091611260.7fnSpvai_Z1VBqn1.webp&quot; alt=&quot;image-20210618091611260&quot; /&gt;&lt;/p&gt;
&lt;h2&gt;1.3、类装载器ClassLoader&lt;/h2&gt;
&lt;blockquote&gt;
&lt;p&gt;​	负责加载class文件，class文件在&lt;strong&gt;文件开头有特定的文件标示&lt;/strong&gt;（cafe.babe），将class文件字节码内容加载到内存中，并&lt;strong&gt;将这些内容转换成方法区中&lt;/strong&gt;的运行时数据结构并且ClassLoader只负责class文件的加载，至于它是否可以运行，则由Execution Engine决定 。&lt;/p&gt;
&lt;p&gt;echo %JAVA_HOME%&lt;/p&gt;
&lt;p&gt;echo %PATH%&lt;/p&gt;
&lt;p&gt;echo %CLASSPATH%&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://tangtangit.com/_astro/image-20210618091749202.CieqBT2v_87mAW.webp&quot; alt=&quot;image-20210618091749202&quot; /&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2&gt;1.4、类装载器ClassLoader2&lt;/h2&gt;
&lt;blockquote&gt;
&lt;p&gt;虚拟机自带的加载器&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;启动类加载器（Bootstrap）C++，（通常是由C/C++编写的，初始化JVM运行时环境所需的类。）&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;扩展类加载器（Extension）Java  (加载javax的扩展包)&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;应用程序类加载器（AppClassLoader）Java也叫（&lt;strong&gt;系统类加载器&lt;/strong&gt;），加载（当前应用的classpath的所有类）&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;用户自定义加载器  Java.lang.ClassLoader的子类，用户可以定制类的加载方式&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;启动-》扩展-》系统-》自定义&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;&amp;lt;!-- &amp;gt; &lt;img src=&quot;image-20210618092516897.png&quot; alt=&quot;image-20210618092516897&quot; /&gt; --&amp;gt;&lt;/p&gt;
&lt;blockquote&gt;
&lt;pre&gt;&lt;code&gt;public class MyObj {

    public static void main(String[] args) {
        Object o = new Object();//是系统自带（根加载器一开始就会加载jdk自带的类）
        //o.getClass() 获取到模板，o.getClass().getClassLoader() 获取到根加载器
        System.out.println(o.getClass().getClassLoader());//应该是根加载器（c++写的），打印null

        MyObj myObj = new MyObj();
        System.out.println(myObj.getClass().getClassLoader());//(应用程序类加载器)sun.misc.Launcher$AppClassLoader@14dad5dc

        System.out.println(myObj.getClass().getClassLoader().getParent().getParent());
        //null  根加载器

        System.out.println(myObj.getClass().getClassLoader().getParent());
        //sun.misc.Launcher$ExtClassLoader@3a71f4dd  扩展加载器

        System.out.println(myObj.getClass().getClassLoader());
        //sun.misc.Launcher$AppClassLoader@14dad5dc  应用程序加载器
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;/blockquote&gt;
&lt;h2&gt;1.5、类装载器ClassLoader3&lt;/h2&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;img src=&quot;https://tangtangit.com/_astro/image-20210618095259389.D25Vwqt6_ZJqYiL.webp&quot; alt=&quot;image-20210618095259389&quot; /&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;Code案例&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;sun.misc.Launcher
它是一个java虚拟机的入口应用&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/blockquote&gt;
&lt;h2&gt;1.6、双亲委派&lt;/h2&gt;
&lt;blockquote&gt;
&lt;p&gt;解释：当一个类收到了类加载请求，他首先不会尝试自己去加载这个类，而是把这个请求委派给父类去完成，每一个层次类加载器都是如此，因此所有的加载请求都应该传送到启动类加载其中，只有当父类加载器反馈自己无法完成这个请求的时候（在它的加载路径下没有找到所需加载的Class），子类加载器才会尝试自己去加载。&lt;/p&gt;
&lt;p&gt;好处：采用双亲委派的一个好处是比如加载位于 rt.jar 包中的类 java.lang.Object，**不管是哪个加载器加载这个类，最终都是委托给顶层的启动类加载器进行加载，这样就保证了使用不同的类加载器最终得到的都是同样一个 Object对象。 **&lt;/p&gt;
&lt;p&gt;理由：沙箱安全机制&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2&gt;1.7、Execution Engine执行引擎负责解释命令，提交操作系统执行。&lt;/h2&gt;
&lt;h2&gt;1.8、Native Interface本地接口&lt;/h2&gt;
&lt;blockquote&gt;
&lt;p&gt;​	本地接口的作用是融合不同的编程语言为 Java 所用，它的初衷是融合 C/C++程序，Java 诞生的时候是 C/C++横行的时候，要想立足，必须有调用 C/C++程序，于是就在内存中专门开辟了一块区域处理标记为native的代码，它的具体做法是 Native Method Stack中登记 native方法，在Execution Engine 执行时加载native libraies。
​	目前该方法使用的越来越少了，除非是与硬件有关的应用，比如通过Java程序驱动打印机或者Java系统管理生产设备，在企业级应用中已经比较少见。因为现在的异构领域间的通信很发达，比如可以使用 Socket通信，也可以使用Web Service等等，不多做介绍。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;本地操作系统&lt;/li&gt;
&lt;li&gt;第三方库&lt;/li&gt;
&lt;/ul&gt;
&lt;/blockquote&gt;
&lt;h2&gt;1.9、Native Method Stack本地方法栈&lt;/h2&gt;
&lt;blockquote&gt;
&lt;p&gt;它的具体做法是Native Method Stack中登记native方法，在Execution Engine 执行时加载本地方法库。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;与 Java 虚拟机栈类似，但用于执行 Native 方法（即使用本地语言如 C/C++ 编写的方法）。&lt;/li&gt;
&lt;li&gt;Native 方法可能涉及操作系统和硬件资源，因此需要单独的栈空间。&lt;/li&gt;
&lt;/ul&gt;
&lt;/blockquote&gt;
&lt;h2&gt;1.10   &lt;strong&gt;程序计数器（Program Counter Register）&lt;/strong&gt;&lt;/h2&gt;
&lt;blockquote&gt;
&lt;p&gt;一句话：就是一个指针，这个指令执行完成后，接下来执行什么&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;每个线程都有一个程序计数器，是线程私有的&lt;/strong&gt;，就是一个指针，指向方法区中的方法字节码（&lt;strong&gt;用来存储指向下一条指令的地址&lt;/strong&gt;,也即将要执行的指令代码），由执行引擎读取下一条指令，是一个非常小的内存空间，几乎可以忽略不记。&lt;/p&gt;
&lt;p&gt;这块内存区域很小，它是当前线程所执行的字节码的行号指示器，字节码解释器通过改变这个计数器的值来选取下一条需要执行的字节码指令。&lt;/p&gt;
&lt;p&gt;如果执行的是一个Native方法，那这个计数器是空的。&lt;/p&gt;
&lt;p&gt;用以完成分支、循环、跳转、异常处理、线程恢复等基础功能。不会发生内存溢出(OutOfMemory=OOM)错误&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;存放当前线程执行的字节码指令的地址。&lt;/li&gt;
&lt;li&gt;用于线程切换、分支、循环、异常处理等。&lt;/li&gt;
&lt;li&gt;不会出现内存溢出问题。&lt;/li&gt;
&lt;li&gt;每个线程独有，不会共享。&lt;/li&gt;
&lt;/ul&gt;
&lt;/blockquote&gt;
&lt;h2&gt;1.11   Method Area 方法区&lt;/h2&gt;
&lt;blockquote&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;供&lt;strong&gt;各线程共享&lt;/strong&gt;的运行时内存区域。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;它存储了&lt;strong&gt;每一个类的结构信息&lt;/strong&gt;（模板，大Class），例如运行时常量池（Runtime Constant Pool）、字段和方法数据、构造函数和普通方法的字节码内容。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;上面讲的是规范，在不同虚拟机里头实现是不一样的，最典型的就是永久代(PermGen space)和元空间(Metaspace)。&lt;/p&gt;
&lt;p&gt;eg：&lt;/p&gt;
&lt;p&gt;方法区  f = new  永久代；（Java1.7）&lt;/p&gt;
&lt;p&gt;方法区  f = new  元空间；  规范和具体实现（Java1.8后）&lt;/p&gt;
&lt;p&gt;But&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;实例变量&lt;/strong&gt;存在&lt;strong&gt;堆内存&lt;/strong&gt;中,和方法区无关&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;ul&gt;
&lt;li&gt;存放类的元数据(是描述类的数据结构和信息的数据)、静态变量、常量、方法信息等。&lt;/li&gt;
&lt;li&gt;也称为永久代（Permanent Generation），但在 Java 8 后逐步被元空间（Metaspace）取代。&lt;/li&gt;
&lt;li&gt;可以通过 &lt;code&gt;-XX:MaxMetaspaceSize&lt;/code&gt; 参数来调整元空间的大小。&lt;/li&gt;
&lt;li&gt;会出现永久代溢出或元空间溢出的问题。&lt;/li&gt;
&lt;/ul&gt;
&lt;blockquote&gt;
&lt;ul&gt;
&lt;li&gt;方法区是 Java 虚拟机规范中的一个概念，就像是一个接口吧；&lt;/li&gt;
&lt;li&gt;永久代是 HotSpot 虚拟机中对方法的一个实现，就像是接口的实现类；&lt;/li&gt;
&lt;li&gt;Java 8 的时候，移除了永久代，取而代之的是元空间，是方法区的另外一个实现。&lt;/li&gt;
&lt;/ul&gt;
&lt;/blockquote&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;在 Java 8 之前，字符串常量池在永久代中。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://tangtangit.com/_astro/image-20240322140639098.D9adr2-3_VFgeK.webp&quot; alt=&quot;image-20240322140639098&quot; /&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;Java 8 之后，移除了永久代，字符串常量池就移到了堆中。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://tangtangit.com/_astro/image-20240322140656899.BA-62zn7_Z1q7xoS.webp&quot; alt=&quot;image-20240322140656899&quot; /&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/blockquote&gt;
&lt;h2&gt;1.12 Stack栈&lt;/h2&gt;
&lt;blockquote&gt;
&lt;p&gt;栈管运行，堆管存储&lt;/p&gt;
&lt;p&gt;程序 =  算法 + 数据结构（学术）&lt;/p&gt;
&lt;p&gt;程序 = 框架 + 业务逻辑 （工作）&lt;/p&gt;
&lt;p&gt;栈也叫栈内存，主管Java程序的运行，是在线程创建时创建，它的&lt;strong&gt;生命期是跟随线程的生命期&lt;/strong&gt;，线程结束栈内存也就释放，对于&lt;strong&gt;栈来说不存在垃圾回收&lt;/strong&gt;问题，只要线程一结束该栈就Over，生命周期和线程一致，&lt;strong&gt;是线程私有&lt;/strong&gt;的。&lt;/p&gt;
&lt;p&gt;java 方法 =  栈帧&lt;/p&gt;
&lt;p&gt;栈保存什么东西？&lt;/p&gt;
&lt;blockquote&gt;
&lt;ul&gt;
&lt;li&gt;存放方法的局部变量（8种基本数据类型+引用数据类型）、操作数栈、动态链接、方法出口等。&lt;/li&gt;
&lt;li&gt;每个方法在执行时都会创建一个栈帧，方法的调用和返回就是通过栈帧来管理的。&lt;/li&gt;
&lt;li&gt;有栈帧大小的限制，如果超出会抛出 StackOverflowError 异常。&lt;/li&gt;
&lt;li&gt;线程私有，生命周期与线程相同。&lt;/li&gt;
&lt;/ul&gt;
&lt;/blockquote&gt;
&lt;p&gt;&lt;img src=&quot;https://tangtangit.com/_astro/image-20230822165844791.CuDqiPPC_1sHVhE.webp&quot; alt=&quot;image-20230822165844791&quot; /&gt;&lt;/p&gt;
&lt;p&gt;局部变量表&lt;/p&gt;
&lt;p&gt;在局部变量表里，&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;32位以内的类型只占用一个slot(包括reference、returnAddress类型)，&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;64位的类型(long和double)占用两个slot。&lt;/strong&gt;
byte 、 short 、 char在存储前被转换为int，boolean也被转换为int，0表示false ;非0 表示true。
long和double 则占据两个slot。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://tangtangit.com/_astro/image-20230822170012474.DNX1bdyf_Z28E8vJ.webp&quot; alt=&quot;image-20230822170012474&quot; /&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://tangtangit.com/_astro/image-20230822171012463.DxLpV3wf_2wRe7P.webp&quot; alt=&quot;image-20230822171012463&quot; /&gt;&lt;/p&gt;
&lt;p&gt;操作数栈的压栈操作&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://tangtangit.com/_astro/image-20230822170130781.CqmgltPw_1Qg7FI.webp&quot; alt=&quot;image-20230822170130781&quot; /&gt;&lt;/p&gt;
&lt;p&gt;操作数栈的存入局部变量表&lt;/p&gt;
&lt;p&gt;如果是引用类型就存入对象的地址&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://tangtangit.com/_astro/image-20230822170230073.aG6p0POP_1K9ERn.webp&quot; alt=&quot;image-20230822170230073&quot; /&gt;&lt;/p&gt;
&lt;p&gt;栈运行原理&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;栈中的数据都是以栈帧（Stack Frame）的格式存在，栈帧是一个内存区块，是一个数据集，是一个有关方法(Method)和运行期数据的数据集，当一个方法A被调用时就产生了一个栈帧 F1，并被压入到栈中，
A方法又调用了 B方法，于是产生栈帧 F2 也被压入栈，
B方法又调用了 C方法，于是产生栈帧 F3 也被压入栈，
……
执行完毕后，先弹出F3栈帧，再弹出F2栈帧，再弹出F1栈帧……&lt;/p&gt;
&lt;p&gt;遵循“先进后出”/“后进先出”原则。&lt;/p&gt;
&lt;p&gt;每个方法执行的同时都会创建一个栈帧，用于存储局部变量表、操作数栈、动态链接、方法出口等信息，每一个方法从调用直至执行完毕的过程，就对应着一个栈帧在虚拟机中入栈到出栈的过程。栈的大小和具体JVM的实现有关，通常在256K~756K之间,与等于1Mb左右。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;栈加载顺序&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;img src=&quot;https://tangtangit.com/_astro/image-20210618111902399.CP2Y54qx_Z2pa9xB.webp&quot; alt=&quot;image-20210618111902399&quot; /&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;图示在一个栈中有两个栈帧：&lt;/p&gt;
&lt;p&gt;栈帧 2是最先被调用的方法，先入栈，&lt;/p&gt;
&lt;p&gt;然后方法 2 又调用了方法1，栈帧 1处于栈顶的位置，&lt;/p&gt;
&lt;p&gt;栈帧 2 处于栈底，执行完毕后，依次弹出栈帧 1和栈帧 2，&lt;/p&gt;
&lt;p&gt;线程结束，栈释放。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;每执行一个方法都会产生一个栈帧，保存到栈(后进先出)的&lt;strong&gt;顶部，顶部栈就是当前的方法，该方法执行完毕 后会自动将此栈帧出栈。&lt;/strong&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/blockquote&gt;
&lt;p&gt;Exception in thread &quot;main&quot; java.lang.StackOverflowError  (不是异常，是错误)&lt;/p&gt;
&lt;blockquote&gt;
&lt;pre&gt;&lt;code&gt;public class JvmNote {

    public static void m1(){
        m1();
    }

    public static void main(String[] args) {
        System.out.println(&quot;111&quot;);
        m1();
        System.out.println(&quot;444&quot;);
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;img src=&quot;https://tangtangit.com/_astro/image-20210618112720388.CSjtzD28_20zo4.webp&quot; alt=&quot;image-20210618112720388&quot; /&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://tangtangit.com/_astro/image-20210618112222027.Cv7q-VN4_Z1JsoiG.webp&quot; alt=&quot;image-20210618112222027&quot; /&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;/blockquote&gt;
&lt;h2&gt;1.14 堆&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;存放对象实例及数组，由垃圾回收器进行管理和回收。&lt;/li&gt;
&lt;li&gt;分为新生代（Young Generation）和老年代（Old Generation）。&lt;/li&gt;
&lt;li&gt;可以通过 &lt;code&gt;-Xmx&lt;/code&gt; 和 &lt;code&gt;-Xms&lt;/code&gt; 参数来调整堆的大小。&lt;/li&gt;
&lt;li&gt;堆是 JVM 中最大的一块内存区域，也是垃圾回收主要关注的区域。&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;1.13 栈+堆+方法区的交互关系&lt;/h2&gt;
&lt;p&gt;&lt;img src=&quot;https://tangtangit.com/_astro/image-20210618112827116.DbOpZyPq_PjPMy.webp&quot; alt=&quot;image-20210618112827116&quot; /&gt;&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;HotSpot(jdk名字)是使用指针的方式来访问对象：
Java堆中会存放访问&lt;strong&gt;类元数据&lt;/strong&gt;的地址，
reference存储的就直接是对象的地址&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2&gt;Q&amp;amp;A&lt;/h2&gt;
&lt;p&gt;交互关系中能不能将所有的细节都讲出来，包含所有静态和非静态方法、常量，字符串，对象，对象的引用，数组，基本数据类型，静态和非静态方法等等所有，分别存在哪里如何相互关联。&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;当 Java 程序运行时，栈、堆和方法区之间相互关联，支持方法调用、内存分配和数据存储。以下是详细的交互关系，涵盖了静态和非静态方法、常量、字符串、对象、对象引用、数组、基本数据类型等各个方面：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;常量池中的字符串：&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;字符串常量（例如：&quot;Hello&quot;）存储在方法区的运行时常量池中，这是方法区的一部分。&lt;/li&gt;
&lt;li&gt;运行时常量池是方法区的一部分，包含了类的元数据、符号引用等信息。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;类的加载和方法调用：&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;类的元数据存储在方法区，包括类的结构、字段、方法信息等。&lt;/li&gt;
&lt;li&gt;当类被加载和解析时，相关信息存储在方法区，方法的字节码也存储在方法区。&lt;/li&gt;
&lt;li&gt;调用静态方法时，方法字节码会被加载到方法区并执行，调用非静态方法时，对象引用和方法调用参数存储在栈上。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;方法调用和栈帧：&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;当方法被调用时，栈帧会被创建在栈上，包含方法的局部变量表、操作数栈、动态链接等。&lt;/li&gt;
&lt;li&gt;方法的参数、局部变量和临时数据存储在栈帧的局部变量表和操作数栈中。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;局部变量和基本数据类型：&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;方法的局部变量，包括基本数据类型（如 int、double）和对象引用，存储在栈帧的局部变量表中。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;对象实例和引用：&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;对象实例存储在堆中，堆中包含了 Java 对象的数据。&lt;/li&gt;
&lt;li&gt;对象引用，即对象的引用变量，可以存储在栈帧的局部变量表中。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;数组：&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;数组是对象的一种形式，数组对象存储在堆中。&lt;/li&gt;
&lt;li&gt;数组引用可以存储在栈帧的局部变量表中。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;实例方法和静态方法：&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;实例方法的调用需要实例对象引用，引用存储在栈帧的局部变量表中。&lt;/li&gt;
&lt;li&gt;静态方法的调用直接使用类名调用，方法字节码在方法区加载并执行。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;总结起来，栈用于方法调用的数据管理，包括方法参数、局部变量和方法调用过程中的数据。堆用于存放对象实例，包括类的实例和数组。方法区用于存放类的元数据、静态变量、方法字节码等信息。这些内存区域的相互关联和协作支持了 Java 程序的执行和内存管理。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;我有一个对象person具有age=12和name=‘tom’，这个数据如何存储在jvm中？&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;在 JVM 中，一个对象的数据（即对象的实例变量）会存储在堆内存中。对于一个名为 &lt;code&gt;Person&lt;/code&gt; 的类的对象，具有 &lt;code&gt;age&lt;/code&gt; 和 &lt;code&gt;name&lt;/code&gt; 两个实例变量，假设 &lt;code&gt;age&lt;/code&gt; 是整数类型，&lt;code&gt;name&lt;/code&gt; 是字符串类型。下面是该对象在 JVM 中的存储示意：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;堆内存中的对象实例：&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;对象的实例数据存储在堆内存中。&lt;code&gt;Person&lt;/code&gt; 类的对象实例会占用一块堆内存空间，其中包含了 &lt;code&gt;age&lt;/code&gt; 和 &lt;code&gt;name&lt;/code&gt; 两个实例变量。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;age&lt;/code&gt; 基本数据类型 &lt;code&gt;int&lt;/code&gt; 的值 12 存储在栈内存中，因为它是一个原始数据类型，它的值直接存储在变量的栈帧中。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;name&lt;/code&gt; 变量会存储指向字符串 &quot;tom&quot; 的引用。&lt;/li&gt;
&lt;li&gt;这些实例变量的数据存储在堆内存中的对象头和数据区域。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;堆内存中的字符串对象：&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;由于字符串是不可变的，&quot;tom&quot; 这个字符串常量会存储在方法区的字符串常量池中。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;name&lt;/code&gt; 变量会持有指向字符串常量池中 &quot;tom&quot; 字符串的引用。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;对象引用：&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;在 Java 中，操作对象通常使用对象的引用。如果有一个名为 &lt;code&gt;person&lt;/code&gt; 的对象引用，它会指向堆内存中的 &lt;code&gt;Person&lt;/code&gt; 对象实例。&lt;/li&gt;
&lt;li&gt;这个对象引用可以存储在栈帧的局部变量表中，或者作为成员变量存储在其他对象中。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;综合来说，&lt;code&gt;Person&lt;/code&gt; 类的对象实例在堆内存中存储了 &lt;code&gt;age&lt;/code&gt; 和 &lt;code&gt;name&lt;/code&gt; 两个实例变量的数据。&lt;code&gt;age&lt;/code&gt; 存储了整数值 12，而 &lt;code&gt;name&lt;/code&gt; 存储了指向字符串常量池中的 &quot;tom&quot; 字符串的引用。对象引用可以在栈帧的局部变量表中或其他对象的成员变量中存储，指向堆内存中的 &lt;code&gt;Person&lt;/code&gt; 对象实例。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2&gt;字符串常量池面试题&lt;/h2&gt;
&lt;p&gt;字符串常量池的设计意图是什么？&lt;/p&gt;
&lt;p&gt;字符串常量池在哪里？&lt;/p&gt;
&lt;p&gt;如何操作字符串常量池？&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;字符串的分配，和其他的对象分配一样，耗费高昂的时间与空间代价，作为最基础的数据类型，大量频繁的创建字符串，极大程度地影响程序的性能&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;JVM为了提高性能和减少内存开销，在实例化字符串常量的时候进行了一些优化，为字符串开辟一个字符串常量池，类似于缓存区创建字符串常量时，首先坚持字符串常量池是否存在该字符串存在该字符串，返回引用实例，不存在，实例化该字符串并放入池中&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;实现的基础：实现该优化的基础是因为 字符串是不可变的，可以不用担心数据冲突进行共享，运行时实例创建的全局字符串常量池中有一个表，总是为池中每个唯一的字符串对象维护一个引用,这就意味着它们一直引用着字符串常量池中的对象，所以，在常量池中的这些字符串不会被垃圾收集器回收代码：从字符串常量池中获取相应的字符串&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;pre&gt;&lt;code&gt;  String str1 = “hello”;

  String str2 = “hello”;

  System.out.printl（&quot;str1 ** str2&quot; : str1 ** str2 ) //true
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;字符串常量池在哪里&lt;/p&gt;
&lt;p&gt;在分析字符串常量池的位置时，首先了解一下堆、栈、方法区：&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://tangtangit.com/_astro/image-20240322141612147.D7EYrHad_G2xvj.webp&quot; alt=&quot;image-20240322141612147&quot; /&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;堆&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;存储的是对象，每个对象都包含一个与之对应的class&lt;/p&gt;
&lt;p&gt;JVM只有一个堆区(heap)被所有 线程共享，堆中不存放基本类型和对象引用，只存放对象本身&lt;/p&gt;
&lt;p&gt;对象的由垃圾回收器负责回收，因此大小和生命周期不需要确定&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;栈&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;每个线程包含一个栈区，栈中只保存基础数据类型的对象和自定义对象的引用(不是对象)&lt;/p&gt;
&lt;p&gt;每个栈中的数据(原始类型和对象引用)都是私有的&lt;/p&gt;
&lt;p&gt;栈分为3个部分：基本类型变量区、执行环境上下文、操作指令区(存放操作指令)&lt;/p&gt;
&lt;p&gt;数据大小和生命周期是可以确定的，当没有引用指向数据时，这个数据就会自动消失&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;方法区&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;静态区，跟堆一样，被所有的 线程共享&lt;/p&gt;
&lt;p&gt;方法区中包含的都是在整个程序中永远唯一的元素，如class，static变量；字符串常量池则存在于方法区&lt;/p&gt;
&lt;h3&gt;重点图&lt;/h3&gt;
&lt;p&gt;代码：堆栈方法区存储字符串&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;String str1 = “abc”;

String str2 = “abc”;

String str3 = “abc”;

String str4 = new String(“abc”);

String str5 = new String(“abc”);
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;img src=&quot;https://tangtangit.com/_astro/image-20240322141757742.B3hF-11C_ZNwm8G.webp&quot; alt=&quot;image-20240322141757742&quot; /&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;面试题：&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;String str4 = new String(“abc”) 创建多少个对象？**&lt;/li&gt;
&lt;/ul&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;在常量池中查找是否有“abc”对象&lt;/p&gt;
&lt;p&gt;有则返回对应的引用实例&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;​		没有则创建对应的实例对象&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;在堆中 new 一个 String(&quot;abc&quot;) 对象&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;将对象地址赋值给str4,创建一个引用&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;​		所以，常量池中没有“abc”字面量则创建两个对象，否则创建一个对象，以及创建一个引用&lt;/p&gt;
&lt;p&gt;​		根据字面量，往往会提出这样的变式题：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;String str1 = new String(&quot;A&quot;+&quot;B&quot;) ; 会创建多少个对象?&lt;/li&gt;
&lt;/ul&gt;
&lt;blockquote&gt;
&lt;p&gt;str1：&lt;/p&gt;
&lt;p&gt;字符串常量池：&quot;A&quot;,&quot;B&quot;,&quot;AB&quot; : 3个&lt;/p&gt;
&lt;p&gt;堆：new String(&quot;AB&quot;) ：1个&lt;/p&gt;
&lt;p&gt;引用： str1 ：1个&lt;/p&gt;
&lt;p&gt;总共 ： 5个&lt;/p&gt;
&lt;/blockquote&gt;
&lt;ul&gt;
&lt;li&gt;String str2 = new String(&quot;ABC&quot;) + &quot;ABC&quot; ; 会创建多少个对象?&lt;/li&gt;
&lt;/ul&gt;
&lt;blockquote&gt;
&lt;p&gt;str2 ：&lt;/p&gt;
&lt;p&gt;字符串常量池：&quot;ABC&quot; : 1个&lt;/p&gt;
&lt;p&gt;堆：new String(&quot;ABC&quot;) ：1个&lt;/p&gt;
&lt;p&gt;引用： str2 ：1个&lt;/p&gt;
&lt;p&gt;总共 ： 3个&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;&lt;strong&gt;基础类型的变量和常量，变量和引用存储在栈中，常量存储在常量池中&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;int a1 = 1;

int a2 = 1;

int a3 = 1;

public static int INT1 =1 ;

public static int INT2 =1 ;

public static int INT3 =1 ;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;img src=&quot;https://tangtangit.com/_astro/image-20240322142230735.cWAZJYDj_2oF0o0.webp&quot; alt=&quot;image-20240322142230735&quot; /&gt;&lt;/p&gt;
&lt;h3&gt;操作字符串常量池的方式&lt;/h3&gt;
&lt;p&gt;JVM实例化字符串常量池时&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;  String str1 = “hello”;

  String str2 = “hello”;

  System.out.printl（&quot;str1 ** str2&quot; : str1 ** str2 ) //true

  String.intern()
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;通过new操作符创建的字符串对象不指向字符串池中的任何对象，但是可以通过使用字符串的intern()方法来指向其中的某一个。&lt;/p&gt;
&lt;p&gt;java.lang.String.intern()返回一个保留池字符串，就是一个在全局字符串池中有了一个入口。如果以前没有在全局字符串池中，那么它就会被添加到里面&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;// Create three strings in three different ways.
String s1 = &quot;Hello&quot;;

String s2 = new StringBuffer(&quot;He&quot;).append(&quot;llo&quot;).toString();

String s3 = s2.intern();

// Determine which strings are equivalent using the ==

// operator

System.out.println(&quot;s1 ** s2? &quot; + (s1 ** s2)); // false

System.out.println(&quot;s1 ** s3? &quot; + (s1 ** s3)); // true
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;字面量和常量池初探&lt;/p&gt;
&lt;p&gt;字符串对象内部是用字符数组存储的，那么看下面的例子:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;String m = &quot;hello,world&quot;;

String n = &quot;hello,world&quot;;

String u = new String(m);

String v = new String(&quot;hello,world&quot;);
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;1.会分配一个11长度的char数组，并在常量池分配一个由这个char数组组成的字符串，然后由m去引用这个字符串&lt;/p&gt;
&lt;p&gt;2.用n去引用常量池里边的 字符串，所以和n引用的是同一个对象&lt;/p&gt;
&lt;p&gt;3.生成一个新的字符串，但内部的字符数组引用着m内部的字符数组&lt;/p&gt;
&lt;p&gt;4.同样会生成一个新的字符串，但内部的字符数组引用常量池里边的字符串内部的字符数组，意思是和u是同样的字符数组&lt;/p&gt;
&lt;p&gt;使用图来表示的话，情况就大概是这样的(使用虚线只是表示两者其实没什么特别的关系):&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://tangtangit.com/_astro/image-20240322142441786.CRA7zZlh_Z2pbzib.webp&quot; alt=&quot;image-20240322142441786&quot; /&gt;&lt;/p&gt;
&lt;p&gt;测试demo：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;    String m = &quot;hello,world&quot;;

    String n = &quot;hello,world&quot;;

    String u = new String(m);

    String v = new String(&quot;hello,world&quot;);

    System.out.println(m == n); //true 

    System.out.println(m == u); //false

    System.out.println(m == v); //false

    System.out.println(u == v); //false
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;结论：&lt;/h3&gt;
&lt;p&gt;m和n是同一个对象&lt;/p&gt;
&lt;p&gt;m,u,v都是不同的对象&lt;/p&gt;
&lt;p&gt;m,u,v,n但都使用了同样的字符数组，并且用equal判断的话也会返回true&lt;/p&gt;
&lt;h1&gt;2、堆体系结构概述&lt;/h1&gt;
&lt;h2&gt;Heap 堆&lt;/h2&gt;
&lt;p&gt;​	一个JVM实例只存在一个堆内存，堆内存的大小是可以调节的。类加载器读取了类文件后，需要把类、方法、常变量放到堆内存中，保存所有引用类型的真实信息，以方便执行器执行，堆内存分为三部分：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;Young Generation Space  新生区                    Young/New&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Tenure generation space  养老区                     Old/ Tenure&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Permanent Space              永久区                         Perm      元空间（java8）&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Heap堆(&lt;strong&gt;Java7之前&lt;/strong&gt;)
一个JVM实例只存在一个堆内存，堆内存的大小是可以调节的。类加载器读取了类文件后，需要把类、方法、常变量放到堆内存中，保存所有引用类型的真实信息，以方便执行器执行。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;堆内存&lt;strong&gt;逻辑上&lt;/strong&gt;分为三部分：新生+养老+永久&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;img src=&quot;https://tangtangit.com/_astro/image-20210618113615439.B33zoDnN_1wRzKU.webp&quot; alt=&quot;image-20210618113615439&quot; /&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;堆内存&lt;strong&gt;物理上&lt;/strong&gt;分为两部分：新生+养老&lt;/li&gt;
&lt;/ul&gt;
&lt;blockquote&gt;
&lt;p&gt;新生区(如下是首次讲解，&lt;strong&gt;简单版&lt;/strong&gt;，先入门大致理解，下一页ppt详细)
新生区是类的诞生、成长、消亡的区域，一个类在这里产生，应用，最后被垃圾回收器收集，结束生命。&lt;/p&gt;
&lt;p&gt;新生区又分为两部分： 伊甸区（Eden space）和幸存者区（Survivor pace） ，所有的类都是在伊甸区被new出来的。幸存区有两个： 0区（Survivor 0 space）和1区（Survivor 1 space）。当伊甸园的空间用完时，程序又需要创建对象，JVM的垃圾回收器将对伊甸园区进行垃圾回收(Minor GC)，将伊甸园区中的不再被其他对象所引用的对象进行销毁。然后将伊甸园中的剩余对象移动到幸存 0区。若幸存 0区也满了，再对该区进行垃圾回收，然后移动到 1 区。那如果1 区也满了呢？再移动到养老区。若养老区也满了，那么这个时候将产生MajorGC（FullGC），进行养老区的内存清理。若养老区执行了Full GC之后发现依然无法进行对象的保存，就会产生OOM异常“OutOfMemoryError”。&lt;/p&gt;
&lt;p&gt;如果出现java.lang.OutOfMemoryError: Java heap space异常，说明Java虚拟机的堆内存不够。原因有二：
（1）Java虚拟机的堆内存设置不够，可以通过参数-Xms、-Xmx来调整。
（2）代码中创建了大量大对象，并且长时间不能被垃圾收集器收集（存在被引用）。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2&gt;MinorGC的过程（复制-&amp;gt;清空-&amp;gt;互换）&lt;/h2&gt;
&lt;p&gt;&lt;img src=&quot;https://tangtangit.com/_astro/image-20210618133853733.GXWHFY7Y_2sjcx4.webp&quot; alt=&quot;image-20210618133853733&quot; /&gt;&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;记：幸存者0区 = from 区、  幸存者1区=to区&lt;/p&gt;
&lt;p&gt;from区和to区，他们的位置和名次，不是固定的，每次GC后会交换Gc之后有交换，谁空谁是to区&lt;/p&gt;
&lt;p&gt;1：eden、SurvivorFrom 复制到 SurvivorTo，年龄+1
首先，当Eden区满的时候会触发第一次GC,把还活着的对象拷贝到SurvivorFrom区，当Eden区再次触发GC的时候会扫描Eden区和From区域,对这两个区域进行垃圾回收，经过这次回收后还存活的对象,则直接复制到To区域（如果有对象的年龄已经达到了老年的标准，则赋值到老年代区），同时把这些对象的年龄+1&lt;/p&gt;
&lt;p&gt;2：清空 eden、SurvivorFrom
然后，清空Eden和SurvivorFrom中的对象，也即复制之后有交换，谁空谁是to&lt;/p&gt;
&lt;p&gt;3：SurvivorTo和 SurvivorFrom 互换
最后，SurvivorTo和SurvivorFrom互换，原SurvivorTo成为下一次GC时的SurvivorFrom区。部分对象会在From和To区域中复制来复制去,如此交换15次(由JVM参数MaxTenuringThreshold决定,这个参数默认是15),最终如果还是存活,就存入到老年代&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2&gt;内存管理&lt;/h2&gt;
&lt;p&gt;&lt;img src=&quot;https://tangtangit.com/_astro/image-20210618133933461.ubwoPcmW_1hu02K.webp&quot; alt=&quot;image-20210618133933461&quot; /&gt;&lt;/p&gt;
&lt;p&gt;​&lt;/p&gt;
&lt;p&gt;实际而言，&lt;strong&gt;方法区（Method Area）和堆一样&lt;/strong&gt;，是各个线程共享的内存区域，它用于存储虚拟机加载的：类信息+普通常量+静态常量+编译器编译后的代码等等，虽然JVM规范将方法区描述为堆的一个逻辑部分，&lt;strong&gt;但它却还有一个别名叫做Non-Heap(非堆)&lt;/strong&gt;，&lt;em&gt;目的就是要和堆分开&lt;/em&gt;。&lt;/p&gt;
&lt;p&gt;对于HotSpot（JDK）虚拟机，很多开发者习惯将方法区称之为“永久代(Parmanent Gen)” ，但严格本质上说两者不同，或者说使用永久代来实现方法区而已，永久代是方法区(相当于是一个接口interface)的一个实现，jdk1.7的版本中，已经将原本放在永久代的字符串常量池移走。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://tangtangit.com/_astro/image-20210618133948997.ozGYqg2Y_2mOCH7.webp&quot; alt=&quot;image-20210618133948997&quot; /&gt;&lt;/p&gt;
&lt;h2&gt;永久区(java7之前有)&lt;/h2&gt;
&lt;p&gt;​	永久存储区是一个&lt;strong&gt;常驻内存区域&lt;/strong&gt;，用于存放JDK自身所携带的 Class,Interface 的元数据，也就是说它&lt;strong&gt;存储的是运行环境必须的类信息&lt;/strong&gt;，被装载进此区域的数据是不会被垃圾回收器回收掉的，关闭 JVM 才会释放此区域所占用的内存。&lt;/p&gt;
&lt;h1&gt;3、堆参数调优入门&lt;/h1&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;JVM垃圾收集(Java Garbage Collection )&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;上集，本次均以JDK1.8+HotSpot为例&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;blockquote&gt;
&lt;p&gt;Java7&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://tangtangit.com/_astro/image-20210618140604885.9V2t7Dls_1Ek8tY.webp&quot; alt=&quot;image-20210618140604885&quot; /&gt;&lt;/p&gt;
&lt;p&gt;Java8&lt;/p&gt;
&lt;p&gt;JDK 1.8之后将最初的永久代取消了，由元空间取代。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://tangtangit.com/_astro/image-20210618140826698.Dsi0vKR0_20Bgty.webp&quot; alt=&quot;image-20210618140826698&quot; /&gt;&lt;/p&gt;
&lt;p&gt;在Java8中，永久代已经被移除，被一个称为元空间的区域所取代。元空间的本质和永久代类似。&lt;/p&gt;
&lt;p&gt;元空间与永久代之间最大的区别在于：
&lt;strong&gt;永久带使用的JVM的堆内存，但是java8以后的元空间并不在虚拟机中而是使用本机物理内存。&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;因此，默认情况下，&lt;strong&gt;元空间的大小仅受本地内存限制&lt;/strong&gt;。类的元数据放入 native memory, 字符串池和类的静态变量放入 java 堆中，这样可以加载多少类的元数据就不再由MaxPermSize 控制, 而由系统的实际可用空间来控制。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2&gt;堆调优&lt;/h2&gt;
&lt;h2&gt;（堆内存调优简介01）&lt;/h2&gt;
&lt;p&gt;&lt;img src=&quot;https://tangtangit.com/_astro/image-20210618141346521.CMgI1N8l_ZgBLdm.webp&quot; alt=&quot;image-20210618141346521&quot; /&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;public static void main(String[] args){
	long maxMemory = Runtime.getRuntime().maxMemory() ;//返回 Java 虚拟机试图使用的最大内存量。
	long totalMemory = Runtime.getRuntime().totalMemory() ;//返回 Java 虚拟机中的内存总量。
	System.out.println(&quot;-Xmx:MAX_MEMORY = &quot; + maxMemory + &quot;（字节）、&quot; + (maxMemory / (double)1024 / 1024) + &quot;MB&quot;);
	System.out.println(&quot;-Xms:TOTAL_MEMORY = &quot; + totalMemory + &quot;（字节）、&quot; + (totalMemory / (double)1024 / 1024) + &quot;MB&quot;);
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;实际上调优：
1、Xmx、Xms设置成一样大 ， 理由 ：防止忽大忽小。&lt;/p&gt;
&lt;h2&gt;（堆内存调优简介02）&lt;/h2&gt;
&lt;blockquote&gt;
&lt;p&gt;发现默认的情况下分配的内存是总内存的“1 / 4”、而初始化的内存为“1 / 64”&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://tangtangit.com/_astro/image-20210618142617839.CI53jQB0_11hj5s.webp&quot; alt=&quot;image-20210618142617839&quot; /&gt;&lt;/p&gt;
&lt;p&gt;VM参数：	-Xms1024m -Xmx1024m -XX:+PrintGCDetails&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://tangtangit.com/_astro/image-20210618142631859.dX3oxKqW_ZcsAH8.webp&quot; alt=&quot;image-20210618142631859&quot; /&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2&gt;（堆内存调优简介03）此图为java7，演示为8&lt;/h2&gt;
&lt;p&gt;&lt;img src=&quot;https://tangtangit.com/_astro/image-20210618143334294.DYssopV2_Z1BrVSy.webp&quot; alt=&quot;image-20210618143334294&quot; /&gt;&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;实际案例：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;public class T2 {

    public static void main(String[] args){
        long maxMemory = Runtime.getRuntime().maxMemory() ;//返回 Java 虚拟机试图使用的最大内存量。
        long totalMemory = Runtime.getRuntime().totalMemory() ;//返回 Java 虚拟机中的内存总量。
        System.out.println(&quot;-Xmx:MAX_MEMORY = &quot; + maxMemory + &quot;（字节）、&quot; + (maxMemory / (double)1024 / 1024) + &quot;MB&quot;);
        System.out.println(&quot;-Xms:TOTAL_MEMORY = &quot; + totalMemory + &quot;（字节）、&quot; + (totalMemory / (double)1024 / 1024) + &quot;MB&quot;);
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;D:\java1.8_64\jdk1.8.0_71\bin\java.exe -Xms1024m -Xmx1024m -XX:+PrintGCDetails &quot;-javaagent:F:\idea\IntelliJ IDEA 2020.3.2\lib\idea_rt.jar=23464:F:\idea\IntelliJ IDEA 2020.3.2\bin&quot; -Dfile.encoding=UTF-8 -classpath box\org\springframework\spring-expression\5.2.1.RELEASE\spring-expression-5.2.1.RELEASE.jar com.eim.jvm.T2


-Xmx:MAX_MEMORY = 1029177344（字节）、981.5MB
-Xms:TOTAL_MEMORY = 1029177344（字节）、981.5MB
Heap
 PSYoungGen      total 305664K, used 26214K [0x00000000eab00000, 0x0000000100000000, 0x0000000100000000)
  eden space 262144K, 10% used [0x00000000eab00000,0x00000000ec499be8,0x00000000fab00000)
  from space 43520K, 0% used [0x00000000fd580000,0x00000000fd580000,0x0000000100000000)
  to   space 43520K, 0% used [0x00000000fab00000,0x00000000fab00000,0x00000000fd580000)
 ParOldGen       total 699392K, used 0K [0x00000000c0000000, 0x00000000eab00000, 0x00000000eab00000)
  object space 699392K, 0% used [0x00000000c0000000,0x00000000c0000000,0x00000000eab00000)
 Metaspace       used 3335K, capacity 4496K, committed 4864K, reserved 1056768K
  class space    used 365K, capacity 388K, committed 512K, reserved 1048576K

Process finished with exit code 0
&lt;/code&gt;&lt;/pre&gt;
&lt;/blockquote&gt;
&lt;h2&gt;（堆内存调优简介04）&lt;/h2&gt;
&lt;blockquote&gt;
&lt;pre&gt;&lt;code&gt;String str = &quot;www.atguigu.com&quot; ;
while(true) 
{
str += str + new Random().nextInt(88888888) + new Random().nextInt(999999999) ;
}

&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;VM参数：-Xms8m -Xmx8m -XX:+PrintGCDetails&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://tangtangit.com/_astro/image-20210618143712793.BL7P208I_1Hz0NC.webp&quot; alt=&quot;image-20210618143712793&quot; /&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;Exception in thread &quot;main&quot; java.lang.OutOfMemoryError: Java heap space
	at java.util.Arrays.copyOf(Arrays.java:3332)
	at java.lang.AbstractStringBuilder.expandCapacity(AbstractStringBuilder.java:137)
	at java.lang.AbstractStringBuilder.ensureCapacityInternal(AbstractStringBuilder.java:121)
	at java.lang.AbstractStringBuilder.append(AbstractStringBuilder.java:647)
	at java.lang.StringBuilder.append(StringBuilder.java:208)
	at com.eim.jvm.T2.main(T2.java:22)
&lt;/code&gt;&lt;/pre&gt;
&lt;/blockquote&gt;
&lt;h1&gt;4、总     结&lt;/h1&gt;
&lt;h2&gt;5个异常&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;java.lang.OutOfMemoryError  内存溢出异常
java.util.ConcurrentModificationException  java并发修改异常    
&lt;/code&gt;&lt;/pre&gt;
&lt;h1&gt;Gc&lt;/h1&gt;
&lt;h2&gt;Gc收集日志信息&lt;/h2&gt;
&lt;p&gt;-XX:+PrintGCDetails&lt;/p&gt;
&lt;h2&gt;GC是什么(分代收集算法)&lt;/h2&gt;
&lt;blockquote&gt;
&lt;p&gt;次数上频繁收集Young区&lt;/p&gt;
&lt;p&gt;次数上较少收集Old区&lt;/p&gt;
&lt;p&gt;基本不动元空间&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://tangtangit.com/_astro/image-20220129153205646.C7UMN_wB_ZoD70N.webp&quot; alt=&quot;image-20220129153205646&quot; /&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2&gt;GC4大算法&lt;/h2&gt;
&lt;h3&gt;Gc算法总体概述&lt;/h3&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;img src=&quot;https://tangtangit.com/_astro/image-20210618150317813.C1jewAXs_ZRyymD.webp&quot; alt=&quot;image-20210618150317813&quot; /&gt;&lt;/p&gt;
&lt;p&gt;JVM在进行GC时，并非每次都对上面三个内存区域一起回收的，大部分时候回收的都是指新生代。
因此GC按照回收的区域又分了两种类型，一种是普通GC（minor GC），一种是全局GC（major GC or Full GC）&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Minor GC和Full GC的区别&lt;/strong&gt;
　　普通GC（minor GC）：只针对新生代区域的GC,指发生在新生代的垃圾收集动作，因为大多数Java对象存活率都不高，所以Minor GC非常频繁，一般回收速度也比较快。
　　全局GC（major GC or Full GC）：指发生在老年代的垃圾收集动作，出现了Major GC，经常会伴随至少一次的Minor GC（但并不是绝对的）。Major GC的速度一般要比Minor GC慢上10倍以上&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h3&gt;4算法&lt;/h3&gt;
&lt;h4&gt;1、引用计数法（了解）&lt;/h4&gt;
&lt;p&gt;&lt;img src=&quot;https://tangtangit.com/_astro/image-20210618150615945.DP14gcT1_9bkUT.webp&quot; alt=&quot;image-20210618150615945&quot; /&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;package com.atguigu.jvm;
 
 
/**@Description:-verbose:gc*/
public class RefCountGC
{
  private byte[] bigSize = new byte[2 * 1024 * 1024];//这个成员属性唯一的作用就是占用一点内存
  Object instance = null;
 
  public static void main(String[] args)
  {
    RefCountGC objectA = new RefCountGC();
    RefCountGC objectB = new RefCountGC();
    objectA.instance = objectB;
    objectB.instance = objectA;
    objectA = null;
    objectB = null;
 
    System.gc();
  }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h4&gt;2、复制算法(Copying)&lt;/h4&gt;
&lt;p&gt;年轻代中使用的是Minor GC，这种GC算法采用的是复制算法(Copying)&lt;/p&gt;
&lt;p&gt;what&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;原理&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://tangtangit.com/_astro/image-20210618151039287.BGZHGO_2_1mn4id.webp&quot; alt=&quot;image-20210618151039287&quot; /&gt;&lt;/p&gt;
&lt;p&gt;Minor GC会把Eden中的所有活的对象都移到Survivor区域中，如果Survivor区中放不下，那么剩下的活的对象就被移到Old  generation中，也即一旦收集后，Eden是就变成空的了。
当对象在 Eden ( 包括一个 Survivor 区域，这里假设是 from 区域 ) 出生后，在经过一次 Minor GC 后，如果对象还存活，并且能够被另外一块 Survivor 区域所容纳( 上面已经假设为 from 区域，这里应为 to 区域，即 to 区域有足够的内存空间来存储 Eden 和 from 区域中存活的对象 )，则使用复制算法将这些仍然还存活的对象复制到另外一块 Survivor 区域 ( 即 to 区域 ) 中，然后清理所使用过的 Eden 以及 Survivor 区域 ( 即 from 区域 )，并且将这些对象的年龄设置为1，以后对象在 Survivor 区每熬过一次 Minor GC，就将对象的年龄 + 1，当对象的年龄达到某个值时 ( 默认是 15 岁，通过-XX:MaxTenuringThreshold 来设定参数)，这些对象就会成为老年代。&lt;/p&gt;
&lt;p&gt;-XX:MaxTenuringThreshold — 设置对象在新生代中存活的次数&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;动态演示&lt;/strong&gt;&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;解释&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;年轻代中的GC,主要是复制算法（Copying）&lt;/p&gt;
&lt;p&gt;HotSpot JVM把年轻代分为了三部分：1个Eden区和2个Survivor区（分别叫from和to）。默认比例为8:1:1,一般情况下，新创建的对象都会被分配到Eden区(一些大对象特殊处理),这些对象经过第一次Minor GC后，如果仍然存活，将会被移到Survivor区。对象在Survivor区中每熬过一次Minor GC，年龄就会增加1岁，当它的年龄增加到一定程度时，就会被移动到年老代中。因为年轻代中的对象基本都是朝生夕死的(90%以上)，所以在年轻代的垃圾回收算法使用的是复制算法，复制算法的基本思想就是将内存分为两块，每次只用其中一块，当这一块内存用完，就将还活着的对象复制到另外一块上面。&lt;strong&gt;复制算法不会产生内存碎片&lt;/strong&gt;（优点）。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://tangtangit.com/_astro/image-20210618151151593.XQ7oBr5r_10CSfQ.webp&quot; alt=&quot;image-20210618151151593&quot; /&gt;&lt;/p&gt;
&lt;p&gt;在GC开始的时候，对象只会存在于Eden区和名为“From”的Survivor区，Survivor区“To”是空的。紧接着进行GC，Eden区中所有存活的对象都会被复制到“To”，而在“From”区中，仍存活的对象会根据他们的年龄值来决定去向。年龄达到一定值(年龄阈值，可以通过-XX:MaxTenuringThreshold来设置)的对象会被移动到年老代中，没有达到阈值的对象会被复制到“To”区域。经过这次GC后，Eden区和From区已经被清空。这个时候，“From”和“To”会交换他们的角色，也就是新的“To”就是上次GC前的“From”，新的“From”就是上次GC前的“To”。不管怎样，都会保证名为To的Survivor区域是空的。Minor GC会一直重复这样的过程，直到“To”区被填满，“To”区被填满之后，会将所有对象移动到年老代中。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://tangtangit.com/_astro/image-20210618151210517.BdyToctb_2kT5ML.webp&quot; alt=&quot;image-20210618151210517&quot; /&gt;&lt;/p&gt;
&lt;p&gt;因为Eden区对象一般存活率较低，一般的，使用两块10%的内存作为空闲和活动区间，而另外80%的内存，则是用来给新建对象分配内存的。一旦发生GC，将10%的from活动区间与另外80%中存活的eden对象转移到10%的to空闲区间，接下来，将之前90%的内存全部释放，以此类推。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;动图&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://tangtangit.com/_astro/gc_copying.CCSLy9xe_Z2hKwDV.webp&quot; alt=&quot;gc_copying&quot; /&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;/blockquote&gt;
&lt;p&gt;劣势&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;复制算法它的缺点也是相当明显的。
　　1、它浪费了一半的内存，这太要命了。
　　2、如果对象的存活率很高，我们可以极端一点，假设是100%存活，那么我们需要将所有对象都复制一遍，并将所有引用地址重置一遍。复制这一工作所花费的时间，在对象存活率达到一定程度时，将会变的不可忽视。 所以从以上描述不难看出，复制算法要想使用，最起码对象的存活率要非常低才行，而且最重要的是，我们必须要克服50%内存的浪费。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h4&gt;3、标记清除(Mark-Sweep)&lt;/h4&gt;
&lt;p&gt;&lt;strong&gt;老年代&lt;/strong&gt;一般是由（标记清除 或者是 标记清除）与 标记整理的混合实现&lt;/p&gt;
&lt;p&gt;what&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;原理&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://tangtangit.com/_astro/image-20210618151444154.CzhxIUhj_1OFvgy.webp&quot; alt=&quot;image-20210618151444154&quot; /&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://tangtangit.com/_astro/image-20210618151450329.CHJuhrcP_Z3uy8t.webp&quot; alt=&quot;image-20210618151450329&quot; /&gt;&lt;/p&gt;
&lt;p&gt;用通俗的话解释一下标记清除算法，就是当程序运行期间，若可以使用的内存被耗尽的时候，GC线程就会被触发并将程序暂停，随后将要回收的对象标记一遍，最终统一回收这些对象，完成标记清理工作接下来便让应用程序恢复运行。&lt;/p&gt;
&lt;p&gt;主要进行两项工作，第一项则是标记，第二项则是清除。&lt;br /&gt;
标记：从引用根节点开始标记遍历所有的GC Roots， 先标记出要回收的对象。
清除：遍历整个堆，把标记的对象清除。
缺点：此算法需要暂停整个应用，会产生内存碎片&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;动态演示&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://tangtangit.com/_astro/mark_sweep.DSSV6a6g_C3x1w.webp&quot; alt=&quot;mark_sweep&quot; /&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;劣势&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;1、首先，它的缺点就是效率比较低（递归与全堆对象遍历），而且在进行GC的时候，需要停止应用程序，这会导致用户体验非常差劲
2、其次，主要的缺点则是这种方式清理出来的空闲内存是不连续的，这点不难理解，我们的死亡对象都是随即的出现在内存的各个角落的，现在把它们清除之后，内存的布局自然会乱七八糟。而为了应付这一点，JVM就不得不维持一个内存的空闲列表，这又是一种开销。而且在分配数组对象的时候，寻找连续的内存空间会不太好找。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h4&gt;4、标记压缩(Mark-Compact)&lt;/h4&gt;
&lt;p&gt;老年代一般是由标记清除或者是标记清除与标记整理的混合实现&lt;/p&gt;
&lt;p&gt;what&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;原理&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://tangtangit.com/_astro/image-20210618151625353.Cn4QJ9YP_Z1J0N0P.webp&quot; alt=&quot;image-20210618151625353&quot; /&gt;&lt;/p&gt;
&lt;p&gt;在整理压缩阶段，不再对标记的对像做回收，而是通过所有存活对像都向一端移动，然后直接清除边界以外的内存。
可以看到，标记的存活对象将会被整理，按照内存地址依次排列，而未被标记的内存会被清理掉。如此一来，当我们需要给新对象分配内存时，JVM只需要持有一个内存的起始地址即可，这比维护一个空闲列表显然少了许多开销。&lt;/p&gt;
&lt;p&gt;　　标记/整理算法不仅可以弥补标记/清除算法当中，内存区域分散的缺点，也消除了复制算法当中，内存减半的高额代价&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;劣势&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;标记/整理算法唯一的缺点就是效率也不高，不仅要标记所有存活对象，还要整理所有存活对象的引用地址。
从效率上来说，标记/整理算法要低于复制算法。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;标记清除压缩(Mark-Sweep-Compact)&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://tangtangit.com/_astro/image-20210618151658052.BBH4o0jl_ZSlL4r.webp&quot; alt=&quot;image-20210618151658052&quot; /&gt;&lt;/p&gt;
&lt;p&gt;动态演示&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://tangtangit.com/_astro/mark_compact.BnQgGDZ6_Z4H8lx.webp&quot; alt=&quot;mark_compact&quot; /&gt;&lt;/p&gt;
&lt;h3&gt;小总结&lt;/h3&gt;
&lt;blockquote&gt;
&lt;p&gt;内存效率：复制算法&amp;gt;标记清除算法&amp;gt;标记整理算法（此处的效率只是简单的对比时间复杂度，实际情况不一定如此）。
内存整齐度：复制算法=标记整理算法&amp;gt;标记清除算法。
内存利用率：标记整理算法=标记清除算法&amp;gt;复制算法。&lt;/p&gt;
&lt;p&gt;可以看出，效率上来说，复制算法是当之无愧的老大，但是却浪费了太多内存，而为了尽量兼顾上面所提到的三个指标，标记/整理算法相对来说更平滑一些，但效率上依然不尽如人意，它比复制算法多了一个标记的阶段，又比标记/清除多了一个整理内存的过程&lt;/p&gt;
&lt;p&gt;　　难道就没有一种最优算法吗？ 猜猜看，下面还有&lt;/p&gt;
&lt;p&gt;回答：无，没有最好的算法，只有最合适的算法。&lt;strong&gt;=****=&lt;/strong&gt;&amp;gt;分代收集算法。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;年轻代(Young Gen)&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;年轻代特点是区域相对老年代较小，对像存活率低。&lt;/p&gt;
&lt;p&gt;这种情况复制算法的回收整理，速度是最快的。复制算法的效率只和当前存活对像大小有关，因而很适用于年轻代的回收。而复制算法内存利用率不高的问题，通过hotspot中的两个survivor的设计得到缓解。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;老年代(Tenure Gen)&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;老年代的特点是区域较大，对像存活率高。&lt;/p&gt;
&lt;p&gt;这种情况，存在大量存活率高的对像，复制算法明显变得不合适。一般是由&lt;strong&gt;标记清除&lt;/strong&gt;或者是&lt;strong&gt;标记清除与标记整理&lt;/strong&gt;的混合实现。&lt;/p&gt;
&lt;p&gt;Mark阶段的开销与存活对像的数量成正比，这点上说来，对于老年代，标记清除或者标记整理有一些不符，但可以通过多核/线程利用，对并发、并行的形式提标记效率。&lt;/p&gt;
&lt;p&gt;Sweep阶段的开销与所管理区域的大小形正相关，但Sweep“就地处决”的特点，回收的过程没有对像的移动。使其相对其它有对像移动步骤的回收算法，仍然是效率最好的。但是需要解决内存碎片问题。&lt;/p&gt;
&lt;p&gt;Compact阶段的开销与存活对像的数据成开比，如上一条所描述，对于大量对像的移动是很大开销的，做为老年代的第一选择并不合适。&lt;/p&gt;
&lt;p&gt;基于上面的考虑，老年代一般是由标记清除或者是标记清除与标记整理的混合实现。以hotspot中的CMS回收器为例，CMS是基于Mark-Sweep实现的，对于对像的回收效率很高，而对于碎片问题，CMS采用基于Mark-Compact算法的Serial Old回收器做为补偿措施：当内存回收不佳（碎片导致的Concurrent Mode Failure时），将采用Serial Old执行Full GC以达到对老年代内存的整理。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h3&gt;面试题&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;JVM内存模型以及分区，需要详细到每个区放什么&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;堆里面的分区：Eden，survival from to，老年代，各自的特点。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;GC的三种收集方法：标记清除、标记整理、复制算法的原理与特点，分别用在什么地方&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Minor GC与Full GC分别在什么时候发生&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h1&gt;JMM&lt;/h1&gt;
&lt;blockquote&gt;
&lt;p&gt;JMM(Java内存模型Java Memory Model,简称JMM)本身是一种抽象的概念 并不真实存在,它描述的是一组规则或规范通过规范定制了程序中各个变量(包括实例字段,静态字段和构成数组对象的元素)的访问方式.
JMM关于同步规定:
1.线程解锁前,必须把共享变量的值刷新回主内存
2.线程加锁前,必须读取主内存的最新值到自己的工作内存
3.加锁解锁是同一把锁&lt;/p&gt;
&lt;p&gt;由于JVM运行程序的实体是线程,而每个线程创建时JVM都会为其创建一个工作内存(有些地方成为栈空间),工作内存是每个线程的私有数据区域,而Java内存模型中规定所有变量都存储在主内存,主内存是共享内存区域,所有线程都可访问,但线程对变量的操作(读取赋值等)必须在工作内存中进行,首先要将变量从主内存拷贝到自己的工作空间,然后对变量进行操作,操作完成再将变量写回主内存,不能直接操作主内存中的变量,各个线程中的工作内存储存着主内存中的变量副本拷贝,因此不同的线程无法访问对方的工作内存,此案成间的通讯(传值) 必须通过主内存来完成,其简要访问过程如下图:&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://tangtangit.com/_astro/image-20210618165232309.51xpB2c__1BMn1s.webp&quot; alt=&quot;image-20210618165232309&quot; /&gt;&lt;/p&gt;
&lt;p&gt;特征：可见、原子、有序&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;1、可见性&lt;/p&gt;
&lt;p&gt;2、原子性&lt;/p&gt;
&lt;p&gt;3、演示可见性+原子性&lt;/p&gt;
&lt;p&gt;4、有序性&lt;/p&gt;
&lt;h2&gt;volatile是Java虚拟机提供的轻量级的同步机制&lt;/h2&gt;
&lt;blockquote&gt;
&lt;p&gt;1.1保证可见性&lt;/p&gt;
&lt;p&gt;1.2不保证原子性&lt;/p&gt;
&lt;p&gt;1.3禁止指令重排&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h1&gt;aqs和cas&lt;/h1&gt;
&lt;p&gt;在面试中，关于 AQS（AbstractQueuedSynchronizer）和 CAS（Compare and Swap）的问题常常涉及到并发编程和锁机制。以下是一些可能的面试题及其答案：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;什么是AQS？它在Java中的作用是什么？&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;答案：&lt;/strong&gt; AQS 是 Java 中用于构建锁和同步器的&lt;strong&gt;框架&lt;/strong&gt;。它提供了一种通用的同步机制，通过内部维护一个同步队列（队列中的节点通常是等待获取锁的线程），实现了基于FIFO等待队列的锁和同步器的抽象。ReentrantLock 和 CountDownLatch 等 Java 中的锁和同步器都是基于 AQS 构建的。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;CAS 是什么？它与AQS有什么关系？&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;答案：&lt;/strong&gt; CAS（Compare and Swap）是一种&lt;strong&gt;无锁原子操作&lt;/strong&gt;，用于实现多线程环境下的并发控制。在Java中，&lt;code&gt;java.util.concurrent.atomic&lt;/code&gt; 包中的一些类（如 &lt;code&gt;AtomicInteger&lt;/code&gt;、&lt;code&gt;AtomicReference&lt;/code&gt;）使用 CAS 操作来实现线程安全的原子操作。AQS 使用 CAS 操作来实现对状态的原子更新，这是实现锁和同步器的关键机制之一。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;简要解释一下AQS的工作原理。&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;答案：&lt;/strong&gt; AQS 的核心思想是基于一个整型的状态（state）来控制同步资源的访问。当线程尝试获取锁时，AQS 会通过 CAS 操作更新状态，如果成功则获取锁，否则将线程加入同步队列。在释放锁的时候，AQS 同样通过 CAS 操作来更新状态，并唤醒同步队列中等待的线程。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;什么是可重入锁？ReentrantLock是如何实现可重入性的？&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;答案：&lt;/strong&gt; 可重入锁是指同一个线程可以多次获取同一个锁，而不会发生死锁。ReentrantLock 实现了可重入性，通过维护一个持有锁的线程和一个计数器。当线程第一次获取锁时，计数器值为1，每次重入锁，计数器递增。当线程释放锁时，计数器递减，只有当计数器为0时，锁才被完全释放。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;什么是ABA问题？CAS 如何解决ABA问题？&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;答案：&lt;/strong&gt; ABA 问题指的是在并发环境下，一个值从A变成B再变成A，而某个线程在这个过程中可能发生误判。CAS 通过引入版本号或者时间戳等机制来解决 ABA 问题。在 CAS 操作中，除了比较当前值是否等于预期值外，还需要比较版本号等辅助信息。如果版本号发生变化，则 CAS 操作失败，即使值从 A 变成 B 再变成 A，版本号也会相应变化，从而保证了正确性。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ol&gt;
</content:encoded></item><item><title>JVM虚拟机核心知识体系</title><link>https://tangtangit.com/posts/java%E6%A0%B8%E5%BF%83-05-jvm/</link><guid isPermaLink="true">https://tangtangit.com/posts/java%E6%A0%B8%E5%BF%83-05-jvm/</guid><description>从JVM组成结构（类加载子系统、运行时数据区、执行引擎）、对象创建与内存布局、字节码文件结构到JVM调优，构建完整的JVM知识图谱。</description><pubDate>Sun, 18 Jan 2026 00:00:00 GMT</pubDate><content:encoded>&lt;h2&gt;组成&lt;/h2&gt;
&lt;h3&gt;类加载子系统（Class Loader SubSystem）&lt;/h3&gt;
&lt;h4&gt;类的生命周期&lt;/h4&gt;
&lt;ul&gt;
&lt;li&gt;类加载
&lt;ul&gt;
&lt;li&gt;过程
&lt;ul&gt;
&lt;li&gt;通过类的全限定名，获取定义此类的二进制字节流&lt;/li&gt;
&lt;li&gt;将这个字节流所代表的静态存储结构转化为方法区的运行时数据结构&lt;/li&gt;
&lt;li&gt;在内存中生成一个代表这个类的 java.lang.Class 对象，作为方法区这个类的各种数据的访问入口&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;类加载器
&lt;ul&gt;
&lt;li&gt;启动类加载器（Bootstrap ClassLoader）：负责加载JRE核心类库，像JRE中的rt.jar等（C/C++）&lt;/li&gt;
&lt;li&gt;扩展类加载器（Extension ClassLoader）：负责加载JRE扩展目录ext中的jar包，由Java语言实现，父类加载器为null&lt;/li&gt;
&lt;li&gt;系统类加载器（Application ClassLoader）：负责加载ClassPath路径下的类包，由Java语言实现，父类加载器为ExtClassLoader&lt;/li&gt;
&lt;li&gt;自定义类加载器（UserDefined ClassLoader）
&lt;ul&gt;
&lt;li&gt;步骤
&lt;ul&gt;
&lt;li&gt;继承ClassLoader&lt;/li&gt;
&lt;li&gt;重写findClass方法&lt;/li&gt;
&lt;li&gt;重写loadClass方法（如果要打破双亲委派模型）&lt;/li&gt;
&lt;li&gt;重写defineClass（如果需要加载外部类）&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;应用
&lt;ul&gt;
&lt;li&gt;对于非.class的文件，需要转为Java类&lt;/li&gt;
&lt;li&gt;加密：Java代码可以轻易的被反编译，如果你需要把自己的代码进行加密以防止反编译&lt;/li&gt;
&lt;li&gt;从非标准的来源加载代码：如果你的字节码是放在数据库、甚至是在云端&lt;/li&gt;
&lt;li&gt;Tomcat的自定义类加载器：详见Tomcat类加载器图&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;类加载机制
&lt;ul&gt;
&lt;li&gt;全盘负责委托机制：由一个ClassLoader加载一个类和与其相关的其他类&lt;/li&gt;
&lt;li&gt;双亲委派机制
&lt;ul&gt;
&lt;li&gt;概念：向上委托，向下加载&lt;/li&gt;
&lt;li&gt;异常示例
&lt;ul&gt;
&lt;li&gt;自定义java.lang.String，并运行main
&lt;ul&gt;
&lt;li&gt;异常：在类java.lang.String中找不到main方法&lt;/li&gt;
&lt;li&gt;原因：main所在的类会触发初始化，且该类由根加载器加载，String不存在main方法&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;自定义java.lang.**，并运行main
&lt;ul&gt;
&lt;li&gt;异常：java.lang.SecurityException：Prohibited package name：java.lang&lt;/li&gt;
&lt;li&gt;原因：无权访问，main所在的类会触发初始化，且该类由根加载器加载，获取不到类，禁止使用该包名&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;优点
&lt;ul&gt;
&lt;li&gt;避免类的重复加载&lt;/li&gt;
&lt;li&gt;Java核心API中定义类型不会被随意替换&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;沙箱安全机制：对java源代码的保护，如防止上述异常示例&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;缓存机制：缓存机制将会保证所有加载过的Class都会被缓存，当程序中需要使用某个Class时，类加载器先从缓存区寻找该Class，只有缓存区不存在，系统才会读取该类对应的二进制数据，并将其转换成Class对象，存入缓存区。这就是为什么修改了Class后，必须重启JVM，程序的修改才会生效&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;类加载方式
&lt;ul&gt;
&lt;li&gt;使用Class静态方法Class.forName&lt;/li&gt;
&lt;li&gt;使用ClassLoader：this.getClass().getClassLoader()&lt;/li&gt;
&lt;li&gt;使用new&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;连接
&lt;ul&gt;
&lt;li&gt;验证
&lt;ul&gt;
&lt;li&gt;目的：确保Class文件的字节流中包含的信息符合当前虚拟机的要求，并不会危害虚拟机的自身安全&lt;/li&gt;
&lt;li&gt;分类
&lt;ul&gt;
&lt;li&gt;文件格式验证
&lt;ul&gt;
&lt;li&gt;是否以魔数0xCAFEBABE开头&lt;/li&gt;
&lt;li&gt;主、次版本号是否在当前虚拟机处理范围之内&lt;/li&gt;
&lt;li&gt;常量池的常量中是否有不被支持的常量类型（检查常量tag标志）&lt;/li&gt;
&lt;li&gt;指向常量的各种索引值中是否有指向不存在的常量或不符合类型的常量&lt;/li&gt;
&lt;li&gt;CONSTANT_Utf8_info型的常量中是否有不符合UTF8编码的数据&lt;/li&gt;
&lt;li&gt;Class文件中各个部分及文件本身是否有被删除的或附加的其他信息&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;元数据验证
&lt;ul&gt;
&lt;li&gt;这个类是否有父类（除了java.lang.Object之外，所有类都应当有父类）&lt;/li&gt;
&lt;li&gt;这个类是否继承了不允许被继承的类（被final修饰的类）&lt;/li&gt;
&lt;li&gt;如果这个类不是抽象类，是否实现了其父类或接口之中所要求实现的所有方法&lt;/li&gt;
&lt;li&gt;类中的字段、方法是否与父类产生矛盾（例如覆盖了父类的final字段，或者出现不符合规则的方法重载，例如方法参数都一致，但返回值类型却不同等等）&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;字节码验证
&lt;ul&gt;
&lt;li&gt;目的：通过数据流和控制流分析，确定程序语义是合法的、符合逻辑的。这个阶段将对类的方法体进行校验分析，保证被校验类的方法在运行时不会产生危害虚拟机安全的事件&lt;/li&gt;
&lt;li&gt;分类
&lt;ul&gt;
&lt;li&gt;保证任意时刻操作数栈的数据类型与指令代码序列都能配合工作，例如不会出现类似这样的情况：在操作数栈放置了一个int类型的数据，使用时却按long类型来加载入本地变量表中&lt;/li&gt;
&lt;li&gt;保证跳转指令不会跳转到方法体以外的字节码指令上&lt;/li&gt;
&lt;li&gt;保证方法体中的类型转换是有效的，例如可以把一个子类对象赋值给父类数据类型，但是把父类对象赋值给子类数据类型，甚至把对象赋值给与它毫无继承关系、完全不相干的一个数据类型，则是危险不合法的&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;符号引用验证
&lt;ul&gt;
&lt;li&gt;目的：类对自身以外（常量池中的各种符号引用）的信息进行匹配性校验&lt;/li&gt;
&lt;li&gt;分类
&lt;ul&gt;
&lt;li&gt;符号引用中通过字符串描述的全限定名是否能够找到对应的类&lt;/li&gt;
&lt;li&gt;在指定类中是否存在符合方法的字段描述符以及简单名称所描述的方法和字段&lt;/li&gt;
&lt;li&gt;符号引用中的类、字段、方法的访问性（private、protected、public、default）是否可被当前类访问&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;准备
&lt;ul&gt;
&lt;li&gt;概念：准备阶段的工作就是为类的静态变量分配内存并设为jvm默认的初值，对于非静态的变量，则不会为它们分配内存。有一点需要注意，这时候，静态变量的初值为jvm默认的初值，而不是我们在程序中设定的初值&lt;/li&gt;
&lt;li&gt;基本类型（int、long、short、char、byte、boolean、float、double、reference）的默认值为（0、0L、（short）0、‘\u0000’、（byte）0、false、0.0f、0.0d、null）&lt;/li&gt;
&lt;li&gt;常量的默认值为我们程序中设定的值，比如我们在程序中定义final static int a = 100，则准备阶段中a的初值就是100&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;解析
&lt;ul&gt;
&lt;li&gt;作用：把常量池中的符号引用转换为直接引用，jvm会将所有的类或接口名、字段名、方法名转换为具体的内存地址&lt;/li&gt;
&lt;li&gt;分类
&lt;ul&gt;
&lt;li&gt;前置条件：假设当前代码所处的类为D，如果要把一个从未解析过的符号引用N解析为一个类或接口C的引用&lt;/li&gt;
&lt;li&gt;类或接口的解析
&lt;ul&gt;
&lt;li&gt;如果C不是一个数组类型，那虚拟机将会把代表N的全限定名传递给D的类加载器去加载这个类C&lt;/li&gt;
&lt;li&gt;如果C是一个数组类型，并且数组的元素类型为对象，那将会按照第1点的规则加载数组元素类型&lt;/li&gt;
&lt;li&gt;访问权限，不具备则抛出java.lang.IllegalAccessError异常&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;字段解析
&lt;ul&gt;
&lt;li&gt;解析字段表内class_index项中索引的CONSTANT_Class_info符号引用&lt;/li&gt;
&lt;li&gt;如果C 本身就包含了简单名称和字段描述符都与目标相匹配的字段，则返回这个字段的直接引用，查找结束&lt;/li&gt;
&lt;li&gt;否则，如果C中实现了接口，将会按照继承关系从下往上递归搜索各个接口和它的父接口如果接口中包含了简单名称和字段描述符都与目标相匹配的字段，则返回这个字段的直接引用，查找结束&lt;/li&gt;
&lt;li&gt;否则，如果C 不是java.lang.Object的话，将会按照继承关系从下往上递归搜索其父类，如果在父类中包含了简单名称和字段描述符都与目标相匹配的字段，则返回这个字段的直接引用，查找结束&lt;/li&gt;
&lt;li&gt;否则，查找失败，抛出java.lang.NoSuchFieldError异常&lt;/li&gt;
&lt;li&gt;访问权限，不具备则抛出java.lang.IllegalAccessError异常&lt;/li&gt;
&lt;li&gt;注：如果有一个同名字段同时出现在C的接口和父类中，或者同时在自己的父类或多个接口中出现，那编译器可能拒绝编译，并提示”The field xxx is ambiguous”&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;类方法解析
&lt;ul&gt;
&lt;li&gt;解析类方法表内class_index项中索引的CONSTANT_Class_info符号引用&lt;/li&gt;
&lt;li&gt;类方法和接口方法符号引用的常量类型定义是分开的，如果在类方法表中发现class_index中索引的C 是个接口，那就直接抛出java.lang.IncompatibleClassChangeError异常&lt;/li&gt;
&lt;li&gt;如果通过了第一步，在类C 中查找是否有简单名称和描述符都与目标相匹配的方法，如果有则返回这个方法的直接引用，查找结束&lt;/li&gt;
&lt;li&gt;否则，在类C的父类中递归查找是否有简单名称和描述符都与目标相匹配的方法，如果有则返回这个方法的直接引用，查找结束&lt;/li&gt;
&lt;li&gt;否则，在类C实现的接口列表以及他们的父接口中递归查找是否有简单名称和描述符都与目标相匹配的方法，如果存在相匹配的方法，说明类C是一个抽象类这时查找结束，抛出java.lang.AbstractMethodError异常&lt;/li&gt;
&lt;li&gt;否则，宣告方法查找失败，抛出java.lang.NoSuchMethodError&lt;/li&gt;
&lt;li&gt;访问权限，不具备则抛出java.lang.IllegalAccessError异常&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;接口方法解析
&lt;ul&gt;
&lt;li&gt;解析接口方法表内class_index项中索引的CONSTANT_Class_info符号引用&lt;/li&gt;
&lt;li&gt;与类解析方法不同，如果在接口方法表中发现class_index中的索引C是个类而不是个接口，那就直接抛出java.lang.IncompatibleClassChangeError异常&lt;/li&gt;
&lt;li&gt;否则，在接口C中查找是否有简单名称和描述符都与目标相匹配的方法，如果有则返回这个方法的直接引用，查找结束&lt;/li&gt;
&lt;li&gt;否则，在接口C的父接口中递归查找，直到java.lang.Object类（查找范围包括Object类）为止，看是否有简单名称和描述符都与目标相匹配的方法，如果有则返回这个方法的直接引用，查找结束&lt;/li&gt;
&lt;li&gt;否则，宣告方法查找失败，抛出java.lang.NoSuchMethodError&lt;/li&gt;
&lt;li&gt;访问权限，接口中所有的方法默认都是public的，所以不存在访问权限的问题&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;符号引用
&lt;ul&gt;
&lt;li&gt;概念：符号引用以一组符号来描述所引用的目标，符号可以是任何形式的字面量，只要使用时能够无歧义的定位到目标即可。&lt;/li&gt;
&lt;li&gt;示例
&lt;ul&gt;
&lt;li&gt;例如，在Class文件中它以CONSTANT_Class_info、CONSTANT_Fieldref_info、CONSTANT_Methodref_info等类型的常量出现，在编译时，java类并不知道所引用的类的实际地址，因此只能使用符号引用来代替。&lt;/li&gt;
&lt;li&gt;org.simple.Language类，在编译时People类并不知道Language类的实际内存地址，因此只能使用符号org.simple.Language（假设是这个，当然实际中是由类似于CONSTANT_Class_info的常量来表示的）来表示Language类的地址。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;特性：各种虚拟机实现的内存布局可能有所不同，但是它们能接受的符号引用都是一致的，因为符号引用的字面量形式明确定义在Java虚拟机规范的Class文件格式中。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;直接引用
&lt;ul&gt;
&lt;li&gt;分类
&lt;ul&gt;
&lt;li&gt;直接指向目标的指针（比如，指向“类型”【Class对象】、类变量、类方法的直接引用可能是指向方法区的指针）&lt;/li&gt;
&lt;li&gt;相对偏移量（比如，指向实例变量、实例方法的直接引用都是偏移量）&lt;/li&gt;
&lt;li&gt;一个能间接定位到目标的句柄&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;特性：直接引用是和虚拟机的布局相关的，同一个符号引用在不同的虚拟机实例上翻译出来的直接引用一般不会相同。如果有了直接引用，那引用的目标必定已经被加载入内存中了&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;初始化
&lt;ul&gt;
&lt;li&gt;概念：为静态变量赋予正确的初始值，此阶段才是程序员编写的程序变量赋予真正的初始值，执行静态代码块&lt;/li&gt;
&lt;li&gt;&amp;lt;clinit&amp;gt;
&lt;ul&gt;
&lt;li&gt;概念：初始化阶段就是执行类构造器方法&amp;lt;clinit&amp;gt;&lt;/li&gt;
&lt;li&gt;来源：javac编译器自动收集类中的所有类变量的赋值动作和静态代码块中的语句合并而来&lt;/li&gt;
&lt;li&gt;&amp;lt;init&amp;gt;：构造器&lt;/li&gt;
&lt;li&gt;注：子类的&amp;lt;clinit&amp;gt;需在父类的&amp;lt;clinit&amp;gt;后运行，虚拟机必须保证&amp;lt;clinit&amp;gt;在多线程下被同步加锁&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;类初始化时机
&lt;ul&gt;
&lt;li&gt;主动引用
&lt;ul&gt;
&lt;li&gt;虚拟机启动时被标为启动类的类（包含 main() 方法的类）&lt;/li&gt;
&lt;li&gt;在类的字节码中遇到new、getstatic、putstatic、invokestatic这四条指令时，会触发类的初始化
&lt;ul&gt;
&lt;li&gt;这四条字节码指令分别对应创建类的实例、访问一个类的静态字段、设置一个类的静态字段、调用一个类的静态方法&lt;/li&gt;
&lt;li&gt;这里的静态字段不包括在编译期就能确定的静态常量，因为静态常量会存放到调用方的常量池中，本质上没有直接引用到定义常量的类，因此不会触发定义常量的类的初始化&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;当初始化类的时候，如果父类还没有进行过初始化，则需要先触发其父类的初始化&lt;/li&gt;
&lt;li&gt;使用反射 API 对类进行反射调用时，会初始化这个类&lt;/li&gt;
&lt;li&gt;当初次调用 MethodHandle 实例时，初始化该 MethodHandle 指向的方法所在的类&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;被动引用
&lt;ul&gt;
&lt;li&gt;静态字段：对于静态字段，只有直接定义这个字段的类才会被初始化，因此通过其子类来引用父类中定义的静态字段，只会触发父类的初始化而不会触发子类的初始化&lt;/li&gt;
&lt;li&gt;数组：通过数组定义来引用类，不会触发此类的初始化，因为数组由 Java 虚拟机直接生成的
&lt;ul&gt;
&lt;li&gt;原始类型的数组变量由newarray创建&lt;/li&gt;
&lt;li&gt;引用类型的数组变量由anewarray创建&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;常量字段编译期不确定：当一个常量字段的值在编译期不确定时(如UUID.random().toString())，那么它不会被放到调用类的常量池中。因此即便这个静态字段是一个常量（被final关键字修饰），但由于它在编译期是不确定的，所以在程序运行时还是会主动使用这个常量所在的类，从而触发常量所在类的初始化&lt;/li&gt;
&lt;li&gt;父类接口：一个接口在初始化时，并不要求其父接口全部都完成了初始化，只有在真正使用到父接口的时候（如引用接口中运行期才确定的常量）才会初始化。
&lt;ul&gt;
&lt;li&gt;注：由于接口是无法定义静态代码块的，所以无法像类那样通过类静态代码块的执行与否判断是否发生初始化。但是在接口的初始化过程中，编译器同样会为接口生成&amp;lt;clinit&amp;gt;()构造器，用于初始化接口中所定义的成员变量。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;在初始化阶段，虚拟机会为类的静态变量赋予正确的初始值，当然接口类也会遵从这条规定。所以我们可以通过初始化阶段对静态字段的赋值来观察接口类是否进行了初始化&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;使用：通过执行引擎执行相应类的方法&lt;/li&gt;
&lt;li&gt;卸载
&lt;ul&gt;
&lt;li&gt;程序正常执行结束&lt;/li&gt;
&lt;li&gt;执行中出现异常&lt;/li&gt;
&lt;li&gt;操作系统出现异常&lt;/li&gt;
&lt;li&gt;通过执行 system.exit()&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;运行时数据区（Runtime Data Areas即JVM内存模型）&lt;/h3&gt;
&lt;p&gt;&lt;img src=&quot;%E5%86%85%E5%AD%98%E6%A8%A1%E5%9E%8B.jpg&quot; alt=&quot;JVM内存模型&quot; /&gt;&lt;/p&gt;
&lt;h4&gt;组成&lt;/h4&gt;
&lt;ul&gt;
&lt;li&gt;线程私有
&lt;ul&gt;
&lt;li&gt;虚拟机栈
&lt;ul&gt;
&lt;li&gt;Java虚拟机栈（Java Virtual Machine Stacks）是线程私有的，它的生命周期与线程相同&lt;/li&gt;
&lt;li&gt;组成：栈帧
&lt;ul&gt;
&lt;li&gt;局部变量表
&lt;ul&gt;
&lt;li&gt;存储对象
&lt;ul&gt;
&lt;li&gt;方法参数&lt;/li&gt;
&lt;li&gt;局部变量&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;容量
&lt;ul&gt;
&lt;li&gt;slot复用：变量脱离作用域后&lt;/li&gt;
&lt;li&gt;单位：变量槽（Variable Slot）
&lt;ul&gt;
&lt;li&gt;大小：只能存放32位以内的数据类型，64位的double及long需使用两个slot,且不允许单独访问其中一个&lt;/li&gt;
&lt;li&gt;32位数据类型
&lt;ul&gt;
&lt;li&gt;boolean&lt;/li&gt;
&lt;li&gt;byte&lt;/li&gt;
&lt;li&gt;char&lt;/li&gt;
&lt;li&gt;short&lt;/li&gt;
&lt;li&gt;int&lt;/li&gt;
&lt;li&gt;float&lt;/li&gt;
&lt;li&gt;reference：引用类型&lt;/li&gt;
&lt;li&gt;returnAddress&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;最大值：方法的code属性的max_locals数据项中确定了该方法所需要分配的局部变量表的最大容量&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;slot分配（通过索引定位）
&lt;ul&gt;
&lt;li&gt;0处存放this（实例方法和构造方法&amp;lt;init&amp;gt;）&lt;/li&gt;
&lt;li&gt;其余按方法体内部定义的变量顺序和作用域分配&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;注：变量使用前需确定初始值&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;操作数栈
&lt;ul&gt;
&lt;li&gt;数据结构：栈FILO&lt;/li&gt;
&lt;li&gt;容量
&lt;ul&gt;
&lt;li&gt;32位数据类型所占的栈容量为1，64位数据类型所占的栈容量为2&lt;/li&gt;
&lt;li&gt;最大值：方法的code属性的max_stack数据项中确定了该方法所需要分配的局部变量表的最大容量&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;作用
&lt;ul&gt;
&lt;li&gt;算术运算&lt;/li&gt;
&lt;li&gt;调用其他方法时参数传递&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;动态连接
&lt;ul&gt;
&lt;li&gt;概念：每个栈帧都包含一个指向运行时常量池中该栈帧所属方法的引用，持有这个引用是为了支持方法调用过程中的动态连接(Dynamic Linking)&lt;/li&gt;
&lt;li&gt;静态连接：类加载解析阶段符号引用转化为直接引用&lt;/li&gt;
&lt;li&gt;过程：当虚拟机运行时，需要从常量池获得对应的符号引用，再在类创建时或运行时解析、翻译到具体的内存地址之中&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;方法出口
&lt;ul&gt;
&lt;li&gt;概念：在方法退出之后，都需要返回到方法被调用的位置，程序才能继续执行，方法返回时可能需要在栈帧中保存一些信息，用来帮助恢复它的上层方法的执行状态&lt;/li&gt;
&lt;li&gt;退出方式
&lt;ul&gt;
&lt;li&gt;正常退出：有返回值，则一并返回，调用者的PC计数器的值可以作为返回地址&lt;/li&gt;
&lt;li&gt;异常退出：无返回值，返回地址是要通过异常处理器表来确定的&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;一些附加信息&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;栈顶缓存技术：将栈顶元素全部缓存在物理CPU的寄存器中，以此降低对内存的读/写次数，提升执行引擎的执行效率&lt;/li&gt;
&lt;li&gt;异常
&lt;ul&gt;
&lt;li&gt;StackoverflowError：固定大小，线程请求分配的栈容量超过Java虚拟机栈允许的最大容量
&lt;ul&gt;
&lt;li&gt;栈帧过多&lt;/li&gt;
&lt;li&gt;局部变量表数据过多&lt;/li&gt;
&lt;li&gt;操作数栈过深&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;OutOfMemoryError：动态扩展，没有足够的内存去创建对应的虚拟机栈&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;命令
&lt;ul&gt;
&lt;li&gt;-Xss：设置栈的大小&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;本地方法栈
&lt;ul&gt;
&lt;li&gt;概念：本地方法栈则是为虚拟机使用到的 Native 方法服务，结合JNI使用&lt;/li&gt;
&lt;li&gt;数据结构：栈FILO&lt;/li&gt;
&lt;li&gt;异常
&lt;ul&gt;
&lt;li&gt;StackoverflowError：固定大小（用参数-Xss设置），程请求分配的栈容量超过Java虚拟机栈允许的最大容量,&lt;/li&gt;
&lt;li&gt;OutOfMemoryError：动态扩展，没有足够的内存去创建对应的虚拟机栈&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;程序计数器：一块较小的内存空间，它可以看作是当前线程所执行的字节码的行号指示器。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;线程共享
&lt;ul&gt;
&lt;li&gt;堆
&lt;ul&gt;
&lt;li&gt;存储：对象数据&lt;/li&gt;
&lt;li&gt;组成
&lt;ul&gt;
&lt;li&gt;新生代
&lt;ul&gt;
&lt;li&gt;Eden&lt;/li&gt;
&lt;li&gt;S0&lt;/li&gt;
&lt;li&gt;S1&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;老年代&lt;/li&gt;
&lt;li&gt;永久代：JDK1.8废除&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;引用
&lt;ul&gt;
&lt;li&gt;强引用(StrongReference)
&lt;ul&gt;
&lt;li&gt;概念：当内存空间不足时，Java虚拟机宁愿抛出OutOfMemoryError错误，使程序异常终止，也不会靠随意回收具有强引用的对象来解决内存不足的问题&lt;/li&gt;
&lt;li&gt;应用场景：对象的一般状态&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;软引用(SoftReference)
&lt;ul&gt;
&lt;li&gt;概念：内存空间充足时，垃圾回收器就不会回收它；如果内存空间不足了，就会回收这些对象的内存&lt;/li&gt;
&lt;li&gt;应用场景：对象缓存&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;弱引用(WeakReference)
&lt;ul&gt;
&lt;li&gt;概念：弱引用的对象拥有更短暂的生命周期。在垃圾回收器线程扫描它所管辖的内存区域的过程中，一旦发现了只具有弱引用的对象，不管当前内存空间足够与否，都会回收它的内存&lt;/li&gt;
&lt;li&gt;应用场景：对象缓存&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;虚引用(PhantomReference)
&lt;ul&gt;
&lt;li&gt;概念：虚引用并不会决定对象的生命周期。如果一个对象仅持有虚引用，那么它就和没有任何引用一样，在任何时候都可能被垃圾回收器回收&lt;/li&gt;
&lt;li&gt;应用场景：跟踪对象的垃圾回收&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;内存分配
&lt;ul&gt;
&lt;li&gt;实现方式
&lt;ul&gt;
&lt;li&gt;指针碰撞
&lt;ul&gt;
&lt;li&gt;原理：假设Java堆中内存时完整的，已分配的内存和空闲内存分别在不同的一侧，通过一个指针作为分界点，需要分配内存时，仅仅需要把指针往空闲的一端移动与对象大小相等的距离&lt;/li&gt;
&lt;li&gt;应用场景：Serial、ParNew，适用堆内存规整（即没有内存碎片）的情况下&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;空闲列表
&lt;ul&gt;
&lt;li&gt;原理：事实上，Java堆的内存并不是完整的，已分配的内存和空闲内存相互交错，JVM通过维护一个列表，记录可用的内存块信息，当分配操作发生时，从列表中找到一个足够大的内存块分配给对象实例，并更新列表上的记录&lt;/li&gt;
&lt;li&gt;CMS，适用堆内存不规整的情况下&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;并发解决
&lt;ul&gt;
&lt;li&gt;CAS： CAS 是乐观锁的一种实现方式。所谓乐观锁就是，每次不加锁而是假设没有冲突而去完成某项操作，如果因为冲突失败就重试，直到成功为止。虚拟机采用 CAS 配上失败重试的方式保证更新操作的原子性&lt;/li&gt;
&lt;li&gt;TLAB： 为每一个线程预先分配一块内存，JVM在给线程中的对象分配内存时，首先在TLAB分配，当对象大于TLAB中的剩余内存或TLAB的内存已用尽时，再采用上述的CAS进行内存分配&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;GC
&lt;ul&gt;
&lt;li&gt;算法
&lt;ul&gt;
&lt;li&gt;存活性判断
&lt;ul&gt;
&lt;li&gt;引用计数
&lt;ul&gt;
&lt;li&gt;原理：给对象中添加一个引用计数器，每当一个地方引用这个对象时，计数器值+1；当引用失效时，计数器值-1；任何时刻计数值为0的对象就是不可能再被使用的&lt;/li&gt;
&lt;li&gt;优点
&lt;ul&gt;
&lt;li&gt;及时回收无效内存，实时性高&lt;/li&gt;
&lt;li&gt;垃圾回收过程中无需挂起&lt;/li&gt;
&lt;li&gt;没有全局扫描，性能高&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;缺点
&lt;ul&gt;
&lt;li&gt;对象的生成、赋值和引用都涉及引用计数的变化，从而导致引用计数的增减处理频繁&lt;/li&gt;
&lt;li&gt;为引用计数分配固定的空间来存放计数，这是固有损耗&lt;/li&gt;
&lt;li&gt;无法处理循环引用&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;可达性分析
&lt;ul&gt;
&lt;li&gt;原理：通过一系列称为“GC Roots”的对象作为起始点，从这些节点向下搜索，搜索所走过的路径称为引用链，当一个对象到GC Roots没有任何引用链（即GC Roots到对象不可达）时，则证明此对象是不可用的&lt;/li&gt;
&lt;li&gt;两次标记过程
&lt;ul&gt;
&lt;li&gt;如果对象在进行可达性分析后发现没有与GC Roots相连接的引用链，则将会被第一次标记并且进行一次筛选&lt;/li&gt;
&lt;li&gt;若这个对象被判定有必要执行finalize()方法，则该对象放置在一个叫F-Queue队列中，并之后由VM自动建立的、低优先级的Finalizer线程去执行它&lt;/li&gt;
&lt;li&gt;finalize()方法是对象逃脱死亡的最后一次机会，稍后GC将对F-Queue队列中的对象进行第二次小规模的标记，若finalize()能够救活自己，则第二次标记时，将被移除出“即将回收”的集合；若对象没有完成自救，则回收&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;GC Roots
&lt;ul&gt;
&lt;li&gt;局部变量表中引用的对象&lt;/li&gt;
&lt;li&gt;类变量引用的对象&lt;/li&gt;
&lt;li&gt;字符串常量引用的对象&lt;/li&gt;
&lt;li&gt;本地方法栈中JNI(Native方法)引用的对象&lt;/li&gt;
&lt;li&gt;异常类、基本类型的Class、系统类加载器&lt;/li&gt;
&lt;li&gt;synchronize持有的对象&lt;/li&gt;
&lt;li&gt;本地缓存&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;优点
&lt;ul&gt;
&lt;li&gt;解决了循环引用问题&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;三色标记
&lt;ul&gt;
&lt;li&gt;原理
&lt;ul&gt;
&lt;li&gt;基于GC Roots获取存活对象&lt;/li&gt;
&lt;li&gt;白色：还没有搜索过的对象&lt;/li&gt;
&lt;li&gt;灰色：正在搜索的对象&lt;/li&gt;
&lt;li&gt;黑色：搜索完成的对象&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;并发标记问题
&lt;ul&gt;
&lt;li&gt;多标-浮动垃圾
&lt;ul&gt;
&lt;li&gt;正在扫描的灰色对象，引用被断开&lt;/li&gt;
&lt;li&gt;并发标记开始后的新对象，直接染成黑色&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;漏标
&lt;ul&gt;
&lt;li&gt;条件
&lt;ul&gt;
&lt;li&gt;灰色对象断开了白色对象的引用&lt;/li&gt;
&lt;li&gt;黑色对象重新引用了该白色对象&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;解决方法
&lt;ul&gt;
&lt;li&gt;读写屏障
&lt;ul&gt;
&lt;li&gt;写屏障用于拦截第二和第三步&lt;/li&gt;
&lt;li&gt;读屏障则是拦截第一步&lt;/li&gt;
&lt;li&gt;在读写前后，将对象G给记录下来&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;增量更新（CMS）：当一个白色对象被一个黑色对象引用，将黑色对象重新标记为灰色，让垃圾回收器重新扫描，但效率较低。（破坏条件二）
&lt;ul&gt;
&lt;li&gt;将黑色染灰&lt;/li&gt;
&lt;li&gt;将白色染灰&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;原始快照：SATB（G1及ZGC）：当引用发生变化之前，记录下原来的引用对象，既原始快照，当灰色对象和白色对象之间的引用马上被断掉时，将这个引用记录下来，使GC依旧能够访问到，那样白色就不会漏标。（破坏条件一）
&lt;ul&gt;
&lt;li&gt;在开始标记的时候生成一个快照图，标记存活对象&lt;/li&gt;
&lt;li&gt;在并发标记的时候所有被改变的对象入队（在write barrier【写屏障】里把所有旧的引用所指向的对象都变成非白的）&lt;/li&gt;
&lt;li&gt;可能存在浮动垃圾，将在下次被收集&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;垃圾回收
&lt;ul&gt;
&lt;li&gt;标记-清理
&lt;ul&gt;
&lt;li&gt;原理：标记后直接清理&lt;/li&gt;
&lt;li&gt;特点：清除后会产生大量不连续的内存碎片空间，碎片空间太多可能会导致当程序后续需要创建较大对象时，无法找到足够连续的内存空间而不得不再次触发垃圾回收&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;标记-压缩
&lt;ul&gt;
&lt;li&gt;原理：在清理的同时整理内存&lt;/li&gt;
&lt;li&gt;特点：避免内存碎片化&lt;/li&gt;
&lt;li&gt;应用场景：生命周期长、回收频率低，但注重回收一次内存空间得到足够释放的场景&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;复制
&lt;ul&gt;
&lt;li&gt;原理：将内存划分为两块区域，每次使用其中一块，当其中一块用满，触发垃圾回收的时候，将存活的对象复制到另一块上去，然后把之前使用的那一块进行格式化，一次性清除干净&lt;/li&gt;
&lt;li&gt;特点：内存空间利用率低&lt;/li&gt;
&lt;li&gt;应用场景：那些生命周期短、回收频率高的内存对象&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;增量更新
&lt;ul&gt;
&lt;li&gt;原理：每次收集部分&lt;/li&gt;
&lt;li&gt;特点：延迟低，线程切换开销较大&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;分区收集
&lt;ul&gt;
&lt;li&gt;原理：内存分区，回收价值最高的区域&lt;/li&gt;
&lt;li&gt;特点：在吞吐量和低延迟间取得较好的平衡&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;回收过程
&lt;ul&gt;
&lt;li&gt;Minor GC与Full GC
&lt;ul&gt;
&lt;li&gt;在初始阶段，新创建的对象被分配到Eden区，survivor的两块空间都为空&lt;/li&gt;
&lt;li&gt;当Eden区满了的时候，Minor GC被触发&lt;/li&gt;
&lt;li&gt;经过扫描与标记，存活的对象被复制到S0，不存活的对象被回收， 并且存活的对象年龄都增大一岁&lt;/li&gt;
&lt;li&gt;在下一次的Minor GC中，Eden区的情况和上面一致，没有引用的对象被回收，存活的对象被复制到survivor区。当Eden 和 s0区空间满了，S0的所有的数据都被复制到S1，需要注意的是，在上次minor GC过程中移动到S0中的两个对象在复制到S1后其年龄要加1。此时Eden区S0区被清空，所有存活的数据都复制到了S1区&lt;/li&gt;
&lt;li&gt;再下一次MinorGC则重复这个过程，这一次survivor的两个区对换，存活的对象被复制到S0，存活的对象年龄加1，Eden区和另一个survivor区被清空&lt;/li&gt;
&lt;li&gt;再经过几次Minor GC之后，当存活对象的年龄达到一个阈值之后（-XX：MaxTenuringThreshold默认是15），就会被从年轻代Promotion到老年代&lt;/li&gt;
&lt;li&gt;随着MinorGC一次又一次的进行，不断会有新的对象被promote到老年代&lt;/li&gt;
&lt;li&gt;老年代空间不足发生Major/Full GC&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;分类
&lt;ul&gt;
&lt;li&gt;Minor GC
&lt;ul&gt;
&lt;li&gt;回收区间
&lt;ul&gt;
&lt;li&gt;新生代&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;触发条件
&lt;ul&gt;
&lt;li&gt;Eden区满时&lt;/li&gt;
&lt;li&gt;注：Survivor区满时不会触发&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;Major GC
&lt;ul&gt;
&lt;li&gt;回收区间
&lt;ul&gt;
&lt;li&gt;老年代&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;触发条件
&lt;ul&gt;
&lt;li&gt;老年代满时&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;Full GC
&lt;ul&gt;
&lt;li&gt;回收区间
&lt;ul&gt;
&lt;li&gt;新生代&lt;/li&gt;
&lt;li&gt;老年代&lt;/li&gt;
&lt;li&gt;元空间&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;触发条件
&lt;ul&gt;
&lt;li&gt;调用System.gc时，系统建议执行Full GC，但是不必然执行&lt;/li&gt;
&lt;li&gt;老年代空间不足&lt;/li&gt;
&lt;li&gt;通过Minor GC后进入老年代的平均大小大于老年代的可用内存&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;对象进入老年代的情况
&lt;ul&gt;
&lt;li&gt;假如进行Minor GC时发现，存活的对象在ToSpace区中存不下，那么把存活的对象存入老年代&lt;/li&gt;
&lt;li&gt;大对象直接进入老年代，假设新创建的对象很大，比如为5M(这个值可以通过PretenureSizeThreshold这个参数进行设置，默认3M)，那么即使Eden区有足够的空间来存放，也不会存放在Eden区，而是直接存入老年代&lt;/li&gt;
&lt;li&gt;长期存活的对象将进入老年代&lt;/li&gt;
&lt;li&gt;动态对象年龄判定，如果在From空间中，相同年龄所有对象的大小总和大于Survivor空间的一半，那么年龄大于等于该年龄的对象就会被移动到老年代，而不用等到15岁(默认)&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;空间分配担保
&lt;ul&gt;
&lt;li&gt;在发生Minor GC之前，虚拟机会先检查老年代最大可用的连续空间是否大于新生代所有对象总空间
&lt;ul&gt;
&lt;li&gt;成立：Minor GC可以确保是安全的&lt;/li&gt;
&lt;li&gt;不成立：虚拟机会查看HandlerPromotionFailure（JDK1.7失效）这个参数设置的值(true或flase)是否允许担保失败
&lt;ul&gt;
&lt;li&gt;true：那么会继续检查老年代最大可用的连续空间是否大于历次晋升到老年代对象的平均大小，如果大于，将尝试进行一次Minor GC，尽管这次Minor GC是有风险的&lt;/li&gt;
&lt;li&gt;false：这次Minor GC将升级为Full GC&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;垃圾回收器
&lt;ul&gt;
&lt;li&gt;前置
&lt;ul&gt;
&lt;li&gt;吞吐量
&lt;ul&gt;
&lt;li&gt;概念：CPU用于运行用户代码时间与CPU总消耗时间的比值，即吞吐量=运行用户代码时间/（运行用户代码时间+垃圾收集时间）&lt;/li&gt;
&lt;li&gt;特性：吞吐量越大，暂停时间越长，响应时间越长，反之吞吐量越小，暂停时间越短，响应时间越短&lt;/li&gt;
&lt;li&gt;参数设置
&lt;ul&gt;
&lt;li&gt;-XX:MaxGCPauseMillis：最大垃圾收集停顿时间&lt;/li&gt;
&lt;li&gt;-XX:GCTimeRatio：吞吐量大小&lt;/li&gt;
&lt;li&gt;-XX:+UseAdaptiveSizePolicy：动态调整，以提供最合适的停顿时间或者最大的吞吐量&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;读写屏障
&lt;ul&gt;
&lt;li&gt;写前屏障（pre-write barrier）：所有即将被删除的引用关系的旧引用记录下来，最后以这些旧引用为根重新扫描一遍，例：satb&lt;/li&gt;
&lt;li&gt;写后屏障（post-write barrier）：记录所有新增的引用关系，然后根据这些引用关系为根重新扫描一遍，例：增量更新&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;跨代引用
&lt;ul&gt;
&lt;li&gt;Card Table&lt;/li&gt;
&lt;li&gt;Rset&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;分类
&lt;ul&gt;
&lt;li&gt;Serial
&lt;ul&gt;
&lt;li&gt;特点
&lt;ul&gt;
&lt;li&gt;单线程&lt;/li&gt;
&lt;li&gt;采用标记复制算法&lt;/li&gt;
&lt;li&gt;新生代收集器&lt;/li&gt;
&lt;li&gt;虚拟机运行在Client模式下的默认新生代收集器&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;优点
&lt;ul&gt;
&lt;li&gt;简单高效，对于单个CPU环境而言，Serial收集器由于没有线程交互开销，可以获取最高的单线程收集效率&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;缺点
&lt;ul&gt;
&lt;li&gt;需要STW（Stop The World），停顿时间长&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;配套垃圾回收器
&lt;ul&gt;
&lt;li&gt;Serial Old&lt;/li&gt;
&lt;li&gt;CMS&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;ParNew
&lt;ul&gt;
&lt;li&gt;特点
&lt;ul&gt;
&lt;li&gt;Serial收集器的多线程版本，并行执行&lt;/li&gt;
&lt;li&gt;采用标记复制算法&lt;/li&gt;
&lt;li&gt;新生代收集器&lt;/li&gt;
&lt;li&gt;虚拟机运行在Server模式下的首选新生代收集器&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;优点
&lt;ul&gt;
&lt;li&gt;CPU数量较多时，效率高于Serial&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;缺点
&lt;ul&gt;
&lt;li&gt;单/双CPU的环境中，由于线程交互开销，效率低于Serial&lt;/li&gt;
&lt;li&gt;需要STW（Stop The World），停顿时间长&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;配套垃圾回收器
&lt;ul&gt;
&lt;li&gt;Serial Old&lt;/li&gt;
&lt;li&gt;CMS&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;Parallel Scavenge
&lt;ul&gt;
&lt;li&gt;特点
&lt;ul&gt;
&lt;li&gt;多线程，并行执行&lt;/li&gt;
&lt;li&gt;采用标记复制算法&lt;/li&gt;
&lt;li&gt;新生代收集器&lt;/li&gt;
&lt;li&gt;虚拟机运行在Server模式下的默认新生代收集器&lt;/li&gt;
&lt;li&gt;吞吐量优先&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;优点：吞吐量大，程序运行效率高&lt;/li&gt;
&lt;li&gt;缺点：响应时间较长&lt;/li&gt;
&lt;li&gt;配套垃圾回收器
&lt;ul&gt;
&lt;li&gt;Serial Old&lt;/li&gt;
&lt;li&gt;Parallel Old&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;Serial Old
&lt;ul&gt;
&lt;li&gt;特点
&lt;ul&gt;
&lt;li&gt;Serial收集器的老年代版本&lt;/li&gt;
&lt;li&gt;采用标记整理算法&lt;/li&gt;
&lt;li&gt;老年代收集器&lt;/li&gt;
&lt;li&gt;给Client模式下的虚拟机使用&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;优点
&lt;ul&gt;
&lt;li&gt;简单高效，对于单个CPU环境而言，Serial收集器由于没有线程交互开销，可以获取最高的单线程收集效率&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;缺点
&lt;ul&gt;
&lt;li&gt;需要STW（Stop The World），停顿时间长&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;配套垃圾回收器
&lt;ul&gt;
&lt;li&gt;Serial&lt;/li&gt;
&lt;li&gt;ParNew&lt;/li&gt;
&lt;li&gt;Parallel Scavenge&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;Parallel Old
&lt;ul&gt;
&lt;li&gt;特点
&lt;ul&gt;
&lt;li&gt;Parallel Scavenge的老年代版本，并行执行&lt;/li&gt;
&lt;li&gt;采用标记整理算法&lt;/li&gt;
&lt;li&gt;老年代收集器&lt;/li&gt;
&lt;li&gt;吞吐量优先&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;优点：吞吐量大，程序运行效率高&lt;/li&gt;
&lt;li&gt;缺点：响应时间较长&lt;/li&gt;
&lt;li&gt;配套垃圾回收器
&lt;ul&gt;
&lt;li&gt;Parallel Scavenge&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;CMS
&lt;ul&gt;
&lt;li&gt;特点
&lt;ul&gt;
&lt;li&gt;并发与并行执行&lt;/li&gt;
&lt;li&gt;采用标记清除算法&lt;/li&gt;
&lt;li&gt;老年代收集器&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;优点：响应时间短，用户体验好&lt;/li&gt;
&lt;li&gt;缺点
&lt;ul&gt;
&lt;li&gt;吞吐量较低&lt;/li&gt;
&lt;li&gt;无法处理浮动垃圾&lt;/li&gt;
&lt;li&gt;会产生大量的内存碎片，不利于大对象的分配，可能会提前触发一次Full GC
&lt;ul&gt;
&lt;li&gt;-XX:+UseCMSCompactAtFullCollection：进行碎片的合并整理过程，这样会使得停顿时间变长&lt;/li&gt;
&lt;li&gt;-XX:+CMSFullGCsBeforeCompaction：设置执行多少次不压缩的Full GC后，接着来一次带压缩的GC&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;收集过程
&lt;ul&gt;
&lt;li&gt;初始标记
&lt;ul&gt;
&lt;li&gt;标记和GCroot直接关联的对象&lt;/li&gt;
&lt;li&gt;标记新生代指向的老年代对象&lt;/li&gt;
&lt;li&gt;需要STW&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;并发标记
&lt;ul&gt;
&lt;li&gt;增量更新&lt;/li&gt;
&lt;li&gt;三色标记&lt;/li&gt;
&lt;li&gt;Dirty Card：引用变更区域&lt;/li&gt;
&lt;li&gt;Write Barrie：记录Dirty Card&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;并发预清理：重新标记Dirty Card&lt;/li&gt;
&lt;li&gt;可被终止的并发预清理
&lt;ul&gt;
&lt;li&gt;触发时机：Eden区的内存占用大于CMSScheduleRemarkEdenSizeThreshold（默认2M）时&lt;/li&gt;
&lt;li&gt;结束时机：Eden的内存使用率达到CMSScheduleRemarkEdenPenetration（默认50%）时&lt;/li&gt;
&lt;li&gt;作用：协调minor gc与重新标记阶段&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;重新标记
&lt;ul&gt;
&lt;li&gt;重新标记和GCroot直接关联的对象&lt;/li&gt;
&lt;li&gt;重新标记新生代指向的老年代对象&lt;/li&gt;
&lt;li&gt;需要STW&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;并发清除，回收内存空间，时间很长&lt;/li&gt;
&lt;li&gt;并发重置&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;配套垃圾回收器
&lt;ul&gt;
&lt;li&gt;Serial&lt;/li&gt;
&lt;li&gt;ParNew&lt;/li&gt;
&lt;li&gt;Serial Old：CMS会在特殊情况（jvm认为内存不够了concurrent mode failure,promotion fail） 转而停下所有线程 去做full gc，也就是MSC(单线程)就是Serial Old（也算一次full gc）作为替补&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;G1
&lt;ul&gt;
&lt;li&gt;特点
&lt;ul&gt;
&lt;li&gt;并发与并行执行&lt;/li&gt;
&lt;li&gt;采用复制算法&lt;/li&gt;
&lt;li&gt;分代收集&lt;/li&gt;
&lt;li&gt;可预测的停顿&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;优点
&lt;ul&gt;
&lt;li&gt;基于标记 - 整理算法，无内存碎片产生&lt;/li&gt;
&lt;li&gt;可预测的停顿时间模型&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;Region
&lt;ul&gt;
&lt;li&gt;大小：为1到32M，根据总的内存大小而定&lt;/li&gt;
&lt;li&gt;数量：不超过2048个&lt;/li&gt;
&lt;li&gt;指针
&lt;ul&gt;
&lt;li&gt;bottom：开始指针&lt;/li&gt;
&lt;li&gt;previous TAMS：并发标记开始指针&lt;/li&gt;
&lt;li&gt;next TAMS：并发标记结束指针&lt;/li&gt;
&lt;li&gt;top：分界指针&lt;/li&gt;
&lt;li&gt;end：结束指针&lt;/li&gt;
&lt;li&gt;注：next与top之间的为并发标记阶段的新生对象&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;分区
&lt;ul&gt;
&lt;li&gt;Eden（新生代）&lt;/li&gt;
&lt;li&gt;Survivor（幸存区）&lt;/li&gt;
&lt;li&gt;Old（老年代）&lt;/li&gt;
&lt;li&gt;Humongous（巨大对象）
&lt;ul&gt;
&lt;li&gt;大于等于Region一半的对象&lt;/li&gt;
&lt;li&gt;直接分配到老年代，防止频繁拷贝&lt;/li&gt;
&lt;li&gt;永远不会移动，要么直接被回收，要么一直存在&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;全局缓冲区日志来源
&lt;ul&gt;
&lt;li&gt;Writer Barrier
&lt;ul&gt;
&lt;li&gt;跨区引用&lt;/li&gt;
&lt;li&gt;索引变更&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;对象分配
&lt;ul&gt;
&lt;li&gt;TLAB：提升Eden分配效率&lt;/li&gt;
&lt;li&gt;PLAB：提升到Survivor及Old的效率&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;Thread
&lt;ul&gt;
&lt;li&gt;App Thread（用户线程）&lt;/li&gt;
&lt;li&gt;Concurrence Refinement Thread（同步优化线程，写后屏障）
&lt;ul&gt;
&lt;li&gt;过程：扫描全局缓冲区的日志，根据日志更新各个区（Region）的RSet&lt;/li&gt;
&lt;li&gt;参数设置
&lt;ul&gt;
&lt;li&gt;-XX:G1ConcRefinementGreenZone&lt;/li&gt;
&lt;li&gt;-XX:G1ConcRefinementYellowZone&lt;/li&gt;
&lt;li&gt;-XX:G1ConcRefinementRedZone&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;注：如果发现全局缓冲区日志积累较多，G1会调用更多的线程来出来缓冲区日志，甚至会调用App Thread 来处理，造成应用任务堵塞&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;GC Thread（GC线程）&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;Set
&lt;ul&gt;
&lt;li&gt;记忆集合(Remembered Sets)
&lt;ul&gt;
&lt;li&gt;概念：储存记录其他Region对当前持有Rset Region中Card的引用&lt;/li&gt;
&lt;li&gt;储存方状态
&lt;ul&gt;
&lt;li&gt;哈希表：到对象&lt;/li&gt;
&lt;li&gt;细粒度：到Region&lt;/li&gt;
&lt;li&gt;粗粒度：到分代区域&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;收集集合(Collection Set)
&lt;ul&gt;
&lt;li&gt;概念：通过扫描RSet来分析垃圾比例最高的Region区，放入CSet中，进行回收&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;GC
&lt;ul&gt;
&lt;li&gt;Yong GC
&lt;ul&gt;
&lt;li&gt;回收年轻代的内存&lt;/li&gt;
&lt;li&gt;收集过程
&lt;ul&gt;
&lt;li&gt;根扫描，静态和本地对象被扫描&lt;/li&gt;
&lt;li&gt;更新RS，处理dirty card队列更新RS&lt;/li&gt;
&lt;li&gt;处理RS，检测从年轻代指向年老代的对象&lt;/li&gt;
&lt;li&gt;对象拷贝，拷贝存活的对象到survivor/old区域&lt;/li&gt;
&lt;li&gt;处理引用队列，软引用，弱引用，虚引用处理&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;需要STW&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;Mixed GC
&lt;ul&gt;
&lt;li&gt;回收所有年轻代内存和部分老年代内存&lt;/li&gt;
&lt;li&gt;收集过程
&lt;ul&gt;
&lt;li&gt;初始标记，标记GCRoots能直接关联到的对象，时间很短，会穿插yong gc，需要STW&lt;/li&gt;
&lt;li&gt;根区域扫描&lt;/li&gt;
&lt;li&gt;并发标记，进行GCRoots Tracing（可达性分析）过程，并通过写前屏障将灰色对白色引用，将白色置灰，放入satb mark queue中，并为bottom与pre指针间的引用进行快照Bitmap&lt;/li&gt;
&lt;li&gt;重新标记，读取satb mark queue中的应用，进行漏标修复，同时这个过程也会处理弱引用，需要STW&lt;/li&gt;
&lt;li&gt;清理阶段，处理并回收Cset中的Region&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;注：当mixed gc回收速度赶不上内存分配的速度，G1会使用单线程（使用Serial Old的代替）进行Full GC&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;参数
&lt;ul&gt;
&lt;li&gt;XX:+UseG1GC：开启G1 GC&lt;/li&gt;
&lt;li&gt;-XX：MaxGCPauseMillis = 200 ：设置最大暂停目标，G1只会仅最大努力达到这个目标&lt;/li&gt;
&lt;li&gt;-XX：InitiatingHeapOccupancyPercent = 45 : 表示并发开始GC周期的堆使用阈值，当整个堆的使用量达到阈值时，就会开始并发周期&lt;/li&gt;
&lt;li&gt;-XX:G1MixedGCCountTarget=8 : 每次Mixed GC回收的老年代内存数量，默认为8&lt;/li&gt;
&lt;li&gt;-XX:ConcGCThreads : 每次GC使用的CPU数量， 值不是固定&lt;/li&gt;
&lt;li&gt;-XX:G1ReservePercent=10%: 假天花板数量，作用是预留10%的空间不使用，&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;ZGC
&lt;ul&gt;
&lt;li&gt;Region
&lt;ul&gt;
&lt;li&gt;小型Region（Small Region）：容量固定为2MB，用于放置小于256KB的小对象&lt;/li&gt;
&lt;li&gt;中型Region（Medium Region）：容量固定为32MB，用于放置大于等于256KB但小于4MB的对象&lt;/li&gt;
&lt;li&gt;大型Region（Large Region）：容量不固定，可以动态变化，但必须为2MB的整数倍，用于放置4MB或以上的大对象。每个大型Region中只会存放一个大对象，最小容量可低至4MB，所有大型Region可能小于中型Region。大型Region在ZGC的实现中是不会被重分配的，因为复制一个大对象的代价非常高昂&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;染色指针
&lt;ul&gt;
&lt;li&gt;HotSpot虚拟机的标记实现方案
&lt;ul&gt;
&lt;li&gt;把标记直接记录在对象头上（如Serial收集器）&lt;/li&gt;
&lt;li&gt;把标记记录在与对象相互独立的数据结构上（如G1、Shenandoah使用了一种相当于堆内存的1/64大小的，称为BitMap的结构来记录标记信息）&lt;/li&gt;
&lt;li&gt;直接把标记信息记在引用对象的指针上（如ZGC）&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;存储位置（64位操作系统）
&lt;ul&gt;
&lt;li&gt;高18位不能用来寻址，所有不能使用&lt;/li&gt;
&lt;li&gt;剩余的46为却可以支持64T的空间，到目前为止我们几乎还用不到这么多内存。于是ZGC将46位中的高4位取出，用来存储4个标志位
&lt;ul&gt;
&lt;li&gt;Finalizable：表示是否只能通过finalize()方法才能被访问到，其他途径不行&lt;/li&gt;
&lt;li&gt;Remapped：表示是否进入了重分配集（即被移动过）&lt;/li&gt;
&lt;li&gt;Marked1、Marked0：表示对象的三色标记状态&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;剩余的42位可以支持4T的内存&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;优势
&lt;ul&gt;
&lt;li&gt;一旦某个Region的存活对象被移走之后，这个Region立即就能够被释放和重用掉&lt;/li&gt;
&lt;li&gt;染色指针可以大幅减少在垃圾收集过程中内存屏障的使用数量，ZGC只使用了读屏障&lt;/li&gt;
&lt;li&gt;染色指针具备强大的扩展性，它可以作为一种可扩展的存储结构用来记录更多与对象标记、重定位过程相关的数据&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;内存多重映射：将多个不同的虚拟内存地址映射到同一个物理内存地址上&lt;/li&gt;
&lt;li&gt;NUMA支持
&lt;ul&gt;
&lt;li&gt;UMA：表示内存只有一块，所有CPU都去访问这一块内存，那么就会存在竞争问题（争夺内存总线访问权），有竞争就会有锁，有锁效率就会受到影响，而且CPU核心数越多，竞争就越激烈&lt;/li&gt;
&lt;li&gt;NUMA：每个CPU对应有一块内存，且这块内存在主板上离这个CPU是最近的，每个CPU优先访问这块内存，那效率自然就提高了&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;收集过程
&lt;ul&gt;
&lt;li&gt;并发标记：会出现短暂的停顿，更新染色指针中的Marked 0、Marked 1标志位
&lt;ul&gt;
&lt;li&gt;初始标记&lt;/li&gt;
&lt;li&gt;并发标记&lt;/li&gt;
&lt;li&gt;重新标记&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;并发预备重分配：根据特定的查询条件统计得出本次收集过程要清理哪些Region，将这些Region组成重分配集（Relocation Set）&lt;/li&gt;
&lt;li&gt;并发重分配：ZGC执行过程中的核心阶段，这个把重分配集中的存活对象复制到新的Region上，并为重分配集中的每个Region维护一个转发表（Forward Table），记录从旧对象到新对象的转向关系&lt;/li&gt;
&lt;li&gt;并发重映射：重映射所做的就是修正整个堆中指向重分配集中旧对象的所有引用，但是ZGC中对象引用存在“自愈”功能，所以这个重映射操作并不是很迫切。ZGC很巧妙地把并发重映射阶段要做的工作，合并到了下一次垃圾收集循环中的并发标记阶段里去完成，反正它们都是要遍历所有对象的，这样合并就节省了一次遍历对象图的开销&lt;/li&gt;
&lt;li&gt;注：指针的“自愈”（Self-Healing），ZGC收集器能仅从引用上就明确得知一个对象是否处于重分配集之中，如果用户线程此时并发访问了位于重分配集中的对象，这次访问将会被预置的内存屏障所截获，然后立即根据Region上的转发表记录将访问转发到新复制的对象上，并同时修正更新该引用的值，使其直接指向新对象&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;优点
&lt;ul&gt;
&lt;li&gt;低停顿&lt;/li&gt;
&lt;li&gt;高吞吐量&lt;/li&gt;
&lt;li&gt;ZGC收集过程中额外耗费的内存小&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;缺点
&lt;ul&gt;
&lt;li&gt;浮动垃圾&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;JDK版本迭代
&lt;ul&gt;
&lt;li&gt;JDK1.3：发布Serial GC及ParNew&lt;/li&gt;
&lt;li&gt;JDK1.4：发布Parallel GC及CMS&lt;/li&gt;
&lt;li&gt;JDK1.6：Parallel GC成为默认&lt;/li&gt;
&lt;li&gt;JDK1.7：发布G1&lt;/li&gt;
&lt;li&gt;JDK1.9：G1成为默认&lt;/li&gt;
&lt;li&gt;JDK11：引入Epsilon及ZGC&lt;/li&gt;
&lt;li&gt;JDK12：增强G1，并引入Shenandoah GC&lt;/li&gt;
&lt;li&gt;JDK13：增强ZGC&lt;/li&gt;
&lt;li&gt;JDK13：删除CMS&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;逃逸分析
&lt;ul&gt;
&lt;li&gt;概念：当一个对象在方法中被定义后，它可能被外部方法所引用，例如作为调用参数传递到其他地方中，称为方法逃逸。&lt;/li&gt;
&lt;li&gt;代码优化
&lt;ul&gt;
&lt;li&gt;栈上分配&lt;/li&gt;
&lt;li&gt;同步省略&lt;/li&gt;
&lt;li&gt;标量替换&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;命令
&lt;ul&gt;
&lt;li&gt;-Xms：设置堆的起始大小&lt;/li&gt;
&lt;li&gt;-Xmx：设置堆的最大大小&lt;/li&gt;
&lt;li&gt;-XX:NewRatio：老年代与新生代比例，默认为2&lt;/li&gt;
&lt;li&gt;-XX:SurvivorRatio：Eden与Survivor比例，默认为8&lt;/li&gt;
&lt;li&gt;-Xmn：设置新生代的最大内存大小，优先级高于NewRatio、SurvivorRatio&lt;/li&gt;
&lt;li&gt;-XX:MaxTenuringThreshold：新生代转移到老年代的年龄阈值&lt;/li&gt;
&lt;li&gt;-XX:UseTLAB：是否开启TLAB，默认开启&lt;/li&gt;
&lt;li&gt;-XX:HandlerPromotionFailure：空间分配担保，JDK1.7失效&lt;/li&gt;
&lt;li&gt;-XX:DoEscapeAnalysis：逃逸分析JDK1.6后默认开启&lt;/li&gt;
&lt;li&gt;-XX:EliminateAllocations：标量替换默认开启&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;元空间
&lt;ul&gt;
&lt;li&gt;永久代（Permanet Generation，也称PermGen）
&lt;ul&gt;
&lt;li&gt;概念：方法区的实现&lt;/li&gt;
&lt;li&gt;垃圾回收：永久代和堆是相互隔离的，但它们使用的物理内存是连续的，和老年代捆绑在一起的，因此无论谁满了，都会触发永久代和老年代的垃圾收集&lt;/li&gt;
&lt;li&gt;存储
&lt;ul&gt;
&lt;li&gt;类信息&lt;/li&gt;
&lt;li&gt;运行时常量池&lt;/li&gt;
&lt;li&gt;字符串常量&lt;/li&gt;
&lt;li&gt;类静态变量&lt;/li&gt;
&lt;li&gt;即时编译器编译后的代码&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;JDK版本变动
&lt;ul&gt;
&lt;li&gt;1.7
&lt;ul&gt;
&lt;li&gt;符号引用(Symbols)转移到了Native Memory&lt;/li&gt;
&lt;li&gt;字符串常量池(interned strings)转移到了Java Heap&lt;/li&gt;
&lt;li&gt;类的静态变量(class statics)转移到了Java Heap&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;1.8：取消了永久代，改为元空间
&lt;ul&gt;
&lt;li&gt;替换原因
&lt;ul&gt;
&lt;li&gt;字符串存在永久代中，容易出现性能问题和内存溢出&lt;/li&gt;
&lt;li&gt;类及方法的信息等比较难确定其大小，因此对于永久代的大小指定比较困难，太小容易出现永久代溢出，太大则容易导致老年代溢出&lt;/li&gt;
&lt;li&gt;永久代会为 GC 带来不必要的复杂度，并且回收效率偏低&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;概念：元空间不再与堆连续，而且是存在于本地内存（Native memory）&lt;/li&gt;
&lt;li&gt;存储内容
&lt;ul&gt;
&lt;li&gt;类信息&lt;/li&gt;
&lt;li&gt;运行时常量池&lt;/li&gt;
&lt;li&gt;即时编译器编译后的代码&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;参数
&lt;ul&gt;
&lt;li&gt;-XX:MetaspaceSize，class metadata的初始空间配额，以bytes为单位，达到该值就会触发垃圾收集进行类型卸载，同时GC会对该值进行调整：如果释放了大量的空间，就适当的降低该值；如果释放了很少的空间，那么在不超过MaxMetaspaceSize（如果设置了的话），适当的提高该值&lt;/li&gt;
&lt;li&gt;-XX：MaxMetaspaceSize，可以为class metadata分配的最大空间。默认是没有限制的&lt;/li&gt;
&lt;li&gt;-XX：MinMetaspaceFreeRatio,在GC之后，最小的Metaspace剩余空间容量的百分比，减少为class metadata分配空间导致的垃圾收集&lt;/li&gt;
&lt;li&gt;-XX:MaxMetaspaceFreeRatio,在GC之后，最大的Metaspace剩余空间容量的百分比，减少为class metadata释放空间导致的垃圾收集&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h4&gt;内存溢出&lt;/h4&gt;
&lt;ul&gt;
&lt;li&gt;栈溢出：StackOverflowError&lt;/li&gt;
&lt;li&gt;堆溢出：OutOfMemoryError:java heap space&lt;/li&gt;
&lt;li&gt;永久代溢出：OutOfMemoryError: PermGen space，已废除&lt;/li&gt;
&lt;li&gt;线程创建失败：OutOfMemoryError:unable to create native thread&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;执行引擎（Execution Engine）&lt;/h3&gt;
&lt;h4&gt;作用：将字节码指令解释/编译为对应平台上的本地机器指令&lt;/h4&gt;
&lt;ul&gt;
&lt;li&gt;输入：字节码文件&lt;/li&gt;
&lt;li&gt;处理过程：字节码解析的等效过程&lt;/li&gt;
&lt;li&gt;执行结果&lt;/li&gt;
&lt;li&gt;注：每个字节码指令都由一个1字节的操作码和附加的操作数组成&lt;/li&gt;
&lt;/ul&gt;
&lt;h4&gt;执行方式&lt;/h4&gt;
&lt;ul&gt;
&lt;li&gt;解释器执行（通过解释器执行）
&lt;ul&gt;
&lt;li&gt;概念：将编译好的字节码一行一行地翻译为机器码执行&lt;/li&gt;
&lt;li&gt;优势：当程序需要迅速启动的时候，解释器可以首先发挥作用，省去了编译的时间，立即执行。解释执行占用更小的内存空间。同时，当编译器进行的激进优化失败的时候，还可以进行逆优化来恢复到解释执行的状态&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;编译器执行（JIT通过即时编译器产生本地代码执行）
&lt;ul&gt;
&lt;li&gt;概念：以方法为单位，将字节码一次性翻译为机器码后执行&lt;/li&gt;
&lt;li&gt;优势：在程序运行时，随着时间的推移，编译器逐渐发挥作用，把越来越多的代码编译成本地代码之后，可以获得更高的执行效率&lt;/li&gt;
&lt;li&gt;即时编译
&lt;ul&gt;
&lt;li&gt;用处：处理热点数据
&lt;ul&gt;
&lt;li&gt;热点数据分类
&lt;ul&gt;
&lt;li&gt;被多次调用的方法&lt;/li&gt;
&lt;li&gt;被多次执行的循环体&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;热点探测
&lt;ul&gt;
&lt;li&gt;概念：判断一段代码是否是热点代码，是不是需要触发即时编译&lt;/li&gt;
&lt;li&gt;探测算法
&lt;ul&gt;
&lt;li&gt;基于采样的热点探测
&lt;ul&gt;
&lt;li&gt;概念：虚拟机会周期的对各个线程栈顶进行检查，如果某些方法经常出现在栈顶，这个方法就是“热点方法”&lt;/li&gt;
&lt;li&gt;优点：实现简单、高效，很容易获取方法调用关系&lt;/li&gt;
&lt;li&gt;缺点：很难确认方法的reduce，容易受到线程阻塞或其他外因扰乱&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;基于计数器的热点探测（HotSpot使用）
&lt;ul&gt;
&lt;li&gt;概念：为每个方法（甚至是代码块）建立计数器，执行次数超过阈值就认为是“热点方法”&lt;/li&gt;
&lt;li&gt;优点：统计结果精确严谨&lt;/li&gt;
&lt;li&gt;缺点：实现麻烦，不能直接获取方法的调用关系&lt;/li&gt;
&lt;li&gt;分类
&lt;ul&gt;
&lt;li&gt;方法调用计数器
&lt;ul&gt;
&lt;li&gt;概念：如果不做任何设置，方法调用计数器统计的并不是方法被调用的绝对次数，而是一个相对的执行频率，即一段时间之内的方法被调用的次数&lt;/li&gt;
&lt;li&gt;热度的衰减及半衰周期：当超过一定的时间限度，如果方法的调用次数仍然不足以让它提交给即时编译器编译，那么这个方法的调用计数器就会被减少一半，这个过程称为方法调用计数器热度的衰减（Counter Decay），而这段时间就成为此方法的统计的半衰周期（ Counter Half Life Time）&lt;/li&gt;
&lt;li&gt;注：进行热度衰减的动作是在虚拟机进行垃圾收集时顺便进行的&lt;/li&gt;
&lt;li&gt;阈值
&lt;ul&gt;
&lt;li&gt;Client 模式：1500&lt;/li&gt;
&lt;li&gt;Server 模式：10000&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;回边计数器
&lt;ul&gt;
&lt;li&gt;概念：统计一个方法中循环体代码执行的次数，在字节码中遇到控制流向后跳转的指令称为“回边”（ Back Edge ）。显然，建立回边计数器统计的目的就是为了触发 OSR 编译&lt;/li&gt;
&lt;li&gt;阈值
&lt;ul&gt;
&lt;li&gt;Client 模式：公式为 方法调用计数器阈值（CompileThreshold）X OSR 比率（OnStackReplacePercentage）/ 100 。其中 OSR 比率默认为 933，那么，回边计数器的阈值为 13995&lt;/li&gt;
&lt;li&gt;Server 模式：公式为 方法调用计数器阈值（Compile Threashold）X （OSR (OnStackReplacePercentage)- 解释器监控比率 （InterpreterProfilePercent））/100
其中 onStackReplacePercentage 默认值为 140，InterpreterProfilePercentage 默认值为 33，如果都取默认值，那么 Server 模式虚拟机回边计数器阈值为 10700&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;即时编译器
&lt;ul&gt;
&lt;li&gt;Client Compiler/C1：即Client编译器，面向对启动性能有要求的客户端GUI程序，采用的优化手段比较简单，因此编译的时间较短&lt;/li&gt;
&lt;li&gt;Server Compiler/C2：即Server编译器，面向对性能峰值有要求的服务端程序，采用的优化手段复杂，因此编译时间长，但是在运行过程中性能更好&lt;/li&gt;
&lt;li&gt;目前的 HotSpot 编译器默认的是解释器和其中一个即时编译器配合的方式工作，具体是哪一个编译器，取决于虚拟机运行的模式，HotSpot 虚拟机会根据自身版本与计算机的硬件性能自动选择运行模式，这种配合使用的方式称为“混合模式”（Mixed Mode）
&lt;ul&gt;
&lt;li&gt;使用 -client 和 -server 参数强制指定虚拟机运行在 Client 模式或者 Server 模式&lt;/li&gt;
&lt;li&gt;使用参数 -Xint 强制虚拟机运行于 “解释模式”（Interpreted Mode），这时候编译器完全不介入工作&lt;/li&gt;
&lt;li&gt;使用 -Xcomp 强制虚拟机运行于 “编译模式”（Compiled Mode），这时候将优先采用编译方式执行，但是解释器仍然要在编译无法进行的情况下接入执行过程&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h4&gt;方法调用&lt;/h4&gt;
&lt;ul&gt;
&lt;li&gt;概念：确定被调用方法的版本，即调用哪一个方法，不涉及方法内部的具体运行过程&lt;/li&gt;
&lt;li&gt;分类
&lt;ul&gt;
&lt;li&gt;解析调用：静态过程，在类装载的解析阶段就会把涉及的符号引用全部转变为可确定的直接引用，而不会延迟到运行期再去完成，即编译期可知、运行期不变&lt;/li&gt;
&lt;li&gt;分派调用
&lt;ul&gt;
&lt;li&gt;静态类型与实际类型
&lt;ul&gt;
&lt;li&gt;静态类型：父类/接口&lt;/li&gt;
&lt;li&gt;实际类型：子类/实现类&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;静态分派与动态分派
&lt;ul&gt;
&lt;li&gt;静态分派
&lt;ul&gt;
&lt;li&gt;依赖静态类型来定位方法的执行版本&lt;/li&gt;
&lt;li&gt;典型应用是方法重载&lt;/li&gt;
&lt;li&gt;发生在编译阶段&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;动态分派
&lt;ul&gt;
&lt;li&gt;依赖动态类型来定位方法的执行版本&lt;/li&gt;
&lt;li&gt;典型应用是方法重写&lt;/li&gt;
&lt;li&gt;发生在运行阶段&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;单分派：根据一个宗量对目标方法进行选择（方法的接受者与方法的参数统称为方法的宗量）&lt;/li&gt;
&lt;li&gt;多分派：根据多于一个宗量对目标方法进行选择&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;invokevirtual 指令
&lt;ul&gt;
&lt;li&gt;弹出栈顶元素，判断其实际类型，记为C&lt;/li&gt;
&lt;li&gt;能否在C中查找到需要调用的简单名称和描述相符的方法
&lt;ul&gt;
&lt;li&gt;能：返回引用&lt;/li&gt;
&lt;li&gt;否：向C的父类再做如上的搜索，能否找到相符方法
&lt;ul&gt;
&lt;li&gt;能：返回引用&lt;/li&gt;
&lt;li&gt;否：抛出异常&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;本地库接口（Native Method Interface -&amp;gt; JNI）&lt;/h3&gt;
&lt;h3&gt;本地方法库（Native Method Library）&lt;/h3&gt;
&lt;h2&gt;对象&lt;/h2&gt;
&lt;h3&gt;实例化方式&lt;/h3&gt;
&lt;h4&gt;new&lt;/h4&gt;
&lt;ul&gt;
&lt;li&gt;类的静态方法&lt;/li&gt;
&lt;li&gt;xxxBuilder/xxxFactory的静态方法&lt;/li&gt;
&lt;/ul&gt;
&lt;h4&gt;Class的newInstance()：反射，只能调用空参的构造器，权限必须是public&lt;/h4&gt;
&lt;h4&gt;Constructor的newInstance()：反射，对参数和访问权限无要求&lt;/h4&gt;
&lt;h4&gt;clone：不调用任何构造器，当前类实现Cloneable接口，实现clone()&lt;/h4&gt;
&lt;h4&gt;反序列化：从文件中，从网络中获取一个对象的二进制流&lt;/h4&gt;
&lt;h4&gt;第三方库Objenesis&lt;/h4&gt;
&lt;h3&gt;创建步骤&lt;/h3&gt;
&lt;h4&gt;判断对象是否加载、链接、初始化&lt;/h4&gt;
&lt;h4&gt;为对象分配内存&lt;/h4&gt;
&lt;ul&gt;
&lt;li&gt;内存规整
&lt;ul&gt;
&lt;li&gt;指针碰撞&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;内存不规整
&lt;ul&gt;
&lt;li&gt;空闲列表&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h4&gt;处理并发安全问题&lt;/h4&gt;
&lt;ul&gt;
&lt;li&gt;采用CAS失败重试，区域加锁保证更新的原子性&lt;/li&gt;
&lt;li&gt;每个线程预先分配一块TLAB，通过-XX: +/-UseTLAB来设置&lt;/li&gt;
&lt;/ul&gt;
&lt;h4&gt;初始化分配到的空间&lt;/h4&gt;
&lt;ul&gt;
&lt;li&gt;所有属性设置默认值，保证对象实例字段在不赋值时可以直接使用&lt;/li&gt;
&lt;/ul&gt;
&lt;h4&gt;设置对象的对象头&lt;/h4&gt;
&lt;h4&gt;执行&amp;lt;init&amp;gt;进行初始化&lt;/h4&gt;
&lt;h3&gt;内存布局&lt;/h3&gt;
&lt;h4&gt;对象头&lt;/h4&gt;
&lt;ul&gt;
&lt;li&gt;运行时元数据
&lt;ul&gt;
&lt;li&gt;哈希地址值&lt;/li&gt;
&lt;li&gt;GC分代年龄&lt;/li&gt;
&lt;li&gt;锁状态标志&lt;/li&gt;
&lt;li&gt;线程持有的锁&lt;/li&gt;
&lt;li&gt;偏向线程id&lt;/li&gt;
&lt;li&gt;偏向时间戳&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;类型指针
&lt;ul&gt;
&lt;li&gt;指向类元数据InstanceKlass，确定对象所属的类型&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;注：数组，还需记录长度&lt;/li&gt;
&lt;/ul&gt;
&lt;h4&gt;实例数据&lt;/h4&gt;
&lt;ul&gt;
&lt;li&gt;概念：它是对象真正存储的有效信息，包括程序代码中定义的各种类型的字段（包括从父类继承下来的和本身拥有的字段）&lt;/li&gt;
&lt;li&gt;规则
&lt;ul&gt;
&lt;li&gt;相同长度的字段总是被分配到一起&lt;/li&gt;
&lt;li&gt;父类中定义的变量会出现在子类前&lt;/li&gt;
&lt;li&gt;如果CompactFields参数设置为true（默认为true），子类的窄变量可能插入到父类的空隙&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h4&gt;对象填充&lt;/h4&gt;
&lt;ul&gt;
&lt;li&gt;非必须，类似占位符&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;访问定位&lt;/h3&gt;
&lt;h4&gt;句柄访问&lt;/h4&gt;
&lt;ul&gt;
&lt;li&gt;栈 ：reference
&lt;ul&gt;
&lt;li&gt;到对象实例数据的指针&lt;/li&gt;
&lt;li&gt;到对象类型数据的指针&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;优点
&lt;ul&gt;
&lt;li&gt;稳定，不受对象移动影响&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;缺点
&lt;ul&gt;
&lt;li&gt;占用空间&lt;/li&gt;
&lt;li&gt;效率较低&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h4&gt;直接指针&lt;/h4&gt;
&lt;ul&gt;
&lt;li&gt;栈 ：reference
&lt;ul&gt;
&lt;li&gt;到对象类型数据的指针
&lt;ul&gt;
&lt;li&gt;实例池：对象实例数据&lt;/li&gt;
&lt;li&gt;方法区：对象类型数据&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;Hotspot的默认实现&lt;/li&gt;
&lt;li&gt;优点
&lt;ul&gt;
&lt;li&gt;空间占用少&lt;/li&gt;
&lt;li&gt;效率高&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;缺点
&lt;ul&gt;
&lt;li&gt;不稳定，受对象移动影响&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;字节码文件&lt;/h2&gt;
&lt;h3&gt;魔数&lt;/h3&gt;
&lt;h4&gt;0xCAFEBABE&lt;/h4&gt;
&lt;h3&gt;版本号&lt;/h3&gt;
&lt;h4&gt;主版本号&lt;/h4&gt;
&lt;h4&gt;次版本号&lt;/h4&gt;
&lt;h3&gt;常量池&lt;/h3&gt;
&lt;h4&gt;存储&lt;/h4&gt;
&lt;ul&gt;
&lt;li&gt;字面量&lt;/li&gt;
&lt;li&gt;符号引用
&lt;ul&gt;
&lt;li&gt;类和接口的全局限定名&lt;/li&gt;
&lt;li&gt;字段的名称和描述符&lt;/li&gt;
&lt;li&gt;方法的名称和描述符&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h4&gt;组成&lt;/h4&gt;
&lt;ul&gt;
&lt;li&gt;常量池计数器&lt;/li&gt;
&lt;li&gt;常量池数据区：14种类型的cp_info&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;访问标志&lt;/h3&gt;
&lt;h4&gt;描述该Class是类还是接口&lt;/h4&gt;
&lt;h4&gt;是否被Public、Abstract、Final等修饰符修饰&lt;/h4&gt;
&lt;h4&gt;注意：JVM并没有穷举所有的访问标志，而是使用按位或操作来进行描述的，比如某个类的修饰符为Public Final，则对应的访问修饰符的值为ACC_PUBLIC | ACC_FINAL，即0x0001 | 0x0010=0x0011&lt;/h4&gt;
&lt;h3&gt;当前类名&lt;/h3&gt;
&lt;h4&gt;访问标志后的两个字节，描述的是当前类的全限定名。这两个字节保存的值为常量池中的索引值，根据索引值就能在常量池中找到这个类的全限定名&lt;/h4&gt;
&lt;h3&gt;父类名称&lt;/h3&gt;
&lt;h4&gt;当前类名后的两个字节，描述父类的全限定名，同上，保存的也是常量池中的索引值&lt;/h4&gt;
&lt;h3&gt;接口信息&lt;/h3&gt;
&lt;h4&gt;父类名称后为两字节的接口计数器，描述了该类或父类实现的接口数量。紧接着的n个字节是所有接口名称的字符串常量的索引值&lt;/h4&gt;
&lt;h3&gt;字段表&lt;/h3&gt;
&lt;h4&gt;描述&lt;/h4&gt;
&lt;ul&gt;
&lt;li&gt;类和接口中声明的变量&lt;/li&gt;
&lt;li&gt;类级别的变量以及实例变量&lt;/li&gt;
&lt;/ul&gt;
&lt;h4&gt;组成&lt;/h4&gt;
&lt;ul&gt;
&lt;li&gt;两个字节，描述字段个数&lt;/li&gt;
&lt;li&gt;每个字段的详细信息fields_info&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;方法表&lt;/h3&gt;
&lt;h4&gt;组成&lt;/h4&gt;
&lt;ul&gt;
&lt;li&gt;两个字节描述方法的个数&lt;/li&gt;
&lt;li&gt;每个方法的详细信息
&lt;ul&gt;
&lt;li&gt;方法的访问标志&lt;/li&gt;
&lt;li&gt;方法名&lt;/li&gt;
&lt;li&gt;方法的描述符&lt;/li&gt;
&lt;li&gt;方法的属性
&lt;ul&gt;
&lt;li&gt;Code区：源代码对应的JVM指令操作码，在进行字节码增强时重点操作的就是“Code区”这一部分&lt;/li&gt;
&lt;li&gt;LineNumberTable：行号表，将Code区的操作码和源代码中的行号对应&lt;/li&gt;
&lt;li&gt;LocalVariableTable：局部变量表，包含This和局部变量&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;JVM调优&lt;/h2&gt;
&lt;h3&gt;子主题 1&lt;/h3&gt;
&lt;h3&gt;子主题 2&lt;/h3&gt;
&lt;h3&gt;子主题 3&lt;/h3&gt;
&lt;h3&gt;子主题 4&lt;/h3&gt;
</content:encoded></item><item><title>Java多线程与并发编程深度解析</title><link>https://tangtangit.com/posts/java%E6%A0%B8%E5%BF%83-04-%E5%A4%9A%E7%BA%BF%E7%A8%8B/</link><guid isPermaLink="true">https://tangtangit.com/posts/java%E6%A0%B8%E5%BF%83-04-%E5%A4%9A%E7%BA%BF%E7%A8%8B/</guid><description>全面覆盖Java多线程核心知识：线程基础、CompletableFuture异步编程、锁机制、CAS与原子类、ThreadLocal、JMM、volatile、对象内存布局与锁升级。</description><pubDate>Thu, 15 Jan 2026 00:00:00 GMT</pubDate><content:encoded>&lt;h1&gt;1. 线程基础知识复习&lt;/h1&gt;
&lt;p&gt;●1把锁：synchronized（后面细讲）
●2个并：
○并发（concurrent）：是在同一实体上的多个事件，是在一台机器上“同时”处理多个任务，同一时刻，其实是只有一个事情再发生。
○并行（parallel）：是在不同实体上的多个事件，是在多台处理器上同时处理多个任务，同一时刻，大家都在做事情，你做你的，我做我的，各干各的。
●3个程：
○进程：在系统中运行的一个应用程序，每个进程都有它自己的内存空间和系统资源
○线程：也被称为轻量级进程，在同一个进程内会有1个或多个线程，是大多数操作系统进行时序调度的基本单元。
○管程：Monitor（锁），也就是我们平时所说的锁。Monitor其实是一种同步机制，它的义务是保证（同一时间）只有一个线程可以访问被保护的数据和代码，JVM中同步是基于进入和退出监视器（Monitor管程对象）来实现的，每个对象实例都会有一个Monitor对象，Monitor对象和Java对象一同创建并销毁，底层		由C++语言实现。
●线程分类（一般不做特别说明配置，默认都是用户线程）：
○用户线程：是系统的工作线程，它会完成这个程序需要完成的业务操作。
○守护线程：是一种特殊的线程为其他线程服务的，在后台默默地完成一些系统性的任务，比如垃圾回收线程就是最典型的例子。守护线程作为一个服务线程，没有服务对象就没有必要继续运行了，如果用户线程全部结束了，意味着程序需要完成的业务操作已经结束了，系统可以退出了。所以假如当系统只剩下守护线程的时候，守护线程伴随着JVM一同结束工作。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;/**
 * @author Guanghao Wei
 * @create 2023-04-10 10:59
 */
public class DaemonDemo {
    public static void main(String[] args) {
        Thread t1 = new Thread(() -&amp;gt; {
            System.out.println(Thread.currentThread().getName() + &quot; 开始运行,&quot; + (Thread.currentThread().isDaemon() ? &quot;守护线程&quot; : &quot;用户线程&quot;));
            while (true) {

            }
        }, &quot;t1&quot;);
        t1.setDaemon(true);//通过设置属性Daemon来设置当前线程是否为守护线程
        t1.start();
        try {
            TimeUnit.SECONDS.sleep(3);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }

        System.out.println(Thread.currentThread().getName() + &quot; 主线程结束&quot;);
    }
}


输出：t1 开始运行,守护线程
main 主线程结束---&amp;gt;在main主线程结束后，守护线程会伴随着JVM一同结束工作，即使还有循环没有结束
&lt;/code&gt;&lt;/pre&gt;
&lt;h1&gt;2. CompletableFuture&lt;/h1&gt;
&lt;h2&gt;2.1 Future接口理论知识复习&lt;/h2&gt;
&lt;p&gt;Future接口（FutureTask实现类）定义了操作异步任务执行一些方法，如获取异步任务的执行结果、取消异步任务的执行、判断任务是否被取消、判断任务执行是否完毕等。&lt;/p&gt;
&lt;p&gt;举例：比如主线程让一个子线程去执行任务，子线程可能比较耗时，启动子线程开始执行任务后，主线程就去做其他事情了，忙完其他事情或者先执行完，过了一会再才去获取子任务的执行结果或变更的任务状态（老师上课时间想喝水，他继续讲课不结束上课这个主线程，让学生去小卖部帮老师买水完成这个耗时和费力的任务）。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://tangtangit.com/_astro/1681096582444-44cfb59a-24eb-4580-8494-6e84cced7157.CtCQEmTf_2te7R7.webp&quot; alt=&quot;image.png&quot; /&gt;&lt;/p&gt;
&lt;h2&gt;2.2 Future接口常用实现类FutureTask异步任务&lt;/h2&gt;
&lt;h3&gt;2.2.1 Future接口能干什么&lt;/h3&gt;
&lt;p&gt;Future是Java5新加的一个接口，它提供一种异步并行计算的功能，如果主线程需要执行一个很耗时的计算任务，我们会就可以通过Future把这个任务放进异步线程中执行，主线程继续处理其他任务或者先行结束，再通过Future获取计算结果。&lt;/p&gt;
&lt;h3&gt;2.2.2 Future接口相关架构&lt;/h3&gt;
&lt;p&gt;●目的：异步多线程任务执行且返回有结果，三个特点：多线程、有返回、异步任务（班长为老师去买水作为新启动的异步多线程任务且买到水有结果返回）&lt;/p&gt;
&lt;p&gt;●代码实现：Runnable接口+Callable接口+Future接口和FutureTask实现类。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://tangtangit.com/_astro/1681097139723-8beb561b-f531-4198-8159-db8582f742e4.BvnESQ7T_BaKf5.webp&quot; alt=&quot;image.png&quot; /&gt;&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;FutureTask开启异步任务&lt;/p&gt;
&lt;/blockquote&gt;
&lt;pre&gt;&lt;code&gt;/**
 * @author Guanghao Wei
 * @create 2023-04-10 11:21
 */
public class CompletableFutureDemo {
    public static void main(String[] args) throws ExecutionException, InterruptedException {
        FutureTask&amp;lt;String&amp;gt; futureTask = new FutureTask(new MyThread());
        Thread t1 = new Thread(futureTask); //开启一个异步线程
        t1.start();

        System.out.println(futureTask.get()); //有返回hello Callable会阻塞等待call方法执行完
    }
}


class MyThread implements Callable&amp;lt;String&amp;gt; {

    @Override
    public String call() throws Exception {
        System.out.println(&quot;--------come in&quot;);
        return &quot;hello Callable&quot;;
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h4&gt;2.2.3 Future编码实战和优缺点分析&lt;/h4&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;优点：Future+线程池异步多线程任务配合，能显著提高程序的运行效率。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;缺点：&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;ul&gt;
&lt;li&gt;get()阻塞---一旦调用get()方法求结果，一旦调用不见不散，非要等到结果才会离开，不管你是否计算完成，如果没有计算完成容易程序堵塞。&lt;/li&gt;
&lt;li&gt;isDone()轮询---轮询的方式会耗费无谓的cpu资源，而且也不见得能及时得到计算结果，如果想要异步获取结果，通常会以轮询的方式去获取结果，尽量不要阻塞。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;结论：Future对于结果的获取不是很友好，只能通过&lt;strong&gt;阻塞或轮询&lt;/strong&gt;的方式得到任务的结果。&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;blockquote&gt;
&lt;p&gt;Future获取结果get()和轮询&lt;/p&gt;
&lt;/blockquote&gt;
&lt;pre&gt;&lt;code&gt;/**
 * @author Guanghao Wei
 * @create 2023-04-10 11:41
 */
public class FutureApiDemo {
    public static void main(String[] args) throws ExecutionException, InterruptedException, TimeoutException {
        FutureTask&amp;lt;String&amp;gt; futureTask = new FutureTask&amp;lt;&amp;gt;(() -&amp;gt; {
            System.out.println(Thread.currentThread().getName() + &quot;--------come in&quot;);
            try {
                TimeUnit.SECONDS.sleep(5);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            return &quot;task over&quot;;
        });

        Thread t1 = new Thread(futureTask, &quot;t1&quot;);
        t1.start();

//        System.out.println(futureTask.get());//这样会有阻塞的可能，在程序没有计算完毕的情况下。
        System.out.println(Thread.currentThread().getName() + &quot; ------忙其他任务&quot;);
//        System.out.println(futureTask.get(3,TimeUnit.SECONDS));//只愿意等待三秒，计算未完成直接抛出异常
        while (true) {//轮询
            if(futureTask.isDone()){
                System.out.println(futureTask.get());
                break;
            }else{
                TimeUnit.MILLISECONDS.sleep(500);
                System.out.println(&quot;正在处理中，不要催了，越催越慢&quot;);
            }
        }
        /* 轮询结果
        * main ------忙其他任务
        t1--------come in
        正在处理中，不要催了，越催越慢
        正在处理中，不要催了，越催越慢
        正在处理中，不要催了，越催越慢
        正在处理中，不要催了，越催越慢
        正在处理中，不要催了，越催越慢
        正在处理中，不要催了，越催越慢
        正在处理中，不要催了，越催越慢
        正在处理中，不要催了，越催越慢
        正在处理中，不要催了，越催越慢
        正在处理中，不要催了，越催越慢
        task over
        Process finished with exit code 0
        * */
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h4&gt;2.2.4 完成一些复杂的任务&lt;/h4&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;对于简单的业务场景使用Future完全ok&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;回调通知：&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;ul&gt;
&lt;li&gt;应对Future的完成时间，完成了可以告诉我，也就是我们的回调通知&lt;/li&gt;
&lt;li&gt;通过轮询的方式去判断任务是否完成这样非常占cpu并且代码也不优雅&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;创建异步任务：Future+线程池组合&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;多个任务前后依赖可以组合处理（水煮鱼---&amp;gt;买鱼---&amp;gt;调料---&amp;gt;下锅）：&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;ul&gt;
&lt;li&gt;想将多个异步任务的结果组合起来，后一个异步任务的计算结果需要前一个异步任务的值&lt;/li&gt;
&lt;li&gt;想将两个或多个异步计算合并成为一个异步计算，这几个异步计算互相独立，同时后面这个又依赖前一个处理的结果&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;对计算速度选最快的：&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;ul&gt;
&lt;li&gt;当Future集合中某个任务最快结束时，返回结果，返回第一名处理结果&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;结论&lt;/strong&gt;：&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;ul&gt;
&lt;li&gt;使用Future之前提供的那点API就囊中羞涩，处理起来不够优雅，这时候还是让CompletableFuture以声明式的方式优雅的处理这些需求。&lt;/li&gt;
&lt;li&gt;从i到i++&lt;/li&gt;
&lt;li&gt;Future能干的，CompletableFuture都能干&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;2.3 CompletableFuture对Future的改进&lt;/h2&gt;
&lt;h3&gt;2.3.1 CompletableFuture为什么会出现&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;get()方法在Future计算完成之前会一直处在阻塞状态下，阻塞的方式和异步编程的设计理念相违背。&lt;/li&gt;
&lt;li&gt;isDene()方法容易耗费cpu资源（cpu空转），&lt;/li&gt;
&lt;li&gt;对于真正的异步处理我们希望是可以通过传入回调函数，在Future结束时自动调用该回调函数，这样，我们就不用等待结果&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;jdk8设计出CompletableFuture，CompletableFuture提供了一种观察者模式类似的机制，可以让任务执行完成后通知监听的一方。&lt;/p&gt;
&lt;p&gt;2.3.2 CompletableFuture和CompletionStage介绍&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;类架构说明&lt;/strong&gt;：&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://tangtangit.com/_astro/1681099783399-867773dc-8ddf-4683-b306-7d85322b9481.8B5Sb4ZG_Zt3EM8.webp&quot; alt=&quot;img&quot; /&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;接口CompletionStage&lt;/strong&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;ul&gt;
&lt;li&gt;代表异步计算过程中的某一个阶段，一个阶段完成以后可能会触发另外一个阶段。&lt;/li&gt;
&lt;li&gt;一个阶段的执行可能是被单个阶段的完成触发，也可能是由多个阶段一起触发&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;类CompletableFuture&lt;/strong&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;ul&gt;
&lt;li&gt;提供了非常强大的Future的扩展功能，可以帮助我们简化异步编程的复杂性，并且提供了函数式编程的能力，可以通过回调的方式处理计算结果，也提供了转换和组合CompletableFuture的方法&lt;/li&gt;
&lt;li&gt;它可能代表一个明确完成的Future，也可能代表一个完成阶段（CompletionStage），它支持在计算完成以后触发一些函数或执行某些动作&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h4&gt;2.3.3 核心的四个静态方法，来创建一个异步任务&lt;/h4&gt;
&lt;p&gt;四个静态构造方法&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://tangtangit.com/_astro/1681100338362-f7b1acfb-0357-4b43-98c1-fc2736ee5e28.weGINKT1_Z27HTje.webp&quot; alt=&quot;img&quot; /&gt;&lt;/p&gt;
&lt;p&gt;对于上述Executor参数说明：若没有指定，则使用默认的ForkJoinPoolcommonPool（）作为它的线程池执行异步代码，如果指定线程池，则使用我们自定义的或者特别指定的线程池执行异步代码&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;四个静态方法演示&lt;/p&gt;
&lt;/blockquote&gt;
&lt;pre&gt;&lt;code&gt;/**
 * @author Guanghao Wei
 * @create 2023-04-10 12:16
 */
public class CompletableFutureBuildDemo {
    public static void main(String[] args) throws ExecutionException, InterruptedException {
        ExecutorService executorService = Executors.newFixedThreadPool(3);

        CompletableFuture&amp;lt;Void&amp;gt; completableFuture = CompletableFuture.runAsync(() -&amp;gt; {
            System.out.println(Thread.currentThread().getName());
            try {
                TimeUnit.SECONDS.sleep(1);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        },executorService);

        System.out.println(completableFuture.get()); //null


        CompletableFuture&amp;lt;String&amp;gt; objectCompletableFuture = CompletableFuture.supplyAsync(()-&amp;gt;{
            System.out.println(Thread.currentThread().getName());
            try {
                TimeUnit.SECONDS.sleep(1);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            return &quot;hello supplyAsync&quot;;
        },executorService);

        System.out.println(objectCompletableFuture.get());//hello supplyAsync

        executorService.shutdown();
        /**executorService.shutdown()
		调用了shutdown方法后
		1、线程池会把正在执行的任务及队列中等待执行的任务都执行完毕后，再去关闭；
		2、如果还有新的任务过来，线程池就会拒绝
		**/
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;CompletableFuture减少阻塞和轮询，可以传入回调对象，当异步任务完成或者发生异常时，自动调用回调对象的回调方法。&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;CompletableFuture使用演示&lt;/p&gt;
&lt;/blockquote&gt;
&lt;pre&gt;&lt;code&gt;/**
 * @author Guanghao Wei
 * @create 2023-04-10 12:28
 */
public class CompletableFutureUseDemo {
    public static void main(String[] args) throws ExecutionException, InterruptedException {
        //一定注意，工作中使用自定义线程池，防止出现用户线程结束同时守护线程自动结束问题。
        /**
        自定义线程池非守护线程,所以就会继续执行。
		如果不传入线程池，用自带的ForkJoinPool就是守护线程，防止出现用户线程结束同时守护线程自动结束问题，whenComplete中输出语句不输出
        **/
        ExecutorService executorService = Executors.newFixedThreadPool(3);
        CompletableFuture&amp;lt;Integer&amp;gt; completableFuture = CompletableFuture.supplyAsync(() -&amp;gt; {
            System.out.println(Thread.currentThread().getName() + &quot;---come in&quot;);
            int result = ThreadLocalRandom.current().nextInt(10);
            try {
                TimeUnit.SECONDS.sleep(1);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            if (result &amp;gt; 5) { //模拟产生异常情况
                int i = 10 / 0;
            }
            System.out.println(&quot;----------1秒钟后出结果&quot; + result);
            return result;
        }, executorService).whenComplete((v, e) -&amp;gt; {
            if (e == null) {
                System.out.println(&quot;计算完成 更新系统&quot; + v);
            }
        }).exceptionally(e -&amp;gt; {
            e.printStackTrace();
            System.out.println(&quot;异常情况：&quot; + e.getCause() + &quot; &quot; + e.getMessage());
            return null;
        });
        System.out.println(Thread.currentThread().getName() + &quot;先去完成其他任务&quot;);
        executorService.shutdown();
    }
}

/**
 * 无异常情况
 * pool-1-thread-1---come in
 * main先去完成其他任务
 * ----------1秒钟后出结果9
 * 计算完成 更新系统9
 */

/**
 * 有异常情况
 *pool-1-thread-1---come in
 * main先去完成其他任务
 * java.util.concurrent.CompletionException: java.lang.ArithmeticException: / by zero
 * 异常情况：java.lang.ArithmeticException: / by zero java.lang.ArithmeticException: / by zero
 */
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;CompletableFuture优点：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;异步任务&lt;strong&gt;结束&lt;/strong&gt;时，会自动回调某个对象的方法&lt;/li&gt;
&lt;li&gt;主线程设置好回调后，不用关心异步任务的执行，异步任务之间可以顺序执行&lt;/li&gt;
&lt;li&gt;异步任务&lt;strong&gt;出错&lt;/strong&gt;时，会自动回调某个对象的方法&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;2.4 案例精讲-从电商网站的比价需求展开&lt;/h2&gt;
&lt;h3&gt;2.4.1 函数式编程已成为主流&lt;/h3&gt;
&lt;p&gt;Lambda表达式+Stream流式调用+Chain链式调用+Java8函数式编程&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;函数时接口：&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Runnable：无参数、无返回值&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;img src=&quot;https://tangtangit.com/_astro/1681102072423-e5257231-ab21-4dd0-8958-08eb25a2fac9.gvtBsK8d_Z6fWmB.webp&quot; alt=&quot;img&quot; /&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Function：接受一个参数，并且有返回值&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;img src=&quot;https://tangtangit.com/_astro/1681102113588-e19a1db1-3298-4fcc-8936-56983d716441.ChGqqYjQ_3gBOf.webp&quot; alt=&quot;img&quot; /&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Consumer：接受一个参数，没有返回值&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;img src=&quot;https://tangtangit.com/_astro/1681102150000-d9768e71-77ae-4bfa-8ec7-c9a4e4358b50.sQwM8Le9_Z13jmS.webp&quot; alt=&quot;img&quot; /&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;BiConsumer：接受两个参数，没有返回值&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;img src=&quot;https://tangtangit.com/_astro/1681102215387-07880574-e9ce-4ca2-8d15-a7902868c0ef.B2jMqQQb_13iEdE.webp&quot; alt=&quot;img&quot; /&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Supplier：没有参数，有返回值&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;img src=&quot;https://tangtangit.com/_astro/1681102267638-4fa2e8b0-69c1-46f9-b259-b6de93f3dc24.BybRLYpf_Z2wlVzX.webp&quot; alt=&quot;img&quot; /&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;小结：&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://tangtangit.com/_astro/1681102318098-5bb02ac2-5d6f-414e-8d50-3f35e2cea54e.DdOr0TyA_AJqnT.webp&quot; alt=&quot;img&quot; /&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;chain链式调用：&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;/**
 * @author Guanghao Wei
 * @create 2023-04-10 12:53
 */
public class CompletableFutureMallDemo {
    public static void main(String[] args) {
        Student student = new Student();
        student.setId(1).setStudentName(&quot;z3&quot;).setMajor(&quot;english&quot;); //链式调用
    }
}


@AllArgsConstructor
@NoArgsConstructor
@Data
@Accessors(chain = true)//开启链式调用
class Student {
    private Integer id;
    private String studentName;
    private String major;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;CompletableFuture中join和get方法&lt;/h3&gt;
&lt;p&gt;是一样的，区别：join在编译的时候不会有异常抛出，get必须声明抛出异常。&lt;/p&gt;
&lt;h3&gt;2.4.2 大厂业务需求说明&lt;/h3&gt;
&lt;p&gt;切记：功能---&amp;gt;性能（完成---&amp;gt;完美）&lt;/p&gt;
&lt;p&gt;电商网站比价需求分析：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;需求说明：&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;ol&gt;
&lt;li&gt;同一款产品，同时搜索出同款产品在各大电商平台的售价&lt;/li&gt;
&lt;li&gt;同一款产品，同时搜索出本产品在同一个电商平台下，各个入驻卖家售价是多少&lt;/li&gt;
&lt;/ol&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;输出返回：&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;ol&gt;
&lt;li&gt;出来结果希望是同款产品的在不同地方的价格清单列表，返回一个List&amp;lt;String&amp;gt;&lt;/li&gt;
&lt;/ol&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;例如：《Mysql》 in jd price is 88.05  《Mysql》 in taobao price is 90.43&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;解决方案，对比同一个产品在各个平台上的价格，要求获得一个清单列表&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;ol&gt;
&lt;li&gt;step by step，按部就班，查完淘宝查京东，查完京东查天猫....&lt;/li&gt;
&lt;li&gt;all in，万箭齐发，一口气多线程异步任务同时查询&lt;/li&gt;
&lt;/ol&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;h3&gt;2.4.3 一波流Java8函数式编程带走-比价案例实战Case&lt;/h3&gt;
&lt;blockquote&gt;
&lt;p&gt;比价实战Case&lt;/p&gt;
&lt;/blockquote&gt;
&lt;pre&gt;&lt;code&gt;/**
 * @author Guanghao Wei
 * @create 2023-04-10 12:53
 * 这里面需要注意一下Stream流方法的使用
 * 这种异步查询的方法大大节省了时间消耗，可以融入简历项目中，和面试官有所探讨
 */
public class CompletableFutureMallDemo {
    static List&amp;lt;NetMall&amp;gt; list = Arrays.asList(new NetMall(&quot;jd&quot;), new NetMall(&quot;taobao&quot;), new NetMall(&quot;dangdang&quot;));

    /**
     * step by step
     * @param list
     * @param productName
     * @return
     */
    public static List&amp;lt;String&amp;gt; getPrice(List&amp;lt;NetMall&amp;gt; list, String productName) {
        //《Mysql》 in jd price is 88.05
        return list
                .stream()
                .map(netMall -&amp;gt;
                        String.format(&quot;《&quot; + productName + &quot;》&quot; + &quot;in %s price is %.2f&quot;,
                                netMall.getNetMallName(),
                                netMall.calcPrice(productName)))
                .collect(Collectors.toList());
    }

    /**
     * all in
     * 把list里面的内容映射给CompletableFuture()
     * @param list
     * @param productName
     * @return
     */
    public static List&amp;lt;String&amp;gt; getPriceByCompletableFuture(List&amp;lt;NetMall&amp;gt; list, String productName) {
        return list.stream().map(netMall -&amp;gt;
                        CompletableFuture.supplyAsync(() -&amp;gt;
                                String.format(&quot;《&quot; + productName + &quot;》&quot; + &quot;in %s price is %.2f&quot;,
                                        netMall.getNetMallName(),
                                        netMall.calcPrice(productName)))) //Stream&amp;lt;CompletableFuture&amp;lt;String&amp;gt;&amp;gt;
                .collect(Collectors.toList()) //List&amp;lt;CompletableFuture&amp;lt;String&amp;gt;&amp;gt;
                .stream()//Stream&amp;lt;String&amp;gt;
                .map(s -&amp;gt; s.join()).collect(Collectors.toList()); //List&amp;lt;String&amp;gt;
    }

    public static void main(String[] args) {
        /**
         * 采用step by setp方式查询
         * 《masql》in jd price is 110.11
         * 《masql》in taobao price is 109.32
         * 《masql》in dangdang price is 109.24
         * ------costTime: 3094 毫秒
         */
        long StartTime = System.currentTimeMillis();
        List&amp;lt;String&amp;gt; list1 = getPrice(list, &quot;masql&quot;);
        for (String element : list1) {
            System.out.println(element);
        }
        long endTime = System.currentTimeMillis();
        System.out.println(&quot;------costTime: &quot; + (endTime - StartTime) + &quot; 毫秒&quot;);

        /**
         * 采用 all in三个异步线程方式查询
         * 《mysql》in jd price is 109.71
         * 《mysql》in taobao price is 110.69
         * 《mysql》in dangdang price is 109.28
         * ------costTime1009 毫秒
         */
        long StartTime2 = System.currentTimeMillis();
        List&amp;lt;String&amp;gt; list2 = getPriceByCompletableFuture(list, &quot;mysql&quot;);
        for (String element : list2) {
            System.out.println(element);
        }
        long endTime2 = System.currentTimeMillis();
        System.out.println(&quot;------costTime&quot; + (endTime2 - StartTime2) + &quot; 毫秒&quot;);

    }
}

@AllArgsConstructor
@NoArgsConstructor
@Data
class NetMall {
    private String netMallName;

    public double calcPrice(String productName) {
        try {
            TimeUnit.SECONDS.sleep(1);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }

        return ThreadLocalRandom.current().nextDouble() * 2 + productName.charAt(0);
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;2.4.4 CompletableFuture常用方法&lt;/h2&gt;
&lt;h3&gt;获得结果和触发计算&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;ul&gt;
&lt;li&gt;获取结果&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;ul&gt;
&lt;li&gt;public T get()  不见不散（阻塞）&lt;/li&gt;
&lt;li&gt;public T get(long timeout,TimeUnit unit) 过时不候（超时就抛出异常）&lt;/li&gt;
&lt;li&gt;public T join() ---&amp;gt;和get一样的作用，只是不需要抛出异常&lt;/li&gt;
&lt;li&gt;public T getNow(T valuelfAbsent) ---&amp;gt;计算完成就返回正常值，否则返回备胎值（传入的参数），立即获取结果不阻塞&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;ul&gt;
&lt;li&gt;主动触发计算&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;ul&gt;
&lt;li&gt;public boolean complete(T value) ----&amp;gt;是否打断get方法立即返回括号值&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;对计算结果进行处理&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;ul&gt;
&lt;li&gt;thenApply ---&amp;gt;计算结果存在依赖关系，这两个线程串行化----&amp;gt;由于存在依赖关系（当前步错，不走下一步），当前步骤有异常的话就叫停&lt;/li&gt;
&lt;li&gt;handle ---&amp;gt;计算结果存在依赖关系，这两个线程串行化----&amp;gt;有异常也可以往下走一步&lt;/li&gt;
&lt;/ul&gt;
&lt;blockquote&gt;
&lt;p&gt;对计算结果进行处理演示&lt;/p&gt;
&lt;/blockquote&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;/**
 * @author Guanghao Wei
 * @create 2023-04-10 13:43
 */
public class CompletableFutureApiDemo {
    public static void main(String[] args) throws ExecutionException, InterruptedException, TimeoutException {
        ExecutorService threadPool = Executors.newFixedThreadPool(3);
        CompletableFuture&amp;lt;Integer&amp;gt; completableFuture = CompletableFuture.supplyAsync(() -&amp;gt; {
            try {
                TimeUnit.SECONDS.sleep(1);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            return 1;
        }, threadPool).thenApply(f -&amp;gt; {
            System.out.println(&quot;222&quot;);
            return f + 2;
        }).handle((f, e) -&amp;gt; {
            System.out.println(&quot;3333&quot;);
            int i=10/0;
            return f + 2;

//             thenApply(f -&amp;gt; {
//            System.out.println(&quot;3333&quot;);
//            return f + 2;
        }).whenComplete((v, e) -&amp;gt; {
            if (e == null) {
                System.out.println(&quot;----计算结果&quot; + v);
            }
        }).exceptionally(e -&amp;gt; {
            e.printStackTrace();
            System.out.println(e.getCause());
            return null;
        });
        System.out.println(Thread.currentThread().getName() + &quot;------主线程先去做其他事情&quot;);
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;对计算结果进行消费&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;ul&gt;
&lt;li&gt;接受任务的处理结果，并消费处理，无返回结果&lt;/li&gt;
&lt;li&gt;thenAccept&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;blockquote&gt;
&lt;p&gt;thenAccetp演示&lt;/p&gt;
&lt;/blockquote&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;/**
 * @author Guanghao Wei
 * @create 2023-04-10 13:59
 */
public class CompletableFutureApi2Demo {
    public static void main(String[] args) {
        ExecutorService threadPool = Executors.newFixedThreadPool(3);
        CompletableFuture.supplyAsync(() -&amp;gt; {
            return 1;
        }, threadPool).thenApply(f -&amp;gt; {
            return f + 2;
        }).thenApply(f -&amp;gt; {
            return f + 2;
        }).thenAccept(r -&amp;gt; {
            System.out.println(r);//5
        });
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;ul&gt;
&lt;li&gt;对比补充&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;ul&gt;
&lt;li&gt;thenRun(Runnable runnable) :任务A执行完执行B，并且不需要A的结果&lt;/li&gt;
&lt;li&gt;thenAccept(Consumer action): 任务A执行完执行B，B需要A的结果，但是任务B没有返回值&lt;/li&gt;
&lt;li&gt;thenApply(Function fn): 任务A执行完执行B，B需要A的结果，同时任务B有返回值&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;/**
 * @author Guanghao Wei
 * @create 2023-04-10 13:59
 */
public class CompletableFutureApi2Demo {
    public static void main(String[] args) {
        System.out.println(CompletableFuture.supplyAsync(() -&amp;gt; &quot;result&quot;).thenRun(() -&amp;gt; {}).join());//null
        System.out.println(CompletableFuture.supplyAsync(() -&amp;gt; &quot;result&quot;).thenAccept(r -&amp;gt; System.out.println(r)).join());//result null
        System.out.println(CompletableFuture.supplyAsync(() -&amp;gt; &quot;result&quot;).thenApply(f -&amp;gt; f + 2).join());//result2
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;ul&gt;
&lt;li&gt;CompletableFuture和线程池说明&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;ul&gt;
&lt;li&gt;如果没有传入自定义线程池，都用默认线程池ForkJoinPool&lt;/li&gt;
&lt;li&gt;传入一个线程池，如果你执行第一个任务时，传入了一个自定义线程池&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;ul&gt;
&lt;li&gt;调用thenRun方法执行第二个任务时，则第二个任务和第一个任务时共用同一个线程池&lt;/li&gt;
&lt;li&gt;调用thenRunAsync执行第二个任务时，则第一个任务使用的是你自定义的线程池，第二个任务使用的是ForkJoin线程池&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;ul&gt;
&lt;li&gt;备注：可能是线程处理太快，系统优化切换原则， 直接使用main线程处理，thenAccept和thenAcceptAsync，thenApply和thenApplyAsync等，之间的区别同理。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;对计算速度进行选用&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;ul&gt;
&lt;li&gt;谁快用谁&lt;/li&gt;
&lt;li&gt;applyToEither&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;blockquote&gt;
&lt;p&gt;applyToEither演示&lt;/p&gt;
&lt;/blockquote&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;/**
 * @author Guanghao Wei
 * @create 2023-04-10 14:11
 * 可以合并写在一起，不必拆分
 */
public class CompletableFutureApiDemo {
    public static void main(String[] args) {
        ExecutorService threadPool = Executors.newFixedThreadPool(3);
        CompletableFuture&amp;lt;String&amp;gt; playA = CompletableFuture.supplyAsync(() -&amp;gt; {
            try {
                System.out.println(&quot;A come in&quot;);
                TimeUnit.SECONDS.sleep(2);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            return &quot;playA&quot;;
        }, threadPool);


        CompletableFuture&amp;lt;String&amp;gt; playB = CompletableFuture.supplyAsync(() -&amp;gt; {
            try {
                System.out.println(&quot;B come in&quot;);
                TimeUnit.SECONDS.sleep(3);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            return &quot;playB&quot;;
        }, threadPool);

        CompletableFuture&amp;lt;String&amp;gt; result = playA.applyToEither(playB, f -&amp;gt; {
            return f + &quot; is winner&quot;;
        });

        /**
         * A come in
         * B come in
         * main-----------winner:playA is winner
         */
        System.out.println(Thread.currentThread().getName() + &quot;-----------winner:&quot; + result.join());
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;对计算结果进行合并&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;ul&gt;
&lt;li&gt;两个CompletableStage任务都完成后，最终能把两个任务的结果一起交给thenCombine来处理&lt;/li&gt;
&lt;li&gt;先完成的先等着，等待其他分支任务&lt;/li&gt;
&lt;/ul&gt;
&lt;blockquote&gt;
&lt;p&gt;thenCombine演示&lt;/p&gt;
&lt;/blockquote&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;/**
 * @author Guanghao Wei
 * @create 2023-04-10 14:28
 * 可以合并写在一起，不必拆分
 */
public class CompletableFutureApi3Demo {
    public static void main(String[] args) {
        CompletableFuture&amp;lt;Integer&amp;gt; completableFuture1 = CompletableFuture.supplyAsync(() -&amp;gt; {
            System.out.println(Thread.currentThread().getName() + &quot; 启动&quot;);
            try {
                TimeUnit.SECONDS.sleep(1);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            return 10;
        });

        CompletableFuture&amp;lt;Integer&amp;gt; completableFuture2 = CompletableFuture.supplyAsync(() -&amp;gt; {
            System.out.println(Thread.currentThread().getName() + &quot; 启动&quot;);
            try {
                TimeUnit.SECONDS.sleep(2);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            return 20;
        });

        CompletableFuture&amp;lt;Integer&amp;gt; finalResult = completableFuture1.thenCombine(completableFuture2, (x, y) -&amp;gt; {
            System.out.println(&quot;----------开始两个结果合并&quot;);
            return x + y;
        });
        System.out.println(finalResult.join());

    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h1&gt;3. 说说Java&quot;锁&quot;事&lt;/h1&gt;
&lt;h2&gt;3.1 从轻松的乐观锁和悲观锁开讲&lt;/h2&gt;
&lt;p&gt;●悲观锁： 认为自己在使用数据的时候一定有别的线程来修改数据，因此在获取数据的时候会先加锁，确保数据不会被别的线程修改，synchronized和Lock的实现类都是悲观锁，适合写操作多的场景，先加锁可以保证写操作时数据正确，显示的锁定之后再操作同步资源-----狼性锁&lt;/p&gt;
&lt;p&gt;●乐观锁： 认为自己在使用数据的时候不会有别的线程修改数据或资源，不会添加锁，Java中使用无锁编程来实现，只是在更新的时候去判断，之前有没有别的线程更新了这个数据，如果这个数据没有被更新，当前线程将自己修改的数据成功写入，如果已经被其他线程更新，则根据不同的实现方式执行不同的操作，比如：放弃修改、重试抢锁等等。判断规则有：版本号机制Version，最常采用的是CAS算法，Java原子类中的递增操作就通过CAS自旋实现的。-----适合读操作多的场景，不加锁的特性能够使其读操作的性能大幅提升，乐观锁则直接去操作同步资源，是一种无锁算法，得之我幸不得我命---佛系锁&lt;/p&gt;
&lt;h2&gt;3.2 通过8种情况演示锁运行案例，看看锁到底是什么&lt;/h2&gt;
&lt;h3&gt;3.2.1 锁相关的8种案例演示code&lt;/h3&gt;
&lt;blockquote&gt;
&lt;p&gt;8种案例演示&lt;/p&gt;
&lt;/blockquote&gt;
&lt;pre&gt;&lt;code&gt;/**
 * @author Guanghao Wei
 * @create 2023-04-10 14:57
 */

class Phone {
    public synchronized void sendEmail() {
        try {
            TimeUnit.SECONDS.sleep(3);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println(&quot;------sendEmail&quot;);
    }

    public synchronized void sendSMS() {
        System.out.println(&quot;------sendSMS&quot;);
    }

    public void hello() {
        System.out.println(&quot;------hello&quot;);
    }
}

/**
 * 现象描述：
 * 1 标准访问ab两个线程，请问先打印邮件还是短信？ --------先邮件，后短信  共用一个对象锁
 * 2. sendEmail钟加入暂停3秒钟，请问先打印邮件还是短信？---------先邮件，后短信  共用一个对象锁
 * 3. 添加一个普通的hello方法，请问先打印普通hello方法还是邮件？ --------先hello，再邮件
 * 4. 有两部手机，请问先打印邮件还是短信？ ----先短信后邮件  资源没有争抢，不是同一个对象锁
 * 5. 有两个静态同步方法，一步手机， 请问先打印邮件还是短信？---------先邮件后短信  共用一个类锁
 * 6. 有两个静态同步方法，两部手机， 请问先打印邮件还是短信？ ----------先邮件后短信 共用一个类锁
 * 7. 有一个静态同步方法 一个普通同步方法，请问先打印邮件还是短信？ ---------先短信后邮件   一个用类锁一个用对象锁
 * 8. 有一个静态同步方法，一个普通同步方法，两部手机，请问先打印邮件还是短信？ -------先短信后邮件 一个类锁一个对象锁
 */

public class Lock8Demo {
    public static void main(String[] args) {
        Phone phone = new Phone();
        new Thread(() -&amp;gt; {
            phone.sendEmail();
        }, &quot;a&quot;).start();

        try {
            TimeUnit.MILLISECONDS.sleep(200);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }

        new Thread(() -&amp;gt; {
            phone.sendSMS();
        }, &quot;b&quot;).start();
    }

}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;结论：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;对于普通同步方法，锁的是当前实例对象，通常指this，所有的同步方法用的都是同一把锁---&amp;gt;实例对象本身&lt;/li&gt;
&lt;li&gt;对于静态同步方法，锁的时当前类的Class对象&lt;/li&gt;
&lt;li&gt;对于同步方法块，锁的时synchronized括号内的对象&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;3.2.2 synchronized有三种应用方式&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;作用于实例方法，当前实例加锁，进入同步代码块前要获得当前实例的锁；&lt;/li&gt;
&lt;li&gt;作用于代码块，对括号里配置的对象加锁&lt;/li&gt;
&lt;li&gt;作用于静态方法，当前类加锁，进去同步代码前要获得当前类对象的锁&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;3.2.3 从字节码角度分析synchronized实现&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;javap -c(v附加信息) ***.class 文件反编译&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;synchronized同步代码块&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;ul&gt;
&lt;li&gt;实现使用的是monitorenter和monitorexit指令&lt;/li&gt;
&lt;li&gt;&lt;img src=&quot;https://tangtangit.com/_astro/1681111681046-2feb9423-0b8d-4d54-aeab-70d3ae432fe7.CZ0lhQNS_Z1SKX65.webp&quot; alt=&quot;img&quot; /&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;synchronized普通同步方法&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;ul&gt;
&lt;li&gt;调用指令将会检查方法的ACC_SYNCHRONIZED访问标志是否被设置，如果设置了，执行线程会将现持有monitor锁，然后再执行该方法，最后在方法完成（无论是否正常结束）时释放monitor&lt;/li&gt;
&lt;li&gt;&lt;img src=&quot;https://tangtangit.com/_astro/1681112009525-6628c636-a35a-420e-b4fd-5980b700de9c.CIvUmIWb_QKFVQ.webp&quot; alt=&quot;img&quot; /&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;synchronized静态同步方法&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;ul&gt;
&lt;li&gt;ACC_STATIC、ACC_SYNCHRONIZED访问标志区分该方法是否是静态同步方法&lt;/li&gt;
&lt;li&gt;&lt;img src=&quot;https://tangtangit.com/_astro/1681112126738-53ea5b8f-aab8-4787-9bad-dcad0c809d8a.DvnNKm_S_Zkwlvb.webp&quot; alt=&quot;img&quot; /&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;3.2.4 反编译synchronized锁的是什么&lt;/h3&gt;
&lt;p&gt;面试题：为什么任何一个对象都可以成为一个锁？&lt;/p&gt;
&lt;p&gt;C++源码：ObjectMonitor.java---&amp;gt;ObjectMonitor.cpp---&amp;gt;ObjectMonitor.hpp&lt;/p&gt;
&lt;p&gt;每个对象天生都带着一个对象监视器，每一个被锁住的对象都会和Monitor关联起来&lt;/p&gt;
&lt;p&gt;总结：指针指向Monitor对象（也称为管程或监视器）的真实地址。每个对象都存在着一个monitor与之关联，当一个monitor被某个线程持有后，它便处于锁定状态。在Java虚拟机（HotSpot）中，monitor是由ObjectMonitor实现的，其主要的数据结构如下（位于HotSpot虚拟机源码ObjectMonitor.hpp文件，C++实现）：&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://tangtangit.com/_astro/1681116200663-e521f7bf-78b9-49a4-a448-2fd845c37ea0.Br-GbyIK_pIj50.webp&quot; alt=&quot;img&quot; /&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://tangtangit.com/_astro/1681116040902-0b37eada-7c94-4611-a45e-db25c56e390d.DxtIflGT_ZBVTJC.webp&quot; alt=&quot;img&quot; /&gt;&lt;/p&gt;
&lt;h3&gt;3.2.5 对于Synchronized关键字&lt;/h3&gt;
&lt;p&gt;后面章节详说&lt;/p&gt;
&lt;h2&gt;3.3 公平锁和非公平锁&lt;/h2&gt;
&lt;h3&gt;3.3.1 何为公平锁/非公平锁&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;公平锁：是指多个线程按照申请锁的顺序来获取锁，这里类似于排队买票，先来的人先买，后来的人再队尾排着，这是公平的----- Lock lock = new ReentrantLock(true)---表示公平锁，先来先得。&lt;/li&gt;
&lt;li&gt;非公平锁：是指多个线程获取锁的顺序并不是按照申请的顺序，有可能后申请的线程比先申请的线程优先获取锁，在高并发环境下，有可能造成优先级反转或者饥饿的状态（某个线程一直得不到锁）---- Lock lock = new  ReentrantLock(false)---表示非公平锁，后来的也可能先获得锁，默认为非公平锁。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;面试题：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;为什么会有公平锁/非公平锁的设计？为什么默认非公平？&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;ul&gt;
&lt;li&gt;减少CPU空闲状态时间：恢复挂起的线程到真正锁的获取还是有时间差的，从开发人员来看这个时间微乎其微，但是从CPU的角度来看，这个时间差存在的还是很明显的。所以非公平锁能更充分地利用CPU的时间片，尽量减少CPU空闲状态时间。&lt;/li&gt;
&lt;li&gt;减少线程切换的开销：使用多线程很重要的考量点是线程切换的开销，当采用非公平锁时，当一个线程请求锁获取同步状态，然后释放同步状态，所以刚释放锁的线程在此刻再次获取同步状态的概率就变得很大，所以就减少了线程的开销。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;什么时候用公平？什么时候用非公平？&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;ul&gt;
&lt;li&gt;如果为了更高的吞吐量，很显然非公平锁是比较合适的，因为节省了很多线程切换的时间，吞吐量自然就上去了；否则就用公平锁，大家公平使用。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;3.3.2 预埋伏AQS&lt;/h3&gt;
&lt;p&gt;后续深入分析&lt;/p&gt;
&lt;h2&gt;3.4 可重入锁（递归锁）&lt;/h2&gt;
&lt;h3&gt;3.4.1 概念说明&lt;/h3&gt;
&lt;p&gt;是指在同一线程在外层方法获取到锁的时侯，在进入该线程的内层方法会自动获取锁（前提，锁对象的是同一个对象），不会因为之前已经获取过还没释放而阻塞---------优点之一就是可一定程度避免死锁。&lt;/p&gt;
&lt;h3&gt;3.4.2 可重入锁种类&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;隐式锁（即synchronized关键字使用的锁），默认是可重入锁&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;ul&gt;
&lt;li&gt;在一个synchronized修饰的方法或者代码块的内部调用本类的其他synchronized修饰的方法或者代码块时，是永远可以得到锁。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;显式锁（即Lock）也有ReentrantLock这样的可重入锁&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;blockquote&gt;
&lt;p&gt;隐式和显示可重入锁的演示&lt;/p&gt;
&lt;/blockquote&gt;
&lt;pre&gt;&lt;code&gt;/**
 * @author Guanghao Wei
 * @create 2023-04-10 16:05
 */
public class ReEntryLockDemo {

    public static void main(String[] args) {
        final Object o = new Object();
        /**
         * ---------------外层调用
         * ---------------中层调用
         * ---------------内层调用
         */
        new Thread(() -&amp;gt; {
            synchronized (o) {
                System.out.println(&quot;---------------外层调用&quot;);
                synchronized (o) {
                    System.out.println(&quot;---------------中层调用&quot;);
                    synchronized (o) {
                        System.out.println(&quot;---------------内层调用&quot;);
                    }
                }
            }
        }, &quot;t1&quot;).start();

        /**
         * 注意：加锁几次就需要解锁几次
         * ---------------外层调用
         * ---------------中层调用
         * ---------------内层调用
         */
        Lock lock = new ReentrantLock();
        new Thread(() -&amp;gt; {
            lock.lock();
            try {
                System.out.println(&quot;---------------外层调用&quot;);
                lock.lock();
                try {
                    System.out.println(&quot;---------------中层调用&quot;);
                    lock.lock();
                    try {
                        System.out.println(&quot;---------------内层调用&quot;);
                    } finally {
                        lock.unlock();
                    }
                } finally {
                    lock.unlock();
                }
            } finally {
                lock.unlock();
            }
        }, &quot;t2&quot;).start();
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;3.5 死锁及排查&lt;/h2&gt;
&lt;h3&gt;3.5.1 概念&lt;/h3&gt;
&lt;p&gt;死锁是指两个或两个以上的线程在执行过程中，因抢夺资源而造成的一种互相等待的现象，若无外力干涉，则它们无法再继续推进下去。&lt;/p&gt;
&lt;p&gt;产生原因：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;系统资源不足&lt;/li&gt;
&lt;li&gt;进程运行推进顺序不合适&lt;/li&gt;
&lt;li&gt;系统资源分配不当&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;img src=&quot;https://tangtangit.com/_astro/1681114824880-04489b15-8c38-4a05-8320-cea66ccc805a.DflJfd60_Jqf44.webp&quot; alt=&quot;img&quot; /&gt;&lt;/p&gt;
&lt;h3&gt;3.5.2 写一个死锁代码case&lt;/h3&gt;
&lt;blockquote&gt;
&lt;p&gt;实现死锁&lt;/p&gt;
&lt;/blockquote&gt;
&lt;pre&gt;&lt;code&gt;/**
 * @author Guanghao Wei
 * @create 2023-04-10 16:20
 */
public class DeadLockDemo {
    static  Object a=new Object();
    static  Object b=new Object();

    public static void main(String[] args) {
        new Thread(() -&amp;gt; {
            synchronized (a){
                System.out.println(&quot;t1线程持有a锁，试图获取b锁&quot;);
                try {
                    TimeUnit.SECONDS.sleep(1);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                synchronized (b){
                    System.out.println(&quot;t1线程获取到b锁&quot;);
                }
            }
         },&quot;t1&quot;).start();

        new Thread(() -&amp;gt; {
            synchronized (b){
                System.out.println(&quot;t2线程持有a锁，试图获取a锁&quot;);
                try {
                    TimeUnit.SECONDS.sleep(1);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                synchronized (a){
                    System.out.println(&quot;t2线程获取到a锁&quot;);
                }
            }
        },&quot;t2&quot;).start();
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;3.5.3 如何排查死锁&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;纯命令&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;ul&gt;
&lt;li&gt;jps -l&lt;/li&gt;
&lt;li&gt;jstack 进程编号&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;图形化&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;ul&gt;
&lt;li&gt;jconsole&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;img src=&quot;https://tangtangit.com/_astro/1681115056604-54d64a63-1ac8-4a35-8305-ceb8c42a8254.wa7Ybc0z_iwJO.webp&quot; alt=&quot;img&quot; /&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://tangtangit.com/_astro/1681115233726-66c08a0f-95ff-43d7-9ef1-471da2a218f2.B6mks447_Z2slYmt.webp&quot; alt=&quot;img&quot; /&gt;&lt;/p&gt;
&lt;h2&gt;3.6 写锁（独占锁）/读锁（共享锁）&lt;/h2&gt;
&lt;p&gt;深度源码分析见后面&lt;/p&gt;
&lt;h2&gt;3.7 自旋锁spinLock&lt;/h2&gt;
&lt;p&gt;深度源码分析见后面&lt;/p&gt;
&lt;h2&gt;3.8 无锁-&amp;gt;独占锁-&amp;gt;读写锁-&amp;gt;邮戳锁&lt;/h2&gt;
&lt;p&gt;深度源码分析见后面&lt;/p&gt;
&lt;h2&gt;3.9 无锁-&amp;gt;偏向锁-&amp;gt;轻量锁-&amp;gt;重量锁&lt;/h2&gt;
&lt;p&gt;深度源码分析见后面&lt;/p&gt;
&lt;h1&gt;4. LockSupport与线程中断&lt;/h1&gt;
&lt;h2&gt;4.1 线程中断机制&lt;/h2&gt;
&lt;h3&gt;4.1.1 从阿里蚂蚁金服面试题讲起&lt;/h3&gt;
&lt;p&gt;Java.lang.Thread下的三个方法:&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://tangtangit.com/_astro/1681180117153-60196330-81fd-412e-bb68-27316eb19977.COalgzYL_Z16jVwr.webp&quot; alt=&quot;image.png&quot; /&gt;&lt;/p&gt;
&lt;p&gt;●如何中断一个运行中的线程？&lt;/p&gt;
&lt;p&gt;●如何停止一个运行中的线程？&lt;/p&gt;
&lt;h3&gt;4.1.2 什么是中断机制&lt;/h3&gt;
&lt;p&gt;●首先，一个线程不应该由其他线程来强制中断或停止，而是应该由线程自己自行停止，自己来决定自己的命运，所以，Thread.stop,Thread.suspend,Thread.resume都已经被废弃了&lt;/p&gt;
&lt;p&gt;●其次，在Java中没有办法立即停止一条线程，然而停止线程却显得尤为重要，如取消一个耗时操作。因此，Java提供了一种用于停止线程的协商机制----中断，也即中断标识协商机制&lt;/p&gt;
&lt;p&gt;○中断只是一种协作协商机制，Java没有给中断增加任何语法，中断的过程完全需要程序员自行实现。若要中断一个线程，你需要手动调用该线程interrupt方法，该方法也仅仅是将该线程对象的中断标识设置为true，接着你需要自己写代码不断检测当前线程的标识位，如果为true，表示别的线程请求这条线程中断，此时究竟应该做什么需要你自己写代码实现。&lt;/p&gt;
&lt;p&gt;○每个线程对象都有一个中断标识位，用于表示线程是否被中断；该标识位为true表示中断，为false表示未中断；通过调用线程对象的interrupt方法将该线程的标识位设置为true；可以在别的线程中调用，也可以在自己的线程中调用。&lt;/p&gt;
&lt;h3&gt;4.1.3 中断的相关API方法之三大方法说明&lt;/h3&gt;
&lt;p&gt;●public void interrupt()&lt;/p&gt;
&lt;p&gt;○实例方法 Just to set the interrupt flag&lt;/p&gt;
&lt;p&gt;○实例方法仅仅是设置线程的中断状态为true，发起一个协商而不会立刻停止线程&lt;/p&gt;
&lt;p&gt;●public static boolean interrupted()&lt;/p&gt;
&lt;p&gt;○静态方法 Thread.interrupted();&lt;/p&gt;
&lt;p&gt;○判断线程是否被中断并清除当前中断状态（做了两件事情）&lt;/p&gt;
&lt;p&gt;■1.返回当前线程的中断状态，测试当前线程是否已被中断&lt;/p&gt;
&lt;p&gt;■2.将当前线程的中断状态清零并重新设置为false，清除线程的中断状态&lt;/p&gt;
&lt;p&gt;■3.这个方法有点不好理解在于如果连续两次调用此方法，则第二次返回false，因为连续调用两次的结果可能不一样&lt;/p&gt;
&lt;p&gt;●public boolean isInterrupted()&lt;/p&gt;
&lt;p&gt;○实例方法&lt;/p&gt;
&lt;p&gt;○判断当前线程是否被中断（通过检查中断标志位）&lt;/p&gt;
&lt;h3&gt;4.1.4 大厂面试题中断机制考点&lt;/h3&gt;
&lt;p&gt;●如何停止中断运行中的线程？&lt;/p&gt;
&lt;p&gt;○通过一个volatile变量实现&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;/**
 * @author Guanghao Wei
 * @create 2023-04-11 10:29
 * 使用volatile修饰一个标识符来决定是否结束线程
 */
public class InterruptDemo {
    static volatile boolean isStop = false; //volatile表示的变量具有可见性

    public static void main(String[] args) {
        new Thread(() -&amp;gt; {
            while (true) {
                if (isStop) {
                    System.out.println(Thread.currentThread().getName() + &quot; isStop的值被改为true，t1程序停止&quot;);
                    break;
                }
                System.out.println(&quot;-----------hello volatile&quot;);
            }
        }, &quot;t1&quot;).start();
        try {
            TimeUnit.MILLISECONDS.sleep(10);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }

        new Thread(() -&amp;gt; {
            isStop = true;
        }, &quot;t2&quot;).start();

    }
}
/**
 * -----------hello volatile
 * -----------hello volatile
 * -----------hello volatile
 * -----------hello volatile
 * -----------hello volatile
 * -----------hello volatile
 * t1 isStop的值被改为true，t1程序停止
 */
&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;ul&gt;
&lt;li&gt;通过AutomicBoolean&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;/**
 * @author Guanghao Wei
 * @create 2023-04-11 10:29
 * 使用AtomicBoolean
 */
public class InterruptDemo {
    static AtomicBoolean atomicBoolean = new AtomicBoolean(false);


    public static void main(String[] args) {
        new Thread(() -&amp;gt; {
            while (true) {
                if (atomicBoolean.get()) {
                    System.out.println(Thread.currentThread().getName() + &quot; atomicBoolean的值被改为true，t1程序停止&quot;);
                    break;
                }
                System.out.println(&quot;-----------hello atomicBoolean&quot;);
            }
        }, &quot;t1&quot;).start();
        try {
            TimeUnit.MILLISECONDS.sleep(10);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }

        new Thread(() -&amp;gt; {
            atomicBoolean.set(true);
        }, &quot;t2&quot;).start();

    }
}

/**
 * -----------hello atomicBoolean
 * -----------hello atomicBoolean
 * -----------hello atomicBoolean
 * -----------hello atomicBoolean
 * -----------hello atomicBoolean
 * t1 atomicBoolean的值被改为true，t1程序停止
 */
&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;ul&gt;
&lt;li&gt;通过Thread类自带的中断API实例方法实现----在需要中断的线程中不断监听中断状态，一旦发生中断，就执行相应的中断处理业务逻辑stop线程。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;blockquote&gt;
&lt;p&gt;interrupt() 和isInterrupted()组合使用来中断某个线程演示&lt;/p&gt;
&lt;/blockquote&gt;
&lt;pre&gt;&lt;code&gt;/**
 * @author Guanghao Wei
 * @create 2023-04-11 10:29
 * 使用interrupt() 和isInterrupted()组合使用来中断某个线程
 */
public class InterruptDemo {
    static AtomicBoolean atomicBoolean = new AtomicBoolean(false);


    public static void main(String[] args) {
        Thread t1 = new Thread(() -&amp;gt; {
            while (true) {
                if (Thread.currentThread().isInterrupted()) {
                    System.out.println(Thread.currentThread().getName() + &quot; isInterrupted()的值被改为true，t1程序停止&quot;);
                    break;
                }
                System.out.println(&quot;-----------hello isInterrupted()&quot;);
            }
        }, &quot;t1&quot;);
        t1.start();

        try {
            TimeUnit.MILLISECONDS.sleep(10);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }

        //t2向t1放出协商，将t1中的中断标识位设为true，希望t1停下来
        new Thread(() -&amp;gt; t1.interrupt(), &quot;t2&quot;).start();

        //当然，也可以t1自行设置
        t1.interrupt();

    }
}
/**
 * -----------hello isInterrupted()
 * -----------hello isInterrupted()
 * -----------hello isInterrupted()
 * -----------hello isInterrupted()
 * t1 isInterrupted()的值被改为true，t1程序停止
 */
&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;当前线程的中断标识为true，是不是线程就立刻停止？&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;答案是不立刻停止，具体来说，当对一个线程，调用interrupt时：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;ul&gt;
&lt;li&gt;如果线程处于正常活动状态，那么会将该线程的中断标志设置为true，仅此而已，被设置中断标志的线程将继续正常运行，不受影响，所以interrupt()并不能真正的中断线程，需要被调用的线程自己进行配合才行，对于不活动的线程没有任何影响。&lt;/li&gt;
&lt;li&gt;如果线程处于阻塞状态（例如sleep,wait,join状态等），在别的线程中调用当前线程对象的interrupt方法，那么线程将立即退出被阻塞状态（interrupt状态也将被清除），并抛出一个InterruptedException异常&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;blockquote&gt;
&lt;p&gt;第一种情况正常活动状态演示&lt;/p&gt;
&lt;/blockquote&gt;
&lt;pre&gt;&lt;code&gt;/**
 * @author Guanghao Wei
 * @create 2023-04-11 11:13
 * 执行interrupt方法将t1标志位设置为true后，t1没有中断，仍然完成了任务后再结束
 * 在2000毫秒后，t1已经结束称为不活动线程，设置状态为没有任何影响
 */
public class InterruptDemo2 {
    public static void main(String[] args) {
        //实例方法interrupt()仅仅是设置线程的中断状态位为true，不会停止线程
        Thread t1 = new Thread(() -&amp;gt; {
            for (int i = 1; i &amp;lt;= 300; i++) {
                System.out.println(&quot;------: &quot; + i);
            }
            /**
             * ------: 298
             * ------: 299
             * ------: 300
             * t1线程调用interrupt()后的中断标志位02：true
             */
            System.out.println(&quot;t1线程调用interrupt()后的中断标志位02：&quot; + Thread.currentThread().isInterrupted());
        }, &quot;t1&quot;);
        t1.start();

        System.out.println(&quot;t1线程默认的中断标志位：&quot; + t1.isInterrupted());//false

        try {
            TimeUnit.MILLISECONDS.sleep(2);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }

        t1.interrupt();//true
        /**
         * ------: 251
         * ------: 252
         * ------: 253
         * t1线程调用interrupt()后的中断标志位01：true
         */
        System.out.println(&quot;t1线程调用interrupt()后的中断标志位01：&quot; + t1.isInterrupted());//true

        try {
            TimeUnit.MILLISECONDS.sleep(2000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        //2000毫秒后，t1线程已经不活动了，不会产生任何影响
        System.out.println(&quot;t1线程调用interrupt()后的中断标志位03：&quot; + t1.isInterrupted());//false

    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote&gt;
&lt;p&gt;第二种情况线程处于阻塞状态演示&lt;/p&gt;
&lt;/blockquote&gt;
&lt;pre&gt;&lt;code&gt;/**
 * @author Guanghao Wei
 * @create 2023-04-11 11:23
 *1. 中断标志位默认为false
 * 2.t2对t1发出中断协商  t1.interrupt();
 * 3. 中断标志位为true： 正常情况 程序停止
 *     中断标志位为true  异常情况，.InterruptedException ，将会把中断状态清楚，中断标志位为false
 * 4。需要在catch块中，再次调用interrupt()方法将中断标志位设置为false;
 */
public class InterruptDemo3 {
    public static void main(String[] args) {
        Thread t1 = new Thread(() -&amp;gt; {
            while (true) {
                if (Thread.currentThread().isInterrupted()) {
                    System.out.println(Thread.currentThread().getName() + &quot; 中断标志位为：&quot; + Thread.currentThread().isInterrupted() + &quot; 程序停止&quot;);
                    break;
                }
                //sleep方法抛出InterruptedException后，中断标识也被清空置为false，如果没有在
                //catch方法中调用interrupt方法再次将中断标识置为true，这将导致无限循环了
                try {
                    Thread.sleep(200);
                } catch (InterruptedException e) {
                    //Thread.currentThread().interrupt(); 
                    e.printStackTrace();
                }
                System.out.println(&quot;-------------hello InterruptDemo3&quot;);

            }
        }, &quot;t1&quot;);
        t1.start();

        try {
            TimeUnit.SECONDS.sleep(1);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }

        new Thread(() -&amp;gt; {
            t1.interrupt();
        }, &quot;t2&quot;).start();
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;对于第二种情况的源码分析如下；&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://tangtangit.com/_astro/1681184391254-fbf8c476-1295-46a1-86e9-6d3d5a4df1fd.DwDlI2VB_Z1w6KvE.webp&quot; alt=&quot;image.png&quot; /&gt;&lt;/p&gt;
&lt;p&gt;总之，需要记住的是中断只是一种协商机制，修改中断标识位仅此而已，不是立刻stop打断&lt;/p&gt;
&lt;p&gt;●静态方法Thread.interrupted()，谈谈你的理解？&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://tangtangit.com/_astro/1681184832631-a1805f72-7551-412f-9374-60dd8521ac87.B1Al6KFZ_Z1x6iSm.webp&quot; alt=&quot;image.png&quot; /&gt;&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;静态方法Thread.interrupted()演示&lt;/p&gt;
&lt;/blockquote&gt;
&lt;pre&gt;&lt;code&gt;/**
 * @author Guanghao Wei
 * @create 2023-04-11 11:47
 */
public class InterruptDemo4 {
    public static void main(String[] args) {
        /**
         * main	false
         * main	false
         * -----------1
         * -----------2
         * main	true
         * main	false
         */
        System.out.println(Thread.currentThread().getName() + &quot;\t&quot; + Thread.interrupted());//false
        System.out.println(Thread.currentThread().getName() + &quot;\t&quot; + Thread.interrupted());//false
        System.out.println(&quot;-----------1&quot;);
        Thread.currentThread().interrupt();
        System.out.println(&quot;-----------2&quot;);
        System.out.println(Thread.currentThread().getName() + &quot;\t&quot; + Thread.interrupted());//true
        System.out.println(Thread.currentThread().getName() + &quot;\t&quot; + Thread.interrupted());//false

    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;对于静态方法Thread.interrupted()和实例方法isInterrupted()区别在于：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;静态方法interrupted将会清除中断状态（传入的参数ClearInterrupted为true）&lt;/li&gt;
&lt;li&gt;实例方法isInterrupted则不会（传入的参数ClearInterrupted为false）&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;img src=&quot;https://tangtangit.com/_astro/1681185598859-c576641a-e30f-4753-8bcd-0ed9250062be.CovmrGEZ_Zl4V5B.webp&quot; alt=&quot;img&quot; /&gt;&lt;/p&gt;
&lt;h3&gt;4.1.5 总结&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;public void interrupt() 是一个实例方法，它通知目标线程中断，也仅仅是设置目标线程的中断标志位为true&lt;/li&gt;
&lt;li&gt;public boolean isInterrupted() 是一个实例方法，它判断当前线程是否被中断（通过检查中断标志位）并获取中断标志&lt;/li&gt;
&lt;li&gt;public static boolean interrupted() 是一个静态方法，返回当前线程的中断真实状态（boolean类型）后会将当前线程的中断状态设为false，此方法调用之后会清楚当前线程的中断标志位的状态（将中断标志置为false了），返回当前值并清零置为false。&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;4.2 LockSupport是什么&lt;/h2&gt;
&lt;p&gt;LockSupport是用来创建锁和其他同步类的基本线程阻塞原语，其中park()和unpack()而作用分别是阻塞线程和解除阻塞线程.&lt;/p&gt;
&lt;h2&gt;4.3 线程等待唤醒机制&lt;/h2&gt;
&lt;h3&gt;4.3.1 三种让线程等待和唤醒的方法&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;方式一：使用Object中的wait()方法让线程等待，使用Object中的notify()方法唤醒线程&lt;/li&gt;
&lt;li&gt;方式二：使用JUC包中的Condition的await()方法让线程等待，使用signal()方法唤醒线程&lt;/li&gt;
&lt;li&gt;方式三：LockSupport类可以阻塞当前线程以及唤醒指定被阻塞的线程&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;4.3.2 Object类中的wait和notify方法实现线程等待和唤醒&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;wait和notify方法必须要在同步代码块或者方法里面，且成对出现使用&lt;/li&gt;
&lt;li&gt;先wait再notify才ok&lt;/li&gt;
&lt;/ul&gt;
&lt;blockquote&gt;
&lt;p&gt;Object类中的wait和notify方法实现线程等待和唤醒演示&lt;/p&gt;
&lt;/blockquote&gt;
&lt;pre&gt;&lt;code&gt;/**
 * @author Guanghao Wei
 * @create 2023-04-11 12:13
 */
public class LockSupportDemo {

    public static void main(String[] args) {
        Object objectLock = new Object();
        /**
         * t1	 -----------come in
         * t2	 -----------发出通知
         * t1	 -------被唤醒
         */
        new Thread(() -&amp;gt; {
            synchronized (objectLock) {
                System.out.println(Thread.currentThread().getName() + &quot;\t -----------come in&quot;);
                try {
                    objectLock.wait();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                System.out.println(Thread.currentThread().getName() + &quot;\t -------被唤醒&quot;);
            }
        }, &quot;t1&quot;).start();

        try {
            TimeUnit.SECONDS.sleep(1);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }

        new Thread(() -&amp;gt; {
            synchronized (objectLock) {
                objectLock.notify();
                System.out.println(Thread.currentThread().getName() + &quot;\t -----------发出通知&quot;);
            }

        }, &quot;t2&quot;).start();
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;4.3.3 Condition接口中的await和signal方法实现线程的等待和唤醒&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;Condition中的线程等待和唤醒方法，需要先获取锁&lt;/li&gt;
&lt;li&gt;一定要先await后signal，不要反了&lt;/li&gt;
&lt;/ul&gt;
&lt;blockquote&gt;
&lt;p&gt;Condition接口中的await和signal方法实现线程的等待和唤醒演示&lt;/p&gt;
&lt;/blockquote&gt;
&lt;pre&gt;&lt;code&gt;/**
 * @author Guanghao Wei
 * @create 2023-04-11 12:13
 */
public class LockSupportDemo {

    public static void main(String[] args) {
        Lock lock = new ReentrantLock();
        Condition condition = lock.newCondition();
        /**
         * t1	 -----------come in
         * t2	 -----------发出通知
         * t1	 -----------被唤醒
         */
        new Thread(() -&amp;gt; {
            lock.lock();
            try {
                System.out.println(Thread.currentThread().getName() + &quot;\t -----------come in&quot;);
                condition.await();
                System.out.println(Thread.currentThread().getName() + &quot;\t -----------被唤醒&quot;);
            } catch (InterruptedException e) {
                e.printStackTrace();
            } finally {
                lock.unlock();
            }
        }, &quot;t1&quot;).start();

        try {
            TimeUnit.SECONDS.sleep(1);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }

        new Thread(() -&amp;gt; {
            lock.lock();
            try {
                condition.signal();
                System.out.println(Thread.currentThread().getName() + &quot;\t -----------发出通知&quot;);
            } finally {
                lock.unlock();
            }
        }, &quot;t2&quot;).start();

    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;4.3.4 上述两个对象Object和Condition使用的限制条件&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;线程需要先获得并持有锁，必须在锁块（synchronized或lock）中&lt;/li&gt;
&lt;li&gt;必须要先等待后唤醒，线程才能够被唤醒&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;4.3.5 LockSupport类中的park等待和unpark唤醒&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;是什么&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;ul&gt;
&lt;li&gt;LockSupport 是用于创建锁和其他同步类的基本线程阻塞原语&lt;/li&gt;
&lt;li&gt;LockSupport类使用了一种名为Permit（许可）的概念来做到阻塞和唤醒线程的功能，每个线程都有一个许可（Permit），许可证只能有一个，累加上限是1。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;主要方法&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;ul&gt;
&lt;li&gt;阻塞: Peimit许可证默认没有不能放行，所以一开始调用park()方法当前线程会阻塞，直到别的线程给当前线程发放peimit，park方法才会被唤醒。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;ul&gt;
&lt;li&gt;park/park(Object blocker)-------阻塞当前线程/阻塞传入的具体线程&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;ul&gt;
&lt;li&gt;唤醒: 调用unpack(thread)方法后 就会将thread线程的许可证peimit发放，会自动唤醒park线程，即之前阻塞中的LockSupport.park()方法会立即返回。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;ul&gt;
&lt;li&gt;unpark(Thread thread)------唤醒处于阻塞状态的指定线程&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;blockquote&gt;
&lt;p&gt;LockSupport类中的park等待和unpark唤醒演示&lt;/p&gt;
&lt;/blockquote&gt;
&lt;pre&gt;&lt;code&gt;/**
 * @author Guanghao Wei
 * @create 2023-04-11 12:13
 */
public class LockSupportDemo {

    public static void main(String[] args) {
        /**
         * t1	 -----------come in
         * t2	 ----------发出通知
         * t1	 ----------被唤醒
         */
        Thread t1 = new Thread(() -&amp;gt; {
            System.out.println(Thread.currentThread().getName() + &quot;\t -----------come in&quot;);
            LockSupport.park();
            System.out.println(Thread.currentThread().getName() + &quot;\t ----------被唤醒&quot;);
        }, &quot;t1&quot;);
        t1.start();

        try {
            TimeUnit.SECONDS.sleep(1);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }

        new Thread(() -&amp;gt; {
            LockSupport.unpark(t1);
            System.out.println(Thread.currentThread().getName() + &quot;\t ----------发出通知&quot;);
        }, &quot;t2&quot;).start();

    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;重点说明（重要）&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;ul&gt;
&lt;li&gt;LockSupport是用来创建锁和其他同步类的基本线程阻塞原语，所有的方法都是静态方法，可以让线程再任意位置阻塞，阻塞后也有对应的唤醒方法。归根结底，LockSupport时调用Unsafe中的native代码&lt;/li&gt;
&lt;li&gt;LockSupport提供park()和unpark()方法实现阻塞线程和解除线程阻塞的过程，LockSupport和每个使用它的线程都有一个许可（Peimit）关联，每个线程都有一个相关的permit，peimit最多只有一个，重复调用unpark也不会积累凭证。&lt;/li&gt;
&lt;li&gt;形象理解：线程阻塞需要消耗凭证（Permit），这个凭证最多只有一个&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;ul&gt;
&lt;li&gt;当调用park时，如果有凭证，则会直接消耗掉这个凭证然后正常退出。如果没有凭证，则必须阻塞等待凭证可用；&lt;/li&gt;
&lt;li&gt;当调用unpark时，它会增加一个凭证，但凭证最多只能有1各，累加无效。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;面试题&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;ul&gt;
&lt;li&gt;为什么LockSupport可以突破wait/notify的原有调用顺序？&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;ul&gt;
&lt;li&gt;因为unpark获得了一个凭证，之后再调用park方法，就可以名正言顺的凭证消费，故不会阻塞，先发放了凭证后续可以畅通无阻。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;ul&gt;
&lt;li&gt;为什么唤醒两次后阻塞两次，但最终结果还会阻塞线程？&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;ul&gt;
&lt;li&gt;因为凭证的数量最多为1，连续调用两次unpark和调用一次unpark效果一样，只会增加一个凭证，而调用两次park却需要消费两个凭证，证不够，不能放行。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h1&gt;5. Java内存模型之JMM&lt;/h1&gt;
&lt;h2&gt;5.1 先从大场面试开始&lt;/h2&gt;
&lt;p&gt;●你知道什么是Java内存模型JMM吗？&lt;/p&gt;
&lt;p&gt;●JMM和volatile他们两个之间的关系？&lt;/p&gt;
&lt;p&gt;●JMM没有那些特征或者它的三大特征是什么？&lt;/p&gt;
&lt;p&gt;●为什么要有JMM，它为什么出现？作用和功能是什么？&lt;/p&gt;
&lt;p&gt;●happens-before先行并发原则你有了解过吗？&lt;/p&gt;
&lt;h2&gt;5.2 计算机硬件存储体系&lt;/h2&gt;
&lt;p&gt;打开资源监视器&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://tangtangit.com/_astro/1681191375454-32895bb4-840e-40ad-9edb-4a22325c433b.kMievKiV_ZigOYS.webp&quot; alt=&quot;image.png&quot; /&gt;&lt;/p&gt;
&lt;p&gt;处理器,高速缓存,主内存间的交互关系&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://tangtangit.com/_astro/1681191495527-59268f69-d6af-42a7-b287-96e81572db58.DXzNBY7H_Zft1WP.webp&quot; alt=&quot;image.png&quot; /&gt;&lt;/p&gt;
&lt;p&gt;CPU的运行并不是直接操作内存而是先把内存里面的数据读到缓存，而内存的读和写操作的时候会造成不一致的问题。JVM规范中试图定义一种Java内存模型来屏蔽掉各种硬件和操作系统的内存访问差异，以实现让Java程序再各种平台下都能达到一致性的内存访问效果。&lt;/p&gt;
&lt;h2&gt;5.3 Java内存模型Java Memory Model&lt;/h2&gt;
&lt;p&gt;JMM（Java内存模型Java Memory Model）本身是一种抽象的概念并不真实存在，它仅仅描述的是一组约定或规范，通过这组规范定义了程序中（尤其是多线程）各个变量的读写访问方式并决定一个线程对共享变量的写入以及如何变成对另一个线程可见，关键技术点都是围绕多线程的原子性、可见性和有序性展开的。&lt;/p&gt;
&lt;p&gt;能干吗？&lt;/p&gt;
&lt;p&gt;●通过JMM来实现线程和主内存之间的抽象关系&lt;/p&gt;
&lt;p&gt;●屏蔽各个硬件平台和操作系统的内存访问差异以实现让Java程序再各种平台下都能达到一致性的内存访问效果。&lt;/p&gt;
&lt;h2&gt;5.4 JMM规范下三大特性&lt;/h2&gt;
&lt;p&gt;●可见性：是指当一个线程修改了某一个共享变量的值，其他线程是否能够立即知道该变更，JMM规定了所有的变量都存储在主内存中。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://tangtangit.com/_astro/1681192235882-a4fec799-ea0f-4c6d-8ed6-0a6948fe3d58.DRnaWA8n_Y8DFs.webp&quot; alt=&quot;image.png&quot; /&gt;&lt;/p&gt;
&lt;p&gt;系统中主内存共享变量数据修改被写入的时机是不确定的，多线程并发下很可能出现“脏读”，所以每个线程都有自己的工作内存，线程自己的工作内存中保存了该线程使用到的变量的主内存副本拷贝，线程对变量的所有操作（读取、赋值等）都必须在线程自己的工作内存中进行，而不能够直接写入主内存中的变量，不同线程之间也无法直接访问对方工作内存中的变量，线程间变量值的传递均需要通过主内存来完成。&lt;/p&gt;
&lt;p&gt;线程脏读&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;主内存中有变量X，初始值为0&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;线程A要将X加1，先将X=0拷贝到自己的私有内存中，然后更新X的值&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;线程A将更新后的X值回刷到主内存的时间是不固定的&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;刚好在线程A没有回刷x到主内存时，线程B同样从主内存中读取X，此时为0，和线程A一样的操作，最后期盼的X=2就会变成X=1&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;●原子性：指一个操作是不可被打断的，即多线程环境下，操作不能被其他线程干扰&lt;/p&gt;
&lt;p&gt;●有序性：对于一个线程的执行代码而言，我们总是习惯性地认为代码的执行总是从上到下，有序执行。但为了提升性能，编译器和处理器通常会对指令序列进行重新排序。Java规范规定JVM线程内部维持顺序化语义，即只要程序的最终结果与它顺序话执行的结果相等，那么指令的执行顺序可以与代码顺序不一致，此过程叫指令的重排序。&lt;/p&gt;
&lt;p&gt;○优缺点：&lt;/p&gt;
&lt;p&gt;■JVM能根据处理器特性（CPU多级缓存系统、多核处理器等）适当的对机器指令进行重排序，使机器指令更符合CPU的执行特性，最大限度的发挥机器性能。&lt;/p&gt;
&lt;p&gt;■但是指令重排可以保证串行语义一致，但没有义务保证多线程的语义也一致（即可能产生“脏读”），简单而言就是两行以上不相干的代码在执行的时候有可能先执行的不是第一条，不见得是从上到下顺序执行，执行顺序会被优化。&lt;/p&gt;
&lt;p&gt;○从源码到最终执行示例图：&lt;/p&gt;
&lt;p&gt;■&lt;/p&gt;
&lt;p&gt;指令并行的重排&lt;/p&gt;
&lt;p&gt;内存系统的重排&lt;/p&gt;
&lt;p&gt;编译器优化的重排&lt;/p&gt;
&lt;p&gt;最终执行的指令&lt;/p&gt;
&lt;p&gt;源代码&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://tangtangit.com/_astro/1681193419486-fd5ca92a-06eb-4003-8b12-dc13b6ff4583.CAZDYHxe_DpS1z.webp&quot; alt=&quot;image.png&quot; /&gt;&lt;/p&gt;
&lt;p&gt;■单线程环境里确实能够保证程序最终执行结果和代码顺序执行的结果一致&lt;/p&gt;
&lt;p&gt;■处理器在进行重排序时必须考虑到指令之间的数据依赖性&lt;/p&gt;
&lt;p&gt;■多线程环境中线程交替执行，由于编译器优化重排的存在，可能出现乱序现象，两个线程使用的变量能否保证一致性是无法确定的，结果无法预测。&lt;/p&gt;
&lt;h2&gt;5.5 JMM规范下多线程对变量的读写过程&lt;/h2&gt;
&lt;p&gt;由于JVM运行程序的实体是线程，而每个线程创建时JVM都会为其创建一个工作内存（有的地方成为栈空间），工作内存是每个线程的私有数据区域，而Java内存模型中规定所有变量都存储在主内存，主内存是共享内存区域，所有线程都可以访问，但线程对变量的操作（读写赋值等）必须在工作内存中进行，首先要将变量从主内存拷贝到线程自己的工作内存空间，然后对变量进行操作，操作完成后再将变量写回主内存，不能直接操作主内存中的变量，各个线程中的工作内存存储着主内存中的变量副本拷贝，因此不同的线程无法访问对方的工作内存，线程间的通信（传值）必须通过主内存来完成，其简要访问过程如下图：&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://tangtangit.com/_astro/1681194119198-00117caa-8b14-4855-bb51-f033155006b5.BFqsFkKN_2jUAUV.webp&quot; alt=&quot;image.png&quot; /&gt;&lt;/p&gt;
&lt;p&gt;JMM定义了线程和主内存之间的抽象关系：&lt;/p&gt;
&lt;p&gt;●线程之间的共享变量存储在主内存中（从硬件角度讲就是内存条）&lt;/p&gt;
&lt;p&gt;●每个线程都有一个自己的本地工作内存，本地工作内存中存储了该线程用来读写共享变量的副本（从硬件角度来说就是CPU的缓存）&lt;/p&gt;
&lt;p&gt;小总结：&lt;/p&gt;
&lt;p&gt;●我们定义的所有共享变量都储存在物理主内存中&lt;/p&gt;
&lt;p&gt;●每个线程都有自己独立的工作内存，里面保证该线程使用到的共享变量的副本（主内存中该变量的一份拷贝）&lt;/p&gt;
&lt;p&gt;●线程对共享变量所有的操作都必须先在线程自己的工作内存中进行后写回主内存，不能直接从主内存在读写（不能越级）&lt;/p&gt;
&lt;p&gt;●不同线程之间也无法直接访问其他线程的工作内存中的变量，线程间变量值的传递需要通过主内存来进行（同级不能互相访问）。&lt;/p&gt;
&lt;p&gt;5.6 JMM规范下多线程先行发生原则之happens-before&lt;/p&gt;
&lt;p&gt;在JVM中，如果一个操作执行的结果需要对另一个操作可见或者代码重排序，那么这两个操作之间必须存在happens-before（先行发生）原则，逻辑上的先后关系。&lt;/p&gt;
&lt;h3&gt;5.6.1 x,y案例说明&lt;/h3&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;x=5&lt;/th&gt;
&lt;th&gt;线程A执行&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;y=x&lt;/td&gt;
&lt;td&gt;线程B执行&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;上述称之为：写后读&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;问题？&lt;/p&gt;
&lt;p&gt;y是否等于5呢？ 如果线程A的操作（x=5）happens-before（先行发生）线程B的操作(y=x)，那么可以确定线程B执行y=5一定成立； 如果他们不存在happens-before原则，那么y=5不一定成立  这就是happens-before原则的为例-----------&amp;gt;包含可见性和有序性的约束&lt;/p&gt;
&lt;h3&gt;5.6.2 先行并发原则说明&lt;/h3&gt;
&lt;p&gt;如果Java内存模型中所有的有序性都仅靠volatile和synchronized来完成，那么有很多操作都将变得非常罗嗦，但是我们在编写Java并发代码的时候并没有察觉到这一点。&lt;/p&gt;
&lt;p&gt;我们没有时时、处处、次次，添加volatile和synchronized来完成程序，这是因为Java语言中JMM原则下，有一个“先行发生”（happens-before）的原则限制和规矩，给你理好了规矩！&lt;/p&gt;
&lt;p&gt;这个原则非常重要：它是判断数据是否存在竞争，线程是否安全的非常有用的手段。依赖这个原则，我们可以通过几条简单规则一揽子解决并发环境下两个操作之间是否可能存在冲突的所有问题，而不需要陷入Java内存模型晦涩难懂的底层编译原理之中。&lt;/p&gt;
&lt;h3&gt;5.6.3 happens-before总原则&lt;/h3&gt;
&lt;p&gt;●如果一个操作happens-before另一个操作，那么第一个操作的执行结果将对第二个操作可见，而且第一个操作的执行顺序排在第二个操作之前&lt;/p&gt;
&lt;p&gt;●如果两个操作之间存在happens-before关系，并不意味着一定要按照happens-before原则制定的顺序来执行。如果重排之后的执行结果与按照happens-before关系来执行的结果一致，那么这种重排序并不非法。&lt;/p&gt;
&lt;h3&gt;5.6.4 happens-before之8条&lt;/h3&gt;
&lt;p&gt;从JDK 5开始，Java使用新的JSR-133内存模型，提供了 happens-before 原则来辅助保证程序执行的原子性、可见性以及有序性的问题，它是判断数据是否存在竞争、线程是否安全的依据，happens-before 原则内容如下：&lt;/p&gt;
&lt;p&gt;1次序规则：一个线程内，按照代码的顺序，写在前面的操作先行发生于写在后面的操作，也就是说前一个操作的结果可以被后续的操作获取（保证语义串行性，按照代码顺序执行）。比如前一个操作把变量x赋值为1，那后面一个操作肯定能知道x已经变成了1&lt;/p&gt;
&lt;p&gt;2锁定规则：一个unLock操作先行发生于后面对同一个锁的lock操作（后面指时间上的先后）。&lt;/p&gt;
&lt;p&gt;3volatile变量规则：对一个volatile变量的写操作先行发生于后面对这个变量的读操作，前面的写对后面的读是可见的，这里的后面同样指时间上的先后&lt;/p&gt;
&lt;p&gt;4传递规则：如果操作A先行发生于操作B，而操作B又先行发生于操作C，则可以得出操作A先行发生于操作C。&lt;/p&gt;
&lt;p&gt;5线程启动规则（Thread start Rule）：Thread对象的start()方法先行发生于此线程的每一个动作&lt;/p&gt;
&lt;p&gt;6线程中断规则（Thread Interruption Rule）：&lt;/p&gt;
&lt;p&gt;a对线程interrupt()方法的调用先行发生于被中断线程的代码检测到中断事件的发生&lt;/p&gt;
&lt;p&gt;b可以通过Thread.interrupted()检测到是否发生中断&lt;/p&gt;
&lt;p&gt;c也就是说你要先调用interrupt()方法设置过中断标志位，我才能检测到中断发生&lt;/p&gt;
&lt;p&gt;7线程终止规则（Thread Termination Rule）：线程中的所有操作都优先发生于对此线程的终止检测，我们可以通过isAlive()等手段检测线程是否已经终止执行。&lt;/p&gt;
&lt;p&gt;8对象终结规则（Finalizer Rule）：一个对象的初始化完成（构造函数执行结束）先行发生于它的finalize(）方法的开始-------&amp;gt;对象没有完成初始化之前，是不能调用finalized()方法的&lt;/p&gt;
&lt;h3&gt;5.6.5 happens-before小总结&lt;/h3&gt;
&lt;p&gt;●在Java语言里面，Happens-before的语义本质上是一种可见性&lt;/p&gt;
&lt;p&gt;●A happens-before B ,意味着A发生过的事情对B而言是可见的，无论A事件和B事件是否发生在同一线程里&lt;/p&gt;
&lt;p&gt;●JVM的设计分为两部分：&lt;/p&gt;
&lt;p&gt;○一部分是面向我们程序员提供的，也就是happens-before规则，它通俗易懂的向我们程序员阐述了一个强内存模型，我们只要理解happens-before规则，就可以编写并发安全的程序了&lt;/p&gt;
&lt;p&gt;○另一部分是针对JVM实现的，为了尽可能少的对编译器和处理器做约束从而提升性能，JMM在不影响程序执行结果的前提下对其不做要求，即允许优化重排序，我们只要关注前者就好了，也就是理解happens-before规则即可，其他繁杂的内容由JMM规范结合操作系统给我们搞定，我们只写好代码即可。&lt;/p&gt;
&lt;h3&gt;5.6.6 案例说明&lt;/h3&gt;
&lt;p&gt;初始案例演示&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;private int value =0;
public int getValue(){
    return value;
}
public int setValue(){
    return ++value;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;问题描述：假设存在线程A和B，线程A先（时间上的先后）调用了setValue()方法，然后线程B调用了同一个对象的getValue()方法，那么线程B收到的返回值是什么？&lt;/p&gt;
&lt;p&gt;答案：不一定&lt;/p&gt;
&lt;p&gt;分析happens-before规则（规则5，6，7，8可以忽略，和代码无关）1 由于两个方法由不同线程调用，不满足一个线程的条件，不满足程序次序规则2 两个方法都没有用锁，不满足锁定规则3 变量没有使用volatile修饰，所以不满足volatile变量规则4 传递规则肯定不满足综上：无法通过happens-before原则推导出线程A happens-before 线程B，虽然可以确定时间上线程A优于线程B，但就是无法确定线程B获得的结果是什么，所以这段代码不是线程安全的注意：如果两个操作的执行次序无法从happens-before原则推导出来，那么就不能保证他们的有序性，虚拟机可以随意对他们进行重排序&lt;/p&gt;
&lt;p&gt;如何修复？&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;把getter/setter方法都定义为synchronized方法-------&amp;gt;不好，重量锁，并发性下降&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;private int value =0;
public synchronized int getValue(){
    return value;
}
public synchronized int setValue(){
    return ++value;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;把Value定义为volatile变量，由于setter方法对value的修改不依赖value的原值，满足volatile关键字使用场景&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;/**
* 利用volatile保证读取操作的可见性，
* 利用synchronized保证符合操作的原子性结合使用锁和volatile变量来减少同步的开销
*/
private volatile int value =0;
public int getValue(){
    return value;
}
public synchronized int setValue(){
    return ++value;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h1&gt;6. volatile与JMM&lt;/h1&gt;
&lt;h2&gt;6.1 被volatile修饰的变量有两大特点&lt;/h2&gt;
&lt;p&gt;●特点：&lt;/p&gt;
&lt;p&gt;○可见性&lt;/p&gt;
&lt;p&gt;○有序性：有排序要求，有时需要禁重排&lt;/p&gt;
&lt;p&gt;●内存语义：&lt;/p&gt;
&lt;p&gt;○当写一个volatile变量时，JMM会把该线程对应的本地内存中的共享变量值立即刷新回主内存中&lt;/p&gt;
&lt;p&gt;○当读一个volatile变量时，JMM会把该线程对应的本地内存设置为无效，重新回到主内存中读取最新共享变量的值&lt;/p&gt;
&lt;p&gt;○所以volatile的写内存语义是直接刷新到主内存中，读的内存语义是直接从主内存中读取&lt;/p&gt;
&lt;p&gt;●volatile凭什么可以保证可见性和有序性？&lt;/p&gt;
&lt;p&gt;○内存屏障Memory Barrier&lt;/p&gt;
&lt;h2&gt;6.2 内存屏障（面试重点必须拿下）&lt;/h2&gt;
&lt;h3&gt;6.2.1 生活case&lt;/h3&gt;
&lt;p&gt;●没有管控，顺序难保&lt;/p&gt;
&lt;p&gt;●设定规则，禁止乱序----&amp;gt;上海南京路武警当红灯&lt;/p&gt;
&lt;p&gt;●再说vilatile两大特性：&lt;/p&gt;
&lt;p&gt;○可见：写完后立即刷新回主内存并及时发出通知，大家可以去主内存拿最新版，前面的修改对后面所有线程可见&lt;/p&gt;
&lt;p&gt;○有序性（禁重排）：&lt;/p&gt;
&lt;p&gt;■重排序是指编译器和处理器为了优化程序性能而对指令序列进行重新排序的一种手段，有时候会改变程序语句的先后顺序，若不存在数据依赖关系，可以重排序；存在数据依赖关系，禁止重排序；但重排后的指令绝对不能改变原有的串行语义！这点在并发设计中必须要重点考虑！&lt;/p&gt;
&lt;h3&gt;6.2.2 是什么&lt;/h3&gt;
&lt;p&gt;内存屏障（也称内存栅栏，屏障指令等）是一类同步屏障指令，是CPU或编译器在对内存随机访问的操作中的一个同步点，使得此点之前的所有读写操作都执行后才可以开始执行此点之后的操作，避免代码重排序。内存屏障其实就是一种JVM指令，Java内存模型的重排规则会要求Java编译器在生成JVM指令时插入特定的内存屏障指令，通过这些内存屏障指令，volatile实现了Java内存模型中的可见性和有序性（禁重排），但volatile无法保证原子性&lt;/p&gt;
&lt;p&gt;●内存屏障之前的所有写操作都要回写到主内存&lt;/p&gt;
&lt;p&gt;●内存屏障之后的所有读操作都能获得内存屏障之前的所有写操作的最新结果（实现了可见性）&lt;/p&gt;
&lt;p&gt;写屏障(Store Memory Barrier)：告诉处理器在写屏障之前将所有存储在缓存(store buffers)中的数据同步到主内存，也就是说当看到Store屏障指令，就必须把该指令之前的所有写入指令执行完毕才能继续往下执行 读屏障(Load Memory Barrier)：处理器在读屏障之后的读操作，都在读屏障之后执行。也就是说在Load屏障指令之后就能够保证后面的读取数据指令一定能够读取到最新的数据。&lt;/p&gt;
&lt;p&gt;因此重排序时，不允许把内存屏障之后的指令重排序到内存屏障之前。一句话：对一个volatile变量的写，先行发生于任意后续对这个volatile变量的读，也叫写后读。&lt;/p&gt;
&lt;h3&gt;6.2.3 内存屏障分类&lt;/h3&gt;
&lt;p&gt;粗分两种：&lt;/p&gt;
&lt;p&gt;●读屏障（Load Barrier）：在读指令之前插入读屏障，让工作内存或CPU高速缓存 当中的缓存数据失效，重新回到主内存中获取最新数据。&lt;/p&gt;
&lt;p&gt;●写屏障（Store Barrier）：在写指令之后插入写屏障，强制把缓冲区的数据刷回到主内存中。&lt;/p&gt;
&lt;p&gt;细分四种：&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;屏障类型&lt;/th&gt;
&lt;th&gt;指令示例&lt;/th&gt;
&lt;th&gt;说明&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;LoadLoad&lt;/td&gt;
&lt;td&gt;Load1;LoadLoad;Load2&lt;/td&gt;
&lt;td&gt;保证Load1的读取操作在Load2及后续读取操作之前执行&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;StoreStore&lt;/td&gt;
&lt;td&gt;Store1;StoreStore;Store2&lt;/td&gt;
&lt;td&gt;在store2及其后的写操作执行前，保证Store1的写操作已经刷新到主内存&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;LoadStore&lt;/td&gt;
&lt;td&gt;Load1;LoadStore;Store2&lt;/td&gt;
&lt;td&gt;在Store2及其后的写操作执行前，保证Load1的读操作已经结束&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;StoreLoad&lt;/td&gt;
&lt;td&gt;Store1;StoreLoad;Load2&lt;/td&gt;
&lt;td&gt;保证Store1的写操作已经刷新到主内存后，Load2及其后的读操作才能执行&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;h3&gt;6.2.4 困难内容&lt;/h3&gt;
&lt;p&gt;●什么叫保证有序性？-----&amp;gt;通过内存屏障禁重排&lt;/p&gt;
&lt;p&gt;○重排序有可能影响程序的执行和实现，因此，我们有时候希望告诉JVM别自动重排序，我这里不需要重排序，一切听我的。&lt;/p&gt;
&lt;p&gt;○对于编译器的重排序，JMM会根据重排序的规则，禁止特定类型的编译器重排序&lt;/p&gt;
&lt;p&gt;○对于处理器的重排序，Java编译器在生成指令序列的适当位置，插入内存屏障指令，来禁止特定类型的处理器排序。&lt;/p&gt;
&lt;p&gt;●happens-before之volatile变量规则&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;第一个操作&lt;/th&gt;
&lt;th&gt;第二个操作：普通读写&lt;/th&gt;
&lt;th&gt;第二个操作：volatile读&lt;/th&gt;
&lt;th&gt;第二个操作：volatile写&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;普通读写&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&gt;
&lt;td&gt;volatile读&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&gt;
&lt;td&gt;volatile写&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;/tbody&gt;
&lt;/table&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;当第一个操作为volatile读时，不论第二个操作是什么，都不能重排序，这个操作保证了volatile读之后的操作不会被重排到volatile读之前。&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;当第一个操作为volatile写时，第二个操作为volatile读时，不能重排&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;当第二个操作为volatile写时，不论第一个操作是什么，都不能重排序，这个操作保证了volatile写之前的操作不会被重排到volatile写之后&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;●JMM就将内存屏障插入策略分为4种规则&lt;/p&gt;
&lt;p&gt;○读屏障：在每个volatile读操作的后面插入一个LoadLoad屏障或者LoadStore屏障（两个屏障都会加到读操作的后面，不是或者的关系）&lt;/p&gt;
&lt;p&gt;■&lt;/p&gt;
&lt;p&gt;VOLATILE读插入内存屏障后生成的指令序列示意图&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://tangtangit.com/_astro/1681203343902-7e14ea6b-094f-4f89-838a-a8cd0fc48d94.CgSG8-qr_ZlUcfF.webp&quot; alt=&quot;image.png&quot; /&gt;&lt;/p&gt;
&lt;p&gt;○写屏障：在每个volatile写操作的前面插入StoreStore屏障；在每个volatile写操作的后面插入StoreLoad屏障；&lt;/p&gt;
&lt;p&gt;■&lt;/p&gt;
&lt;p&gt;VOLATILE写插入内存屏障后生成的指令序列示意图&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://tangtangit.com/_astro/1681203485651-b5a20886-997a-43cb-8a30-6f473a59d2f1.ChBxwrAi_19ihoJ.webp&quot; alt=&quot;image.png&quot; /&gt;&lt;/p&gt;
&lt;h3&gt;6.3 volatile特性&lt;/h3&gt;
&lt;h3&gt;6.3.1 保证可见性&lt;/h3&gt;
&lt;p&gt;保证不同线程对某个变量完成操作后结果及时可见，即该共享变量一旦改变所有线程立即可见&lt;/p&gt;
&lt;p&gt;●Code&lt;/p&gt;
&lt;p&gt;○不加volatile，没有可见性，程序无法停止&lt;/p&gt;
&lt;p&gt;○加了volatile，保证可见性，程序可以停止&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;加了volatile保证可见性&lt;/p&gt;
&lt;/blockquote&gt;
&lt;pre&gt;&lt;code&gt;/**
 * @author Guanghao Wei
 * @create 2023-04-11 18:05
 */
public class VolatileSeeDemo {

    /**
     * t1	-------come in
     * main	 修改完成
     * t1	-------flag被设置为false，程序停止
     */
    static volatile boolean flag = true;

    public static void main(String[] args) {
        new Thread(() -&amp;gt; {
            System.out.println(Thread.currentThread().getName() + &quot;\t-------come in&quot;);
            while (flag) {

            }
            System.out.println(Thread.currentThread().getName() + &quot;\t-------flag被设置为false，程序停止&quot;);
        }, &quot;t1&quot;).start();

        try {
            TimeUnit.SECONDS.sleep(2);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }

        //更新flag值
        flag = false;

        System.out.println(Thread.currentThread().getName() + &quot;\t 修改完成&quot;);
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;volatile变量的读写过程（了解即可）; write时必须加锁，同时导致其他线程内存的对应变量失效&lt;/li&gt;
&lt;li&gt;&lt;img src=&quot;https://tangtangit.com/_astro/1681207881381-37c38d15-7c78-486c-9d10-4a4aaa6b4da4.BMlezr07_Z1egKhK.webp&quot; alt=&quot;img&quot; /&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;6.3.2 没有原子性&lt;/h3&gt;
&lt;p&gt;volatile变量的符合操作不具有原子性&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;对于voaltile变量具备可见性，JVM只是保证从主内存加载到线程工作内存的值是最新的，也仅仅是数据加载时是最新的。但是多线程环境下，“数据计算”和“数据赋值”操作可能多次出现，若数据在加载之后，若主内存volatile修饰变量发生修改之后，线程工作内存的操作将会作废去读主内存最新值，操作出现写丢失问题。即各线程私有内存和主内存公共内存中变量不同步，进而导致数据不一致。由此可见volatile解决的是变量读时的可见性问题，但无法保证原子性，对于多线程修改主内存共享变量的场景必须加锁同步。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;至于怎么去理解这个写丢失的问题，就是再将数据读取到本地内存到写回主内存中有三个步骤：数据加载----&amp;gt;数据计算----&amp;gt;数据赋值，如果第二个线程在第一个线程读取旧值与写回新值期间读取共享变量的值，那么第二个线程将会与第一个线程一起看到同一个值，并执行自己的操作，一旦其中一个线程对volatile修饰的变量先行完成操作刷回主内存后，另一个线程会作废自己的操作，然后重新去读取最新的值再进行操作，这样的话，它自身的那一次操作就丢失了，这就造成了 线程安全失败，因此，这个问题需要使用synchronized修饰以保证线程安全性。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;结论：volatile变量不适合参与到依赖当前值的运算，如i++，i=i+1之类的，通常用来保存某个状态的boolean值或者int值，也正是由于volatile变量只能保证可见性，在不符合以下规则的运算场景中，我们仍然要通过加锁来保证原子性：&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;ul&gt;
&lt;li&gt;运算结果并不依赖变量的当前值，或者能够确保只有单一的线程修改变量的值&lt;/li&gt;
&lt;li&gt;变量不需要与其他的状态变量共同参与不变约束&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;面试回答为什么不具备原子性：举例i++的例子，在字节码文件中，i++分为三部，间隙期间不同步非原子操作&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;ul&gt;
&lt;li&gt;对于volatile变量，JVM只是保证从主内存加载到线程工作内存的值是最新的，也就是数据加载时是最新的，如果第二个线程在第一个线程读取旧值和写回新值期间读取i的域值，也就造成了线程安全问题。&lt;/li&gt;
&lt;li&gt;&lt;img src=&quot;https://tangtangit.com/_astro/1681275465892-ca223f7f-6078-409a-95fa-02613915ab71.CK2gBrOo_ZWw0MN.webp&quot; alt=&quot;img&quot; /&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;6.3.3 指令禁重排&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;在每一个volatile写操作前面插入一个StoreStore屏障---&amp;gt;StoreStore屏障可以保证在volatile写之前，其前面所有的普通写操作都已经刷新到主内存中。&lt;/li&gt;
&lt;li&gt;在每一个volatile写操作后面插入一个StoreLoad屏障---&amp;gt;StoreLoad屏障的作用是避免volatile写与后面可能有的volatile读/写操作重排序&lt;/li&gt;
&lt;li&gt;在每一个volatile读操作后面插入一个LoadLoad屏障---&amp;gt;LoadLoad屏障用来禁止处理器把上面的volatile读与下面的普通读重排序&lt;/li&gt;
&lt;li&gt;在每一个volatile读操作后面插入一个LoadStore屏障---&amp;gt;LoadTore屏障用来禁止处理器把上面的volatile读与下面的普通写重排序&lt;/li&gt;
&lt;li&gt;案例说明（volatile读写前或后加了屏障保证有序性）：&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;img src=&quot;https://tangtangit.com/_astro/1681277247911-78195672-ffaf-461c-844b-1b58d529c2ff.Cmm9097S_1ncnl.webp&quot; alt=&quot;img&quot; /&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://tangtangit.com/_astro/1681277282086-36ef04bc-1c91-42a1-acf1-15faa2ef2837.D3sajzh0_Zj07tn.webp&quot; alt=&quot;img&quot; /&gt;&lt;/p&gt;
&lt;h2&gt;6.4 如何正确使用volatile&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;单一赋值可以，但是含复合运算赋值不可以（i++之类的）&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;ul&gt;
&lt;li&gt;volatile int a = 10;&lt;/li&gt;
&lt;li&gt;volatile boolean flag = true;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;状态标志，判断业务是否结束&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;ul&gt;
&lt;li&gt;作为一个布尔状态标志，用于指示发生了一个重要的一次性事件，例如完成初始化或任务结束&lt;/li&gt;
&lt;li&gt;&lt;img src=&quot;https://tangtangit.com/_astro/1681277553116-c8cc74c4-47aa-4398-9262-8dc8b25d917a.M_H0PrD2_r8eEP.webp&quot; alt=&quot;img&quot; /&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;开销较低的读，写锁策略&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;ul&gt;
&lt;li&gt;当读远多于写，结合使用内部锁和volatile变量来减少同步的开销&lt;/li&gt;
&lt;li&gt;原理是：利用volatile保证读操作的可见性，利用synchronized保证符合操作的原子性&lt;/li&gt;
&lt;li&gt;&lt;img src=&quot;https://tangtangit.com/_astro/1681277613302-5adef887-27ed-449c-bd88-0e4f4e6b56bf.C24G7zif_UKUSu.webp&quot; alt=&quot;img&quot; /&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;DCL双端锁的发布&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;ul&gt;
&lt;li&gt;问题描述：首先设定一个加锁的单例模式场景&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;ul&gt;
&lt;li&gt;&lt;img src=&quot;https://tangtangit.com/_astro/1681277769628-287b69ba-cc2f-4a20-b193-d255570d5dfa.CpybTBCC_1wVMAR.webp&quot; alt=&quot;img&quot; /&gt;&lt;/li&gt;
&lt;li&gt;在单线程环境下（或者说正常情况下），在“问题代码处”，会执行以下操作，保证能获取到已完成初始化的实例：&lt;/li&gt;
&lt;li&gt;&lt;img src=&quot;https://tangtangit.com/_astro/1681277858260-07127c5a-8bb5-4af3-a253-149085d4b849.BB_783ej_3M6Qo.webp&quot; alt=&quot;img&quot; /&gt;&lt;/li&gt;
&lt;li&gt;隐患：在多线程环境下，在“问题代码处”，会执行以下操作，由于重排序导致2，3乱序，后果就是其他线程得到的是null而不是完成初始化的对象，其中第3步中实例化分多步执行（分配内存空间、初始化对象、将对象指向分配的内存空间），某些编译器为了性能原因，会将第二步和第三步重排序，这样某个线程肯能会获得一个未完全初始化的实例：&lt;/li&gt;
&lt;li&gt;&lt;img src=&quot;https://tangtangit.com/_astro/1681277949781-4b436fdd-4cac-43b9-9089-0dfa91f4d812.ChI4Z3Fv_Z15QrHh.webp&quot; alt=&quot;img&quot; /&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;ul&gt;
&lt;li&gt;多线程下的解决方案：加volatile修饰&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;ul&gt;
&lt;li&gt;&lt;img src=&quot;https://tangtangit.com/_astro/1681278158734-fe21f869-71b1-4b80-a457-011b7b1832b0.D84ZGY1B_17CLmh.webp&quot; alt=&quot;img&quot; /&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;6.5  本章最后的小总结&lt;/h2&gt;
&lt;h3&gt;6.5.1 volatile可见性&lt;/h3&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;volatile关键字保证可见性：&lt;/th&gt;
&lt;th&gt;对一个被volatile关键字修饰的变量&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;td&gt;写操作的话，这个变量的最新值会立即刷新回到主内存中&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;2&lt;/td&gt;
&lt;td&gt;读操作的话，总是能够读取到这个变量的最新值，也就是这个变量最后被修改的值&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;3&lt;/td&gt;
&lt;td&gt;当某个线程收到通知，去读取volatile修饰的变量的值的时候，线程私有工作内存的数据失效，需要重新回到主内存中去读取最新的数据。&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;h3&gt;6.5.2 volatile没有原子性&lt;/h3&gt;
&lt;h3&gt;6.5.3 volatile禁重排&lt;/h3&gt;
&lt;p&gt;&lt;img src=&quot;https://tangtangit.com/_astro/1681278663255-cf8b4fec-7ef4-4fc5-a9b4-5e517b7a9a6e.BVFSl6OK_1qj11W.webp&quot; alt=&quot;img&quot; /&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://tangtangit.com/_astro/1681278816338-75242ddd-9ac6-412d-9437-b5b62b147311.ChyqC_ha_dBUkj.webp&quot; alt=&quot;img&quot; /&gt;&lt;/p&gt;
&lt;h3&gt;6.5.4 凭什么我们Java写了一个volatile关键字，系统底层加入内存屏障？两者的关系如何勾搭？&lt;/h3&gt;
&lt;p&gt;&lt;img src=&quot;https://tangtangit.com/_astro/1681278867074-b0a21e10-d868-4dd1-a7d2-536d2b2010a0.DS_U88SD_Z2sHRtp.webp&quot; alt=&quot;img&quot; /&gt;&lt;/p&gt;
&lt;h3&gt;6.5.5 内存屏障是什么？&lt;/h3&gt;
&lt;p&gt;是一种屏障指令，它使得CPU或编译器对屏障指令的前和后所发出的内存操作执行一个排序的约束。也称为内存栅栏或栅栏指令。&lt;/p&gt;
&lt;h3&gt;6.5.6 内存屏障能干吗？&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;阻止屏障两边的指令重排序&lt;/li&gt;
&lt;li&gt;写操作时加入屏障，强制将线程私有工作内存的数据刷回主物理内存&lt;/li&gt;
&lt;li&gt;读操作时加入屏障，线程私有工作内存的数据失效，重新回到主物理内存中获取最新值&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;6.5.7 内存屏障四大指令&lt;/h3&gt;
&lt;p&gt;&lt;img src=&quot;https://tangtangit.com/_astro/1681279106353-e2d0f17f-32e0-41d3-b1af-4eeaa4016f6b.D3Sg1Fcf_1iVPhN.webp&quot; alt=&quot;img&quot; /&gt;&lt;/p&gt;
&lt;h3&gt;6.5.8 3句话总结&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;volatile写之前的操作，都禁止重排序到volatile之后&lt;/li&gt;
&lt;li&gt;volatile读之后的操作，都禁止重排序到volatile之前&lt;/li&gt;
&lt;li&gt;volatile写之后volatile读，禁止重排序&lt;/li&gt;
&lt;/ul&gt;
&lt;h1&gt;7. CAS&lt;/h1&gt;
&lt;h2&gt;7.1 原子类&lt;/h2&gt;
&lt;p&gt;Java.util.concurrent.atomic&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://tangtangit.com/_astro/1681279535601-bdff9fb4-717b-4ae5-9ff0-bc2264473a2a.Dof3IXhY_Z1YElTU.webp&quot; alt=&quot;image.png&quot; /&gt;&lt;/p&gt;
&lt;h2&gt;7.2 没有CAS之前&lt;/h2&gt;
&lt;p&gt;多线程环境中不使用原子类保证线程安全i++（基本数据类型）&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;class Test {
        private volatile int count = 0;
        //若要线程安全执行执行count++，需要加锁
        public synchronized void increment() {
                  count++;
        }

        public int getCount() {
                  return count;
        }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;7.3 使用CAS之后&lt;/h2&gt;
&lt;p&gt;多线程环境中使用原子类保证线程安全i++（基本数据类型）----------&amp;gt;类似于乐观锁&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;class Test2 {
        private AtomicInteger count = new AtomicInteger();

        public void increment() {
                  count.incrementAndGet();
        }
      //使用AtomicInteger之后，不需要加锁，也可以实现线程安全。
       public int getCount() {
                return count.get();
        }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;7.4 是什么？&lt;/h2&gt;
&lt;p&gt;CAS(compare and swap)，中文翻译为比较并交换，实现并发算法时常用到的一种技术，用于保证共享变量的原子性更新，它包含三个操作数---内存位置、预期原值与更新值。&lt;/p&gt;
&lt;p&gt;执行CAS操作的时候，将内存位置的值与预期原值进行比较：&lt;/p&gt;
&lt;p&gt;●如果相匹配，那么处理器会自动将该位置更新为新值&lt;/p&gt;
&lt;p&gt;●如果不匹配，处理器不做任何操作，多个线程同时执行CAS操作只有一个会成功。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://tangtangit.com/_astro/1681279948065-f7d04f1d-7cd7-43b3-8786-693a1d016f84.SyW90xBt_2bvLnb.webp&quot; alt=&quot;image.png&quot; /&gt;&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;CASDemo演示&lt;/p&gt;
&lt;/blockquote&gt;
&lt;pre&gt;&lt;code&gt;/**
 * @author Guanghao Wei
 * @create 2023-04-12 14:15
 */
public class CASDemo {
    public static void main(String[] args) {
        AtomicInteger atomicInteger = new AtomicInteger(5);
        System.out.println(atomicInteger.compareAndSet(5, 2022) + &quot;\t&quot; + atomicInteger.get());//true	2022
        System.out.println(atomicInteger.compareAndSet(5, 2022) + &quot;\t&quot; + atomicInteger.get());//false	2022
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;compareAndSet 源码&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://tangtangit.com/_astro/1681280524693-7e39b744-3963-48ae-bed3-d9b2c2cce3f6.BoCA7kwC_Z2ozgyN.webp&quot; alt=&quot;image.png&quot; /&gt;&lt;/p&gt;
&lt;h2&gt;7.5 CAS底层原理？谈谈对Unsafe类的理解？&lt;/h2&gt;
&lt;h3&gt;7.5.1 Unsafe&lt;/h3&gt;
&lt;p&gt;Unsafe类是CAS的核心类，由于Java方法无法直接访问底层系统，需要通过本地（native）方法来访问，Unsafe相当于一个后门，基于该类可以直接操作特定内存的数据。Unsafe类存在于sun.misc包中，其内部方法操作可以像C的指针一样直接操作内存，因此Java中CAS操作的执行依赖于Unsafe类的方法。&lt;/p&gt;
&lt;p&gt;注意：Unsafe类中的所有方法都是native修饰的，也就是说Unsafe类中的所有方法都直接调用操作系统底层资源执行相应任务。&lt;/p&gt;
&lt;p&gt;问题：我们知道i++是线程不安全的，那AtomicInteger.getAndIncrement()如何保证原子性？&lt;/p&gt;
&lt;p&gt;AtomicInteger类主要利用CAS+volatile和native方法来保证原子操作，从而避免synchronized的高开销，执行效率大为提升：&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://tangtangit.com/_astro/1681281445135-332ae9b5-ae46-4021-a7ea-237b1f37b927.CVxNG9j9_Z23fPfz.webp&quot; alt=&quot;image.png&quot; /&gt;&lt;/p&gt;
&lt;p&gt;CAS并发原语体现在Java语言中就是sun.misc.Unsafe类中的各个方法。调用Unsafe类中的CAS方法，JVM会帮我们实现出CAS汇编指令。这是一种完全依赖于硬件的功能，通过它实现了原子操作。再次强调，由于CAS是一种系统原语，原语属于操作系统用语范畴，是由若干条指令组成的，用于完成某个功能的一个过程，并且原语的执行必须是连续的，在执行过程中不允许被中断，也就是说CAS是一条CPU的原子指令，不会造成所谓的数据不一致问题。&lt;/p&gt;
&lt;h3&gt;7.5.2 源码分析&lt;/h3&gt;
&lt;p&gt;假设线程A和线程B两个线程同时执行GETANDADDLNT操作(分别跑在不同CPU上):&lt;/p&gt;
&lt;p&gt;1 ATOMICINTEGER里面的VALUE原始值为3.即主内存中ATOMICINTEGER的VALUE为3,根据A初线程已各自持有二&lt;/p&gt;
&lt;p&gt;份值为3的VALUE的副本分别到各自的工作内存.&lt;/p&gt;
&lt;p&gt;2 线程A通过GETLNTVOLATILE(VAR1,VAR2)拿到VALUE值3,这时线程A被挂起.&lt;/p&gt;
&lt;p&gt;3 线程B也通过GETGETIVOLATIE(VARL.VARL.VALUE但3,此时刚好线图已预有校捷起型理同行COMPAREANDSWAPLNT方法&lt;/p&gt;
&lt;p&gt;比较内存值也为3,成功修改内存值为4,线程B打完收工,一切OK.&lt;/p&gt;
&lt;p&gt;4这时线程A恢复,执行COMPAREANDSWAPINT方法比较,发现自己手里的值数字3和主内存的值数字4不一致,说明该值已&lt;/p&gt;
&lt;p&gt;经被其它线程抢先一步修改过了,那A线程本次修改失败,只能重新读取重新来一遍了.&lt;/p&gt;
&lt;p&gt;5线程A重新获取VALUE值,因为变量VALATHE修饰,所以其它线程对它的修改,线程A总是能够看到,线程A继续执&lt;/p&gt;
&lt;p&gt;行COMPAREANDSWAPLNT进行比较替换,直到成功.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://tangtangit.com/_astro/1681281868126-e9883f13-1aa9-4a8b-aca0-31d9544ff4b6.DIJ5I6dp_1d2Wt5.webp&quot; alt=&quot;image.png&quot; /&gt;&lt;/p&gt;
&lt;h3&gt;7.5.3 底层汇编&lt;/h3&gt;
&lt;p&gt;&lt;img src=&quot;https://tangtangit.com/_astro/1681281979716-e2fc797b-c6eb-4f5c-bdc2-fb9832effced.BFFyv72u_ZOPlyG.webp&quot; alt=&quot;image.png&quot; /&gt;&lt;/p&gt;
&lt;p&gt;JDK提供的CAS机制，在汇编层级会禁止变量两侧的指令优化，然后使用compxchg指令比较并更新变量值（原子性）&lt;/p&gt;
&lt;p&gt;总结：&lt;/p&gt;
&lt;p&gt;●CAS是靠硬件实现的从而在硬件层面提升效率，最底层还是交给硬件来保证原子性和可见性&lt;/p&gt;
&lt;p&gt;●实现方式是基于硬件平台的汇编指令，在inter的CPU中，使用的是汇编指令compxchg指令&lt;/p&gt;
&lt;p&gt;●核心思想就是比较要更新变量V的值和预期值E，相等才会将V的值设为新值N，如果不相等自旋再来&lt;/p&gt;
&lt;h2&gt;7.6 原子引用&lt;/h2&gt;
&lt;p&gt;&lt;img src=&quot;https://tangtangit.com/_astro/1681282437108-0d23bc7e-e378-4a02-b0fd-d2be94961cff.QcyVpkFm_Z1foAD.webp&quot; alt=&quot;image.png&quot; /&gt;&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;原子引用演示&lt;/p&gt;
&lt;/blockquote&gt;
&lt;pre&gt;&lt;code&gt;/**
 * @author Guanghao Wei
 * @create 2023-04-12 14:51
 */
@Data
@AllArgsConstructor
@NoArgsConstructor
class User {
    String userName;
    int age;
}

public class AtomicReferenceDemo {
    public static void main(String[] args) {
        AtomicReference&amp;lt;User&amp;gt; atomicReference = new AtomicReference&amp;lt;&amp;gt;();
        User z3 = new User(&quot;z3&quot;, 22);
        User li4 = new User(&quot;li4&quot;, 25);

        atomicReference.set(z3);
        System.out.println(atomicReference.compareAndSet(z3, li4) + &quot;\t&quot; + atomicReference.get().toString());//true	User(userName=li4, age=25)
        System.out.println(atomicReference.compareAndSet(z3, li4) + &quot;\t&quot; + atomicReference.get().toString());//false	User(userName=li4, age=25)

    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;7.7 CAS与自旋锁，借鉴CAS思想&lt;/h2&gt;
&lt;h3&gt;7.7.1 是什么？&lt;/h3&gt;
&lt;p&gt;CAS是实现自旋锁的基础，CAS利用CPU指令保证了操作的原子性，以达到锁的效果，至于自旋锁---字面意思自己旋转。是指尝试获取锁的线程不会立即阻塞，而是采用循环的方式去尝试获取锁，当线程发现锁被占用时，会不断循环判断锁的状态，直到获取。这样的好处是减少线程上下文切换的消耗，缺点是循环会消耗CPU。&lt;/p&gt;
&lt;h3&gt;7.7.2 自己实现一个自旋锁spinLockDemo&lt;/h3&gt;
&lt;p&gt;题目：实现一个自旋锁，借鉴CAS思想&lt;/p&gt;
&lt;p&gt;通过CAS完成自旋锁，A线程先进来调用myLock方法自己持有锁5秒钟，B随后进来后发现当前有线程持有锁，所以只能通过自旋等待，直到A释放锁后B随后抢到。&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;借鉴CAS思想实现自旋锁&lt;/p&gt;
&lt;/blockquote&gt;
&lt;pre&gt;&lt;code&gt;/**
 * @author Guanghao Wei
 * @create 2023-04-12 15:00
 */
public class SpinLockDemo {

    AtomicReference&amp;lt;Thread&amp;gt; atomicReference = new AtomicReference&amp;lt;&amp;gt;();

    public void lock() {
        Thread thread = Thread.currentThread();
        System.out.println(Thread.currentThread().getName() + &quot;\t --------come in&quot;);
        while (!atomicReference.compareAndSet(null, thread)) {

        }
    }

    public void unLock() {
        Thread thread = Thread.currentThread();
        atomicReference.compareAndSet(thread, null);
        System.out.println(Thread.currentThread().getName() + &quot;\t --------task over,unLock.........&quot;);
    }

    public static void main(String[] args) {
        SpinLockDemo spinLockDemo = new SpinLockDemo();
        new Thread(() -&amp;gt; {
            spinLockDemo.lock();
            try {
                TimeUnit.SECONDS.sleep(5);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            spinLockDemo.unLock();
        }, &quot;A&quot;).start();


        try {
            TimeUnit.MILLISECONDS.sleep(500);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }

        new Thread(() -&amp;gt; {
            spinLockDemo.lock();
            spinLockDemo.unLock();
        }, &quot;B&quot;).start();
    }
}
/**
 * A	 --------come in
 * B	 --------come in
 * A	 --------task over,unLock.........
 * B	 --------task over,unLock.........
 */
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;7.8 CAS缺点&lt;/h2&gt;
&lt;h3&gt;7.8.1 循环时间长开销很大&lt;/h3&gt;
&lt;p&gt;FINAL INT GETANDADDINT(OBJECT VAR1, LONG VAR2, INT VAR4)&lt;/p&gt;
&lt;p&gt;PUBLIC&lt;/p&gt;
&lt;p&gt;INT VAR5;&lt;/p&gt;
&lt;p&gt;DO&lt;/p&gt;
&lt;p&gt;VAR5 - THIS.GETINTVOLATILE(VAR1, VAR2);&lt;/p&gt;
&lt;p&gt;} WHILE(!THIS.COMPAREANDSWAPINT(VAR1, VAR2, VAR5,&lt;/p&gt;
&lt;p&gt;VAR5:VAR5 + VAR4));&lt;/p&gt;
&lt;p&gt;RETURN VAR5;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://tangtangit.com/_astro/1681283435669-2927eebd-5d97-431f-9e7f-a5b970d54f76.C1zgR9kb_vGXx4.webp&quot; alt=&quot;image.png&quot; /&gt;&lt;/p&gt;
&lt;p&gt;●getAndAddInt方法有一个do while&lt;/p&gt;
&lt;p&gt;●如果CAS失败，会一直进行尝试，如果CAS长时间一直不成功，可能会给CPU带来很大开销&lt;/p&gt;
&lt;h3&gt;7.8.2 引出来ABA问题？&lt;/h3&gt;
&lt;p&gt;●ABA问题怎么产生的？&lt;/p&gt;
&lt;p&gt;○CAS算法实现一个重要前提需要提取出内存中某时刻的数据并在当下时刻比较并替换，那么在这个时间差类会导致数据的变化。&lt;/p&gt;
&lt;p&gt;○比如说一个线程1从内存位置V中取出A，这时候另一个线程2也从内存中取出A，并且线程2进行了一些操作将值变成了B，然后线程2又将V位置的数据变成A，这时候线程1进行CAS操作发现内存中仍然是A，预期ok，然后线程1操作成功--------尽管线程1的CAS操作成功，但是不代表这个过程就是没有问题的。&lt;/p&gt;
&lt;p&gt;●版本号时间戳原子引用&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://tangtangit.com/_astro/1681284076874-d8cab49f-baef-49cb-8b6a-5aaa051aa4d2.TP4zfRO5_jPsuo.webp&quot; alt=&quot;image.png&quot; /&gt;&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;AtomicStampedReference带戳记流水的简单演示（单线程）&lt;/p&gt;
&lt;/blockquote&gt;
&lt;pre&gt;&lt;code&gt;/**
 * @author Guanghao Wei
 * @create 2023-04-12 15:21
 */
@Data
@AllArgsConstructor
@NoArgsConstructor
class Book {
    private int id;
    private String bookName;
}

public class AtomicStampedReferenceDemo {
    public static void main(String[] args) {
        Book javaBook = new Book(1, &quot;javaBook&quot;);
        AtomicStampedReference&amp;lt;Book&amp;gt; atomicStampedReference = new AtomicStampedReference&amp;lt;&amp;gt;(javaBook, 1);
        System.out.println(atomicStampedReference.getReference() + &quot;\t&quot; + atomicStampedReference.getStamp());

        Book mysqlBook = new Book(2, &quot;mysqlBook&quot;);
        boolean b;
        b = atomicStampedReference.compareAndSet(javaBook, mysqlBook, atomicStampedReference.getStamp(), atomicStampedReference.getStamp() + 1);
        System.out.println(b + &quot;\t&quot; + atomicStampedReference.getReference() + &quot;\t&quot; + atomicStampedReference.getStamp());

        b = atomicStampedReference.compareAndSet(mysqlBook, javaBook, atomicStampedReference.getStamp(), atomicStampedReference.getStamp() + 1);
        System.out.println(b + &quot;\t&quot; + atomicStampedReference.getReference() + &quot;\t&quot; + atomicStampedReference.getStamp());
    }
}
/**
 * Book(id=1, bookName=javaBook)	1
 * true	Book(id=2, bookName=mysqlBook)	2
 * true	Book(id=1, bookName=javaBook)	3
 */
&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote&gt;
&lt;p&gt;多线程情况下演示AtomicStampedReference解决ABA问题&lt;/p&gt;
&lt;/blockquote&gt;
&lt;pre&gt;&lt;code&gt;/**
 * @author Guanghao Wei
 * @create 2023-04-12 15:28
 */
public class ABADemo {
    static AtomicInteger atomicInteger = new AtomicInteger(100);
    static AtomicStampedReference&amp;lt;Integer&amp;gt; atomicStampedReference = new AtomicStampedReference&amp;lt;&amp;gt;(100, 1);

    public static void main(String[] args) {
//        abaHappen();//true	2023
        /**
         * t3	首次版本号: 1
         * t4	首次版本号: 1
         * t3	2次版本号: 2
         * t3	3次版本号: 3
         * false	100	3
         */
        abaNoHappen();

    }

    private static void abaNoHappen() {
        new Thread(() -&amp;gt; {
            int stamp = atomicStampedReference.getStamp();
            System.out.println(Thread.currentThread().getName() + &quot;\t&quot; + &quot;首次版本号: &quot; + stamp);
            try {
                TimeUnit.MILLISECONDS.sleep(500);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            atomicStampedReference.compareAndSet(100, 101, atomicStampedReference.getStamp(), atomicStampedReference.getStamp() + 1);
            System.out.println(Thread.currentThread().getName() + &quot;\t&quot; + &quot;2次版本号: &quot; + atomicStampedReference.getStamp());
            atomicStampedReference.compareAndSet(101, 100, atomicStampedReference.getStamp(), atomicStampedReference.getStamp() + 1);
            System.out.println(Thread.currentThread().getName() + &quot;\t&quot; + &quot;3次版本号: &quot; + atomicStampedReference.getStamp());
        }, &quot;t3&quot;).start();


        new Thread(() -&amp;gt; {
            int stamp = atomicStampedReference.getStamp();
            System.out.println(Thread.currentThread().getName() + &quot;\t&quot; + &quot;首次版本号: &quot; + stamp);
            try {
                TimeUnit.SECONDS.sleep(1);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            boolean b = atomicStampedReference.compareAndSet(100, 200, stamp, stamp + 1);
            System.out.println(b + &quot;\t&quot; + atomicStampedReference.getReference() + &quot;\t&quot; + atomicStampedReference.getStamp());
        }, &quot;t4&quot;).start();
    }

    private static void abaHappen() {
        new Thread(() -&amp;gt; {
            atomicInteger.compareAndSet(100, 101);
            try {
                TimeUnit.MILLISECONDS.sleep(10);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            atomicInteger.compareAndSet(101, 100);
        }, &quot;t1&quot;).start();


        new Thread(() -&amp;gt; {
            try {
                TimeUnit.MILLISECONDS.sleep(200);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println(atomicInteger.compareAndSet(100, 2023) + &quot;\t&quot; + atomicInteger.get());//true	2023
        }, &quot;t2&quot;).start();
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;一句话：比较加版本号一起上&lt;/li&gt;
&lt;/ul&gt;
&lt;h1&gt;8. 原子操作类&lt;/h1&gt;
&lt;p&gt;Atomic 翻译成中文是原子的意思。在化学上，我们知道原子是构成一般物质的最小单位，在化学反应中是不可分割的。在我们这里 Atomic 是指一个操作是不可中断的。即使是在多个线程一起执行的时候，一个操作一旦开始，就不会被其他线程干扰。&lt;/p&gt;
&lt;h2&gt;8.1 基本类型原子类&lt;/h2&gt;
&lt;p&gt;●AtomicInteger：整型原子类&lt;/p&gt;
&lt;p&gt;●AtomicBoolean：布尔型原子类&lt;/p&gt;
&lt;p&gt;●AtomicLong：长整型原子类&lt;/p&gt;
&lt;h3&gt;8.1.1 常用API简介&lt;/h3&gt;
&lt;blockquote&gt;
&lt;p&gt;基本类型原子类常用API简介&lt;/p&gt;
&lt;/blockquote&gt;
&lt;pre&gt;&lt;code&gt;public final int get() //获取当前的值
public final int getAndSet(int newValue)//获取当前的值，并设置新的值
public final int getAndIncrement()//获取当前的值，并自增
public final int getAndDecrement() //获取当前的值，并自减
public final int getAndAdd(int delta) //获取当前的值，并加上预期的值
boolean compareAndSet(int expect, int update) //如果输入的数值等于预期值，则以原子方式将该值设置为输入值（update）
public final void lazySet(int newValue)//最终设置为newValue,使用 lazySet 设置之后可能导致其他线程在之后的一小段时间内还是可以读到旧的值。
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;8.1.2 Case&lt;/h3&gt;
&lt;blockquote&gt;
&lt;p&gt;AtomicInteger案例演示&lt;/p&gt;
&lt;/blockquote&gt;
&lt;pre&gt;&lt;code&gt;/**
 * @author Guanghao Wei
 * @create 2023-04-12 16:34
 */
class MyNumber {
    AtomicInteger atomicInteger = new AtomicInteger();

    public void addPlusPlus() {
        atomicInteger.getAndIncrement();
    }

}

public class AtomicIntegerDemo {

    public static final int SIZE = 50;

    public static void main(String[] args) throws InterruptedException {
        MyNumber myNumber = new MyNumber();
        CountDownLatch countDownLatch = new CountDownLatch(SIZE);
        for (int i = 1; i &amp;lt;= SIZE; i++) {
            new Thread(() -&amp;gt; {
                try {
                    for (int j = 1; j &amp;lt;= 10; j++) {
                        myNumber.addPlusPlus();
                    }
                } finally {
                    countDownLatch.countDown();
                }
            }, String.valueOf(i)).start();

        }
        countDownLatch.await();

        System.out.println(Thread.currentThread().getName() + &quot;\t&quot; + &quot;result: &quot; + myNumber.atomicInteger.get());//main	result: 500
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;8.2 数组类型原子类&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;AtomicIntegerArray&lt;/code&gt;：整型数组原子类&lt;/li&gt;
&lt;li&gt;&lt;code&gt;AtomicLongrArray&lt;/code&gt;：长整型数组原子类&lt;/li&gt;
&lt;li&gt;&lt;code&gt;AtomicReferenceArray&lt;/code&gt;：用类型数组原子类&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;8.2.1 常用API简介&lt;/h3&gt;
&lt;blockquote&gt;
&lt;p&gt;数组类型原子类常用API简介&lt;/p&gt;
&lt;/blockquote&gt;
&lt;pre&gt;&lt;code&gt;public final int get(int i) //获取 index=i 位置元素的值
public final int getAndSet(int i, int newValue)//返回 index=i 位置的当前的值，并将其设置为新值：newValue
public final int getAndIncrement(int i)//获取 index=i 位置元素的值，并让该位置的元素自增
public final int getAndDecrement(int i) //获取 index=i 位置元素的值，并让该位置的元素自减
public final int getAndAdd(int i, int delta) //获取 index=i 位置元素的值，并加上预期的值
boolean compareAndSet(int i, int expect, int update) //如果输入的数值等于预期值，则以原子方式将 index=i 位置的元素值设置为输入值（update）
public final void lazySet(int i, int newValue)//最终 将index=i 位置的元素设置为newValue,使用 lazySet 设置之后可能导致其他线程在之后的一小段时间内还是可以读到旧的值。
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;8.2.2 Case&lt;/h3&gt;
&lt;blockquote&gt;
&lt;p&gt;AtomicIntegerArray案例演示&lt;/p&gt;
&lt;/blockquote&gt;
&lt;pre&gt;&lt;code&gt;/**
 * @author Guanghao Wei
 * @create 2023-04-12 16:49
 */
public class AtomicIntegerArrayDemo {
    public static void main(String[] args) {
//        AtomicIntegerArray atomicIntegerArray = new AtomicIntegerArray(new int[]{1, 2, 3, 4, 5});
        AtomicIntegerArray atomicIntegerArray = new AtomicIntegerArray(new int[5]);
        for (int i = 0; i &amp;lt; atomicIntegerArray.length(); i++) {
            System.out.println(atomicIntegerArray.get(i));
        }
        System.out.println();
        int tempInt = 0;
        tempInt = atomicIntegerArray.getAndSet(0, 1122);
        System.out.println(tempInt + &quot;\t&quot; + atomicIntegerArray.get(0));
        tempInt = atomicIntegerArray.getAndIncrement(0);
        System.out.println(tempInt + &quot;\t&quot; + atomicIntegerArray.get(0));
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;8.3 引用类型原子类&lt;/h2&gt;
&lt;p&gt;●AtomicReference :引用类型原子类&lt;/p&gt;
&lt;p&gt;●AtomicStampedReference：原子更新带有版本号的引用类型。该类将整数值与引用关联起来，可用于解决原子的更新数据和数据的版本号，可以解决使用 CAS 进行原子更新时可能出现的 ABA 问题。&lt;/p&gt;
&lt;p&gt;○解决修改过几次&lt;/p&gt;
&lt;p&gt;●AtomicMarkableReference：原子更新带有标记的引用类型。该类将 boolean 标记与引用关联起来&lt;/p&gt;
&lt;p&gt;○解决是否修改过，它的定义就是将标记戳简化为true/false，类似于一次性筷子&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;AtomicMarkableReference案例演示&lt;/p&gt;
&lt;/blockquote&gt;
&lt;pre&gt;&lt;code&gt;/**
 * @author Guanghao Wei
 * @create 2023-04-12 16:55
 */
public class AtomicMarkableReferenceDemo {
    static AtomicMarkableReference markableReference = new AtomicMarkableReference(100, false);

    public static void main(String[] args) {
        new Thread(() -&amp;gt; {
            boolean marked = markableReference.isMarked();
            System.out.println(Thread.currentThread().getName() + &quot;\t&quot; + &quot;默认标识: &quot; + marked);//t1	默认标识: false
            try {
                TimeUnit.SECONDS.sleep(1);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            markableReference.compareAndSet(100, 1000, marked, !marked);//t2	默认标识: false

        }, &quot;t1&quot;).start();

        new Thread(() -&amp;gt; {
            boolean marked = markableReference.isMarked();
            System.out.println(Thread.currentThread().getName() + &quot;\t&quot; + &quot;默认标识: &quot; + marked);//t2	t2线程CASResult：false
            try {
                TimeUnit.SECONDS.sleep(2);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            boolean b = markableReference.compareAndSet(100, 2000, marked, !marked);
            System.out.println(Thread.currentThread().getName() + &quot;\t&quot; + &quot;t2线程CASResult：&quot; + b);
            System.out.println(Thread.currentThread().getName() + &quot;\t&quot; + markableReference.isMarked());//t2	true
            System.out.println(Thread.currentThread().getName() + &quot;\t&quot; + markableReference.getReference());//t2	1000

        }, &quot;t2&quot;).start();
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;8.4 对象的属性修改原子类&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;AtomicIntegerFieldUpdater&lt;/code&gt;：原子更新对象中int类型字段的值&lt;/li&gt;
&lt;li&gt;&lt;code&gt;AtomicLongFieldUpdater&lt;/code&gt;：原子更新对象中Long类型字段的值&lt;/li&gt;
&lt;li&gt;&lt;code&gt;AtomicReferenceFieldUpdater&lt;/code&gt;：原子更新对象中引用类型字段的值&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;8.4.1 使用目的&lt;/h3&gt;
&lt;p&gt;以一种线程安全的方式操作非线程安全对象内的某些字段&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://tangtangit.com/_astro/1681290583632-754e9071-1371-46a1-a975-d4484e2f3455.D4Gczyuv_16RLIH.webp&quot; alt=&quot;image.png&quot; /&gt;&lt;/p&gt;
&lt;h3&gt;8.4.2 使用要求&lt;/h3&gt;
&lt;p&gt;●更新的对象属性必须使用public volatile修饰符&lt;/p&gt;
&lt;p&gt;●因为对象的属性修改类型原子类都是抽象类，所以每次使用都必须使用静态方法newUpdater()创建一个更新器，并且需要设置想要更新的类和属性&lt;/p&gt;
&lt;h3&gt;8.4.3 Case&lt;/h3&gt;
&lt;blockquote&gt;
&lt;p&gt;AtomicIntegerFieldUpdater使用案例&lt;/p&gt;
&lt;/blockquote&gt;
&lt;pre&gt;&lt;code&gt;/**
 * @author Guanghao Wei
 * @create 2023-04-12 17:13
 * 需求：10个线程各自转账1000
 */
class BankAccount {
    public volatile int money = 0;


    AtomicIntegerFieldUpdater&amp;lt;BankAccount&amp;gt; atomicIntegerFieldUpdater = AtomicIntegerFieldUpdater.newUpdater(BankAccount.class, &quot;money&quot;);

    public void transferMoney(BankAccount bankAccount) {
        atomicIntegerFieldUpdater.getAndIncrement(bankAccount);

    }
}

public class AtomicIntegerFieldUpdaterDemo {
    public static void main(String[] args) throws InterruptedException {
        BankAccount bankAccount = new BankAccount();
        CountDownLatch countDownLatch = new CountDownLatch(10);
        for (int i = 1; i &amp;lt;= 10; i++) {
            new Thread(() -&amp;gt; {
                try {
                    for (int j = 1; j &amp;lt;= 1000; j++) {
                        bankAccount.transferMoney(bankAccount);
                    }
                } finally {
                    countDownLatch.countDown();
                }
            }, String.valueOf(i)).start();

        }
        countDownLatch.await();
        System.out.println(Thread.currentThread().getName() + &apos;\t&apos; + &quot;result: &quot; + bankAccount.money); //main	result: 10000
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote&gt;
&lt;p&gt;AtomicReferenceFieldUpdater案例演示&lt;/p&gt;
&lt;/blockquote&gt;
&lt;pre&gt;&lt;code&gt;/**
 * @author Guanghao Wei
 * @create 2023-04-12 17:43
 * 需求：多线程并发调用一个类的初始化方法，如果未被初始化过，将执行初始化工作
 * 要求只能被初始化一次，只有一个线程操作成功
 */
class MyVar {
    public volatile Boolean isInit = Boolean.FALSE;
    AtomicReferenceFieldUpdater&amp;lt;MyVar, Boolean&amp;gt; referenceFieldUpdater = AtomicReferenceFieldUpdater.newUpdater(MyVar.class, Boolean.class, &quot;isInit&quot;);

    public void init(MyVar myVar) {
        if (referenceFieldUpdater.compareAndSet(myVar, Boolean.FALSE, Boolean.TRUE)) {
            System.out.println(Thread.currentThread().getName() + &quot;\t&quot; + &quot;--------------start init ,need 2 secondes&quot;);
            try {
                TimeUnit.SECONDS.sleep(2);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println(Thread.currentThread().getName() + &quot;\t&quot; + &quot;--------------over init&quot;);
        } else {
            System.out.println(Thread.currentThread().getName() + &quot;\t&quot; + &quot;--------------已经有线程进行初始化工作了。。。。。&quot;);
        }
    }
}

public class AtomicReferenceFieldUpdaterDemo {

    public static void main(String[] args) {
        MyVar myVar = new MyVar();
        for (int i = 1; i &amp;lt;= 5; i++) {
            new Thread(() -&amp;gt; {
                myVar.init(myVar);
            }, String.valueOf(i)).start();
        }
    }
}
/**
 * 1	--------------start init ,need 2 secondes
 * 5	--------------已经有线程进行初始化工作了。。。。。
 * 2	--------------已经有线程进行初始化工作了。。。。。
 * 4	--------------已经有线程进行初始化工作了。。。。。
 * 3	--------------已经有线程进行初始化工作了。。。。。
 * 1	--------------over init
 */
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;8.5 原子操作增强类原理深度解析&lt;/h2&gt;
&lt;p&gt;●DoubleAccumulator：一个或多个变量，它们一起保持运行double使用所提供的功能更新值&lt;/p&gt;
&lt;p&gt;●DoubleAdder：一个或多个变量一起保持初始为零double总和&lt;/p&gt;
&lt;p&gt;●LongAccumulator：一个或多个变量，一起保持使用提供的功能更新运行的值long ，提供了自定义的函数操作&lt;/p&gt;
&lt;p&gt;●LongAdder：一个或多个变量一起维持初始为零long总和（重点），只能用来计算加法，且从0开始计算&lt;/p&gt;
&lt;h3&gt;8.5.1 常用API&lt;/h3&gt;
&lt;p&gt;&lt;img src=&quot;https://tangtangit.com/_astro/1681359055325-03283a0d-d17b-4444-9327-8415222124a2.CxUBYy8a_2qeTyD.webp&quot; alt=&quot;image.png&quot; /&gt;&lt;/p&gt;
&lt;h3&gt;8.5.2 面试题&lt;/h3&gt;
&lt;p&gt;1热点商品点赞计算器，点赞数加加统计，不要求实时精确&lt;/p&gt;
&lt;p&gt;2一个很大的list，里面都是int类型，如何实现加加，思路？&lt;/p&gt;
&lt;h3&gt;8.5.3 点赞计数器&lt;/h3&gt;
&lt;blockquote&gt;
&lt;p&gt;多种方式实现点赞计数器案例演示结果&lt;/p&gt;
&lt;/blockquote&gt;
&lt;pre&gt;&lt;code&gt;/**
 * @author Guanghao Wei
 * @create 2023-04-13 12:19
 * 需求：50个线程，每个线程100w此，总点赞数出来
 */
class ClickNumber {
    int number = 0;

    public synchronized void clickBySynchronized() {
        number++;
    }

    AtomicLong atomicLong = new AtomicLong(0);

    public void clickByAtomicLong() {
        atomicLong.getAndIncrement();
    }

    LongAdder longAdder = new LongAdder();

    public void clickByLongAdder() {
        longAdder.increment();
    }

    LongAccumulator longAccumulator = new LongAccumulator((x, y) -&amp;gt; x + y, 0);

    public void clickByLongAccumulator() {
        longAccumulator.accumulate(1);
    }
}

public class AccumulatorCompareDemo {
    public static final int _1W = 10000;
    public static final int THREAD_NUMBER = 50;

    public static void main(String[] args) throws InterruptedException {
        ClickNumber clickNumber = new ClickNumber();
        long StartTime;
        long endTime;
        CountDownLatch countDownLatch1 = new CountDownLatch(THREAD_NUMBER);
        CountDownLatch countDownLatch2 = new CountDownLatch(THREAD_NUMBER);
        CountDownLatch countDownLatch3 = new CountDownLatch(THREAD_NUMBER);
        CountDownLatch countDownLatch4 = new CountDownLatch(THREAD_NUMBER);

        StartTime = System.currentTimeMillis();
        for (int i = 1; i &amp;lt;= 50; i++) {
            new Thread(() -&amp;gt; {
                try {
                    for (int j = 1; j &amp;lt;= 100 * _1W; j++) {
                        clickNumber.clickBySynchronized();
                    }
                } finally {
                    countDownLatch1.countDown();
                }
            }, String.valueOf(i)).start();
        }
        countDownLatch1.await();
        endTime = System.currentTimeMillis();
        System.out.println(&quot;------costTime: &quot; + (endTime - StartTime) + &quot; 毫秒&quot; + &quot;\t clickBySynchronized: &quot; + clickNumber.number);

        StartTime = System.currentTimeMillis();
        for (int i = 1; i &amp;lt;= 50; i++) {
            new Thread(() -&amp;gt; {
                try {
                    for (int j = 1; j &amp;lt;= 100 * _1W; j++) {
                        clickNumber.clickByAtomicLong();
                    }
                } finally {
                    countDownLatch2.countDown();
                }
            }, String.valueOf(i)).start();
        }
        countDownLatch2.await();
        endTime = System.currentTimeMillis();
        System.out.println(&quot;------costTime: &quot; + (endTime - StartTime) + &quot; 毫秒&quot; + &quot;\t clickByAtomicLong: &quot; + clickNumber.atomicLong.get());

        StartTime = System.currentTimeMillis();
        for (int i = 1; i &amp;lt;= 50; i++) {
            new Thread(() -&amp;gt; {
                try {
                    for (int j = 1; j &amp;lt;= 100 * _1W; j++) {
                        clickNumber.clickByLongAdder();
                    }
                } finally {
                    countDownLatch3.countDown();
                }
            }, String.valueOf(i)).start();
        }
        countDownLatch3.await();
        endTime = System.currentTimeMillis();
        System.out.println(&quot;------costTime: &quot; + (endTime - StartTime) + &quot; 毫秒&quot; + &quot;\t clickByLongAdder: &quot; + clickNumber.longAdder.sum());

        StartTime = System.currentTimeMillis();
        for (int i = 1; i &amp;lt;= 50; i++) {
            new Thread(() -&amp;gt; {
                try {
                    for (int j = 1; j &amp;lt;= 100 * _1W; j++) {
                        clickNumber.clickByLongAccumulator();
                    }
                } finally {
                    countDownLatch4.countDown();
                }
            }, String.valueOf(i)).start();
        }
        countDownLatch4.await();
        endTime = System.currentTimeMillis();
        System.out.println(&quot;------costTime: &quot; + (endTime - StartTime) + &quot; 毫秒&quot; + &quot;\t clickByLongAccumulator: &quot; + clickNumber.longAccumulator.get());

    }
}
/**
 * ------costTime: 1313 毫秒	 clickBySynchronized: 50000000
 * ------costTime: 825 毫秒	 clickByAtomicLong: 50000000
 * ------costTime: 92 毫秒	 clickByLongAdder: 50000000
 * ------costTime: 61 毫秒	 clickByLongAccumulator: 50000000
 */
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;8.5.4 源码、原理分析&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;架构&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;ul&gt;
&lt;li&gt;&lt;img src=&quot;https://tangtangit.com/_astro/1681360405215-82cffced-6287-4f86-bafb-e643b53b4691.DBg2HCOV_Zu6y3V.webp&quot; alt=&quot;img&quot; /&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;原理（LongAdder为什么这么快）&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;ul&gt;
&lt;li&gt;如果是JDK8，推荐使用LongAdder对象，比AtomicLong性能更好（减少乐观锁的重试次数）&lt;/li&gt;
&lt;li&gt;LongAdder是Striped64的子类&lt;/li&gt;
&lt;li&gt;Striped64的基本结构&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;ul&gt;
&lt;li&gt;&lt;img src=&quot;https://tangtangit.com/_astro/1681360745992-3a3145bd-da77-4342-bb17-dc6fae081555.DI8ImdJH_3Ka3C.webp&quot; alt=&quot;img&quot; /&gt;&lt;/li&gt;
&lt;li&gt;&lt;img src=&quot;https://tangtangit.com/_astro/1681360807691-66a0483c-4c65-46ba-8060-2b5a01a16031.MM9xN31H_ZB6eyB.webp&quot; alt=&quot;img&quot; /&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;ul&gt;
&lt;li&gt;cell：是java.util.concurrent.atomic下Striped64的一个内部类&lt;/li&gt;
&lt;li&gt;LongAdder为什么这么快&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;ul&gt;
&lt;li&gt;LongAdder的基本思路就是分散热点，将value值分散到一个Cell数组中，不同线程会命中到数组的不同槽中，各个线程只对自己槽中的那个值进行CAS操作，这样热点就被分散了，冲突的概率就小很多，如果要获取真正的long值，只要将各个槽中的变量值累加返回&lt;/li&gt;
&lt;li&gt;sum()会将所有的Cell数组中的value和base累加作为返回值，核心的思想就是将之前AtomicLong一个value的更新压力分散到多个value中去，从而降级更新热点。&lt;/li&gt;
&lt;li&gt;内部有一个base变量，一个Cell[]数组&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;ul&gt;
&lt;li&gt;base变量：低并发，直接累加到该变量上&lt;/li&gt;
&lt;li&gt;Cell[]数组：高并发，累加进各个线程自己的槽Cell[i]中&lt;/li&gt;
&lt;li&gt;&lt;img src=&quot;https://tangtangit.com/_astro/1681361518539-d3e1667b-7e89-4e3e-9d63-13bdf0b67223.DERPpbYp_ZGHPyp.webp&quot; alt=&quot;img&quot; /&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;源码解读深度分析&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;ul&gt;
&lt;li&gt;LongAdder在无竞争的情况下，跟AtomicLong一样，对同一个base进行操作，当出现竞争关系时则是采用化整为零分散热点的做法，用空间换时间，用一个数组cells，将一个value值拆分进这个数组cells。多个线程需要同时对value进行操作的时候，可以对线程id进行hash得到hash值，再根据hash值映射到这个数组cells的某个下标，再对该下标所对应的值进行自增操作。当所有线程操作完毕，将数组cells的所有值和base都加起来作为最终结果&lt;/li&gt;
&lt;li&gt;add(1L)&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;ul&gt;
&lt;li&gt;&lt;img src=&quot;https://tangtangit.com/_astro/1681363434366-afadf40f-cf35-4142-bb92-fb5dc8f1e61c.B9ivAfN6_mq40O.webp&quot; alt=&quot;img&quot; /&gt;&lt;/li&gt;
&lt;li&gt;1 如果Cells表为空，尝试用CAS更新base字段，成功则退出&lt;/li&gt;
&lt;li&gt;2 如果Cells表为空，CAS更新base字段失败，出现竞争，uncontended为true，调用longAccumulate（新建数组）&lt;/li&gt;
&lt;li&gt;3 如果Cells表非空，但当前线程映射的槽为空，uncontended为true，调用longAccumulate（初始化）&lt;/li&gt;
&lt;li&gt;4 如果Cells表非空，且当前线程映射的槽非空，CAS更新Cell的值，成功则返回，否则，uncontended设为false，调用longAccumulate（扩容）&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;ul&gt;
&lt;li&gt;longAccumulate&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;ul&gt;
&lt;li&gt;&lt;img src=&quot;https://tangtangit.com/_astro/1681364292323-28e579ea-8ca5-4b26-875e-7b1bb9bd5e17.P48ybyag_190Ik5.webp&quot; alt=&quot;img&quot; /&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;ul&gt;
&lt;li&gt;sum&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;ul&gt;
&lt;li&gt;&lt;img src=&quot;https://tangtangit.com/_astro/1681364333621-bdae58b6-65ef-491f-8df6-9120adae0ff0.y5Ir5AxN_Z2jS0VM.webp&quot; alt=&quot;img&quot; /&gt;&lt;/li&gt;
&lt;li&gt;sum()会将所有Cell数组中的value和base累加作为返回值。核心思想就是将之前AtomicLong一个value的更新压力分散到多个value中去，从而降级更新热点。&lt;/li&gt;
&lt;li&gt;sum执行时，并没有限制对base和cells的更新，所以LongAdder不是强一致性的，它是最终一致性的，对cell的读取无法保证是最后一次写入的值，所以在没有并发的场景下，可以获得正确的结果。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;使用总结&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;ul&gt;
&lt;li&gt;AtomicLong线程安全，可允许一些性能损耗，要求高精度时可使用，保证精度，多个线程对单个热点值value进行了原子操作-----保证精度，性能代码&lt;/li&gt;
&lt;li&gt;LongAdder当需要在高并发场景下有较好的性能表现，且对值得精确度要求不高时，可以使用，LongAdder时每个线程拥有自己得槽，各个线程一般只对自己槽中得那个值进行CAS操作---保证性能，精度代价&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;8.5.5 总结&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;AtomicLong&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;ul&gt;
&lt;li&gt;原理：CAS+自旋&lt;/li&gt;
&lt;li&gt;场景：低并发下的全局计算，AtomicLong能保证并发情况下计数的准确性，其内部通过CAS来解决并发安全性问题&lt;/li&gt;
&lt;li&gt;缺陷：高并发后性能急剧下降----AtomicLong的自旋会成为瓶颈（N个线程CAS操作修改线程的值，每次只有一个成功过，其他N-1失败，失败的不停自旋直至成功，这样大量失败自旋的情况，一下子cpu就打高了）&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;LongAdder&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;ul&gt;
&lt;li&gt;原理：CAS+Base+Cell数组分散-----空间换时间并分散了热点数据&lt;/li&gt;
&lt;li&gt;场景：高并发下的全局计算&lt;/li&gt;
&lt;li&gt;缺陷：sum求和后还有计算线程修改结果的话，最后结果不够准确&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h1&gt;9. 聊聊ThreadLocal&lt;/h1&gt;
&lt;h2&gt;9.1 ThreadLocal简介&lt;/h2&gt;
&lt;h3&gt;9.1.1 面试题&lt;/h3&gt;
&lt;p&gt;●ThreadLocal中ThreadLocalMap的数据结构和关系&lt;/p&gt;
&lt;p&gt;●ThreadLocal的key是弱引用，这是为什么？&lt;/p&gt;
&lt;p&gt;●ThreadLocal内存泄漏问题你知道吗？&lt;/p&gt;
&lt;p&gt;●ThreadLocal中最后为什么要加remove方法？&lt;/p&gt;
&lt;h3&gt;9.1.2 是什么？&lt;/h3&gt;
&lt;p&gt;ThreadLocal提供线程局部变量。这些变量与正常的变量不同，因为每一个线程在访问ThreadLocal实例的时候（通过其get或set方法）都有自己的、独立初始化的变量副本。ThreadLocal实例通常是类中的私有静态字段，使用它的目的是希望将状态（例如，用户ID或事物ID）与线程关联起来。&lt;/p&gt;
&lt;h3&gt;9.1.3 能干吗？&lt;/h3&gt;
&lt;p&gt;实现每一个线程都有自己专属的本地变量副本（自己用自己的变量不用麻烦别人，不和其他人共享，人人有份，人各一份）。主要解决了让每个线程绑定自己的值，通过使用get()和set()方法，获取默认值或将其改为当前线程所存的副本的值从而避免了线程安全问题。比如8锁案例中，资源类是使用同一部手机，多个线程抢夺同一部手机，假如人手一份不是天下太平？&lt;/p&gt;
&lt;h3&gt;9.1.4 API介绍&lt;/h3&gt;
&lt;p&gt;&lt;img src=&quot;https://tangtangit.com/_astro/1681365874862-d474b847-86c3-44b8-8456-3bdf8faffba1.m8hTktKf_Z2mWeoh.webp&quot; alt=&quot;image.png&quot; /&gt;&lt;/p&gt;
&lt;h3&gt;9.1.5 永远的helloworld讲起&lt;/h3&gt;
&lt;p&gt;●问题描述：5个销售买房子，集团只关心销售总量的准确统计数，按照总销售额统计，方便集团公司给部分发送奖金--------群雄逐鹿起纷争------为了数据安全只能加锁&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;需求1演示&lt;/p&gt;
&lt;/blockquote&gt;
&lt;pre&gt;&lt;code&gt;/**
 * @author Guanghao Wei
 * @create 2023-04-13 14:06
 * 需求：5个销售卖房子，集团只关心销售总量的精确统计数
 */
class House {
    int saleCount = 0;

    public synchronized void saleHouse() {
        saleCount++;
    }

}

public class ThreadLocalDemo {
    public static void main(String[] args) {
        House house = new House();
        for (int i = 1; i &amp;lt;= 5; i++) {
            new Thread(() -&amp;gt; {
                int size = new Random().nextInt(5) + 1;
                System.out.println(size);
                for (int j = 1; j &amp;lt;= size; j++) {
                    house.saleHouse();
                }
            }, String.valueOf(i)).start();

        }
        try {
            TimeUnit.MILLISECONDS.sleep(300);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println(Thread.currentThread().getName() + &quot;\t&quot; + &quot;共计卖出多少套： &quot; + house.saleCount);
    }
}
/**
 * 3
 * 4
 * 2
 * 4
 * 2
 * main	共计卖出多少套： 15
 */
&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;需求变更：希望各自分灶吃饭，各凭销售本事提成，按照出单数各自统计-------比如房产中介销售都有自己的销售额指标，自己专属自己的，不和别人参和。----人手一份天下安&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;/**
 * @author Guanghao Wei
 * @create 2023-04-13 14:06
 * 需求：需求变更：希望各自分灶吃饭，各凭销售本事提成，按照出单数各自统计-------比如房产中介销售都有自己的销售额指标，自己专属自己的，不和别人参和。
 */
class House {
    int saleCount = 0;

    public synchronized void saleHouse() {
        saleCount++;
    }

    ThreadLocal&amp;lt;Integer&amp;gt; saleVolume = ThreadLocal.withInitial(() -&amp;gt; 0);

    public void saleVolumeByThreadLocal() {
        saleVolume.set(1 + saleVolume.get());
    }


}

public class ThreadLocalDemo {
    public static void main(String[] args) {
        House house = new House();
        for (int i = 1; i &amp;lt;= 5; i++) {
            new Thread(() -&amp;gt; {
                int size = new Random().nextInt(5) + 1;
                try {
                    for (int j = 1; j &amp;lt;= size; j++) {
                        house.saleHouse();
                        house.saleVolumeByThreadLocal();
                    }
                    System.out.println(Thread.currentThread().getName() + &quot;\t&quot; + &quot;号销售卖出：&quot; + house.saleVolume.get());
                } finally {
                    house.saleVolume.remove();
                }
            }, String.valueOf(i)).start();

        }
        try {
            TimeUnit.MILLISECONDS.sleep(300);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println(Thread.currentThread().getName() + &quot;\t&quot; + &quot;共计卖出多少套： &quot; + house.saleCount);
    }
}
/**
 * 3	号销售卖出：1
 * 4	号销售卖出：3
 * 5	号销售卖出：4
 * 2	号销售卖出：3
 * 1	号销售卖出：5
 * main	共计卖出多少套： 16
 */
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;9.1.6 总结&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;因为每个Thread内有自己的实例副本且该副本只有当前线程自己使用&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;既然其他ThreadLocal不可访问，那就不存在多线程间共享问题&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;统一设置初始值，但是每个线程对这个值得修改都是各自线程互相独立得&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;如何才能不争抢&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;ul&gt;
&lt;li&gt;加入synchronized或者Lock控制资源的访问顺序&lt;/li&gt;
&lt;li&gt;人手一份，大家各自安好，没有必要争抢&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;9.2 ThreadLocal源码分析&lt;/h2&gt;
&lt;h3&gt;9.2.1 源码解读&lt;/h3&gt;
&lt;h3&gt;9.2.2 Thread、ThreadLocal、ThreadLocalMap关系&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;Thread和ThreadLocal，人手一份&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;ul&gt;
&lt;li&gt;&lt;img src=&quot;https://tangtangit.com/_astro/1681367558745-dbaf5520-ddba-4900-8d0a-03b328d79141.DV7aYTUv_1c1lQh.webp&quot; alt=&quot;img&quot; /&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;ThreadLocal和ThreadLocalMap&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;ul&gt;
&lt;li&gt;&lt;img src=&quot;https://tangtangit.com/_astro/1681367687947-8ac0ca12-440a-4945-bcdb-fbdfb5ad4fda.nGScys6R_ZNJo2A.webp&quot; alt=&quot;img&quot; /&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;三者总概括&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;ul&gt;
&lt;li&gt;&lt;img src=&quot;https://tangtangit.com/_astro/1681367721060-7f26ea88-3756-4aab-8a78-76e87eb3827d.PVwJvYuw_LQuor.webp&quot; alt=&quot;img&quot; /&gt;&lt;/li&gt;
&lt;li&gt;ThreadLocalMap实际上就是一个以ThreadLocal实例为Key，任意对象为value的Entry对象&lt;/li&gt;
&lt;li&gt;当我们为ThreadLocal变量赋值，实际上就是以当前ThreadLocal实例为Key，值为value的Entry往这个ThreadLocalMap中存放&lt;/li&gt;
&lt;li&gt;&lt;img src=&quot;https://tangtangit.com/_astro/1681370109524-7f3802f0-49be-4f8a-a9a3-bfd16c480eb1.nvB3l23k_19UijQ.webp&quot; alt=&quot;img&quot; /&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;9.2.3 总结&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;ThreadLocalMap从字面上就可以看出这是一个保存ThreadLocal对象的map（其实是以ThreadLocal为Key），不过是经过了两层包装的ThreadLocal对象：&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;ul&gt;
&lt;li&gt;&lt;img src=&quot;https://tangtangit.com/_astro/1681368148461-8f9c0519-545d-447a-bc77-0ea31484ad59.DsWNKR81_1Kjwh5.webp&quot; alt=&quot;img&quot; /&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;JVM内部维护了一个线程版的Map&amp;lt;ThreadLocal, Value&amp;gt;（通过ThreadLocal对象的set方法，结果把ThreadLocal对象自己当作Key，放进了ThreadLocalMap中），每个线程要用到这个T的时候，用当前的线程去Map里面获取，通过这样让每个线程都拥有了自己独立的变量，人手一份，竞争条件被彻底消除，在并发模式下是绝对安全的变量。&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;9.3 ThreadLocal内存泄漏问题&lt;/h2&gt;
&lt;h3&gt;9.3.1 什么是内存泄漏&lt;/h3&gt;
&lt;p&gt;不再会被使用的对象或者变量占用的内存不能被回收，就是内存泄漏&lt;/p&gt;
&lt;h3&gt;9.3.2 谁惹的祸？&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;再回首ThreadLocalMap&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;ul&gt;
&lt;li&gt;&lt;img src=&quot;https://tangtangit.com/_astro/1681368935944-413631bd-34e1-4c46-9076-3b6e388f18f1.Dc2i0vgh_15dPaz.webp&quot; alt=&quot;img&quot; /&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;强软弱虚引用&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;ul&gt;
&lt;li&gt;&lt;img src=&quot;https://tangtangit.com/_astro/1681368976782-7841e56a-c7fa-4bc6-9f93-e649ecef85df.CY4GNNMY_25NTu6.webp&quot; alt=&quot;img&quot; /&gt;&lt;/li&gt;
&lt;li&gt;强引用：&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;ul&gt;
&lt;li&gt;对于强引用的对象，就算是出现了OOM也不会对该对象进行回收，死都不收，当一个对象被强引用变量引用时，它处于可达状态，是不可能被垃圾回收机制回收的，即使该对象以后永远都不会被用到，JVM也不会回收，因此强引用是造成Java内存泄露的主要原因之一。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;ul&gt;
&lt;li&gt;软引用：&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;ul&gt;
&lt;li&gt;是一种相对强引用弱化了一些的引用，对于只有软引用的对象而言，当系统内存充足时，不会被回收，当系统内存不足时，他会被回收，软引用通常用在对内存敏感的程序中，比如高速缓存，内存够用就保留，不够用就回收。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;ul&gt;
&lt;li&gt;弱引用：&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;ul&gt;
&lt;li&gt;比软引用的生命周期更短，对于只有弱引用的对象而言，只要垃圾回收机制一运行，不管JVM的内存空间是否足够，都会回收该对象占用的内存。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;ul&gt;
&lt;li&gt;软引用和弱引用的使用场景-----&amp;gt;假如有一个应用需要读取大量的本地图片：&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;ul&gt;
&lt;li&gt;如果每次读取图片都从硬盘读取则会严重影响性能&lt;/li&gt;
&lt;li&gt;如果一次性全部加载到内存中又可能会造成内存溢出&lt;/li&gt;
&lt;li&gt;此时使用软应用来解决，设计思路时：用一个HashMap来保存图片的路径和与相应图片对象关联的软引用之间的映射关系，在内存不足时，JVM会自动回收这些缓存图片对象所占用的空间，有效避免了OOM的问题&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;ul&gt;
&lt;li&gt;虚引用：&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;ul&gt;
&lt;li&gt;虚引用必须和引用队列联合使用，如果一个对象仅持有虚引用，那么它就和没有任何引用一样，在任何时候都有可能被垃圾回收器回收，它不能单独使用也不能通过它访问对象。&lt;/li&gt;
&lt;li&gt;虚引用的主要作用是跟踪对象被垃圾回收的状态。仅仅是提供了一种确保对象被finalize后，做某些事情的通知机制。换句话说就是在对象被GC的时候会收到一个系统通知或者后续添加进一步的处理，用来实现比finalize机制更灵活的回收操作。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;9.3.3 为什么要用弱引用？不用如何？&lt;/h3&gt;
&lt;p&gt;&lt;img src=&quot;https://tangtangit.com/_astro/1681370661933-928e40a9-f163-482b-8c0e-998f1fc0d276.ClNeo1_8_10llSb.webp&quot; alt=&quot;img&quot; /&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;为什么要用弱引用：&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;ul&gt;
&lt;li&gt;当方法执行完毕后，栈帧销毁，强引用t1也就没有了，但此时线程的ThreadLocalMap里某个entry的Key引用还指向这个对象，若这个Key是强引用，就会导致Key指向的ThreadLocal对象即V指向的对象不能被gc回收，造成内存泄露&lt;/li&gt;
&lt;li&gt;若这个引用时弱引用就大概率会减少内存泄漏的问题（当然，还得考虑key为null这个坑），使用弱引用就可以使ThreadLocal对象在方法执行完毕后顺利被回收且entry的key引用指向为null&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;这里有个需要注意的问题：&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;ul&gt;
&lt;li&gt;ThreadLocalMap使用ThreadLocal的弱引用作为Key，如果一个ThreadLocal没有外部强引用引用他，那么系统gc时，这个ThreadLocal势必会被回收，这样一来，ThreadLocalMap中就会出现Key为null的Entry，就没有办法访问这些Key为null的Entry的value，如果当前线程迟迟不结束的话（好比正在使用线程池），这些key为null的Entry的value就会一直存在一条强引用链&lt;/li&gt;
&lt;li&gt;虽然弱引用，保证了Key指向的ThreadLocal对象能够被及时回收，但是v指向的value对象是需要ThreadLocalMap调用get、set时发现key为null时才会去回收整个entry、value，因此弱引用不能100%保证内存不泄露，我们要在不使用某个ThreadLocal对象后，手动调用remove方法来删除它，尤其是在线程池中，不仅仅是内存泄漏的问题，因为线程池中的线程是重复使用的，意味着这个线程的ThreadLocalMap对象也是重复使用的，如果我们不手动调用remove方法，那么后面的线程就有可能获取到上个线程遗留下来的value值，造成bug。&lt;/li&gt;
&lt;li&gt;清除脏Entry----key为null的entry&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;ul&gt;
&lt;li&gt;set()方法&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;ul&gt;
&lt;li&gt;&lt;img src=&quot;https://tangtangit.com/_astro/1681439164133-0c01db19-01cd-43ff-92fc-fb827c8927d8.DqXXQEZ9_1xGJO.webp&quot; alt=&quot;img&quot; /&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;ul&gt;
&lt;li&gt;get()方法&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;ul&gt;
&lt;li&gt;&lt;img src=&quot;https://tangtangit.com/_astro/1681439454200-887f0822-96d6-4060-ab08-e49f72383790.CQLWpQ5X_Zho4jw.webp&quot; alt=&quot;img&quot; /&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;ul&gt;
&lt;li&gt;remove()&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;ul&gt;
&lt;li&gt;&lt;img src=&quot;https://tangtangit.com/_astro/1681439555545-01a892b9-b0f4-479c-8c4c-7e2dadc07d17.BNbv636q_1nUs9I.webp&quot; alt=&quot;img&quot; /&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;9.3.4 最佳实践&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;ThreadLocal一定要初始化，避免空指针异常。&lt;/li&gt;
&lt;li&gt;建议把ThreadLocal修饰为static&lt;/li&gt;
&lt;li&gt;用完记得手动remove&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;9.4 小总结&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;ThreadLocal并不解决线程间共享数据的问题&lt;/li&gt;
&lt;li&gt;ThreadLocal适用于变量在线程间隔离且在方法间共享的场景&lt;/li&gt;
&lt;li&gt;ThreadLocal通过隐式的在不同线程内创建独立实例副本避免了实例线程安全的问题&lt;/li&gt;
&lt;li&gt;每个线程持有一个只属于它自己的专属map并维护了ThreadLocal对象与具体实例的映射，该Map由于只被持有他的线程访问，故不存在线程安全以及锁的问题&lt;/li&gt;
&lt;li&gt;ThreadLocalMap的Entry对ThreadLocal的引用为弱引用。避免了ThreadLocal对象无法被回收的问题&lt;/li&gt;
&lt;li&gt;都会通过expungeStaleEntry，cleanSomeSlots，replaceStaleEntry这三个方法回收键为null的Entry对象的值（即为具体实例）以及entry对象本身从而防止内存泄漏，属于安全加固的方法&lt;/li&gt;
&lt;li&gt;群雄逐鹿起纷争，人各一份天下安&lt;/li&gt;
&lt;/ul&gt;
&lt;h1&gt;10. Java对象内存布局和对象头&lt;/h1&gt;
&lt;h2&gt;10.1 面试题&lt;/h2&gt;
&lt;p&gt;●说下JUC，AQS的大致流程&lt;/p&gt;
&lt;p&gt;●CAS自旋锁，是获取不到锁就一直自旋吗？CAS和synchronized区别在哪里，为什么CAS好，具体优势在哪里？&lt;/p&gt;
&lt;p&gt;●sychronized底层是如何实现的，实现同步的时候用到了CAS 了吗？具体哪里用到了？&lt;/p&gt;
&lt;p&gt;●对象头存储那些信息？长度是多少位存储？&lt;/p&gt;
&lt;h2&gt;10.2 Object object = new Object()谈谈你对这句话的理解？&lt;/h2&gt;
&lt;p&gt;●位置所在--------&amp;gt;JVM堆-&amp;gt;新生区-&amp;gt;伊甸园区&lt;/p&gt;
&lt;p&gt;●构成布局--------&amp;gt;对象头+实例数据+对齐填充&lt;/p&gt;
&lt;h2&gt;10.3 对象在堆内存中布局&lt;/h2&gt;
&lt;h3&gt;10.3.1 权威定义----周志明老师JVM&lt;/h3&gt;
&lt;p&gt;在HotSpot虚拟机里，对象在堆内存的存储布局可以划分为三个部分：对象头（Header）、实例数据（Instance Data） 和对齐填充（Padding）。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://tangtangit.com/_astro/1681442187170-c371a7b9-fa6a-477a-b7f5-6ea12ce59281.CXTCfWhq_1Vb7lL.webp&quot; alt=&quot;image.png&quot; /&gt;&lt;/p&gt;
&lt;h3&gt;10.3.2 对象在堆内存中的存储布局&lt;/h3&gt;
&lt;p&gt;●对象头（在64位系统中，Mark Word占了8个字节，类型指针占了8个字节，一共是16个字节）&lt;/p&gt;
&lt;p&gt;○对象标记（Mark Word）&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://tangtangit.com/_astro/1681442831019-c219d11a-c62e-4ddc-a378-069a233ed7f3.Clov8_kN_f2UAO.webp&quot; alt=&quot;image.png&quot; /&gt;&lt;/p&gt;
&lt;p&gt;■默认存储对象的HashCode、分代年龄和锁标志等信息。&lt;/p&gt;
&lt;p&gt;■这些信息都是与对象自身定义无关的数据，所以Mark Word被设计成一个非固定的数据结构以便在极小的空间内存存储尽量多的数据。&lt;/p&gt;
&lt;p&gt;■它会根据对象的状态复用自己的存储空间，也就是说在运行期间MarkWord里存储的数据会随着锁标志位的变化而变化。&lt;/p&gt;
&lt;p&gt;○类元信息（类型指针）&lt;/p&gt;
&lt;p&gt;■对象指向它的类元数据的指针，虚拟机通过这个指针来确定这个对象哪个类的实例&lt;/p&gt;
&lt;p&gt;**●**实例数据&lt;/p&gt;
&lt;p&gt;○存放类的属性（Field）数据信息，包括父类的属性信息&lt;/p&gt;
&lt;p&gt;●对齐填充（保证8个字节的倍数）&lt;/p&gt;
&lt;p&gt;○虚拟机要求对象起始地址必须是8字节的整数倍，填充数据不是必须存在的，仅仅是为了字节对齐，这部分内存按8字节补充对齐。&lt;/p&gt;
&lt;h2&gt;10.4 再说对象头的MarkWord&lt;/h2&gt;
&lt;p&gt;&lt;img src=&quot;https://tangtangit.com/_astro/1681443457091-02bfb636-4c94-44d4-b56a-99485c94318b.BN05bBQ3_ZyXPVn.webp&quot; alt=&quot;image.png&quot; /&gt;&lt;/p&gt;
&lt;h2&gt;10.5 聊聊Object obj = new Object()&lt;/h2&gt;
&lt;h3&gt;10.5.1 运行结果展示&lt;/h3&gt;
&lt;p&gt;&lt;img src=&quot;https://tangtangit.com/_astro/1681444196164-d7d7c166-59de-42af-ab00-ea9b96b5c6bb.cHirniIQ_ZnLe5C.webp&quot; alt=&quot;image.png&quot; /&gt;&lt;/p&gt;
&lt;h3&gt;10.5.2 压缩指针&lt;/h3&gt;
&lt;p&gt;●Java -XX:+PrintCommandLineFlags -version 查看当前虚拟机信息&lt;/p&gt;
&lt;p&gt;●默认开启压缩指针，开启后将上述类型指针压缩为4字节，以节约空间&lt;/p&gt;
&lt;p&gt;○&lt;/p&gt;
&lt;p&gt;默认开启压缩说明&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://tangtangit.com/_astro/1681444473777-8ef4114e-44a5-4cd7-ab0f-845843b05696.BdX25Sbm_1vjyBJ.webp&quot; alt=&quot;image.png&quot; /&gt;&lt;/p&gt;
&lt;p&gt;●手动关闭压缩指针： -XX: -UseCompressedClassPointers&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://tangtangit.com/_astro/1681444812291-4f3c2e48-baec-4ab6-bf6b-e5a105b11d17.8Y9h89nW_Z1a7DA6.webp&quot; alt=&quot;image.png&quot; /&gt;&lt;/p&gt;
&lt;h2&gt;10.6 换成其他对象试试&lt;/h2&gt;
&lt;p&gt;&lt;img src=&quot;https://tangtangit.com/_astro/1681444853949-574fc73a-fb08-4c63-a998-d7599c48cd9a.UvWdFjDD_VGl9g.webp&quot; alt=&quot;image.png&quot; /&gt;&lt;/p&gt;
&lt;h1&gt;11. Synchronized与锁升级&lt;/h1&gt;
&lt;h2&gt;11.1 面试题&lt;/h2&gt;
&lt;p&gt;●谈谈你对Synchronized的理解&lt;/p&gt;
&lt;p&gt;●Sychronized的锁升级你聊聊&lt;/p&gt;
&lt;p&gt;●Synchronized实现原理，monitor对象什么时候生成的？知道monitor的monitorenter和monitorexit这两个是怎么保证同步的嘛？或者说这两个操作计算机底层是如何执行的&lt;/p&gt;
&lt;p&gt;●偏向锁和轻量级锁有什么区别&lt;/p&gt;
&lt;h2&gt;11.2 Synchronized的性能变化&lt;/h2&gt;
&lt;p&gt;●Java5以前，只有Synchronized，这个是操作系统级别的重量级操作&lt;/p&gt;
&lt;p&gt;○重量级锁，假如锁的竞争比较激烈的话，性能下降&lt;/p&gt;
&lt;p&gt;○Java 5之前 用户态和内核态之间的转换&lt;/p&gt;
&lt;p&gt;■JAVA5之前,用户态和内核态之间的切换&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://tangtangit.com/_astro/1681448109572-488801b7-7a7c-4003-88f8-a10c59b3859d.H31WlFAO_14rE0u.webp&quot; alt=&quot;image.png&quot; /&gt;&lt;/p&gt;
&lt;p&gt;●Java6 之后为了减少获得锁和释放锁所带来的性能消耗，引入了轻量级锁和偏向锁&lt;/p&gt;
&lt;h2&gt;11.3 Synchronized锁种类及升级步骤&lt;/h2&gt;
&lt;h3&gt;11.3.1 多线程访问情况&lt;/h3&gt;
&lt;p&gt;●只有一个线程来访问，有且唯一Only One&lt;/p&gt;
&lt;p&gt;●有两个线程（2个线程交替访问）&lt;/p&gt;
&lt;p&gt;●竞争激烈，更多线程来访问&lt;/p&gt;
&lt;h3&gt;11.3.2 升级流程&lt;/h3&gt;
&lt;p&gt;●Synchronized用的锁是存在Java对象头里的MarkWord中，锁升级功能主要依赖MarkWord中锁标志位和释放偏向锁标志位&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://tangtangit.com/_astro/1681448615179-68918d36-eeaa-4454-b672-0d2eb1ae30fe.Bo1uURZA_ZJSQyk.webp&quot; alt=&quot;image.png&quot; /&gt;&lt;/p&gt;
&lt;p&gt;●锁指向，请牢记&lt;/p&gt;
&lt;p&gt;○偏向锁：MarkWord存储的是偏向的线程ID&lt;/p&gt;
&lt;p&gt;○轻量锁：MarkWord存储的是指向线程栈中Lock Record的指针&lt;/p&gt;
&lt;p&gt;○重量锁：MarkWord存储的是指向堆中的monitor对象（系统互斥量指针）&lt;/p&gt;
&lt;h3&gt;11.3.3 无锁&lt;/h3&gt;
&lt;p&gt;&lt;img src=&quot;https://tangtangit.com/_astro/1681449173639-bc332c5c-a510-4b7c-8bc6-336646b5ab00.-iVKE30s_Z1WU1ag.webp&quot; alt=&quot;image.png&quot; /&gt;&lt;/p&gt;
&lt;h3&gt;11.3.4 偏锁&lt;/h3&gt;
&lt;p&gt;偏向锁：单线程竞争，当线程A第一次竞争到锁时，通过修改MarkWord中的偏向线程ID、偏向模式。如果不存在其他线程竞争，那么持有偏向锁的线程将永远不需要进行同步。&lt;/p&gt;
&lt;p&gt;主要作用：&lt;/p&gt;
&lt;p&gt;●当一段同步代码一直被同一个线程多次访问，由于只有一个线程那么该线程在后续访问时便会自动获得锁&lt;/p&gt;
&lt;p&gt;●同一个老顾客来访，直接老规矩行方便&lt;/p&gt;
&lt;p&gt;结论：&lt;/p&gt;
&lt;p&gt;●HotSpot的作者经过研究发现，大多数情况下：在多线程情况下，锁不仅不存在多线程竞争，还存在由同一个线程多次获得的情况，偏向锁就是在这种情况下出现的，它的出现是为了解决只有一个线程执行同步时提高性能。&lt;/p&gt;
&lt;p&gt;●偏向锁会偏向于第一个访问锁的线程，如果在接下来的运行过程中，该锁没有被其他线程访问，则持有偏向锁的线程将永远不需要出发同步。也即偏向锁在资源在没有竞争情况下消除了同步语句，懒得连CAS操作都不做了，直接提高程序性能。&lt;/p&gt;
&lt;p&gt;理论落地：&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://tangtangit.com/_astro/1681451836192-5ae30f1e-085d-4c2b-aa8c-1bd47f199116.gGwCJ8It_BxTn4.webp&quot; alt=&quot;image.png&quot; /&gt;&lt;/p&gt;
&lt;p&gt;技术实现：&lt;/p&gt;
&lt;p&gt;一个SYNCHRONIZED方法放一个线程抢到了锁时,那这个方法所在的对家就会在共所在的MAKWARD中将偷向锁修改状态位,&lt;/p&gt;
&lt;p&gt;会有占用前54位来任储线解请整体系统理.安该线程再次访问同一个SYNCHRONZED方法时,该统程只需去对家头的MAKWARD中去&lt;/p&gt;
&lt;p&gt;断一下是否有偏向锁指向本身的ID,无需再进入MONITOR去竞争对象了.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://tangtangit.com/_astro/1681451914425-01aa0721-8f56-4413-a1db-5092fcbfdf06.CsptfM_J_2p2Y5i.webp&quot; alt=&quot;image.png&quot; /&gt;&lt;/p&gt;
&lt;p&gt;偏向锁JVM命令：&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://tangtangit.com/_astro/1681452242110-e3b8e768-acfb-4f43-9a50-8699e9952ece.BZ_XjBc2_Zw5KFI.webp&quot; alt=&quot;image.png&quot; /&gt;&lt;/p&gt;
&lt;p&gt;案例演示：&lt;/p&gt;
&lt;p&gt;●偏向锁默认情况演示---只有一个线程&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;偏向锁默认情况演示---只有一个线程&lt;/p&gt;
&lt;/blockquote&gt;
&lt;pre&gt;&lt;code&gt;/**
 * @author Guanghao Wei
 * @create 2023-04-14 14:06
 */
public class SynchronizedUpDemo {

    public static void main(String[] args) {
        /**
         * 这里偏向锁在JDK6以上默认开启，开启后程序启动几秒后才会被激活，可以通过JVM参数来关闭延迟 -XX:BiasedLockingStartupDelay=0
         */
//        try { TimeUnit.SECONDS.sleep(5); } catch (InterruptedException e) { e.printStackTrace(); }
        Object o = new Object();
        synchronized (o) {
            System.out.println(ClassLayout.parseInstance(o).toPrintable());
        }
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;img src=&quot;https://tangtangit.com/_astro/1681452733726-2fd91d28-effd-48e8-aa2b-9f4521d90f8b.C8E-g31s_Z7zygC.webp&quot; alt=&quot;image.png&quot; /&gt;&lt;/p&gt;
&lt;p&gt;偏向锁的撤销：&lt;/p&gt;
&lt;p&gt;●当有另外一个线程逐步来竞争锁的时候，就不能再使用偏向锁了，要升级为轻量级锁，使用的是等到竞争出现才释放锁的机制&lt;/p&gt;
&lt;p&gt;●竞争线程尝试CAS更新对象头失败，会等到全局安全点（此时不会执行任何代码）撤销偏向锁，同时检查持有偏向锁的线程是否还在执行：&lt;/p&gt;
&lt;p&gt;○第一个线程正在执行Synchronized方法（处于同步块），它还没有执行完，其他线程来抢夺，该偏向锁会被取消掉并出现锁升级，此时轻量级锁由原来持有偏向锁的线程持有，继续执行同步代码块，而正在竞争的线程会自动进入自旋等待获得该轻量级锁&lt;/p&gt;
&lt;p&gt;○第一个线程执行完Synchronized（退出同步块），则将对象头设置为无所状态并撤销偏向锁，重新偏向。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://tangtangit.com/_astro/1681453654704-4252f703-ba8d-43c1-90b6-cdf78a965570.DeCcjKQp_ZNAP0I.webp&quot; alt=&quot;image.png&quot; /&gt;&lt;/p&gt;
&lt;p&gt;题外话：Java15以后逐步废弃偏向锁，需要手动开启-------&amp;gt;维护成本高&lt;/p&gt;
&lt;h3&gt;11.3.5 轻锁&lt;/h3&gt;
&lt;p&gt;概念：多线程竞争，但是任意时候最多只有一个线程竞争，即不存在锁竞争太激烈的情况，也就没有线程阻塞。&lt;/p&gt;
&lt;p&gt;主要作用：有线程来参与锁的竞争，但是获取锁的冲突时间极短----------&amp;gt;本质是自旋锁CAS&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://tangtangit.com/_astro/1681454510407-cde9df3c-e7ed-415f-a459-76f64160035f.BsphKjcV_2i4ixr.webp&quot; alt=&quot;image.png&quot; /&gt;&lt;/p&gt;
&lt;p&gt;轻量锁的获取：&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://tangtangit.com/_astro/1681454665890-db296f20-69b8-4bd8-8538-0197584c61e5.CK7LW6Ql_utMaF.webp&quot; alt=&quot;image.png&quot; /&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://tangtangit.com/_astro/1681454779796-56f6d99a-8850-44da-ad34-890a738a230c.CVc8VnfH_Z2ub4fV.webp&quot; alt=&quot;image.png&quot; /&gt;&lt;/p&gt;
&lt;p&gt;案例演示：&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://tangtangit.com/_astro/1681454830354-8d42c5c5-0025-4381-898d-b733317de70a.DhEZzj1__Z1Ngd7e.webp&quot; alt=&quot;image.png&quot; /&gt;&lt;/p&gt;
&lt;p&gt;自旋一定程度和次数（Java8 之后是自适应自旋锁------意味着自旋的次数不是固定不变的）：&lt;/p&gt;
&lt;p&gt;●线程如果自旋成功了，那下次自旋的最大次数会增加，因为JVM认为既然上次成功了，那么这一次也大概率会成功&lt;/p&gt;
&lt;p&gt;●如果很少会自选成功，那么下次会减少自旋的次数甚至不自旋，避免CPU空转&lt;/p&gt;
&lt;p&gt;轻量锁和偏向锁的区别：&lt;/p&gt;
&lt;p&gt;●争夺轻量锁失败时，自旋尝试抢占锁&lt;/p&gt;
&lt;p&gt;●轻量级锁每次退出同步块都需要释放锁，而偏向锁是在竞争发生时才释放锁&lt;/p&gt;
&lt;h3&gt;11.3.6 重锁&lt;/h3&gt;
&lt;p&gt;有大量线程参与锁的竞争，冲突性很高&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://tangtangit.com/_astro/1681455335764-3bb5bb5f-d014-4b25-9755-e0ca2c9aa809.CyaLEvhu_Z1VhXSe.webp&quot; alt=&quot;image.png&quot; /&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://tangtangit.com/_astro/1681455375540-a112e2b5-9e7f-4682-8cd0-885f5cf7957e.BzLi9ymI_Q3Gfc.webp&quot; alt=&quot;image.png&quot; /&gt;&lt;/p&gt;
&lt;h3&gt;11.3.7 小总结&lt;/h3&gt;
&lt;p&gt;●锁升级的过程&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://tangtangit.com/_astro/1681459024640-cadca197-5d19-433d-b6d8-abc8965ce494.CiWZJyBy_1UUYNp.webp&quot; alt=&quot;20200602120540100.jpg&quot; /&gt;&lt;/p&gt;
&lt;p&gt;●锁升级后，hashcode去哪儿了?&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://tangtangit.com/_astro/1681455960993-7d1f944c-3f3e-47ad-a7e3-83abedc0a8fe.BsayndPw_x1Dbx.webp&quot; alt=&quot;image.png&quot; /&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://tangtangit.com/_astro/1681455773115-6a73a005-970b-4450-a264-cdec59354184.BENfjDIg_2bxE4m.webp&quot; alt=&quot;image.png&quot; /&gt;&lt;/p&gt;
&lt;p&gt;●各种锁优缺点、synchronized锁升级和实现原理&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://tangtangit.com/_astro/1681456189232-f2cbc220-e6bc-4854-9a5d-e963623a860a.DJOSI2Tk_cmm7N.webp&quot; alt=&quot;image.png&quot; /&gt;&lt;/p&gt;
&lt;h3&gt;11.4 JIT编译器对锁的优化&lt;/h3&gt;
&lt;h3&gt;11.4.1 JIT&lt;/h3&gt;
&lt;p&gt;Just In Time Compiler 即时编译器&lt;/p&gt;
&lt;h3&gt;11.4.2 锁消除&lt;/h3&gt;
&lt;blockquote&gt;
&lt;p&gt;锁消除案例演示&lt;/p&gt;
&lt;/blockquote&gt;
&lt;pre&gt;&lt;code&gt;/**
 * @author Guanghao Wei
 * @create 2023-04-14 15:13
 * 锁消除
 * 从JIT角度看想相当于无视他，synchronized(o)不存在了
 * 这个锁对象并没有被共用扩散到其他线程使用
 * 极端的说就是根本没有加锁对象的底层机器码，消除了锁的使用
 */

public class LockClearUpDemo {
    static Object object = new Object();

    public void m1() {
        //锁消除问题，JIT会无视它，synchronized(o)每次new出来的，都不存在了，非正常的
        Object o = new Object();
        synchronized (o) {
            System.out.println(&quot;-----------hello LockClearUpDemo&quot; + &quot;\t&quot; + o.hashCode() + &quot;\t&quot; + object.hashCode());
        }
    }

    public static void main(String[] args) {
        LockClearUpDemo lockClearUpDemo = new LockClearUpDemo();
        for (int i = 0; i &amp;lt; 10; i++) {
            new Thread(() -&amp;gt; {
                lockClearUpDemo.m1();
            }, String.valueOf(i)).start();
        }
    }
}
/**
 * -----------hello LockClearUpDemo	229465744	57319765
 * -----------hello LockClearUpDemo	219013680	57319765
 * -----------hello LockClearUpDemo	1109337020	57319765
 * -----------hello LockClearUpDemo	94808467	57319765
 * -----------hello LockClearUpDemo	973369600	57319765
 * -----------hello LockClearUpDemo	64667370	57319765
 * -----------hello LockClearUpDemo	1201983305	57319765
 * -----------hello LockClearUpDemo	573110659	57319765
 * -----------hello LockClearUpDemo	1863380256	57319765
 * -----------hello LockClearUpDemo	1119787251	57319765
 */
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;11.4.3 锁粗化&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;/**
 * @author Guanghao Wei
 * @create 2023-04-14 15:18
 * 锁粗化
 * 假如方法中首尾相接，前后相邻的都是同一个锁对象，那JIT编译器会把这几个synchronized块合并为一个大块
 * 加粗加大范围，一次申请锁使用即可，避免次次的申请和释放锁，提高了性能
 */
public class LockBigDemo {
    static Object objectLock = new Object();

    public static void main(String[] args) {
        new Thread(() -&amp;gt; {
            synchronized (objectLock) {
                System.out.println(&quot;111111111111&quot;);
            }
            synchronized (objectLock) {
                System.out.println(&quot;222222222222&quot;);
            }
            synchronized (objectLock) {
                System.out.println(&quot;333333333333&quot;);
            }
            synchronized (objectLock) {
                System.out.println(&quot;444444444444&quot;);
            }
            //底层JIT的锁粗化优化
            synchronized (objectLock) {
                System.out.println(&quot;111111111111&quot;);
                System.out.println(&quot;222222222222&quot;);
                System.out.println(&quot;333333333333&quot;);
                System.out.println(&quot;444444444444&quot;);
            }
        }, &quot;t1&quot;).start();
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;11.5 小总结&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;没有锁：自由自在&lt;/li&gt;
&lt;li&gt;偏向锁：唯我独尊&lt;/li&gt;
&lt;li&gt;轻量锁：楚汉争霸&lt;/li&gt;
&lt;li&gt;重量锁：群雄逐鹿&lt;/li&gt;
&lt;/ul&gt;
&lt;h1&gt;12. AbstractQueuedSynchronizer之AQS&lt;/h1&gt;
&lt;h2&gt;12.1 前置知识&lt;/h2&gt;
&lt;p&gt;●公平锁和非公平锁&lt;/p&gt;
&lt;p&gt;○公平锁：锁被释放以后，先申请的线程先得到锁。性能较差一些，因为公平锁为了保证时间上的绝对顺序，上下文切换更频繁&lt;/p&gt;
&lt;p&gt;○非公平锁：锁被释放以后，后申请的线程可能会先获取到锁，是随机或者按照其他优先级排序的。性能更好，但可能会导致某些线程永远无法获取到锁&lt;/p&gt;
&lt;p&gt;●可重入锁&lt;/p&gt;
&lt;p&gt;○也叫做递归锁，指的是线程可以再次获取自己的内部锁，比如一个线程获取到了对象锁，此时这个对象锁还没有释放，当其想再次获取这个对象锁的时候还是可以获取的，如果不可重入的话，会导致死锁。&lt;/p&gt;
&lt;p&gt;●自旋思想&lt;/p&gt;
&lt;p&gt;○当线程请求锁时，如果锁已经被其他线程持有，那么该线程会不断地重试获取锁，而不是被挂起等待，这种不断尝试获取锁的行为称为自旋&lt;/p&gt;
&lt;p&gt;●LockSupport&lt;/p&gt;
&lt;p&gt;○一个工具类，用于线程的阻塞和唤醒操作，类似于wait()和notify()方法，但是更加灵活和可控&lt;/p&gt;
&lt;p&gt;○提供了park()和unpark()两个静态方法用于线程阻塞和唤醒操作。&lt;/p&gt;
&lt;p&gt;○优点在于可以在任意时刻阻塞和唤醒线程而不需要事先获取锁或监视器对象。&lt;/p&gt;
&lt;p&gt;●数据结构之双向链表&lt;/p&gt;
&lt;p&gt;○双向链表（Doubly Linked List）是一种常见的数据结构，它是由一系列结点（Node）组成的，每个结点包含三个部分：数据域、前驱指针和后继指针。其中，数据域存储结点的数据，前驱指针指向前一个结点，后继指针指向后一个结点。通过这种方式，双向链表可以实现双向遍历和插入、删除操作。&lt;/p&gt;
&lt;p&gt;●设计模式之模板设计模式&lt;/p&gt;
&lt;p&gt;○模板设计模式是一种行为型设计模式，定义了一种算法的框架，并将某些步骤延迟到子类中事先，这种设计模式的主要目的是允许子类在不改变算法结构的情况下重新定义算法中的某些步骤。&lt;/p&gt;
&lt;p&gt;○优点是能够提高代码复用性和可维护性。&lt;/p&gt;
&lt;h2&gt;12.2 AQS入门级别理论知识&lt;/h2&gt;
&lt;h3&gt;12.2.1 是什么？&lt;/h3&gt;
&lt;p&gt;抽象的队列同步器&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://tangtangit.com/_astro/1681462615181-7a080983-aba2-4362-a60e-b6cd2f3f385e.Dt-DBFNu_ZDmrGg.webp&quot; alt=&quot;image.png&quot; /&gt;&lt;/p&gt;
&lt;p&gt;技术解释&lt;/p&gt;
&lt;p&gt;●是用来实现锁或者其他同步器组件的公共基础部分的抽象实现&lt;/p&gt;
&lt;p&gt;●是重量级基础框架及整个JUC体系的基石，只要用于解决锁分配给”谁“的问题。&lt;/p&gt;
&lt;p&gt;●整体就是一个抽象的FIFO队列来完成资源获取线程的排队工作，并通过一个int类变量表示持有锁的状态&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://tangtangit.com/_astro/1681462962192-d1aaba76-dc90-4e8c-abbf-ee9b6dfeb339.CJFklrj7_14Ptqc.webp&quot; alt=&quot;image.png&quot; /&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://tangtangit.com/_astro/1681463110554-9897b98c-fbd0-4b6f-a2ea-823cae280c79.Dea97pUs_Z1dyKln.webp&quot; alt=&quot;image.png&quot; /&gt;&lt;/p&gt;
&lt;h3&gt;12.2.2 AQS为什么是JUC内容中最重要的基石&lt;/h3&gt;
&lt;p&gt;●和AQS有关的&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://tangtangit.com/_astro/1681463320987-a8675735-806a-4111-b540-1744b40b2cdf.DLekKxyD_Z1Go54Y.webp&quot; alt=&quot;image.png&quot; /&gt;&lt;/p&gt;
&lt;p&gt;○ReentrantLock&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://tangtangit.com/_astro/1681463492324-b22562a9-d247-4c52-b3c1-8f0bf72d8b02.BFqVnjeA_Z7R7Ni.webp&quot; alt=&quot;image.png&quot; /&gt;&lt;/p&gt;
&lt;p&gt;○CountDownLatch&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://tangtangit.com/_astro/1681463501078-095749f5-19e8-4eff-a039-5e65cfb774fc.BvrM-Dxi_lJnqr.webp&quot; alt=&quot;image.png&quot; /&gt;&lt;/p&gt;
&lt;p&gt;○ReentrantReadWriteLock&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://tangtangit.com/_astro/1681463508752-430d08be-760f-49ee-aabf-c75543b3bbb6.D_6io2Zu_al3UW.webp&quot; alt=&quot;image.png&quot; /&gt;&lt;/p&gt;
&lt;p&gt;○Semaphore&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://tangtangit.com/_astro/1681463515146-849f9a76-5b5f-468f-8749-e1cd9ba8238e.iA_bBUXZ_FL1XA.webp&quot; alt=&quot;image.png&quot; /&gt;&lt;/p&gt;
&lt;p&gt;●进一步理解锁和同步器的关系&lt;/p&gt;
&lt;p&gt;○锁，面向锁的使用者：定义了程序员和锁交互的使用层API，隐藏了实现细节，你调用即可&lt;/p&gt;
&lt;p&gt;○同步器，面向锁的实现者：Java并发大神DoungLee，提出了统一规范并简化了锁的实现，将其抽象出来，屏蔽了同步状态管理、同步队列的管理和维护、阻塞线程排队和通知、唤醒机制等，是一切锁和同步组件实现的----公共基础部分&lt;/p&gt;
&lt;h3&gt;12.2.3 能干嘛？&lt;/h3&gt;
&lt;p&gt;加锁会导致阻塞------有阻塞就需要排队，实现排队必然需要队列&lt;/p&gt;
&lt;p&gt;●抢到资源的线程直接使用处理业务，抢不到资源的必然涉及一种排队等候机制。抢占失败的线程继续去等待（类似于银行办理窗口都满了，暂时没有受理窗口的顾客只能去候客区排队等待），但等候线程仍然保留获取锁的可能且获取锁流程仍在继续（候客区的顾客也在等着叫号，轮到了再去受理窗口办理业务）&lt;/p&gt;
&lt;p&gt;●既然说到了排队等候机制，那么就一定会有某种队列形成，这样的队列是什么数据结构呢？&lt;/p&gt;
&lt;p&gt;○如果共享资源被占用，就需要一定的阻塞等待唤醒机制来保证锁分配。这个机制主要用的是CLH队列的变体实现的，将暂时获取不到锁的线程加入到队列中，这个队列就是AQS同步队列的抽象表现。它将要请求共享资源的线程及自身的等待状态封装成队列的节点对象（Node），通过CAS、自旋以及LockSupport.park()的方式，维护着state变量的状态，使其达到同步的状态。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://tangtangit.com/_astro/1681464739076-a012f721-6fb6-4e4d-a37f-58087ccace1c.C8gCR2pv_1mgSNw.webp&quot; alt=&quot;image.png&quot; /&gt;&lt;/p&gt;
&lt;h3&gt;12.2.4 小总结&lt;/h3&gt;
&lt;p&gt;AQS同步队列的基本结构&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://tangtangit.com/_astro/1681464797821-57493597-fe4f-4b36-b642-5700fb3391af.FiN1BAnL_ZFDoSX.webp&quot; alt=&quot;image.png&quot; /&gt;&lt;/p&gt;
&lt;h2&gt;12.3 AQS源码分析前置知识储备&lt;/h2&gt;
&lt;h3&gt;12.3.1 AQS内部体系架构图&lt;/h3&gt;
&lt;p&gt;&lt;img src=&quot;https://tangtangit.com/_astro/1681464982519-3cbfe547-99ab-4d38-8aa6-7748fa36c8aa.BWeeiRyN_2dyjDo.webp&quot; alt=&quot;image.png&quot; /&gt;&lt;/p&gt;
&lt;h3&gt;12.3.2 AQS内部体系架构----AQS自身&lt;/h3&gt;
&lt;p&gt;●AQS的int类型变量state&lt;/p&gt;
&lt;p&gt;○AQS的同步状态State成员变量&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://tangtangit.com/_astro/1681465319285-a6cf8348-bcf6-47ac-aba9-2262ba1a304c.Me13qw8y_1ADao9.webp&quot; alt=&quot;image.png&quot; /&gt;&lt;/p&gt;
&lt;p&gt;○银行办理业务的受理窗口状态&lt;/p&gt;
&lt;p&gt;■零就是没人，自由状态可以去办理&lt;/p&gt;
&lt;p&gt;■大于等于1，有人占用窗口，等着去&lt;/p&gt;
&lt;p&gt;●AQS的CLH队列&lt;/p&gt;
&lt;p&gt;○CLH（三个大牛的名字组成）队列为一个双向队列&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://tangtangit.com/_astro/1681465485315-4d8ae807-6640-4be3-acd1-647ebfed99da.KwzGPDo__ZxqjL1.webp&quot; alt=&quot;image.png&quot; /&gt;&lt;/p&gt;
&lt;p&gt;○银行候客区的等待顾客&lt;/p&gt;
&lt;p&gt;●小总结&lt;/p&gt;
&lt;p&gt;○有阻塞就需要排队，实现排队必然需要队列&lt;/p&gt;
&lt;p&gt;○State变量+CLH双端队列&lt;/p&gt;
&lt;h3&gt;12.3.1 AQS内部体系架构----内部类Node&lt;/h3&gt;
&lt;p&gt;●Node的int变量&lt;/p&gt;
&lt;p&gt;○Node的等待状态waitState成员变量&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://tangtangit.com/_astro/1681465816752-04931358-ef60-4cf5-922c-f0190eb6e969.D0mG5AQ-_Z1pjVOa.webp&quot; alt=&quot;image.png&quot; /&gt;&lt;/p&gt;
&lt;p&gt;○说人话&lt;/p&gt;
&lt;p&gt;■等候区其他顾客（其他线程）的等待状态&lt;/p&gt;
&lt;p&gt;■队列中每个排队的个体就是一个Node&lt;/p&gt;
&lt;p&gt;●Node此类的讲解&lt;/p&gt;
&lt;p&gt;○内部结构&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://tangtangit.com/_astro/1681466335871-7b8ba42c-7e65-4b87-ae93-022db4a9e46b.Bvnn-CDB_ZjUVNe.webp&quot; alt=&quot;image.png&quot; /&gt;&lt;/p&gt;
&lt;p&gt;○属性说明&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://tangtangit.com/_astro/1681466320982-866b0d0b-e9fe-45f0-92a2-46404b93adde.Dj-89BLz_Z1aaXLh.webp&quot; alt=&quot;image.png&quot; /&gt;&lt;/p&gt;
&lt;h2&gt;12.4 AQS源码深度讲解和分析&lt;/h2&gt;
&lt;h3&gt;12.4.1 ReentrantLock的原理&lt;/h3&gt;
&lt;p&gt;Lock接口的实现类，基本都是通过聚合了一个队列同步器的子类完成线程访问控制的&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://tangtangit.com/_astro/1681708942433-24863784-4adb-4c77-887d-4bf93d7b68da.BtoexJ0-_U0YYa.webp&quot; alt=&quot;image.png&quot; /&gt;&lt;/p&gt;
&lt;h3&gt;12.4.2 从最简单的lock方法开始看看公平和非公平&lt;/h3&gt;
&lt;p&gt;&lt;img src=&quot;https://tangtangit.com/_astro/1681709439030-81798c4b-ced7-4ca8-a77b-cfd401909593.Crh0gR-n_QsKAT.webp&quot; alt=&quot;image.png&quot; /&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://tangtangit.com/_astro/1681709370911-49da32ec-5035-49aa-bb8e-91d4c3c8975e.ba-j9j2s_1I5wme.webp&quot; alt=&quot;image.png&quot; /&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://tangtangit.com/_astro/1681709650075-ef5304ee-ac73-4253-a8f7-84c98da9cdda.DjbuyOiM_2mY14C.webp&quot; alt=&quot;image.png&quot; /&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://tangtangit.com/_astro/1681709787379-b3ff74c7-0112-462f-9e17-b2cfa6d1fee5.C0_SZU0o_1zfSEB.webp&quot; alt=&quot;image.png&quot; /&gt;&lt;/p&gt;
&lt;p&gt;公平锁和非公平锁的lock()方法唯一的区别就在于公平锁在获取同步状态时多了一个限制条件：hasQueuedPredecessors()-----公平锁加锁时判断等待队列中是否存在有效节点的方法&lt;/p&gt;
&lt;h3&gt;12.4.3 以非公平锁ReentrantLock()为例作为突破走起---方法lock()&lt;/h3&gt;
&lt;p&gt;对比公平锁和非公平锁的tryAcquire()方法的实现代码，其实差异就在于非公平锁获取锁时比公平锁中少了一个判断!hasQueuedPredecessors()，hasQueuedPredecessors()中判断了是否需要排队，导致公平锁和非公平锁的差异如下：&lt;/p&gt;
&lt;p&gt;●公平锁：公平锁讲究先来后到，线程在获取锁时，如果这个锁的等待队列中已经有线程在等待，那么当前线程就会进入到等待队列中；&lt;/p&gt;
&lt;p&gt;●非公平锁：不管是否有等待队列，如果可以获取到锁，则立刻占有锁对象。也就是说队列的第一个排队线程苏醒后，不一定就是排头的这个线程获得锁，它还需要参加竞争锁（存在线程竞争的情况下），后来的线程可能不讲武德插队夺锁了。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://tangtangit.com/_astro/1681710488888-4480bfac-991b-4c7a-8478-8bf5aa70b438.B4b82qhH_18Miua.webp&quot; alt=&quot;image.png&quot; /&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://tangtangit.com/_astro/1681710546972-3bd553ee-b9fd-49cd-9e1c-21c3e6b346f0.DTAkYJdN_dTo1J.webp&quot; alt=&quot;image.png&quot; /&gt;&lt;/p&gt;
&lt;p&gt;正式开始源码解读：&lt;/p&gt;
&lt;p&gt;●lock()&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://tangtangit.com/_astro/1681710807464-69fe3f14-c4b9-4c2a-a6aa-9306f552d21f.DeQAPeG__1QtWLW.webp&quot; alt=&quot;image.png&quot; /&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://tangtangit.com/_astro/1681710825148-dd7e96ea-e0c0-4b10-8645-3d057acd5bd0.1Lgp6t5__tj0B0.webp&quot; alt=&quot;image.png&quot; /&gt;&lt;/p&gt;
&lt;p&gt;●acquire()&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://tangtangit.com/_astro/1681711454995-05e9dc6b-1e2a-4be5-9bc4-377aad3b739d.qbsmGAZr_1DAY57.webp&quot; alt=&quot;image.png&quot; /&gt;&lt;/p&gt;
&lt;p&gt;●tryAcquire(arg)&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://tangtangit.com/_astro/1681712020967-c262aac4-5875-4bbc-a38f-4ff3f5251079.xtdh_ilp_Zorjv1.webp&quot; alt=&quot;image.png&quot; /&gt;&lt;/p&gt;
&lt;p&gt;○return false：继续推进条件，走下一个方法&lt;/p&gt;
&lt;p&gt;○return true：结束&lt;/p&gt;
&lt;p&gt;●addwaiter(Node.EXCLUSIVE)&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://tangtangit.com/_astro/1681712641453-cce21764-ca76-4f9c-93ed-f14a448d6659.BcTvqXZd_Z1U6PpX.webp&quot; alt=&quot;image.png&quot; /&gt;&lt;/p&gt;
&lt;p&gt;○注意：在双向链表中，第一个节点为虚节点（也叫做哨兵节点），其实不存储任何信息，只是占位。真正的第一个有数据的节点，是从第二个节点开始的&lt;/p&gt;
&lt;p&gt;○假如此时有线程C进入：&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://tangtangit.com/_astro/1681712960816-a469c5dc-9314-4a22-8e40-d27b14f420c9._9ajBU-N_Z10SH6P.webp&quot; alt=&quot;image.png&quot; /&gt;&lt;/p&gt;
&lt;p&gt;●acquireQueued(addWeiter(Node.EXCLUSIVE), arg)-----坐稳队列&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://tangtangit.com/_astro/1681714066377-9cf09409-c2de-4d43-a482-c3d608686f97.BtQtkfXi_Z15Uxfk.webp&quot; alt=&quot;image.png&quot; /&gt;&lt;/p&gt;
&lt;h3&gt;12.4.4 unlock()&lt;/h3&gt;
&lt;p&gt;&lt;img src=&quot;https://tangtangit.com/_astro/1681714888755-4fcc4f71-3217-4326-b545-f0b7ffb5fcf5.sRORxpCU_UTvV2.webp&quot; alt=&quot;image.png&quot; /&gt;&lt;/p&gt;
&lt;h1&gt;13. ReentrantLock、ReentrantReadWriteLock、StampedLock讲解&lt;/h1&gt;
&lt;h2&gt;13.1 关于锁的面试题&lt;/h2&gt;
&lt;p&gt;●你知道Java里面有那些锁&lt;/p&gt;
&lt;p&gt;●你说说你用过的锁，锁饥饿问题是什么？&lt;/p&gt;
&lt;p&gt;●有没有比读写锁更快的锁&lt;/p&gt;
&lt;p&gt;●StampedLock知道吗？（邮戳锁/票据锁）&lt;/p&gt;
&lt;p&gt;●ReentrantReadWriteLock有锁降级机制，你知道吗？&lt;/p&gt;
&lt;h2&gt;13.2 简单聊聊ReentrantReadWriteLock&lt;/h2&gt;
&lt;h3&gt;13.2.1 是什么？&lt;/h3&gt;
&lt;p&gt;●读写锁说明&lt;/p&gt;
&lt;p&gt;○一个资源能够被多个读线程访问，或者被一个写线程访问，但是不能同时存在读写线程&lt;/p&gt;
&lt;p&gt;●再说说演变&lt;/p&gt;
&lt;p&gt;○无锁无序-&amp;gt;加锁-&amp;gt;读写锁-&amp;gt;邮戳锁&lt;/p&gt;
&lt;p&gt;●读写锁意义和特点&lt;/p&gt;
&lt;p&gt;○它只允许读读共存，而读写和写写依然是互斥的，大多实际场景是”读/读“线程间不存在互斥关系，只有”读/写“线程或者”写/写“线程间的操作是需要互斥的，因此引入了 ReentrantReadWriteLock&lt;/p&gt;
&lt;p&gt;○一个ReentrantReadWriteLock同时只能存在一个写锁但是可以存在多个读锁，但是不能同时存在写锁和读锁，也即资源可以被多个读操作访问，或一个写操作访问，但两者不能同时进行。&lt;/p&gt;
&lt;p&gt;○只有在读多写少情景之下，读写锁才具有较高的性能体现。&lt;/p&gt;
&lt;h3&gt;13.2.2 特点&lt;/h3&gt;
&lt;p&gt;●可重入&lt;/p&gt;
&lt;p&gt;●读写兼顾&lt;/p&gt;
&lt;p&gt;●结论：一体两面，读写互斥，读读共享，读没有完成的时候其他线程写锁无法获得&lt;/p&gt;
&lt;p&gt;●锁降级：&lt;/p&gt;
&lt;p&gt;○将写锁降级为读锁------&amp;gt;遵循获取写锁、获取读锁再释放写锁的次序，写锁能够降级为读锁&lt;/p&gt;
&lt;p&gt;○如果一个线程持有了写锁，在没有释放写锁的情况下，它还可以继续获得读锁。这就是写锁的降级，降级成为了读锁。&lt;/p&gt;
&lt;p&gt;○如果释放了写锁，那么就完全转换为读锁&lt;/p&gt;
&lt;p&gt;○如果有线程在读，那么写线程是无法获取写锁的，是悲观锁的策略&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://tangtangit.com/_astro/1681719044891-56077a92-8f12-49e4-85df-bdffbc21b424.CrudczL0_Z1mTCno.webp&quot; alt=&quot;image.png&quot; /&gt;&lt;/p&gt;
&lt;h2&gt;13.3 面试题：有没有比读写锁更快的锁？&lt;/h2&gt;
&lt;h2&gt;13.4 邮戳锁StampedLock&lt;/h2&gt;
&lt;h3&gt;13.4.1 是什么？&lt;/h3&gt;
&lt;p&gt;StampedLock是JDK1.8中新增的一个读写锁，也是对JDK1.5中的读写锁ReentrantReadWriteLock的优化&lt;/p&gt;
&lt;p&gt;stamp 代表了锁的状态。当stamp返回零时，表示线程获取锁失败，并且当释放锁或者转换锁的时候，都要传入最初获取的stamp值。&lt;/p&gt;
&lt;h3&gt;13.4.2 它是由饥饿问题引出&lt;/h3&gt;
&lt;p&gt;●锁饥饿问题：&lt;/p&gt;
&lt;p&gt;○ReentrantReadWriteLock实现了读写分离，但是一旦读操作比较多的时候，想要获取写锁就变得比较困难了，因此当前有可能会一直存在读锁，而无法获得写锁。&lt;/p&gt;
&lt;p&gt;●如何解决锁饥饿问题：&lt;/p&gt;
&lt;p&gt;○使用”公平“策略可以一定程度上缓解这个问题&lt;/p&gt;
&lt;p&gt;○使用”公平“策略是以牺牲系统吞吐量为代价的&lt;/p&gt;
&lt;p&gt;○StampedLock类的乐观读锁方式---&amp;gt;采取乐观获取锁，其他线程尝试获取写锁时不会被阻塞，在获取乐观读锁后，还需要对结果进行校验&lt;/p&gt;
&lt;h3&gt;13.4.3 StampedLock的特点&lt;/h3&gt;
&lt;p&gt;●所有获取锁的方法，都返回一个邮戳，stamp为零表示失败，其余都表示成功&lt;/p&gt;
&lt;p&gt;●所有释放锁的方法，都需要一个邮戳，这个stamp必须是和成功获取锁时得到的stamp一致&lt;/p&gt;
&lt;p&gt;●StampedLock是不可重入的，危险（如果一个线程已经持有了写锁，在去获取写锁的话会造成死锁）&lt;/p&gt;
&lt;p&gt;●StampedLock有三种访问模式：&lt;/p&gt;
&lt;p&gt;○Reading（读模式悲观）：功能和ReentrantReadWriteLock的读锁类似&lt;/p&gt;
&lt;p&gt;○Writing（写模式）：功能和ReentrantReadWriteLock的写锁类似&lt;/p&gt;
&lt;p&gt;○Optimistic reading（乐观读模式）：无锁机制，类似与数据库中的乐观锁，支持读写并发，很乐观认为读时没人修改，假如被修改在实现升级为悲观读模式&lt;/p&gt;
&lt;p&gt;●一句话：读的过程中也允许写锁介入&lt;/p&gt;
&lt;h3&gt;13.4.5 乐观读模式Code演示&lt;/h3&gt;
&lt;p&gt;●传统的读写锁模式----读的时候写锁不能获取&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;传统的读写锁模式案例演示&lt;/p&gt;
&lt;/blockquote&gt;
&lt;pre&gt;&lt;code&gt;/**
 * @author Guanghao Wei
 * @create 2023-04-17 16:41
 */
public class StampedLockDemo {
    static int number = 37;
    static StampedLock stampedLock = new StampedLock();

    public void write() {
        long stamp = stampedLock.writeLock();
        System.out.println(Thread.currentThread().getName() + &quot;\t&quot; + &quot;写线程准备修改&quot;);
        try {
            number = number + 13;
        } finally {
            stampedLock.unlockWrite(stamp);
        }
        System.out.println(Thread.currentThread().getName() + &quot;\t&quot; + &quot;写线程结束修改&quot;);
    }

    public void read() {
        long stamp = stampedLock.readLock();
        System.out.println(Thread.currentThread().getName() + &quot;\t&quot; + &quot; come in readLock codeBlock&quot;);
        for (int i = 0; i &amp;lt; 4; i++) {
            try {
                TimeUnit.SECONDS.sleep(1);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println(Thread.currentThread().getName() + &quot;\t&quot; + &quot; 正在读取中&quot;);
        }
        try {
            int result = number;
            System.out.println(Thread.currentThread().getName() + &quot;\t&quot; + &quot;获得成员变量值result: &quot; + result);
            System.out.println(&quot;写线程没有修改成功，读锁时候写锁无法介入，传统的读写互斥&quot;);
        } finally {
            stampedLock.unlockRead(stamp);
        }

    }

    public static void main(String[] args) {
        StampedLockDemo resource = new StampedLockDemo();
        new Thread(() -&amp;gt; {
            resource.read();
        }, &quot;readThread&quot;).start();

        try {
            TimeUnit.SECONDS.sleep(1);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }

        new Thread(() -&amp;gt; {
            System.out.println(Thread.currentThread().getName()+&quot;\t&quot;+&quot; come in&quot;);
            resource.write();
        }, &quot;writeThread&quot;).start();
    }
}
/**
 * readThread	 come in readLock codeBlock
 * readThread	 正在读取中
 * writeThread	 come in
 * readThread	 正在读取中
 * readThread	 正在读取中
 * readThread	 正在读取中
 * readThread	获得成员变量值result: 37
 * 写线程没有修改成功，读锁时候写锁无法介入，传统的读写互斥
 * writeThread	写线程准备修改
 * writeThread	写线程结束修改
 */
&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote&gt;
&lt;ul&gt;
&lt;li&gt;乐观读模式----读的过程中也允许写锁介入&lt;/li&gt;
&lt;/ul&gt;
&lt;/blockquote&gt;
&lt;pre&gt;&lt;code&gt;/**
 * @author Guanghao Wei
 * @create 2023-04-17 16:41
 */
public class StampedLockDemo {
    static int number = 37;
    static StampedLock stampedLock = new StampedLock();

    public void write() {
        long stamp = stampedLock.writeLock();
        System.out.println(Thread.currentThread().getName() + &quot;\t&quot; + &quot;写线程准备修改&quot;);
        try {
            number = number + 13;
        } finally {
            stampedLock.unlockWrite(stamp);
        }
        System.out.println(Thread.currentThread().getName() + &quot;\t&quot; + &quot;写线程结束修改&quot;);
    }

    public void read() {
        long stamp = stampedLock.tryOptimisticRead();

        int result = number;

        System.out.println(&quot;4秒前 stampedLock.validate方法值（true 无修改 false有修改）&quot; + &quot;\t&quot; + stampedLock.validate(stamp));
        for (int i = 0; i &amp;lt; 4; i++) {
            try {
                TimeUnit.SECONDS.sleep(1);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println(Thread.currentThread().getName() + &quot;\t&quot; + &quot; 正在读取....&quot; + i + &quot;秒后&quot; + &quot;stampedLock.validate方法值（true 无修改 false有修改）&quot; + &quot;\t&quot; + stampedLock.validate(stamp));
        }
        if (!stampedLock.validate(stamp)) {
            System.out.println(&quot;有人修改----------有写操作&quot;);
            stamp = stampedLock.readLock();
            try {
                System.out.println(&quot;从乐观读升级为悲观读&quot;);
                result = number;
                System.out.println(&quot;重新悲观读后result：&quot; + result);
            } finally {
                stampedLock.unlockRead(stamp);
            }
        }
        System.out.println(Thread.currentThread().getName() + &quot;\t&quot; + &quot;finally value: &quot; + result);

    }


    public static void main(String[] args) {
        StampedLockDemo resource = new StampedLockDemo();
        new Thread(() -&amp;gt; {
            resource.read();
        }, &quot;readThread&quot;).start();

        try {
            TimeUnit.SECONDS.sleep(2);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }

        new Thread(() -&amp;gt; {
            System.out.println(Thread.currentThread().getName() + &quot;\t&quot; + &quot; come in&quot;);
            resource.write();
        }, &quot;writeThread&quot;).start();
    }
}
/**
 * 4秒前 stampedLock.validate方法值（true 无修改 false有修改）	true
 * readThread	 正在读取....0秒后stampedLock.validate方法值（true 无修改 false有修改）	true
 * readThread	 正在读取....1秒后stampedLock.validate方法值（true 无修改 false有修改）	true
 * writeThread	 come in
 * writeThread	写线程准备修改
 * writeThread	写线程结束修改
 * readThread	 正在读取....2秒后stampedLock.validate方法值（true 无修改 false有修改）	false
 * readThread	 正在读取....3秒后stampedLock.validate方法值（true 无修改 false有修改）	false
 * 有人修改----------有写操作
 * 从乐观读升级为悲观读
 * 重新悲观读后result：50
 * readThread	finally value: 50
 */
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;13.4.6 StampedLock的缺点&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;StampedLock不支持重入，没有Re开头&lt;/li&gt;
&lt;li&gt;StampedLock的悲观读锁和写锁都不支持条件变量，这个也需要注意&lt;/li&gt;
&lt;li&gt;使用StampedLock一定不要调用中断操作，即不要调用interrupt()方法&lt;/li&gt;
&lt;/ul&gt;
</content:encoded></item><item><title>Java反射机制详解：Class、Field、Method与Constructor</title><link>https://tangtangit.com/posts/java%E6%A0%B8%E5%BF%83-03-%E5%8F%8D%E5%B0%84/</link><guid isPermaLink="true">https://tangtangit.com/posts/java%E6%A0%B8%E5%BF%83-03-%E5%8F%8D%E5%B0%84/</guid><description>系统讲解Java反射机制的核心类（Class/Field/Method/Constructor）的常用方法，通过Book示例演示反射创建对象、访问私有成员，并分享通联支付项目中的反射架构实践。</description><pubDate>Mon, 12 Jan 2026 00:00:00 GMT</pubDate><content:encoded>&lt;h1&gt;Java高级特性——反射&lt;/h1&gt;
&lt;h2&gt;定义&lt;/h2&gt;
&lt;p&gt;&lt;strong&gt;JAVA反射机制是在运行状态中，对于任意一个类，都能够知道这个类的所有属性和方法；对于任意一个对象，都能够调用它的任意方法和属性；这种动态获取信息以及动态调用对象方法的功能称为java语言的反射机制&lt;/strong&gt;&lt;/p&gt;
&lt;h2&gt;用途&lt;/h2&gt;
&lt;p&gt;在日常的第三方应用开发过程中，经常会遇到某个类的某个成员变量、方法或是属性是私有的或是只对系统应用开放，这时候就可以利用Java的反射机制通过反射来获取所需的私有成员或是方法。当然，也不是所有的都适合反射，之前就遇到一个案例，通过反射得到的结果与预期不符。阅读源码发现，经过层层调用后在最终返回结果的地方对应用的权限进行了校验，对于没有权限的应用返回值是没有意义的缺省值，否则返回实际值起到保护用户的隐私目的。&lt;/p&gt;
&lt;h2&gt;反射机制的相关类&lt;/h2&gt;
&lt;p&gt;与Java反射相关的类如下：&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;类名&lt;/th&gt;
&lt;th&gt;用途&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Class类&lt;/td&gt;
&lt;td&gt;代表类的实体，在运行的Java应用程序中表示类和接口&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Field类&lt;/td&gt;
&lt;td&gt;代表类的成员变量（成员变量也称为类的属性）&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Method类&lt;/td&gt;
&lt;td&gt;代表类的方法&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Constructor类&lt;/td&gt;
&lt;td&gt;代表类的构造方法&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;h1&gt;Class类&lt;/h1&gt;
&lt;p&gt;&lt;a href=&quot;https://developer.android.google.cn/reference/java/lang/Class&quot;&gt;Class&lt;/a&gt;代表类的实体，在运行的Java应用程序中表示类和接口。在这个类中提供了很多有用的方法，这里对他们简单的分类介绍。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;获得类相关的方法&lt;/strong&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;方法&lt;/th&gt;
&lt;th&gt;用途&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;asSubclass(Class&amp;lt;U&amp;gt; clazz)&lt;/td&gt;
&lt;td&gt;把传递的类的对象转换成代表其子类的对象&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Cast&lt;/td&gt;
&lt;td&gt;把对象转换成代表类或是接口的对象&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;getClassLoader()&lt;/td&gt;
&lt;td&gt;获得类的加载器&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;getClasses()&lt;/td&gt;
&lt;td&gt;返回一个数组，数组中包含该类中所有公共类和接口类的对象&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;getDeclaredClasses()&lt;/td&gt;
&lt;td&gt;返回一个数组，数组中包含该类中所有类和接口类的对象&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;forName(String className)&lt;/td&gt;
&lt;td&gt;根据类名返回类的对象&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;getName()&lt;/td&gt;
&lt;td&gt;获得类的完整路径名字&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;newInstance()&lt;/td&gt;
&lt;td&gt;创建类的实例&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;getPackage()&lt;/td&gt;
&lt;td&gt;获得类的包&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;getSimpleName()&lt;/td&gt;
&lt;td&gt;获得类的名字&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;getSuperclass()&lt;/td&gt;
&lt;td&gt;获得当前类继承的父类的名字&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;getInterfaces()&lt;/td&gt;
&lt;td&gt;获得当前类实现的类或是接口&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;获得类中属性相关的方法&lt;/strong&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;方法&lt;/th&gt;
&lt;th&gt;用途&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;getField(String name)&lt;/td&gt;
&lt;td&gt;获得某个公有的属性对象&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;getFields()&lt;/td&gt;
&lt;td&gt;获得所有公有的属性对象&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;getDeclaredField(String name)&lt;/td&gt;
&lt;td&gt;获得某个属性对象&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;getDeclaredFields()&lt;/td&gt;
&lt;td&gt;获得所有属性对象&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;获得类中注解相关的方法&lt;/strong&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;方法&lt;/th&gt;
&lt;th&gt;用途&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;getAnnotation(Class&amp;lt;A&amp;gt; annotationClass)&lt;/td&gt;
&lt;td&gt;返回该类中与参数类型匹配的公有注解对象&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;getAnnotations()&lt;/td&gt;
&lt;td&gt;返回该类所有的公有注解对象&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;getDeclaredAnnotation(Class&amp;lt;A&amp;gt; annotationClass)&lt;/td&gt;
&lt;td&gt;返回该类中与参数类型匹配的所有注解对象&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;getDeclaredAnnotations()&lt;/td&gt;
&lt;td&gt;返回该类所有的注解对象&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;获得类中构造器相关的方法&lt;/strong&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;方法&lt;/th&gt;
&lt;th&gt;用途&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;getConstructor(Class...&amp;lt;?&amp;gt; parameterTypes)&lt;/td&gt;
&lt;td&gt;获得该类中与参数类型匹配的公有构造方法&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;getConstructors()&lt;/td&gt;
&lt;td&gt;获得该类的所有公有构造方法&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;getDeclaredConstructor(Class...&amp;lt;?&amp;gt; parameterTypes)&lt;/td&gt;
&lt;td&gt;获得该类中与参数类型匹配的构造方法&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;getDeclaredConstructors()&lt;/td&gt;
&lt;td&gt;获得该类所有构造方法&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;获得类中方法相关的方法&lt;/strong&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;方法&lt;/th&gt;
&lt;th&gt;用途&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;getMethod(String name, Class...&amp;lt;?&amp;gt; parameterTypes)&lt;/td&gt;
&lt;td&gt;获得该类某个公有的方法&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;getMethods()&lt;/td&gt;
&lt;td&gt;获得该类所有公有的方法&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;getDeclaredMethod(String name, Class...&amp;lt;?&amp;gt; parameterTypes)&lt;/td&gt;
&lt;td&gt;获得该类某个方法&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;getDeclaredMethods()&lt;/td&gt;
&lt;td&gt;获得该类所有方法&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;类中其他重要的方法&lt;/strong&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;方法&lt;/th&gt;
&lt;th&gt;用途&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;isAnnotation()&lt;/td&gt;
&lt;td&gt;如果是注解类型则返回true&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;isAnnotationPresent(Class&amp;lt;? extends Annotation&amp;gt; annotationClass)&lt;/td&gt;
&lt;td&gt;如果是指定类型注解类型则返回true&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;isAnonymousClass()&lt;/td&gt;
&lt;td&gt;如果是匿名类则返回true&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;isArray()&lt;/td&gt;
&lt;td&gt;如果是一个数组类则返回true&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;isEnum()&lt;/td&gt;
&lt;td&gt;如果是枚举类则返回true&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;isInstance(Object obj)&lt;/td&gt;
&lt;td&gt;如果obj是该类的实例则返回true&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;isInterface()&lt;/td&gt;
&lt;td&gt;如果是接口类则返回true&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;isLocalClass()&lt;/td&gt;
&lt;td&gt;如果是局部类则返回true&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;isMemberClass()&lt;/td&gt;
&lt;td&gt;如果是内部类则返回true&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;h1&gt;Field类&lt;/h1&gt;
&lt;p&gt;&lt;a href=&quot;https://developer.android.google.cn/reference/java/lang/reflect/Field&quot;&gt;Field&lt;/a&gt;代表类的成员变量（成员变量也称为类的属性）。&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;方法&lt;/th&gt;
&lt;th&gt;用途&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;equals(Object obj)&lt;/td&gt;
&lt;td&gt;属性与obj相等则返回true&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;get(Object obj)&lt;/td&gt;
&lt;td&gt;获得obj中对应的属性值&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;set(Object obj, Object value)&lt;/td&gt;
&lt;td&gt;设置obj中对应属性值&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;h1&gt;Method类&lt;/h1&gt;
&lt;p&gt;&lt;a href=&quot;https://developer.android.google.cn/reference/java/lang/reflect/Method&quot;&gt;Method&lt;/a&gt;代表类的方法。&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;方法&lt;/th&gt;
&lt;th&gt;用途&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;invoke(Object obj, Object... args)&lt;/td&gt;
&lt;td&gt;传递object对象及参数调用该对象对应的方法&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;h1&gt;Constructor类&lt;/h1&gt;
&lt;p&gt;&lt;a href=&quot;https://developer.android.google.cn/reference/java/lang/reflect/Constructor&quot;&gt;Constructor&lt;/a&gt;代表类的构造方法。&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;方法&lt;/th&gt;
&lt;th&gt;用途&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;newInstance(Object... initargs)&lt;/td&gt;
&lt;td&gt;根据传递的参数创建类的对象&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;h1&gt;示例&lt;/h1&gt;
&lt;p&gt;为了演示反射的使用，首先构造一个与书籍相关的model——Book.java，然后通过反射方法示例创建对象、反射私有构造方法、反射私有属性、反射私有方法，最后给出两个比较复杂的反射示例——获得当前ZenMode和关机Shutdown。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;被反射类Book.java&lt;/strong&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;public class Book{
    private final static String TAG = &quot;BookTag&quot;;

    private String name;
    private String author;

    @Override
    public String toString() {
        return &quot;Book{&quot; +
                &quot;name=&apos;&quot; + name + &apos;\&apos;&apos; +
                &quot;, author=&apos;&quot; + author + &apos;\&apos;&apos; +
                &apos;}&apos;;
    }

    public Book() {
    }

    private Book(String name, String author) {
        this.name = name;
        this.author = author;
    }

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    public String getAuthor() {
        return author;
    }

    public void setAuthor(String author) {
        this.author = author;
    }

    private String declaredMethod(int index) {
        String string = null;
        switch (index) {
            case 0:
                string = &quot;I am declaredMethod 1 !&quot;;
                break;
            case 1:
                string = &quot;I am declaredMethod 2 !&quot;;
                break;
            default:
                string = &quot;I am declaredMethod 1 !&quot;;
        }

        return string;
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;反射逻辑封装在ReflectClass.java&lt;/strong&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;public class ReflectClass {
    private final static String TAG = &quot;peter.log.ReflectClass&quot;;

    // 创建对象
    public static void reflectNewInstance() {
        try {
            Class&amp;lt;?&amp;gt; classBook = Class.forName(&quot;com.android.peter.reflectdemo.Book&quot;);
            Object objectBook = classBook.newInstance();
            Book book = (Book) objectBook;
            book.setName(&quot;Android进阶之光&quot;);
            book.setAuthor(&quot;刘望舒&quot;);
            Log.d(TAG,&quot;reflectNewInstance book = &quot; + book.toString());
        } catch (Exception ex) {
            ex.printStackTrace();
        }
    }

    // 反射私有的构造方法
    public static void reflectPrivateConstructor() {
        try {
            Class&amp;lt;?&amp;gt; classBook = Class.forName(&quot;com.android.peter.reflectdemo.Book&quot;);
            Constructor&amp;lt;?&amp;gt; declaredConstructorBook = classBook.getDeclaredConstructor(String.class,String.class);
            //将构造方法设置成可以访问的
            declaredConstructorBook.setAccessible(true);
            Object objectBook = declaredConstructorBook.newInstance(&quot;Android开发艺术探索&quot;,&quot;任玉刚&quot;);
            Book book = (Book) objectBook;
            Log.d(TAG,&quot;reflectPrivateConstructor book = &quot; + book.toString());
        } catch (Exception ex) {
            ex.printStackTrace();
        }
    }

    // 反射私有属性
    public static void reflectPrivateField() {
        try {
            Class&amp;lt;?&amp;gt; classBook = Class.forName(&quot;com.android.peter.reflectdemo.Book&quot;);
            Object objectBook = classBook.newInstance();
            Field fieldTag = classBook.getDeclaredField(&quot;TAG&quot;);
            //将私有属性设置成可以访问的
            fieldTag.setAccessible(true);
            String tag = (String) fieldTag.get(objectBook);
            Log.d(TAG,&quot;reflectPrivateField tag = &quot; + tag);
        } catch (Exception ex) {
            ex.printStackTrace();
        }
    }

    // 反射私有方法
    public static void reflectPrivateMethod() {
        try {
            Class&amp;lt;?&amp;gt; classBook = Class.forName(&quot;com.android.peter.reflectdemo.Book&quot;);
            Method methodBook = classBook.getDeclaredMethod(&quot;declaredMethod&quot;,int.class);
            //将私有方法设置成可以访问的
            methodBook.setAccessible(true);
            Object objectBook = classBook.newInstance();
            String string = (String) methodBook.invoke(objectBook,0);

            Log.d(TAG,&quot;reflectPrivateMethod string = &quot; + string);
        } catch (Exception ex) {
            ex.printStackTrace();
        }
    }

    // 获得系统Zenmode值
    public static int getZenMode() {
        int zenMode = -1;
        try {
            Class&amp;lt;?&amp;gt; cServiceManager = Class.forName(&quot;android.os.ServiceManager&quot;);
            Method mGetService = cServiceManager.getMethod(&quot;getService&quot;, String.class);
            Object oNotificationManagerService = mGetService.invoke(null, Context.NOTIFICATION_SERVICE);
            Class&amp;lt;?&amp;gt; cINotificationManagerStub = Class.forName(&quot;android.app.INotificationManager$Stub&quot;);
            Method mAsInterface = cINotificationManagerStub.getMethod(&quot;asInterface&quot;,IBinder.class);
            Object oINotificationManager = mAsInterface.invoke(null,oNotificationManagerService);
            Method mGetZenMode = cINotificationManagerStub.getMethod(&quot;getZenMode&quot;);
            zenMode = (int) mGetZenMode.invoke(oINotificationManager);
        } catch (Exception ex) {
            ex.printStackTrace();
        }

        return zenMode;
    }

    // 关闭手机
    public static void shutDown() {
        try {
            Class&amp;lt;?&amp;gt; cServiceManager = Class.forName(&quot;android.os.ServiceManager&quot;);
            Method mGetService = cServiceManager.getMethod(&quot;getService&quot;,String.class);
            Object oPowerManagerService = mGetService.invoke(null,Context.POWER_SERVICE);
            Class&amp;lt;?&amp;gt; cIPowerManagerStub = Class.forName(&quot;android.os.IPowerManager$Stub&quot;);
            Method mShutdown = cIPowerManagerStub.getMethod(&quot;shutdown&quot;,boolean.class,String.class,boolean.class);
            Method mAsInterface = cIPowerManagerStub.getMethod(&quot;asInterface&quot;,IBinder.class);
            Object oIPowerManager = mAsInterface.invoke(null,oPowerManagerService);
            mShutdown.invoke(oIPowerManager,true,null,true);

        } catch (Exception ex) {
            ex.printStackTrace();
        }
    }

    public static void shutdownOrReboot(final boolean shutdown, final boolean confirm) {
        try {
            Class&amp;lt;?&amp;gt; ServiceManager = Class.forName(&quot;android.os.ServiceManager&quot;);
            // 获得ServiceManager的getService方法
            Method getService = ServiceManager.getMethod(&quot;getService&quot;, java.lang.String.class);
            // 调用getService获取RemoteService
            Object oRemoteService = getService.invoke(null, Context.POWER_SERVICE);
            // 获得IPowerManager.Stub类
            Class&amp;lt;?&amp;gt; cStub = Class.forName(&quot;android.os.IPowerManager$Stub&quot;);
            // 获得asInterface方法
            Method asInterface = cStub.getMethod(&quot;asInterface&quot;, android.os.IBinder.class);
            // 调用asInterface方法获取IPowerManager对象
            Object oIPowerManager = asInterface.invoke(null, oRemoteService);
            if (shutdown) {
                // 获得shutdown()方法
                Method shutdownMethod = oIPowerManager.getClass().getMethod(
                        &quot;shutdown&quot;, boolean.class, String.class, boolean.class);
                // 调用shutdown()方法
                shutdownMethod.invoke(oIPowerManager, confirm, null, false);
            } else {
                // 获得reboot()方法
                Method rebootMethod = oIPowerManager.getClass().getMethod(&quot;reboot&quot;,
                        boolean.class, String.class, boolean.class);
                // 调用reboot()方法
                rebootMethod.invoke(oIPowerManager, confirm, null, false);
            }
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;调用相应反射逻辑方法&lt;/strong&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;        try {
            // 创建对象
            ReflectClass.reflectNewInstance();

            // 反射私有的构造方法
            ReflectClass.reflectPrivateConstructor();

            // 反射私有属性
            ReflectClass.reflectPrivateField();

            // 反射私有方法
            ReflectClass.reflectPrivateMethod();
        } catch (Exception ex) {
            ex.printStackTrace();
        }

        Log.d(TAG,&quot; zenmode = &quot; + ReflectClass.getZenMode());
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Log输出结果如下：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;08-27 15:11:37.999 11987-11987/com.android.peter.reflectdemo D/peter.log.ReflectClass: reflectNewInstance book = Book{name=&apos;Android进阶之光&apos;, author=&apos;刘望舒&apos;}
08-27 15:11:38.000 11987-11987/com.android.peter.reflectdemo D/peter.log.ReflectClass: reflectPrivateConstructor book = Book{name=&apos;Android开发艺术探索&apos;, author=&apos;任玉刚&apos;}
08-27 15:11:38.000 11987-11987/com.android.peter.reflectdemo D/peter.log.ReflectClass: reflectPrivateField tag = BookTag
08-27 15:11:38.000 11987-11987/com.android.peter.reflectdemo D/peter.log.ReflectClass: reflectPrivateMethod string = I am declaredMethod 1 !
08-27 15:11:38.004 11987-11987/com.android.peter.reflectdemo D/peter.log.ReflectDemo:  zenmode = 0
&lt;/code&gt;&lt;/pre&gt;
&lt;h1&gt;总结&lt;/h1&gt;
&lt;h1&gt;总结反射案例用到的方法：&lt;/h1&gt;
&lt;p&gt;&lt;strong&gt;1.实例化Class类对象&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt; Class&amp;lt;?&amp;gt; classu = Class.forName(&quot;reflect.User&quot;);
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;2.获得某个类的全部属性&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;Field[] userFields = classu.getDeclaredFields();
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;3.取消权限的访问控制&lt;/strong&gt;(可以通过  获取私有的属性值)&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;userFields[i].setAccessible(true);
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;4.得到属性的字段名&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;userFields[i].getName()
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;5.将指定对象变量上(user) 此Field对象表示的字段设置为指定的新值。&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;userFields[i].set(user,map.get(s));
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;6.通过反射实例化类的对象&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;User user = (User) classu.newInstance();
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;7.通过反射机制调用某个类的方法&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt; Method method = classu.getMethod();//参数：方法名  若方法有参数，加上参数类型的class对象
 method.invoke();//参数：实例化出来的类对象，若方法有参数，加上值value
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;本文列举了反射机制使用过程中常用的、重要的一些类及其方法，更多信息和用法需要近一步的阅读Google提供的相关文档和示例。&lt;/p&gt;
&lt;p&gt;在阅读Class类文档时发现一个特点，以通过反射获得Method对象为例，一般会提供四种方法，getMethod(parameterTypes)、getMethods()、getDeclaredMethod(parameterTypes)和getDeclaredMethods()。getMethod(parameterTypes)用来获取某个公有的方法的对象，getMethods()获得该类所有公有的方法，getDeclaredMethod(parameterTypes)获得该类某个方法，getDeclaredMethods()获得该类所有方法。**带有Declared修饰的方法可以反射到私有的方法，没有Declared修饰的只能用来反射公有的方法。**其他的Annotation、Field、Constructor也是如此。&lt;/p&gt;
&lt;p&gt;在ReflectClass类中还提供了两种反射PowerManager.shutdown()的方法，在调用的时候会输出如下log，提示没有相关权限。之前在项目中尝试反射其他方法的时候还遇到过有权限和没权限返回的值不一样的情况。&lt;strong&gt;如果源码中明确进行了权限验证，而你的应用又无法获得这个权限的话，建议就不要浪费时间反射了。&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt; W/System.err: java.lang.reflect.InvocationTargetException
 W/System.err:     at java.lang.reflect.Method.invoke(Native Method)
 W/System.err:     at .ReflectClass.shutDown(ReflectClass.java:104)
 W/System.err:     at .MainActivity$1.onClick(MainActivity.java:25)
 W/System.err:     at android.view.View.performClick(View.java:6259)
 W/System.err:     at android.view.View$PerformClick.run(View.java:24732)
 W/System.err:     at android.os.Handler.handleCallback(Handler.java:789)
 W/System.err:     at android.os.Handler.dispatchMessage(Handler.java:98)
 W/System.err:     at android.os.Looper.loop(Looper.java:164)
 W/System.err:     at android.app.ActivityThread.main(ActivityThread.java:6592)
 W/System.err:     at java.lang.reflect.Method.invoke(Native Method)
 W/System.err:     at com.android.internal.os.Zygote$MethodAndArgsCaller.run(Zygote.java:240)
 W/System.err:     at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:769)
 W/System.err: Caused by: java.lang.SecurityException: Neither user 10224 nor current process has android.permission.REBOOT.
 W/System.err:     at android.os.Parcel.readException(Parcel.java:1942)
 W/System.err:     at android.os.Parcel.readException(Parcel.java:1888)
 W/System.err:     at android.os.IPowerManager$Stub$Proxy.shutdown(IPowerManager.java:787)
 W/System.err:  ... 12 more
&lt;/code&gt;&lt;/pre&gt;
&lt;h1&gt;项目中反射的使用案例1&lt;/h1&gt;
&lt;h2&gt;1、主要用到类介绍&lt;/h2&gt;
&lt;h3&gt;1.1、方法参数&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;@Data
@Builder
public class Command {
    private String method;
    private JSONObject params;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;1.2、方法集合接口&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;@Validated
public interface Service {

    /**
     * 用户注册
     *
     * @param bizUserId 用户ID
     * @return 返回信息
     * @throws IOException 异常
     */
    String createMember(@NotNull String bizUserId) throws IOException;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;1.3、方法接口实现类&lt;/h3&gt;
&lt;blockquote&gt;
&lt;p&gt;调用的发送请求的方法，RequestFactory去构建出一个统一参数对象RequestDTO&lt;/p&gt;
&lt;/blockquote&gt;
&lt;pre&gt;&lt;code&gt;public class ServiceImpl implements Service {

    @Override
    public String createMember(String bizUserId) throws IOException {
        return new YunHttpClient().post(RequestFactory.registerMember(bizUserId, RegisterMemberRq.Source.Mobile));
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;1.4、发送请求方法&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;/**
 * YunHttpClient
 *
 * @author zhengyong
 * @date 2019/5/16
 */
@Slf4j
public class YunHttpClient extends BaseClient {

    /**
     * 封装的post请求方法
     * @param entity  请求体
     * @param &amp;lt;T&amp;gt;     请求参数
     * @return        返回信息
     * @throws IOException  io异常
     */
    public &amp;lt;T&amp;gt; String post(RequestDTO&amp;lt;T&amp;gt; entity) throws IOException {
        StringBuilder result = new StringBuilder();
        BufferedWriter writer = null;
        BufferedReader reader = null;
        log.info(&quot;发送通联请求参数，{}&quot;, JSON.toJSONString(entity));
        String data;
        try {
          //自定义编码签名
            data = urlEncode(entity);
        } catch (Exception e) {
            throw new IOException(e);
        }
        try {
            URL httpUrl = new URL(TonglianContext.getServerUrl());
            HttpURLConnection connection = (HttpURLConnection)httpUrl.openConnection();
            connection.setRequestProperty(&quot;Connection&quot;, &quot;keep-alive&quot;);
            connection.setRequestProperty(&quot;Content-Type&quot;, &quot;application/x-www-form-urlencoded;charset=utf-8&quot;);
            connection.setRequestMethod(&quot;POST&quot;);
            connection.setDoOutput(true);
            connection.setDoInput(true);
            writer = new BufferedWriter(new OutputStreamWriter(connection.getOutputStream(), StandardCharsets.UTF_8));
            writer.write(data);
            writer.flush();
            reader = new BufferedReader(new InputStreamReader(connection.getInputStream(), StandardCharsets.UTF_8));
            String line;
            while ((line = reader.readLine()) != null) {
                result.append(line);
            }
        } finally {
            IOUtils.close(writer);
            IOUtils.close(reader);
        }
        log.info(&quot;【通联同步返回参数】：{}&quot;,result.toString());
        ResponseDTO resp = JSON.parseObject(result.toString(), ResponseDTO.class);
        resp.verify();
        if (resp.isSuccees()) {
            return resp.getSignedValue();
        } else {
            throw new ApiException(resp.getErrorCode(), resp.getMessage());
        }
    }
  
      /**
     * 封装的get请求方法
     * @param entity  请求体
     * @param &amp;lt;T&amp;gt;     请求参数
     * @return        返回信息
     * @throws IOException  io异常
     */
    public &amp;lt;T&amp;gt; String get(RequestDTO&amp;lt;T&amp;gt; entity) throws IOException {
        StringBuilder result = new StringBuilder();
        BufferedReader reader = null;
        String data;
        try {
            data = urlEncode(entity);
        } catch (Exception e) {
            throw new IOException(e);
        }
        try {
            URL httpUrl = new URL(TonglianContext.getServerUrl() + &quot;?&quot; + data);
            HttpURLConnection connection = (HttpURLConnection)httpUrl.openConnection();
            connection.setRequestProperty(&quot;Connection&quot;, &quot;keep-alive&quot;);
            connection.setRequestProperty(&quot;Content-Type&quot;, &quot;application/x-www-form-urlencoded;charset=utf-8&quot;);
            connection.setRequestMethod(&quot;GET&quot;);
            connection.setDoOutput(true);
            reader = new BufferedReader(new InputStreamReader(connection.getInputStream(), StandardCharsets.UTF_8));

            String line;
            while ((line = reader.readLine()) != null) {
                result.append(line);
            }
        } finally {
            IOUtils.close(reader);
        }
        ResponseDTO resp = JSON.parseObject(result.toString(), ResponseDTO.class).verify();
        if (resp.isSuccees()) {
            return resp.getSignedValue();
        } else {
            throw new ApiException(resp.getErrorCode(), resp.getMessage());
        }
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;1.5、根据实际情况封装的请求对象&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;/**
 * RequestDTO  请求DTO
 *
 * @author zhengyong
 * @date 2019/5/16
 */
@Data
@Builder
public class RequestDTO&amp;lt;T&amp;gt; {
    /**
     * 通联分配id
     */
    private String sysid;
    /**
     * 时间戳
     */
    private Date timestamp;
    /**
     * 版本号
     */
    private String v;
    /**
     * 请求参数
     */
    private RqWrapper&amp;lt;T&amp;gt; req;

}

/**
 * RqWrapper
 * 请求包装器
 * @author  zhengyong
 * @date 2019/5/20
 */
@Data
@Builder
public class RqWrapper&amp;lt;T&amp;gt; {
    /**
     * 服务对象
     */
    private String service;
    /**
     * 调用方法
     */
    private String method;
    /**
     * 参数
     */
    private T param;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;1.6、RequestFactory&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;public class RequestFactory {

    private RequestFactory() {

    }

    /**
     * 用户注册
     *
     * @param bizUserId 业务系统用户ID
     * @param type    类型
     * @return        注册信息
     */
    public static RequestDTO&amp;lt;RegisterMemberRq&amp;gt; registerMember(String bizUserId, RegisterMemberRq.Source type) {
        //构建指定类型的请求参数
        RegisterMemberRq member = RegisterMemberRq.builder().bizUserId(bizUserId)
                .memberType(RegisterMemberRq.MenberType.Personal.getType()).source(type.getSource()).build();
        return build(member, &quot;createMember&quot;, &quot;MemberService&quot;);
    }
  
    private static &amp;lt;T&amp;gt; RequestDTO&amp;lt;T&amp;gt; build(T t, String method, String service) {
        //组装 RqWrapper，并设置到 RequestDTO中
        RqWrapper&amp;lt;T&amp;gt; re = RqWrapper.&amp;lt;T&amp;gt;builder().method(method).service(service).param(t).build();
        return RequestDTO.&amp;lt;T&amp;gt;builder().sysid(TonglianContext.getSysId()).timestamp(new Date()).v(&quot;2.0&quot;).req(re).build();
    }
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;1.7、TonglianAdapter 适配器&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;@Validated
public interface TonglianAdapter {

    /**
     * 执行方法
     * @param cmd 命令
     * @return 结果
     */
    String excute(Command cmd);
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;1.8、TonglianAdapterImpl&lt;/h3&gt;
&lt;blockquote&gt;
&lt;p&gt;1、创建 时，在构造器中注入了 service&lt;/p&gt;
&lt;p&gt;2、使用反射，执行command参数中指定的方法&lt;/p&gt;
&lt;p&gt;3、类型转换器 中去取出不同类型的参数&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;jo.getJSONArray(name)&lt;/li&gt;
&lt;li&gt;jo.getJSONObject(name)&lt;/li&gt;
&lt;li&gt;jo.getString(name)&lt;/li&gt;
&lt;li&gt;jo.getLong(name)&lt;/li&gt;
&lt;li&gt;jo.get(name)&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;4、调用反射，执行，返回string&lt;/p&gt;
&lt;/blockquote&gt;
&lt;pre&gt;&lt;code&gt;&amp;lt;R&amp;gt; R collect(Supplier&amp;lt;R&amp;gt; supplier,
                  BiConsumer&amp;lt;R, ? super T&amp;gt; accumulator,
                  BiConsumer&amp;lt;R, R&amp;gt; combiner);

来说，
参数supplier 是一个生成目标类型实例的方法，代表着目标容器是什么；

accumulator是将操作的目标数据填充到supplier 生成的目标类型实例中去的方法，代表着如何将元素添加到容器中；

而combiner是将多个supplier 生成的实例整合到一起的方法，代表着规约操作，将多个结果合并。
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;@Slf4j
public class TonglianAdapterImpl implements TonglianAdapter {

    private Service service;

    private OrderService orderService;

    public TonglianAdapterImpl(AdapterProperties adapterProperties) {
        TonglianContext.init(adapterProperties);
        service = new ServiceImpl();
        orderService = new OrderServiceImpl();
    }

    @Override
    public String excute(Command cmd) {
        //去取所有方法
        Method[] methods = Service.class.getDeclaredMethods();
        for (Method me : methods) {
            //匹配传进来的方法名字
            if (me.getName().equals(cmd.getMethod())) {
                //获取方法的所有参数
                Parameter[] params = me.getParameters();
                //来说，参数supplier 是一个生成目标类型实例的方法，代表着目标容器是什么；
                // accumulator是将操作的目标数据填充到supplier 生成的目标类型实例中去的方法，代表着如何将元素添加到容器中；
                // 而combiner是将多个supplier 生成的实例整合到一起的方法，代表着规约操作，将多个结果合并。
                //将参数转为数组，并获取参数类型，参数名字，typeTransformer去转换出统一的Object参数
                ArrayList&amp;lt;Object&amp;gt; objList = Arrays.stream(params).collect
                        //声明创建ArrayList
                        (ArrayList::new,
                        //BiConsumer 接口接收两个泛型参数 a:list,b:Parameter
                        (a, b) -&amp;gt; { a.add(typeTransformer(cmd.getParams(), b.getType(), b.getName())); },
                        //合并
                        (a, b) -&amp;gt; a.addAll(b));
                try {
                    //反射调用，service在类实例化的时候已经被传入进来
                    return (String) me.invoke(service, objList.toArray(new Object[objList.size()]));
                } catch (InvocationTargetException e1){
                    Throwable targetException = e1.getTargetException();
                    if(targetException instanceof ApiException) {
                        throw (ApiException) targetException;
                    }
                    throw new RuntimeException(e1);
                } catch(IllegalAccessException e) {
                    throw new RuntimeException(e);
                }
            }
        }
        throw new RuntimeException(&quot;excute:CMD Method Error。。。。&quot;);
    }
  
  
      /**
     * 类型转换器
     * @param jo  JSONObject
     * @param type  类型
     * @param name 名字
     * @param &amp;lt;T&amp;gt;  对应类型
     * @return     对应类型
     */
    private &amp;lt;T&amp;gt; Object typeTransformer(JSONObject jo, Class&amp;lt;T&amp;gt; type, String name) {
        if (type.equals(Long.class)) {
            return jo.getLong(name);
        } else if (type.equals(PayType.class)) {
            return PayType.valueOf(jo.getString(name));
        } else if (type.equals(ValidateType.class)) {
            return ValidateType.valueOf(jo.getString(name));
        } else if (type.equals(AuthType.class)) {
            return AuthType.valueOf(jo.getString(name));
        } else if (type.equals(WechatConfig.AppType.class)) {
            return WechatConfig.AppType.valueOf(jo.getString(name));
        } else if (type.equals(JSONObject.class)) {
            return jo.getJSONObject(name);
        } else if (type.equals(JSONArray.class)) {
            return jo.getJSONArray(name);
        } else {
            return jo.get(name);
        }
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;2、整体架构图&lt;/h2&gt;
&lt;p&gt;&lt;img src=&quot;%E9%80%9A%E8%81%94%E5%8F%8D%E5%B0%84%E6%9E%B6%E6%9E%84%E5%9B%BE.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
</content:encoded></item><item><title>Java集合核心问题：哈希冲突与并发修改异常</title><link>https://tangtangit.com/posts/java%E6%A0%B8%E5%BF%83-02-%E9%9B%86%E5%90%88/</link><guid isPermaLink="true">https://tangtangit.com/posts/java%E6%A0%B8%E5%BF%83-02-%E9%9B%86%E5%90%88/</guid><description>解析HashMap哈希冲突的解决机制、equals与hashCode的契约关系，以及ConcurrentModificationException的成因与迭代器安全删除方案。</description><pubDate>Fri, 09 Jan 2026 00:00:00 GMT</pubDate><content:encoded>&lt;h1&gt;哈希冲突&lt;/h1&gt;
&lt;p&gt;在哈希表中，哈希冲突指的是不同的键（Key）在经过哈希函数计算后，得到的哈希值相同的情况。哈希冲突的解决是哈希表实现中的一个重要问题。&lt;/p&gt;
&lt;p&gt;解决哈希冲突的常用方法包括链地址法（Separate Chaining）和开放地址法（Open Addressing）等。在解决哈希冲突时，除了依赖于哈希值的计算外，还需要依赖于键的 &lt;code&gt;equals&lt;/code&gt; 方法和 &lt;code&gt;hashCode&lt;/code&gt; 方法。&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;&lt;code&gt;equals&lt;/code&gt; 方法&lt;/strong&gt;：&lt;code&gt;equals&lt;/code&gt; 方法是用来比较两个对象是否相等的方法。在哈希表中，当发生哈希冲突时，需要通过 &lt;code&gt;equals&lt;/code&gt; 方法来判断两个键是否相等。如果两个键通过 &lt;code&gt;equals&lt;/code&gt; 方法比较相等，则它们被认为是同一个键，此时可能需要进行后续的操作，比如更新值或者插入到链表中等。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;&lt;code&gt;hashCode&lt;/code&gt; 方法&lt;/strong&gt;：&lt;code&gt;hashCode&lt;/code&gt; 方法是用来计算对象的哈希值的方法。在哈希表中，哈希值被用作为键的索引，用于确定键在哈希表中的存储位置。当哈希冲突发生时，需要通过 &lt;code&gt;hashCode&lt;/code&gt; 方法来计算键的哈希值，并据此确定键的存储位置。&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;在 Java 中，当我们自定义类作为键存储在哈希表中时，通常需要重写 &lt;code&gt;equals&lt;/code&gt; 方法和 &lt;code&gt;hashCode&lt;/code&gt; 方法，以确保对象在逻辑上相等（根据业务需求自定义相等的标准），并且具有相同的哈希值。这样可以保证哈希表在使用自定义类作为键时能够正确地处理哈希冲突。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;equals相等，hashCode一定相等&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;hashCode相等，equals不一定相等&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;img src=&quot;https://tangtangit.com/_astro/put.CCDOWbOf_Z1gPWEL.webp&quot; alt=&quot;HashMap put流程&quot; /&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://tangtangit.com/_astro/resize.NLQ77ppV_1IMOpO.webp&quot; alt=&quot;HashMap resize流程&quot; /&gt;&lt;/p&gt;
&lt;h1&gt;为什么迭代器删除元素不会有并发修改异常？&lt;/h1&gt;
&lt;p&gt;迭代器删除元素时不会引发并发修改异常的原因是因为迭代器的 &lt;code&gt;remove&lt;/code&gt; 方法是在迭代器内部对集合进行修改的一种安全方式。它通过迭代器本身来进行操作，确保了在删除元素时不会导致并发修改异常。&lt;/p&gt;
&lt;p&gt;在迭代器的实现中，&lt;strong&gt;迭代器内部会维护一个称为 &quot;expectedModCount&quot; 的计数器，该计数器表示创建迭代器时集合的修改次数。当你使用迭代器的 &lt;code&gt;remove&lt;/code&gt; 方法删除元素时，它会检查 &quot;expectedModCount&quot; 是否与实际的修改次数（&quot;modCount&quot;）相匹配。如果相匹配，说明在迭代过程中没有其他线程对集合进行修改，删除操作是合法的，不会引发并发修改异常。&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;如果在迭代器使用过程中集合发生了结构性修改（例如，通过集合的其他方法进行了添加或删除操作），&quot;modCount&quot; 会增加，而 &quot;expectedModCount&quot; 保持不变。此时，当你尝试使用迭代器的 &lt;code&gt;remove&lt;/code&gt; 方法时，它会检测到这个不一致，抛出 &lt;code&gt;ConcurrentModificationException&lt;/code&gt; 异常，以提示发生了并发修改。&lt;/p&gt;
&lt;p&gt;总之，迭代器的 &lt;code&gt;remove&lt;/code&gt; 方法通过内部维护的计数器来确保在安全的情况下对集合进行修改，以防止并发修改异常。这种机制是为了在多线程环境中提供一些基本的安全性，尽管在并发编程中，还需要额外的同步手段来确保线程安全。&lt;/p&gt;
&lt;h1&gt;避免ConcurrentModificationException&lt;/h1&gt;
&lt;blockquote&gt;
&lt;p&gt;使用迭代器来遍历，并且使用迭代器来删除&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;是的，确切地说，如果在使用迭代器遍历集合的同时需要对集合进行修改，应该使用迭代器的 &lt;code&gt;remove&lt;/code&gt; 方法而不是集合的 &lt;code&gt;remove&lt;/code&gt; 方法。这是因为迭代器的 &lt;code&gt;remove&lt;/code&gt; 方法是集合修改的安全方式，它不会导致 &lt;code&gt;ConcurrentModificationException&lt;/code&gt; 异常。&lt;/p&gt;
&lt;p&gt;以下是一个示例，演示了使用迭代器遍历并删除元素的正确方式：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;import java.util.ArrayList;
import java.util.Iterator;
import java.util.List;

public class IteratorExample {
    public static void main(String[] args) {
        List&amp;lt;String&amp;gt; list = new ArrayList&amp;lt;&amp;gt;();
        list.add(&quot;One&quot;);
        list.add(&quot;Two&quot;);
        list.add(&quot;Three&quot;);

        Iterator&amp;lt;String&amp;gt; iterator = list.iterator();

        while (iterator.hasNext()) {
            String element = iterator.next();
            System.out.println(element);

            // 使用迭代器的 remove 方法安全地删除元素
            if (element.equals(&quot;Two&quot;)) {
                iterator.remove();
            }
        }

        System.out.println(&quot;List after removal: &quot; + list);
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;在这个例子中，通过使用迭代器的 &lt;code&gt;remove&lt;/code&gt; 方法，我们可以在遍历过程中安全地删除元素而不会导致并发修改异常。在集合的迭代器模式中，这是一种常见的做法，以确保在遍历过程中修改集合时不会破坏迭代器的状态。&lt;/p&gt;
&lt;h2&gt;延伸阅读&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://airms.notion.site/HashMap-3943f6867ab248e98a94802b33d4521d&quot;&gt;HashMap源码解析&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
</content:encoded></item><item><title>Java基础核心知识：OOP原则、访问修饰符、泛型与内部类</title><link>https://tangtangit.com/posts/java%E6%A0%B8%E5%BF%83-01-%E5%9F%BA%E7%A1%80/</link><guid isPermaLink="true">https://tangtangit.com/posts/java%E6%A0%B8%E5%BF%83-01-%E5%9F%BA%E7%A1%80/</guid><description>深入理解Java面向对象编程六大原则、四种访问修饰符、泛型的类/接口/方法/通配符用法，以及四种内部类的特点与示例。</description><pubDate>Tue, 06 Jan 2026 00:00:00 GMT</pubDate><content:encoded>&lt;h1&gt;面向对象编程的六大原则?&lt;/h1&gt;
&lt;p&gt;记忆：&lt;strong&gt;单开里迪，依赖接口&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;单一职责：对象设计要求独立，不能设计万能对象。&lt;/li&gt;
&lt;li&gt;开闭原则：对象修改最小化。&lt;/li&gt;
&lt;li&gt;里式替换：程序扩展中抽象被具体可以替换（接口、父类、可以被实现类对象、子类替换对象）&lt;/li&gt;
&lt;li&gt;迪米特：高内聚，低耦合。尽量不要依赖细节。&lt;/li&gt;
&lt;li&gt;依赖倒置：面向抽象编程。也就是参数传递，或者返回值，可以使用父类类型或者接口类型。从广义上讲：基于接口编程，提前设计好接口框架。&lt;/li&gt;
&lt;li&gt;接口隔离：接口设计大小要适中。过大导致污染，过小，导致调用麻烦。&lt;/li&gt;
&lt;/ul&gt;
&lt;h1&gt;数组到底是不是对象？&lt;/h1&gt;
&lt;hr /&gt;
&lt;p&gt;著作权归大彬所有 原文链接：https://www.topjavaer.cn/java/java-basic.html&lt;/p&gt;
&lt;h1&gt;访问修饰符&lt;/h1&gt;
&lt;h2&gt;Java访问控制修饰符总结&lt;/h2&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;修饰符&lt;/th&gt;
&lt;th&gt;可见性&lt;/th&gt;
&lt;th&gt;说明&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;public&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;所有类可见&lt;/td&gt;
&lt;td&gt;具有最高级别的访问权限。&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;protected&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;同一包内和继承类（不同包）可见&lt;/td&gt;
&lt;td&gt;提供比默认访问级别更高的权限，通常与继承结合使用。&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;默认（无修饰符）&lt;/td&gt;
&lt;td&gt;同一包内可见，其他包内不可见&lt;/td&gt;
&lt;td&gt;如果没有使用任何修饰符，默认为包级别的访问权限。&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;private&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;仅在声明它的类内部可见&lt;/td&gt;
&lt;td&gt;提供最高级别的封装，对于外部类完全不可见。&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;Java中有四种主要的访问控制修饰符，用于控制类的成员（字段、方法等）的访问权限。这些修饰符按照访问权限逐渐降低的顺序排列：&lt;/p&gt;
&lt;h2&gt;1. public&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;可以被任何类访问，无论是否在同一包内。&lt;/li&gt;
&lt;li&gt;具有最高的访问级别。&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;2. protected&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;可以被同一包内的类和继承类访问。&lt;/li&gt;
&lt;li&gt;具有比默认访问级别更高的访问权限，但比&lt;code&gt;public&lt;/code&gt;低。&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;3. 默认（包级别）&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;如果没有使用任何修饰符，默认是包级别的访问权限。&lt;/li&gt;
&lt;li&gt;可以被同一包内的类访问，但对于不在同一包内的类是不可见的。&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;4. private&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;只能被声明它的类内部访问。&lt;/li&gt;
&lt;li&gt;提供最高级别的封装，对于外部类完全不可见。&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;p&gt;&lt;img src=&quot;%E4%BD%9C%E7%94%A8%E5%9F%9F.jpg&quot; alt=&quot;访问修饰符作用域&quot; /&gt;&lt;/p&gt;
&lt;h2&gt;重要注意事项：&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;封装：使用&lt;code&gt;private&lt;/code&gt;和&lt;code&gt;protected&lt;/code&gt;有助于实现封装，隐藏内部实现细节。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;继承：&lt;code&gt;protected&lt;/code&gt;通常与继承结合使用，允许子类访问父类的&lt;code&gt;protected&lt;/code&gt;成员。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;面向对象原则：访问控制修饰符有助于实现面向对象编程的封装、继承和多态原则。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;安全性：选择适当的访问级别有助于确保代码的安全性和可维护性。&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;p&gt;总体而言，Java的访问控制修饰符为程序员提供了灵活性，使其能够根据设计需求控制类成员的访问权限，从而更好地组织和保护代码。&lt;/p&gt;
&lt;h1&gt;泛型使用&lt;/h1&gt;
&lt;p&gt;Java 中泛型的使用涉及到类、接口、方法等不同的场景。以下是一些泛型的常见使用场景和示例：&lt;/p&gt;
&lt;h2&gt;1. 泛型类&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;// 泛型类
class Box&amp;lt;T&amp;gt; {
    private T value;

    // 构造方法
    public Box(T value) {
        this.value = value;
    }

    // 获取值的方法
    public T getValue() {
        return value;
    }
}

// 使用泛型类
Box&amp;lt;Integer&amp;gt; integerBox = new Box&amp;lt;&amp;gt;(123);
Box&amp;lt;String&amp;gt; stringBox = new Box&amp;lt;&amp;gt;(&quot;Hello&quot;);

// 获取值
int intValue = integerBox.getValue();
String stringValue = stringBox.getValue();
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;2. 泛型接口&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;// 泛型接口
interface Pair&amp;lt;K, V&amp;gt; {
    K getKey();
    V getValue();
}

// 实现泛型接口
class OrderedPair&amp;lt;K, V&amp;gt; implements Pair&amp;lt;K, V&amp;gt; {
    private K key;
    private V value;

    public OrderedPair(K key, V value) {
        this.key = key;
        this.value = value;
    }

    @Override
    public K getKey() {
        return key;
    }

    @Override
    public V getValue() {
        return value;
    }
}

// 使用泛型接口
Pair&amp;lt;String, Integer&amp;gt; pair = new OrderedPair&amp;lt;&amp;gt;(&quot;One&quot;, 1);
System.out.println(pair.getKey() + &quot;: &quot; + pair.getValue());
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;3. 泛型方法&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;// 泛型方法
class Utils {
    //在方法定义中，使用了尖括号 &amp;lt;T&amp;gt; 来声明泛型类型 T，这表示这是一个泛型方法。 返回值是T
    public static &amp;lt;T&amp;gt; T getLastElement(T[] array) {
        return array[array.length - 1];
    }
}

// 使用泛型方法
Integer[] intArray = {1, 2, 3, 4, 5};
String[] stringArray = {&quot;A&quot;, &quot;B&quot;, &quot;C&quot;};

Integer lastInt = Utils.getLastElement(intArray);
String lastString = Utils.getLastElement(stringArray);

System.out.println(&quot;Last Integer: &quot; + lastInt);
System.out.println(&quot;Last String: &quot; + lastString);
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;4. 通配符&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;// 使用通配符
class Printer {
    public static void printList(List&amp;lt;?&amp;gt; list) {
        for (Object item : list) {
            System.out.print(item + &quot; &quot;);
        }
        System.out.println();
    }
}

// 使用通配符
List&amp;lt;Integer&amp;gt; intList = Arrays.asList(1, 2, 3, 4, 5);
List&amp;lt;String&amp;gt; stringList = Arrays.asList(&quot;A&quot;, &quot;B&quot;, &quot;C&quot;);

Printer.printList(intList);
Printer.printList(stringList);
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这些例子展示了 Java 中泛型的一些常见用法，包括泛型类、泛型接口、泛型方法以及通配符的使用。泛型使得代码更加灵活、类型安全，并提高了代码的重用性。&lt;/p&gt;
&lt;h1&gt;内部类&lt;/h1&gt;
&lt;p&gt;在 Java 中，内部类是在一个类的内部定义的类。有四种主要类型的内部类：成员内部类、局部内部类、匿名内部类和静态内部类。以下是它们的写法：&lt;/p&gt;
&lt;h2&gt;1. 成员内部类&lt;/h2&gt;
&lt;p&gt;成员内部类是定义在另一个类的内部的类。它可以访问外部类的成员，包括私有成员。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;public class OuterClass {
    private int outerVar;

    public class InnerClass {
        private int innerVar;

        public void innerMethod() {
            // 可以访问外部类的成员
            outerVar = 10;
            System.out.println(&quot;Inner Method&quot;);
        }
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;2. 局部内部类&lt;/h2&gt;
&lt;p&gt;局部内部类是定义在&lt;strong&gt;方法或作用域内&lt;/strong&gt;的类，只在定义它的方法或作用域内可见。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;public class OuterClass {
    public void outerMethod() {
        // 局部内部类
        class LocalInnerClass {
            public void localMethod() {
                System.out.println(&quot;Local Method&quot;);
            }
        }

        LocalInnerClass localInner = new LocalInnerClass();
        localInner.localMethod();
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;3. 匿名内部类&lt;/h3&gt;
&lt;p&gt;匿名内部类是&lt;strong&gt;没有类名的内部类，通常用于创建接口的实例&lt;/strong&gt;。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;public interface MyInterface {
    void myMethod();
}

public class OuterClass {
    public void useInner() {
        // 匿名内部类
        MyInterface myInterface = new MyInterface() {
            @Override
            public void myMethod() {
                System.out.println(&quot;Anonymous Inner Method&quot;);
            }
        };

        myInterface.myMethod();
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;4. 静态内部类&lt;/h3&gt;
&lt;p&gt;静态内部类是使用 &lt;code&gt;static&lt;/code&gt; 修饰的内部类，不依赖于外部类的实例。可以直接通过外部类名访问。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;public class OuterClass {
    private static int staticVar;

    public static class StaticInnerClass {
        private int innerVar;

        public void innerMethod() {
            // 可以访问外部类的静态成员
            staticVar = 20;
            System.out.println(&quot;Static Inner Method&quot;);
        }
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这是 Java 中内部类的常见写法。每种内部类都有其自身的使用场景和特点，选择合适的内部类类型取决于具体的需求。&lt;/p&gt;
</content:encoded></item></channel></rss>